diff options
| author | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-01-15 13:15:06 +1100 |
|---|---|---|
| committer | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-01-15 13:15:06 +1100 |
| commit | 9f843558c029daa85cdcb6bc20e7b837c751be08 (patch) | |
| tree | aa51dbd7b5eb2f4fa71b6578bed93c127bac8d11 | |
| parent | Pass home and cache through bundler (diff) | |
| download | caelestia-shell-9f843558c029daa85cdcb6bc20e7b837c751be08.tar.gz caelestia-shell-9f843558c029daa85cdcb6bc20e7b837c751be08.tar.bz2 caelestia-shell-9f843558c029daa85cdcb6bc20e7b837c751be08.zip | |
bar: update indicator
| -rw-r--r-- | config.ts | 6 | ||||
| -rw-r--r-- | modules/bar.tsx | 17 | ||||
| -rw-r--r-- | scss/bar.scss | 4 | ||||
| -rw-r--r-- | services/updates.ts | 148 |
4 files changed, 175 insertions, 0 deletions
@@ -1,3 +1,4 @@ +// Modules export const bar = { wsPerGroup: 10, dateTimeFormat: "%d/%m/%y %R", @@ -19,3 +20,8 @@ export const notifpopups = { maxPopups: -1, expire: false, }; + +// Services +export const updates = { + interval: 900000, +}; diff --git a/modules/bar.tsx b/modules/bar.tsx index a928ae8..a6ff6d1 100644 --- a/modules/bar.tsx +++ b/modules/bar.tsx @@ -8,6 +8,7 @@ import AstalNotifd from "gi://AstalNotifd"; import AstalTray from "gi://AstalTray"; import { bar as config } from "../config"; import Players from "../services/players"; +import Updates from "../services/updates"; import { getAppCategoryIcon } from "../utils/icons"; import { ellipsize } from "../utils/strings"; import { osIcon } from "../utils/system"; @@ -307,6 +308,21 @@ const StatusIcons = () => ( </box> ); +const PkgUpdates = () => ( + <box + className="module updates" + setup={self => + setupCustomTooltip( + self, + bind(Updates.get_default(), "numUpdates").as(n => `${n} update${n === 1 ? "" : "s"} available`) + ) + } + > + <label className="icon" label="download" /> + <label label={bind(Updates.get_default(), "numUpdates").as(String)} /> + </box> +); + const Notifications = () => { const unreadCount = Variable(0); return ( @@ -396,6 +412,7 @@ export default ({ monitor }: { monitor: AstalHyprland.Monitor }) => ( <box halign={Gtk.Align.END}> <Tray /> <StatusIcons /> + <PkgUpdates /> <Notifications /> <DateTime /> <Power /> diff --git a/scss/bar.scss b/scss/bar.scss index 1726cac..ab75b00 100644 --- a/scss/bar.scss +++ b/scss/bar.scss @@ -88,6 +88,10 @@ color: scheme.$rosewater; } + .updates { + color: scheme.$blue; + } + .notifications { color: scheme.$mauve; } diff --git a/services/updates.ts b/services/updates.ts new file mode 100644 index 0000000..0b04e85 --- /dev/null +++ b/services/updates.ts @@ -0,0 +1,148 @@ +import { execAsync, GLib, GObject, property, readFileAsync, register, writeFileAsync } from "astal"; +import { updates as config } from "../config"; + +interface Update { + name: string; + full: string; +} + +interface Repo { + repo?: string[]; + updates: Update[]; + icon: string; + name: string; +} + +interface Data { + cached?: boolean; + repos: Repo[]; + errors: 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.txt`; + + #timeout?: GLib.Source; + #loading = false; + #data: Data = { cached: true, repos: [], errors: [] }; + + @property(Boolean) + get loading() { + return this.#loading; + } + + @property(Object) + get data() { + 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); + } + + async #updateFromCache() { + this.#data = JSON.parse(await readFileAsync(this.#cachePath)); + this.notify("data"); + this.notify("list"); + this.notify("num-updates"); + } + + async #getRepo(repo: string) { + return (await execAsync(`bash -c "comm -12 <(pacman -Qq | sort) <(pacman -Slq '${repo}' | sort)"`)).split("\n"); + } + + 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")]) + .then(async ([pacman, yay]) => { + const data: Data = { repos: [], errors: [] }; + + // Pacman updates (checkupdates) + if (pacman.status === "fulfilled") { + const repos: Repo[] = [ + { repo: await this.#getRepo("core"), updates: [], icon: "hub", name: "Core repository" }, + { + repo: await this.#getRepo("extra"), + updates: [], + icon: "add_circle", + name: "Extra repository", + }, + { + repo: await this.#getRepo("multilib"), + updates: [], + icon: "account_tree", + name: "Multilib 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({ name: pkg, full: 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({ name: update.split(" ")[0], full: 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 { + // Cache and set + writeFileAsync(this.#cachePath, JSON.stringify({ cached: true, ...data })).catch(console.error); + this.#data = data; + this.notify("data"); + this.notify("list"); + this.notify("num-updates"); + } + + this.#loading = false; + this.notify("loading"); + + this.#timeout?.destroy(); + this.#timeout = setTimeout(() => this.getUpdates(), config.interval); + }) + .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(); + } +} |