A button row's Enter belongs to whichever chip is lit

The quit confirm promised "Enter quit · Esc cancel", which held only while Quit
was the highlighted button — and on a bonded device there is an "Unpair & quit"
between Cancel and Quit, so Enter has three things it might do. The hint also
left out ←→, the one key that changes which. Both dialogs (the TX winner's
"Enter apply" is the same shape) drop back to the dialog default, which says the
mechanism instead of guessing the outcome: ←→ choose · Enter select · Esc cancel.

A row-picking list keeps its licence to name a committing verb, because every
row commits the same way. A lone-button acknowledgement keeps "Enter OK" too,
having nothing to choose between — that is the exception the new standards test
walks the package for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5 committed 2026-08-31 03:02:29 -04:00
1 parent 2a7d01108b
commit 7407358ede
4 files changed
+70 -3

No files matched your search

+5 -1
View File
@@ -175,7 +175,11 @@ as a grouped list does. Filling a page in is editing its `.md`; no Python follow
`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
of the generic three (`pick`, `choose`, `commit`) is not. The filter atom is always
`type to filter`.
`type to filter`. A **button row** is the one place the specific verb is barred: Enter
commits whichever chip is highlighted, so the hint stays `←→ choose · Enter select ·
Esc cancel` — `Enter quit` on the quit confirm was true only until ←→ moved, and hid
the key that moves it. A lone-button acknowledgement names its verb (`Enter OK`) and
offers no `←→`, having nothing to choose between.
- A hint is drawn **once per frame**, and which surface draws it is the footer's call.
Where the footer is the hint line (regular) it carries the *top* screen's hint, so a
floating dialog's border stays silent — the same sentence in the border and at the
+3 -1
View File
@@ -591,7 +591,9 @@ async def _menu_round(
buttons,
title="Quit MeshTerm",
default=len(buttons) - 1, # highlight Quit
footer_hint="Enter quit · Esc cancel",
# The hint stays the dialog default: Enter commits *the highlighted
# button*, which is Quit only until ←→ moves it — and with a third
# button in the row, naming the key that moves it is the point.
prompt_style="warn",
button_style="selected",
button_idle_style="muted",
+2 -1
View File
@@ -854,7 +854,8 @@ async def open_tx_optimize(
[("Cancel", "cancel"), ("Apply", "apply")],
title="Apply winner",
default=1,
footer_hint="Enter apply · Esc cancel",
# The dialog default hint: Enter commits whichever button is highlighted,
# and ←→ is what moves it (see the quit confirm in ui/menu.py).
)
if choice != "apply":
return
+60
View File
@@ -2818,3 +2818,63 @@ def test_an_off_screen_rows_live_title_is_left_alone() -> None:
lines = screen.render_body(53)
lines[0:10]
assert calls == ["top"]
def _button_dialog_hints() -> list[tuple[str, int, int | None, str]]:
"""Every literal ``footer_hint=`` on a button dialog: (file, line, buttons, hint).
``buttons`` is the count when the call passes a list literal, else ``None`` — a
computed row (the quit confirm grows a third button on a bonded device) can hold any
number, so it is held to the many-button rule.
"""
import ast
from pathlib import Path
found: list[tuple[str, int, int | None, str]] = []
root = Path(__file__).resolve().parent.parent / "meshterm"
for path in sorted(root.rglob("*.py")):
tree = ast.parse(path.read_text(encoding="utf-8"))
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
func = node.func
name = func.attr if isinstance(func, ast.Attribute) else getattr(func, "id", "")
if name not in ("button_dialog", "ButtonDialog"):
continue
hint = next(
(k.value.value for k in node.keywords
if k.arg == "footer_hint" and isinstance(k.value, ast.Constant)),
None,
)
if hint is None:
continue # the default hint, which is the generic one
row = node.args[1] if len(node.args) > 1 else None
count = len(row.elts) if isinstance(row, ast.List) else None
found.append((path.name, node.lineno, count, hint))
return found
def test_a_button_dialogs_hint_never_names_the_verb_of_one_button() -> None:
"""Enter commits *the highlighted* button, so a row of them may only say ``select``.
The quit confirm used to promise ``Enter quit · Esc cancel``, which was true only
while Quit happened to be highlighted — ←→ moves it, and on a bonded device a third
button sits between the two (JP, 2026-08-31). The same hint also left out the one key
that changes what Enter will do. A row-picking list may name its committing verb
(``Enter adopt path``) because every row commits the same way; a button row cannot.
A dialog with a single button is the exception the rule is shaped around: a lone
acknowledgement has nothing to choose between, so it names its verb (``Enter OK``)
and does not advertise an ←→ that would move nothing.
"""
for where, line, buttons, hint in _button_dialog_hints():
if buttons == 1:
assert "←→" not in hint, f"{where}:{line} offers ←→ over a single button"
continue
assert "Enter select" in hint, (
f"{where}:{line} promises {hint!r} — Enter commits whichever button is "
"highlighted, so the atom stays 'Enter select'"
)
assert "←→" in hint, (
f"{where}:{line} hides ←→, the key that moves the highlight Enter commits"
)