The splash says bye, and keeps the reader's place when a row goes

Three passes over what landed yesterday.

The hidden-count line stops standing in the list. With devices still
listed the footer already names ⇧H on every row, so the line was a row
of the box spent saying it twice; it stays only for a list hiding has
emptied, where there is no footer atom to read it off and "no
companion devices detected" would be a lie about a machine with a
radio plugged into it.

Hiding now leaves the highlight where the row was - the next device
down, or the one above at the end, never rolling round to the top - so
a run of adapters clears with a run of presses instead of the cursor
jumping back to the remembered default after each one.

And Esc says bye here rather than quit: the splash is the door, and
the reader leaving it has not started anything to quit out of.

One consequence, on the console: the splash border is 47 cells and the
sentence now sheds the hide key too when it must. Between the two
shortcuts the survivor is the way back - ⇧H only appears once
something is hidden, which is exactly when a reader needs to be told
how to undo it, and by then they have found h.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5 committed 2026-09-09 01:45:19 -04:00
1 parent 4109c73522
commit 398a184eed
4 files changed
+111 -35

No files matched your search

+9 -2
View File
@@ -187,8 +187,15 @@ as a grouped list does. Filling a page in is editing its `.md`; no Python follow
- ≤72 cells. Sentence shape: navigation keys, then action keys, **Esc last**.
- Esc verb by surface: `Esc back` leaves a screen · `Esc close` dismisses a read-only
floating view · `Esc cancel` abandons a prompt/dialog · `Esc keep` leaves a value
picker unchanged · `Esc quit` only at the main menu. While a find-as-you-type filter
is standing the verb becomes `Esc clear`, because that is what the press does then.
picker unchanged · `Esc quit` only at the main menu · `Esc bye` only on the device
splash, which is the door rather than a screen — nothing has been started there to
quit out of. While a find-as-you-type filter is standing the verb becomes `Esc clear`,
because that is what the press does then.
- A hint that outgrows the surface drawing it **sheds atoms rather than being cut off**,
because the cut takes the end and the end is Esc (`frame.fit_hint`, the chromeless
splash). They go from the right, in front of Esc; a screen may name the ones it can
spare first (`spare_hint_atoms`) — a key the reader would find anyway, like the ←→ its
move atom already named, before one that is unguessable and advertised nowhere else.
- Enter verb by action: `Enter open` when the row pushes a screen or dialog ·
`Enter select` when it picks a value or action row · `Enter set` in a value picker.
A more specific committing verb (`Enter adopt path`, `Enter add`) is fine; a synonym
+58 -21
View File
@@ -297,6 +297,9 @@ async def prompt_device(
which the caller treats as a request to exit.
"""
scanned = list(devices)
#: The row the *next* redraw should open on, when the pass just finished moved the list
#: under the reader (see :func:`_after_hiding`). Cleared as soon as it is spent.
focus: DiscoveredDevice | None = None
while True:
remembered = store.load()
# The full registry (not just the single last device) so *every* confirmed companion
@@ -318,13 +321,19 @@ async def prompt_device(
# row hidden a moment ago is gone the next time the list is drawn — which is the
# only feedback hiding needs.
hidden = store.hidden_ids()
visible = [d for d in listed if d.stable_id not in hidden]
# Preselect the remembered "last known good" device when it is currently attached/in range.
default = next((d for d in visible if remembered and remembered.matches(d)), None)
# In display order, so a shortcut acting on a row can say what the row *after* it is.
ordered = _order([d for d in listed if d.stable_id not in hidden], registry)
# Preselect the remembered "last known good" device when it is currently attached/in
# range — unless the last pass asked for a particular row, which a hide does so the
# highlight lands where the vanished row was rather than jumping back to the default.
default = next((d for d in ordered if remembered and remembered.matches(d)), None)
if focus is not None:
default = focus if focus in ordered else default
focus = None
# Build the rows once so the same list can be redrawn as the backdrop behind a
# removal confirm (so it floats over the picker rather than replacing it).
items = _build_items(visible, remembered, registry, hidden=len(hidden))
items = _build_items(ordered, remembered, registry, hidden=len(hidden))
chosen = await ui.select_startup(
"Select a companion device",
items,
@@ -339,7 +348,7 @@ async def prompt_device(
return None
if isinstance(chosen, KeyRequest):
_run_shortcut(store, chosen)
focus = _run_shortcut(store, chosen, ordered)
continue
if chosen is _ADD_TCP:
@@ -372,20 +381,52 @@ async def prompt_device(
return chosen
def _run_shortcut(store: DeviceStore, request: KeyRequest) -> None:
"""Act on ``h`` / ``⇧H``; the redrawn list is the feedback either way.
def _run_shortcut(
store: DeviceStore, request: KeyRequest, listed: list[DiscoveredDevice]
) -> DiscoveredDevice | None:
"""Act on ``h`` / ``⇧H``, and say which row the redrawn list should open on.
Nothing is said about it, because there is nothing to say that the list does not: after
a hide the row is gone, after a show-all the hidden rows are back. A dialog acknowledging
a change the reader is looking straight at is the acknowledgement this app has been
taking out of screens, not adding to them.
Nothing is *said* about either: after a hide the row is gone, after a show-all the hidden
rows are back, and a dialog acknowledging a change the reader is looking straight at is
the acknowledgement this app takes out of screens rather than adding to them. What the
redraw does owe them is their place — see :func:`_after_hiding`.
Args:
store: The registry the hidden set lives in.
request: The shortcut the splash resolved with.
listed: The devices as displayed, in display order.
Returns:
The device to highlight when the list is redrawn, or ``None`` to let the remembered
default decide (which is what a show-all wants: the list it restores is a different
list, and the reader's place in the old one means nothing in it).
"""
if request.action is _SHOW_ALL:
store.show_all()
elif request.action is _HIDE and isinstance(request.value, DiscoveredDevice):
return None
if request.action is _HIDE and isinstance(request.value, DiscoveredDevice):
# Only a device row can be hidden — ``h`` on Add a network device or Quit is inert,
# the way Delete is on a row that never opted into removal.
store.hide(request.value.stable_id)
return _after_hiding(listed, request.value)
return None
def _after_hiding(
listed: list[DiscoveredDevice], hidden: DiscoveredDevice
) -> DiscoveredDevice | None:
"""The row the highlight should land on once ``hidden`` leaves the list.
The next device down, so a run of adapters can be cleared with a run of presses without
the hand moving; the one above where there is no next, because the highlight has reached
the end and there is nowhere further down to go. Neither ever rolls round to the top —
nothing in the app does — and an emptied list has nothing to land on at all.
"""
remaining = [d for d in listed if d.stable_id != hidden.stable_id]
if not remaining:
return None
at = next((i for i, d in enumerate(listed) if d.stable_id == hidden.stable_id), 0)
return remaining[min(at, len(remaining) - 1)]
def _shortcut_hint(hidden: int) -> Callable[[object], str]:
@@ -415,14 +456,12 @@ def _shortcut_hint(hidden: int) -> Callable[[object], str]:
def _hidden_note(hidden: int) -> Separator:
"""The muted row stating what the list is *not* showing, and the key that brings it back.
"""The muted row that stands in for a list hiding has emptied.
Hiding is the one action here that leaves no trace: a row that is gone looks exactly like
a device that was never plugged in, so without this the list would quietly misrepresent
the machine. It also carries ``⇧H`` in the body, which matters on a console whose splash
border is 47 cells wide — the footer atom naming that key is among the first to be shed
when the sentence outgrows the box (see :func:`~meshterm.ui.tui.frame.fit_hint`), and a
way back that can be squeezed off the screen is not a way back.
Only then. With devices still listed, the footer already names ``⇧H`` on every row and a
standing line saying the same thing is a row of the box spent on chrome. With *nothing*
listed there is no footer atom to read it off — and "no companion devices detected" would
be a lie about a machine with a radio plugged into it.
"""
it = "it" if hidden == 1 else "them"
device = "device" if hidden == 1 else "devices"
@@ -801,8 +840,6 @@ def _build_items(
# remembered endpoint, so forgetting it is the only way it leaves the picker. A scanned
# serial/BLE device would just reappear, so Delete stays inert on those rows.
items.append(Choice(title=row, value=device, deletable=device.is_tcp, hscroll_from=head))
if hidden:
items.append(_hidden_note(hidden))
items.extend(_action_rows())
return items
+11 -7
View File
@@ -895,14 +895,16 @@ class TuiSession:
default: Any = None,
banner: Any | None = None,
footnote: str | None = None,
footer_hint: str = "↑↓ move · Enter select · Esc quit",
footer_hint: str = "↑↓ move · Enter select · Esc bye",
keys: Mapping[str, Any] | None = None,
key_hint: Callable[[Any], str] | None = None,
) -> Any:
"""Show a chromeless select splash (banner above a content-sized box).
Like :meth:`select`, but drawn without the header/footer status bars and centered
under ``banner`` — the startup device picker's presentation. Type-to-filter is off:
under ``banner`` — the startup device picker's presentation. Esc's verb here is
``bye``, the app's one send-off: this splash is the door, and the reader leaving it
has not started anything to quit out of. Type-to-filter is off:
the device list is short and fixed, so stray keys never narrow it. An optional
``footnote`` (e.g. a copyright notice) sits muted below the box. When any row opts
into removal (a :attr:`~meshterm.ui.tui.select.Choice.deletable` row), a "Del remove"
@@ -932,10 +934,12 @@ class TuiSession:
screen.banner = banner
screen.footnote = footnote
# The splash's border is its only hint line and it is narrow (47 cells on the
# PicoCalc), so when the sentence outgrows the box the scroll atom is the one it
# can spare: ←→ are already named by the move atom in front of it, while a
# bare-letter shortcut is unguessable and has nowhere else to be advertised here.
screen.spare_hint_atoms = ("←→ scroll",)
# PicoCalc), so when the sentence outgrows the box it gives up the scroll atom
# first — ←→ are already named by the move atom in front of it — and then the
# *hide* key. Between the two shortcuts the one that survives is the way back:
# ⇧H only appears at all once something is hidden, which is exactly the state where
# a reader needs to be told how to undo it, and by then they have already found h.
screen.spare_hint_atoms = ("←→ scroll", "h hide")
result = await self.run_screen(screen)
return None if result is CANCEL else result
@@ -1005,7 +1009,7 @@ class TuiSession:
backdrop_title,
backdrop_items,
default=backdrop_default,
footer_hint="↑↓ move · Enter select · Esc quit",
footer_hint="↑↓ move · Enter select · Esc bye",
delete_hint="Del remove",
filterable=False,
)
+33 -5
View File
@@ -3085,7 +3085,8 @@ async def test_the_splash_hides_the_highlighted_device_and_redraws(tmp_path) ->
def test_the_splash_says_so_when_it_is_empty_only_because_of_hiding() -> None:
""" "Nothing detected" would be a lie, and the way back is a key nobody could guess."""
"""Nothing-detected would be a lie, and with no rows there is no footer atom either."""
from meshterm.core.discovery import serial_device
from meshterm.ui.device_picker import _build_items
lines = [str(it.title) for it in _build_items([], None, {}, hidden=2)]
@@ -3094,6 +3095,28 @@ def test_the_splash_says_so_when_it_is_empty_only_because_of_hiding() -> None:
detected = [str(it.title) for it in _build_items([], None, {}, hidden=0)]
assert any("no companion devices detected" in line for line in detected)
# With something still listed the footer names ⇧H on every row, so the line would be a
# row of the box spent saying it twice.
radio = serial_device("COM7", name="A Radio")
rows = [str(it.title) for it in _build_items([radio], None, {}, hidden=2)]
assert not any("hidden" in line for line in rows)
def test_hiding_a_device_leaves_the_highlight_where_the_row_was() -> None:
"""The next device down, or the one above at the end — so a run of adapters clears in a run."""
from meshterm.core.discovery import serial_device
from meshterm.ui.device_picker import _after_hiding
first, middle, last = (serial_device(f"COM{n}", name=f"D{n}") for n in (1, 2, 3))
listed = [first, middle, last]
assert _after_hiding(listed, first) is middle
assert _after_hiding(listed, middle) is last
# Off the end there is nowhere further down, so the highlight steps up rather than
# rolling round to the top — nothing in the app rolls over.
assert _after_hiding(listed, last) is middle
assert _after_hiding([first], first) is None # an emptied list has nothing to land on
def test_the_splash_names_a_shortcut_only_where_it_would_act() -> None:
"""Named on a device row only, and ⇧H only while hidden — the Del remove rule."""
@@ -3126,10 +3149,15 @@ def test_a_hint_too_long_for_its_box_drops_atoms_rather_than_its_tail() -> None:
assert fit_hint(full, 45) == "↑↓ move · ←→ scroll · Enter select · Esc quit"
assert fit_hint(full, 20).endswith("Esc quit")
# ...unless the caller names an atom it can spare first: the scroll keys are already
# named by the move atom, while a bare letter is unguessable.
spared = fit_hint(full, 56, shed_first=("←→ scroll",))
# ...unless the caller names atoms it can spare first, in the order it can spare them:
# the scroll keys are already named by the move atom, and between the two hide keys the
# one to keep is the way back.
spared = fit_hint(full, 56, shed_first=("←→ scroll", "h hide"))
assert spared == "↑↓ move · Enter select · h hide · ⇧H show all · Esc quit"
assert (
fit_hint(full, 50, shed_first=("←→ scroll", "h hide"))
== "↑↓ move · Enter select · ⇧H show all · Esc quit"
)
def test_the_splash_scrolls_the_hardware_column_of_the_row_it_is_on() -> None:
@@ -3184,7 +3212,7 @@ def test_the_splash_hint_stays_inside_the_box_it_is_drawn_in() -> None:
from meshterm.ui.device_picker import _shortcut_hint
from meshterm.ui.tui.select import _splice_hint
base = "↑↓ move · Enter select · Esc quit"
base = "↑↓ move · Enter select · Esc bye" # the splash's own send-off
radio = serial_device("COM7", name="A Radio")
common = _splice_hint(base, _shortcut_hint(0)(radio))
for platform in (REGULAR, PICOCALC):