#!/usr/bin/env python3 """ toolbox — interaktiver Terminal-Tool-Launcher mit Kategorie-Navigation Läuft auf macOS (iTerm2) und Linux (Xubuntu/xfce4-terminal, gnome-terminal, …) """ import subprocess import shutil import sys import os import platform import shlex from dataclasses import dataclass IS_MAC = platform.system() == "Darwin" IS_LINUX = platform.system() == "Linux" CURRENT_DIR = os.getcwd() @dataclass class Tool: category: str name: str description: str command: str binary: str mac: bool = True linux: bool = True TOOLS = [ # --- Git --- Tool("Git", "lazygit", "Git TUI: stagen, committen, pushen, rebasen", "lazygit", "lazygit"), Tool("Git", "gitcheck", "Status aller Repos prüfen", "~/git-check-all.sh", "git"), Tool("Git", "gitsync", "Alle Repos mit Gitea synchronisieren", "~/git-projekte/dotfiles-rene/bin/git-sync-all.sh", "git"), Tool("Git", "git log", "Commit-History (mit delta, side-by-side)", "git log -p", "git"), Tool("Git", "git diff", "Änderungen anzeigen (mit delta)", "git diff", "git"), # --- Navigation & Dateien --- Tool("Dateien", "yazi", "Terminal-Dateimanager (q = quit)", "yazi", "yazi"), Tool("Dateien", "fzf", "Fuzzy-Finder: Dateien und History durchsuchen", "fzf", "fzf"), Tool("Dateien", "ncdu", "Interaktive Festplattennutzung", "ncdu ~", "ncdu"), Tool("Dateien", "duf", "Übersicht freier Speicherplatz", "duf", "duf"), Tool("Dateien", "fd", "Schnelles find (Beispiel: fd .py)", "fd", "fd"), Tool("Dateien", "rg", "Blitzschnelles grep (Beispiel: rg TODO)", "rg", "rg"), # --- Anzeige --- Tool("Anzeige", "bat", "cat mit Syntax-Highlighting", "bat", "bat"), Tool("Anzeige", "eza -la", "Modernes ls mit Details, Farben, Git-Status", "eza -la", "eza"), Tool("Anzeige", "eza -T", "Verzeichnisbaum", "eza -T", "eza"), Tool("Anzeige", "delta", "Schöne Git-Diffs (wird automatisch verwendet)", "git diff HEAD~1 | delta", "delta"), # --- System & Monitoring --- Tool("System", "btop", "Systemmonitor: CPU, RAM, Netz, Prozesse", "btop", "btop"), Tool("System", "fastfetch", "Systeminfo-Übersicht", "fastfetch", "fastfetch"), Tool("System", "temps", "CPU/GPU-Temperatur + Akku (Mac)", "sudo powermetrics -s cpu_power,gpu_power,thermal,battery -i 1000 -n 1", "sudo", linux=False), # --- Netzwerk --- Tool("Netzwerk", "nmap", "Netzwerk-Scanner (Beispiel: nmap 10.47.11.0/24)", "nmap -sn 10.47.11.0/24", "nmap"), # --- Berechnung --- Tool("Rechnen", "units", "Einheitenumrechnung (Beispiel: units 1kWh MJ)", "units", "units"), Tool("Rechnen", "python3", "Python REPL für schnelle Berechnungen", "python3", "python3"), # --- KI --- Tool("KI", "ki-chat", "Offline-KI interaktiver Chat", "ki interactive", "ki"), Tool("KI", "ki-agent", "Offline-KI Agent-Modus: Datei- und Shell-Zugriff", "ki interactive --agent-mode", "ki"), Tool("KI", "ki-commit", "Commit-Message mit KI generieren", "ki commit", "ki"), Tool("KI", "ki-diff", "Git-Diff mit KI erklären lassen", "ki diff", "ki"), # --- Screensaver / Spass --- Tool("Spass", "cmatrix", "Matrix-Regen (q = quit)", "cmatrix -sab", "cmatrix"), Tool("Spass", "asciiquarium", "Aquarium im Terminal (q = quit)", "asciiquarium", "asciiquarium"), Tool("Spass", "pipes.sh", "Animierte Rohre (q = quit)", "pipes.sh -t 0 -p 4", "pipes.sh"), Tool("Spass", "cbonsai", "Wachsender Bonsai-Baum (q = quit)", "cbonsai -l", "cbonsai"), Tool("Spass", "nms", "Sneakers-Entschlüsselungseffekt", "ls -la | nms", "nms"), ] LINUX_ALIASES = { "fd": "fdfind", "bat": "batcat", } _FZF_BASE = [ "fzf", "--ansi", "--height=80%", "--layout=reverse", "--border=rounded", "--color=header:italic:cyan,prompt:green,pointer:green,border:blue", ] def is_available(tool: Tool) -> bool: if IS_MAC and not tool.mac: return False if IS_LINUX and not tool.linux: return False binary = LINUX_ALIASES.get(tool.binary, tool.binary) if IS_LINUX else tool.binary return shutil.which(binary) is not None def resolve_command(cmd: str) -> str: if IS_LINUX: for mac_bin, linux_bin in LINUX_ALIASES.items(): if cmd == mac_bin or cmd.startswith(mac_bin + " "): return linux_bin + cmd[len(mac_bin):] return cmd def open_new_window(command: str) -> None: """Öffnet den Befehl in einem neuen Fenster im aktuellen Verzeichnis.""" full_cmd = f"cd {shlex.quote(CURRENT_DIR)} && {command}" if IS_MAC: escaped = full_cmd.replace("\\", "\\\\").replace('"', '\\"') script = ( 'tell application "iTerm2"\n' ' activate\n' ' set w to (create window with current profile)\n' ' tell current session of w\n' f' write text "{escaped}"\n' ' end tell\n' 'end tell\n' ) subprocess.run(["osascript", "-e", script], capture_output=True) return # Linux: Terminal-Emulator nach Verfügbarkeit bash_cmd = f"{command}; exec bash" candidates = [ ("xfce4-terminal", ["xfce4-terminal", f"--working-directory={CURRENT_DIR}", "-e", f"bash -c {shlex.quote(bash_cmd)}"]), ("gnome-terminal", ["gnome-terminal", f"--working-directory={CURRENT_DIR}", "--", "bash", "-c", bash_cmd]), ("kitty", ["kitty", "--directory", CURRENT_DIR, "bash", "-c", bash_cmd]), ("alacritty", ["alacritty", "--working-directory", CURRENT_DIR, "-e", "bash", "-c", bash_cmd]), ("xterm", ["xterm", "-e", f"bash -c {shlex.quote(full_cmd + '; exec bash')}"]), ] for binary, args in candidates: if shutil.which(binary): subprocess.Popen(args) return os.system(full_cmd) # Fallback: inline def fzf_run(items: list[str], extra_args: list[str]) -> str | None: result = subprocess.run( _FZF_BASE + extra_args, input="\n".join(items), text=True, capture_output=True, ) return result.stdout.strip() if result.returncode == 0 and result.stdout.strip() else None def main() -> None: if not shutil.which("fzf"): print("fzf nicht gefunden – bitte installieren:") print(" macOS: brew install fzf") print(" Linux: sudo apt install fzf") sys.exit(1) available = [t for t in TOOLS if is_available(t)] categories = list(dict.fromkeys(t.category for t in available)) ALL = "★ Alle Tools" # Kategorie-Vorschau: Anzahl verfügbarer Tools pro Kategorie cat_counts = {c: sum(1 for t in available if t.category == c) for c in categories} cat_items = [f"{ALL} ({len(available)})"] + [ f"{c} ({cat_counts[c]})" for c in categories ] # Preview für Kategorien: Tools der markierten Kategorie anzeigen # {1} = Kategoriename (erstes Wort), fzf-Feldreferenz cat_preview = ( "cat << 'CATEOF'\n" # Dummy — wir nutzen awk auf die Eingabeliste ) # Einfacherer Ansatz: kein Preview auf Kategorie-Ebene, nur auf Tool-Ebene while True: # ── Schritt 1: Kategorie wählen ────────────────────────────────────── cat_choice = fzf_run( cat_items, [ "--prompt= Kategorie > ", "--header= toolbox · Kategorie wählen (Esc = beenden)", "--no-preview", ], ) if not cat_choice: sys.exit(0) if cat_choice.startswith(ALL[:3]): # "★ Alle …" filtered, cat_label = available, "Alle Tools" else: cat_label = cat_choice.split(" (")[0] # Name ohne "(N)" filtered = [t for t in available if t.category == cat_label] # ── Schritt 2: Tool wählen ─────────────────────────────────────────── # Felder TAB-getrennt: name \t description \t command tool_lines = [f"{t.name}\t{t.description}\t{t.command}" for t in filtered] # Preview zeigt Beschreibung + Befehl für das markierte Tool. # {2} und {3} sind fzf-Feldreferenzen (TAB-Delimiter), werden von fzf # korrekt gequotet, bevor sie in den Shell-Befehl eingefügt werden. preview = r"printf '\n \033[1m%s\033[0m\n\n \033[33m$\033[0m %s\n' {2} {3}" chosen = fzf_run( tool_lines, [ f"--prompt= {cat_label} > ", f"--header= ‹ {cat_label} (Esc = zurück zur Kategorie)", "--delimiter=\t", "--with-nth=1,2", f"--preview={preview}", "--preview-window=bottom:5:wrap", ], ) if not chosen: continue # Esc → zurück zur Kategorie-Auswahl parts = chosen.split("\t") if len(parts) < 3: continue tool_name, _, tool_cmd = parts[0], parts[1], parts[2] command = resolve_command(tool_cmd) print(f"\n▶ {tool_name} — {command}\n") open_new_window(command) sys.exit(0) if __name__ == "__main__": main()