summaryrefslogtreecommitdiff
path: root/src/services
diff options
context:
space:
mode:
Diffstat (limited to 'src/services')
-rw-r--r--src/services/calendar.ts168
-rw-r--r--src/services/monitors.ts2
-rw-r--r--src/services/schemes.ts3
-rw-r--r--src/services/wallpapers.ts3
4 files changed, 172 insertions, 4 deletions
diff --git a/src/services/calendar.ts b/src/services/calendar.ts
new file mode 100644
index 0000000..9743aad
--- /dev/null
+++ b/src/services/calendar.ts
@@ -0,0 +1,168 @@
+import { notify } from "@/utils/system";
+import { execAsync, GLib, GObject, property, register, timeout, type AstalIO } from "astal";
+import { calendar as config } from "config";
+import ical from "ical.js";
+
+export interface IEvent {
+ calendar: string;
+ event: ical.Event;
+}
+
+@register({ GTypeName: "Calendar" })
+export default class Calendar extends GObject.Object {
+ static instance: Calendar;
+ static get_default() {
+ if (!this.instance) this.instance = new Calendar();
+
+ return this.instance;
+ }
+
+ #calCount: number = 1;
+ #reminders: AstalIO.Time[] = [];
+ #loading: boolean = false;
+ #calendars: { [name: string]: ical.Component } = {};
+ #upcoming: { [date: string]: IEvent[] } = {};
+
+ @property(Boolean)
+ get loading() {
+ return this.#loading;
+ }
+
+ @property(Object)
+ get calendars() {
+ return this.#calendars;
+ }
+
+ @property(Object)
+ get upcoming() {
+ return this.#upcoming;
+ }
+
+ @property(Number)
+ get numUpcoming() {
+ return Object.values(this.#upcoming).reduce((acc, e) => acc + e.length, 0);
+ }
+
+ getCalendarIndex(name: string) {
+ return Object.keys(this.#calendars).indexOf(name) + 1;
+ }
+
+ async updateCalendars() {
+ this.#loading = true;
+ this.notify("loading");
+
+ this.#calendars = {};
+ this.#calCount = 1;
+
+ const cals = await Promise.allSettled(config.webcals.get().map(c => execAsync(["curl", c])));
+ for (const cal of cals) {
+ if (cal.status === "fulfilled") {
+ const comp = new ical.Component(ical.parse(cal.value));
+ const name = (comp.getFirstPropertyValue("x-wr-calname") ?? `Calendar ${this.#calCount++}`) as string;
+ this.#calendars[name] = comp;
+ } else console.error(`Failed to get calendar: ${cal.reason}`);
+ }
+ this.notify("calendars");
+
+ this.updateUpcoming();
+
+ this.#loading = false;
+ this.notify("loading");
+ }
+
+ updateUpcoming() {
+ this.#upcoming = {};
+
+ const today = ical.Time.now();
+ const upcoming = ical.Time.now().adjust(config.upcomingDays.get(), 0, 0, 0);
+ for (const [name, cal] of Object.entries(this.#calendars)) {
+ for (const e of cal.getAllSubcomponents()) {
+ const event = new ical.Event(e);
+
+ // Skip invalid events
+ if (!event.startDate) continue;
+
+ if (event.isRecurring()) {
+ // Recurring events
+ const iter = event.iterator();
+ for (let next = iter.next(); next && next.compare(upcoming) <= 0; next = iter.next())
+ if (next.compare(today) >= 0) {
+ const date = next.toJSDate().toDateString();
+ if (!this.#upcoming.hasOwnProperty(date)) this.#upcoming[date] = [];
+
+ const rEvent = new ical.Event(e);
+ rEvent.startDate = next;
+ this.#upcoming[date].push({ calendar: name, event: rEvent });
+ }
+ } else if (event.startDate.compare(today) >= 0 && event.startDate.compare(upcoming) <= 0) {
+ // Add to upcoming if in upcoming range
+ const date = event.startDate.toJSDate().toDateString();
+ if (!this.#upcoming.hasOwnProperty(date)) this.#upcoming[date] = [];
+ this.#upcoming[date].push({ calendar: name, event });
+ }
+ }
+ }
+
+ for (const events of Object.values(this.#upcoming))
+ events.sort((a, b) => a.event.startDate.compare(b.event.startDate));
+
+ this.notify("upcoming");
+ this.notify("num-upcoming");
+
+ this.setReminders();
+ }
+
+ #notifyEvent(event: ical.Event, calendar: string) {
+ const start = GLib.DateTime.new_from_unix_local(event.startDate.toUnixTime());
+ const end = GLib.DateTime.new_from_unix_local(event.endDate.toUnixTime());
+ const time = `${start.format(`%A, %-d %B`)} • Now — ${end.format("%-I:%M%P")}`;
+ const locIfExists = event.location ? ` ${event.location}\n` : "";
+ const descIfExists = event.description ? `󰒿 ${event.description}\n` : "";
+
+ notify({
+ summary: `󰨱 ${event.summary} 󰨱`,
+ body: `${time}\n${locIfExists}${descIfExists}󰃭 ${calendar}`,
+ }).catch(console.error);
+ }
+
+ #createReminder(event: ical.Event, calendar: string, next: ical.Time) {
+ const diff = next.toUnixTime() - ical.Time.now().toUnixTime();
+ if (diff > 0) this.#reminders.push(timeout(diff * 1000, () => this.#notifyEvent(event, calendar)));
+ }
+
+ setReminders() {
+ this.#reminders.forEach(r => r.cancel());
+ this.#reminders = [];
+
+ if (!config.notify.get()) return;
+
+ const today = ical.Time.now();
+ const upcoming = ical.Time.now().adjust(config.upcomingDays.get(), 0, 0, 0);
+ for (const [name, cal] of Object.entries(this.#calendars)) {
+ for (const e of cal.getAllSubcomponents()) {
+ const event = new ical.Event(e);
+
+ // Skip invalid events
+ if (!event.startDate) continue;
+
+ if (event.isRecurring()) {
+ // Recurring events
+ const iter = event.iterator();
+ for (let next = iter.next(); next && next.compare(upcoming) <= 0; next = iter.next())
+ if (next.compare(today) >= 0) this.#createReminder(event, name, next);
+ } else if (event.startDate.compare(today) >= 0 && event.startDate.compare(upcoming) <= 0)
+ // Create reminder if in upcoming range
+ this.#createReminder(event, name, event.startDate);
+ }
+ }
+ }
+
+ constructor() {
+ super();
+
+ this.updateCalendars().catch(console.error);
+ config.webcals.subscribe(() => this.updateCalendars().catch(console.error));
+ config.upcomingDays.subscribe(() => this.updateUpcoming());
+ config.notify.subscribe(() => this.setReminders());
+ }
+}
diff --git a/src/services/monitors.ts b/src/services/monitors.ts
index 4cef256..6ae7ecb 100644
--- a/src/services/monitors.ts
+++ b/src/services/monitors.ts
@@ -55,7 +55,7 @@ export class Monitor extends GObject.Object {
.then(out => {
this.isDdc = out.split("\n\n").some(display => {
if (!/^Display \d+/.test(display)) return false;
- const lines = display.split("\n");
+ const lines = display.split("\n").map(l => l.trimStart());
if (lines.find(l => l.startsWith("Monitor:"))?.split(":")[3] !== monitor.serial) return false;
this.busNum = lines.find(l => l.startsWith("I2C bus:"))?.split("/dev/i2c-")[1];
return this.busNum !== undefined;
diff --git a/src/services/schemes.ts b/src/services/schemes.ts
index 548975c..2808b55 100644
--- a/src/services/schemes.ts
+++ b/src/services/schemes.ts
@@ -32,7 +32,6 @@ export default class Schemes extends GObject.Object {
}
readonly #schemeDir: string = `${DATA}/scripts/data/schemes`;
- readonly #monitor;
#map: { [k: string]: Scheme } = {};
@@ -106,7 +105,7 @@ export default class Schemes extends GObject.Object {
super();
this.update().catch(console.error);
- this.#monitor = monitorDirectory(this.#schemeDir, (_m, file, _f, type) => {
+ monitorDirectory(this.#schemeDir, (_m, file, _f, type) => {
if (type !== Gio.FileMonitorEvent.DELETED) this.updateFile(file).catch(console.error);
});
}
diff --git a/src/services/wallpapers.ts b/src/services/wallpapers.ts
index 0e0e1de..4c7c49b 100644
--- a/src/services/wallpapers.ts
+++ b/src/services/wallpapers.ts
@@ -40,7 +40,8 @@ export default class Wallpapers extends GObject.Object {
async #thumbnail(path: string) {
const dir = path.slice(1, path.lastIndexOf("/")).replaceAll("/", "-");
const thumbPath = `${this.#thumbnailDir}/${dir}-${basename(path)}.jpg`;
- await execAsync(`magick -define jpeg:size=1000x500 ${path} -thumbnail 500x250 -unsharp 0x.5 ${thumbPath}`);
+ if (!GLib.file_test(thumbPath, GLib.FileTest.EXISTS))
+ await execAsync(`magick -define jpeg:size=1000x500 ${path} -thumbnail 500x250 -unsharp 0x.5 ${thumbPath}`);
return thumbPath;
}