Add tree-based file browser for snapshot contents
Replace flat file list with a Tree widget that shows directory structure. Strips remote path prefix to show relative paths only. Folders shown in bold with trailing /, sorted dirs-first. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -245,6 +245,35 @@ SelectionList {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Snapshot browser */
|
||||
#snapshot-browser {
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
padding: 1;
|
||||
background: $panel;
|
||||
border: thick $accent;
|
||||
}
|
||||
|
||||
#sb-title {
|
||||
text-style: bold;
|
||||
color: #00cc00;
|
||||
margin: 0 0 1 0;
|
||||
}
|
||||
|
||||
#sb-tree {
|
||||
height: 1fr;
|
||||
border: round $accent;
|
||||
}
|
||||
|
||||
#sb-buttons {
|
||||
height: auto;
|
||||
margin: 1 0 0 0;
|
||||
}
|
||||
|
||||
#sb-buttons Button {
|
||||
margin: 0 1 0 0;
|
||||
}
|
||||
|
||||
/* Log viewer */
|
||||
#log-viewer {
|
||||
height: 1fr;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import re
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Header, Footer, Static, Button, Select, DataTable
|
||||
@@ -6,7 +8,7 @@ from textual import work
|
||||
|
||||
from tui.config import list_conf_dir
|
||||
from tui.backend import run_cli
|
||||
from tui.widgets import OperationLog
|
||||
from tui.widgets import SnapshotBrowser
|
||||
|
||||
|
||||
class SnapshotsScreen(Screen):
|
||||
@@ -91,17 +93,33 @@ class SnapshotsScreen(Screen):
|
||||
return
|
||||
target = str(target_sel.value)
|
||||
remote = str(remote_sel.value)
|
||||
log_screen = OperationLog(f"Files: {target}/{snapshot}", show_spinner=False)
|
||||
self.app.push_screen(log_screen)
|
||||
|
||||
self.notify("Loading files...")
|
||||
rc, stdout, stderr = await run_cli(
|
||||
"snapshots", "browse", f"--target={target}", f"--remote={remote}", f"--snapshot={snapshot}"
|
||||
)
|
||||
if stdout:
|
||||
log_screen.write(stdout)
|
||||
if stderr:
|
||||
log_screen.write(stderr)
|
||||
if not stdout and not stderr:
|
||||
log_screen.write("No files found.")
|
||||
|
||||
# Parse file list, strip remote prefix to get relative paths
|
||||
# Paths look like: /remote/base/.../snapshots/<timestamp>/etc/foo.conf
|
||||
# We want everything after the snapshot timestamp directory
|
||||
files = []
|
||||
pattern = re.compile(re.escape(snapshot) + r"/(.*)")
|
||||
for line in (stdout or "").splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
m = pattern.search(line)
|
||||
if m:
|
||||
files.append(m.group(1))
|
||||
else:
|
||||
files.append(line)
|
||||
|
||||
if not files:
|
||||
self.notify("No files found in snapshot", severity="warning")
|
||||
return
|
||||
|
||||
browser = SnapshotBrowser(f"{target} / {snapshot}", files)
|
||||
self.app.push_screen(browser)
|
||||
|
||||
def action_go_back(self) -> None:
|
||||
self.app.pop_screen()
|
||||
|
||||
@@ -2,3 +2,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
|
||||
from tui.widgets.snapshot_browser import SnapshotBrowser
|
||||
|
||||
64
tui/widgets/snapshot_browser.py
Normal file
64
tui/widgets/snapshot_browser.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import PurePosixPath
|
||||
from textual.app import ComposeResult
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Tree, Static, Button
|
||||
from textual.containers import Vertical, Horizontal
|
||||
|
||||
|
||||
class SnapshotBrowser(ModalScreen[None]):
|
||||
"""Modal file browser for remote snapshot contents."""
|
||||
|
||||
BINDINGS = [("escape", "close", "Close")]
|
||||
|
||||
def __init__(self, title: str, file_list: list[str]):
|
||||
super().__init__()
|
||||
self._title = title
|
||||
self._file_list = file_list
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="snapshot-browser"):
|
||||
yield Static(self._title, id="sb-title")
|
||||
yield Tree("snapshot", id="sb-tree")
|
||||
with Horizontal(id="sb-buttons"):
|
||||
yield Button("Close", variant="primary", id="sb-close")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
tree = self.query_one("#sb-tree", Tree)
|
||||
tree.root.expand()
|
||||
self._build_tree(tree)
|
||||
|
||||
def _build_tree(self, tree: Tree) -> None:
|
||||
# Build a nested dict from file paths
|
||||
root: dict = {}
|
||||
for filepath in self._file_list:
|
||||
filepath = filepath.strip()
|
||||
if not filepath:
|
||||
continue
|
||||
parts = PurePosixPath(filepath).parts
|
||||
node = root
|
||||
for part in parts:
|
||||
if part not in node:
|
||||
node[part] = {}
|
||||
node = node[part]
|
||||
|
||||
# Add to tree widget recursively
|
||||
self._add_nodes(tree.root, root)
|
||||
tree.root.expand_all()
|
||||
|
||||
def _add_nodes(self, parent, structure: dict) -> None:
|
||||
# Sort: directories first, then files
|
||||
dirs = sorted(k for k, v in structure.items() if v)
|
||||
files = sorted(k for k, v in structure.items() if not v)
|
||||
for name in dirs:
|
||||
branch = parent.add(f"[bold]{name}/[/bold]")
|
||||
self._add_nodes(branch, structure[name])
|
||||
for name in files:
|
||||
parent.add_leaf(name)
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
self.dismiss(None)
|
||||
|
||||
def action_close(self) -> None:
|
||||
self.dismiss(None)
|
||||
Reference in New Issue
Block a user