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 Notifications from "./src/modules/notifications";
|
||||||
import NotifPopups from "./src/modules/notifpopups";
|
import NotifPopups from "./src/modules/notifpopups";
|
||||||
import Osds from "./src/modules/osds";
|
import Osds from "./src/modules/osds";
|
||||||
|
import Updates from "./src/modules/updates";
|
||||||
import Monitors from "./src/services/monitors";
|
import Monitors from "./src/services/monitors";
|
||||||
import Players from "./src/services/players";
|
import Players from "./src/services/players";
|
||||||
|
|
||||||
|
|
@ -26,6 +27,7 @@ App.start({
|
||||||
<Osds />;
|
<Osds />;
|
||||||
Monitors.get_default().forEach(m => <Bar monitor={m} />);
|
Monitors.get_default().forEach(m => <Bar monitor={m} />);
|
||||||
<Notifications />;
|
<Notifications />;
|
||||||
|
<Updates />;
|
||||||
|
|
||||||
console.log("Caelestia started");
|
console.log("Caelestia started");
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@
|
||||||
color: scheme.$rosewater;
|
color: scheme.$rosewater;
|
||||||
}
|
}
|
||||||
|
|
||||||
.updates {
|
.pkg-updates {
|
||||||
color: scheme.$blue;
|
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();
|
network.wifi.scan();
|
||||||
execAsync(
|
execAsync(
|
||||||
"uwsm app -- foot -T nmtui fish -c 'sleep .1; set -e COLORTERM; TERM=xterm-old nmtui connect'"
|
"uwsm app -- foot -T nmtui fish -c 'sleep .1; set -e COLORTERM; TERM=xterm-old nmtui connect'"
|
||||||
).catch(err => {
|
).catch(() => {}); // Ignore errors
|
||||||
// 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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
setup={self => {
|
setup={self => {
|
||||||
|
|
@ -372,8 +365,16 @@ const StatusIcons = () => (
|
||||||
);
|
);
|
||||||
|
|
||||||
const PkgUpdates = () => (
|
const PkgUpdates = () => (
|
||||||
<box
|
<button
|
||||||
className="module updates"
|
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 =>
|
setup={self =>
|
||||||
setupCustomTooltip(
|
setupCustomTooltip(
|
||||||
self,
|
self,
|
||||||
|
|
@ -381,9 +382,11 @@ const PkgUpdates = () => (
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<box className="module pkg-updates">
|
||||||
<label className="icon" label="download" />
|
<label className="icon" label="download" />
|
||||||
<label label={bind(Updates.get_default(), "numUpdates").as(String)} />
|
<label label={bind(Updates.get_default(), "numUpdates").as(String)} />
|
||||||
</box>
|
</box>
|
||||||
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
const Unread = () => {
|
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 { execAsync, GLib, GObject, property, readFileAsync, register, writeFileAsync } from "astal";
|
||||||
import { updates as config } from "../../config";
|
import { updates as config } from "../../config";
|
||||||
|
|
||||||
interface Update {
|
export interface Update {
|
||||||
name: string;
|
|
||||||
full: string;
|
full: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
url: string;
|
||||||
|
version: {
|
||||||
|
old: string;
|
||||||
|
new: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Repo {
|
export interface Repo {
|
||||||
repo?: string[];
|
repo?: string[];
|
||||||
updates: Update[];
|
updates: Update[];
|
||||||
icon: string;
|
icon: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Data {
|
export interface Data {
|
||||||
cached?: boolean;
|
cached?: boolean;
|
||||||
repos: Repo[];
|
repos: Repo[];
|
||||||
errors: string[];
|
errors: string[];
|
||||||
|
|
@ -40,7 +46,7 @@ export default class Updates extends GObject.Object {
|
||||||
}
|
}
|
||||||
|
|
||||||
@property(Object)
|
@property(Object)
|
||||||
get data() {
|
get updateData() {
|
||||||
return this.#data;
|
return this.#data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,15 +62,29 @@ export default class Updates extends GObject.Object {
|
||||||
|
|
||||||
async #updateFromCache() {
|
async #updateFromCache() {
|
||||||
this.#data = JSON.parse(await readFileAsync(this.#cachePath));
|
this.#data = JSON.parse(await readFileAsync(this.#cachePath));
|
||||||
this.notify("data");
|
this.notify("update-data");
|
||||||
this.notify("list");
|
this.notify("list");
|
||||||
this.notify("num-updates");
|
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");
|
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() {
|
getUpdates() {
|
||||||
// Return if already getting updates
|
// Return if already getting updates
|
||||||
if (this.#loading) return;
|
if (this.#loading) return;
|
||||||
|
|
@ -80,15 +100,15 @@ export default class Updates extends GObject.Object {
|
||||||
// Pacman updates (checkupdates)
|
// Pacman updates (checkupdates)
|
||||||
if (pacman.status === "fulfilled") {
|
if (pacman.status === "fulfilled") {
|
||||||
const repos: Repo[] = [
|
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: [],
|
updates: [],
|
||||||
icon: "add_circle",
|
icon: "add_circle",
|
||||||
name: "Extra repository",
|
name: "Extra repository",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
repo: await this.#getRepo("multilib"),
|
repo: await this.getRepo("multilib"),
|
||||||
updates: [],
|
updates: [],
|
||||||
icon: "account_tree",
|
icon: "account_tree",
|
||||||
name: "Multilib repository",
|
name: "Multilib repository",
|
||||||
|
|
@ -98,7 +118,7 @@ export default class Updates extends GObject.Object {
|
||||||
for (const update of pacman.value.split("\n")) {
|
for (const update of pacman.value.split("\n")) {
|
||||||
const pkg = update.split(" ")[0];
|
const pkg = update.split(" ")[0];
|
||||||
for (const repo of repos)
|
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);
|
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")) {
|
for (const update of yay.value.split("\n")) {
|
||||||
if (/^\s*->/.test(update)) data.errors.push(update); // Error
|
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);
|
if (aur.updates.length > 0) data.repos.push(aur);
|
||||||
|
|
@ -122,7 +142,7 @@ export default class Updates extends GObject.Object {
|
||||||
// Cache and set
|
// Cache and set
|
||||||
writeFileAsync(this.#cachePath, JSON.stringify({ cached: true, ...data })).catch(console.error);
|
writeFileAsync(this.#cachePath, JSON.stringify({ cached: true, ...data })).catch(console.error);
|
||||||
this.#data = data;
|
this.#data = data;
|
||||||
this.notify("data");
|
this.notify("update-data");
|
||||||
this.notify("list");
|
this.notify("list");
|
||||||
this.notify("num-updates");
|
this.notify("num-updates");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Binding } from "astal";
|
import { Binding, register } from "astal";
|
||||||
import { Astal, Widget } from "astal/gtk3";
|
import { Astal, astalify, Gtk, Widget, type ConstructProps } from "astal/gtk3";
|
||||||
import AstalHyprland from "gi://AstalHyprland";
|
import AstalHyprland from "gi://AstalHyprland";
|
||||||
|
|
||||||
export const setupCustomTooltip = (self: any, text: string | Binding<string>) => {
|
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) =>
|
export const setupChildClickthrough = (self: any) =>
|
||||||
self.connect("size-allocate", () => self.get_window()?.set_child_input_shapes());
|
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/launcher";
|
||||||
@use "scss/osds";
|
@use "scss/osds";
|
||||||
@use "scss/notifications";
|
@use "scss/notifications";
|
||||||
|
@use "scss/updates";
|
||||||
|
|
||||||
* {
|
* {
|
||||||
all: unset; // Remove GTK theme styles
|
all: unset; // Remove GTK theme styles
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue