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 (
+
+
+
+
+
+
+ {
+ operationInput = entry.get_text();
+ }}
+ onActivate={entry => {
+ operationInput = entry.get_text();
+ sendOperationInput();
+ }}
+ />
+
+
+
+
+ );
+}
+
function PackageManagerWindow() {
return (