From e6aa828111cc0d9429a315c27b21e2c92ba7cc6f Mon Sep 17 00:00:00 2001 From: shuki Date: Fri, 6 Mar 2026 02:53:31 +0200 Subject: [PATCH] Toggle SSH key/password fields by auth method and add key file browser Show only the SSH key path input (with Browse button) when auth method is "key", and only the password input when auth method is "password". Adds a FilePicker widget for browsing SSH key files, defaulting to ~/.ssh directory. Co-Authored-By: Claude Opus 4.6 --- tui/gniza.tcss | 44 ++++++++++++++++++++++++++++++++++++ tui/screens/remote_edit.py | 34 +++++++++++++++++++++++----- tui/widgets/__init__.py | 1 + tui/widgets/file_picker.py | 46 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 tui/widgets/file_picker.py diff --git a/tui/gniza.tcss b/tui/gniza.tcss index f1a76d0..a5de427 100644 --- a/tui/gniza.tcss +++ b/tui/gniza.tcss @@ -241,3 +241,47 @@ Switch { color: #00cc00; margin: 1 0 0 0; } + +/* SSH key browse row */ +#re-key-row { + height: auto; + margin: 0 0 1 0; +} + +#re-key-row Input { + width: 1fr; +} + +#re-key-row Button { + width: auto; + min-width: 12; + margin: 0 0 0 1; +} + +/* File picker */ +#file-picker { + width: 70; + height: 30; + padding: 1; + background: $panel; + border: thick $accent; +} + +#fip-title { + text-style: bold; + color: #00cc00; + margin: 0 0 1 0; +} + +#fip-tree { + height: 1fr; +} + +#fip-buttons { + height: auto; + margin: 1 0 0 0; +} + +#fip-buttons Button { + margin: 0 1 0 0; +} diff --git a/tui/screens/remote_edit.py b/tui/screens/remote_edit.py index c4c2a54..b61adbe 100644 --- a/tui/screens/remote_edit.py +++ b/tui/screens/remote_edit.py @@ -1,4 +1,5 @@ import re +from pathlib import Path from textual.app import ComposeResult from textual.screen import Screen from textual.widgets import Header, Footer, Static, Button, Input, Select, RadioSet, RadioButton @@ -6,6 +7,7 @@ from textual.containers import Vertical, Horizontal from tui.config import parse_conf, write_conf, CONFIG_DIR from tui.models import Remote +from tui.widgets import FilePicker _NAME_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9_-]{0,31}$') @@ -54,10 +56,12 @@ class RemoteEditScreen(Screen): value=remote.auth_method, classes="ssh-field", ) - yield Static("SSH Key path:", id="lbl-key", classes="ssh-field") - yield Input(value=remote.key, placeholder="~/.ssh/id_rsa", id="re-key", classes="ssh-field") - yield Static("Password:", id="lbl-password", classes="ssh-field") - yield Input(value=remote.password, placeholder="SSH password", password=True, id="re-password", classes="ssh-field") + yield Static("SSH Key path:", id="lbl-key", classes="ssh-field ssh-key-field") + with Horizontal(id="re-key-row", classes="ssh-field ssh-key-field"): + yield Input(value=remote.key, placeholder="~/.ssh/id_rsa", id="re-key") + yield Button("Browse...", id="btn-browse-key") + yield Static("Password:", id="lbl-password", classes="ssh-field ssh-password-field") + yield Input(value=remote.password, placeholder="SSH password", password=True, id="re-password", classes="ssh-field ssh-password-field") # Common fields yield Static("Base path:") yield Input(value=remote.base, placeholder="/backups", id="re-base") @@ -90,25 +94,43 @@ class RemoteEditScreen(Screen): self._update_field_visibility() def on_select_changed(self, event: Select.Changed) -> None: - if event.select.id == "re-type": + if event.select.id in ("re-type", "re-auth"): self._update_field_visibility() def _update_field_visibility(self) -> None: type_sel = self.query_one("#re-type", Select) rtype = str(type_sel.value) if isinstance(type_sel.value, str) else "ssh" + is_ssh = rtype == "ssh" for w in self.query(".ssh-field"): - w.display = rtype == "ssh" + w.display = is_ssh for w in self.query(".s3-field"): w.display = rtype == "s3" for w in self.query(".gdrive-field"): w.display = rtype == "gdrive" + # Toggle key vs password fields based on auth method + if is_ssh: + auth_sel = self.query_one("#re-auth", Select) + auth = str(auth_sel.value) if isinstance(auth_sel.value, str) else "key" + for w in self.query(".ssh-key-field"): + w.display = auth == "key" + for w in self.query(".ssh-password-field"): + w.display = auth == "password" def on_button_pressed(self, event: Button.Pressed) -> None: if event.button.id == "btn-cancel": self.dismiss(None) + elif event.button.id == "btn-browse-key": + self.app.push_screen( + FilePicker("Select SSH key file", start=str(Path.home() / ".ssh")), + callback=self._key_file_selected, + ) elif event.button.id == "btn-save": self._save() + def _key_file_selected(self, path: str | None) -> None: + if path: + self.query_one("#re-key", Input).value = path + def _save(self) -> None: if self._is_new: name = self.query_one("#re-name", Input).value.strip() diff --git a/tui/widgets/__init__.py b/tui/widgets/__init__.py index 7212319..a35da25 100644 --- a/tui/widgets/__init__.py +++ b/tui/widgets/__init__.py @@ -1,3 +1,4 @@ from tui.widgets.folder_picker import FolderPicker +from tui.widgets.file_picker import FilePicker from tui.widgets.confirm_dialog import ConfirmDialog from tui.widgets.operation_log import OperationLog diff --git a/tui/widgets/file_picker.py b/tui/widgets/file_picker.py new file mode 100644 index 0000000..1558eae --- /dev/null +++ b/tui/widgets/file_picker.py @@ -0,0 +1,46 @@ +from pathlib import Path + +from textual.app import ComposeResult +from textual.screen import ModalScreen +from textual.widgets import DirectoryTree, Static, Button +from textual.containers import Horizontal, Vertical + + +class FilePicker(ModalScreen[str | None]): + + BINDINGS = [("escape", "cancel", "Cancel")] + + def __init__(self, title: str = "Select file", start: str = "/"): + super().__init__() + self._title = title + self._start = start + + def compose(self) -> ComposeResult: + with Vertical(id="file-picker"): + yield Static(self._title, id="fip-title") + yield DirectoryTree(self._start, id="fip-tree") + with Horizontal(id="fip-buttons"): + yield Button("Select", variant="primary", id="fip-select") + yield Button("Cancel", variant="default", id="fip-cancel") + + def _get_selected_path(self) -> Path | None: + tree = self.query_one("#fip-tree", DirectoryTree) + node = tree.cursor_node + if node and node.data and node.data.path: + return node.data.path + return None + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "fip-select": + path = self._get_selected_path() + if path and path.is_file(): + self.dismiss(str(path)) + elif path and path.is_dir(): + self.notify("Please select a file, not a directory", severity="warning") + else: + self.dismiss(None) + else: + self.dismiss(None) + + def action_cancel(self) -> None: + self.dismiss(None)