From 562ebb64f2f5581b25738c781a1f7d613f30c6ce Mon Sep 17 00:00:00 2001 From: rene Date: Fri, 27 Mar 2026 13:58:50 +0100 Subject: [PATCH] toolbox: Kategorie-Navigation, neues Fenster, tools-Symlink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Zweistufiges Menü: Kategorie → Tool (mit Esc zurück) - Preview-Pane zeigt Beschreibung + Befehl beim Navigieren - Öffnet immer in neuem Fenster mit current profile im aktuellen Verzeichnis - macOS: iTerm2 new window, Linux: xfce4-terminal/gnome-terminal/kitty/alacritty - Alias tools=toolbox entfernt (tools läuft jetzt als eigenständiges Binary) Co-Authored-By: Claude Sonnet 4.6 --- bin/toolbox | 299 +++++++++++++++++++++++++++++----------------------- zsh/.zshrc | 3 - 2 files changed, 169 insertions(+), 133 deletions(-) diff --git a/bin/toolbox b/bin/toolbox index d0f7c58..5b50cb8 100755 --- a/bin/toolbox +++ b/bin/toolbox @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -toolbox — interaktiver Terminal-Tool-Launcher +toolbox — interaktiver Terminal-Tool-Launcher mit Kategorie-Navigation Läuft auf macOS (iTerm2) und Linux (Xubuntu/xfce4-terminal, gnome-terminal, …) """ @@ -9,195 +9,234 @@ import shutil import sys import os import platform -from dataclasses import dataclass, field +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 + category: str + name: str description: str - command: str - binary: str # Binary für Verfügbarkeitscheck - new_tab: bool = True # TUI → neuer Tab, Output-Tools → inline - mac: bool = True # auf macOS anzeigen - linux: bool = True # auf Linux anzeigen + 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", new_tab=False), - Tool("Git", "gitsync", "Alle Repos mit Gitea synchronisieren", "~/git-projekte/dotfiles-rene/bin/git-sync-all.sh", "git", new_tab=False), - Tool("Git", "git log", "Commit-History (mit delta, side-by-side)", "git log -p", "git", new_tab=False), - Tool("Git", "git diff", "Änderungen anzeigen (mit delta)", "git diff", "git", new_tab=False), + 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 (Ctrl+T Dateien, Ctrl+R History)", "fzf", "fzf"), - Tool("Dateien", "ncdu", "Interaktive Festplattennutzung", "ncdu ~", "ncdu"), - Tool("Dateien", "duf", "Übersicht freier Speicherplatz", "duf", "duf", new_tab=False), - Tool("Dateien", "fd", "Schnelles find (Beispiel: fd .py)", "fd", "fd", new_tab=False), - Tool("Dateien", "rg", "Blitzschnelles grep (Beispiel: rg TODO)", "rg", "rg", new_tab=False), + 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", new_tab=False), - Tool("Anzeige", "eza -la", "Modernes ls mit Details, Farben, Git-Status", "eza -la", "eza", new_tab=False), - Tool("Anzeige", "eza -T", "Verzeichnisbaum", "eza -T", "eza", new_tab=False), - Tool("Anzeige", "delta", "Schöne Git-Diffs (wird automatisch verwendet)", "git diff HEAD~1 | delta", "delta", new_tab=False), + 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", new_tab=False), - Tool("System", "temps", "CPU/GPU-Temperatur + Akku (Mac)", "sudo powermetrics -s cpu_power,gpu_power,thermal,battery -i 1000 -n 1", - "sudo", new_tab=False, linux=False), + 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", new_tab=False), + 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 '1 kWh' MJ)", "units", "units"), - Tool("Rechnen", "python3", "Python REPL für schnelle Berechnungen", "python3", "python3"), + 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-/Shell-Zugriff)", "ki interactive --agent-mode", "ki"), - Tool("KI", "ki-commit", "Commit-Message mit KI generieren", "ki commit", "ki", new_tab=False), - Tool("KI", "ki-diff", "Git-Diff mit KI erklären lassen", "ki diff", "ki", new_tab=False), + 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", new_tab=False), + 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"), ] -# --- Binaries die auf Linux anders heissen --- 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 = tool.binary - if IS_LINUX: - binary = LINUX_ALIASES.get(binary, binary) + binary = LINUX_ALIASES.get(tool.binary, tool.binary) if IS_LINUX else tool.binary return shutil.which(binary) is not None -def resolve_command(command: str) -> str: - """Passt Befehle für Linux an (bat→batcat etc.).""" +def resolve_command(cmd: str) -> str: if IS_LINUX: for mac_bin, linux_bin in LINUX_ALIASES.items(): - if command.startswith(mac_bin + " ") or command == mac_bin: - command = linux_bin + command[len(mac_bin):] - return command + if cmd == mac_bin or cmd.startswith(mac_bin + " "): + return linux_bin + cmd[len(mac_bin):] + return cmd -def detect_linux_terminal() -> list[str]: - """Findet einen verfügbaren Terminal-Emulator auf Linux.""" - candidates = [ - (["xfce4-terminal", "--tab", "-e"], "xfce4-terminal"), - (["gnome-terminal", "--tab", "--"], "gnome-terminal"), - (["kitty", "bash", "-c"], "kitty"), - (["alacritty", "-e"], "alacritty"), - (["xterm", "-e"], "xterm"), - ] - for args, binary in candidates: - if shutil.which(binary): - return args - return [] +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}" - -def open_new_tab(command: str): - """Öffnet Befehl in neuem Tab — macOS oder Linux.""" if IS_MAC: - script = f''' - tell application "iTerm2" - activate - tell current window - create tab with default profile - tell current session - write text "{command}" - end tell - end tell - end tell - ''' + 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) - else: - terminal_args = detect_linux_terminal() - if terminal_args: - subprocess.Popen(terminal_args + [f"bash -c '{command}; exec bash'"]) - else: - # Fallback: inline - os.system(command) + return - -def run_inline(command: str): - os.system(command) - - -def main(): - available = [t for t in TOOLS if is_available(t)] - - 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) - - fzf_lines = [ - f"{t.category:<12} {t.name:<20} {t.description}" - for t in available + # 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", - "--ansi", - "--height=70%", - "--layout=reverse", - "--border=rounded", - "--prompt= Tools > ", - "--header= Kategorie Name Beschreibung\n ─────────────────────────────────────────────────────────────", - "--color=header:italic:cyan,prompt:green,pointer:green", - ], - input="\n".join(fzf_lines), + _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 - if result.returncode != 0 or not result.stdout.strip(): + +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) - chosen_line = result.stdout.strip() - chosen_tool = None - for i, line in enumerate(fzf_lines): - if line == chosen_line: - chosen_tool = available[i] - break - - if not chosen_tool: - sys.exit(0) - - command = resolve_command(chosen_tool.command) - print(f"\n▶ {chosen_tool.name} — {command}\n") - - if chosen_tool.new_tab: - open_new_tab(command) - else: - run_inline(command) - if __name__ == "__main__": main() diff --git a/zsh/.zshrc b/zsh/.zshrc index ef97a07..88d8184 100644 --- a/zsh/.zshrc +++ b/zsh/.zshrc @@ -97,9 +97,6 @@ alias ki-tl='ki tl' alias fetch="fastfetch" alias temps="sudo powermetrics -s cpu_power,gpu_power,thermal,battery -i 1000 -n 1" -# Interaktiver Tool-Launcher (fzf + iTerm2/xfce4-terminal) -alias tools='toolbox' - # Cheat-Sheet: statische Referenz (tools-ref für Vollübersicht) tools-ref() { cat <<'TOOLS'