Sync current Hyprland widgets

This commit is contained in:
Pascal
2026-04-28 18:25:20 +02:00
parent b4e4081f25
commit 25f0e3653e
8 changed files with 925 additions and 19 deletions

View File

@@ -1,10 +1,12 @@
import app from "ags/gtk4/app";
import { Astal, Gtk } from "ags/gtk4";
import { execAsync } from "ags/process";
import { execAsync, subprocess, Process } from "ags/process";
import GLib from "gi://GLib";
import css from "./package-manager.css";
const WINDOW_MARGIN_TOP = 48;
const ESC_KEYVAL = 65307;
const RUNNER = `${GLib.get_home_dir()}/.config/hypr/Scripts/ags-package-runner.py`;
type Helper = "pacman" | "paru";
@@ -22,6 +24,12 @@ let results: PackageResult[] = [];
let busy = false;
let statusMessage = "Suchbegriff eingeben und Enter druecken.";
let errorMessage = "";
let activeProcess: Process | null = null;
let operationTitle = "";
let operationOutput = "";
let operationInput = "";
let secretInput = false;
let lastOutput = "";
function shQuote(value: string) {
return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -40,6 +48,17 @@ function setBusy(value: boolean) {
rebuild();
}
function appendOperationOutput(data: string) {
operationOutput = `${operationOutput}${data}`.slice(-30000);
lastOutput = data;
secretInput = /(\[sudo\].*password|passwort|password).*:/i.test(data);
rebuild();
}
function commandLabel(command: string[]) {
return command.map(shQuote).join(" ");
}
function helperAvailable(nextHelper: Helper) {
return runShell(`command -v ${nextHelper} >/dev/null 2>&1`);
}
@@ -109,33 +128,111 @@ function searchPackages() {
});
}
function terminalCommand(command: string) {
const hold = `${command}; printf '\\n'; read -r -p 'Enter zum Schliessen... ' _`;
return `kitty --title ${shQuote("Paketmanager")} sh -lc ${shQuote(hold)} || alacritty -e sh -lc ${shQuote(hold)} || foot sh -lc ${shQuote(hold)} || xterm -e sh -lc ${shQuote(hold)}`;
function startOperation(title: string, command: string[]) {
if (activeProcess) {
statusMessage = "Es laeuft bereits ein Paketprozess.";
rebuild();
return;
}
operationTitle = title;
operationOutput = `$ ${commandLabel(command)}\n\n`;
operationInput = "";
errorMessage = "";
statusMessage = `${title} laeuft in AGS.`;
busy = true;
const process = subprocess(
[RUNNER, "--", ...command],
line => {
try {
const event = JSON.parse(line);
if (event.type === "out") {
appendOperationOutput(event.data || "");
} else if (event.type === "exit") {
const suffix = event.signaled ? `Signal ${event.code}` : `Exit ${event.code}`;
appendOperationOutput(`\n[${suffix}]\n`);
}
} catch {
appendOperationOutput(`${line}\n`);
}
},
line => appendOperationOutput(`${line}\n`),
);
activeProcess = process;
process.connect("exit", (_, code: number, signaled: boolean) => {
activeProcess = null;
busy = false;
secretInput = false;
statusMessage = signaled
? `${title} wurde beendet.`
: code === 0
? `${title} abgeschlossen.`
: `${title} fehlgeschlagen (Exit ${code}).`;
notify(statusMessage);
rebuild();
});
rebuild();
}
function sendOperationInput(value = operationInput, allowEmpty = false) {
if (!activeProcess || (!allowEmpty && value.length === 0)) {
return;
}
activeProcess.writeAsync(`${JSON.stringify({ type: "input", data: `${value}\n` })}\n`).catch(error => {
console.error(error);
errorMessage = "Eingabe konnte nicht gesendet werden.";
rebuild();
});
operationInput = "";
secretInput = false;
rebuild();
}
function yesAnswer() {
return /[\[(][YyJj]\/[Nn][\])]/.test(lastOutput) ? "" : "y";
}
function sendYesAnswer() {
const answer = yesAnswer();
appendOperationOutput(answer ? `\n> ${answer}\n` : "\n> Enter\n");
sendOperationInput(answer, true);
}
function sendNoAnswer() {
appendOperationOutput("\n> n\n");
sendOperationInput("n");
}
function cancelOperation() {
if (!activeProcess) {
return;
}
activeProcess.writeAsync(`${JSON.stringify({ type: "signal", signal: 15 })}\n`).catch(console.error);
statusMessage = "Abbruch angefordert.";
rebuild();
}
function installPackage(pkg: PackageResult) {
const command = helper === "pacman"
? `sudo pacman -S ${shQuote(pkg.name)}`
: `paru -S ${shQuote(pkg.name)}`;
? ["sudo", "pacman", "-S", pkg.name]
: ["paru", "-S", pkg.name];
notify(`Installation gestartet: ${pkg.name}`);
runShell(terminalCommand(command)).catch(error => {
console.error(error);
notify("Kein Terminal fuer Paketinstallation gefunden.");
});
startOperation(`Installation: ${pkg.name}`, command);
}
function updateSystem() {
const command = helper === "pacman"
? "sudo pacman -Syu"
: "paru -Syu";
? ["sudo", "pacman", "-Syu"]
: ["paru", "-Syu"];
notify(`Update gestartet mit ${helper}.`);
runShell(terminalCommand(command)).catch(error => {
console.error(error);
notify("Kein Terminal fuer Updates gefunden.");
});
startOperation(`Systemupdate mit ${helper}`, command);
}
function HelperButton({ id, label }: { id: Helper; label: string }) {
@@ -169,6 +266,49 @@ function PackageRow({ pkg }: { pkg: PackageResult }) {
);
}
function OperationPanel() {
if (!operationTitle && !operationOutput) {
return <box />;
}
return (
<box class="operation-panel" orientation={Gtk.Orientation.VERTICAL} spacing={8}>
<box class="operation-header" spacing={8}>
<label class="operation-title" xalign={0} hexpand label={operationTitle || "Paketprozess"} />
<button class="button danger" sensitive={Boolean(activeProcess)} onClicked={cancelOperation} label="Abbrechen" />
</box>
<scrolledwindow
class="operation-scroll"
hexpand
vexpand
hscrollbarPolicy={Gtk.PolicyType.AUTOMATIC}
vscrollbarPolicy={Gtk.PolicyType.AUTOMATIC}
>
<label class="operation-output" xalign={0} yalign={0} wrap selectable label={operationOutput || "Warte auf Ausgabe..."} />
</scrolledwindow>
<box class="operation-input-row" spacing={8}>
<entry
hexpand
sensitive={Boolean(activeProcess)}
visibility={!secretInput}
text={operationInput}
placeholderText={secretInput ? "Passwort eingeben..." : "Antwort eingeben..."}
onChanged={entry => {
operationInput = entry.get_text();
}}
onActivate={entry => {
operationInput = entry.get_text();
sendOperationInput();
}}
/>
<button class="button" sensitive={Boolean(activeProcess)} onClicked={sendYesAnswer} label="Ja" />
<button class="button" sensitive={Boolean(activeProcess)} onClicked={sendNoAnswer} label="Nein" />
<button class="button primary" sensitive={Boolean(activeProcess)} onClicked={() => sendOperationInput()} label="Senden" />
</box>
</box>
);
}
function PackageManagerWindow() {
return (
<window
@@ -191,7 +331,7 @@ function PackageManagerWindow() {
<box class="header">
<box orientation={Gtk.Orientation.VERTICAL} hexpand>
<label class="title" xalign={0} label="Paket Installation / Updates" />
<label class="subtitle" xalign={0} label="Pacman und AUR Suche, Installation im Terminal." />
<label class="subtitle" xalign={0} label="Pacman und AUR Suche, Installation direkt in AGS." />
</box>
<button class="icon-button close" tooltipText="Schliessen" onClicked={() => app.quit()}>
<label label="" />
@@ -219,6 +359,7 @@ function PackageManagerWindow() {
<box class="status-strip">
<label class={errorMessage ? "error" : "muted"} xalign={0} hexpand label={errorMessage || statusMessage} />
</box>
<OperationPanel />
<scrolledwindow
class="results-scroll"
hexpand
@@ -253,7 +394,7 @@ function rebuild() {
<box class="header">
<box orientation={Gtk.Orientation.VERTICAL} hexpand>
<label class="title" xalign={0} label="Paket Installation / Updates" />
<label class="subtitle" xalign={0} label="Pacman und AUR Suche, Installation im Terminal." />
<label class="subtitle" xalign={0} label="Pacman und AUR Suche, Installation direkt in AGS." />
</box>
<button class="icon-button close" tooltipText="Schliessen" onClicked={() => app.quit()}>
<label label="" />
@@ -282,6 +423,7 @@ function rebuild() {
<box class="status-strip">
<label class={errorMessage ? "error" : "muted"} xalign={0} hexpand label={errorMessage || statusMessage} />
</box>
<OperationPanel />
<scrolledwindow
class="results-scroll"
hexpand