updates: make popup window
This commit is contained in:
parent
e539fe0bab
commit
9e32cd4b61
8 changed files with 238 additions and 29 deletions
2
app.tsx
2
app.tsx
|
|
@ -5,6 +5,7 @@ import Launcher from "./src/modules/launcher";
|
|||
import Notifications from "./src/modules/notifications";
|
||||
import NotifPopups from "./src/modules/notifpopups";
|
||||
import Osds from "./src/modules/osds";
|
||||
import Updates from "./src/modules/updates";
|
||||
import Monitors from "./src/services/monitors";
|
||||
import Players from "./src/services/players";
|
||||
|
||||
|
|
@ -26,6 +27,7 @@ App.start({
|
|||
<Osds />;
|
||||
Monitors.get_default().forEach(m => <Bar monitor={m} />);
|
||||
<Notifications />;
|
||||
<Updates />;
|
||||
|
||||
console.log("Caelestia started");
|
||||
},
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@
|
|||
color: scheme.$rosewater;
|
||||
}
|
||||
|
||||
.updates {
|
||||
.pkg-updates {
|
||||
color: scheme.$blue;
|
||||
}
|
||||
|
||||
|
|
|
|||
74
scss/updates.scss
Normal file
74
scss/updates.scss
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
@use "sass:color";
|
||||
@use "scheme";
|
||||
@use "lib";
|
||||
@use "font";
|
||||
|
||||
.updates {
|
||||
@include lib.rounded(8);
|
||||
@include lib.border(scheme.$blue, 0.4, 2);
|
||||
@include lib.shadow;
|
||||
@include font.mono;
|
||||
|
||||
min-width: lib.s(600);
|
||||
min-height: lib.s(400);
|
||||
background-color: scheme.$base;
|
||||
color: scheme.$blue;
|
||||
padding: lib.s(10) lib.s(12);
|
||||
font-size: lib.s(14);
|
||||
|
||||
.wrapper {
|
||||
@include lib.element-decel;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: color.mix(scheme.$blue, scheme.$base, 80%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: color.mix(scheme.$blue, scheme.$base, 60%);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
@include lib.spacing(8);
|
||||
|
||||
padding: 0 lib.s(5);
|
||||
margin-bottom: lib.s(8);
|
||||
font-size: lib.s(15);
|
||||
|
||||
button {
|
||||
@include lib.rounded(5);
|
||||
@include lib.element-decel;
|
||||
|
||||
padding: lib.s(3) lib.s(8);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: scheme.$surface0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: scheme.$surface1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: lib.s(32);
|
||||
}
|
||||
|
||||
.repos {
|
||||
@include lib.spacing($vertical: true);
|
||||
|
||||
.repo {
|
||||
.header {
|
||||
font-size: lib.s(16);
|
||||
}
|
||||
|
||||
.list {
|
||||
color: scheme.$text;
|
||||
margin-left: lib.s(12);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
102
src/modules/updates.tsx
Normal file
102
src/modules/updates.tsx
Normal file
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
@use "scss/launcher";
|
||||
@use "scss/osds";
|
||||
@use "scss/notifications";
|
||||
@use "scss/updates";
|
||||
|
||||
* {
|
||||
all: unset; // Remove GTK theme styles
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue