Files
MeshTerm/meshterm/ui/tui/fastrender.py
T
Jean-Pierre MartineauandClaude Opus 5 4545173c65 Every public callable says what it is for
The 46 missing docstrings, written rather than generated.

Most of the test ones already carried the explanation — as a comment sitting on
the first line of the body. That prose is the docstring; it was only in the
wrong place, so the config editor's PIN and lane tests keep their own words and
lose the duplicate comment. The rest are new: the two `repo` fixtures, the
counting `FakeDevice` in `test_device_state` (each method now says what it
answers *and* that it counts the call), the PicoCalc chrome and 16-slot theme
tests, and three one-offs.

In the package: `PathLine` and `FastRenderer`'s constructors point at the class
docstring that already documents their arguments rather than repeating it,
`SelectItem.__post_init__` and `courier_screen`'s opener say what they cache and
why, and `emoji_width.create_content` explains the memo it wraps the line lookup
in. `flash.py` and `uart1-mux.py` get theirs too, and `tests/__init__.py`
finally says what the package is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 20:28:01 -04:00

173 lines
8.4 KiB
Python

"""Write an already-composed frame straight to the terminal, one changed row at a time.
MeshTerm composes every frame itself: :func:`~meshterm.ui.tui.frame.compose_base` returns
a finished ANSI string, exactly one line per terminal row, already sliced to the viewport
and already folded to the platform's glyph set. prompt_toolkit's own renderer then takes
that string and does a full round trip with it — parses the ANSI back into style/text
fragments, writes every one of the ~1400 cells into a fresh grid of ``Char`` objects,
diffs that grid against the previous frame's grid, and re-emits ANSI for the cells that
moved.
That round trip is the single largest cost in a keystroke on the PicoCalc, and it is paid
in full on *every* paint — a measured 70-125 ms whether the frame changed by one row, by
all of them, or not at all (the 2 s header tick recomposes an identical frame and still
pays it). Since we already know the frame as rows of text, the same pixels can be reached
by comparing this frame's rows against the last frame's rows and rewriting only the ones
that differ: ~0.1 ms, and a smaller write besides.
The bypass is deliberately narrow. It runs only when the frame is a plain full-screen
composition — one base screen, no floating dialog and no busy overlay — because those are
laid out by prompt_toolkit's float containers rather than by us, and compositing them here
would mean re-implementing that placement. Anything else falls straight through to the
stock renderer, which stays the authority: :meth:`FastRenderer.render` is an accelerator
for the common case, never a second implementation of the whole layout engine.
Two details make the row diff safe rather than merely fast:
* **A row is rewritten whole, from column 0.** The terminal's own cursor advance re-lays
the line, so a glyph it draws narrower than we measured cannot leave the rest of the row
shifted — the same guarantee :meth:`~meshterm.ui.tui.session.TuiSession._emit` relies on,
reached here for free rather than by a scrub pass.
* **Each row is prefixed with a reset and followed by an erase-to-end-of-line**, so no
style leaks in from the row above and a row that got shorter leaves no tail behind.
"""
from __future__ import annotations
import os
from collections.abc import Callable
from prompt_toolkit.renderer import Renderer
def enabled() -> bool:
"""Whether the direct row writer is active.
On by default — it is 2-3x on every navigation keystroke and its output is verified
byte-identical against the stock renderer by reading the console back. The escape
hatch, for a terminal that ever disagrees, is the ``fast_render`` preference;
``MESHTERM_FASTRENDER=0``/``=1`` overrules it for one run without editing anything.
Read at session build, so a change made on the Preferences page takes hold at the
next launch — which is what that preference's description promises.
"""
from ...core.preferences import current as current_preferences
override = os.environ.get("MESHTERM_FASTRENDER")
if override is not None:
return override != "0"
return bool(current_preferences().fast_render)
class FastRenderer(Renderer):
"""A :class:`~prompt_toolkit.renderer.Renderer` that short-circuits simple frames.
Args:
frame_source: Returns the composed full-screen frame as one ANSI string (rows
joined by newlines), or ``None`` when this paint is not a plain base frame
and must go through prompt_toolkit's layout instead.
"""
def __init__(self, *args, frame_source: Callable[[], str | None], **kwargs) -> None:
"""Wrap the stock renderer, starting with no remembered frame.
With nothing to compare against, the first paint is always a full one; the
``frame_source`` argument is described in the class docstring above.
"""
super().__init__(*args, **kwargs)
self._frame_source = frame_source
self._prev_rows: list[str] | None = None
self._prev_size: tuple[int, int] | None = None
#: Paints answered here vs handed to the stock renderer — read by the bench.
self.fast_paints = 0
self.slow_paints = 0
def reset(self, _scroll: bool = False, leave_alternate_screen: bool = True) -> None:
"""Forget the last frame along with prompt_toolkit's own render state."""
self._prev_rows = None
super().reset(_scroll=_scroll, leave_alternate_screen=leave_alternate_screen)
def erase(self, leave_alternate_screen: bool = True) -> None:
"""The screen is being cleared out from under us — the next paint must be full."""
self._prev_rows = None
super().erase(leave_alternate_screen=leave_alternate_screen)
def render(self, app, layout, is_done: bool = False) -> None: # noqa: ANN001
"""Paint the frame, taking the row-diff path when this frame is a plain one."""
text = None if is_done else self._frame_source()
if text is None:
# A dialog, the busy overlay, or the closing paint: prompt_toolkit lays those
# out, so it must also own the diff — its grid is the only record of what is
# on screen, and it is about to be rebuilt from scratch anyway.
self._prev_rows = None
self.slow_paints += 1
super().render(app, layout, is_done)
return
output = self.output
pending = False # whether the terminal setup below has queued anything to flush
if self.full_screen and not self._in_alternate_screen:
self._in_alternate_screen = True
output.enter_alternate_screen()
pending = True
if not self._bracketed_paste_enabled:
output.enable_bracketed_paste()
self._bracketed_paste_enabled = True
pending = True
if not self._cursor_key_mode_reset:
output.reset_cursor_key_mode()
self._cursor_key_mode_reset = True
pending = True
size = output.get_size()
dims = (size.rows, size.columns)
rows = text.split("\n")[: size.rows]
# A resize invalidates both records of the screen: ours and prompt_toolkit's.
full = self._prev_rows is None or self._prev_size != dims
if not full and not pending and rows == self._prev_rows:
# The app repaints on a timer to keep the header's pulse moving, and most of
# those frames come back identical. Writing nothing at all is not merely cheaper
# than writing the rows again — it leaves the panel's damage region empty, so
# the display never flushes and the SPI bus stays quiet.
self._last_size = size
self.fast_paints += 1
return
if full:
self._last_screen = None
output.hide_cursor()
output.reset_attributes()
output.erase_screen()
output.cursor_goto(0, 0)
write = output.write_raw
prev = self._prev_rows
last = -2
for i, row in enumerate(rows):
if not full and i < len(prev) and prev[i] == row:
continue
# Stepping down a row with CRLF returns the terminal to a true column 0
# whatever happened on the row above, and costs three bytes less than a
# cursor address; only a real jump pays for one.
write("\r\n" if i == last + 1 else f"\x1b[{i + 1};1H")
# Erase *before* drawing, never after. A row that fills the terminal exactly
# leaves the cursor in the last column with wrap pending, and an erase-to-end
# there wipes the very character just written (the row's last cell went
# missing on the PicoCalc's 53-column console). Clearing first also drops the
# tail of a row that got shorter, which is what the erase was for.
write("\x1b[0m\x1b[K")
write(row)
last = i
# Park the cursor out of the text. The app hides it, but a terminal that ignores
# that should not leave it blinking in the middle of a row.
write(f"\x1b[{len(rows)};1H")
output.flush()
self._prev_rows = rows
self._prev_size = dims
self._last_size = size
# prompt_toolkit's grid no longer describes the screen — if a later paint falls
# back to it, it must redraw everything rather than diff against a stale record.
self._last_screen = None
self.fast_paints += 1