diff --git a/meshterm/ui/node_detail_screen.py b/meshterm/ui/node_detail_screen.py index 8527b4b..b54bf4e 100644 --- a/meshterm/ui/node_detail_screen.py +++ b/meshterm/ui/node_detail_screen.py @@ -340,15 +340,17 @@ class NodeDetailScreen(Screen): @property def fkey_lane(self): - """The shared pager over the route list, plus one chip per tab on F1/F2. + """The shared pager over the route list, plus the tab switch on F3. The strip shows *that* there are two views; nothing on screen says the key that - moves between them, and on a platform with no hint line the chips are the only - place to learn it. Each tab takes its own slot in strip order — F1 ``Info``, F2 - ``Routes`` (JP, 2026-08-08) — so a view is one press away rather than a toggle to - cycle. The chip you are already on dims: the view is a thing here, pressing its - key just changes nothing this paint. A page with one tab has nothing to switch, - so the slots stay empty. + moves between them, and on a platform with no hint line the chip is the only place + to learn it. It names the tab it would take you *to*, never the one you are on — + the strip already marks that, and a chip repeating it would say nothing about what + pressing it does. (A per-tab chip on F1/F2 was tried and reverted — JP, 2026-08-09: + the toggle was better.) Unlike the Time Machine's window chip, this one carries no + ``▸`` lead-in: ``Routes`` is exactly six cells on its own, and the strip beside it + makes the direction plain anyway. A page with one tab has nothing to switch, so + the slot stays empty. The pager gates on the route list actually being windowed: on the Info tab, and on a node whose routes all fit, the keys move nothing. @@ -357,8 +359,8 @@ class NodeDetailScreen(Screen): lane = list(default_lane(nav=self._list_hidden)) if len(self._tabs) >= 2: - for i, tab in enumerate(self._tabs[:3]): - lane[i] = FPair(tab.name, f"tab_{i}", enabled=i != self._tab_index) + nxt = self._tabs[(self._tab_index + 1) % len(self._tabs)] + lane[2] = FPair(nxt.name, "tab") return lane def __init__( @@ -558,9 +560,6 @@ class NodeDetailScreen(Screen): self._switch_tab(1) elif action == "shift_tab": self._switch_tab(-1) - elif action.startswith("tab_") and action[4:].isdigit(): - # The lane's per-tab chips (F1 Info, F2 Routes) land on a view directly. - self._select_tab(int(action[4:])) elif action == "up": if n: self._row_index = (self._row_index - 1) % n @@ -611,20 +610,10 @@ class NodeDetailScreen(Screen): return self._cursor def _switch_tab(self, delta: int) -> None: - """Cycle the active tab (``Tab``/``Shift+Tab``); a lone tab has nowhere to go.""" + """Move the active tab, resetting the cursor and list window to that tab's top.""" if len(self._tabs) < 2: return - self._select_tab((self._tab_index + delta) % len(self._tabs)) - - def _select_tab(self, index: int) -> None: - """Land the stage on tab ``index``, resetting the cursor and list window to its top. - - A no-op off the strip or on the tab already showing, so the lane's dimmed - current-tab chip stays presentational — ``handle`` remains the authority. - """ - if not (0 <= index < len(self._tabs)) or index == self._tab_index: - return - self._tab_index = index + self._tab_index = (self._tab_index + delta) % len(self._tabs) self._row_index = 0 self._route_sel = 0 self._list_top = 0 diff --git a/meshterm/ui/timemachine_screen.py b/meshterm/ui/timemachine_screen.py index be6d827..f068e97 100644 --- a/meshterm/ui/timemachine_screen.py +++ b/meshterm/ui/timemachine_screen.py @@ -5,7 +5,7 @@ overheard packet since the first session, but nothing surfaced that history beyo dashboard's two-hour window — this screen is the archaeology dig. A picker offers the whole mesh or any node ever heard; each subject renders as a scrollable page of braille charts and stats over a switchable window (``w`` cycles 24 h → 7 d → 30 d → all time — -the PicoCalc stops at 30 d, its F1–F3 chips carrying the three spans directly): +the PicoCalc's ring stops at 30 d, its F3 chip cycling the same three spans): * a **node** shows its reception volume over the window, its median-SNR band (coloured by quality), its hour-of-day rhythm (when does this node talk?), and the roll-up @@ -68,9 +68,10 @@ _ALL_WINDOWS: tuple[tuple[str, Optional[timedelta]], ...] = ( ("all time", None), ) -#: The offered windows. The PicoCalc stops at 30 d (JP, 2026-08-08): its three window -#: chips on F1–F3 *are* the offer there, and all-time — the one span whose scan grows -#: with the whole history — stays a desktop affordance. Bound at platform-switch time. +#: The offered windows. The PicoCalc's ring stops at 30 d (JP, 2026-08-08): all time — +#: the one span whose scan grows with the whole history — stays a desktop affordance, +#: and the F3 chip there cycles the three spans that remain. Bound at platform-switch +#: time. _WINDOWS: tuple[tuple[str, Optional[timedelta]], ...] = _ALL_WINDOWS @@ -80,6 +81,7 @@ def _bind_windows(platform: Platform) -> None: global _WINDOWS _WINDOWS = _ALL_WINDOWS[:3] if platform.footer_fkeys else _ALL_WINDOWS + #: Picker sentinel for the whole-mesh overview page. MESH = ("mesh",) @@ -221,21 +223,26 @@ class TimeMachineScreen(Screen): @property def fkey_lane(self): - """The shared pager, plus one chip per span on F1–F3. + """The shared pager, plus the window cycle on F3. - The window is the whole point of this screen — the same history at several spans - — and on a platform with no hint line the chips are the only place to learn the - spans exist. Each takes its own slot in ring order — F1 ``24 h``, F2 ``7 d``, F3 - ``30 d`` (JP, 2026-08-08) — so any span is one press away rather than a cycle to - chase with ``w`` (which still cycles). The span on screen dims: it is a thing - here, its key just changes nothing this paint. All time is not in this platform's - ring at all (see :func:`_bind_windows`) — three chips, three spans. + ``w`` is the whole point of this screen — the same history at several spans — and + it is exactly the kind of affordance that vanishes on a platform with no hint line + to read it off. The chip is the only place the PicoCalc can learn the key exists, + so it earns the free F3 slot even though the letter itself is easy to press. (One + chip per span on F1–F3 was tried and reverted — JP, 2026-08-09: the cycle was + better.) + + The chip names the span it would *take you to*, not the one on screen: the title + already says where you are, so a chip repeating it would be the same claim twice + and would never tell you what pressing it does. ``all time`` shortens to ``all`` + to stay inside the 6-cell chip — the only span whose name doesn't already fit, + and one this platform's ring may not even offer (see :func:`_bind_windows`). """ from .tui.fkeys import FPair, default_lane lane = list(default_lane(nav=self.content_overflows)) - for i, (name, _delta) in enumerate(_WINDOWS[:3]): - lane[i] = FPair(name, f"window_{i}", enabled=i != self._window_index) + nxt, _delta = _WINDOWS[(self._window_index + 1) % len(_WINDOWS)] + lane[2] = FPair(f"▸ {'all' if nxt == 'all time' else nxt}", "window") return lane def __init__( @@ -267,12 +274,11 @@ class TimeMachineScreen(Screen): self.title = f"{self._label} · {name}" def handle(self, action: str, data: str = "") -> None: - """Scroll, switch the window, or dismiss. + """Scroll, cycle the window, or dismiss. - Two ways onto a span, one behaviour: ``w`` (and the legacy ``window`` action) - cycles the platform's ring, while the lane's per-span chips land on a window - directly (``window_0`` … ``window_2``) — so a chip is never a second - implementation of the letter. + The window cycles on either the ``w`` key or the ``window`` action the F-key lane + dispatches — one behaviour, two ways in, so the chip is not a second implementation + of the letter. The ring it cycles is the platform's (see :func:`_bind_windows`). """ if action == "up": self.scroll_lines(-1) @@ -287,21 +293,13 @@ class TimeMachineScreen(Screen): elif action in ("end", "ctrl_end"): self.scroll_to_bottom() elif action == "window" or (action == "text" and data.lower() == "w"): - self._set_window((self._window_index + 1) % len(_WINDOWS)) - elif action.startswith("window_") and action[7:].isdigit(): - self._set_window(int(action[7:])) + self._window_index = (self._window_index + 1) % len(_WINDOWS) + self._set_title() + self.scroll_to_top() + self._session.invalidate() elif action == "escape": self.resolve(None) - def _set_window(self, index: int) -> None: - """Land the page on window ``index`` (a no-op off the ring or already there).""" - if not (0 <= index < len(_WINDOWS)) or index == self._window_index: - return - self._window_index = index - self._set_title() - self.scroll_to_top() - self._session.invalidate() - def render_body(self, width: int) -> list[str]: """Render (or reuse) the current window's sections.""" key = (self._window_index, width) diff --git a/tests/test_node_detail.py b/tests/test_node_detail.py index 16129c4..0455815 100644 --- a/tests/test_node_detail.py +++ b/tests/test_node_detail.py @@ -844,17 +844,13 @@ def test_node_detail_screen_tabs_switch_the_stage() -> None: assert "│ Info │" in body and "centred here" in body assert screen.consume_edge_scrub() == 2 - # One F-key chip per tab, in strip order; the tab on screen dims (its key would - # change nothing), and pressing the other's chip lands on that view directly. - assert [pair.label for pair in screen.fkey_lane[:2]] == ["Info", "Routes"] - assert screen.fkey_lane[0].enabled is False and screen.fkey_lane[1].enabled is True - screen.handle("tab_1") # the F2 chip: land on the Routes tab + # The F-key chip names where the switch would take you, so it flips with the stage. + assert screen.fkey_lane[2].label == "Routes" + screen.handle("tab") # switch to the Routes tab body = _plain(screen.render_body(72)) assert "│ Routes │" in body and "no route observed yet" in body assert screen.consume_edge_scrub() == 0 # the braille preview isn't showing now - assert screen.fkey_lane[0].enabled is True and screen.fkey_lane[1].enabled is False - screen.handle("tab") # Tab still cycles, wrapping back to Info - assert screen.fkey_lane[0].enabled is False + assert screen.fkey_lane[2].label == "Info" def test_node_detail_single_tab_hides_the_switch_hint() -> None: diff --git a/tests/test_timemachine.py b/tests/test_timemachine.py index 6efbf10..eb1dd79 100644 --- a/tests/test_timemachine.py +++ b/tests/test_timemachine.py @@ -909,17 +909,33 @@ def test_screen_cycles_windows_and_caches(tmp_path: Path) -> None: screen.render_body(80) assert len(calls) == 2 - # One chip per span on F1–F3 — the only way a platform that draws no hint line can - # learn the spans exist. The span on screen dims; the direct chips land on their - # window without cycling; `w` (and the legacy "window" action) still cycles the ring. - assert [pair.label for pair in screen.fkey_lane[:3]] == ["24 h", "7 d", "30 d"] - assert screen.fkey_lane[2].enabled is False # 30 d is what's on screen + # The F-key chip is the same behaviour under another name — the only way a platform + # that draws no hint line can learn that `w` exists at all. It names the span it takes + # you *to*, since the title already says where you are. + assert screen.fkey_lane[2].label == "▸ all" screen.handle("window") - assert "all time" in screen.title # the desktop ring still carries all time - assert all(pair.enabled for pair in screen.fkey_lane[:3]) - screen.handle("window_0") - assert "24 h" in screen.title - assert screen.fkey_lane[0].enabled is False + assert "all time" in screen.title + assert screen.fkey_lane[2].label == "▸ 24 h" # the ring wraps back around + + +def test_picocalc_window_ring_stops_at_30_days() -> None: + """The handheld's ring skips all time: `w` wraps 24 h → 7 d → 30 d → 24 h. + + All time is the one span whose scan grows with the whole history — a desktop + affordance (see ``_bind_windows``); the chip cycles the three spans that remain. + """ + from meshterm.platforms import PICOCALC, set_platform + + set_platform(PICOCALC) + screen = TimeMachineScreen( + session=_FakeSession(), label="YUL", build=lambda window, width: [] + ) + assert "7 d" in screen.title # opens on 7 d, as everywhere + screen.handle("text", "w") + assert "30 d" in screen.title + assert screen.fkey_lane[2].label == "▸ 24 h" # never "▸ all" on this ring + screen.handle("window") + assert "24 h" in screen.title # wrapped straight past where all time would sit # --- the own-node page ------------------------------------------------------------------