summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-01-15 13:15:06 +1100
committer2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-01-15 13:15:06 +1100
commit9f843558c029daa85cdcb6bc20e7b837c751be08 (patch)
treeaa51dbd7b5eb2f4fa71b6578bed93c127bac8d11
parentPass home and cache through bundler (diff)
downloadcaelestia-shell-9f843558c029daa85cdcb6bc20e7b837c751be08.tar.gz
caelestia-shell-9f843558c029daa85cdcb6bc20e7b837c751be08.tar.bz2
caelestia-shell-9f843558c029daa85cdcb6bc20e7b837c751be08.zip
bar: update indicator
-rw-r--r--config.ts6
-rw-r--r--modules/bar.tsx17
-rw-r--r--scss/bar.scss4
-rw-r--r--services/updates.ts148
4 files changed, 175 insertions, 0 deletions
diff --git a/config.ts b/config.ts
index ccfa08a..39f0b58 100644
--- a/config.ts
+++ b/config.ts
@@ -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();
+ }
+}