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 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
46
tui/widgets/file_picker.py
Normal file
46
tui/widgets/file_picker.py
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user