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:
1 parent
4109c73522
commit
398a184eed
4 files changed
+111
-35
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user