From 1eda585c50a93ffec9679ac2245a99feacb53768 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Martineau Date: Sun, 9 Aug 2026 11:37:38 -0400 Subject: [PATCH] F-lane: You was standing in for a list end it isn't The mesh walk's You had been living behind Page up, in the pager's Shift bank, because that bank is where a screen's Home lands by default. But You is not an end of the link list -- it drops the whole trail and refocuses our own node -- so it had to pretend to be Top, and Bottom had to stay blank to keep the pretence, and the walk was the one scrolling screen whose Home didn't scroll. So You takes a slot of the screen's own (F3, which is what F1-F3 are for), and the pager's Shift bank goes back to meaning what it means everywhere else: F10 Top, F9 Bottom, the two ends of the list. Home and End on a keyboard follow the same correction and now move the highlight. Its chord moves with it: locate is ^U, for you, on the map as well as here. The letter is the mnemonic of the action rather than one screen's word for it -- ^L only ever read as "locate" from inside the map's code, and the action is the same one on both screens, so it should not have been two mnemonics waiting to happen. You dims once the focus is already us and no find is narrowing the list -- there is nowhere to come back from. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 2 +- meshterm/ui/map_screen.py | 12 +++++----- meshterm/ui/tui/session.py | 5 ++++- meshterm/ui/walk_screen.py | 41 +++++++++++++++++++++++----------- tests/test_picocalc_chrome.py | 10 ++++++--- tests/test_walk.py | 42 ++++++++++++++++++++++++++++------- 6 files changed, 80 insertions(+), 32 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e528a72..b0a0de7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -219,7 +219,7 @@ branch on the platform per frame, and never `from meshterm.platforms import PLAT thing that just isn't available this paint. Dimming is presentational — `handle` stays the authority and no-ops. The lane is also the *only* affordance advertisement here, so anything the desktop reaches by a chord or a bare letter (a list's `^PgUp/^PgDn` section - jumps, the Time Machine's `w`, the map's `^L`, a row's `Del`) earns a slot — otherwise + jumps, the Time Machine's `w`, the map's `^U`, a row's `Del`) earns a slot — otherwise it is undiscoverable on the device. - `meshterm specimen` prints the whole visual language through the real funnels — the acceptance card on-device, a preview under `--platform picocalc` on the desktop. diff --git a/meshterm/ui/map_screen.py b/meshterm/ui/map_screen.py index cec3164..d2e5577 100644 --- a/meshterm/ui/map_screen.py +++ b/meshterm/ui/map_screen.py @@ -14,8 +14,8 @@ render_map`. Keys: * ``PgUp`` / ``PgDn`` zoom in / out, * ``Home`` recenters and refits to the dense core of the nodes — the *region* the mesh covers, and the same default view the map opens on, -* ``Ctrl+L`` recenters on **your own node**, keeping the zoom you chose and clearing any - find (the lane's ``You`` chip; its Shift half also zooms in close), +* ``^U`` (for *you*) recenters on **your own node**, keeping the zoom you chose and + clearing any find (the lane's ``You`` chip; its Shift half also zooms in close), * **typing finds nodes**: every letter key feeds a live name filter — matching nodes keep bright labels while the rest dim to context, ``Enter`` frames the matches, ``Backspace`` edits, and ``Esc`` clears the filter (a second ``Esc`` leaves the map). This is why no @@ -224,7 +224,7 @@ class MapScreen(Screen): editing keys, so the typed text is always visible somewhere fixed. The line spends its whole 72-cell budget, so the two view-jump keys share one atom - (``Home/^L region/you``) and ``⇧ fine`` — a refinement of a key the line already + (``Home/^U region/you``) and ``⇧ fine`` — a refinement of a key the line already names, and the only atom here that documents a *modifier* rather than a binding — is the one that gives way to make room for them. The basemap's state used to hang off the end of this line as a suffix; it is a status atom, not a key, so it moved @@ -233,7 +233,7 @@ class MapScreen(Screen): """ if self._filter: return f"find: {self._filter}▏ · Enter frame · ⌫ erase · Esc clear" - return "↑↓←→ pan · PgUp/PgDn zoom · Home/^L region/you · type to find · Esc back" + return "↑↓←→ pan · PgUp/PgDn zoom · Home/^U region/you · type to find · Esc back" def consume_edge_scrub(self) -> int: """Right-edge columns the session should force-repaint on the next paint (0 = none). @@ -520,7 +520,7 @@ class MapScreen(Screen): active filter first, the map itself only once the filter is clear. Three actions reframe the view, and each has both a key and an F-key chip: - ``home`` the whole region, ``locate`` (Ctrl+L) our own node — ``locate_zoom`` + ``home`` the whole region, ``locate`` (^U, for *you*) our own node — ``locate_zoom`` the Shift-bank variant that also homes in — and ``frame`` the find matches, which is what Enter already does while a query is being typed, kept as a separate action so the lane can name it on a platform that draws no hint line. @@ -589,7 +589,7 @@ class MapScreen(Screen): return next((m for m in self._markers if m.is_self), None) def _locate(self, vp: Viewport, *, zoom_in: bool = False) -> None: - """Recentre on our own node (``Ctrl+L`` / the ``You`` chip), clearing any find. + """Recentre on our own node (``^U`` / the ``You`` chip), clearing any find. Plain ``You`` keeps the zoom the user chose — pressing it twice does the same thing twice, and pairing it with one Zoom + is a single extra press. Its Shift diff --git a/meshterm/ui/tui/session.py b/meshterm/ui/tui/session.py index 84535f8..a2b2991 100644 --- a/meshterm/ui/tui/session.py +++ b/meshterm/ui/tui/session.py @@ -58,11 +58,14 @@ from .select import Choice, ReorderScreen, SelectScreen, Separator #: genuine third-level character arrives as some *other* glyph and never matches this table. #: ``m``/``i``/``h`` are not free: prompt_toolkit spells Enter, Tab and Backspace as #: ``Keys.ControlM``/``ControlI``/``ControlH``, so claiming them here would rebind those keys. +#: A chord's letter is the mnemonic of the *action*, not of one screen's word for it — +#: ``locate`` is ^U for **you** (JP, 2026-08-09), the same key on the map and in the mesh +#: walk, because what it names on both is our own node. _CTRL_LETTER_CHORDS: dict[str, str] = { "c": "quit", - "l": "locate", "p": "paths", "r": "retry", + "u": "locate", "v": "paste_clipboard", } diff --git a/meshterm/ui/walk_screen.py b/meshterm/ui/walk_screen.py index 195636a..ea94b23 100644 --- a/meshterm/ui/walk_screen.py +++ b/meshterm/ui/walk_screen.py @@ -213,20 +213,24 @@ class WalkScreen(Screen): @property def fkey_lane(self): - """The shared pager over the link list, with ``You`` behind Page ↑ and nothing behind ↓. + """``You`` on the screen's own F3, the shared pager and its two ends on F4/F5. - The walk's Home does not scroll to a top — it drops the whole trail and puts the - focus back on our own node — so the Shift companion that would say *Top* says - ``You`` instead. ``End`` is bound to nothing here, so its companion stays blank: - a trail has a beginning (us) but no far end to jump to. Both nav slots need rows - to move through, which a leaf node in a sparse graph may not have. + ``You`` is not an end of the link list — it drops the whole trail and puts the + focus back on our own node — so it never belonged behind the pager, where it had + to stand in for *Top* and leave *Bottom* blank to keep the pretence. It takes a + slot of the screen's own instead (JP, 2026-08-09), which is what F1-F3 are for, + and the pager's Shift bank goes back to meaning here what it means everywhere + else: the two ends of the list this screen scrolls. ^U reaches the same action on + a keyboard, the same chord the map spends on our node. + + Both nav slots need rows to move through, which a leaf node in a sparse graph may + not have; ``You`` needs somewhere to come back *from* — a walked trail, or a find + narrowing the list — and dims once the focus is already us. """ from .tui.fkeys import FPair, default_lane - paging = len(self._rows()) > 1 - lane = list(default_lane(nav=paging)) - lane[3] = FPair("Page ↓", "pagedown", enabled=paging) - lane[4] = FPair("Page ↑", "pageup", "You", "home", enabled=paging) + lane = list(default_lane(nav=len(self._rows()) > 1)) + lane[2] = FPair("You", "locate", enabled=len(self._trail) > 1 or bool(self._filter)) return lane def __init__( @@ -364,10 +368,15 @@ class WalkScreen(Screen): """Walking keys — or the live find query while one is being typed.""" if self._filter: return f"find: {self._filter}▏ · ↑↓ move · Enter focus · ⌫ erase · Esc clear" - return "↑↓ move · Enter focus · ⌫ back · Home you · type to find · Esc back" + return "↑↓ move · Enter focus · ⌫ back · ^U you · type to find · Esc back" def handle(self, action: str, data: str = "") -> None: - """Move the highlight, walk, back up, find, or dismiss.""" + """Move the highlight, walk, back up, find, or dismiss. + + Home/End are the list's own ends here, as on every other scrolling screen; the jump + back to our own node is ``locate`` (^U, and the lane's F3), which is not a place in + this list at all — see :attr:`fkey_lane`. + """ rows = self._rows() if action == "escape": if self._filter: @@ -395,7 +404,13 @@ class WalkScreen(Screen): elif len(self._trail) > 1: self._trail.pop() self._index = 0 - elif action in ("home", "ctrl_home"): + elif action in ("home", "ctrl_home") and rows: + self._index = 0 + elif action in ("end", "ctrl_end") and rows: + self._index = len(rows) - 1 + elif action == "locate": + # ^U (and F3): abandon the walk rather than move within it — the trail goes + # back to just us and any find narrowing the list is dropped with it. self._trail = [self._topo.self_id] self._filter = "" self._index = 0 diff --git a/tests/test_picocalc_chrome.py b/tests/test_picocalc_chrome.py index d73969e..e260281 100644 --- a/tests/test_picocalc_chrome.py +++ b/tests/test_picocalc_chrome.py @@ -212,13 +212,17 @@ def test_dialogs_draw_no_lane_at_all() -> None: 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.""" + """``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["l"] == "locate" # the right-Ctrl rescue's half - assert _KEY_ACTIONS[Keys.ControlL] == "locate" # the ordinary binding, generated from it + 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: diff --git a/tests/test_walk.py b/tests/test_walk.py index 4f6bd3e..254eae4 100644 --- a/tests/test_walk.py +++ b/tests/test_walk.py @@ -305,13 +305,22 @@ def test_walk_trail_names_carry_their_node_hues() -> None: ) -def test_walk_home_refocuses_us() -> None: - """Home resets the walk to our own node from anywhere.""" +def test_walk_locate_refocuses_us_and_home_end_walk_the_list() -> None: + """^U resets the walk to our own node from anywhere; Home/End are the list's ends.""" topo = _topo() screen = _screen(topo) screen.render_body(80) screen.handle("enter") # us → YUL + rows = screen._rows() + assert len(rows) > 1, "the fixture's YUL needs a list to move within" + + # Home/End move the highlight — they no longer abandon the walk. + screen.handle("end") + assert screen._index == len(rows) - 1 and screen._trail[-1] != topo.self_id screen.handle("home") + assert screen._index == 0 and screen._trail[-1] != topo.self_id + + screen.handle("locate") assert screen._trail == [topo.self_id] @@ -348,8 +357,8 @@ def test_walk_echoes_the_find_query_above_the_matches_it_narrows() -> None: set_platform(REGULAR) -def test_walk_fkey_lane_says_you_where_the_shared_lane_would_say_top() -> None: - """Home drops the trail rather than scrolling, and End is bound to nothing at all.""" +def test_walk_fkey_lane_gives_you_its_own_slot_over_the_pagers_ends() -> None: + """``You`` is a verb of this screen (F3), not an end of the list the pager scrolls.""" from meshterm.ui.tui.fkeys import action_for screen = _screen(_topo()) @@ -357,11 +366,28 @@ def test_walk_fkey_lane_says_you_where_the_shared_lane_would_say_top() -> None: lane = screen.fkey_lane assert [pair.label if pair else None for pair in lane] == [ - None, None, None, "Page ↓", "Page ↑", + None, None, "You", "Page ↓", "Page ↑", ] - # The jump behind Page ↑ goes back to us; the one behind Page ↓ doesn't exist here. - assert lane[4].opp_label == "You" and action_for(lane, 10) == "home" - assert lane[3].opp_label == "" and action_for(lane, 9) is None + assert action_for(lane, 3) == "locate" + assert lane[2].opp_label == "" # nothing is the opposite of going home + # And the pager's Shift bank means what it means everywhere else: the list's two ends. + assert lane[4].opp_label == "Top" and action_for(lane, 10) == "home" + assert lane[3].opp_label == "Bottom" and action_for(lane, 9) == "end" + + +def test_walk_you_dims_once_the_focus_is_already_us() -> None: + """Dim says *a thing here, just not right now* — there is nowhere to come back from.""" + screen = _screen(_topo()) + screen.render_body(80) + assert screen.fkey_lane[2].enabled is False # opens on us, trail of one + + screen.handle("enter") # walked away: now there is + assert screen.fkey_lane[2].enabled is True + + screen.handle("locate") + assert screen.fkey_lane[2].enabled is False + screen.handle("text", "a") # a find narrows the list off our own neighbours too + assert screen.fkey_lane[2].enabled is True def test_walk_came_from_anchors_west_and_the_fan_stays_east() -> None: