From ced2e9a88983da0db6002f53cfbf9a8ba2b12caf Mon Sep 17 00:00:00 2001 From: shuki Date: Fri, 6 Mar 2026 04:25:57 +0200 Subject: [PATCH] Revert OperationLog and all screens to pre-spinner working state Reverts all spinner/loading indicator changes and debug code. Restores the exact code from commit 0e02ba6 which was confirmed working. Co-Authored-By: Claude Opus 4.6 --- tui/gniza.tcss | 13 +--------- tui/screens/backup.py | 45 +++++++++++------------------------ tui/screens/remotes.py | 8 ++----- tui/screens/restore.py | 10 +++----- tui/screens/retention.py | 22 ++++++----------- tui/screens/schedule.py | 28 +++++++--------------- tui/widgets/confirm_dialog.py | 1 - tui/widgets/operation_log.py | 34 ++++++++++++++++++-------- 8 files changed, 59 insertions(+), 102 deletions(-) diff --git a/tui/gniza.tcss b/tui/gniza.tcss index 5a83b92..c511c2d 100644 --- a/tui/gniza.tcss +++ b/tui/gniza.tcss @@ -193,21 +193,10 @@ SelectionList { border: thick $accent; } -#ol-header { - height: auto; - margin: 0 0 1 0; -} - #ol-title { text-style: bold; color: #00cc00; - width: 1fr; -} - -#ol-spinner { - width: auto; - min-width: 4; - height: 1; + margin: 0 0 1 0; } #ol-log { diff --git a/tui/screens/backup.py b/tui/screens/backup.py index c097ecf..a927c0a 100644 --- a/tui/screens/backup.py +++ b/tui/screens/backup.py @@ -42,8 +42,6 @@ class BackupScreen(Screen): yield Footer() def on_button_pressed(self, event: Button.Pressed) -> None: - with open("/tmp/gniza_debug.log", "a") as f: - f.write(f"on_button_pressed: {event.button.id}\n") if event.button.id == "btn-back": self.app.pop_screen() elif event.button.id == "btn-backup": @@ -54,38 +52,23 @@ class BackupScreen(Screen): target = str(target_sel.value) remote_sel = self.query_one("#backup-remote", Select) remote = str(remote_sel.value) if isinstance(remote_sel.value, str) else "" - # Skip ConfirmDialog — go straight to OperationLog for debugging - self._confirmed_backup(target, remote) + msg = f"Run backup for target '{target}'?" + if remote: + msg += f"\nRemote: {remote}" + self.app.push_screen( + ConfirmDialog(msg, "Confirm Backup"), + callback=lambda ok: self._do_backup(target, remote) if ok else None, + ) elif event.button.id == "btn-backup-all": self.app.push_screen( ConfirmDialog("Backup ALL targets now?", "Confirm Backup"), - callback=lambda ok: self._confirmed_backup_all() if ok else None, + callback=lambda ok: self._do_backup_all() if ok else None, ) - def _confirmed_backup(self, target: str, remote: str) -> None: - import traceback - dbg = open("/tmp/gniza_debug.log", "a") - dbg.write("=== _confirmed_backup called ===\n") - dbg.write(f"target={target} remote={remote}\n") - try: - log_screen = OperationLog(f"Backup: {target}") - dbg.write("OperationLog created\n") - self.app.push_screen(log_screen) - dbg.write("push_screen called\n") - self._run_backup(log_screen, target, remote) - dbg.write("_run_backup started\n") - except Exception as e: - dbg.write(f"ERROR: {e}\n") - dbg.write(traceback.format_exc()) - dbg.close() - - def _confirmed_backup_all(self) -> None: - log_screen = OperationLog("Backup All Targets") - self.app.push_screen(log_screen) - self._run_backup_all(log_screen) - @work - async def _run_backup(self, log_screen: OperationLog, target: str, remote: str) -> None: + async def _do_backup(self, target: str, remote: str) -> None: + log_screen = OperationLog(f"Backup: {target}") + self.app.push_screen(log_screen) args = ["backup", f"--target={target}"] if remote: args.append(f"--remote={remote}") @@ -94,16 +77,16 @@ class BackupScreen(Screen): log_screen.write("\n[green]Backup completed successfully.[/green]") else: log_screen.write(f"\n[red]Backup failed (exit code {rc}).[/red]") - log_screen.finish() @work - async def _run_backup_all(self, log_screen: OperationLog) -> None: + async def _do_backup_all(self) -> None: + log_screen = OperationLog("Backup All Targets") + self.app.push_screen(log_screen) rc = await stream_cli(log_screen.write, "backup", "--all") if rc == 0: log_screen.write("\n[green]All backups completed.[/green]") else: log_screen.write(f"\n[red]Backup failed (exit code {rc}).[/red]") - log_screen.finish() def action_go_back(self) -> None: self.app.pop_screen() diff --git a/tui/screens/remotes.py b/tui/screens/remotes.py index 4703d2e..e742b4e 100644 --- a/tui/screens/remotes.py +++ b/tui/screens/remotes.py @@ -81,13 +81,10 @@ class RemotesScreen(Screen): else: self.notify("Select a remote first", severity="warning") - def _test_remote(self, name: str) -> None: + @work + async def _test_remote(self, name: str) -> None: log_screen = OperationLog(f"Testing Remote: {name}") self.app.push_screen(log_screen) - self._run_test_remote(log_screen, name) - - @work - async def _run_test_remote(self, log_screen: OperationLog, name: str) -> None: rc, stdout, stderr = await run_cli("remotes", "test", f"--name={name}") if stdout: log_screen.write(stdout) @@ -97,7 +94,6 @@ class RemotesScreen(Screen): log_screen.write("\n[green]Connection test passed.[/green]") else: log_screen.write(f"\n[red]Connection test failed (exit code {rc}).[/red]") - log_screen.finish() def _delete_remote(self, name: str) -> None: conf = CONFIG_DIR / "remotes.d" / f"{name}.conf" diff --git a/tui/screens/restore.py b/tui/screens/restore.py index 586d44a..9b186c7 100644 --- a/tui/screens/restore.py +++ b/tui/screens/restore.py @@ -107,16 +107,13 @@ class RestoreScreen(Screen): msg += "\nLocation: In-place" self.app.push_screen( ConfirmDialog(msg, "Confirm Restore"), - callback=lambda ok: self._confirmed_restore(target, remote, snapshot, dest) if ok else None, + callback=lambda ok: self._do_restore(target, remote, snapshot, dest) if ok else None, ) - def _confirmed_restore(self, target: str, remote: str, snapshot: str, dest: str) -> None: + @work + async def _do_restore(self, target: str, remote: str, snapshot: str, dest: str) -> None: log_screen = OperationLog(f"Restore: {target}") self.app.push_screen(log_screen) - self._run_restore(log_screen, target, remote, snapshot, dest) - - @work - async def _run_restore(self, log_screen: OperationLog, target: str, remote: str, snapshot: str, dest: str) -> None: args = ["restore", f"--target={target}", f"--remote={remote}", f"--snapshot={snapshot}"] if dest: args.append(f"--dest={dest}") @@ -125,7 +122,6 @@ class RestoreScreen(Screen): log_screen.write("\n[green]Restore completed successfully.[/green]") else: log_screen.write(f"\n[red]Restore failed (exit code {rc}).[/red]") - log_screen.finish() def action_go_back(self) -> None: self.app.pop_screen() diff --git a/tui/screens/retention.py b/tui/screens/retention.py index 9d4da7b..0a99d96 100644 --- a/tui/screens/retention.py +++ b/tui/screens/retention.py @@ -51,12 +51,12 @@ class RetentionScreen(Screen): target = str(target_sel.value) self.app.push_screen( ConfirmDialog(f"Run retention cleanup for '{target}'?", "Confirm"), - callback=lambda ok: self._confirmed_cleanup(target) if ok else None, + callback=lambda ok: self._do_cleanup(target) if ok else None, ) elif event.button.id == "btn-cleanup-all": self.app.push_screen( ConfirmDialog("Run retention cleanup for ALL targets?", "Confirm"), - callback=lambda ok: self._confirmed_cleanup_all() if ok else None, + callback=lambda ok: self._do_cleanup_all() if ok else None, ) elif event.button.id == "btn-save-count": val = self.query_one("#ret-count", Input).value.strip() @@ -66,33 +66,25 @@ class RetentionScreen(Screen): update_conf_key(CONFIG_DIR / "gniza.conf", "RETENTION_COUNT", val) self.notify(f"Retention count set to {val}.") - def _confirmed_cleanup(self, target: str) -> None: + @work + async def _do_cleanup(self, target: str) -> None: log_screen = OperationLog(f"Retention: {target}") self.app.push_screen(log_screen) - self._run_cleanup(log_screen, target) - - def _confirmed_cleanup_all(self) -> None: - log_screen = OperationLog("Retention: All Targets") - self.app.push_screen(log_screen) - self._run_cleanup_all(log_screen) - - @work - async def _run_cleanup(self, log_screen: OperationLog, target: str) -> None: rc = await stream_cli(log_screen.write, "retention", f"--target={target}") if rc == 0: log_screen.write("\n[green]Cleanup completed.[/green]") else: log_screen.write(f"\n[red]Cleanup failed (exit code {rc}).[/red]") - log_screen.finish() @work - async def _run_cleanup_all(self, log_screen: OperationLog) -> None: + async def _do_cleanup_all(self) -> None: + log_screen = OperationLog("Retention: All Targets") + self.app.push_screen(log_screen) rc = await stream_cli(log_screen.write, "retention", "--all") if rc == 0: log_screen.write("\n[green]All cleanups completed.[/green]") else: log_screen.write(f"\n[red]Cleanup failed (exit code {rc}).[/red]") - log_screen.finish() def action_go_back(self) -> None: self.app.pop_screen() diff --git a/tui/screens/schedule.py b/tui/screens/schedule.py index cd2a345..db0a06d 100644 --- a/tui/screens/schedule.py +++ b/tui/screens/schedule.py @@ -216,47 +216,35 @@ class ScheduleScreen(Screen): self.notify(f"Schedule '{name}' deleted.") self._refresh_table() - def _install_schedules(self) -> None: + @work + async def _install_schedules(self) -> None: log_screen = OperationLog("Install Schedules") self.app.push_screen(log_screen) - self._run_install(log_screen) - - @work - async def _run_install(self, log_screen: OperationLog) -> None: rc, stdout, stderr = await run_cli("schedule", "install") if stdout: log_screen.write(stdout) if stderr: log_screen.write(stderr) - log_screen.finish() - - def _remove_schedules(self) -> None: - log_screen = OperationLog("Remove Schedules") - self.app.push_screen(log_screen) - self._run_remove(log_screen) @work - async def _run_remove(self, log_screen: OperationLog) -> None: + async def _remove_schedules(self) -> None: + log_screen = OperationLog("Remove Schedules") + self.app.push_screen(log_screen) rc, stdout, stderr = await run_cli("schedule", "remove") if stdout: log_screen.write(stdout) if stderr: log_screen.write(stderr) - log_screen.finish() - - def _show_crontab(self) -> None: - log_screen = OperationLog("Current Crontab") - self.app.push_screen(log_screen) - self._run_show(log_screen) @work - async def _run_show(self, log_screen: OperationLog) -> None: + async def _show_crontab(self) -> None: + log_screen = OperationLog("Current Crontab") + self.app.push_screen(log_screen) rc, stdout, stderr = await run_cli("schedule", "show") if stdout: log_screen.write(stdout) if stderr: log_screen.write(stderr) - log_screen.finish() def action_go_back(self) -> None: self.app.pop_screen() diff --git a/tui/widgets/confirm_dialog.py b/tui/widgets/confirm_dialog.py index 9d77391..c08142c 100644 --- a/tui/widgets/confirm_dialog.py +++ b/tui/widgets/confirm_dialog.py @@ -22,7 +22,6 @@ class ConfirmDialog(ModalScreen[bool]): yield Button("No", variant="default", id="cd-no") def on_button_pressed(self, event: Button.Pressed) -> None: - self.notify(f"DEBUG CD: button={event.button.id}, dismissing with {event.button.id == 'cd-yes'}") self.dismiss(event.button.id == "cd-yes") def action_cancel(self) -> None: diff --git a/tui/widgets/operation_log.py b/tui/widgets/operation_log.py index cb3ff4a..1b1176a 100644 --- a/tui/widgets/operation_log.py +++ b/tui/widgets/operation_log.py @@ -1,3 +1,5 @@ +import asyncio + from rich.text import Text from textual.app import ComposeResult from textual.screen import ModalScreen @@ -12,29 +14,41 @@ class OperationLog(ModalScreen[None]): def __init__(self, title: str = "Operation Output"): super().__init__() self._title = title - self._running = True + self._mounted_event = asyncio.Event() + self._buffer: list[str] = [] def compose(self) -> ComposeResult: with Vertical(id="op-log"): yield Static(self._title, id="ol-title") - yield Static("Running...", id="ol-status") + yield RichLog(id="ol-log", wrap=True, highlight=True, markup=True) yield Button("Close", variant="primary", id="ol-close") + def on_mount(self) -> None: + # Flush any buffered writes + log = self.query_one("#ol-log", RichLog) + for text in self._buffer: + self._write_to_log(log, text) + self._buffer.clear() + self._mounted_event.set() + def on_button_pressed(self, event: Button.Pressed) -> None: self.dismiss(None) def action_close(self) -> None: self.dismiss(None) - def finish(self) -> None: - self._running = False - try: - self.query_one("#ol-status", Static).update("Done ✅") - except Exception: - pass + def _write_to_log(self, log: RichLog, text: str) -> None: + if "[" in text and "[/" in text: + log.write(Text.from_markup(text)) + else: + log.write(text) def write(self, text: str) -> None: + if not self._mounted_event.is_set(): + self._buffer.append(text) + return try: - self.query_one("#ol-status", Static).update(text) + log = self.query_one("#ol-log", RichLog) + self._write_to_log(log, text) except Exception: - pass + self._buffer.append(text)