diff --git a/config/hypr/Scripts/package-manager.sh b/config/hypr/Scripts/package-manager.sh new file mode 100755 index 0000000..9e51134 --- /dev/null +++ b/config/hypr/Scripts/package-manager.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +HYPR_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" + +notify() { + notify-send "Pakete" "$1" >/dev/null 2>&1 || true +} + +if ! command -v ags >/dev/null 2>&1; then + notify "ags ist nicht installiert." + exit 1 +fi + +cd "$HYPR_DIR" +ags quit --instance package-manager >/dev/null 2>&1 || true +exec ags run "$HYPR_DIR/ags/package-manager.tsx" diff --git a/config/hypr/Scripts/system-menu.sh b/config/hypr/Scripts/system-menu.sh index 6ee92ff..660988b 100755 --- a/config/hypr/Scripts/system-menu.sh +++ b/config/hypr/Scripts/system-menu.sh @@ -40,6 +40,7 @@ choice="$( printf '%s\n' \ "󰑓 Hyprland neu laden" \ "󰌢 Waybar neu starten" \ + "󰏖 Paket Installation / Updates" \ "󰗽 Bildschirm heller" \ "󰗾 Bildschirm dunkler" \ "󰍃 Session beenden" | @@ -53,6 +54,9 @@ case "$choice" in *"Waybar neu starten"*) restart_waybar ;; + *"Paket Installation / Updates"*) + "$SCRIPT_DIR/package-manager.sh" + ;; *"Bildschirm heller"*) if command -v brightnessctl >/dev/null 2>&1; then brightnessctl -e4 -n2 set 5%+ diff --git a/config/hypr/ags/package-manager.css b/config/hypr/ags/package-manager.css new file mode 100644 index 0000000..49e4135 --- /dev/null +++ b/config/hypr/ags/package-manager.css @@ -0,0 +1,144 @@ +* { + all: unset; + font-family: "JetBrainsMono Nerd Font", "Noto Sans", sans-serif; + font-size: 13px; +} + +.package-window { + background: transparent; +} + +.package-panel { + min-width: 940px; + min-height: 620px; + border: 1px solid rgba(205, 214, 244, 0.16); + border-radius: 16px; + background: rgba(20, 20, 30, 0.97); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45); + color: #cdd6f4; + padding: 18px; +} + +.header { + min-height: 42px; +} + +.title { + color: #00ff9c; + font-size: 21px; + font-weight: 800; +} + +.subtitle, +.muted, +.package-meta { + color: #a6adc8; + font-size: 12px; +} + +entry { + padding: 10px 12px; + border: 1px solid rgba(205, 214, 244, 0.14); + border-radius: 10px; + background: rgba(40, 40, 55, 0.82); + color: #cdd6f4; +} + +entry:focus { + border-color: rgba(0, 255, 156, 0.78); +} + +.button, +.tool-button, +.icon-button { + padding: 8px 10px; + border-radius: 8px; + background: rgba(40, 40, 55, 0.82); + color: #cdd6f4; +} + +.button:hover, +.button:focus, +.tool-button:hover, +.tool-button:focus, +.icon-button:hover, +.icon-button:focus { + background: rgba(0, 255, 156, 0.22); +} + +.primary, +.tool-button.active { + background: rgba(0, 255, 156, 0.24); + color: #00ff9c; +} + +.close { + color: #f38ba8; +} + +.icon-button { + min-width: 34px; + min-height: 34px; + padding: 0; +} + +.toolbar { + min-height: 38px; +} + +.status-strip { + min-height: 30px; + padding: 7px 10px; + border: 1px solid rgba(205, 214, 244, 0.10); + border-radius: 8px; + background: rgba(40, 40, 55, 0.48); +} + +.results-scroll { + min-height: 420px; +} + +.package-row { + min-height: 76px; + padding: 10px; + border: 1px solid rgba(205, 214, 244, 0.10); + border-radius: 8px; + background: rgba(40, 40, 55, 0.62); +} + +.package-row:hover { + border-color: rgba(0, 255, 156, 0.35); + background: rgba(40, 40, 55, 0.82); +} + +.package-name { + color: #cdd6f4; + font-weight: 800; +} + +.package-desc { + color: #cdd6f4; +} + +.repo-pill, +.installed-pill { + padding: 4px 8px; + border-radius: 999px; + font-size: 11px; + background: rgba(116, 199, 236, 0.16); + color: #89dceb; +} + +.installed-pill { + background: rgba(0, 255, 156, 0.16); + color: #00ff9c; +} + +.empty { + min-height: 180px; + color: #a6adc8; +} + +.error { + color: #f38ba8; +} diff --git a/config/hypr/ags/package-manager.tsx b/config/hypr/ags/package-manager.tsx new file mode 100644 index 0000000..5efdd62 --- /dev/null +++ b/config/hypr/ags/package-manager.tsx @@ -0,0 +1,314 @@ +import app from "ags/gtk4/app"; +import { Astal, Gtk } from "ags/gtk4"; +import { execAsync } from "ags/process"; +import css from "./package-manager.css"; + +const WINDOW_MARGIN_TOP = 48; +const ESC_KEYVAL = 65307; + +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 = ""; + +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 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 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 installPackage(pkg: PackageResult) { + const command = helper === "pacman" + ? `sudo pacman -S ${shQuote(pkg.name)}` + : `paru -S ${shQuote(pkg.name)}`; + + notify(`Installation gestartet: ${pkg.name}`); + runShell(terminalCommand(command)).catch(error => { + console.error(error); + notify("Kein Terminal fuer Paketinstallation gefunden."); + }); +} + +function updateSystem() { + const command = helper === "pacman" + ? "sudo pacman -Syu" + : "paru -Syu"; + + notify(`Update gestartet mit ${helper}.`); + runShell(terminalCommand(command)).catch(error => { + console.error(error); + notify("Kein Terminal fuer Updates gefunden."); + }); +} + +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(); + }} + /> +