Files
gniza4linux/tui/widgets/operation_log.py
shuki d596a747a4 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>
2026-03-06 04:28:08 +02:00

85 lines
2.5 KiB
Python

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, 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]):
BINDINGS = [("escape", "close", "Close")]
def __init__(self, title: str = "Operation Output"):
super().__init__()
self._title = title
self._mounted_event = asyncio.Event()
self._buffer: list[str] = []
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")
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 _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 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)
return
try:
log = self.query_one("#ol-log", RichLog)
self._write_to_log(log, text)
except Exception:
self._buffer.append(text)