dotfiles-rene/bin/toolbox
rene 562ebb64f2 toolbox: Kategorie-Navigation, neues Fenster, tools-Symlink
- 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 <noreply@anthropic.com>
2026-03-27 13:58:50 +01:00

242 lines
11 KiB
Python
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()