The classic Windows console renders through GDI with the console font and no emoji font behind it, so every icon in the menu came up as a replacement box. It is what a double-clicked build gets, which is most of the Windows audience. The fix already existed: the compact single-character glyph table built for the PicoCalc, reached by Platform.emoji. The seam's flags are independent, so clearing that one alone keeps the desktop's width, colour and accented names and moves only the icons — without_emoji() is the whole change to the platform. Identifying the host has to be structural. It cannot be asked in band, because a missing glyph still fills its cell: a classic console stores an emoji intact and advances two cells while showing a box, and a ConPTY reads back as U+FFFD while drawing it perfectly, so a write-and-read-back probe answers both hosts backwards. Nor can the environment answer, since WT_SESSION is inherited across process launches. So two independent facts must agree — the console font has a real pixel width (a ConPTY's hidden conhost reports zero) and the buffer is taller than its window (a ConPTY has no scrollback of its own) — and an unknown never degrades anything. MESHTERM_EMOJI overrides either way, and `meshterm platform` reports the verdict and its reason. Verified on Windows 10 22H2 in all three: a real conhost turns the icons compact, Windows Terminal keeps its emoji, a redirected run is left alone. _has_wide_glyph asked the same flag to mean "nothing here is two cells wide", which was true of the PicoCalc's fixed font rather than of the flag. A console that merely lacks emoji still renders the rest of the BMP, and a contact named in CJK is two cells wide there like anywhere else, so it now asks ascii_fold — the fold is what actually guarantees it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
202 lines
7.7 KiB
Python
202 lines
7.7 KiB
Python
"""Tests for the platform seam.
|
|
|
|
Resolution order, the active-platform singleton, and the two platform specs' own
|
|
sanity — PicoCalc being strictly the narrower, plainer flavour.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
import meshterm.platforms as platforms_mod
|
|
from meshterm.platforms import (
|
|
PICOCALC,
|
|
REGULAR,
|
|
get_platform,
|
|
resolve,
|
|
set_platform,
|
|
without_emoji,
|
|
)
|
|
|
|
|
|
def test_regular_is_todays_behaviour() -> None:
|
|
"""REGULAR is the desktop terminal exactly as it behaves without this seam."""
|
|
assert REGULAR.name == "regular"
|
|
assert REGULAR.width_reclaim is True
|
|
assert REGULAR.emoji is True
|
|
assert REGULAR.truecolor is True
|
|
assert REGULAR.frame_border is True
|
|
assert REGULAR.readable_cols == 72
|
|
|
|
|
|
def test_picocalc_is_the_narrower_plainer_flavour() -> None:
|
|
"""PICOCALC never claims a capability the console can't back up."""
|
|
assert PICOCALC.name == "picocalc"
|
|
assert PICOCALC.readable_cols < REGULAR.readable_cols
|
|
assert PICOCALC.width_reclaim is False # a phantom column would tear the exact-width frame
|
|
assert PICOCALC.emoji is False
|
|
assert PICOCALC.truecolor is False
|
|
|
|
|
|
def test_get_platform_defaults_to_regular() -> None:
|
|
"""Every test starts on REGULAR (see the autouse fixture in conftest.py)."""
|
|
assert get_platform() is REGULAR
|
|
|
|
|
|
def test_set_platform_is_visible_through_get_platform() -> None:
|
|
"""set_platform takes effect immediately for any caller reading via get_platform()."""
|
|
set_platform(PICOCALC)
|
|
assert get_platform() is PICOCALC
|
|
set_platform(REGULAR)
|
|
assert get_platform() is REGULAR
|
|
|
|
|
|
def test_set_platform_reaches_a_dotted_import_too() -> None:
|
|
"""A consumer that imports the module (not the name) sees the change immediately.
|
|
|
|
This is the exact footgun the module docstring warns about: ``from meshterm.platforms
|
|
import PLATFORM`` at a module's top level would NOT see this, but module-qualified
|
|
access (``platforms_mod.PLATFORM``) — and every real binding point in this codebase,
|
|
which reads through :func:`get_platform` — does.
|
|
"""
|
|
set_platform(PICOCALC)
|
|
assert platforms_mod.PLATFORM is PICOCALC
|
|
|
|
|
|
def test_resolve_defaults_to_regular_off_device(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""No flag, no env, no device tree (every desktop/CI machine) → REGULAR, source 'default'."""
|
|
monkeypatch.delenv("MESHTERM_PLATFORM", raising=False)
|
|
monkeypatch.setattr(platforms_mod, "_DEVICE_TREE_MODEL_PATH", Path("/nonexistent/model"))
|
|
r = resolve()
|
|
assert r.platform is REGULAR
|
|
assert r.source == "default"
|
|
assert r.flag is None and r.env is None and r.detected_model is None
|
|
|
|
|
|
def test_resolve_flag_wins_over_everything(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""An explicit --platform overrides both the env var and auto-detection."""
|
|
monkeypatch.setenv("MESHTERM_PLATFORM", "picocalc")
|
|
monkeypatch.setattr(platforms_mod, "_DEVICE_TREE_MODEL_PATH", Path("/nonexistent/model"))
|
|
r = resolve("regular")
|
|
assert r.platform is REGULAR
|
|
assert r.source == "--platform flag"
|
|
assert r.env == "picocalc" # recorded even though it didn't decide the outcome
|
|
|
|
|
|
def test_resolve_env_wins_when_no_flag(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""MESHTERM_PLATFORM decides when --platform wasn't passed."""
|
|
monkeypatch.setenv("MESHTERM_PLATFORM", "picocalc")
|
|
monkeypatch.setattr(platforms_mod, "_DEVICE_TREE_MODEL_PATH", Path("/nonexistent/model"))
|
|
r = resolve(None)
|
|
assert r.platform is PICOCALC
|
|
assert r.source == "MESHTERM_PLATFORM env var"
|
|
|
|
|
|
def test_resolve_matching_is_case_insensitive_and_trims_space() -> None:
|
|
"""A flag/env value matches regardless of case or stray whitespace."""
|
|
assert resolve(" PicoCalc ").platform is PICOCALC
|
|
assert resolve("REGULAR").platform is REGULAR
|
|
|
|
|
|
def test_resolve_unknown_flag_raises_with_the_choices(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""A typo'd --platform is a clear ValueError, not a silent fallback to REGULAR."""
|
|
monkeypatch.delenv("MESHTERM_PLATFORM", raising=False)
|
|
with pytest.raises(ValueError) as exc:
|
|
resolve("laptop")
|
|
message = str(exc.value)
|
|
assert "laptop" in message and "--platform" in message
|
|
assert "picocalc" in message and "regular" in message
|
|
|
|
|
|
def test_resolve_unknown_env_raises_only_when_it_would_decide(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""A bad env var is only an error when nothing else (the flag) overrides it."""
|
|
monkeypatch.setenv("MESHTERM_PLATFORM", "laptop")
|
|
# A valid flag wins outright — the bad env var is recorded but never validated.
|
|
assert resolve("regular").platform is REGULAR
|
|
with pytest.raises(ValueError) as exc:
|
|
resolve(None)
|
|
assert "MESHTERM_PLATFORM" in str(exc.value)
|
|
|
|
|
|
def test_resolve_auto_detects_the_lyra_model(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""The exact on-device string (P0, 2026-08-01) — trailing space and NUL included."""
|
|
monkeypatch.delenv("MESHTERM_PLATFORM", raising=False)
|
|
|
|
class _FakePath:
|
|
def read_bytes(self) -> bytes:
|
|
return b"Luckfox Lyra \x00"
|
|
|
|
monkeypatch.setattr(platforms_mod, "_DEVICE_TREE_MODEL_PATH", _FakePath())
|
|
r = resolve()
|
|
assert r.platform is PICOCALC
|
|
assert r.source == "auto-detect"
|
|
assert r.detected_model == "Luckfox Lyra" # trailing space/NUL stripped
|
|
|
|
|
|
def test_resolve_auto_detect_is_conservative(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""A device tree that exists but names different hardware never guesses picocalc."""
|
|
monkeypatch.delenv("MESHTERM_PLATFORM", raising=False)
|
|
|
|
class _FakePath:
|
|
def read_bytes(self) -> bytes:
|
|
return b"Raspberry Pi 4 Model B\x00"
|
|
|
|
monkeypatch.setattr(platforms_mod, "_DEVICE_TREE_MODEL_PATH", _FakePath())
|
|
r = resolve()
|
|
assert r.platform is REGULAR
|
|
assert r.detected_model == "Raspberry Pi 4 Model B"
|
|
|
|
|
|
# -- the emoji-less variant ------------------------------------------------------------
|
|
|
|
|
|
def test_without_emoji_moves_one_flag_and_nothing_else() -> None:
|
|
"""A terminal that can't draw emoji is not a different flavour.
|
|
|
|
The classic Windows console has the desktop's width, colour depth, borders and
|
|
keyboard — only the icons have to change. Every other field must survive, and
|
|
``ascii_fold`` above all: folding there would strip accents from names the console
|
|
renders perfectly well.
|
|
"""
|
|
plain = without_emoji(REGULAR)
|
|
assert plain.emoji is False
|
|
assert plain.ascii_fold is False
|
|
assert plain.truecolor is True
|
|
assert plain.readable_cols == REGULAR.readable_cols
|
|
assert plain.footer_fkeys is REGULAR.footer_fkeys
|
|
|
|
changed = {
|
|
f.name
|
|
for f in dataclasses.fields(REGULAR)
|
|
if getattr(plain, f.name) != getattr(REGULAR, f.name)
|
|
}
|
|
assert changed == {"emoji"}
|
|
|
|
|
|
def test_without_emoji_leaves_an_already_compact_platform_alone() -> None:
|
|
"""A platform whose icons are already compact comes back untouched.
|
|
|
|
The same object, not a copy that would compare equal but break every ``is`` check
|
|
the suite makes against PICOCALC.
|
|
"""
|
|
assert without_emoji(PICOCALC) is PICOCALC
|
|
|
|
|
|
def test_the_emoji_less_variant_still_drives_the_theme() -> None:
|
|
"""The derived platform is a real one: binding it routes icons through the table.
|
|
|
|
This is the whole point — the seam's flags are independent, so clearing one reaches
|
|
the glyph funnel without dragging the console font's fold along with it.
|
|
"""
|
|
from meshterm.ui.theme import fold_text, glyph
|
|
|
|
set_platform(without_emoji(REGULAR))
|
|
assert glyph("📡") == "☼" # the advert icon, compacted
|
|
assert fold_text("café Montréal") == "café Montréal" # no fold
|
|
assert get_platform().readable_cols == 72
|