diff --git a/CLAUDE.md b/CLAUDE.md index abf5ae9..8e95285 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -203,11 +203,12 @@ branch on the platform per frame, and never `from meshterm.platforms import PLAT wanted a chip for consistency. That leaves **F1–F3 free on every screen** for its own verbs, and `EMPTY_LANE` for a screen with none at all (every dialog). **A chip names an action, never a key** — `Page ↑`, not `PgUp`; `Latest` on a transcript; `Region` on the - map, whose Home reframes and whose paging zooms. **A directional pair rises to the - right**: where two adjacent chips are opposite ends of one axis (F4/F5's paging, the - map's zoom, a select list's section jumps), the *up · in · more* end takes the right - slot — `Zoom -` then `Zoom +`, a rocker — and a slot's Shift companion follows the same - rule along its own slot. The lane *is* the footer here, so it obeys the hint line's rule + map, whose Home reframes and whose paging zooms. **A directional pair rises toward its + outer key**: where two adjacent chips are opposite ends of one axis, the *up · in · + more* end takes the slot nearer the lane's edge — F5 on the right-hand pair (F4/F5's + paging, the map's zoom: `Zoom -` then `Zoom +`, a rocker), F1 on a left-hand pair (a + select list's `Sect ↑` then `Sect ↓`) — and a slot's Shift companion follows its own + slot's direction. The lane *is* the footer here, so it obeys the hint line's rule — never advertise a key that would do nothing. Empty and dim are different claims: leave the slot **empty** when the action isn't a thing on this screen (Retry in a channel), and clear `enabled`/`opp_enabled` to draw it **dim** (label kept, fill dropped) when it's a diff --git a/meshterm/ui/tui/fkeys.py b/meshterm/ui/tui/fkeys.py index 5240db3..924e6a6 100644 --- a/meshterm/ui/tui/fkeys.py +++ b/meshterm/ui/tui/fkeys.py @@ -104,13 +104,15 @@ Lane = Sequence[Optional[FPair]] #: none leaves them blank rather than filling the lane out. A screen whose body is not one #: scrolling column says so in its own words (see :class:`ChatScreen`, ``MapScreen``). #: -#: **A directional pair rises to the right** (JP, 2026-08-08): wherever two adjacent chips -#: are opposite ends of one axis — the F4/F5 pager here, the map's zoom, a select list's -#: section jumps — the *up · in · more* end takes the right slot and its opposite the left, -#: so every such pair reads like a rocker (``−`` then ``+``) and one handedness carries -#: across screens. A slot's own Shift companion follows the same rule *along its slot*: the -#: jump behind ``Page ↑`` is the one Page ↑ is heading for, so F10 is Top and F9 Bottom. A -#: screen relabelling a pair keeps that order (a chat's ``Latest``/``Oldest``). +#: **A directional pair rises toward its outer key** (JP, 2026-08-08): wherever two +#: adjacent chips are opposite ends of one axis, the *up · in · more* end takes the slot +#: nearer the lane's own edge. On the right-hand F4/F5 pair that is F5 — the pager here, +#: the map's zoom, ``Zoom -`` then ``Zoom +``, a rocker — and on a left-hand F1/F2 pair it +#: is F1, so a select list reads ``Sect ↑`` then ``Sect ↓``: ergonomically the hand +#: anchors on the lane's edge and rocks inward, whichever side the pair lives on. A slot's +#: own Shift companion follows the same rule *along its slot*: the jump behind ``Page ↑`` +#: is the one Page ↑ is heading for, so F10 is Top and F9 Bottom. A screen relabelling a +#: pair keeps its order (a chat's ``Latest``/``Oldest``). DEFAULT_LANE: tuple[Optional[FPair], ...] = ( None, None, diff --git a/meshterm/ui/tui/select.py b/meshterm/ui/tui/select.py index 8eb4b71..60fd636 100644 --- a/meshterm/ui/tui/select.py +++ b/meshterm/ui/tui/select.py @@ -213,8 +213,8 @@ class SelectScreen(Screen): Ctrl+PageUp/PageDown step the highlight heading by heading — and on the PicoCalc they are the one binding in the app that is *physically unreachable*: paging has no key there to hold Ctrl over. So a grouped list promotes them onto the lane, - keeping the shared handedness (down left, up right — see - :data:`~meshterm.ui.tui.fkeys.DEFAULT_LANE`). + keeping the shared handedness — a pair rises toward its outer key, so on this + left-edge pair *up* takes F1 (see :data:`~meshterm.ui.tui.fkeys.DEFAULT_LANE`). Their two gates say different things, per the lane's empty-versus-dim rule. A list that has no headings *at all* leaves both slots blank: sections aren't a thing @@ -237,8 +237,8 @@ class SelectScreen(Screen): lane = list(default_lane(nav=len(self._choices(rows)) > 1)) if len(self._all_section_starts()) >= 2: live = len(self._section_starts(rows)) > 1 - lane[0] = FPair("Sect ↓", "ctrl_pagedown", enabled=live) - lane[1] = FPair("Sect ↑", "ctrl_pageup", enabled=live) + lane[0] = FPair("Sect ↑", "ctrl_pageup", enabled=live) + lane[1] = FPair("Sect ↓", "ctrl_pagedown", enabled=live) if self._delete_hint: choices = self._choices(rows) current = choices[max(0, min(self._index, len(choices) - 1))] if choices else None diff --git a/tests/test_picocalc_chrome.py b/tests/test_picocalc_chrome.py index b42e0bf..d73969e 100644 --- a/tests/test_picocalc_chrome.py +++ b/tests/test_picocalc_chrome.py @@ -181,9 +181,9 @@ def test_select_lane_promotes_the_section_jumps_only_where_there_are_sections() Separator("── Far ──"), Choice("gamma", 3), ]) lane = grouped.fkey_lane - assert [pair.label for pair in lane[:2]] == ["Sect ↓", "Sect ↑"] - # Down left, up right — the same handedness the pager below them reads with. - assert action_for(lane, 1) == "ctrl_pagedown" and action_for(lane, 2) == "ctrl_pageup" + 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: @@ -191,7 +191,7 @@ def test_select_lane_promotes_the_section_jumps_only_where_there_are_sections() for ch in "gam": grouped.handle("text", ch) dim = grouped.fkey_lane - assert [pair.label for pair in dim[:2]] == ["Sect ↓", "Sect ↑"] + assert [pair.label for pair in dim[:2]] == ["Sect ↑", "Sect ↓"] assert not any(pair.enabled for pair in dim[:2])