Fix OperationLog not rendering by removing LoadingIndicator

LoadingIndicator was causing OperationLog ModalScreen to fail silently
during compose. Replaced with a simple Static emoji spinner ().
Reverted screen push patterns back to simple callback approach.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shuki
2026-03-06 04:11:16 +02:00
parent f70ce53dc5
commit 4d4b55047b
6 changed files with 56 additions and 54 deletions

View File

@@ -57,19 +57,26 @@ class BackupScreen(Screen):
msg += f"\nRemote: {remote}"
self.app.push_screen(
ConfirmDialog(msg, "Confirm Backup"),
callback=lambda ok: self._do_backup(target, remote) if ok else None,
callback=lambda ok: self._confirmed_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._do_backup_all() if ok else None,
callback=lambda ok: self._confirmed_backup_all() if ok else None,
)
@work
async def _do_backup(self, target: str, remote: str) -> None:
def _confirmed_backup(self, target: str, remote: str) -> None:
log_screen = OperationLog(f"Backup: {target}")
self.app.push_screen(log_screen)
await log_screen.wait_ready()
self._run_backup(log_screen, target, remote)
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:
args = ["backup", f"--target={target}"]
if remote:
args.append(f"--remote={remote}")
@@ -81,10 +88,7 @@ class BackupScreen(Screen):
log_screen.finish()
@work
async def _do_backup_all(self) -> None:
log_screen = OperationLog("Backup All Targets")
self.app.push_screen(log_screen)
await log_screen.wait_ready()
async def _run_backup_all(self, log_screen: OperationLog) -> None:
rc = await stream_cli(log_screen.write, "backup", "--all")
if rc == 0:
log_screen.write("\n[green]All backups completed.[/green]")

View File

@@ -81,11 +81,13 @@ class RemotesScreen(Screen):
else:
self.notify("Select a remote first", severity="warning")
@work
async def _test_remote(self, name: str) -> None:
def _test_remote(self, name: str) -> None:
log_screen = OperationLog(f"Testing Remote: {name}")
self.app.push_screen(log_screen)
await log_screen.wait_ready()
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)

View File

@@ -107,14 +107,16 @@ class RestoreScreen(Screen):
msg += "\nLocation: In-place"
self.app.push_screen(
ConfirmDialog(msg, "Confirm Restore"),
callback=lambda ok: self._do_restore(target, remote, snapshot, dest) if ok else None,
callback=lambda ok: self._confirmed_restore(target, remote, snapshot, dest) if ok else None,
)
@work
async def _do_restore(self, target: str, remote: str, snapshot: str, dest: str) -> None:
def _confirmed_restore(self, target: str, remote: str, snapshot: str, dest: str) -> None:
log_screen = OperationLog(f"Restore: {target}")
self.app.push_screen(log_screen)
await log_screen.wait_ready()
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}")

View File

@@ -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._do_cleanup(target) if ok else None,
callback=lambda ok: self._confirmed_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._do_cleanup_all() if ok else None,
callback=lambda ok: self._confirmed_cleanup_all() if ok else None,
)
elif event.button.id == "btn-save-count":
val = self.query_one("#ret-count", Input).value.strip()
@@ -66,11 +66,18 @@ class RetentionScreen(Screen):
update_conf_key(CONFIG_DIR / "gniza.conf", "RETENTION_COUNT", val)
self.notify(f"Retention count set to {val}.")
@work
async def _do_cleanup(self, target: str) -> None:
def _confirmed_cleanup(self, target: str) -> None:
log_screen = OperationLog(f"Retention: {target}")
self.app.push_screen(log_screen)
await log_screen.wait_ready()
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]")
@@ -79,10 +86,7 @@ class RetentionScreen(Screen):
log_screen.finish()
@work
async def _do_cleanup_all(self) -> None:
log_screen = OperationLog("Retention: All Targets")
self.app.push_screen(log_screen)
await log_screen.wait_ready()
async def _run_cleanup_all(self, log_screen: OperationLog) -> None:
rc = await stream_cli(log_screen.write, "retention", "--all")
if rc == 0:
log_screen.write("\n[green]All cleanups completed.[/green]")

View File

@@ -216,11 +216,13 @@ class ScheduleScreen(Screen):
self.notify(f"Schedule '{name}' deleted.")
self._refresh_table()
@work
async def _install_schedules(self) -> None:
def _install_schedules(self) -> None:
log_screen = OperationLog("Install Schedules")
self.app.push_screen(log_screen)
await log_screen.wait_ready()
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)
@@ -228,11 +230,13 @@ class ScheduleScreen(Screen):
log_screen.write(stderr)
log_screen.finish()
@work
async def _remove_schedules(self) -> None:
def _remove_schedules(self) -> None:
log_screen = OperationLog("Remove Schedules")
self.app.push_screen(log_screen)
await log_screen.wait_ready()
self._run_remove(log_screen)
@work
async def _run_remove(self, log_screen: OperationLog) -> None:
rc, stdout, stderr = await run_cli("schedule", "remove")
if stdout:
log_screen.write(stdout)
@@ -240,11 +244,13 @@ class ScheduleScreen(Screen):
log_screen.write(stderr)
log_screen.finish()
@work
async def _show_crontab(self) -> None:
def _show_crontab(self) -> None:
log_screen = OperationLog("Current Crontab")
self.app.push_screen(log_screen)
await log_screen.wait_ready()
self._run_show(log_screen)
@work
async def _run_show(self, log_screen: OperationLog) -> None:
rc, stdout, stderr = await run_cli("schedule", "show")
if stdout:
log_screen.write(stdout)

View File

@@ -3,7 +3,7 @@ import asyncio
from rich.text import Text
from textual.app import ComposeResult
from textual.screen import ModalScreen
from textual.widgets import RichLog, Button, Static, LoadingIndicator
from textual.widgets import RichLog, Button, Static
from textual.containers import Vertical, Horizontal
@@ -14,26 +14,16 @@ class OperationLog(ModalScreen[None]):
def __init__(self, title: str = "Operation Output"):
super().__init__()
self._title = title
self._mounted_event = asyncio.Event()
self._buffer: list[str] = []
self._running = True
def compose(self) -> ComposeResult:
with Vertical(id="op-log"):
with Horizontal(id="ol-header"):
yield Static(self._title, id="ol-title")
yield LoadingIndicator(id="ol-spinner")
yield Static("", id="ol-spinner")
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)
@@ -46,22 +36,16 @@ class OperationLog(ModalScreen[None]):
else:
log.write(text)
async def wait_ready(self) -> None:
await self._mounted_event.wait()
def finish(self) -> None:
self._running = False
try:
self.query_one("#ol-spinner", LoadingIndicator).display = False
self.query_one("#ol-spinner", Static).update("")
except Exception:
pass
def write(self, text: str) -> None:
if not self._mounted_event.is_set():
self._buffer.append(text)
return
try:
log = self.query_one("#ol-log", RichLog)
self._write_to_log(log, text)
except Exception:
self._buffer.append(text)
pass