diff --git a/config/hypr/Scripts/ags-package-runner.py b/config/hypr/Scripts/ags-package-runner.py new file mode 100755 index 0000000..312a558 --- /dev/null +++ b/config/hypr/Scripts/ags-package-runner.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +import json +import os +import pty +import select +import sys + + +def emit(event_type, **payload): + print(json.dumps({"type": event_type, **payload}), flush=True) + + +def handle_message(pid, fd, raw_line): + try: + message = json.loads(raw_line.decode("utf-8", "replace")) + except json.JSONDecodeError: + return + + if message.get("type") == "input": + os.write(fd, str(message.get("data", "")).encode("utf-8", "replace")) + elif message.get("type") == "signal": + os.kill(pid, int(message.get("signal", 15))) + + +def main(): + if "--" not in sys.argv: + emit("exit", code=2, signaled=False) + return 2 + + command = sys.argv[sys.argv.index("--") + 1 :] + if not command: + emit("exit", code=2, signaled=False) + return 2 + + pid, fd = pty.fork() + if pid == 0: + os.execvp(command[0], command) + + emit("start", pid=pid) + stdin_fd = sys.stdin.fileno() + open_fds = [fd, stdin_fd] + stdin_buffer = b"" + + while open_fds: + readable, _, _ = select.select(open_fds, [], [], 0.2) + + if fd in readable: + try: + data = os.read(fd, 4096) + except OSError: + data = b"" + + if data: + emit("out", data=data.decode("utf-8", "replace")) + else: + open_fds.remove(fd) + + if stdin_fd in readable: + try: + chunk = os.read(stdin_fd, 4096) + except OSError: + chunk = b"" + + if not chunk: + open_fds.remove(stdin_fd) + continue + + stdin_buffer += chunk + while b"\n" in stdin_buffer: + line, stdin_buffer = stdin_buffer.split(b"\n", 1) + handle_message(pid, fd, line) + + try: + finished_pid, status = os.waitpid(pid, os.WNOHANG) + except ChildProcessError: + break + + if finished_pid == pid: + if os.WIFSIGNALED(status): + emit("exit", code=os.WTERMSIG(status), signaled=True) + return 128 + os.WTERMSIG(status) + + code = os.WEXITSTATUS(status) + emit("exit", code=code, signaled=False) + return code + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/config/hypr/Scripts/dev-menu.sh b/config/hypr/Scripts/dev-menu.sh index ba82c5a..341f591 100755 --- a/config/hypr/Scripts/dev-menu.sh +++ b/config/hypr/Scripts/dev-menu.sh @@ -322,11 +322,41 @@ docker_menu() { esac } +launch_codex() { + if ! command -v kitty >/dev/null 2>&1; then + notify "kitty ist nicht installiert." + return 0 + fi + + if ! command -v codex >/dev/null 2>&1; then + notify "codex ist nicht installiert." + return 0 + fi + + kitty --title "Codex" sh -lc 'cd "$HOME" && exec codex' >/dev/null 2>&1 & +} + +launch_opencode() { + if ! command -v kitty >/dev/null 2>&1; then + notify "kitty ist nicht installiert." + return 0 + fi + + if ! command -v opencode >/dev/null 2>&1; then + notify "opencode ist nicht installiert." + return 0 + fi + + kitty --title "opencode" sh -lc 'cd "$HOME" && exec opencode' >/dev/null 2>&1 & +} + choice="$( printf '%s\n' \ "๐Ÿ“ Projekt Management" \ "๓ฐŒ˜ Homelab Controlcenter" \ "๐Ÿณ Docker Control" \ + "๓ฐšฉ Codex" \ + "๓ฐšฉ opencode" \ "๎ž• Terminal" \ "๏ป Projektordner" \ "๎œŒ VS Code / Codium" \ @@ -344,6 +374,12 @@ case "$choice" in *"Docker Control"*) docker_menu ;; + *"Codex"*) + launch_codex + ;; + *"opencode"*) + launch_opencode + ;; *"Terminal"*) kitty ;; diff --git a/config/hypr/Scripts/widget-panel.sh b/config/hypr/Scripts/widget-panel.sh new file mode 100755 index 0000000..a38c6b8 --- /dev/null +++ b/config/hypr/Scripts/widget-panel.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +HYPR_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" +export HYPR_DIR + +notify() { + notify-send "Widgetbereich" "$1" >/dev/null 2>&1 || true +} + +if ! command -v ags >/dev/null 2>&1; then + notify "ags ist nicht installiert." + exit 1 +fi + +mapfile -t AGS_INSTANCES < <(ags list 2>/dev/null || true) +for INSTANCE in "${AGS_INSTANCES[@]}"; do + if [[ "$INSTANCE" == "widget-panel" ]]; then + ags toggle widget-panel --instance widget-panel >/dev/null 2>&1 || true + exit 0 + fi +done + +cd "$HYPR_DIR" +nohup ags run "$HYPR_DIR/ags/widget-panel.tsx" >/dev/null 2>&1 & diff --git a/config/hypr/ags/package-manager.css b/config/hypr/ags/package-manager.css index 49e4135..c90d869 100644 --- a/config/hypr/ags/package-manager.css +++ b/config/hypr/ags/package-manager.css @@ -94,6 +94,42 @@ entry:focus { background: rgba(40, 40, 55, 0.48); } +.operation-panel { + min-height: 220px; + padding: 10px; + border: 1px solid rgba(205, 214, 244, 0.12); + border-radius: 8px; + background: rgba(10, 10, 16, 0.58); +} + +.operation-header, +.operation-input-row { + min-height: 34px; +} + +.operation-title { + color: #00ff9c; + font-weight: 800; +} + +.operation-scroll { + min-height: 142px; + border: 1px solid rgba(205, 214, 244, 0.10); + border-radius: 8px; + background: rgba(4, 4, 8, 0.72); +} + +.operation-output { + padding: 10px; + color: #cdd6f4; + font-family: "JetBrainsMono Nerd Font", monospace; + font-size: 12px; +} + +.danger { + color: #f38ba8; +} + .results-scroll { min-height: 420px; } diff --git a/config/hypr/ags/package-manager.tsx b/config/hypr/ags/package-manager.tsx index 5efdd62..f8b6b37 100644 --- a/config/hypr/ags/package-manager.tsx +++ b/config/hypr/ags/package-manager.tsx @@ -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 ; + } + + return ( + + + + + ); +} + +function hidePanel() { + const win = panelWindow || app.get_window("widget-panel"); + win?.set_visible(false); +} + +function PanelContent() { + return ( + + + + + + + + + + + ); +} + +function panelLayout() { + const geometry = app.monitors[0]?.get_geometry(); + const screenWidth = geometry?.width || 1280; + const screenHeight = geometry?.height || 720; + + return { + width: Math.round(clamp(screenWidth * 0.28, 360, 460)), + height: Math.round(clamp(screenHeight - 96, 560, 900)), + }; +} + +function WidgetPanelWindow() { + const layout = panelLayout(); + + return ( + + { + if (keyval === ESC_KEYVAL) { + hidePanel(); + return true; + } + return false; + }} /> + + + + + + + ); +} + +function rebuild() { + const win = app.get_window("widget-panel"); + if (!win) { + return; + } + + disposeRebuild?.(); + const layout = panelLayout(); + createRoot(dispose => { + disposeRebuild = dispose; + win.set_child( + + + + + as Gtk.Widget, + ); + }); +} + +function startTimer() { + if (timerStarted) { + return; + } + + timerStarted = true; + updateSystem(); + updateWeather(true); + + GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, REFRESH_SECONDS, () => { + updateSystem(); + updateWeather(); + rebuild(); + return GLib.SOURCE_CONTINUE; + }); +} + +app.start({ + instanceName: "widget-panel", + css: themeCss(activeTheme()), + main() { + panelWindow = WidgetPanelWindow() as Gtk.Window; + app.add_window(panelWindow); + if (!START_HIDDEN) { + panelWindow.present(); + } + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + startTimer(); + rebuild(); + return GLib.SOURCE_REMOVE; + }); + }, +}); diff --git a/config/hypr/hyprland.conf b/config/hypr/hyprland.conf index c3591a6..3edd8af 100644 --- a/config/hypr/hyprland.conf +++ b/config/hypr/hyprland.conf @@ -47,6 +47,7 @@ $menu = wofi exec-once = sh -c 'if command -v awww-daemon >/dev/null 2>&1; then awww-daemon; elif command -v swww-daemon >/dev/null 2>&1; then swww-daemon; else hyprpaper; fi' exec-once = waybar exec-once = swaync +exec-once = env WIDGET_PANEL_START_HIDDEN=1 ~/.config/hypr/Scripts/widget-panel.sh ############################# ### ENVIRONMENT VARIABLES ### @@ -204,7 +205,8 @@ bind = $mainMod, E, exec, $fileManager bind = $mainMod, N, exec, swaync-client -t bind = $mainMod, V, togglefloating, bind = $mainMod, R, exec, $menu -bind = $mainMod, W, exec, ~/.config/hypr/Scripts/ags-switcher.sh wallpaper +bind = $mainMod, W, exec, ~/.config/hypr/Scripts/widget-panel.sh +bind = $mainMod SHIFT, W, exec, ~/.config/hypr/Scripts/ags-switcher.sh wallpaper bind = $mainMod, P, exec, ~/.config/hypr/Scripts/power-menu.py bind = $mainMod, L, exec, hyprlock bind = $mainMod SHIFT, T, exec, ~/.config/hypr/Scripts/ags-switcher.sh theme