On a short frame, a graph's caption and node-type key step aside

Message paths and the mesh walk keep their list's rows on the Cardputer instead of
spending three on a key to marks that mean the same thing everywhere; the rule is
widgets.short_frame, the same one that draws a tab strip on one row. The message
paths budget also counts the key's real height, which wraps to two lines at 53
columns and had been counted as one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5.5 committed 2026-09-30 21:14:55 -04:00
1 parent 5675ba0e1f
commit 390a0a1616
6 files changed
+42 -30

No files matched your search

+15 -9
View File
@@ -75,6 +75,7 @@ from .widgets import (
revisit_note,
route_graph_style,
scope_text,
short_frame,
)
if TYPE_CHECKING:
@@ -322,20 +323,25 @@ class MessagePathsScreen(Screen):
# No blank line above the graph: its canvas already opens with air over the
# topmost lane, so a spacer here would read as two rows of margin.
revisit = self._revisit_line(width)
# The fan's own trailing chrome (caption, node-type key, the revisit note) plus the
# blank that sets the list apart — all struck from the budget before the ceiling is.
chrome = 2 + len(revisit) + 1
room = viewport - len(lines) - chrome - min(sum(heights), _LIST_MIN_LINES)
graph = self._graph_lines(width, max(_GRAPH_MIN_ROWS, min(_GRAPH_MAX_ROWS, room)))
if graph:
lines.extend(graph)
# The fan's caption and node-type key, which a short frame leaves out: the marks
# mean one thing app-wide, and the rows go to the list (see widgets.short_frame).
key: list[str] = []
if not short_frame(viewport):
far = "you" if not self._destination else self._destination
caption = Text(
f"origin → {far} · white = selected path · labels = hash byte",
style="faint",
)
lines.append(render_to_ansi(caption, width, no_wrap=True))
lines.extend(render_lines(node_type_legend(width=width), width, no_wrap=True))
key.append(render_to_ansi(caption, width, no_wrap=True))
key.extend(render_lines(node_type_legend(width=width), width, no_wrap=True))
# The fan's own trailing chrome (that key, the revisit note) plus the blank that
# sets the list apart — all struck from the budget before the ceiling is.
chrome = len(key) + len(revisit) + 1
room = viewport - len(lines) - chrome - min(sum(heights), _LIST_MIN_LINES)
graph = self._graph_lines(width, max(_GRAPH_MIN_ROWS, min(_GRAPH_MAX_ROWS, room)))
if graph:
lines.extend(graph)
lines.extend(key)
lines.extend(revisit)
lines.append("")
+2 -2
View File
@@ -131,8 +131,8 @@ from .widgets import (
format_ago,
highlighted_hash,
node_type_legend,
short_frame,
tab_air,
tab_compact,
tab_strip,
)
@@ -767,7 +767,7 @@ class NodeDetailScreen(Screen):
[t.name for t in self._tabs],
self._tab_index,
width,
compact=tab_compact(viewport),
compact=short_frame(viewport),
),
width,
no_wrap=True,
+2 -2
View File
@@ -62,8 +62,8 @@ from .widgets import (
node_type_legend,
route_graph_style,
self_marker,
short_frame,
tab_air,
tab_compact,
tab_strip,
)
@@ -649,7 +649,7 @@ class RecordScreen(Screen):
lines: list[str] = [render_to_ansi(self._header(), width, no_wrap=True)]
lines.extend([""] * tab_air())
strip = tab_strip(
self._tabs, self._tab_index, width, compact=tab_compact(self._scroll_viewport)
self._tabs, self._tab_index, width, compact=short_frame(self._scroll_viewport)
)
lines.extend(render_lines(strip, width))
lines.extend([""] * tab_air())
+8 -4
View File
@@ -76,7 +76,7 @@ from .theme import mark_rgb, name_style, snr_style
from .trace_screen import snr_bar
from .tui.render import crop_cells, query_line, render_to_ansi
from .tui.screen import ListWindow, Screen
from .widgets import DEFAULT_GLYPH, NODE_GLYPHS, format_age, highlighted_hash
from .widgets import DEFAULT_GLYPH, NODE_GLYPHS, format_age, highlighted_hash, short_frame
if TYPE_CHECKING:
from ..context import AppContext
@@ -557,14 +557,18 @@ class WalkScreen(Screen):
viewport = self._scroll_viewport # recorded by the frame before this render
header = self._header_lines(width, depths)
chrome = len(header) + 3 + self._query_row() # legend, blank, list heading (+find echo)
# The legend and the blank under it go on a short frame, where the rows are the
# list's (see widgets.short_frame); the list's heading stays either way.
keyed = not short_frame(viewport)
chrome = len(header) + (3 if keyed else 1) + self._query_row() # (+find echo)
canvas_h = self._canvas_height(viewport, chrome)
list_win = max(1, viewport - chrome - canvas_h)
lines: list[str] = list(header)
lines.extend(self._canvas_lines(width, canvas_h, selected))
lines.append(render_to_ansi(self._legend(), width, no_wrap=True))
lines.append("")
if keyed:
lines.append(render_to_ansi(self._legend(), width, no_wrap=True))
lines.append("")
lines.extend(self._list_lines(width, rows, depths, list_win))
self._scroll_total = max(1, len(lines))
return lines
+12 -10
View File
@@ -1247,17 +1247,19 @@ def tab_air() -> int:
return _TAB_AIR
#: The viewport, in rows, below which a tabbed page draws its strip on one row (see
#: :func:`tab_strip`'s ``compact``). The boxed strip costs three rows and its air; on the
#: Cardputer's 11-row viewport that was the difference between a page's actions on screen
#: and below the fold. Read from the viewport rather than the platform, so a desktop
#: terminal dragged short gets the same answer the handheld does.
COMPACT_TABS_BELOW = 16
#: The viewport, in rows, below which a frame is *short* and spends its rows on content:
#: a tabbed page draws its strip on one row (see :func:`tab_strip`'s ``compact``), and a
#: graph's caption and node-type key step aside so the list under it gets the rows. The
#: boxed strip alone costs three; on the Cardputer's 11-row viewport that was the
#: difference between a page's actions on screen and below the fold. Read from the
#: viewport rather than the platform, so a desktop terminal dragged short gets the same
#: answer the handheld does.
SHORT_FRAME_BELOW = 16
def tab_compact(viewport: int) -> bool:
"""Whether a page with ``viewport`` rows draws its tab strip compact (one row)."""
return viewport < COMPACT_TABS_BELOW
def short_frame(viewport: int) -> bool:
"""Whether a page with ``viewport`` rows is short (see :data:`SHORT_FRAME_BELOW`)."""
return viewport < SHORT_FRAME_BELOW
def tab_strip(labels: Sequence[str], active: int, width: int, *, compact: bool = False) -> Group:
@@ -1290,7 +1292,7 @@ def tab_strip(labels: Sequence[str], active: int, width: int, *, compact: bool =
owning screen switches the active index (``Tab``/``Shift+Tab``); the strip itself is
pure presentation.
Where rows are scarcer than that (``compact``, see :func:`tab_compact`) the same strip is
Where rows are scarcer than that (``compact``, see :func:`short_frame`) the same strip is
drawn on its own bottom rule, in one row: ``──┤ Info ├── Routes ──────``. The active tab
stands on the rule between tees, lit; an inactive one is its label on the rule, faint.
Every tab is the same width in either state, so switching still never moves a label.
+3 -3
View File
@@ -290,10 +290,10 @@ def test_every_advert_type_has_one_name() -> None:
def test_a_compact_tab_strip_is_one_row_and_never_moves_a_label() -> None:
"""Below COMPACT_TABS_BELOW rows the strip draws on its rule, every tab a fixed width."""
from meshterm.ui.widgets import COMPACT_TABS_BELOW, tab_compact, tab_strip
"""Below SHORT_FRAME_BELOW rows the strip draws on its rule, every tab a fixed width."""
from meshterm.ui.widgets import SHORT_FRAME_BELOW, short_frame, tab_strip
assert tab_compact(COMPACT_TABS_BELOW - 1) and not tab_compact(COMPACT_TABS_BELOW)
assert short_frame(SHORT_FRAME_BELOW - 1) and not short_frame(SHORT_FRAME_BELOW)
rows = [tab_strip(["Info", "Routes", "Area"], i, 40, compact=True) for i in range(3)]
plains = [group.renderables[0].plain for group in rows]
assert all(len(group.renderables) == 1 for group in rows)