"""Reusable, state-local status presentation for Textual surfaces."""
from __future__ import annotations
from typing import Final, Literal, override
from textual.app import ComposeResult
from textual.containers import Vertical
from textual.widgets import LoadingIndicator, Static
from ..components.theme import tokenised
type StatusTone = Literal["idle", "progress", "success", "warning", "error"]
"""Closed presentation states supported by :class:`PinnedStatusBar`."""
_TONES: Final[tuple[StatusTone, ...]] = ("idle", "progress", "success", "warning", "error")
#: The tone a status carries before any has been applied.
_DEFAULT_TONE: Final[StatusTone] = "idle"
_TONE_CLASSES: Final[tuple[str, ...]] = tuple(f"tone-{tone}" for tone in _TONES)
_GLYPH: Final[dict[StatusTone, str]] = {
"idle": "·",
"progress": "◌",
"success": "✓",
"warning": "⚠",
"error": "✕",
}
[docs]
class PinnedStatusBar(Vertical):
"""Render supplied summary and status text in a pinned screen channel."""
DEFAULT_CSS = tokenised("""
PinnedStatusBar {
dock: top;
width: 100%;
height: auto;
padding: $cadrumo-space-0 $cadrumo-gutter;
background: $surface;
border-bottom: $cadrumo-rule $primary;
}
PinnedStatusBar.empty { display: none; }
PinnedStatusBar > .status-summary {
width: 100%;
height: auto;
}
PinnedStatusBar > .status-summary.empty { display: none; }
PinnedStatusBar > .status-message {
width: 100%;
height: auto;
}
PinnedStatusBar > .status-summary {
color: $text-muted;
text-style: bold;
}
PinnedStatusBar > .status-spinner { height: $cadrumo-band-height; display: none; }
PinnedStatusBar.tone-progress > .status-spinner { display: block; }
PinnedStatusBar.tone-idle > .status-message { color: $text-muted; }
PinnedStatusBar.tone-progress > .status-message { color: $accent; }
PinnedStatusBar.tone-success > .status-message { color: $success; }
PinnedStatusBar.tone-warning > .status-message { color: $warning; }
PinnedStatusBar.tone-error > .status-message { color: $error; text-style: bold; }
""")
def __init__(self, *, summary: str = "", id: str | None = None, classes: str | None = None) -> None:
"""Initialize the pinned channel with an optional supplied summary."""
super().__init__(id=id, classes=classes)
self._summary = self._require_text(summary, field="summary")
self._message = ""
self.add_class("tone-idle")
self.set_class(not self._summary, "empty")
@staticmethod
def _require_text(value: object, *, field: str) -> str:
if not isinstance(value, str):
raise TypeError(f"{field} must be str, got {type(value).__name__}")
return value
@property
def summary(self) -> str:
"""Plain summary currently rendered by the first line."""
return self._summary
@property
def message(self) -> str:
"""Plain status text currently rendered by the second line."""
return self._message
@property
def tone(self) -> StatusTone:
"""Closed tone currently rendered by the status message."""
for tone in _TONES:
if self.has_class(f"tone-{tone}"):
return tone
return _DEFAULT_TONE
[docs]
@override
def compose(self) -> ComposeResult:
yield Static(self._summary, markup=False, classes="status-summary")
yield LoadingIndicator(classes="status-spinner")
yield Static("", markup=False, classes="status-message")
[docs]
def clear_message(self) -> None:
"""Render an idle empty message and collapse an empty channel."""
self._set_message("idle", "")
[docs]
def show_progress(self, message: str) -> None:
"""Render the supplied in-progress status text."""
self._set_message("progress", message)
[docs]
def show_success(self, message: str) -> None:
"""Render the supplied completed status text."""
self._set_message("success", message)
[docs]
def show_warning(self, message: str) -> None:
"""Render the supplied attention status text."""
self._set_message("warning", message)
[docs]
def show_error(self, message: str) -> None:
"""Render the supplied failure or refusal status text."""
self._set_message("error", message)
def _set_message(self, tone: StatusTone, message: str) -> None:
rendered = self._require_text(message, field="message")
self._message = rendered
self.remove_class(*_TONE_CLASSES)
self.add_class(f"tone-{tone}")
line = "" if not rendered else f"{_GLYPH[tone]} {rendered}"
self.query_one(".status-message", Static).update(line)
self._sync_visibility()
def _sync_visibility(self) -> None:
"""Consume screen space only while supplied text has something to show."""
self.set_class(not (self._summary or self._message), "empty")
__all__ = ["PinnedStatusBar", "StatusTone"]