Files
MeshTerm/tests/test_termfont.py
T
Jean-Pierre MartineauandClaude Opus 5.5 69d9836546 The PicoCalc draws its path lines as chips
The console font gains the two powerline chevrons, drawn into the next
two donor slots (U+2559 and U+255B, which nothing draws), in both the
6x12 and the 6x8 builds. With them in its inventory, the PicoCalc's
verdict is core, so its routes are ribbons of chips like the
Cardputer's.

A chip there can only be filled from the eight background slots, since
under a 512-glyph font the VT has no bright backgrounds. So a node's
chip is the dim twin of its hue (theme.DIM_TWIN), and its label is the
hue itself: the colour the name has in every list and in arrow mode. A
hash label and an annotation are that colour throughout. A keyless chip
is white on light grey, the same pair one bank down. Our star, a faded
hop and the composer's slot have no dark grey to stand on, so they
stand on the page, the slot as a white + in a break of the ribbon. Two
chips of one colour join on a thin chevron in that colour.

Bold is brightness on the console, and Rich merges a selected row's
bold into every span on it, which turned the dim fills bright. Every
chip style there says "not bold"; the tests read the rendered row back
through the emulator's VT to hold that.

The new font takes effect once the font script runs again on the Lyra
(as root). Until then the PicoCalc's installed font lacks the chevrons.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 03:14:12 -04:00

380 lines
16 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Terminal-font detection tests: the recommended list, the ladder, the verdict.
Every verdict runs against injected environments, temp settings files and injected
probes, so the suite decides the same answers on any box. Two tests deliberately do
touch the real machine — the installed-font scan and the console buffer probe — and both
assert only that asking is safe, never what this particular box replies.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from meshterm.platforms import CARDPUTER_ZERO, PICOCALC_LYRA, REGULAR, set_platform
from meshterm.ui.fontset import CARDPUTER_ZERO_CODEPOINTS, FONT_CODEPOINTS
from meshterm.ui.termfont import (
CORE,
FULL,
NERD_FONT_GENERIC,
NONE,
UNKNOWN,
RecommendedFont,
_emoji_support,
_is_classic_console,
_powerline_support,
_read_jsonc,
_vscode_default_face,
_vscode_face,
_windows_terminal_face,
detect_terminal_font,
installed_recommended,
match_recommended,
normalize_face,
powerline_support,
primary_family,
)
# --- name matching ----------------------------------------------------------------
def test_primary_family_takes_the_first_of_a_css_list() -> None:
"""VS Code stores comma lists with optional quotes; the head family is judged."""
assert primary_family("'Hack Nerd Font Mono', monospace") == "Hack Nerd Font Mono"
assert primary_family('"Fira Code", Consolas') == "Fira Code"
assert primary_family("Cascadia Mono") == "Cascadia Mono"
def test_normalize_face_folds_case_quotes_and_spacing() -> None:
"""Matching is done on a lower-cased, single-spaced, unquoted form."""
assert normalize_face(" 'Hack Nerd Font' ") == "hack nerd font"
assert normalize_face('"MesloLGS NF"') == "meslolgs nf"
def test_match_recommended_knows_the_nerd_font_spellings() -> None:
"""The long name and the v3 short suffix both land on the same full entry."""
assert match_recommended("Hack Nerd Font Mono").name == "Hack Nerd Font"
assert match_recommended("Hack NFM").name == "Hack Nerd Font"
assert match_recommended("MesloLGS NF").coverage == FULL # powerlevel10k's install
def test_match_recommended_orders_full_patches_before_core_families() -> None:
"""A full patched family is matched ahead of the core family it is built on.
``JetBrainsMono Nerd Font`` must hit its full entry, never the plain ``JetBrains
Mono`` core prefix — first match wins, so the full entries lead.
"""
assert match_recommended("JetBrainsMono Nerd Font Mono").coverage == FULL
assert match_recommended("JetBrains Mono").coverage == CORE
def test_match_recommended_core_natives_and_strangers() -> None:
"""Stock coder fonts with native triangles read core; anything else, nothing."""
assert match_recommended("Fira Code").coverage == CORE
assert match_recommended("Source Code Variable").coverage == CORE
assert match_recommended("Consolas") is None
assert match_recommended(None) is None
def test_match_recommended_generic_nerd_rule_catches_unlisted_patches() -> None:
"""Any family carrying the Nerd Font branding has the full block patched in."""
assert match_recommended("Comic Shanns Mono Nerd Font") is NERD_FONT_GENERIC
assert match_recommended("Terminess NF") is NERD_FONT_GENERIC
# --- settings-file reading --------------------------------------------------------
def test_read_jsonc_survives_comments_and_trailing_commas(tmp_path: Path) -> None:
"""Both WT and VS Code write JSON-with-comments; the reader shrugs it off."""
path = tmp_path / "settings.json"
path.write_text(
"{\n"
" // the font\n"
' "editor.fontFamily": "Hack NFM", /* inline */\n'
' "url": "https://example.org//not-a-comment",\n'
' "list": [1, 2,],\n'
"}\n",
encoding="utf-8",
)
data = _read_jsonc(path)
assert data == {
"editor.fontFamily": "Hack NFM",
"url": "https://example.org//not-a-comment",
"list": [1, 2],
}
broken = tmp_path / "broken.json"
broken.write_text("{nope", encoding="utf-8")
assert _read_jsonc(broken) is None
def _wt_env(tmp_path: Path, settings: dict, guid: str = "{abc-123}") -> dict:
"""A fake Windows Terminal environment around a written settings file."""
folder = tmp_path / "Microsoft" / "Windows Terminal"
folder.mkdir(parents=True, exist_ok=True)
(folder / "settings.json").write_text(json.dumps(settings), encoding="utf-8")
return {"LOCALAPPDATA": str(tmp_path), "WT_SESSION": "s", "WT_PROFILE_ID": guid}
def test_windows_terminal_face_resolves_profile_then_defaults(tmp_path: Path) -> None:
"""The session profile's ``font.face`` wins; ``profiles.defaults`` backs it up."""
env = _wt_env(
tmp_path,
{
"profiles": {
"defaults": {"font": {"face": "Cascadia Code PL"}},
"list": [{"guid": "{ABC-123}", "font": {"face": "Hack Nerd Font Mono"}}],
},
},
)
assert _windows_terminal_face(env) == "Hack Nerd Font Mono" # guid case-folded
env2 = _wt_env(
tmp_path,
{
"profiles": {
"defaults": {"font": {"face": "Cascadia Code PL"}},
"list": [{"guid": "{other}"}],
},
},
)
assert _windows_terminal_face(env2) == "Cascadia Code PL"
def test_windows_terminal_face_reads_legacy_and_defaults_to_cascadia(tmp_path: Path) -> None:
"""The old flat ``fontFace`` key still counts; nothing set means the built-in."""
env = _wt_env(
tmp_path,
{
"profiles": {"list": [{"guid": "{abc-123}", "fontFace": "MesloLGS NF"}]},
},
)
assert _windows_terminal_face(env) == "MesloLGS NF"
env2 = _wt_env(tmp_path, {"profiles": {"list": [{"guid": "{abc-123}"}]}})
assert _windows_terminal_face(env2) == "Cascadia Mono"
assert _windows_terminal_face({"LOCALAPPDATA": str(tmp_path / "nowhere")}) == "Cascadia Mono"
def test_vscode_face_precedence_terminal_over_editor_workspace_over_user(
tmp_path: Path,
) -> None:
"""VS Code's font settings are read in precedence order.
``terminal.integrated.fontFamily`` beats ``editor.fontFamily``, and within a key
the workspace file beats the user file. The value's head family is the one judged.
"""
workspace = tmp_path / "repo"
(workspace / ".vscode").mkdir(parents=True)
(workspace / ".vscode" / "settings.json").write_text(
'{"editor.fontFamily": "Consolas"}', encoding="utf-8"
)
appdata = tmp_path / "appdata"
user = appdata / "Code" / "User"
user.mkdir(parents=True)
(user / "settings.json").write_text(
'{"terminal.integrated.fontFamily": "\'Hack Nerd Font Mono\', monospace"}',
encoding="utf-8",
)
env = {"APPDATA": str(appdata)}
assert _vscode_face(env, workspace) == "Hack Nerd Font Mono" # terminal.* wins
(user / "settings.json").write_text("{}", encoding="utf-8")
assert _vscode_face(env, workspace) == "Consolas" # workspace editor fallback
(workspace / ".vscode" / "settings.json").write_text("{}", encoding="utf-8")
assert _vscode_face(env, workspace) == _vscode_default_face()
# --- the ladder -------------------------------------------------------------------
def test_detect_terminal_font_ladder(tmp_path: Path) -> None:
"""The terminal is identified by working down a ladder of markers.
Windows Terminal first, then VS Code, then a genuine conhost — and ``TERM`` being
set disqualifies the conhost probe, since some other emulator is hosting it.
"""
wt = detect_terminal_font(_wt_env(tmp_path, {"profiles": {"list": []}}))
assert wt is not None and wt.source == "windows-terminal"
code = detect_terminal_font({"TERM_PROGRAM": "vscode", "APPDATA": str(tmp_path)}, cwd=tmp_path)
assert code is not None and code.source == "vscode"
con = detect_terminal_font({}, conhost_probe=lambda: "Consolas")
assert con is not None and (con.face, con.source) == ("Consolas", "conhost")
assert detect_terminal_font({"TERM": "xterm"}, conhost_probe=lambda: "Consolas") is None
def test_powerline_support_env_override_always_wins() -> None:
"""``MESHTERM_POWERLINE`` is the user's word: off, on/full, or core."""
assert _powerline_support({"MESHTERM_POWERLINE": "0"}).level == NONE
assert _powerline_support({"MESHTERM_POWERLINE": "off"}).level == NONE
on = _powerline_support({"MESHTERM_POWERLINE": "1", "WT_SESSION": "s"})
assert (on.level, on.source) == (FULL, "env")
assert _powerline_support({"MESHTERM_POWERLINE": "core"}).level == CORE
def test_powerline_support_matched_font_sets_the_level(tmp_path: Path) -> None:
"""A recommended face read from the terminal's own config decides coverage."""
env = _wt_env(
tmp_path,
{
"profiles": {"list": [{"guid": "{abc-123}", "font": {"face": "Hack Nerd Font Mono"}}]},
},
)
verdict = _powerline_support(env)
assert verdict.level == FULL
assert verdict.source == "font:windows-terminal"
assert verdict.matched is not None and verdict.matched.name == "Hack Nerd Font"
def test_powerline_support_renderer_fallback_and_honest_none(tmp_path: Path) -> None:
"""An unmatched face still earns core where the renderer can supply the glyphs.
On Windows Terminal that is the bundled-symbol fallback; the same face on a bare
conhost is an honest ``none``.
"""
env = _wt_env(tmp_path, {"profiles": {"list": [{"guid": "{abc-123}"}]}})
wt = _powerline_support(env) # face resolves to Cascadia Mono → no match
assert (wt.level, wt.source) == (CORE, "renderer:windows-terminal")
con = _powerline_support({}, conhost_probe=lambda: "Consolas")
assert (con.level, con.source, con.face) == (NONE, "font:conhost", "Consolas")
def test_powerline_support_kitty_ssh_and_unknown() -> None:
"""A glyph-drawing terminal earns core by its marker; anything else stays unknown.
ssh sessions and terminals we do not recognise are unknown rather than none: the
font lives on glass we cannot see.
"""
kitty = _powerline_support({"TERM": "xterm-kitty"})
assert (kitty.level, kitty.source) == (CORE, "renderer:kitty")
ssh = _powerline_support({"TERM": "xterm", "SSH_CONNECTION": "1.2.3.4"})
assert (ssh.level, ssh.source) == (UNKNOWN, "ssh")
lost = _powerline_support({"TERM": "xterm-256color"})
assert (lost.level, lost.source) == (UNKNOWN, "unknown")
def test_powerline_support_a_handheld_answers_from_its_own_font() -> None:
"""A handheld's font draws the screen, so its inventory answers and no terminal does.
The Cardputer's Terminus and the PicoCalc's console font both have the core chevrons
and no rounded caps. A font without the chevrons gets ``none``, also when a kitty
started the app. The override still outranks the font, as it outranks each probe.
"""
kitty = {"TERM": "xterm-kitty"} # a terminal that would otherwise earn core
cardputer = _powerline_support(kitty, handheld=("cardputer-zero", CARDPUTER_ZERO_CODEPOINTS))
assert (cardputer.level, cardputer.source) == (CORE, "platform:cardputer-zero")
picocalc = _powerline_support(kitty, handheld=("picocalc-lyra", FONT_CODEPOINTS))
assert (picocalc.level, picocalc.source) == (CORE, "platform:picocalc-lyra")
bare = _powerline_support(kitty, handheld=("bare", frozenset(range(0x20, 0x7F))))
assert (bare.level, bare.source) == (NONE, "platform:bare")
capped = CARDPUTER_ZERO_CODEPOINTS | {0xE0B4, 0xE0B6}
assert _powerline_support({}, handheld=("capped", capped)).level == FULL
pinned = {"MESHTERM_POWERLINE": "0"}
assert _powerline_support(pinned, handheld=("cardputer-zero", CARDPUTER_ZERO_CODEPOINTS)) == (
_powerline_support(pinned)
)
def test_powerline_support_follows_a_platform_switch(monkeypatch: pytest.MonkeyPatch) -> None:
"""The memoized verdict is dropped on a switch, so it never answers for the last glass."""
monkeypatch.delenv("MESHTERM_POWERLINE", raising=False)
monkeypatch.setenv("TERM", "xterm-kitty")
monkeypatch.delenv("WT_SESSION", raising=False)
monkeypatch.delenv("TERM_PROGRAM", raising=False)
set_platform(CARDPUTER_ZERO)
assert powerline_support().source == "platform:cardputer-zero"
set_platform(PICOCALC_LYRA)
assert powerline_support().source == "platform:picocalc-lyra"
set_platform(REGULAR)
assert powerline_support().source == "renderer:kitty"
def test_installed_recommended_never_raises() -> None:
"""Scanning the machine for installed fonts never raises.
It is best-effort context for a future nudge screen, so whatever the platform
answers, the result is a RecommendedFont or None.
"""
result = installed_recommended()
assert result is None or isinstance(result, RecommendedFont)
# --- emoji support ----------------------------------------------------------------
def test_emoji_is_only_the_windows_consoles_problem() -> None:
"""Everywhere but Windows, a terminal draws emoji and the question does not arise.
The probe is never even run there — a Linux or macOS session pays nothing for a limit
only the classic console has.
"""
def never_called() -> bool | None:
raise AssertionError("the console probe must not run off Windows")
for system in ("linux", "darwin", "freebsd"):
verdict = _emoji_support({}, console_probe=never_called, system=system)
assert (verdict.supported, verdict.source) == (True, "platform")
def test_a_classic_console_turns_the_icons_compact() -> None:
"""The verdict that matters: conhost draws no emoji, so the icons go compact.
This is the double-clicked build on Windows — the case the whole check exists for.
"""
verdict = _emoji_support({}, console_probe=lambda: True, system="win32")
assert (verdict.supported, verdict.source) == (False, "console")
def test_every_other_windows_terminal_keeps_its_emoji() -> None:
"""Windows Terminal, VS Code's terminal and ssh all draw them; nothing changes."""
verdict = _emoji_support({}, console_probe=lambda: False, system="win32")
assert (verdict.supported, verdict.source) == (True, "console")
def test_an_unanswerable_probe_never_degrades_the_ui() -> None:
"""No console to identify (redirected output, a failed call) leaves emoji on.
An unknown is not evidence of a limit. Degrading on one would strip the icons from
every piped or redirected run, which is most of the test suite and all of CI.
"""
verdict = _emoji_support({}, console_probe=lambda: None, system="win32")
assert (verdict.supported, verdict.source) == (True, "no-console")
def test_the_override_beats_the_probe_both_ways() -> None:
"""``MESHTERM_EMOJI`` wins, as the powerline gate's override does.
Someone on a console this cannot recognise knows their own glass better than a probe
does — and the negative direction is how anyone asks for the compact icons on purpose.
"""
forced_on = _emoji_support({"MESHTERM_EMOJI": "1"}, console_probe=lambda: True, system="win32")
assert (forced_on.supported, forced_on.source) == (True, "env")
def never_called() -> bool | None:
raise AssertionError("the override decides before the probe runs")
forced_off = _emoji_support({"MESHTERM_EMOJI": "0"}, console_probe=never_called, system="linux")
assert (forced_off.supported, forced_off.source) == (False, "env")
def test_an_inherited_wt_session_cannot_speak_for_the_host() -> None:
"""The environment is not consulted, and this is why.
``WT_SESSION`` is inherited by child processes, so a program launched from Windows
Terminal into a console of its own still carries it — measured, not supposed. Reading
it would have answered for the terminal that did the launching.
"""
env = {"WT_SESSION": "670f6626-8bdf-4c09-9164-b1eef91abe4f"}
verdict = _emoji_support(env, console_probe=lambda: True, system="win32")
assert verdict.supported is False
def test_identifying_the_host_never_raises() -> None:
"""Whatever this machine is, asking is safe and answers one of three things.
It runs on a startup path, so "it degrades rather than raises" is the property that
keeps an unusual host from taking the app down with it.
"""
assert _is_classic_console() in (True, False, None)