summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/modules/bar.tsx29
-rw-r--r--src/modules/updates.tsx102
-rw-r--r--src/services/updates.ts46
-rw-r--r--src/utils/widgets.ts11
4 files changed, 160 insertions, 28 deletions
diff --git a/src/modules/bar.tsx b/src/modules/bar.tsx
index aeb6e42..cf08634 100644
--- a/src/modules/bar.tsx
+++ b/src/modules/bar.tsx
@@ -221,14 +221,7 @@ const Network = () => (
network.wifi.scan();
execAsync(
"uwsm app -- foot -T nmtui fish -c 'sleep .1; set -e COLORTERM; TERM=xterm-old nmtui connect'"
- ).catch(err => {
- // Idk why but foot always throws this error when it opens
- if (
- err.message !==
- "warn: wayland.c:1619: compositor does not implement the XDG toplevel icon protocol\nwarn: terminal.c:1973: slave exited with signal 1 (Hangup)"
- )
- console.error(err);
- });
+ ).catch(() => {}); // Ignore errors
});
}}
setup={self => {
@@ -372,8 +365,16 @@ const StatusIcons = () => (
);
const PkgUpdates = () => (
- <box
- className="module updates"
+ <button
+ onClick={(self, event) => {
+ if (event.button === Astal.MouseButton.PRIMARY) {
+ const popup = App.get_window("updates") as PopupWindow | null;
+ if (popup) {
+ if (popup.visible) popup.hide();
+ else popup.popup_at_widget(self, event);
+ }
+ }
+ }}
setup={self =>
setupCustomTooltip(
self,
@@ -381,9 +382,11 @@ const PkgUpdates = () => (
)
}
>
- <label className="icon" label="download" />
- <label label={bind(Updates.get_default(), "numUpdates").as(String)} />
- </box>
+ <box className="module pkg-updates">
+ <label className="icon" label="download" />
+ <label label={bind(Updates.get_default(), "numUpdates").as(String)} />
+ </box>
+ </button>
);
const Unread = () => {
diff --git a/src/modules/updates.tsx b/src/modules/updates.tsx
new file mode 100644
index 0000000..0a8cbea
--- /dev/null
+++ b/src/modules/updates.tsx
@@ -0,0 +1,102 @@
+import { bind, execAsync, Variable } from "astal";
+import { App, Astal, Gtk } from "astal/gtk3";
+import Updates, { Repo as IRepo, Update as IUpdate } from "../services/updates";
+import { MenuItem } from "../utils/widgets";
+import PopupWindow from "../widgets/popupwindow";
+
+const constructItem = (label: string, exec: string, quiet = true) =>
+ new MenuItem({
+ label,
+ onActivate() {
+ App.get_window("updates")?.hide();
+ execAsync(exec).catch(e => !quiet && console.error(e));
+ },
+ });
+
+const Update = (update: IUpdate) => {
+ const menu = new Gtk.Menu();
+ menu.append(constructItem("Open info in browser", `xdg-open '${update.url}'`, false));
+ menu.append(constructItem("Open info in terminal", `uwsm app -- foot -H pacman -Qi ${update.name}`));
+ menu.append(constructItem("Reinstall", `uwsm app -T -- yay -S ${update.name}`));
+ menu.append(constructItem("Remove with dependencies", `uwsm app -T -- yay -Rns ${update.name}`));
+
+ return (
+ <button
+ onClick={(_, event) => event.button === Astal.MouseButton.SECONDARY && menu.popup_at_pointer(null)}
+ onDestroy={() => menu.destroy()}
+ >
+ <label
+ truncate
+ xalign={0}
+ label={`${update.name} (${update.version.old} -> ${update.version.new})\n ${update.description}`}
+ />
+ </button>
+ );
+};
+
+const Repo = (repo: IRepo) => {
+ const expanded = Variable(false);
+
+ return (
+ <box vertical className="repo">
+ <button className="wrapper" cursor="pointer" onClicked={() => expanded.set(!expanded.get())}>
+ <box className="header">
+ <label className="icon" label={repo.icon} />
+ <label label={`${repo.name} (${repo.updates.length})`} />
+ <box hexpand />
+ <label className="icon" label={bind(expanded).as(e => (e ? "expand_less" : "expand_more"))} />
+ </box>
+ </button>
+ <revealer
+ revealChild={bind(expanded)}
+ transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
+ transitionDuration={200}
+ >
+ <box vertical className="list">
+ {repo.updates.map(Update)}
+ </box>
+ </revealer>
+ </box>
+ );
+};
+
+const List = () => (
+ <box vertical valign={Gtk.Align.START} className="repos">
+ {bind(Updates.get_default(), "updateData").as(d => d.repos.map(Repo))}
+ </box>
+);
+
+export default () => (
+ <PopupWindow name="updates">
+ <box vertical className="updates">
+ <box className="header">
+ <label label={bind(Updates.get_default(), "numUpdates").as(n => `${n} update${n === 1 ? "" : "s"}`)} />
+ <box hexpand />
+ <button
+ cursor="pointer"
+ onClicked={() =>
+ execAsync("uwsm app -T -- yay")
+ .then(() => Updates.get_default().getUpdates())
+ // Ignore errors
+ .catch(() => {})
+ }
+ label="Update all"
+ />
+ <button cursor="pointer" onClicked={() => Updates.get_default().getUpdates()} label="Reload" />
+ </box>
+ <stack
+ transitionType={Gtk.StackTransitionType.CROSSFADE}
+ transitionDuration={150}
+ shown={bind(Updates.get_default(), "numUpdates").as(n => (n > 0 ? "list" : "empty"))}
+ >
+ <box vertical valign={Gtk.Align.CENTER} name="empty">
+ <label className="icon" label="deployed_code_history" />
+ <label label="All packages up to date!" />
+ </box>
+ <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
+ <List />
+ </scrollable>
+ </stack>
+ </box>
+ </PopupWindow>
+);
diff --git a/src/services/updates.ts b/src/services/updates.ts
index 5bb6bd1..91c9e21 100644
--- a/src/services/updates.ts
+++ b/src/services/updates.ts
@@ -1,19 +1,25 @@
import { execAsync, GLib, GObject, property, readFileAsync, register, writeFileAsync } from "astal";
import { updates as config } from "../../config";
-interface Update {
- name: string;
+export interface Update {
full: string;
+ name: string;
+ description: string;
+ url: string;
+ version: {
+ old: string;
+ new: string;
+ };
}
-interface Repo {
+export interface Repo {
repo?: string[];
updates: Update[];
icon: string;
name: string;
}
-interface Data {
+export interface Data {
cached?: boolean;
repos: Repo[];
errors: string[];
@@ -40,7 +46,7 @@ export default class Updates extends GObject.Object {
}
@property(Object)
- get data() {
+ get updateData() {
return this.#data;
}
@@ -56,15 +62,29 @@ export default class Updates extends GObject.Object {
async #updateFromCache() {
this.#data = JSON.parse(await readFileAsync(this.#cachePath));
- this.notify("data");
+ this.notify("update-data");
this.notify("list");
this.notify("num-updates");
}
- async #getRepo(repo: string) {
+ 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
+ );
+ }
+
getUpdates() {
// Return if already getting updates
if (this.#loading) return;
@@ -80,15 +100,15 @@ export default class Updates extends GObject.Object {
// 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("core"), updates: [], icon: "hub", name: "Core repository" },
{
- repo: await this.#getRepo("extra"),
+ repo: await this.getRepo("extra"),
updates: [],
icon: "add_circle",
name: "Extra repository",
},
{
- repo: await this.#getRepo("multilib"),
+ repo: await this.getRepo("multilib"),
updates: [],
icon: "account_tree",
name: "Multilib repository",
@@ -98,7 +118,7 @@ export default class Updates extends GObject.Object {
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 });
+ 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);
@@ -110,7 +130,7 @@ export default class Updates extends GObject.Object {
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 });
+ else aur.updates.push(await this.constructUpdate(update));
}
if (aur.updates.length > 0) data.repos.push(aur);
@@ -122,7 +142,7 @@ export default class Updates extends GObject.Object {
// Cache and set
writeFileAsync(this.#cachePath, JSON.stringify({ cached: true, ...data })).catch(console.error);
this.#data = data;
- this.notify("data");
+ this.notify("update-data");
this.notify("list");
this.notify("num-updates");
}
diff --git a/src/utils/widgets.ts b/src/utils/widgets.ts
index 08f9740..64325a0 100644
--- a/src/utils/widgets.ts
+++ b/src/utils/widgets.ts
@@ -1,5 +1,5 @@
-import { Binding } from "astal";
-import { Astal, Widget } from "astal/gtk3";
+import { Binding, register } from "astal";
+import { Astal, astalify, Gtk, Widget, type ConstructProps } from "astal/gtk3";
import AstalHyprland from "gi://AstalHyprland";
export const setupCustomTooltip = (self: any, text: string | Binding<string>) => {
@@ -43,3 +43,10 @@ export const setupCustomTooltip = (self: any, text: string | Binding<string>) =>
export const setupChildClickthrough = (self: any) =>
self.connect("size-allocate", () => self.get_window()?.set_child_input_shapes());
+
+@register()
+export class MenuItem extends astalify(Gtk.MenuItem) {
+ constructor(props: ConstructProps<MenuItem, Gtk.MenuItem.ConstructorProps, { onActivate: [] }>) {
+ super(props as any);
+ }
+}