import app from "ags/gtk4/app"; import { Astal, Gtk } from "ags/gtk4"; 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"; type PackageResult = { repo: string; name: string; version: string; installed: boolean; description: string; }; let helper: Helper = "paru"; let query = ""; 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, `'\\''`)}'`; } function notify(message: string) { execAsync(["notify-send", "Pakete", message]).catch(console.error); } function runShell(command: string) { return execAsync(["bash", "-lc", command]); } function setBusy(value: boolean) { busy = value; 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`); } function parseSearch(output: string) { const parsed: PackageResult[] = []; const lines = output.split("\n"); for (let index = 0; index < lines.length; index += 1) { const header = lines[index]; const match = header.match(/^([^/\s]+)\/([^\s]+)\s+([^\s]+)(?:\s+\[(installed[^\]]*)\])?/); if (!match) { continue; } const descriptionLines: string[] = []; let next = index + 1; while (next < lines.length && /^\s+/.test(lines[next])) { descriptionLines.push(lines[next].trim()); next += 1; } parsed.push({ repo: match[1], name: match[2], version: match[3], installed: Boolean(match[4]), description: descriptionLines.join(" ") || "Keine Beschreibung.", }); index = next - 1; } return parsed.slice(0, 80); } function searchPackages() { const term = query.trim(); if (!term) { results = []; statusMessage = "Suchbegriff fehlt."; rebuild(); return; } setBusy(true); errorMessage = ""; statusMessage = `Suche mit ${helper} nach "${term}"...`; helperAvailable(helper) .then(() => runShell(`${helper} -Ss ${shQuote(term)} 2>/dev/null || true`)) .then(output => { results = parseSearch(output); statusMessage = results.length ? `${results.length} Treffer fuer "${term}" mit ${helper}.` : `Keine Treffer fuer "${term}".`; }) .catch(error => { console.error(error); results = []; errorMessage = `${helper} ist nicht verfuegbar oder die Suche ist fehlgeschlagen.`; statusMessage = "Suche fehlgeschlagen."; }) .finally(() => { setBusy(false); }); } 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", pkg.name] : ["paru", "-S", pkg.name]; notify(`Installation gestartet: ${pkg.name}`); startOperation(`Installation: ${pkg.name}`, command); } function updateSystem() { const command = helper === "pacman" ? ["sudo", "pacman", "-Syu"] : ["paru", "-Syu"]; notify(`Update gestartet mit ${helper}.`); startOperation(`Systemupdate mit ${helper}`, command); } function HelperButton({ id, label }: { id: Helper; label: string }) { return ( { query = entry.get_text(); }} onActivate={entry => { query = entry.get_text(); searchPackages(); }} /> { query = entry.get_text(); }} onActivate={entry => { query = entry.get_text(); searchPackages(); }} />