diff options
| author | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-01-16 22:29:13 +1100 |
|---|---|---|
| committer | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-01-16 22:29:13 +1100 |
| commit | 9e32cd4b61b7a22554d1ac046d685a916a926f3f (patch) | |
| tree | 628a57f8375630c35b4002831a3fa61534913ea4 /src | |
| parent | notifications: empty text (diff) | |
| download | caelestia-shell-9e32cd4b61b7a22554d1ac046d685a916a926f3f.tar.gz caelestia-shell-9e32cd4b61b7a22554d1ac046d685a916a926f3f.tar.bz2 caelestia-shell-9e32cd4b61b7a22554d1ac046d685a916a926f3f.zip | |
updates: make popup window
Diffstat (limited to 'src')
| -rw-r--r-- | src/modules/bar.tsx | 29 | ||||
| -rw-r--r-- | src/modules/updates.tsx | 102 | ||||
| -rw-r--r-- | src/services/updates.ts | 46 | ||||
| -rw-r--r-- | src/utils/widgets.ts | 11 |
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); + } +} |