diff --git a/CHANGELOG.md b/CHANGELOG.md index f1e92e7..4a464fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,9 +22,11 @@ allowed to change behaviour, not just add to it. - **The PicoCalc picks its console font, and MeshTerm hands the console back.** A *Console font* row under Display on the Preferences page offers `6x12 (53x26)` — the font the device boots in — and `6x8 (53x40)`, fourteen more rows of the same glyph inventory in a - shorter cell. Choosing one loads it there and then; the kernel raises SIGWINCH on a font - change, so the frame repaints at the new height without a relaunch. The 6×8 base has no - Cyrillic, so a node named in Cyrillic draws as boxes on it. + shorter cell. Picking one is the preview: the console loads it there and then, and the + page repaints at the new height (the kernel raises SIGWINCH on a font change), so the + choice is looked at before it is kept. Apply keeps it; leaving and discarding puts the + saved font back. The 6×8 base has no Cyrillic, so a node named in Cyrillic draws as + boxes on it. A virtual terminal has one font for the whole console, so switching MeshTerm's switches the shell's. It saves whatever font was loaded before the first paint and reloads it on diff --git a/docs/hardware.md b/docs/hardware.md index 2bf5f0e..b15e330 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -211,9 +211,10 @@ and nothing more: the default is installed either way. **Choosing between them is a preference, not a command.** *Display → Console font* on the Preferences page offers `6x12 (53x26)` and `6x8 (53x40)`, and it is drawn on the PicoCalc only — it is a row about hardware no desktop has, though the value round-trips through -`preferences.yaml` on any machine. Picking one loads it immediately: the kernel raises -SIGWINCH on a font change, so the whole frame repaints at the new row count without a -relaunch. +`preferences.yaml` on any machine. Picking one is the preview: the console loads it +immediately and the page repaints at the new row count (the kernel raises SIGWINCH on a +font change), so you look at the choice before you keep it. *Apply* keeps it; leaving the +page and discarding puts the saved font back. MeshTerm puts the font back when it exits. Before the first paint it saves the console's current font with `setfont -O ~/.meshterm/console-font-before.psf` — whatever the *shell* diff --git a/meshterm/ui/preferences.py b/meshterm/ui/preferences.py index e12717b..0a8bc4f 100644 --- a/meshterm/ui/preferences.py +++ b/meshterm/ui/preferences.py @@ -44,6 +44,7 @@ from ..core.preferences import ( range_hint, ) from ..platforms import get_platform +from ..services import consolefont from .menus import ( confirm_discard, exit_rows, @@ -84,6 +85,22 @@ async def edit_preferences(ctx: AppContext) -> dict[str, Any] | None: prefs = ctx.preferences pending: dict[str, Any] = {} # preference key -> staged new value + # The console font previews. A VT loads one font for the whole screen, so the only + # honest way to show what the 6x8 build looks like is to load it — and this page is + # the preview: it repaints at the new row count the moment the value is picked. + # ``shown`` is what the console is drawn in right now; a discard puts the saved value + # back, and Apply's own re-apply after the write finds nothing to change. + previewing = consolefont.applies() + shown = prefs.get(consolefont.PREFERENCE_KEY) + + def preview() -> None: + nonlocal shown + if not previewing: + return + want = _effective(prefs, consolefont.PREFERENCE_KEY, pending) + if want != shown and consolefont.apply(want): + shown = want + # The rows *are* the data — each carries its own staged ``current → new`` value and # the title counts what is staged — so they refresh in place after every round # (``replace_items``) rather than being rebuilt as a new screen. One screen for the @@ -103,6 +120,8 @@ async def edit_preferences(ctx: AppContext) -> dict[str, Any] | None: if choice in (None, _CANCEL): if pending and not await confirm_discard(ctx, len(pending), verb="saving"): continue # keep editing — the same list, the same place in it + if previewing and shown != prefs.get(consolefont.PREFERENCE_KEY): + consolefont.apply(prefs.get(consolefont.PREFERENCE_KEY)) return None if choice == _APPLY: return dict(pending) or None @@ -110,6 +129,7 @@ async def edit_preferences(ctx: AppContext) -> dict[str, Any] | None: await _stage_reset(ctx, prefs, pending) else: # a preference key await _stage_preference(ctx, prefs, choice, pending) + preview() title, items = _menu_items(prefs, pending) menu.replace_items(items, title=title) diff --git a/tests/test_preferences.py b/tests/test_preferences.py index e9d3d4e..9a69730 100644 --- a/tests/test_preferences.py +++ b/tests/test_preferences.py @@ -491,6 +491,73 @@ async def test_leaving_with_unsaved_changes_is_gated(ctx: AppContext) -> None: assert await edit_preferences(ctx) == {"history_days": 30} +def _preview_console(monkeypatch: pytest.MonkeyPatch, *, on_device: bool) -> list[str]: + """Stand in for the console-font service: record every font the page loads.""" + from meshterm.ui import preferences as page + + loaded: list[str] = [] + monkeypatch.setattr(page.consolefont, "applies", lambda: on_device) + monkeypatch.setattr(page.consolefont, "apply", lambda choice: loaded.append(choice) or True) + return loaded + + +async def test_picking_a_console_font_previews_it_and_apply_keeps_it( + ctx: AppContext, monkeypatch: pytest.MonkeyPatch +) -> None: + """The console loads the font the moment it is picked; Apply hands it over to save.""" + loaded = _preview_console(monkeypatch, on_device=True) + _install(ctx, [("select", "console_font"), ("select", "6x8"), ("select", "__apply__")]) + assert await edit_preferences(ctx) == {"console_font": "6x8"} + assert loaded == ["6x8"] # previewed once, and nothing put back on the way out + + +async def test_discarding_a_previewed_console_font_puts_the_saved_one_back( + ctx: AppContext, monkeypatch: pytest.MonkeyPatch +) -> None: + """A preview that is not kept is undone: the saved font comes back on discard.""" + loaded = _preview_console(monkeypatch, on_device=True) + _install( + ctx, + [ + ("select", "console_font"), + ("select", "6x8"), + ("select", None), # Esc + ("dialog", "discard"), + ], + ) + assert await edit_preferences(ctx) is None + assert loaded == ["6x8", "6x12"] + + +async def test_setting_the_console_font_back_reloads_it_at_once( + ctx: AppContext, monkeypatch: pytest.MonkeyPatch +) -> None: + """Staging the value in force again is a clean row — and the console follows it.""" + loaded = _preview_console(monkeypatch, on_device=True) + _install( + ctx, + [ + ("select", "console_font"), + ("select", "6x8"), + ("select", "console_font"), + ("select", "6x12"), # back to what is saved: nothing staged, Esc leaves quietly + ("select", None), + ], + ) + assert await edit_preferences(ctx) is None + assert loaded == ["6x8", "6x12"] + + +async def test_a_desktop_page_never_touches_a_console_font( + ctx: AppContext, monkeypatch: pytest.MonkeyPatch +) -> None: + """Off the handheld the service says no once, and the page asks it nothing more.""" + loaded = _preview_console(monkeypatch, on_device=False) + _install(ctx, [("select", "console_font"), ("select", "6x8"), ("select", "__apply__")]) + assert await edit_preferences(ctx) == {"console_font": "6x8"} + assert loaded == [] + + async def test_discarding_at_the_gate_drops_the_changes(ctx: AppContext) -> None: """Confirming the discard leaves with nothing, staged values and all.""" _install(