diff options
Diffstat (limited to 'src/services')
| -rw-r--r-- | src/services/apps.ts | 3 | ||||
| -rw-r--r-- | src/services/calendar.ts | 228 | ||||
| -rw-r--r-- | src/services/cpu.ts | 49 | ||||
| -rw-r--r-- | src/services/gpu.ts | 63 | ||||
| -rw-r--r-- | src/services/math.ts | 155 | ||||
| -rw-r--r-- | src/services/memory.ts | 64 | ||||
| -rw-r--r-- | src/services/monitors.ts | 127 | ||||
| -rw-r--r-- | src/services/news.ts | 153 | ||||
| -rw-r--r-- | src/services/palette.ts | 298 | ||||
| -rw-r--r-- | src/services/players.ts | 148 | ||||
| -rw-r--r-- | src/services/schemes.ts | 109 | ||||
| -rw-r--r-- | src/services/storage.ts | 65 | ||||
| -rw-r--r-- | src/services/updates.ts | 191 | ||||
| -rw-r--r-- | src/services/wallpapers.ts | 127 | ||||
| -rw-r--r-- | src/services/weather.ts | 388 |
15 files changed, 0 insertions, 2168 deletions
diff --git a/src/services/apps.ts b/src/services/apps.ts deleted file mode 100644 index 5396ac7..0000000 --- a/src/services/apps.ts +++ /dev/null @@ -1,3 +0,0 @@ -import AstalApps from "gi://AstalApps"; - -export const Apps = new AstalApps.Apps(); diff --git a/src/services/calendar.ts b/src/services/calendar.ts deleted file mode 100644 index d5e0329..0000000 --- a/src/services/calendar.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { pathToFileName } from "@/utils/strings"; -import { notify } from "@/utils/system"; -import { - execAsync, - GLib, - GObject, - property, - readFileAsync, - register, - timeout, - writeFileAsync, - type AstalIO, -} from "astal"; -import { calendar as config } from "config"; -import ical from "ical.js"; - -export interface IEvent { - calendar: string; - event: ical.Event; - startDate: ical.Time; - endDate: ical.Time; -} - -@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; - } - - readonly #cacheDir = `${CACHE}/calendars`; - - #calCount: number = 1; - #reminders: AstalIO.Time[] = []; - #loading: boolean = false; - #calendars: { [name: string]: ical.Component } = {}; - #upcoming: { [date: string]: IEvent[] } = {}; - #cachedEvents: { [date: string]: IEvent[] } = {}; - #cachedMonths: Set<string> = new Set(); - - @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; - } - - getEventsForMonth(date: ical.Time) { - const start = date.startOfMonth(); - - if (this.#cachedMonths.has(start.toJSDate().toDateString())) return this.#cachedEvents; - - this.#cachedMonths.add(start.toJSDate().toDateString()); - const end = date.endOfMonth(); - - const modDates = new Set<string>(); - - 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(end) <= 0; next = iter.next()) - if (next.compare(start) >= 0) { - const date = next.toJSDate().toDateString(); - if (!this.#cachedEvents.hasOwnProperty(date)) this.#cachedEvents[date] = []; - - const end = next.clone(); - end.addDuration(event.duration); - this.#cachedEvents[date].push({ calendar: name, event, startDate: next, endDate: end }); - modDates.add(date); - } - } else if (event.startDate.compare(start) >= 0 && event.startDate.compare(end) <= 0) { - const date = event.startDate.toJSDate().toDateString(); - if (!this.#cachedEvents.hasOwnProperty(date)) this.#cachedEvents[date] = []; - this.#cachedEvents[date].push({ - calendar: name, - event, - startDate: event.startDate, - endDate: event.endDate, - }); - modDates.add(date); - } - } - } - - for (const date of modDates) this.#cachedEvents[date].sort((a, b) => a.startDate.compare(b.startDate)); - - return this.#cachedEvents; - } - - getEventsForDay(date: ical.Time) { - return this.getEventsForMonth(date)[date.toJSDate().toDateString()] ?? []; - } - - 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 (let i = 0; i < cals.length; i++) { - const cal = cals[i]; - const webcal = pathToFileName(config.webcals.get()[i]); - - let icalStr; - if (cal.status === "fulfilled") { - icalStr = cal.value; - } else { - console.error(`Failed to get calendar from ${config.webcals.get()[i]}:\n${cal.reason}`); - if (GLib.file_test(`${this.#cacheDir}/${webcal}`, GLib.FileTest.EXISTS)) - icalStr = await readFileAsync(`${this.#cacheDir}/${webcal}`); - } - - if (icalStr) { - const comp = new ical.Component(ical.parse(icalStr)); - const name = (comp.getFirstPropertyValue("x-wr-calname") ?? `Calendar ${this.#calCount++}`) as string; - this.#calendars[name] = comp; - writeFileAsync(`${this.#cacheDir}/${webcal}`, icalStr).catch(console.error); - } - } - this.#cachedEvents = {}; - this.#cachedMonths.clear(); - - this.notify("calendars"); - - this.updateUpcoming(); - - this.#loading = false; - this.notify("loading"); - } - - updateUpcoming() { - this.#upcoming = {}; - - for (let i = 0; i < config.upcomingDays.get(); i++) { - const date = ical.Time.now().adjust(i, 0, 0, 0); - const events = this.getEventsForDay(date); - if (events.length > 0) this.#upcoming[date.toJSDate().toDateString()] = events; - } - - this.notify("upcoming"); - this.notify("num-upcoming"); - - this.setReminders(); - } - - #notifyEvent(event: IEvent) { - 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.event.location ? ` ${event.event.location}\n` : ""; - const descIfExists = event.event.description ? ` ${event.event.description}\n` : ""; - - notify({ - summary: ` ${event.event.summary} `, - body: `${time}\n${locIfExists}${descIfExists} ${event.calendar}`, - }).catch(console.error); - } - - #createReminder(event: IEvent) { - const diff = event.startDate.toJSDate().getTime() - ical.Time.now().toJSDate().getTime(); - if (diff > 0) this.#reminders.push(timeout(diff, () => this.#notifyEvent(event))); - } - - setReminders() { - this.#reminders.forEach(r => r.cancel()); - this.#reminders = []; - - if (!config.notify.get()) return; - - for (const events of Object.values(this.#upcoming)) for (const event of events) this.#createReminder(event); - } - - constructor() { - super(); - - GLib.mkdir_with_parents(this.#cacheDir, 0o755); - - const cals = config.webcals.get().map(async c => { - const webcal = pathToFileName(c); - - if (GLib.file_test(`${this.#cacheDir}/${webcal}`, GLib.FileTest.EXISTS)) { - const data = await readFileAsync(`${this.#cacheDir}/${webcal}`); - const comp = new ical.Component(ical.parse(data)); - const name = (comp.getFirstPropertyValue("x-wr-calname") ?? `Calendar ${this.#calCount++}`) as string; - this.#calendars[name] = comp; - } - }); - Promise.allSettled(cals).then(() => { - this.#cachedEvents = {}; - this.#cachedMonths.clear(); - this.notify("calendars"); - this.updateUpcoming(); - }); - - 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/cpu.ts b/src/services/cpu.ts deleted file mode 100644 index 5f80d11..0000000 --- a/src/services/cpu.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { GObject, interval, property, register } from "astal"; -import { cpu as config } from "config"; -import GTop from "gi://GTop"; - -@register({ GTypeName: "Cpu" }) -export default class Cpu extends GObject.Object { - static instance: Cpu; - static get_default() { - if (!this.instance) this.instance = new Cpu(); - - return this.instance; - } - - #previous: GTop.glibtop_cpu = new GTop.glibtop_cpu(); - #usage: number = 0; - - @property(Number) - get usage() { - return this.#usage; - } - - calculateUsage() { - const current = new GTop.glibtop_cpu(); - GTop.glibtop_get_cpu(current); - - // Calculate the differences from the previous to current data - const total = current.total - this.#previous.total; - const idle = current.idle - this.#previous.idle; - - this.#previous = current; - - return total > 0 ? ((total - idle) / total) * 100 : 0; - } - - update() { - this.#usage = this.calculateUsage(); - this.notify("usage"); - } - - constructor() { - super(); - - let source = interval(config.interval.get(), () => this.update()); - config.interval.subscribe(i => { - source.cancel(); - source = interval(i, () => this.update()); - }); - } -} diff --git a/src/services/gpu.ts b/src/services/gpu.ts deleted file mode 100644 index 5ac2d8d..0000000 --- a/src/services/gpu.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { execAsync, Gio, GLib, GObject, interval, property, register } from "astal"; -import { gpu as config } from "config"; - -@register({ GTypeName: "Gpu" }) -export default class Gpu extends GObject.Object { - static instance: Gpu; - static get_default() { - if (!this.instance) this.instance = new Gpu(); - - return this.instance; - } - - readonly available: boolean = false; - #usage: number = 0; - - @property(Number) - get usage() { - return this.#usage; - } - - async calculateUsage() { - const percs = (await execAsync("fish -c 'cat /sys/class/drm/card*/device/gpu_busy_percent'")).split("\n"); - return percs.reduce((a, b) => a + parseFloat(b), 0) / percs.length; - } - - update() { - this.calculateUsage() - .then(usage => { - this.#usage = usage; - this.notify("usage"); - }) - .catch(console.error); - } - - constructor() { - super(); - - let enumerator = null; - try { - enumerator = Gio.File.new_for_path("/sys/class/drm").enumerate_children( - Gio.FILE_ATTRIBUTE_STANDARD_NAME, - Gio.FileQueryInfoFlags.NONE, - null - ); - } catch {} - - let info: Gio.FileInfo | undefined | null; - while ((info = enumerator?.next_file(null))) { - if (GLib.file_test(`/sys/class/drm/${info.get_name()}/device/gpu_busy_percent`, GLib.FileTest.EXISTS)) { - this.available = true; - break; - } - } - - if (this.available) { - let source = interval(config.interval.get(), () => this.update()); - config.interval.subscribe(i => { - source.cancel(); - source = interval(i, () => this.update()); - }); - } - } -} diff --git a/src/services/math.ts b/src/services/math.ts deleted file mode 100644 index 0cddf1b..0000000 --- a/src/services/math.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { GLib, GObject, property, readFile, register, writeFileAsync } from "astal"; -import { math as config } from "config"; -import { derivative, evaluate, rationalize, simplify } from "mathjs/number"; - -export interface HistoryItem { - equation: string; - result: string; - icon: string; -} - -@register({ GTypeName: "Math" }) -export default class Math extends GObject.Object { - static instance: Math; - static get_default() { - if (!this.instance) this.instance = new Math(); - - return this.instance; - } - - readonly #path = `${STATE}/math-history.json`; - readonly #history: HistoryItem[] = []; - - #variables: Record<string, string> = {}; - #lastExpression: HistoryItem | null = null; - - @property(Object) - get history() { - return this.#history; - } - - #save() { - writeFileAsync(this.#path, JSON.stringify(this.#history)).catch(console.error); - } - - /** - * Commits the last evaluated expression to the history - */ - commit() { - if (!this.#lastExpression) return; - - // Try select first to prevent duplicates, if it fails, add it - if (!this.select(this.#lastExpression)) { - this.#history.unshift(this.#lastExpression); - while (this.#history.length > config.maxHistory.get()) this.#history.pop(); - this.notify("history"); - this.#save(); - } - this.#lastExpression = null; - } - - /** - * Moves an item in the history to the top - * @param item The item to select - * @returns If the item was successfully selected - */ - select(item: HistoryItem) { - const idx = this.#history.findIndex(i => i.equation === item.equation && i.result === item.result); - if (idx >= 0) { - this.#history.splice(idx, 1); - this.#history.unshift(item); - this.notify("history"); - this.#save(); - - return true; - } - - return false; - } - - /** - * Clears the history and variables - */ - clear() { - if (this.#history.length > 0) { - this.#history.length = 0; - this.notify("history"); - this.#save(); - } - this.#lastExpression = null; - this.#variables = {}; - } - - /** - * Evaluates an equation and adds it to the history - * @param equation The equation to evaluate - * @returns A {@link HistoryItem} representing the result of the equation - */ - evaluate(equation: string): HistoryItem { - if (equation.startsWith("clear")) - return { - equation: "Clear history", - result: "Delete history and previously set variables", - icon: "delete_forever", - }; - - let result: string, icon: string; - try { - if (equation.startsWith("help")) { - equation = "Help"; - result = - "This is a calculator powered by Math.js.\nAvailable functions:\n\thelp: show help\n\tclear: clear history\n\t<x> = <equation>: sets <x> to <equation>\n\tsimplify <equation>: simplifies <equation>\n\tderive <x> <equation>: derives <equation> with respect to <x>\n\tdd<x> <equation>: short form of derive\n\trationalize <equation>: rationalizes <equation>\n\t<equation>: evaluates <equation>\nSee the documentation for syntax and inbuilt functions."; - icon = "help"; - } else if (equation.includes("=")) { - const [left, right] = equation.split("="); - try { - this.#variables[left.trim()] = simplify(right, this.#variables).toString(); - } catch { - this.#variables[left.trim()] = right.trim(); - } - result = this.#variables[left.trim()]; - icon = "equal"; - } else if (equation.startsWith("simplify")) { - result = simplify(equation.slice(8), this.#variables).toString(); - icon = "function"; - } else if (equation.startsWith("derive") || equation.startsWith("dd")) { - const isShortForm = equation.startsWith("dd"); - const respectTo = isShortForm ? equation.split(" ")[0].slice(2) : equation.split(" ")[1]; - if (!respectTo) throw new Error(`Format: ${isShortForm ? "dd" : "derive "}<respect-to> <equation>`); - result = derivative(equation.slice((isShortForm ? 2 : 7) + respectTo.length), respectTo).toString(); - icon = "function"; - } else if (equation.startsWith("rationalize")) { - result = rationalize(equation.slice(11), this.#variables).toString(); - icon = "function"; - } else { - result = evaluate(equation, this.#variables).toString(); - icon = "calculate"; - } - } catch (e) { - equation = "Invalid equation: " + equation; - result = String(e); - icon = "error"; - } - - return (this.#lastExpression = { equation, result, icon }); - } - - constructor() { - super(); - - // Load history - if (GLib.file_test(this.#path, GLib.FileTest.EXISTS)) { - try { - this.#history = JSON.parse(readFile(this.#path)); - // Init eval to create variables and last expression - for (const item of this.#history) this.evaluate(item.equation); - } catch (e) { - console.error("Math - Unable to load history", e); - } - } - - config.maxHistory.subscribe(n => { - while (this.#history.length > n) this.#history.pop(); - }); - } -} diff --git a/src/services/memory.ts b/src/services/memory.ts deleted file mode 100644 index b1231b9..0000000 --- a/src/services/memory.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { GObject, interval, property, readFileAsync, register } from "astal"; -import { memory as config } from "config"; - -@register({ GTypeName: "Memory" }) -export default class Memory extends GObject.Object { - static instance: Memory; - static get_default() { - if (!this.instance) this.instance = new Memory(); - - return this.instance; - } - - #total: number = 0; - #free: number = 0; - #used: number = 0; - #usage: number = 0; - - @property(Number) - get total() { - return this.#total; - } - - @property(Number) - get free() { - return this.#free; - } - - @property(Number) - get used() { - return this.#used; - } - - @property(Number) - get usage() { - return this.#usage; - } - - async update() { - const info = await readFileAsync("/proc/meminfo"); - this.#total = parseInt(info.match(/MemTotal:\s+(\d+)/)?.[1] ?? "0", 10) * 1024; - this.#free = parseInt(info.match(/MemAvailable:\s+(\d+)/)?.[1] ?? "0", 10) * 1024; - - if (isNaN(this.#total)) this.#total = 0; - if (isNaN(this.#free)) this.#free = 0; - - this.#used = this.#total - this.#free; - this.#usage = this.#total > 0 ? (this.#used / this.#total) * 100 : 0; - - this.notify("total"); - this.notify("free"); - this.notify("used"); - this.notify("usage"); - } - - constructor() { - super(); - - let source = interval(config.interval.get(), () => this.update().catch(console.error)); - config.interval.subscribe(i => { - source.cancel(); - source = interval(i, () => this.update().catch(console.error)); - }); - } -} diff --git a/src/services/monitors.ts b/src/services/monitors.ts deleted file mode 100644 index 6ae7ecb..0000000 --- a/src/services/monitors.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { GObject, execAsync, property, register } from "astal"; -import AstalHyprland from "gi://AstalHyprland"; - -@register({ GTypeName: "Monitor" }) -export class Monitor extends GObject.Object { - readonly monitor: AstalHyprland.Monitor; - readonly width: number; - readonly height: number; - readonly id: number; - readonly serial: string; - readonly name: string; - readonly description: string; - - @property(AstalHyprland.Workspace) - get activeWorkspace() { - return this.monitor.activeWorkspace; - } - - isDdc: boolean = false; - busNum?: string; - - #brightness: number = 0; - - @property(Number) - get brightness() { - return this.#brightness; - } - - set brightness(value) { - value = Math.min(1, Math.max(0, value)); - - this.#brightness = value; - this.notify("brightness"); - execAsync( - this.isDdc - ? `ddcutil -b ${this.busNum} setvcp 10 ${Math.round(value * 100)}` - : `brightnessctl set ${Math.floor(value * 100)}% -q` - ).catch(console.error); - } - - constructor(monitor: AstalHyprland.Monitor) { - super(); - - this.monitor = monitor; - this.width = monitor.width; - this.height = monitor.height; - this.id = monitor.id; - this.serial = monitor.serial; - this.name = monitor.name; - this.description = monitor.description; - - monitor.connect("notify::active-workspace", () => this.notify("active-workspace")); - - execAsync("ddcutil detect --brief") - .then(out => { - this.isDdc = out.split("\n\n").some(display => { - if (!/^Display \d+/.test(display)) return false; - 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; - }); - }) - .catch(() => (this.isDdc = false)) - .finally(async () => { - if (this.isDdc) { - const info = (await execAsync(`ddcutil -b ${this.busNum} getvcp 10 --brief`)).split(" "); - this.#brightness = Number(info[3]) / Number(info[4]); - } else - this.#brightness = - Number(await execAsync("brightnessctl get")) / Number(await execAsync("brightnessctl max")); - }); - } -} - -@register({ GTypeName: "Monitors" }) -export default class Monitors extends GObject.Object { - static instance: Monitors; - static get_default() { - if (!this.instance) this.instance = new Monitors(); - - return this.instance; - } - - readonly #map: Map<number, Monitor> = new Map(); - - @property(Object) - get map() { - return this.#map; - } - - @property(Object) - get list() { - return Array.from(this.#map.values()); - } - - @property(Monitor) - get active() { - return this.#map.get(AstalHyprland.get_default().focusedMonitor.id)!; - } - - #notify() { - this.notify("map"); - this.notify("list"); - } - - forEach(fn: (monitor: Monitor) => void) { - for (const monitor of this.#map.values()) fn(monitor); - } - - constructor() { - super(); - - const hyprland = AstalHyprland.get_default(); - - for (const monitor of hyprland.monitors) this.#map.set(monitor.id, new Monitor(monitor)); - if (this.#map.size > 0) this.#notify(); - - hyprland.connect("monitor-added", (_, monitor) => { - this.#map.set(monitor.id, new Monitor(monitor)); - this.#notify(); - }); - hyprland.connect("monitor-removed", (_, id) => this.#map.delete(id) && this.#notify()); - - hyprland.connect("notify::focused-monitor", () => this.notify("active")); - } -} diff --git a/src/services/news.ts b/src/services/news.ts deleted file mode 100644 index 14c980c..0000000 --- a/src/services/news.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { notify } from "@/utils/system"; -import { execAsync, GLib, GObject, property, readFileAsync, register, writeFileAsync } from "astal"; -import { news as config } from "config"; -import { setConfig } from "config/funcs"; - -export interface IArticle { - title: string; - link: string; - keywords: string[] | null; - creator: string[] | null; - description: string | null; - pubDate: string; - source_name: string; - source_priority: number; - category: string[]; -} - -@register({ GTypeName: "News" }) -export default class News extends GObject.Object { - static instance: News; - static get_default() { - if (!this.instance) this.instance = new News(); - - return this.instance; - } - - readonly #cachePath = `${CACHE}/news.json`; - #notified = false; - - #loading: boolean = false; - #articles: IArticle[] = []; - #categories: { [category: string]: IArticle[] } = {}; - - @property(Boolean) - get loading() { - return this.#loading; - } - - @property(Object) - get articles() { - return this.#articles; - } - - @property(Object) - get categories() { - return this.#categories; - } - - async getNews() { - if (!config.apiKey.get()) { - if (!this.#notified) { - notify({ - summary: "A newsdata.io API key is required", - body: "You can get one by creating an account at https://newsdata.io", - icon: "dialog-warning-symbolic", - urgency: "critical", - actions: { - "Get API key": () => execAsync("app2unit -O -- https://newsdata.io").catch(console.error), - Disable: () => setConfig("sidebar.modules.headlines.enabled", false), - }, - }); - this.#notified = true; - } - return; - } - - this.#loading = true; - this.notify("loading"); - - let countries = config.countries.get().join(","); - const categories = config.categories.get().join(","); - const languages = config.languages.get().join(","); - const domains = config.domains.get().join(","); - const excludeDomains = config.excludeDomains.get().join(","); - const timezone = config.timezone.get(); - - if (countries.includes("current")) { - const out = JSON.parse(await execAsync("curl ipinfo.io")).country.toLowerCase(); - countries = countries.replace("current", out); - } - - let args = "removeduplicate=1&prioritydomain=top"; - if (countries) args += `&country=${countries}`; - if (categories) args += `&category=${categories}`; - if (languages) args += `&language=${languages}`; - if (domains) args += `&domain=${domains}`; - if (excludeDomains) args += `&excludedomain=${excludeDomains}`; - if (timezone) args += `&timezone=${timezone}`; - - const url = `https://newsdata.io/api/1/latest?apikey=${config.apiKey.get()}&${args}`; - try { - const res = JSON.parse(await execAsync(["curl", url])); - if (res.status !== "success") throw new Error(`Failed to get news: ${res.results.message}`); - - this.#articles = [...res.results]; - - let page = res.nextPage; - for (let i = 1; i < config.pages.get(); i++) { - const res = JSON.parse(await execAsync(["curl", `${url}&page=${page}`])); - if (res.status !== "success") throw new Error(`Failed to get news: ${res.results.message}`); - this.#articles.push(...res.results); - page = res.nextPage; - } - - writeFileAsync(this.#cachePath, JSON.stringify(this.#articles)).catch(console.error); - } catch (e) { - console.error(e); - - if (GLib.file_test(this.#cachePath, GLib.FileTest.EXISTS)) - this.#articles = JSON.parse(await readFileAsync(this.#cachePath)); - } - this.notify("articles"); - - this.updateCategories(); - - this.#loading = false; - this.notify("loading"); - } - - updateCategories() { - this.#categories = {}; - for (const article of this.#articles) { - for (const category of article.category) { - if (!this.#categories.hasOwnProperty(category)) this.#categories[category] = []; - this.#categories[category].push(article); - } - } - this.notify("categories"); - } - - constructor() { - super(); - - if (GLib.file_test(this.#cachePath, GLib.FileTest.EXISTS)) - readFileAsync(this.#cachePath) - .then(data => { - this.#articles = JSON.parse(data); - this.notify("articles"); - this.updateCategories(); - }) - .catch(console.error); - - this.getNews().catch(console.error); - config.apiKey.subscribe(() => this.getNews().catch(console.error)); - config.countries.subscribe(() => this.getNews().catch(console.error)); - config.categories.subscribe(() => this.getNews().catch(console.error)); - config.languages.subscribe(() => this.getNews().catch(console.error)); - config.domains.subscribe(() => this.getNews().catch(console.error)); - config.excludeDomains.subscribe(() => this.getNews().catch(console.error)); - config.timezone.subscribe(() => this.getNews().catch(console.error)); - config.pages.subscribe(() => this.getNews().catch(console.error)); - } -} diff --git a/src/services/palette.ts b/src/services/palette.ts deleted file mode 100644 index 952543f..0000000 --- a/src/services/palette.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { execAsync, GLib, GObject, monitorFile, property, readFile, readFileAsync, register } from "astal"; -import Schemes from "./schemes"; - -export type ColourMode = "light" | "dark"; - -export type Hex = `#${string}`; - -export interface IPalette { - rosewater: Hex; - flamingo: Hex; - pink: Hex; - mauve: Hex; - red: Hex; - maroon: Hex; - peach: Hex; - yellow: Hex; - green: Hex; - teal: Hex; - sky: Hex; - sapphire: Hex; - blue: Hex; - lavender: Hex; - text: Hex; - subtext1: Hex; - subtext0: Hex; - overlay2: Hex; - overlay1: Hex; - overlay0: Hex; - surface2: Hex; - surface1: Hex; - surface0: Hex; - base: Hex; - mantle: Hex; - crust: Hex; - primary: Hex; - secondary: Hex; - tertiary: Hex; -} - -@register({ GTypeName: "Palette" }) -export default class Palette extends GObject.Object { - static instance: Palette; - static get_default() { - if (!this.instance) this.instance = new Palette(); - - return this.instance; - } - - #mode: ColourMode; - #scheme: string; - #flavour?: string; - #colours!: IPalette; - - @property(Boolean) - get mode() { - return this.#mode; - } - - @property(String) - get scheme() { - return this.#scheme; - } - - @property(String) - get flavour() { - return this.#flavour; - } - - @property(Object) - get colours() { - return this.#colours; - } - - @property(String) - get rosewater() { - return this.#colours.rosewater; - } - - @property(String) - get flamingo() { - return this.#colours.flamingo; - } - - @property(String) - get pink() { - return this.#colours.pink; - } - - @property(String) - get mauve() { - return this.#colours.mauve; - } - - @property(String) - get red() { - return this.#colours.red; - } - - @property(String) - get maroon() { - return this.#colours.maroon; - } - - @property(String) - get peach() { - return this.#colours.peach; - } - - @property(String) - get yellow() { - return this.#colours.yellow; - } - - @property(String) - get green() { - return this.#colours.green; - } - - @property(String) - get teal() { - return this.#colours.teal; - } - - @property(String) - get sky() { - return this.#colours.sky; - } - - @property(String) - get sapphire() { - return this.#colours.sapphire; - } - - @property(String) - get blue() { - return this.#colours.blue; - } - - @property(String) - get lavender() { - return this.#colours.lavender; - } - - @property(String) - get text() { - return this.#colours.text; - } - - @property(String) - get subtext1() { - return this.#colours.subtext1; - } - - @property(String) - get subtext0() { - return this.#colours.subtext0; - } - - @property(String) - get overlay2() { - return this.#colours.overlay2; - } - - @property(String) - get overlay1() { - return this.#colours.overlay1; - } - - @property(String) - get overlay0() { - return this.#colours.overlay0; - } - - @property(String) - get surface2() { - return this.#colours.surface2; - } - - @property(String) - get surface1() { - return this.#colours.surface1; - } - - @property(String) - get surface0() { - return this.#colours.surface0; - } - - @property(String) - get base() { - return this.#colours.base; - } - - @property(String) - get mantle() { - return this.#colours.mantle; - } - - @property(String) - get crust() { - return this.#colours.crust; - } - - @property(String) - get primary() { - return this.#colours.primary; - } - - @property(String) - get secondary() { - return this.#colours.secondary; - } - - @property(String) - get tertiary() { - return this.#colours.tertiary; - } - - #notify() { - this.notify("colours"); - this.notify("rosewater"); - this.notify("flamingo"); - this.notify("pink"); - this.notify("mauve"); - this.notify("red"); - this.notify("maroon"); - this.notify("peach"); - this.notify("yellow"); - this.notify("green"); - this.notify("teal"); - this.notify("sky"); - this.notify("sapphire"); - this.notify("blue"); - this.notify("lavender"); - this.notify("text"); - this.notify("subtext1"); - this.notify("subtext0"); - this.notify("overlay2"); - this.notify("overlay1"); - this.notify("overlay0"); - this.notify("surface2"); - this.notify("surface1"); - this.notify("surface0"); - this.notify("base"); - this.notify("mantle"); - this.notify("crust"); - this.notify("primary"); - this.notify("secondary"); - this.notify("tertiary"); - } - - update() { - let schemeColours; - if (GLib.file_test(`${STATE}/scheme/current.txt`, GLib.FileTest.EXISTS)) { - const currentScheme = readFile(`${STATE}/scheme/current.txt`); - schemeColours = currentScheme.split("\n").map(l => l.split(" ")); - } else - schemeColours = readFile(`${SRC}/scss/scheme/_default.scss`) - .split("\n") - .map(l => { - const [name, hex] = l.split(":"); - return [name.slice(1), hex.trim().slice(1, -1)]; - }); - - this.#colours = schemeColours.reduce((acc, [name, hex]) => ({ ...acc, [name]: `#${hex}` }), {} as IPalette); - this.#notify(); - } - - switchMode(mode: ColourMode) { - execAsync(`caelestia scheme ${this.scheme} ${this.flavour ?? ""} ${mode}`).catch(console.error); - } - - hasMode(mode: ColourMode) { - const scheme = Schemes.get_default().map[this.scheme]; - if (scheme?.colours?.[mode]) return true; - return scheme?.flavours?.[this.flavour ?? ""]?.colours?.[mode] !== undefined; - } - - constructor() { - super(); - - this.#mode = readFile(`${STATE}/scheme/current-mode.txt`) === "light" ? "light" : "dark"; - monitorFile(`${STATE}/scheme/current-mode.txt`, async file => { - this.#mode = (await readFileAsync(file)) === "light" ? "light" : "dark"; - this.notify("mode"); - }); - - [this.#scheme, this.#flavour] = readFile(`${STATE}/scheme/current-name.txt`).split("-"); - monitorFile(`${STATE}/scheme/current-name.txt`, async file => { - [this.#scheme, this.#flavour] = (await readFileAsync(file)).split("-"); - this.notify("scheme"); - this.notify("flavour"); - }); - - this.update(); - monitorFile(`${STATE}/scheme/current.txt`, () => this.update()); - } -} diff --git a/src/services/players.ts b/src/services/players.ts deleted file mode 100644 index aca7344..0000000 --- a/src/services/players.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { isRealPlayer } from "@/utils/mpris"; -import { GLib, GObject, property, readFileAsync, register, writeFileAsync } from "astal"; -import AstalMpris from "gi://AstalMpris"; - -@register({ GTypeName: "Players" }) -export default class Players extends GObject.Object { - static instance: Players; - static get_default() { - if (!this.instance) this.instance = new Players(); - - return this.instance; - } - - readonly #path = `${STATE}/players.txt`; - readonly #players: AstalMpris.Player[] = []; - readonly #subs = new Map< - JSX.Element, - { signals: string[]; callback: () => void; ids: number[]; player: AstalMpris.Player | null } - >(); - - @property(AstalMpris.Player) - get lastPlayer(): AstalMpris.Player | null { - return this.#players.length > 0 && this.#players[0].identity !== null ? this.#players[0] : null; - } - - /** - * List of real players. - */ - @property(Object) - get list() { - return this.#players; - } - - hookLastPlayer(widget: JSX.Element, signal: string, callback: () => void): this; - hookLastPlayer(widget: JSX.Element, signals: string[], callback: () => void): this; - hookLastPlayer(widget: JSX.Element, signals: string | string[], callback: () => void) { - if (!Array.isArray(signals)) signals = [signals]; - // Add subscription - if (this.lastPlayer) - this.#subs.set(widget, { - signals, - callback, - ids: signals.map(s => this.lastPlayer!.connect(s, callback)), - player: this.lastPlayer, - }); - else this.#subs.set(widget, { signals, callback, ids: [], player: null }); - - // Remove subscription on widget destroyed - widget.connect("destroy", () => { - const sub = this.#subs.get(widget); - if (sub?.player) sub.ids.forEach(id => sub.player!.disconnect(id)); - this.#subs.delete(widget); - }); - - // Initial run of callback - callback(); - - // For chaining - return this; - } - - makeCurrent(player: AstalMpris.Player) { - const index = this.#players.findIndex(p => p.busName === player.busName); - // Ignore if already current - if (index === 0 || !isRealPlayer(player)) return; - // Remove if present - else if (index > 0) this.#players.splice(index, 1); - // Connect signals if not already in list (i.e. new player) - else this.#connectPlayerSignals(player); - - // Add to front - this.#players.unshift(player); - this.#updatePlayer(); - this.notify("list"); - - // Save to file - this.#save(); - } - - #updatePlayer() { - this.notify("last-player"); - - for (const sub of this.#subs.values()) { - sub.callback(); - if (sub.player) sub.ids.forEach(id => sub.player!.disconnect(id)); - sub.ids = this.lastPlayer ? sub.signals.map(s => this.lastPlayer!.connect(s, sub.callback)) : []; - sub.player = this.lastPlayer; - } - } - - #save() { - writeFileAsync(this.#path, this.#players.map(p => p.busName).join("\n")).catch(console.error); - } - - #connectPlayerSignals(player: AstalMpris.Player) { - // Change order on attribute change - for (const signal of [ - "notify::playback-status", - "notify::shuffle-status", - "notify::loop-status", - "notify::volume", - "notify::rate", - ]) - player.connect(signal, () => this.makeCurrent(player)); - } - - constructor() { - super(); - - const mpris = AstalMpris.get_default(); - - // Load players - if (GLib.file_test(this.#path, GLib.FileTest.EXISTS)) { - readFileAsync(this.#path).then(out => { - for (const busName of out.split("\n").reverse()) { - const player = mpris.get_players().find(p => p.busName === busName); - if (player) this.makeCurrent(player); - } - // Add new players from in between sessions - for (const player of mpris.get_players()) this.makeCurrent(player); - }); - } else { - const sortOrder = [ - AstalMpris.PlaybackStatus.PLAYING, - AstalMpris.PlaybackStatus.PAUSED, - AstalMpris.PlaybackStatus.STOPPED, - ]; - const players = mpris - .get_players() - .sort((a, b) => sortOrder.indexOf(b.playbackStatus) - sortOrder.indexOf(a.playbackStatus)); - for (const player of players) this.makeCurrent(player); - } - - // Add and connect signals when added - mpris.connect("player-added", (_, player) => this.makeCurrent(player)); - - // Remove when closed - mpris.connect("player-closed", (_, player) => { - const index = this.#players.indexOf(player); - if (index >= 0) { - this.#players.splice(index, 1); - this.notify("list"); - if (index === 0) this.#updatePlayer(); - this.#save(); - } - }); - } -} diff --git a/src/services/schemes.ts b/src/services/schemes.ts deleted file mode 100644 index c85fa72..0000000 --- a/src/services/schemes.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { basename } from "@/utils/strings"; -import { monitorDirectory } from "@/utils/system"; -import { execAsync, Gio, GLib, GObject, property, readFileAsync, register } from "astal"; -import type { IPalette } from "./palette"; - -export interface Colours { - light?: IPalette; - dark?: IPalette; -} - -export interface Flavour { - name: string; - scheme: string; - colours: Colours; -} - -export interface Scheme { - name: string; - flavours?: { [k: string]: Flavour }; - colours?: Colours; -} - -const DATA = `${GLib.get_user_data_dir()}/caelestia`; - -@register({ GTypeName: "Schemes" }) -export default class Schemes extends GObject.Object { - static instance: Schemes; - static get_default() { - if (!this.instance) this.instance = new Schemes(); - - return this.instance; - } - - readonly #schemeDir: string = `${DATA}/scripts/data/schemes`; - - #map: { [k: string]: Scheme } = {}; - - @property(Object) - get map() { - return this.#map; - } - - async parseMode(path: string): Promise<IPalette | undefined> { - const schemeColours = (await readFileAsync(path).catch(() => undefined))?.split("\n").map(l => l.split(" ")); - return schemeColours?.reduce((acc, [name, hex]) => ({ ...acc, [name]: `#${hex}` }), {} as IPalette); - } - - async parseColours(path: string): Promise<Colours> { - const light = await this.parseMode(`${path}/light.txt`); - const dark = await this.parseMode(`${path}/dark.txt`); - return { light, dark }; - } - - async parseFlavour(scheme: string, name: string): Promise<Flavour> { - const path = `${this.#schemeDir}/${scheme}/${name}`; - return { name, scheme, colours: await this.parseColours(path) }; - } - - async parseScheme(name: string): Promise<Scheme> { - const path = `${this.#schemeDir}/${name}`; - - const flavours = await execAsync(`find ${path}/ -mindepth 1 -maxdepth 1 -type d`); - if (flavours.trim()) - return { - name, - flavours: ( - await Promise.all(flavours.split("\n").map(f => this.parseFlavour(name, basename(f)))) - ).reduce((acc, f) => ({ ...acc, [f.name]: f }), {} as { [k: string]: Flavour }), - }; - - return { name, colours: await this.parseColours(path) }; - } - - async update() { - const schemes = await execAsync(`find ${this.#schemeDir}/ -mindepth 1 -maxdepth 1 -type d`); - (await Promise.all(schemes.split("\n").map(s => this.parseScheme(basename(s))))).forEach( - s => (this.#map[s.name] = s) - ); - this.notify("map"); - } - - async updateFile(file: Gio.File) { - if (file.get_basename() !== "light.txt" && file.get_basename() !== "dark.txt") { - await this.update(); - return; - } - - const mode = file.get_basename()!.slice(0, -4) as "light" | "dark"; - const parent = file.get_parent()!; - const parentParent = parent.get_parent()!; - - if (parentParent.get_basename() === "schemes") - this.#map[parent.get_basename()!].colours![mode] = await this.parseMode(file.get_path()!); - else - this.#map[parentParent.get_basename()!].flavours![parent.get_basename()!].colours![mode] = - await this.parseMode(file.get_path()!); - - this.notify("map"); - } - - constructor() { - super(); - - this.update().catch(console.error); - monitorDirectory(this.#schemeDir, (_m, file, _f, type) => { - if (type !== Gio.FileMonitorEvent.DELETED) this.updateFile(file).catch(console.error); - }); - } -} diff --git a/src/services/storage.ts b/src/services/storage.ts deleted file mode 100644 index 3f8992d..0000000 --- a/src/services/storage.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { GObject, interval, property, register } from "astal"; -import { storage as config } from "config"; -import GTop from "gi://GTop"; - -@register({ GTypeName: "Storage" }) -export default class Storage extends GObject.Object { - static instance: Storage; - static get_default() { - if (!this.instance) this.instance = new Storage(); - - return this.instance; - } - - #total: number = 0; - #free: number = 0; - #used: number = 0; - #usage: number = 0; - - @property(Number) - get total() { - return this.#total; - } - - @property(Number) - get free() { - return this.#free; - } - - @property(Number) - get used() { - return this.#used; - } - - @property(Number) - get usage() { - return this.#usage; - } - - update() { - const root = new GTop.glibtop_fsusage(); - GTop.glibtop_get_fsusage(root, "/"); - const home = new GTop.glibtop_fsusage(); - GTop.glibtop_get_fsusage(home, "/home"); - - this.#total = root.blocks * root.block_size + home.blocks * home.block_size; - this.#free = root.bavail * root.block_size + home.bavail * home.block_size; - this.#used = this.#total - this.#free; - this.#usage = this.#total > 0 ? (this.#used / this.#total) * 100 : 0; - - this.notify("total"); - this.notify("free"); - this.notify("used"); - this.notify("usage"); - } - - constructor() { - super(); - - let source = interval(config.interval.get(), () => this.update()); - config.interval.subscribe(i => { - source.cancel(); - source = interval(i, () => this.update()); - }); - } -} diff --git a/src/services/updates.ts b/src/services/updates.ts deleted file mode 100644 index 62a8f65..0000000 --- a/src/services/updates.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { capitalize } from "@/utils/strings"; -import { execAsync, GLib, GObject, property, readFileAsync, register, writeFileAsync } from "astal"; -import { updates as config } from "config"; - -export interface Update { - full: string; - name: string; - description: string; - url: string; - version: { - old: string; - new: string; - }; -} - -export interface Repo { - repo?: string[]; - updates: Update[]; - icon: string; - name: string; -} - -export interface Data { - cached?: boolean; - repos: Repo[]; - errors: string[]; - news: string; -} - -@register({ GTypeName: "Updates" }) -export default class Updates extends GObject.Object { - static instance: Updates; - static get_default() { - if (!this.instance) this.instance = new Updates(); - - return this.instance; - } - - readonly #cachePath = `${CACHE}/updates.json`; - - #timeout?: GLib.Source; - #loading = false; - #data: Data = { cached: true, repos: [], errors: [], news: "" }; - - @property(Boolean) - get loading() { - return this.#loading; - } - - @property(Object) - get updateData() { - return this.#data; - } - - @property(Object) - get list() { - return this.#data.repos.map(r => r.updates).flat(); - } - - @property(Number) - get numUpdates() { - return this.#data.repos.reduce((acc, repo) => acc + repo.updates.length, 0); - } - - @property(String) - get news() { - return this.#data.news; - } - - async #updateFromCache() { - this.#data = JSON.parse(await readFileAsync(this.#cachePath)); - this.notify("update-data"); - this.notify("list"); - this.notify("num-updates"); - this.notify("news"); - } - - async getRepo(repo: string) { - return (await execAsync(`bash -c "comm -12 <(pacman -Qq | sort) <(pacman -Slq '${repo}' | sort)"`)).split("\n"); - } - - async constructUpdate(update: string) { - const info = await execAsync(`pacman -Qi ${update.split(" ")[0]}`); - return info.split("\n").reduce( - (acc, line) => { - let [key, value] = line.split(" : "); - key = key.trim().toLowerCase(); - if (key === "name" || key === "description" || key === "url") acc[key] = value.trim(); - else if (key === "version") acc.version.old = value.trim(); - return acc; - }, - { version: { new: update.split("->")[1].trim() } } as Update - ); - } - - getRepoIcon(repo: string) { - switch (repo) { - case "core": - return "hub"; - case "extra": - return "add_circle"; - case "multilib": - return "account_tree"; - default: - return "deployed_code_update"; - } - } - - getUpdates() { - // Return if already getting updates - if (this.#loading) return; - - this.#loading = true; - this.notify("loading"); - - // Get new updates - Promise.allSettled([execAsync("checkupdates"), execAsync("yay -Qua"), execAsync("yay -Pw")]) - .then(async ([pacman, yay, news]) => { - const data: Data = { repos: [], errors: [], news: news.status === "fulfilled" ? news.value : "" }; - - // Pacman updates (checkupdates) - if (pacman.status === "fulfilled") { - const repos: Repo[] = await Promise.all( - (await execAsync("pacman-conf -l")).split("\n").map(async r => ({ - repo: await this.getRepo(r), - updates: [], - icon: this.getRepoIcon(r), - name: capitalize(r) + " repository", - })) - ); - - for (const update of pacman.value.split("\n")) { - const pkg = update.split(" ")[0]; - for (const repo of repos) - if (repo.repo?.includes(pkg)) repo.updates.push(await this.constructUpdate(update)); - } - - for (const repo of repos) if (repo.updates.length > 0) data.repos.push(repo); - } - - // AUR and devel updates (yay -Qua) - if (yay.status === "fulfilled") { - const aur: Repo = { updates: [], icon: "deployed_code_account", name: "AUR" }; - - for (const update of yay.value.split("\n")) { - if (/^\s*->/.test(update)) data.errors.push(update); // Error - else aur.updates.push(await this.constructUpdate(update)); - } - - if (aur.updates.length > 0) data.repos.push(aur); - } - - if (data.errors.length > 0 && data.repos.length === 0) { - this.#updateFromCache().catch(console.error); - } else { - // Sort updates by name - for (const repo of data.repos) repo.updates.sort((a, b) => a.name.localeCompare(b.name)); - - // Cache and set - writeFileAsync(this.#cachePath, JSON.stringify({ cached: true, ...data })).catch(console.error); - this.#data = data; - this.notify("update-data"); - this.notify("list"); - this.notify("num-updates"); - this.notify("news"); - } - - this.#loading = false; - this.notify("loading"); - - this.#timeout?.destroy(); - this.#timeout = setTimeout(() => this.getUpdates(), config.interval.get()); - }) - .catch(console.error); - } - - constructor() { - super(); - - // Initial update from cache, if fail then write valid data to cache so future reads don't fail - this.#updateFromCache().catch(() => - writeFileAsync(this.#cachePath, JSON.stringify(this.#data)).catch(console.error) - ); - this.getUpdates(); - - config.interval.subscribe(i => { - this.#timeout?.destroy(); - this.#timeout = setTimeout(() => this.getUpdates(), i); - }); - } -} diff --git a/src/services/wallpapers.ts b/src/services/wallpapers.ts deleted file mode 100644 index b5447c2..0000000 --- a/src/services/wallpapers.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { monitorDirectory } from "@/utils/system"; -import Thumbnailer from "@/utils/thumbnailer"; -import { execAsync, GObject, property, register } from "astal"; -import { wallpapers as config } from "config"; -import Monitors from "./monitors"; - -export interface IWallpaper { - path: string; - thumbnails: { - compact: string; - medium: string; - large: string; - }; -} - -export interface ICategory { - path: string; - wallpapers: IWallpaper[]; -} - -@register({ GTypeName: "Wallpapers" }) -export default class Wallpapers extends GObject.Object { - static instance: Wallpapers; - static get_default() { - if (!this.instance) this.instance = new Wallpapers(); - - return this.instance; - } - - #list: IWallpaper[] = []; - #categories: ICategory[] = []; - - @property(Object) - get list() { - return this.#list; - } - - @property(Object) - get categories() { - return this.#categories; - } - - async #listDir(path: { path: string; recursive: boolean; threshold: number }, type: "f" | "d") { - const absPath = path.path.replace("~", HOME); - const maxDepth = path.recursive ? "" : "-maxdepth 1"; - const files = await execAsync(`find ${absPath} ${maxDepth} -path '*/.*' -prune -o -type ${type} -print`); - - if (type === "f" && path.threshold > 0) { - const data = ( - await execAsync([ - "fish", - "-c", - `identify -ping -format '%i %w %h\n' ${files.replaceAll("\n", " ")} ; true`, - ]) - ).split("\n"); - - return data - .filter(l => l && this.#filterSize(l, path.threshold)) - .map(l => l.split(" ").slice(0, -2).join(" ")) - .join("\n"); - } - - return files; - } - - #filterSize(line: string, threshold: number) { - const [width, height] = line.split(" ").slice(-2).map(Number); - const mWidth = Math.max(...Monitors.get_default().list.map(m => m.width)); - const mHeight = Math.max(...Monitors.get_default().list.map(m => m.height)); - - return width >= mWidth * threshold && height >= mHeight * threshold; - } - - async update() { - const results = await Promise.allSettled( - config.paths.get().map(async p => ({ path: p, files: await this.#listDir(p, "f") })) - ); - const successes = results.filter(r => r.status === "fulfilled").map(r => r.value); - - if (!successes.length) { - this.#list = []; - this.notify("list"); - this.#categories = []; - this.notify("categories"); - return; - } - - const files = successes.map(r => r.files.replaceAll("\n", " ")).join(" "); - const list = (await execAsync(["fish", "-c", `identify -ping -format '%i\n' ${files} ; true`])).split("\n"); - - this.#list = await Promise.all( - list.map(async p => ({ - path: p, - thumbnails: { - compact: await Thumbnailer.thumbnail(p, { width: 60, height: 60, exact: true }), - medium: await Thumbnailer.thumbnail(p, { width: 400, height: 150, exact: true }), - large: await Thumbnailer.thumbnail(p, { width: 400, height: 200, exact: true }), - }, - })) - ); - this.#list.sort((a, b) => a.path.localeCompare(b.path)); - this.notify("list"); - - const categories = await Promise.all(successes.map(r => this.#listDir(r.path, "d"))); - this.#categories = categories - .flatMap(c => c.split("\n")) - .map(c => ({ path: c, wallpapers: this.#list.filter(w => w.path.startsWith(c)) })) - .filter(c => c.wallpapers.length > 0) - .sort((a, b) => a.path.localeCompare(b.path)); - this.notify("categories"); - } - - constructor() { - super(); - - this.update().catch(console.error); - - let monitors = config.paths - .get() - .map(p => monitorDirectory(p.path, () => this.update().catch(console.error), p.recursive)); - config.paths.subscribe(v => { - this.update().catch(console.error); - for (const m of monitors) m.cancel(); - monitors = v.map(p => monitorDirectory(p.path, () => this.update().catch(console.error), p.recursive)); - }); - } -} diff --git a/src/services/weather.ts b/src/services/weather.ts deleted file mode 100644 index d51e7fc..0000000 --- a/src/services/weather.ts +++ /dev/null @@ -1,388 +0,0 @@ -import { weatherIcons } from "@/utils/icons"; -import { notify } from "@/utils/system"; -import { - execAsync, - GLib, - GObject, - interval, - property, - readFileAsync, - register, - writeFileAsync, - type Time, -} from "astal"; -import { weather as config } from "config"; - -export interface WeatherCondition { - text: string; - icon: string; - code: number; -} - -interface _WeatherState { - temp_c: number; - temp_f: number; - is_day: number; - condition: WeatherCondition; - wind_mph: number; - wind_kph: number; - wind_degree: number; - wind_dir: "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW"; - pressure_mb: number; - pressure_in: number; - precip_mm: number; - precip_in: number; - humidity: number; - cloud: number; - feelslike_c: number; - feelslike_f: number; - windchill_c: number; - windchill_f: number; - heatindex_c: number; - heatindex_f: number; - dewpoint_c: number; - dewpoint_f: number; - vis_km: number; - vis_miles: number; - uv: number; - gust_mph: number; - gust_kph: number; -} - -export interface WeatherCurrent extends _WeatherState { - last_updated_epoch: number; - last_updated: string; -} - -export interface WeatherHour extends _WeatherState { - time_epoch: number; - time: string; -} - -export interface WeatherDay { - date: string; - date_epoch: number; - day: { - maxtemp_c: number; - maxtemp_f: number; - mintemp_c: number; - mintemp_f: number; - avgtemp_c: number; - avgtemp_f: number; - maxwind_mph: number; - maxwind_kph: number; - totalprecip_mm: number; - totalprecip_in: number; - totalsnow_cm: number; - avgvis_km: number; - avgvis_miles: number; - avghumidity: number; - daily_will_it_rain: number; - daily_chance_of_rain: number; - daily_will_it_snow: number; - daily_chance_of_snow: number; - condition: WeatherCondition; - uv: number; - }; - astro: { - sunrise: string; - sunset: string; - moonrise: string; - moonset: string; - moon_phase: string; - moon_illumination: string; - is_moon_up: number; - is_sun_up: number; - }; - hour: WeatherHour[]; -} - -export interface WeatherLocation { - name: string; - region: string; - country: string; - lat: number; - lon: number; - tz_id: string; - localtime_epoch: number; - localtime: string; -} - -export interface WeatherData { - current: WeatherCurrent; - forecast: { forecastday: WeatherDay[] }; - location: WeatherLocation; -} - -const DEFAULT_STATE: _WeatherState = { - temp_c: 0, - temp_f: 0, - is_day: 0, - condition: { text: "", icon: "", code: 0 }, - wind_mph: 0, - wind_kph: 0, - wind_degree: 0, - wind_dir: "N", - pressure_mb: 0, - pressure_in: 0, - precip_mm: 0, - precip_in: 0, - humidity: 0, - cloud: 0, - feelslike_c: 0, - feelslike_f: 0, - windchill_c: 0, - windchill_f: 0, - heatindex_c: 0, - heatindex_f: 0, - dewpoint_c: 0, - dewpoint_f: 0, - vis_km: 0, - vis_miles: 0, - uv: 0, - gust_mph: 0, - gust_kph: 0, -}; - -const DEFAULT: WeatherData = { - current: { - last_updated_epoch: 0, - last_updated: "", - ...DEFAULT_STATE, - }, - forecast: { - forecastday: [ - { - date: "", - date_epoch: 0, - day: { - maxtemp_c: 0, - maxtemp_f: 0, - mintemp_c: 0, - mintemp_f: 0, - avgtemp_c: 0, - avgtemp_f: 0, - maxwind_mph: 0, - maxwind_kph: 0, - totalprecip_mm: 0, - totalprecip_in: 0, - totalsnow_cm: 0, - avgvis_km: 0, - avgvis_miles: 0, - avghumidity: 0, - daily_will_it_rain: 0, - daily_chance_of_rain: 0, - daily_will_it_snow: 0, - daily_chance_of_snow: 0, - condition: { text: "", icon: "", code: 0 }, - uv: 0, - }, - astro: { - sunrise: "", - sunset: "", - moonrise: "", - moonset: "", - moon_phase: "", - moon_illumination: "", - is_moon_up: 0, - is_sun_up: 0, - }, - hour: Array.from({ length: 24 }, () => ({ - time_epoch: 0, - time: "", - ...DEFAULT_STATE, - })), - }, - ], - }, - location: { - name: "", - region: "", - country: "", - lat: 0, - lon: 0, - tz_id: "", - localtime_epoch: 0, - localtime: "", - }, -}; - -@register({ GTypeName: "Weather" }) -export default class Weather extends GObject.Object { - static instance: Weather; - static get_default() { - if (!this.instance) this.instance = new Weather(); - - return this.instance; - } - - readonly #cache: string = `${CACHE}/weather.json`; - #notified = false; - - #data: WeatherData = DEFAULT; - - #interval: Time | null = null; - - @property(Object) - get raw() { - return this.#data; - } - - @property(Object) - get current() { - return this.#data.current; - } - - @property(Object) - get forecast() { - return this.#data.forecast.forecastday[0].hour; - } - - @property(Object) - get location() { - return this.#data.location; - } - - @property(String) - get condition() { - return this.#data.current.condition.text; - } - - @property(String) - get temperature() { - return this.getTemp(this.#data.current); - } - - @property(String) - get wind() { - return `${Math.round(this.#data.current[`wind_${config.imperial.get() ? "m" : "k"}ph`])} ${ - config.imperial.get() ? "m" : "k" - }ph`; - } - - @property(String) - get rainChance() { - return this.#data.forecast.forecastday[0].day.daily_chance_of_rain + "%"; - } - - @property(String) - get icon() { - return this.getIcon(this.#data.current.condition.text); - } - - @property(String) - get tempIcon() { - return this.getTempIcon(this.#data.current.temp_c); - } - - @property(String) - get tempColour() { - return this.getTempDesc(this.#data.current.temp_c); - } - - getIcon(status: string) { - let query = status.trim().toLowerCase().replaceAll(" ", "_"); - if (!this.#data.current.is_day && query + "_night" in weatherIcons) query += "_night"; - return weatherIcons[query] ?? weatherIcons.warning; - } - - getTemp(data: _WeatherState) { - return `${Math.round(data[`temp_${config.imperial.get() ? "f" : "c"}`])}°${config.imperial.get() ? "F" : "C"}`; - } - - getTempIcon(temp: number) { - if (temp >= 40) return ""; - if (temp >= 30) return ""; - if (temp >= 20) return ""; - if (temp >= 10) return ""; - return ""; - } - - getTempDesc(temp: number) { - if (temp >= 40) return "burning"; - if (temp >= 30) return "hot"; - if (temp >= 20) return "normal"; - if (temp >= 10) return "cold"; - return "freezing"; - } - - #notify() { - this.notify("raw"); - this.notify("current"); - this.notify("forecast"); - this.notify("location"); - this.notify("condition"); - this.notify("temperature"); - this.notify("wind"); - this.notify("rain-chance"); - this.notify("icon"); - this.notify("temp-icon"); - this.notify("temp-colour"); - } - - async getWeather() { - const location = config.location || JSON.parse(await execAsync("curl ipinfo.io")).city; - const opts = `key=${config.apiKey.get()}&q=${location}&days=1&aqi=no&alerts=no`; - const url = `https://api.weatherapi.com/v1/forecast.json?${opts}`; - return JSON.parse(await execAsync(["curl", url])); - } - - async updateWeather() { - if (!config.apiKey.get()) { - if (!this.#notified) { - notify({ - summary: "Weather API key required", - body: `A weather API key is required to get weather data. Get one from https://www.weatherapi.com.`, - icon: "dialog-warning-symbolic", - urgency: "critical", - actions: { - "Get API key": () => execAsync(`app2unit -O 'https://www.weatherapi.com'`).catch(print), - }, - }); - this.#notified = true; - } - return; - } - - if (GLib.file_test(this.#cache, GLib.FileTest.EXISTS)) { - const cache = await readFileAsync(this.#cache); - const cache_data: WeatherData = JSON.parse(cache); - if (cache_data.location.localtime_epoch * 1000 + config.interval.get() > Date.now()) { - if (JSON.stringify(this.#data) !== cache) { - this.#data = cache_data; - this.#notify(); - } - return; - } - } - - try { - const data = await this.getWeather(); - this.#data = data; - writeFileAsync(this.#cache, JSON.stringify(data)).catch(console.error); // Catch here so it doesn't propagate - } catch (e) { - console.error("Error getting weather:", e); - this.#data = DEFAULT; - } - this.#notify(); - } - - constructor() { - super(); - - this.updateWeather().catch(console.error); - this.#interval = interval(config.interval.get(), () => this.updateWeather().catch(console.error)); - - config.apiKey.subscribe(() => this.updateWeather()); - - config.interval.subscribe(i => { - this.#interval?.cancel(); - this.#interval = interval(i, () => this.updateWeather().catch(console.error)); - }); - - config.imperial.subscribe(() => { - this.notify("temperature"); - this.notify("wind"); - }); - } -} |