Add animated spinner to OperationLog using Rich Spinner
Uses rich.spinner.Spinner with set_interval refresh instead of Textual's LoadingIndicator which caused rendering failures. Spinner shows dots animation while running, changes to checkmark on finish. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -193,10 +193,21 @@ SelectionList {
|
||||
border: thick $accent;
|
||||
}
|
||||
|
||||
#ol-header {
|
||||
height: auto;
|
||||
margin: 0 0 1 0;
|
||||
}
|
||||
|
||||
#ol-title {
|
||||
text-style: bold;
|
||||
color: #00cc00;
|
||||
margin: 0 0 1 0;
|
||||
width: 1fr;
|
||||
}
|
||||
|
||||
#ol-spinner {
|
||||
width: auto;
|
||||
min-width: 3;
|
||||
height: 1;
|
||||
}
|
||||
|
||||
#ol-log {
|
||||
|
||||
@@ -77,6 +77,7 @@ 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 _do_backup_all(self) -> None:
|
||||
@@ -87,6 +88,7 @@ class BackupScreen(Screen):
|
||||
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()
|
||||
|
||||
@@ -94,6 +94,7 @@ 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"
|
||||
|
||||
@@ -122,6 +122,7 @@ 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()
|
||||
|
||||
@@ -75,6 +75,7 @@ class RetentionScreen(Screen):
|
||||
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 _do_cleanup_all(self) -> None:
|
||||
@@ -85,6 +86,7 @@ class RetentionScreen(Screen):
|
||||
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()
|
||||
|
||||
@@ -1,10 +1,32 @@
|
||||
import asyncio
|
||||
|
||||
from rich.spinner import Spinner as RichSpinner
|
||||
from rich.text import Text
|
||||
from textual.app import ComposeResult
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import RichLog, Button, Static
|
||||
from textual.containers import Vertical
|
||||
from textual.containers import Vertical, Horizontal
|
||||
from textual.timer import Timer
|
||||
|
||||
|
||||
class SpinnerWidget(Static):
|
||||
"""Animated spinner using Rich's Spinner renderable."""
|
||||
|
||||
def __init__(self, style: str = "dots", **kwargs):
|
||||
super().__init__("", **kwargs)
|
||||
self._spinner = RichSpinner(style)
|
||||
self._timer: Timer | None = None
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self._timer = self.set_interval(1 / 12, self._tick)
|
||||
|
||||
def _tick(self) -> None:
|
||||
self.update(self._spinner)
|
||||
|
||||
def stop(self) -> None:
|
||||
if self._timer:
|
||||
self._timer.stop()
|
||||
self.update("✅")
|
||||
|
||||
|
||||
class OperationLog(ModalScreen[None]):
|
||||
@@ -19,7 +41,9 @@ class OperationLog(ModalScreen[None]):
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="op-log"):
|
||||
with Horizontal(id="ol-header"):
|
||||
yield Static(self._title, id="ol-title")
|
||||
yield SpinnerWidget(id="ol-spinner")
|
||||
yield RichLog(id="ol-log", wrap=True, highlight=True, markup=True)
|
||||
yield Button("Close", variant="primary", id="ol-close")
|
||||
|
||||
@@ -43,6 +67,12 @@ class OperationLog(ModalScreen[None]):
|
||||
else:
|
||||
log.write(text)
|
||||
|
||||
def finish(self) -> None:
|
||||
try:
|
||||
self.query_one("#ol-spinner", SpinnerWidget).stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def write(self, text: str) -> None:
|
||||
if not self._mounted_event.is_set():
|
||||
self._buffer.append(text)
|
||||
|
||||
Reference in New Issue
Block a user