Files
MeshTerm/tests/test_picocalc_chrome.py
T
Jean-Pierre MartineauandClaude Opus 5 8fc24d4901 The clip arrows lead the bar, and say it in colour
They rode the PicoCalc title bar's tail, appearing and disappearing with the
scroll — and a half-shown pair put a blank cell where the missing arrow would
be, so reading it meant comparing the row against a memory of itself. Both
arrows now sit at the left edge, where the eye starts, and never move: an arrow
whose direction has more takes the border's own accent, one with nothing that
way drops to muted. That is the live/dim language the F-key lane already draws
one row below, and the bar keeps its width whatever the body is doing.

The Esc atom keeps the tail it was given, now on its own — a hair more room for
a long title, since the two arrow cells left that end of the row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 03:20:48 -04:00

411 lines
18 KiB
Python

"""P4 chrome contracts: borderless frame, slim header, the F-key lane, dialog gate."""
from __future__ import annotations
from rich.cells import cell_len
from rich.text import Text
from meshterm.platforms import PICOCALC, REGULAR, set_platform
from meshterm.ui.tui import frame
from meshterm.ui.tui.fkeys import (
DEFAULT_LANE,
FPair,
action_for,
default_lane,
lane_text,
strip_lane_atoms,
)
from meshterm.ui.tui.screen import ScrollScreen
from tests.conftest import plain as _plain
def _screen(lines: int = 40) -> ScrollScreen:
body = Text("\n".join(f"row {i}" for i in range(lines)))
screen = ScrollScreen(body, title="Chrome probe", floating=False)
return screen
def _slot_style(row: Text, index: int) -> str:
"""The style the lane painted onto slot ``index``'s chip (slots are 9 cells + 2 gap)."""
start = index * 11
return next(str(span.style) for span in row.spans if span.start <= start < span.end)
def test_borderless_frame_swaps_the_panel_for_a_title_bar() -> None:
set_platform(PICOCALC)
composed = frame.compose_base(Text("hdr"), _screen(), "hint", 53, 26).split("\n")
assert len(composed) == 26
plain = [_plain(line) for line in composed]
# Row 1 (under the one-line header) is the title bar: rule + title + clip arrow.
assert "Chrome probe" in plain[1] and "─" in plain[1]
assert "↓" in plain[1] # 40 rows into a 23-row viewport: more below
# No Panel borders anywhere — the body owns the full width.
assert not any("│" in line or "╭" in line for line in plain)
assert all(cell_len(line) <= 53 for line in plain)
# The body's first row starts at column 0 of row 2 (no panel padding).
assert plain[2].startswith("row 0")
def _bar_text(title: str, hint: str, cols: int = 53, *, above: bool = False,
below: bool = True) -> Text:
"""The borderless title bar for a screen with ``title`` and ``hint``, as styled Text."""
screen = ScrollScreen(Text("x"), title=title, floating=False)
screen._footer_hint = hint
return frame._title_bar(screen, cols, above, below)
def _bar(title: str, hint: str, cols: int = 53, *, below: bool = True) -> str:
"""The borderless title bar's plain text for a screen with ``title`` and ``hint``."""
return _bar_text(title, hint, cols, below=below).plain
def _arrow_styles(bar: Text) -> tuple[str, str]:
"""The styles the bar painted on its two clip arrows, ``(up, down)``."""
return tuple(
next(str(span.style) for span in bar.spans
if span.start <= index < span.end and span.end - span.start == 1)
for index in (0, 1)
)
def test_the_title_bar_says_how_to_leave_the_screen() -> None:
"""This platform has no footer hint line, so the way out rides the bar's tail.
The F-key lane stands where the footer would be and only advertises what its slots do,
which left Esc — the key every screen answers to, and the reason no screen carries a
*Back* row — completely unadvertised (JP, 2026-08-30). It lands last, past the title,
with the clip arrows holding the row's other end.
"""
bar = _bar("Chrome probe", "↑↓ move · Enter open · Esc back")
assert bar.startswith("↑↓ ")
assert bar.rstrip().endswith("Esc back")
assert "Chrome probe" in bar
def test_the_clip_arrows_are_always_drawn_and_say_it_in_colour() -> None:
"""Both arrows are fixed furniture at the row's left edge; colour reads the scroll.
They used to appear and vanish, and a half-shown pair left a blank cell standing in
for the missing one — the reader had to compare the row against a memory of itself
(JP, 2026-08-31). Now the pair never moves and never changes width, and an arrow
whose direction has more takes the border's own accent while one with nothing that way
drops to muted: the F-key lane's live/dim language, one row up.
"""
hint = "↑↓ move · Esc back"
assert _arrow_styles(_bar_text("Contacts", hint, above=True, below=True)) == (
"accent", "accent")
assert _arrow_styles(_bar_text("Contacts", hint, above=False, below=True)) == (
"muted", "accent")
assert _arrow_styles(_bar_text("Contacts", hint, above=True, below=False)) == (
"accent", "muted")
# A body that fits whole keeps the pair, both dim — nothing appears or disappears.
assert _arrow_styles(_bar_text("Contacts", hint, above=False, below=False)) == (
"muted", "muted")
widths = {
_bar_text("Contacts", hint, above=a, below=b).plain
for a in (False, True) for b in (False, True)
}
assert len(widths) == 1, "the bar's text must not shift as the body scrolls"
def test_the_bar_speaks_the_screen_s_own_esc_verb() -> None:
"""Lifted off the screen's own footer hint, so each surface keeps its true verb."""
assert _bar("MeshTerm", "↑↓ move · Enter open · Esc quit").endswith("Esc quit")
assert _bar("Path width", "↑↓ move · Enter set · Esc keep").endswith("Esc keep")
# A hint that names no way out advertises none — the lane's rule, one row up.
assert "Esc" not in _bar("Working", "↑↓ move")
def test_the_esc_hint_gives_way_before_it_crowds_the_title() -> None:
"""Compact by construction: the verb goes first, then the atom, and the title stays.
Two rungs down, in order — a long title keeps the key without its verb, a longer one
takes the cells back altogether. The title is what the reader came for.
"""
verbless = _bar("Trace — YUL-Cartierville over a spec", "↑↓ move · Esc back")
assert verbless.rstrip().endswith(" Esc") # the verb went, the key stayed
assert "Esc back" not in verbless
crowded = _bar("Trace — YUL-Cartierville over a longer spec", "↑↓ move · Esc back")
assert "Esc" not in crowded
for bar in (verbless, crowded):
assert "YUL-Cartierville" in bar and cell_len(bar) <= 53
def test_bordered_frame_is_unchanged_on_regular() -> None:
set_platform(REGULAR)
composed = frame.compose_base(Text("hdr"), _screen(), "hint", 72, 24).split("\n")
plain = [_plain(line) for line in composed]
assert any("╭" in line or "┌" in line for line in plain), "regular keeps its Panel"
def test_picocalc_header_brands_the_app_not_the_device() -> None:
"""The header_atoms wiring, per JP's spec: picocalc shows ``MeshTerm vX`` (a soldered
radio's port never changes, so the device segment earns nothing), regular keeps both."""
from meshterm.ui.menu import _header_segments
class _Badges:
def unread_total(self) -> int:
return 0
def unacked_count(self) -> int:
return 0
class _Ctx:
mock = True
chat = _Badges()
watchtower = _Badges()
set_platform(PICOCALC)
text = "".join(seg.plain for seg in _header_segments(_Ctx(), {}))
assert "MeshTerm" in text
assert "simulator" not in text # the device atom is composed out
assert "pulse" in PICOCALC.header_atoms # the sparkline takes the remaining room
set_platform(REGULAR)
text = "".join(seg.plain for seg in _header_segments(_Ctx(), {}))
assert "MeshTerm" in text and "simulator" in text
def test_fkey_lane_resolution_and_banks() -> None:
lane = DEFAULT_LANE
# F4/F5 (paging — no physical key at all) carry the pager, and each jump rides the
# Shift half of the very pager heading for it: Home behind Page ↑, End behind Page ↓.
assert action_for(lane, 4) == "pagedown" and action_for(lane, 9) == "end"
assert action_for(lane, 5) == "pageup" and action_for(lane, 10) == "home"
# F1-F3 are free on every screen — the shared lane claims none of them.
for number in (1, 2, 3, 6, 7, 8):
assert action_for(lane, number) is None
# Enter/Esc never occupy a slot — both keys are already close at hand.
assert "enter" not in {action_for(lane, n) for n in range(1, 11)}
assert "escape" not in {action_for(lane, n) for n in range(1, 11)}
def test_fkey_lane_text_fits_and_flips() -> None:
primary = lane_text(DEFAULT_LANE)
shifted = lane_text(DEFAULT_LANE, shifted=True)
assert cell_len(primary.plain) == 53 and cell_len(shifted.plain) == 53
# A directional pair rises to the right: down/out left, up/in right — and each
# companion keeps its slot's end of that axis.
assert "F4 Page ↓" in primary.plain and "F5 Page ↑" in primary.plain
assert "F9 Bottom" in shifted.plain and "10 Top" in shifted.plain
# The free bank renders as bare, unfilled key numbers in both banks.
for number in (1, 2, 3):
assert f"F{number} " in primary.plain
assert f"F{number + 5} " in shifted.plain
wide = [FPair("Muchtoolonglabel", "a", "Muchtoolonglabel", "b")] * 5
assert cell_len(lane_text(wide).plain) == 53 # clipped to the slot budget
assert cell_len(lane_text(wide, shifted=True).plain) == 53
def test_fkey_slot_dims_when_its_action_is_unavailable() -> None:
"""An unavailable slot keeps its label and drops its fill — it never offers a dead key."""
lane = [FPair("Paths", "paths", "Retry", "retry", enabled=False)] + [None] * 4
primary = lane_text(lane)
shifted = lane_text(lane, shifted=True)
# The label survives: the lane still says what F1 is for, just not that it acts now.
assert "F1 Paths" in primary.plain and _slot_style(primary, 0) == "muted"
# Its Shift companion is untouched — the two banks gate independently.
assert "F6 Retry" in shifted.plain and _slot_style(shifted, 0) == "fkey.chip.shift"
# The row is still exactly the lane's width, dim chips and all.
assert cell_len(primary.plain) == 53 and cell_len(shifted.plain) == 53
# Dimming is presentational: the key still resolves, and the screen's handler no-ops.
assert action_for(lane, 1) == "paths"
def test_default_lane_dims_every_nav_slot_when_nothing_moves() -> None:
"""A screen with nothing to move gets the shared lane back inert, not absent."""
assert default_lane() is DEFAULT_LANE # the live lane is the constant itself
dim = default_lane(nav=False)
assert [pair.label for pair in dim if pair] == ["Page ↓", "Page ↑"]
assert not any(pair.enabled or pair.opp_enabled for pair in dim if pair)
row = lane_text(dim)
assert cell_len(row.plain) == 53 and "F4 Page ↓" in row.plain and "F5 Page ↑" in row.plain
assert {str(span.style) for span in row.spans} == {"muted"}
# Both banks dim together: the jump behind a dead pager is just as dead.
assert {str(span.style) for span in lane_text(dim, shifted=True).spans} == {"muted"}
def test_lane_is_built_after_the_body_renders() -> None:
"""The frame defers the lane, so its gates read this paint's metrics, not the last one's."""
set_platform(PICOCALC)
screen = _screen() # 40 body rows into a 23-row viewport: it overflows
seen: dict[str, bool] = {}
def build() -> Text:
seen["nav"] = all(pair.enabled for pair in screen.fkey_lane if pair)
return lane_text(screen.fkey_lane)
composed = frame.compose_base(Text("hdr"), screen, "hint", 53, 26, footer_lane=build)
assert seen["nav"] is True # lit on the very first paint, with no stale frame to lag
assert "F4 Page ↓" in _plain(composed.split("\n")[-1])
def test_scroll_screen_lane_tracks_whether_its_body_overflows() -> None:
"""The result window's nav slots light only once there is something to scroll to."""
screen = _screen()
screen.note_metrics(4, 20) # the whole body fits the viewport
assert not any(pair.enabled for pair in screen.fkey_lane if pair)
screen.note_metrics(80, 20) # taller than the viewport
assert all(pair.enabled for pair in screen.fkey_lane if pair)
def test_select_lane_promotes_the_section_jumps_only_where_there_are_sections() -> None:
"""Ctrl+PgUp/PgDn is unreachable on a keyboard with no PgUp, so a grouped list lanes it."""
from meshterm.ui.tui.select import Choice, Separator, SelectScreen
flat = SelectScreen("Flat", [Choice("one", 1), Choice("two", 2)])
# No headings anywhere: sections are not a thing on this list, so the slots are empty
# rather than dim — and F1/F2 resolve to nothing at all.
assert flat.fkey_lane[0] is None and flat.fkey_lane[1] is None
assert action_for(flat.fkey_lane, 1) is None
grouped = SelectScreen("Grouped", [
Separator("── Near ──"), Choice("alpha", 1), Choice("beta", 2),
Separator("── Far ──"), Choice("gamma", 3),
])
lane = grouped.fkey_lane
assert [pair.label for pair in lane[:2]] == ["Sect ↑", "Sect ↓"]
# A pair rises toward its outer key: on this left-edge pair, up takes F1.
assert action_for(lane, 1) == "ctrl_pageup" and action_for(lane, 2) == "ctrl_pagedown"
assert all(pair.enabled for pair in lane[:2])
# A filter that collapses the list onto one section keeps the labels and dims them:
# the sections are still a thing here, they just have nowhere to jump right now.
for ch in "gam":
grouped.handle("text", ch)
dim = grouped.fkey_lane
assert [pair.label for pair in dim[:2]] == ["Sect ↑", "Sect ↓"]
assert not any(pair.enabled for pair in dim[:2])
def test_dialogs_draw_no_lane_at_all() -> None:
"""A prompt has nothing to page, so it shows bare key numbers, not a dim pager."""
from meshterm.ui.tui.fkeys import EMPTY_LANE
from meshterm.ui.tui.prompt import ButtonDialog, ConfirmScreen, TextScreen
for screen in (
TextScreen("Name", prompt="Call it what?"),
ConfirmScreen("Sure?"),
ButtonDialog("Reboot the node?", ["Cancel", "Reboot"]),
):
assert list(screen.fkey_lane) == list(EMPTY_LANE)
row = lane_text(screen.fkey_lane)
assert cell_len(row.plain) == 53
assert {str(span.style) for span in row.spans} == {"muted"}
def test_map_locate_is_reachable_from_both_control_keys() -> None:
"""``locate`` is bound as a chord app-wide, so both Ctrl keys and F2 reach the same action.
The letter is the mnemonic of the action — ^U for *you* — so it is the same chord on
every screen that can point at our own node, not one screen's initial.
"""
from prompt_toolkit.keys import Keys
from meshterm.ui.tui.session import _CTRL_LETTER_CHORDS, _KEY_ACTIONS
assert _CTRL_LETTER_CHORDS["u"] == "locate" # the right-Ctrl rescue's half
assert _KEY_ACTIONS[Keys.ControlU] == "locate" # the ordinary binding, generated from it
def test_host_battery_reads_the_sysfs_supply(tmp_path, monkeypatch) -> None:
"""The PicoCalc battery path: a true percent and charging flag straight from sysfs."""
from meshterm.services import battery_service
(tmp_path / "capacity").write_text("53\n")
(tmp_path / "status").write_text("Discharging\n")
(tmp_path / "voltage_now").write_text("4012000\n")
monkeypatch.setattr(battery_service, "_HOST_SUPPLY", tmp_path)
service = battery_service.BatteryService(ctx=None)
service._poll_host()
reading = service.reading()
assert reading is not None
assert (reading.percent, reading.charging, reading.millivolts) == (53, False, 4012)
(tmp_path / "status").write_text("Charging\n")
service._poll_host()
assert service.reading().charging is True
monkeypatch.setattr(battery_service, "_HOST_SUPPLY", tmp_path / "gone")
service._poll_host()
assert service.reading() is None # unreadable supply = absent, header draws nothing
def test_dialog_gate_shrinks_on_picocalc() -> None:
from meshterm.ui.surface import _dialog_max_cells
set_platform(PICOCALC)
assert _dialog_max_cells() == 43
set_platform(REGULAR)
assert _dialog_max_cells() == 76
# --- the dialog border's hint, against the lane one row below it ------------------------
def _dialog(hint: str) -> ScrollScreen:
"""A floating screen carrying ``hint``, on the shared pager lane."""
screen = ScrollScreen(Text("body"), title="Probe")
screen._footer_hint = hint
return screen
def test_a_dialog_keeps_its_hint_where_the_lane_is_the_footer() -> None:
"""There is no hint line here, so a floating box's border is the only place keys read.
The desktop drops the border hint entirely (the footer row below repeats it word for
word); this platform's footer row is the lane, which speaks only for its five chips,
so Enter, Esc and the arrows have nowhere else to be said.
"""
screen = _dialog("↑↓ move · Enter select · Esc back")
set_platform(REGULAR)
assert frame._dialog_hint(screen) == ""
set_platform(PICOCALC)
assert "Esc back" in _plain(frame.compose_dialog(screen, 53, 26))
def test_the_dialog_hint_drops_what_the_lane_already_says() -> None:
"""An atom whose every key is a chip on the very next row is the same claim twice."""
set_platform(PICOCALC)
screen = _dialog("↑↓ move · PgUp/PgDn scroll · Home/End ends · Enter select · Esc back")
# The shared lane pages on F4/F5 and jumps to either end behind their Shift halves.
assert frame._dialog_hint(screen) == "↑↓ move · Enter select · Esc back"
assert "PgUp" not in _plain(frame.compose_dialog(screen, 53, 26))
def test_the_dialog_hint_is_resolved_on_every_paint() -> None:
"""Never folded into a screen's hint once: both halves move while the screen is up.
The packet viewer rewrites its own hint as the list it pages through grows past one
entry (JP, 2026-08-31), and every screen reads its lane fresh each frame.
"""
set_platform(PICOCALC)
screen = _dialog("Esc close")
assert frame._dialog_hint(screen) == "Esc close"
screen._footer_hint = "↑↓ newer/older · PgUp/PgDn scroll · Home/End ends · Esc close"
assert frame._dialog_hint(screen) == "↑↓ newer/older · Esc close"
def test_an_atom_the_lane_only_half_covers_stands() -> None:
"""Half a truth is worse than the whole atom: it survives unless every key is a chip."""
pager_only = (None, None, None, FPair("Page ↓", "pagedown"), FPair("Page ↑", "pageup"))
assert strip_lane_atoms("PgUp/PgDn scroll", pager_only) == ""
# No Shift bank here, so Home/End are nowhere on the lane.
assert strip_lane_atoms("Home/End ends", pager_only) == "Home/End ends"
# The atom documents the arrows too, and no slot ever claims those.
assert strip_lane_atoms("↑↓ PgUp/PgDn scroll", DEFAULT_LANE) == "↑↓ PgUp/PgDn scroll"
# A second key riding in the verb half keeps the atom whole (the map's region/you).
assert strip_lane_atoms("Home/^U region/you", DEFAULT_LANE) == "Home/^U region/you"
def test_a_dimmed_chip_still_covers_its_atom() -> None:
"""Dim says *a thing here, just not right now* — the reader has been told where it is."""
assert strip_lane_atoms("PgUp/PgDn scroll", default_lane(nav=False)) == ""