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)
This commit is contained in:
x3
2026-08-31 15:46:30 +00:00
parent ed0a943d21
commit 7a1bf948f1
6 changed files with 237 additions and 13 deletions
+39 -2
View File
@@ -95,6 +95,23 @@ pip install -e .
# paramiko - SFTP/SSH # 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 ### Als Paket
```bash ```bash
@@ -142,8 +159,10 @@ ftptui
| Taste | Aktion | | Taste | Aktion |
|--------------|---------------------------------| |--------------|---------------------------------|
| `Tab` | Zwischen lokalem und entferntem Paneel wechseln | | `Tab` | Zwischen lokalem und entferntem Paneel wechseln |
| `Enter` | Ordner öffnen (aktiv: fokussiertes Paneel) | | `↑` / `↓` | Zeilenauswahl bewegen |
| `→` | Markierten Ordner/Eintrag öffnen |
| `←` / `⌫` | Eine Verzeichnisebene höher | | `←` / `⌫` | Eine Verzeichnisebene höher |
| `Enter` | Ordner öffnen (aktiv: fokussiertes Paneel) |
| `↑ ..` | Eine Ebene höher (Tabellenzeile) | | `↑ ..` | Eine Ebene höher (Tabellenzeile) |
| `l` | Auswahl **herunterladen** (entfernt → lokal) | | `l` | Auswahl **herunterladen** (entfernt → lokal) |
| `u` | Auswahl **hochladen** (lokal → entfernt) | | `u` | Auswahl **hochladen** (lokal → entfernt) |
@@ -154,7 +173,8 @@ ftptui
> Hinweis: Hoch-/Herunterladen (`u`/`l`), Anlegen (`n`), Löschen (`d`) und > Hinweis: Hoch-/Herunterladen (`u`/`l`), Anlegen (`n`), Löschen (`d`) und
> Umbenennen (`r`) wirken auf das **aktive (grün markierte) Paneel**. Wechsel > 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/ ftptui/
├── pyproject.toml # Paketmetadaten, Abhängigkeiten, Einstiegspunkt ├── pyproject.toml # Paketmetadaten, Abhängigkeiten, Einstiegspunkt
├── README.md # diese Dokumentation ├── README.md # diese Dokumentation
├── install.sh # systemweite Installation (/opt/ftptui/venv)
├── uninstall.sh # Deinstallation systemweiter Installation
├── .gitignore ├── .gitignore
└── ftptui/ └── ftptui/
├── __init__.py # Versionsnummer ├── __init__.py # Versionsnummer
@@ -305,6 +327,21 @@ MIT siehe Projektträger/Repositories. Keine gewerblichen Einschränkungen.
## Changelog ## 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 ### v0.2.0
- **Bugfix:** Wechsel in den Browser-Bildschirm korrigiert - **Bugfix:** Wechsel in den Browser-Bildschirm korrigiert
(`set_screen``switch_screen`). Vorher führte eine erfolgreiche Verbindung (`set_screen``switch_screen`). Vorher führte eine erfolgreiche Verbindung
+1 -1
View File
@@ -1,3 +1,3 @@
"""ftptui - Interaktiver FTP/SFTP-Client als Terminal-User-Interface.""" """ftptui - Interaktiver FTP/SFTP-Client als Terminal-User-Interface."""
__version__ = "0.2.0" __version__ = "0.3.0"
+44 -9
View File
@@ -67,6 +67,28 @@ def _join(path: str, name: str, is_local: bool) -> str:
return path.rstrip("/") + "/" + name 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): class ConnectionScreen(Screen):
"""Startbildschirm zur Eingabe der Verbindungsdaten (Terminal-Look).""" """Startbildschirm zur Eingabe der Verbindungsdaten (Terminal-Look)."""
@@ -191,7 +213,6 @@ class BrowserScreen(Screen):
Binding("n", "mkdir", "Verzeichnis"), Binding("n", "mkdir", "Verzeichnis"),
Binding("d", "delete", "Löschen"), Binding("d", "delete", "Löschen"),
Binding("r", "rename", "Umbenennen"), Binding("r", "rename", "Umbenennen"),
Binding("left", "up_level", "Eine Ebene hoch"),
Binding("backspace", "up_level", "Eine Ebene hoch"), Binding("backspace", "up_level", "Eine Ebene hoch"),
] ]
@@ -202,13 +223,13 @@ class BrowserScreen(Screen):
yield Horizontal( yield Horizontal(
Vertical( Vertical(
Static(" Lokal ", classes="pane-title local-title active"), Static(" Lokal ", classes="pane-title local-title active"),
DataTable(id="local_table"), FileTable(id="local_table"),
classes="pane", classes="pane",
), ),
Static(" ", classes="vsep"), Static(" ", classes="vsep"),
Vertical( Vertical(
Static(" Entfernt ", classes="pane-title remote-title"), Static(" Entfernt ", classes="pane-title remote-title"),
DataTable(id="remote_table"), FileTable(id="remote_table"),
classes="pane", classes="pane",
), ),
classes="panes", classes="panes",
@@ -228,13 +249,26 @@ class BrowserScreen(Screen):
self._refresh_local() self._refresh_local()
self._refresh_remote() 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("Name", key="name")
table.add_column("Größe", key="size") table.add_column("Größe", key="size")
table.add_column("Datum", key="date") table.add_column("Datum", key="date")
table.cursor_type = "row" table.cursor_type = "row"
table.zebra_stripes = False 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: def on_descendant_focus(self, event) -> None:
if event.widget is self.table_local: if event.widget is self.table_local:
self.query_one(".local-title", Static).add_class("active") 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}" f"lokal: {self.local_path} | entfernt: {self.remote_path}"
) )
self.query_one("#statusbar", Static).update( self.query_one("#statusbar", Static).update(
" F2/Tab Panele · Enter Öffnen · l Download · u Upload · " " Tab Panele · ↑/↓ Auswahl · ← Hoch · → Öffnen · Enter Öffnen · "
"n Neu · d Löschen · r Umbenennen · ← Hoch · q Beenden" "l Download · u Upload · n Neu · d Löschen · r Umbenennen · q Beenden"
) )
@on(DataTable.RowHighlighted) @on(DataTable.RowHighlighted)
def _highlight(self, event: DataTable.RowHighlighted) -> None: def _highlight(self, event: DataTable.RowHighlighted) -> None:
self._update_info() 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 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 row = table.cursor_row
if row is None: if row is None:
return 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") @on(DataTable.RowSelected, "#local_table")
def _local_selected(self, event: DataTable.RowSelected) -> None: def _local_selected(self, event: DataTable.RowSelected) -> None:
Executable
+91
View File
@@ -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"
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "ftptui" name = "ftptui"
version = "0.2.0" version = "0.3.0"
description = "Interaktiver FTP/SFTP-Client als Terminal-User-Interface in Python" description = "Interaktiver FTP/SFTP-Client als Terminal-User-Interface in Python"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
Executable
+61
View File
@@ -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."