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}" msg += f"\nRemote: {remote}"
self.app.push_screen( self.app.push_screen(
ConfirmDialog(msg, "Confirm Backup"), 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": elif event.button.id == "btn-backup-all":
self.app.push_screen( self.app.push_screen(
ConfirmDialog("Backup ALL targets now?", "Confirm Backup"), 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 def _confirmed_backup(self, target: str, remote: str) -> None:
async def _do_backup(self, target: str, remote: str) -> None:
log_screen = OperationLog(f"Backup: {target}") log_screen = OperationLog(f"Backup: {target}")
self.app.push_screen(log_screen) 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}"] args = ["backup", f"--target={target}"]
if remote: if remote:
args.append(f"--remote={remote}") args.append(f"--remote={remote}")
@@ -81,10 +88,7 @@ class BackupScreen(Screen):
log_screen.finish() log_screen.finish()
@work @work
async def _do_backup_all(self) -> None: async def _run_backup_all(self, log_screen: OperationLog) -> None:
log_screen = OperationLog("Backup All Targets")
self.app.push_screen(log_screen)
await log_screen.wait_ready()
rc = await stream_cli(log_screen.write, "backup", "--all") rc = await stream_cli(log_screen.write, "backup", "--all")
if rc == 0: if rc == 0:
log_screen.write("\n[green]All backups completed.[/green]") log_screen.write("\n[green]All backups completed.[/green]")

View File

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

View File

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

View File

@@ -51,12 +51,12 @@ class RetentionScreen(Screen):
target = str(target_sel.value) target = str(target_sel.value)
self.app.push_screen( self.app.push_screen(
ConfirmDialog(f"Run retention cleanup for '{target}'?", "Confirm"), 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": elif event.button.id == "btn-cleanup-all":
self.app.push_screen( self.app.push_screen(
ConfirmDialog("Run retention cleanup for ALL targets?", "Confirm"), 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": elif event.button.id == "btn-save-count":
val = self.query_one("#ret-count", Input).value.strip() 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) update_conf_key(CONFIG_DIR / "gniza.conf", "RETENTION_COUNT", val)
self.notify(f"Retention count set to {val}.") self.notify(f"Retention count set to {val}.")
@work def _confirmed_cleanup(self, target: str) -> None:
async def _do_cleanup(self, target: str) -> None:
log_screen = OperationLog(f"Retention: {target}") log_screen = OperationLog(f"Retention: {target}")
self.app.push_screen(log_screen) 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}") rc = await stream_cli(log_screen.write, "retention", f"--target={target}")
if rc == 0: if rc == 0:
log_screen.write("\n[green]Cleanup completed.[/green]") log_screen.write("\n[green]Cleanup completed.[/green]")
@@ -79,10 +86,7 @@ class RetentionScreen(Screen):
log_screen.finish() log_screen.finish()
@work @work
async def _do_cleanup_all(self) -> None: async def _run_cleanup_all(self, log_screen: OperationLog) -> None:
log_screen = OperationLog("Retention: All Targets")
self.app.push_screen(log_screen)
await log_screen.wait_ready()
rc = await stream_cli(log_screen.write, "retention", "--all") rc = await stream_cli(log_screen.write, "retention", "--all")
if rc == 0: if rc == 0:
log_screen.write("\n[green]All cleanups completed.[/green]") 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.notify(f"Schedule '{name}' deleted.")
self._refresh_table() self._refresh_table()
@work def _install_schedules(self) -> None:
async def _install_schedules(self) -> None:
log_screen = OperationLog("Install Schedules") log_screen = OperationLog("Install Schedules")
self.app.push_screen(log_screen) 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") rc, stdout, stderr = await run_cli("schedule", "install")
if stdout: if stdout:
log_screen.write(stdout) log_screen.write(stdout)
@@ -228,11 +230,13 @@ class ScheduleScreen(Screen):
log_screen.write(stderr) log_screen.write(stderr)
log_screen.finish() log_screen.finish()
@work def _remove_schedules(self) -> None:
async def _remove_schedules(self) -> None:
log_screen = OperationLog("Remove Schedules") log_screen = OperationLog("Remove Schedules")
self.app.push_screen(log_screen) 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") rc, stdout, stderr = await run_cli("schedule", "remove")
if stdout: if stdout:
log_screen.write(stdout) log_screen.write(stdout)
@@ -240,11 +244,13 @@ class ScheduleScreen(Screen):
log_screen.write(stderr) log_screen.write(stderr)
log_screen.finish() log_screen.finish()
@work def _show_crontab(self) -> None:
async def _show_crontab(self) -> None:
log_screen = OperationLog("Current Crontab") log_screen = OperationLog("Current Crontab")
self.app.push_screen(log_screen) 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") rc, stdout, stderr = await run_cli("schedule", "show")
if stdout: if stdout:
log_screen.write(stdout) log_screen.write(stdout)

View File

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