summaryrefslogtreecommitdiff
path: root/src/services
diff options
context:
space:
mode:
Diffstat (limited to 'src/services')
-rw-r--r--src/services/apps.ts3
-rw-r--r--src/services/calendar.ts228
-rw-r--r--src/services/cpu.ts49
-rw-r--r--src/services/gpu.ts63
-rw-r--r--src/services/math.ts155
-rw-r--r--src/services/memory.ts64
-rw-r--r--src/services/monitors.ts127
-rw-r--r--src/services/news.ts153
-rw-r--r--src/services/palette.ts298
-rw-r--r--src/services/players.ts148
-rw-r--r--src/services/schemes.ts109
-rw-r--r--src/services/storage.ts65
-rw-r--r--src/services/updates.ts191
-rw-r--r--src/services/wallpapers.ts127
-rw-r--r--src/services/weather.ts388
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");
- });
- }
-}