diff --git a/CLAUDE.md b/CLAUDE.md index 9c6342d..8f7fd12 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/meshterm/ui/menu.py b/meshterm/ui/menu.py index 5afd6ff..4d7d80b 100644 --- a/meshterm/ui/menu.py +++ b/meshterm/ui/menu.py @@ -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", diff --git a/meshterm/ui/tx_screen.py b/meshterm/ui/tx_screen.py index 3ef8233..77d2547 100644 --- a/meshterm/ui/tx_screen.py +++ b/meshterm/ui/tx_screen.py @@ -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 diff --git a/tests/test_tui.py b/tests/test_tui.py index b533ff3..f7e5ff3 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -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" + )