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