Picking a console font is its own preview, and a discard puts the saved one back

The Preferences page loads the font the moment its value is picked, rather than
after Apply: a VT has one font for the whole screen, so the only honest way to
show what the 6x8 build looks like is to load it, and the page is the preview,
repainting at the new row count on the kernel's SIGWINCH. The page tracks what
the console is drawn in right now; staging the saved value again reloads it at
once, Apply keeps whatever is showing (the tool's re-apply after the write then
finds nothing to change), and leaving with a discard reloads the saved font.
Off the handheld the service says no once at page open and is asked nothing
more. Four tests drive the page through each path with the service stubbed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Fable 5.1 committed 2026-09-13 00:40:55 -04:00
1 parent 3b67c6813e
commit 646a0b7b7c
4 files changed
+96 -6

No files matched your search

+5 -3
View File
@@ -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
+4 -3
View File
@@ -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*
+20
View File
@@ -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)
+67
View File
@@ -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(