Merge branch 'main' into cardputer-zero
This commit is contained in:
commit
032e686beb
6 files changed
+95
-4
No files matched your search
@@ -157,7 +157,9 @@ deliberately, one at a time, and say why in the code.
|
||||
on the main menu's own hint, where Esc is inert and the reader looks for the way out.
|
||||
On picocalc that atom rides the title bar's tail (`frame._way_out`), and the menu's lane
|
||||
puts the way out on F3: `Quit?` asks, its Shift half F8 `Quit!` leaves without asking
|
||||
(`quit_now`, the one door that skips the confirm, and only on the menu).
|
||||
(`quit_now`, the one door that skips the confirm, and only on the menu). The confirm
|
||||
keeps the chip that asked: its F3 reads `Quit!` and sends the same `quit`, which with
|
||||
the question up is the leaving — one key pressed twice, as ^Q is.
|
||||
`test_navigation` walks every string literal in the package to keep them out of the rest
|
||||
of the UI.
|
||||
- **Esc at the main menu does nothing** but peel a typed filter: the menu is the bottom of
|
||||
@@ -660,7 +662,8 @@ branch on the platform per frame, and never `from meshterm.platforms import PLAT
|
||||
rides the Shift half of the very pager heading for it** (F10 Top behind F5 Page ↑, F9
|
||||
Bottom behind F4 Page ↓), because Home and End *are* on this keyboard and only ever
|
||||
wanted a chip for consistency. That leaves **F1–F3 free on every screen** for its own
|
||||
verbs, and `EMPTY_LANE` for a screen with none at all (every dialog). **A chip names an
|
||||
verbs, and `EMPTY_LANE` for a screen with none at all (every dialog but the quit
|
||||
confirm, whose F3 is the chip that asked, pressed again). **A chip names an
|
||||
action, never a key** — `Page ↑`, not `PgUp`; `Latest` on a transcript; `Region` on the
|
||||
map, whose Home reframes and whose paging zooms. **A directional pair rises toward its
|
||||
outer key**: where two adjacent chips are opposite ends of one axis, the *up · in ·
|
||||
|
||||
+8
-1
@@ -48,7 +48,7 @@ from .tui import (
|
||||
TuiSession,
|
||||
)
|
||||
from .tui.emoji_width import install as install_emoji_widths
|
||||
from .tui.fkeys import FPair
|
||||
from .tui.fkeys import EMPTY_LANE, FPair
|
||||
from .tui.spinner import spinner_interval
|
||||
from .widgets import battery_cell
|
||||
|
||||
@@ -514,6 +514,12 @@ async def _confirm_quit(ctx: AppContext, session: TuiSession) -> bool:
|
||||
if await _can_unpair(ctx):
|
||||
buttons.append(("Unpair & quit", "unpair"))
|
||||
buttons.append(("Quit", "quit"))
|
||||
# On a handheld the question keeps the chip that asked it: F3, where the menu's
|
||||
# ``Quit?`` sat, reads ``Quit!`` here and sends the same ``quit`` — which, while this
|
||||
# confirm is up, leaves at once (see TuiSession.request_quit), as a second ^Q does.
|
||||
# So the way out from the menu is one key pressed twice (JP, 2026-10-02).
|
||||
lane = list(EMPTY_LANE)
|
||||
lane[2] = FPair("Quit!", "quit")
|
||||
choice = await session.button_dialog(
|
||||
"Are you sure you want to quit?",
|
||||
buttons,
|
||||
@@ -526,6 +532,7 @@ async def _confirm_quit(ctx: AppContext, session: TuiSession) -> bool:
|
||||
button_style="selected",
|
||||
button_idle_style="muted",
|
||||
border_style="warn",
|
||||
lane=lane,
|
||||
)
|
||||
if choice == "unpair":
|
||||
ctx.unpair_on_exit = True
|
||||
|
||||
@@ -8,7 +8,8 @@ prompt shares the framework's look, layering, resize, and Esc-to-cancel behavior
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Sequence
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from rich.cells import cell_len
|
||||
from rich.console import Group, RenderableType
|
||||
@@ -19,6 +20,9 @@ from .render import render_lines, right_aligned_tail
|
||||
from .screen import Screen
|
||||
from .spinner import Spinner
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .fkeys import FPair
|
||||
|
||||
#: A validator returns ``True`` when the input is acceptable, or an error message to show.
|
||||
Validator = Callable[[str], "bool | str"]
|
||||
|
||||
@@ -506,6 +510,7 @@ class ButtonDialog(_KeylessDialog):
|
||||
button_style: str = "selected",
|
||||
button_idle_style: str = "muted",
|
||||
border_style: str = "accent",
|
||||
lane: Sequence[FPair | None] | None = None,
|
||||
) -> None:
|
||||
"""Build a button dialog.
|
||||
|
||||
@@ -525,8 +530,12 @@ class ButtonDialog(_KeylessDialog):
|
||||
button_style: Rich style for the highlighted button.
|
||||
button_idle_style: Rich style for the un-highlighted buttons.
|
||||
border_style: Rich style for the dialog border (read by the frame compositor).
|
||||
lane: An F-key lane for the one question that has a chip worth offering — the
|
||||
quit confirm's, whose F3 is the chip that asked it, pressed again (see
|
||||
``menu._confirm_quit``). ``None`` keeps a prompt's empty lane.
|
||||
"""
|
||||
super().__init__()
|
||||
self._lane = tuple(lane) if lane is not None else None
|
||||
self.title = title
|
||||
self.footer_hint = footer_hint
|
||||
self.border_style = border_style
|
||||
@@ -550,6 +559,14 @@ class ButtonDialog(_KeylessDialog):
|
||||
return list(self._prompt.split("\n")) or [Text("")]
|
||||
return [Text(self._prompt, style=self._prompt_style)]
|
||||
|
||||
@property
|
||||
def picocalc_lane(self):
|
||||
"""The caller's lane where one was given, else a prompt's empty one.
|
||||
|
||||
The per-deck definition, as every screen's is; the Cardputer's deck follows it.
|
||||
"""
|
||||
return self._lane if self._lane is not None else super().picocalc_lane
|
||||
|
||||
@property
|
||||
def _button_row_width(self) -> int:
|
||||
"""Display width of the button row (`` Label `` cells joined by 4-space gaps)."""
|
||||
|
||||
@@ -1360,6 +1360,7 @@ class TuiSession:
|
||||
button_style: str = "selected",
|
||||
button_idle_style: str = "muted",
|
||||
border_style: str = "accent",
|
||||
lane: Sequence[fkeys.FPair | None] | None = None,
|
||||
) -> Any:
|
||||
"""Show a centered button dialog; return the chosen value or ``None`` if cancelled.
|
||||
|
||||
@@ -1380,6 +1381,7 @@ class TuiSession:
|
||||
button_style=button_style,
|
||||
button_idle_style=button_idle_style,
|
||||
border_style=border_style,
|
||||
lane=lane,
|
||||
)
|
||||
result = await self.run_dialog(screen)
|
||||
return None if result is CANCEL else result
|
||||
|
||||
@@ -123,3 +123,27 @@ def test_no_icon_lane_leaves_the_bare_word_flush_with_the_titles(tools: list) ->
|
||||
assert items[-1].value == menu._QUIT_VALUE
|
||||
assert items[-1].title.plain == "Quit"
|
||||
assert set(starts.values()) == {0}
|
||||
|
||||
|
||||
def test_the_quit_confirm_keeps_the_chip_that_asked(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""The confirm's F3 reads ``Quit!`` and sends ``quit`` — the menu's own F3, again."""
|
||||
import asyncio
|
||||
|
||||
from meshterm.ui.tui import fkeys
|
||||
|
||||
asked: dict = {}
|
||||
|
||||
class Session:
|
||||
async def button_dialog(self, prompt, buttons, **kwargs): # noqa: ANN001, ANN202
|
||||
asked.update(kwargs)
|
||||
return "cancel"
|
||||
|
||||
async def no_bond(ctx) -> bool: # noqa: ANN001
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(menu, "_can_unpair", no_bond)
|
||||
assert asyncio.run(menu._confirm_quit(SimpleNamespace(), Session())) is False
|
||||
assert asked["lane"][2].label == "Quit!"
|
||||
assert fkeys.PICOCALC_DECK.action_for(asked["lane"], 3) == "quit" # F3
|
||||
assert fkeys.CARDPUTER_DECK.action_for(asked["lane"], 6) == "quit" # Fn+6, its middle chip
|
||||
assert [slot for i, slot in enumerate(asked["lane"]) if i != 2] == [None] * 4
|
||||
@@ -1242,3 +1242,41 @@ async def test_the_busy_dialog_is_one_card_however_deeply_it_nests() -> None:
|
||||
await asyncio.wait_for(session.run(main()), timeout=5)
|
||||
|
||||
assert depths == [2, 2, 2, 1] # pushed once, popped once, hub still underneath
|
||||
|
||||
|
||||
async def test_the_chip_that_asked_to_quit_leaves_when_pressed_again() -> None:
|
||||
"""On a handheld, F3 asks from the menu and the confirm keeps F3: a second press leaves.
|
||||
|
||||
The confirm's lane carries ``Quit!`` on the slot the menu's ``Quit?`` sat on, sending
|
||||
the same ``quit`` — which, with the question up, is the leaving, as a second ^Q is.
|
||||
"""
|
||||
from meshterm.platforms import PICOCALC, set_platform
|
||||
from meshterm.ui.tui.fkeys import EMPTY_LANE, FPair
|
||||
|
||||
set_platform(PICOCALC)
|
||||
lane = list(EMPTY_LANE)
|
||||
lane[2] = FPair("Quit!", "quit")
|
||||
with create_pipe_input() as inp:
|
||||
session = _session(inp)
|
||||
|
||||
async def ask() -> bool:
|
||||
dialog = ButtonDialog("Quit?", [("Cancel", "cancel"), ("Quit", "quit")], lane=lane)
|
||||
return await session.run_dialog(dialog) == "quit"
|
||||
|
||||
session.set_quit_confirm(ask)
|
||||
torn_down: list[str] = []
|
||||
|
||||
async def main() -> None:
|
||||
session.push(ScrollScreen("the menu"))
|
||||
try:
|
||||
session._dispatch("quit") # the menu's Quit? chip
|
||||
for _ in range(5):
|
||||
await asyncio.sleep(0)
|
||||
assert isinstance(session.top, ButtonDialog), "the confirm is up"
|
||||
session._dispatch("f3") # the same chip, on the confirm's lane
|
||||
await asyncio.sleep(5) # the leaving cancels this
|
||||
finally:
|
||||
torn_down.append("services closed")
|
||||
|
||||
await asyncio.wait_for(session.run(main()), timeout=5)
|
||||
assert torn_down == ["services closed"]
|
||||
Reference in new issue
Block a user