Trophy case: the cells belong to the walk

A board of records is a list of routes, and the routes were the part with no room
left. Every row spent thirty-odd cells before the first hop — `#1  Jul 12 14:30`,
a score padded to a fixed twelve — and then opened the walk by naming our own node
in full, `Homestead (cc)`, twice, on every row, on a screen where every record
starts and ends on us by construction.

So the rows draw on THE path widget with both ends bare: the app-wide ★, which the
graph on the record card already plants on us and its legend already reads back as
"you". The stars keep the `you` white — path_line's fade means "not yours to
compose", and nothing on a board of walks already made is being composed, which is
what `dim_self` now says. The lanes in front are held to what they actually say:
the day the record was set (the card carries the time), and a score lane fitted to
the widest score on *that* board rather than to the worst case everywhere.

The record card was carrying the same kind of slack. Its route ran under a `route`
label lane, hanging in the twelve cells left over — under a graph captioned
"you → … → you", where the line can only be the route. It now runs unlabelled
across the whole card, wrapping at hop boundaries, on the same widget as the rows,
with the same ★ at both ends.

And the card was three rows taller than its content. The route graph took the
shared widget's five-row floor, which reserves room for a fan of alternatives; one
walk is one lane and never fans, so it draws in three — and both stacked blocks now
hand back the rows nothing landed on. The area drawing keeps two rows in hand for a
tall shape, and a walk that comes out flat never reaches them: that, plus the
graph's unused label row, is where the three blank lines under `round trip` came
from. Trimming happens after the drawing, so nothing placed is ever lost.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5 committed 2026-07-29 04:13:44 -04:00
1 parent 0a41303366
commit 65a47af037
4 files changed
+287 -45

No files matched your search

+20 -10
View File
@@ -42,7 +42,9 @@ drawn*, so every surface the survey found can eventually route through it:
reads identically whichever mode drew it. A surface whose route always starts and
ends on us can ask for ``bare_self``, replacing our name and hash with the app-wide
``★`` — the reader already knows who both ends are, and the cells belong to the hops
that *are* news.
that *are* news. Those stars fade where the route is being *composed* (our two ends
are fixtures, not choices); a surface that only *shows* a walk passes ``dim_self=False``
and keeps us in the ``you`` white.
Existing call sites still render through ``path_text``; migrating them here is a
separate, per-surface pass. The composer's insertion cursor rides along as a hop in
@@ -725,6 +727,7 @@ def path_line(
dim_from: Optional[int] = None,
hash_as_name: bool = False,
bare_self: bool = False,
dim_self: bool = True,
cursor: Optional[int] = None,
mode: str = "auto",
) -> PathLine:
@@ -759,10 +762,15 @@ def path_line(
bare_self: Draw our own device (a ``None`` hop) as the bare
:data:`SELF_GLYPH`. For a surface whose route *always* begins and ends on
us, the name and hash on both ends say nothing the reader doesn't know,
and cost the cells the hops in between need. Both stars also fade: setting
out from us and coming home to us are fixtures of every such route, no more
composed — or removable — than a mirrored return leg is, so they wear the
same automatic grey.
and cost the cells the hops in between need.
dim_self: Fade the bare stars (the default). On a surface where the route is
something you *compose*, setting out from us and coming home to us are
fixtures — no more yours to choose or remove than a mirrored return leg —
so they wear the same automatic grey, and the colour on the line belongs to
the nodes actually being picked. Pass ``False`` where nothing is being
composed (a record of a walk already made): there is no "not yours" to say,
and our node then reads in the app-wide ``you`` white like everywhere else.
A ``dim_from`` that reaches a star still fades it either way.
cursor: Splice an editor's insertion slot (:data:`CURSOR_GLYPH`) in *at* this
rendered-hop index — the position a chosen hop would take, so index ``0``
opens the route and ``len`` closes it. Counted over rendered hops exactly
@@ -779,11 +787,13 @@ def path_line(
dim = dim_from is not None and i >= dim_from
if hop is None:
if bare_self:
# Both our ends are fixed: a route leaves us and comes home to us, and
# neither end is anybody's to compose or remove. So the stars fade like
# every other automatic hop — the colour on the line belongs to the
# nodes actually being chosen.
built.append(PathHop(SELF_GLYPH, you=True, dim=True))
# Where the route is being composed, both our ends are fixed — a walk
# leaves us and comes home to us, and neither end is anybody's to choose
# or remove — so the stars fade like every other automatic hop and the
# colour belongs to the nodes actually being picked. Where nothing is
# being composed (``dim_self=False``), that has nothing to say, and our
# own node keeps the app-wide ``you`` white.
built.append(PathHop(SELF_GLYPH, you=True, dim=dim_self or dim))
continue
note = _shorten(device_hash, hash_bytes) if (show_hash and device_hash) else None
built.append(
+116 -34
View File
@@ -6,15 +6,20 @@ opened from the main menu. Every successful trace — a *Trace target* boomerang
(see :mod:`~meshterm.services.records`); this screen is where the survivors live.
* the browser groups the six disciplines, each under its heading with a one-line
description of the game (word-wrapped when it must), then that discipline's records
ranked best-first — dated, scored in the discipline's own unit, with the walked route
through THE path widget. A discipline holding records at more than one hash width tags
each row with its width, since the widths are genuinely different games;
description of the game (word-wrapped when it must) — the two pin overhead together as
one block while that board scrolls, so a row deep in a discipline still says which game
it is winning and what that game scores — then the discipline's records ranked
best-first: the day, the score in the discipline's own unit, and the walk itself on THE
path widget with both ends bare (every record is a boomerang, so the cells go to the
hops). A discipline holding records at more than one hash width tags each row with its
width, since the widths are genuinely different games;
* opening a record floats :class:`RecordDialog` — every stat the walk was measured by
(the far point named with the node it reached), the walk drawn two ways: on THE route
graph (the Message paths dialog's shape, us at both ends) and, beside the stats, as the
enclosed area it swept on a braille mini-map (us and every positioned hop, coloured node
pins, no labels); then the full route and spec, and when/by which app version it was
graph (the Message paths dialog's shape, us at both ends, drawn no taller than one lane
needs) and, beside the stats, as the enclosed area it swept on a braille mini-map (us and
every positioned hop, coloured node pins, no labels); then the full route — the path
widget again, unlabelled across the card's whole width, wrapping at hop boundaries — and
the spec, and when/by which app version it was
set. The card scrolls (PgUp/PgDn/Home/End) when it outgrows the terminal. From there
*Trace this path* reopens Trace path with the record's route prefilled, so a claim
worth re-testing is one Enter from the air again — and when that screen closes, the
@@ -33,6 +38,7 @@ import textwrap
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Sequence
from rich.cells import cell_len
from rich.text import Text
from ..core.geo import haversine_km
@@ -40,8 +46,9 @@ from ..persistence.repository import DiscoveredPath
from ..services import trace_runner
from ..services.records import CATEGORIES, CATEGORY_BY_ID, Category, _local_xy
from .mapcanvas import RGB, MapCanvas
from .menus import back_rows, section_heading
from .menus import back_rows, fit_cells, section_heading
from .pathgraph import PathLayer, render_path_graph
from .pathline import path_line
from .theme import name_style, snr_style
from .tui.render import render_hanging, render_to_ansi
from .tui.screen import Screen
@@ -51,7 +58,6 @@ from .widgets import (
name_rgb,
node_marker,
node_type_legend,
path_text,
route_graph_style,
self_marker,
)
@@ -67,6 +73,12 @@ _DESC_WRAP = 64
#: on the canvas, so it needs no colour to set it apart — white just reads as "the walk".
_WALK_EDGE = (255, 255, 255)
#: Canvas rows a record's route graph draws in. One walk is one lane, so the graph is flat
#: and needs only its marker row with a row either side for the labels — the shared widget's
#: own default (5) leaves a scored walk sitting in two rows of blank canvas. A fan would grow
#: past this on its own; nothing here ever fans.
_GRAPH_ROWS = 3
#: The area drawing's one body tone — a plain muted slate the enclosed region and its loop
#: both draw in. Deliberately a single shade (no dimmer interior wash under a brighter rim):
#: an even-odd fill shades only the enclosed side, and the coloured node pins carry the
@@ -83,6 +95,25 @@ _POLY_EXTRA_ROWS = 2
_SIDE_BY_SIDE_MIN = 44
def _drawn_rows(lines: list[str]) -> list[str]:
"""``lines`` with the rows nothing actually landed on stripped from either end.
Both blocks the card stacks are drawn on a canvas sized for the worst case and then
filled: the stats' area drawing keeps :data:`_POLY_EXTRA_ROWS` in hand for a tall shape,
and the route graph pads a row either side of its markers for the labels. A shape that
comes out flat, or labels that all seat on one side, leave those rows empty — and an
empty row still costs a line of card, stacking up as a gap the layout never intended
(three blank lines between the stats and the graph where one was meant). Trimming happens
*after* the drawing, so nothing placed is ever lost: only rows that stayed blank go.
"""
kept = list(lines)
while kept and not Text.from_ansi(kept[-1]).plain.strip():
kept.pop()
while kept and not Text.from_ansi(kept[0]).plain.strip():
kept.pop(0)
return kept
@dataclass(frozen=True, slots=True)
class WalkVertex:
"""One positioned point of a walk's circuit, projected onto the local km plane.
@@ -114,7 +145,8 @@ class RecordDialog(Screen):
drawn beside them on a braille mini-map (us at the origin, every positioned hop pinned
in its own name hue, no labels) when the terminal has the room; below, the walk on THE
route graph (us at both ends, relays wearing their map marker over a node-type key),
the route in full (wrapped, never truncated), and the
the route in full on THE path widget — no label lane, the card's whole width, wrapped
at hop boundaries and never truncated — and the
record's provenance — when it was set and by which app version. The card scrolls
(PgUp/PgDn/Home/End) when it outgrows the frame, while the arrows drive the actions.
Two actions besides Back: *Trace this path* reopens Trace path with the record's route
@@ -235,6 +267,12 @@ class RecordDialog(Screen):
re-walking one node must never make the shape look bigger than the ground it covered —
one node, one marker, whatever the spec asked for. An overheard via chain has the
opposite duty: nothing there is ours to inflate, and a repeat is evidence.
One walk is one lane, so the graph draws in :data:`_GRAPH_ROWS` — the marker row and
a row either side for the labels — rather than the widget's default, which reserves
room for a fan this canvas never carries and spends it on blank rows. Whichever of
those two label rows stays empty is handed back (:func:`_drawn_rows`), so the graph
sits exactly one blank line under the stats and the caption follows it directly.
"""
seen: set[str] = set()
hops: list[str] = []
@@ -248,11 +286,12 @@ class RecordDialog(Screen):
source=self._device_label,
type_of=self._type_of,
)
return render_path_graph(
return _drawn_rows(render_path_graph(
[PathLayer(hops=tuple(hops), color=_WALK_EDGE, priority=3)],
width,
glyph_of=glyph_of, label_of=label_of, label_rgb_of=label_rgb_of,
)
min_rows=_GRAPH_ROWS,
))
def _stat_lanes(self) -> list[Text]:
"""Every stat the walk was measured by, as label/value lanes (score → round trip).
@@ -314,7 +353,10 @@ class RecordDialog(Screen):
With a drawable walk and room to spare, the polygon takes a fixed cell box on the
right and the lanes are cropped to the column beside it; too narrow, or no shape,
and the lanes reclaim the whole width and the drawing is dropped.
and the lanes reclaim the whole width and the drawing is dropped. The block is
trimmed to the rows something landed on (:func:`_drawn_rows`), so a shape that comes
out flat hands its unused rows back instead of leaving the card gaping under the
last stat.
"""
if not self._shape or width < _SIDE_BY_SIDE_MIN:
return [render_to_ansi(lane, width, no_wrap=True) for lane in lanes]
@@ -331,7 +373,7 @@ class RecordDialog(Screen):
row.append(" ")
row.append_text(Text.from_ansi(poly[i]))
out.append(render_to_ansi(row, width, no_wrap=True))
return out
return _drawn_rows(out)
def _shape_lines(self, cell_w: int, cell_h: int) -> list[str]:
"""Draw the walk's enclosed area on a braille canvas: fill, loop, coloured pins.
@@ -390,16 +432,28 @@ class RecordDialog(Screen):
lines.append(render_to_ansi(node_type_legend(), width, no_wrap=True))
lines.append("")
route = path_text(
# The walk itself, on THE path widget, across the card's whole width. No ``route``
# label lane: the line under a route graph captioned "you → … → you" is the route,
# and the twelve cells a label would take are hops the reader came here for. It wraps
# at hop boundaries (never mid-name, never mid-chip) rather than hanging under a lane.
# Our two ends stand on the app-wide ★ rather than repeating our name and key — the
# graph above already marks us with the same star at both ends, and the legend under
# it reads that star back as "you" — kept in the ``you`` white, since nothing about a
# walk already made is ours to compose.
route = path_line(
[None, *record.route, None],
self._resolve,
prefix_bytes=record.width_bytes,
self_name=self._device_label,
show_hash=True,
hash_bytes=record.width_bytes,
device_hash=self._device_hash,
bare_self=True,
dim_self=False,
)
lines.extend(
render_to_ansi(line, width, no_wrap=True) for line in route.wrapped(width)
)
lines.extend(render_hanging(Text("route ", style="muted"), route, width,
indent=12))
spec = Text(record.spec, style="brand")
spec.append(f" ({record.width_bytes}-byte hops)", style="muted")
lines.extend(render_hanging(Text("spec ", style="muted"), spec, width,
@@ -594,30 +648,52 @@ async def open_records(ctx: "AppContext") -> dict:
rows.append(Separator(f" {line}", style="muted"))
return rows
def browser_row(
rank: int, category: Category, record: DiscoveredPath, *, show_width: bool
) -> Text:
"""One record row: rank, date, score (width when it disambiguates), route."""
row = Text(f"#{rank} ", style="muted")
row.append(record.discovered_at.astimezone().strftime("%b %d %H:%M"),
style="muted")
row.append(" ")
def scored(category: Category, record: DiscoveredPath) -> str:
"""A record's score in its discipline's unit, bounded where the walk was partial."""
score = category.format_score(record.score)
if category.id == "long_haul" and not record.stats.get("km_complete", True):
score = "≥ " + score
row.append(f"{score:<12}", style="accent")
return score
def browser_row(
rank: int,
category: Category,
record: DiscoveredPath,
*,
show_width: bool,
score_w: int,
) -> Text:
"""One record row: rank, date, score (width when it disambiguates), then the walk.
The columns left of the walk are held to what they actually say — the day the record
was set (the dialog carries the time), and a score lane fitted to the widest score on
*this* board rather than a fixed twelve — because every cell they don't spend is a hop
the route gets to show. The walk itself draws on THE path widget with both ends bare
(:data:`~meshterm.ui.pathline.SELF_GLYPH`): a record is a boomerang by construction,
so naming ourselves twice per row would cost more cells than the whole score lane and
tell the reader what every other row already told them. Those stars keep the ``you``
white (``dim_self=False``) — the fade means "not yours to compose", and nothing on a
board of walks already made is being composed. Overflow stays the list's job — the
browser h-scrolls the highlighted row, so a long walk is read by sliding it, not by
eliding it here.
"""
row = Text(f"#{rank} ", style="muted")
row.append(record.discovered_at.astimezone().strftime("%b %d"), style="muted")
row.append(" ")
row.append(fit_cells(scored(category, record), score_w), style="accent")
if show_width:
row.append(f"{record.width_bytes} B ", style="muted")
row.append(" ")
row.append(f" {record.width_bytes} B", style="muted")
row.append(" ")
row.append_text(
path_text(
path_line(
[None, *record.route, None],
resolve,
self_name=device_label,
show_hash=True,
prefix_bytes=record.width_bytes,
hash_bytes=record.width_bytes,
device_hash=device_hash,
)
self_name=device_label,
bare_self=True,
dim_self=False,
).text()
)
return row
@@ -664,9 +740,15 @@ async def open_records(ctx: "AppContext") -> dict:
if not board:
items.append(Separator(" no records yet", style="muted"))
show_width = len({r.width_bytes for r in board}) > 1
# The score lane is this board's own widest score — "3 nodes" and "+6.0 dB"
# measure differently, and a lane sized for the worst case everywhere would
# spend the difference on padding in front of every route.
score_w = max((cell_len(scored(category, r)) for r in board), default=0)
for rank, record in enumerate(board, start=1):
items.append(Choice(
title=browser_row(rank, category, record, show_width=show_width),
title=browser_row(
rank, category, record, show_width=show_width, score_w=score_w
),
value=("open", category, rank, record),
))
total = len(ctx.repo.discoveries())
+26
View File
@@ -426,3 +426,29 @@ def test_bare_self_stands_us_on_a_star_and_fades_both_ends() -> None:
assert len(lines) > 1
assert all(line.plain[2 + WRAP_OFFSET] == POWERLINE_SEP for line in lines[1:])
assert all(line.cell_len <= 28 for line in lines)
def test_bare_self_keeps_us_in_the_you_white_where_nothing_is_composed() -> None:
"""``dim_self=False`` un-mutes the stars: the fade means "not yours to compose".
A surface that only *shows* a walk (the Trophy case's boards and record card) composes
nothing, so there is no "not yours" to say and our own node reads in the app-wide ``you``
white — the same identity colour it wears everywhere else. A ``dim_from`` that reaches a
star still fades it, so a mirrored return leg keeps its grey either way.
"""
hops = [None, *(f"{i:02x}aa" for i in range(4)), None]
text = path_line(hops, prefix_bytes=2, self_name="Me", bare_self=True,
dim_self=False, mode="plain").text()
stars = [s for s in text.spans if text.plain[s.start : s.end] == SELF_GLYPH]
assert [str(s.style) for s in stars] == ["you", "you"]
chips = path_line(hops, prefix_bytes=2, self_name="Me", bare_self=True,
dim_self=False, mode="powerline").text()
fills = [str(s.style) for s in chips.spans
if chips.plain[s.start : s.end] == SELF_GLYPH]
assert all("#ffffff" in fill for fill in fills) # the you white, as a chip fill
# The explicit fade still rules: a star inside a dimmed return leg stays grey.
faded = path_line(hops, prefix_bytes=2, self_name="Me", bare_self=True,
dim_self=False, dim_from=3, mode="plain").text()
tail = [s for s in faded.spans if faded.plain[s.start : s.end] == SELF_GLYPH]
assert [str(s.style) for s in tail] == ["you", "faint"]
+125 -1
View File
@@ -21,9 +21,10 @@ from meshterm.core.admin_store import AdminStore
from meshterm.core.config import Settings
from meshterm.core.device_store import DeviceStore
from meshterm.persistence.repository import DiscoveredPath, Repository
from meshterm.services.records import CATEGORY_BY_ID
from meshterm.services.records import CATEGORIES, CATEGORY_BY_ID
from meshterm.ui.records_screen import RecordDialog, WalkVertex, open_records
from meshterm.ui.surface import TuiUi
from meshterm.ui.tui import frame
from meshterm.ui.tui.prompt import TypedConfirmDialog
from meshterm.ui.tui.screen import CANCEL
from meshterm.ui.tui.select import SelectScreen
@@ -96,6 +97,42 @@ def test_record_dialog_draws_the_walk_as_a_route_graph() -> None:
assert any("⠀" <= ch <= "⣿" for ch in body) # braille edges are drawn
def test_record_graph_is_only_as_tall_as_one_lane_needs() -> None:
"""A walk is one path: the marker row, plus a label row only where a label landed.
The shared widget's default floor (5) reserves room for a fan of alternatives; nothing
here ever fans, so the walk sat in rows of blank canvas and pushed the route and the
actions down the card for nothing.
"""
for record in (_record(), _record(route=(HUB_ID, FAR_ID, HUB_ID, FAR_ID, HUB_ID))):
lines = _dialog(record)._graph_lines(60) # however many relays: still one lane
assert 0 < len(lines) <= 3
assert all(_plain([line]).strip() for line in lines) # not a blank row among them
def test_record_dialog_route_runs_unlabelled_across_the_whole_card() -> None:
"""The route is THE path widget at full width — no ``route`` lane eating twelve cells.
Under a graph captioned ``you → … → you``, the line *is* the route; the label lane only
cost hops. It wraps at hop boundaries, so a long walk folds instead of truncating, and
our own two ends stand on the ★ the graph above already marks us with.
"""
dialog = _dialog(_record(route=tuple([HUB_ID, FAR_ID] * 4)))
body = _plain(dialog.render_body(60)).splitlines()
spec_at = next(i for i, line in enumerate(body) if line.startswith("spec"))
# The route's own first line: the last one before the spec that isn't a hanging fold.
route_at = next(i for i in range(spec_at - 1, 0, -1) if not body[i].startswith(" "))
route = body[route_at:spec_at]
assert not route[0].startswith("route") # no label lane
assert route[0].startswith("★") # our end opens the walk on the app-wide star…
assert route[-1].endswith("★") # …and closes it on the same
assert "Homestead" not in "".join(route) # never our name, and never our key
assert "YUL-Cartierville" in "".join(route) # the hops themselves are named in full
assert len(route) > 1 # it folded rather than truncating…
# …every fold hanging under the step, and the labelled lanes resume at the spec.
assert all(line.startswith(" ") for line in route[1:])
def test_record_dialog_shows_the_node_type_legend() -> None:
"""The standard node-type key sits under the graph, so its markers read."""
body = _plain(_dialog(_record()).render_body(60))
@@ -330,6 +367,93 @@ async def test_delete_a_disciplines_records_is_a_popup_over_the_browser(tui_ctx)
assert result == {"records": 1}
async def test_a_board_row_spends_its_cells_on_the_walk(tui_ctx) -> None:
"""A record row leads with rank/day/score, then the walk with both ends bare.
Every record is a boomerang, so naming ourselves at both ends said nothing twice a row
and cost more cells than the whole score lane; the ends go to ``★`` and the score lane
is fitted to this board's own widest score instead of a fixed twelve.
"""
ctx = tui_ctx
session = ctx.ui.session
ctx.repo.record_discovery(
"grand_tour", 1, "3d,f2", (HUB_ID, FAR_ID, HUB_ID),
score=2.0, stats={"hop_count": 3, "distinct_nodes": 2}, app_version="0.1.0",
)
task = asyncio.ensure_future(open_records(ctx))
try:
browser = await _step_until(lambda: _trophy_case(session))
assert browser is not None
label = next(
choice.label for choice in browser._choices()
if isinstance(choice.value, tuple) and choice.value[0] == "open"
)
row = label.plain
assert row.startswith("#1 ") # the standing on the board
assert "2 nodes" in row # the score, in the discipline's unit
assert row.count("★") == 2 # our two ends, bare — never named twice a row
assert "Homestead" not in row # …so the device name is nowhere on the line
# And those ends are *us*, in the you white — nothing here is ours to compose, so
# there is no "not yours" fade to wear.
stars = [s for s in label.spans if row[s.start : s.end] == "★"]
assert [str(s.style) for s in stars] == ["you", "you"]
assert "3d" in row and "f2" in row # the hops, at the record's own hash width
# The lanes before the walk are held to what they say: a score lane fitted to this
# board's widest score (no dead padding), and the day without the minute the dialog
# carries — every cell they don't spend is a hop the route gets to show.
assert "2 nodes ★" in row
assert ":" not in row[: row.index("★")]
finally:
if not task.done():
task.cancel()
async def test_the_board_pins_its_discipline_heading_and_description(tui_ctx) -> None:
"""Scrolling into a board keeps its ``── discipline ──`` heading *and* description overhead.
Every discipline heading is followed by its word-wrapped description — what that board
scores, which is exactly what a reader partway down it needs. The two pin as one block:
with every separator a pinning candidate, the *last muted line* under a heading won the
top row on its own and the heading never stuck — the first discipline's least of all,
since its description is what the very first scrolled row sits under.
"""
ctx = tui_ctx
session = ctx.ui.session
for i in range(6): # one full board, so its records outlast a short viewport
ctx.repo.record_discovery(
"grand_tour", 1, f"3d,{i:02x}", (HUB_ID, FAR_ID),
score=float(i + 1), stats={"hop_count": 2, "distinct_nodes": 2},
app_version="0.1.0",
)
task = asyncio.ensure_future(open_records(ctx))
try:
browser = await _step_until(lambda: _trophy_case(session))
assert browser is not None
# Exactly the six disciplines are landmarks, each block led by its own heading and
# carrying the description written under it — nothing else is a candidate.
browser.render_body(72)
blocks = [[_plain([line]).strip() for line in rows]
for _idx, rows in browser._sticky_headers]
assert [rows[0] for rows in blocks] == [f"── {c.icon} {c.title} ──" for c in CATEGORIES]
assert all(len(rows) >= 2 for rows in blocks) # each carries its description too
# Highlighting deep in the one populated board scrolls its heading off the top of a
# short viewport; the heading leads what pins, its description under it.
for _ in range(4):
browser.handle("down")
visible, above, _below = frame._visible_slice(browser, browser.render_body(72), 10)
board = CATEGORY_BY_ID["grand_tour"]
assert _plain([visible[0]]).strip() == f"── {board.icon} {board.title} ──"
assert _plain([visible[1]]).strip().startswith(board.description[:20])
assert above is True
browser.resolve(("back", None, 0, None))
await task
finally:
if not task.done():
task.cancel()
async def test_trace_this_path_unwinds_to_the_menu_not_the_browser(
tui_ctx, monkeypatch
) -> None: