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)