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;
|
border: thick $accent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ol-header {
|
||||||
|
height: auto;
|
||||||
|
margin: 0 0 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
#ol-title {
|
#ol-title {
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
color: #00cc00;
|
color: #00cc00;
|
||||||
margin: 0 0 1 0;
|
width: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ol-spinner {
|
||||||
|
width: auto;
|
||||||
|
min-width: 3;
|
||||||
|
height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ol-log {
|
#ol-log {
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ class BackupScreen(Screen):
|
|||||||
log_screen.write("\n[green]Backup completed successfully.[/green]")
|
log_screen.write("\n[green]Backup completed successfully.[/green]")
|
||||||
else:
|
else:
|
||||||
log_screen.write(f"\n[red]Backup failed (exit code {rc}).[/red]")
|
log_screen.write(f"\n[red]Backup failed (exit code {rc}).[/red]")
|
||||||
|
log_screen.finish()
|
||||||
|
|
||||||
@work
|
@work
|
||||||
async def _do_backup_all(self) -> None:
|
async def _do_backup_all(self) -> None:
|
||||||
@@ -87,6 +88,7 @@ class BackupScreen(Screen):
|
|||||||
log_screen.write("\n[green]All backups completed.[/green]")
|
log_screen.write("\n[green]All backups completed.[/green]")
|
||||||
else:
|
else:
|
||||||
log_screen.write(f"\n[red]Backup failed (exit code {rc}).[/red]")
|
log_screen.write(f"\n[red]Backup failed (exit code {rc}).[/red]")
|
||||||
|
log_screen.finish()
|
||||||
|
|
||||||
def action_go_back(self) -> None:
|
def action_go_back(self) -> None:
|
||||||
self.app.pop_screen()
|
self.app.pop_screen()
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class RemotesScreen(Screen):
|
|||||||
log_screen.write("\n[green]Connection test passed.[/green]")
|
log_screen.write("\n[green]Connection test passed.[/green]")
|
||||||
else:
|
else:
|
||||||
log_screen.write(f"\n[red]Connection test failed (exit code {rc}).[/red]")
|
log_screen.write(f"\n[red]Connection test failed (exit code {rc}).[/red]")
|
||||||
|
log_screen.finish()
|
||||||
|
|
||||||
def _delete_remote(self, name: str) -> None:
|
def _delete_remote(self, name: str) -> None:
|
||||||
conf = CONFIG_DIR / "remotes.d" / f"{name}.conf"
|
conf = CONFIG_DIR / "remotes.d" / f"{name}.conf"
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ class RestoreScreen(Screen):
|
|||||||
log_screen.write("\n[green]Restore completed successfully.[/green]")
|
log_screen.write("\n[green]Restore completed successfully.[/green]")
|
||||||
else:
|
else:
|
||||||
log_screen.write(f"\n[red]Restore failed (exit code {rc}).[/red]")
|
log_screen.write(f"\n[red]Restore failed (exit code {rc}).[/red]")
|
||||||
|
log_screen.finish()
|
||||||
|
|
||||||
def action_go_back(self) -> None:
|
def action_go_back(self) -> None:
|
||||||
self.app.pop_screen()
|
self.app.pop_screen()
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ class RetentionScreen(Screen):
|
|||||||
log_screen.write("\n[green]Cleanup completed.[/green]")
|
log_screen.write("\n[green]Cleanup completed.[/green]")
|
||||||
else:
|
else:
|
||||||
log_screen.write(f"\n[red]Cleanup failed (exit code {rc}).[/red]")
|
log_screen.write(f"\n[red]Cleanup failed (exit code {rc}).[/red]")
|
||||||
|
log_screen.finish()
|
||||||
|
|
||||||
@work
|
@work
|
||||||
async def _do_cleanup_all(self) -> None:
|
async def _do_cleanup_all(self) -> None:
|
||||||
@@ -85,6 +86,7 @@ class RetentionScreen(Screen):
|
|||||||
log_screen.write("\n[green]All cleanups completed.[/green]")
|
log_screen.write("\n[green]All cleanups completed.[/green]")
|
||||||
else:
|
else:
|
||||||
log_screen.write(f"\n[red]Cleanup failed (exit code {rc}).[/red]")
|
log_screen.write(f"\n[red]Cleanup failed (exit code {rc}).[/red]")
|
||||||
|
log_screen.finish()
|
||||||
|
|
||||||
def action_go_back(self) -> None:
|
def action_go_back(self) -> None:
|
||||||
self.app.pop_screen()
|
self.app.pop_screen()
|
||||||
|
|||||||
@@ -1,10 +1,32 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
from rich.spinner import Spinner as RichSpinner
|
||||||
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
|
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]):
|
class OperationLog(ModalScreen[None]):
|
||||||
@@ -19,7 +41,9 @@ class OperationLog(ModalScreen[None]):
|
|||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
with Vertical(id="op-log"):
|
with Vertical(id="op-log"):
|
||||||
|
with Horizontal(id="ol-header"):
|
||||||
yield Static(self._title, id="ol-title")
|
yield Static(self._title, id="ol-title")
|
||||||
|
yield SpinnerWidget(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")
|
||||||
|
|
||||||
@@ -43,6 +67,12 @@ class OperationLog(ModalScreen[None]):
|
|||||||
else:
|
else:
|
||||||
log.write(text)
|
log.write(text)
|
||||||
|
|
||||||
|
def finish(self) -> None:
|
||||||
|
try:
|
||||||
|
self.query_one("#ol-spinner", SpinnerWidget).stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def write(self, text: str) -> None:
|
def write(self, text: str) -> None:
|
||||||
if not self._mounted_event.is_set():
|
if not self._mounted_event.is_set():
|
||||||
self._buffer.append(text)
|
self._buffer.append(text)
|
||||||
|
|||||||
Reference in New Issue
Block a user