F-key lane: the F3 toggles were better - restored

The per-item chips (node page F1 Info / F2 Routes, Time machine
F1-F3 spans) revert to the single next-thing toggle on F3, per JP's
call. The PicoCalc window ring still stops at 30 d; its F3 chip just
cycles the three spans that remain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Fable 5 committed 2026-08-09 00:10:37 -04:00
1 parent f5244eb4f5
commit 70fb0ceea3
4 files changed
+72 -73

No files matched your search

+13 -24
View File
@@ -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
+29 -31
View File
@@ -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)
+4 -8
View File
@@ -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:
+26 -10
View File
@@ -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 ------------------------------------------------------------------