Source code for matmelt.ui.monitor_live
"""The monitor's live loop: a Rich ``Live`` context around a build callback.
The tiny UI-layer seam between the ``monitor`` command and Rich's ``Live``
display -- it exists so that *all* Rich usage stays inside ``ui/`` (the
one-console rule) while the command layer keeps the control flow: what to
render each tick (the ``build`` callback), how often, and when to stop.
This module never queries state, never sleeps forever on its own terms, and
never decides what the dashboard contains.
Terminal safety: the loop runs inside ``with Live(...)`` so that
``Live.__exit__`` always restores the terminal -- a Ctrl-C mid-refresh must
never leave the screen in Live's grip. ``KeyboardInterrupt`` therefore
*propagates* out of this function (after the context manager has cleaned
up); the calling command catches it and turns it into a clean exit, so the
user sees a short note, never a traceback.
"""
from __future__ import annotations
import time
from typing import TYPE_CHECKING
from rich.live import Live
from matmelt.ui.console import console
if TYPE_CHECKING:
from collections.abc import Callable
from rich.console import RenderableType
[docs]
def run_live(
build: Callable[[], RenderableType],
*,
interval_s: float,
max_ticks: int | None = None,
) -> None:
"""Render a renderable under Rich ``Live``, refreshing on an interval.
``screen=False`` is deliberate: the stacked panels redraw in place and
scroll away naturally when the loop ends, leaving the last frame visible in
the terminal history instead of wiping it.
Parameters
----------
build
Called for every frame. The first call happens *before* the display
starts, so the user sees a dashboard immediately rather than a blank
frame.
interval_s
Seconds to sleep between updates.
max_ticks
Bound the number of *updates* after the initial render. The testing
knob; ``None`` runs until interrupted.
Raises
------
KeyboardInterrupt
Propagated to the caller, as is any exception from ``build`` -- but
only after ``Live.__exit__`` has restored the terminal, which is the
whole point of the ``with`` block.
"""
with Live(build(), console=console, screen=False, refresh_per_second=4) as live:
ticks = 0
while max_ticks is None or ticks < max_ticks:
time.sleep(interval_s)
live.update(build())
ticks += 1