Files
gniza4linux/tui/screens/logs.py
shuki 587149f062 Add Python Textual TUI replacing gum-based bash TUI
New tui/ package with 14 screens (main menu, backup, restore, targets,
remotes, snapshots, verify, retention, schedule, logs, settings, wizard),
3 custom widgets (folder picker, confirm dialog, operation log), async
backend wrapper, pure-Python config parser, and TCSS theme.

bin/gniza now launches Textual TUI when available, falls back to gum.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:39:48 +02:00

112 lines
4.1 KiB
Python

from pathlib import Path
from textual.app import ComposeResult
from textual.screen import Screen
from textual.widgets import Header, Footer, Static, Button, DataTable, RichLog
from textual.containers import Vertical, Horizontal
from tui.config import LOG_DIR
class LogsScreen(Screen):
BINDINGS = [("escape", "go_back", "Back")]
def compose(self) -> ComposeResult:
yield Header()
with Vertical(id="logs-screen"):
yield Static("Logs", id="screen-title")
yield DataTable(id="logs-table")
with Horizontal(id="logs-buttons"):
yield Button("View", variant="primary", id="btn-view")
yield Button("Status", id="btn-status")
yield Button("Back", id="btn-back")
yield RichLog(id="log-viewer", wrap=True, highlight=True)
yield Footer()
def on_mount(self) -> None:
self._refresh_table()
def _refresh_table(self) -> None:
table = self.query_one("#logs-table", DataTable)
table.clear(columns=True)
table.add_columns("File", "Size")
log_dir = Path(LOG_DIR)
if not log_dir.is_dir():
return
logs = sorted(log_dir.glob("gniza-*.log"), key=lambda p: p.stat().st_mtime, reverse=True)[:20]
for f in logs:
size = f.stat().st_size
if size >= 1048576:
size_str = f"{size / 1048576:.1f} MB"
elif size >= 1024:
size_str = f"{size / 1024:.1f} KB"
else:
size_str = f"{size} B"
table.add_row(f.name, size_str, key=f.name)
def _selected_log(self) -> str | None:
table = self.query_one("#logs-table", DataTable)
if table.cursor_row is not None and table.row_count > 0:
return str(table.coordinate_to_cell_key((table.cursor_row, 0)).row_key.value)
return None
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "btn-back":
self.app.pop_screen()
elif event.button.id == "btn-view":
name = self._selected_log()
if name:
self._view_log(name)
else:
self.notify("Select a log file first", severity="warning")
elif event.button.id == "btn-status":
self._show_status()
def _view_log(self, name: str) -> None:
filepath = (Path(LOG_DIR) / name).resolve()
if not filepath.is_relative_to(Path(LOG_DIR).resolve()):
self.notify("Invalid log path", severity="error")
return
viewer = self.query_one("#log-viewer", RichLog)
viewer.clear()
if filepath.is_file():
content = filepath.read_text()
viewer.write(content)
else:
viewer.write(f"File not found: {filepath}")
def _show_status(self) -> None:
viewer = self.query_one("#log-viewer", RichLog)
viewer.clear()
log_dir = Path(LOG_DIR)
viewer.write("Backup Status Overview")
viewer.write("=" * 40)
if not log_dir.is_dir():
viewer.write("Log directory does not exist.")
return
logs = sorted(log_dir.glob("gniza-*.log"), key=lambda p: p.stat().st_mtime, reverse=True)
if logs:
latest = logs[0]
from datetime import datetime
mtime = datetime.fromtimestamp(latest.stat().st_mtime)
viewer.write(f"Last log: {mtime.strftime('%Y-%m-%d %H:%M:%S')}")
last_line = ""
with open(latest) as f:
for line in f:
last_line = line.rstrip()
if last_line:
viewer.write(f"Last entry: {last_line}")
else:
viewer.write("No backup logs found.")
viewer.write(f"Log files: {len(logs)}")
total = sum(f.stat().st_size for f in logs)
if total >= 1048576:
viewer.write(f"Total size: {total / 1048576:.1f} MB")
elif total >= 1024:
viewer.write(f"Total size: {total / 1024:.1f} KB")
else:
viewer.write(f"Total size: {total} B")
def action_go_back(self) -> None:
self.app.pop_screen()