From 7a1bf948f1ad52e659cc18858e689843217ecf04 Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 31 Aug 2026 15:46:30 +0000 Subject: [PATCH] v0.3.0: Pfeil-/Tab-Navigation gefixt, install/uninstall-Skripte - FileTable(DataTable): links/rechts fuer Datei-Manager-Navigation umgebunden (links = eine Ebene hoch, rechts = Ordner oeffnen); zuvor verschluckte das DataTable-Widget die Tasten - _selected(): liefert jetzt die Klartext-Zeichenkette statt rich.Text-Objekt (behob AttributeError beim Oeffnen/Enter) - action_focus_next(): Tab wechselt zuverlaessig zwischen lokalem und entferntem Paneel (statt unzuverlaessigem focus_next) - install.sh / uninstall.sh: systemweite Installation (PEP-668-konform via /opt/ftptui/venv + /usr/local/bin/ftptui) --- README.md | 41 ++++++++++++++++++++- ftptui/__init__.py | 2 +- ftptui/app.py | 53 ++++++++++++++++++++++----- install.sh | 91 ++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- uninstall.sh | 61 +++++++++++++++++++++++++++++++ 6 files changed, 237 insertions(+), 13 deletions(-) create mode 100755 install.sh create mode 100755 uninstall.sh diff --git a/README.md b/README.md index 482a6aa..ab628bd 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,23 @@ pip install -e . # paramiko - SFTP/SSH ``` +### Installations- & Deinstallations-Skripte (systemweit) + +Im Projektverzeichnis liegen zwei Scripts für eine **systemweite Installation** +auf Debian/Ubuntu (PEP 668 „externally managed“ wird berücksichtigt: es wird eine +eigene Umgebung unter `/opt/ftptui/venv` angelegt und nach +`/usr/local/bin/ftptui` verlinkt): + +```bash +./install.sh # systemweit installieren (fragt ggf. nach sudo) +./install.sh --force # erneut installieren/aktualisieren + +./uninstall.sh # systemweite Installation entfernen (Nutzdaten bleiben) +./uninstall.sh --purge # zusätzlich Konfigurationsdateien löschen +``` + +Nach `./install.sh` steht das Kommando `ftptui` global im PATH bereit. + ### Als Paket ```bash @@ -142,8 +159,10 @@ ftptui | Taste | Aktion | |--------------|---------------------------------| | `Tab` | Zwischen lokalem und entferntem Paneel wechseln | -| `Enter` | Ordner öffnen (aktiv: fokussiertes Paneel) | +| `↑` / `↓` | Zeilenauswahl bewegen | +| `→` | Markierten Ordner/Eintrag öffnen | | `←` / `⌫` | Eine Verzeichnisebene höher | +| `Enter` | Ordner öffnen (aktiv: fokussiertes Paneel) | | `↑ ..` | Eine Ebene höher (Tabellenzeile) | | `l` | Auswahl **herunterladen** (entfernt → lokal) | | `u` | Auswahl **hochladen** (lokal → entfernt) | @@ -154,7 +173,8 @@ ftptui > Hinweis: Hoch-/Herunterladen (`u`/`l`), Anlegen (`n`), Löschen (`d`) und > Umbenennen (`r`) wirken auf das **aktive (grün markierte) Paneel**. Wechsel -> der Seite mit `Tab`. +> der Seite mit `Tab`. Die Pfeiltasten `→`/`←` navigieren die Verzeichnisstruktur +> (öffnen / eine Ebene hoch), `↑`/`↓` bewegen die Zeilenauswahl. --- @@ -164,6 +184,8 @@ ftptui ftptui/ ├── pyproject.toml # Paketmetadaten, Abhängigkeiten, Einstiegspunkt ├── README.md # diese Dokumentation +├── install.sh # systemweite Installation (/opt/ftptui/venv) +├── uninstall.sh # Deinstallation systemweiter Installation ├── .gitignore └── ftptui/ ├── __init__.py # Versionsnummer @@ -305,6 +327,21 @@ MIT – siehe Projektträger/Repositories. Keine gewerblichen Einschränkungen. ## Changelog +### v0.3.0 +- **Bugfix (Pfeil-Navigation):** Im Dateibrowser navigieren die Pfeiltasten nun + im File-Manager-Stil: `←` wechselt eine Ebene nach oben, `→` öffnet den + markierten Eintrag, `↑`/`↓` bewegen die Auswahl. Zuvor verschluckte das + `DataTable`-Widget die Tasten (Widget-Bindings haben Vorrang). Dafür wird die + neue `FileTable`-Klasse genutzt, die `left`/`right` passend umbindet. +- **Bugfix (Tab-Panel-Wechsel):** `Tab` wechselt jetzt zuverlässig zwischen dem + lokalen und dem entfernten Paneel (explizite `action_focus_next` statt + unzuverlässigem `focus_next`). +- **Bugfix (`_selected`):** Die Auswahlauswertung liefert jetzt die + Klartext-Zeichenkette statt eines `rich.Text`-Objekts, wodurch Öffnen/Enter + nicht mehr mit `AttributeError` abstürzte. +- **Neu:** `install.sh` / `uninstall.sh` für systemweite Installation + (PEP-668-konform via `/opt/ftptui/venv` + `/usr/local/bin/ftptui`). + ### v0.2.0 - **Bugfix:** Wechsel in den Browser-Bildschirm korrigiert (`set_screen` → `switch_screen`). Vorher führte eine erfolgreiche Verbindung diff --git a/ftptui/__init__.py b/ftptui/__init__.py index d7ad592..30cf32e 100644 --- a/ftptui/__init__.py +++ b/ftptui/__init__.py @@ -1,3 +1,3 @@ """ftptui - Interaktiver FTP/SFTP-Client als Terminal-User-Interface.""" -__version__ = "0.2.0" +__version__ = "0.3.0" diff --git a/ftptui/app.py b/ftptui/app.py index 4563984..cb3a257 100644 --- a/ftptui/app.py +++ b/ftptui/app.py @@ -67,6 +67,28 @@ def _join(path: str, name: str, is_local: bool) -> str: return path.rstrip("/") + "/" + name +class FileTable(DataTable): + """DataTable mit Datei-Manager-Navigation. + + ``DataTable`` bindet die Pfeiltasten selbst (``left``/``right`` bewegen den + Cursor horizontal, was bei einer einzelnen Spalte sinnlos ist). Damit die + klassischen File-Manager-Bindings (Total Commander / ranger) funktionieren, + werden ``left``/``right`` hier auf „eine Ebene hoch“ bzw. „Ordner öffnen“ + umgebunden. ``up``/``down`` bewegen weiterhin die Zeilenauswahl. + """ + + BINDINGS = [ + Binding("left", "up_level", "Eine Ebene hoch", priority=True, show=False), + Binding("right", "open_row", "Öffnen", priority=True, show=False), + ] + + def action_up_level(self) -> None: + self.screen.action_up_level() + + def action_open_row(self) -> None: + self.screen.action_open() + + class ConnectionScreen(Screen): """Startbildschirm zur Eingabe der Verbindungsdaten (Terminal-Look).""" @@ -191,7 +213,6 @@ class BrowserScreen(Screen): Binding("n", "mkdir", "Verzeichnis"), Binding("d", "delete", "Löschen"), Binding("r", "rename", "Umbenennen"), - Binding("left", "up_level", "Eine Ebene hoch"), Binding("backspace", "up_level", "Eine Ebene hoch"), ] @@ -202,13 +223,13 @@ class BrowserScreen(Screen): yield Horizontal( Vertical( Static(" Lokal ", classes="pane-title local-title active"), - DataTable(id="local_table"), + FileTable(id="local_table"), classes="pane", ), Static(" ", classes="vsep"), Vertical( Static(" Entfernt ", classes="pane-title remote-title"), - DataTable(id="remote_table"), + FileTable(id="remote_table"), classes="pane", ), classes="panes", @@ -228,13 +249,26 @@ class BrowserScreen(Screen): self._refresh_local() self._refresh_remote() - def _setup_table(self, table: DataTable) -> None: + def _setup_table(self, table: FileTable) -> None: table.add_column("Name", key="name") table.add_column("Größe", key="size") table.add_column("Datum", key="date") table.cursor_type = "row" table.zebra_stripes = False + def action_focus_next(self) -> None: + """Wechselt den Fokus direkt zwischen lokalem und entferntem Paneel. + + ``focus_next`` (das Standard-Tab-Verhalten) springt über alle fokussierbaren + Widgets und erreicht die zweite Tabelle nicht zuverlässig. Deshalb wird der + Fokus hier explizit auf die jeweils andere Tabelle gesetzt. + """ + if self.app.focused is self.table_local: + self.table_remote.focus() + else: + self.table_local.focus() + self._update_info() + def on_descendant_focus(self, event) -> None: if event.widget is self.table_local: self.query_one(".local-title", Static).add_class("active") @@ -304,22 +338,23 @@ class BrowserScreen(Screen): f"lokal: {self.local_path} | entfernt: {self.remote_path}" ) self.query_one("#statusbar", Static).update( - " F2/Tab Panele · Enter Öffnen · l Download · u Upload · " - "n Neu · d Löschen · r Umbenennen · ← Hoch · q Beenden" + " Tab Panele · ↑/↓ Auswahl · ← Hoch · → Öffnen · Enter Öffnen · " + "l Download · u Upload · n Neu · d Löschen · r Umbenennen · q Beenden" ) @on(DataTable.RowHighlighted) def _highlight(self, event: DataTable.RowHighlighted) -> None: self._update_info() - def _focused_table(self) -> DataTable: + def _focused_table(self) -> FileTable: return self.table_local if self.app.focused is self.table_local else self.table_remote - def _selected(self, table: DataTable) -> str | None: + def _selected(self, table: FileTable) -> str | None: row = table.cursor_row if row is None: return None - return table.get_row_at(row)[0] + cell = table.get_row_at(row)[0] + return cell.plain if isinstance(cell, Text) else str(cell) @on(DataTable.RowSelected, "#local_table") def _local_selected(self, event: DataTable.RowSelected) -> None: diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..3e7b74d --- /dev/null +++ b/install.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# +# ftptui – Installations-Skript (systemweit) +# +# Das System nutzt ein "externally managed" Python (PEP 668, Debian/Ubuntu), +# daher wird NIE direkt in System-Site-Packages installiert. Stattdessen wird +# eine eigene systemweite virtuelle Umgebung unter /opt/ftptui/venv angelegt +# und der Einstiegspunkt nach /usr/local/bin/ftptui verlinkt. Damit steht das +# Kommando global im PATH, ohne das System-Python zu gefährden. +# +# Verwendung: +# ./install.sh # installiert systemweit (fragt ggf. nach sudo) +# ./install.sh --force # überschreibt eine bereits installierte Version +# ./install.sh --uninstall +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PACKAGE_DIR="$SCRIPT_DIR" +APP_NAME="ftptui" +VENV_DIR="/opt/ftptui/venv" +BIN_LINK="/usr/local/bin/$APP_NAME" + +FORCE=0 +MODE="install" +for arg in "$@"; do + case "$arg" in + --force|-f) FORCE=1 ;; + --uninstall|-u) MODE="uninstall" ;; + -h|--help) echo "Verwendung: $0 [--force|--uninstall]"; exit 0 ;; + *) echo "Unbekanntes Argument: $arg"; exit 1 ;; + esac +done + +run_sudo() { + if [ "$(id -u)" -eq 0 ]; then + "$@" + else + sudo "$@" + fi +} + +if [ "$MODE" = "uninstall" ]; then + echo "==> Entferne systemweite Installation von '$APP_NAME' ..." + run_sudo rm -f "$BIN_LINK" + run_sudo rm -rf /opt/ftptui + echo "==> Entfernt: $BIN_LINK und $VENV_DIR" + echo "=> Fertig. (Konfigurationsdateien unter ~/.config/ftptui bleiben erhalten.)" + exit 0 +fi + +echo "==> ftptui Installations-Skript (systemweit, via /opt/ftptui/venv)" + +# Abhängigkeiten prüfen +command -v python3 >/dev/null 2>&1 || { echo "FEHLER: python3 nicht gefunden."; exit 1; } +if ! python3 -m venv --help >/dev/null 2>&1; then + echo "FEHLER: python3-venv fehlt. Bitte installieren: sudo apt install python3-venv" + exit 1 +fi + +# Bereits installiert? +if [ -x "$BIN_LINK" ] && [ "$FORCE" -ne 1 ]; then + echo "WARNUNG: '$APP_NAME' ist bereits installiert: $BIN_LINK" + echo " Mit --force wird neu installiert." + echo " Zum Entfernen: $0 --uninstall (bzw. ./uninstall.sh)" + exit 0 +fi + +echo "==> Lege systemweite virtuelle Umgebung an: $VENV_DIR" +run_sudo python3 -m venv "$VENV_DIR" +run_sudo "$VENV_DIR/bin/pip" install --upgrade pip + +echo "==> Installiere '$APP_NAME' + Abhängigkeiten ..." +run_sudo "$VENV_DIR/bin/pip" install --upgrade "$PACKAGE_DIR" + +echo "==> Verlinke Einstiegspunkt nach $BIN_LINK" +run_sudo ln -sf "$VENV_DIR/bin/$APP_NAME" "$BIN_LINK" + +# Konfigurationsverzeichnis anlegen +CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/ftptui" +mkdir -p "$CONFIG_DIR" +echo "==> Konfigurationsverzeichnis bereit: $CONFIG_DIR" + +# Fertig +echo +echo "Installation abgeschlossen." +echo " Befehl: $(command -v "$APP_NAME" 2>/dev/null || echo "$BIN_LINK")" +echo " Start: $APP_NAME" +echo " Venv: $VENV_DIR" +echo +echo "Tipp: 'ftptui' startet die TUI. Entfernen mit: ./uninstall.sh" diff --git a/pyproject.toml b/pyproject.toml index 18e176f..7ffaab1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ftptui" -version = "0.2.0" +version = "0.3.0" description = "Interaktiver FTP/SFTP-Client als Terminal-User-Interface in Python" readme = "README.md" requires-python = ">=3.10" diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 0000000..e4e21fa --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# +# ftptui – Deinstallations-Skript (systemweit) +# +# Entfernt die systemweite Installation (venv unter /opt/ftptui und den +# Einstiegspunkt /usr/local/bin/ftptui). +# +# Verwendung: +# ./uninstall.sh # entfernt das Paket (Nutzdaten bleiben) +# ./uninstall.sh --purge # entfernt zusätzlich die Konfigurationsdateien +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APP_NAME="ftptui" +VENV_DIR="/opt/ftptui/venv" +BIN_LINK="/usr/local/bin/$APP_NAME" + +PURGE=0 +for arg in "$@"; do + case "$arg" in + --purge|-p) PURGE=1 ;; + -h|--help) echo "Verwendung: $0 [--purge]"; exit 0 ;; + *) echo "Unbekanntes Argument: $arg"; exit 1 ;; + esac +done + +run_sudo() { + if [ "$(id -u)" -eq 0 ]; then + "$@" + else + sudo "$@" + fi +} + +echo "==> ftptui Deinstallations-Skript" + +if [ -x "$BIN_LINK" ] || [ -d "$VENV_DIR" ]; then + echo "==> Entferne systemweite Installation ..." + run_sudo rm -f "$BIN_LINK" + run_sudo rm -rf /opt/ftptui + echo "==> Entfernt: $BIN_LINK und $VENV_DIR" +else + echo "==> Keine systemweite Installation gefunden (kein $BIN_LINK / $VENV_DIR)." +fi + +CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/ftptui" +if [ "$PURGE" -eq 1 ]; then + if [ -e "$CONFIG_DIR" ]; then + rm -rf "$CONFIG_DIR" + echo "==> Konfigurationsverzeichnis entfernt: $CONFIG_DIR" + else + echo "==> Kein Konfigurationsverzeichnis gefunden." + fi +else + echo "==> Nutzdaten (Verbindungsprofile) bleiben erhalten: $CONFIG_DIR" + echo " (mit --purge werden auch diese entfernt)" +fi + +echo +echo "Deinstallation abgeschlossen."