Term font: detect powerline support from a recommended-font list

MESHTERM_POWERLINE override, then the configured face (Windows Terminal
profile, VS Code settings, genuine conhost via Win32) matched against the
recommended fonts and their Nerd Font alias spellings, then renderer-backed
terminals that draw the triangles themselves; ssh and strangers stay an
honest unknown. Plus an installed-font scan for a future nudge screen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Fable 5 committed 2026-07-26 18:27:20 -04:00
1 parent 8fdd7572bd
commit 8992e413de
2 files changed
+866

No files matched your search

+637
View File
@@ -0,0 +1,637 @@
"""Terminal font detection — can this terminal draw powerline path separators?
The path-line widget (:mod:`~meshterm.ui.pathline`) can render a hop sequence as
interlocking powerline segments — each hop a colour-filled chip, joined by the solid
triangle U+E0B0 whose foreground is the previous chip's fill and whose background is
the next's, the oh-my-posh look. That triangle lives in the Private Use Area, so it
only draws when the *terminal's configured font* (or the terminal itself) supplies the
glyph; a font without it shows tofu boxes. Nothing in-band can ask "do you have this
glyph?" — a missing glyph still occupies its cell, so even cursor-position probes see
nothing — which makes this an *out-of-band* detection problem: identify the terminal,
read its configuration, and match the configured face against fonts known to carry the
powerline block.
Three sources of truth, in confidence order:
* **An explicit override** — ``MESHTERM_POWERLINE`` (``1``/``full``, ``core``,
``0``/``off``) always wins, the same gate pattern as ``MESHTERM_FULL_WIDTH``. The
user knows their glass better than any probe.
* **The configured font**, resolved per terminal: Windows Terminal (``WT_SESSION`` +
``WT_PROFILE_ID`` → the profile's ``font.face`` in ``settings.json``), VS Code's
integrated terminal (``TERM_PROGRAM=vscode`` → ``terminal.integrated.fontFamily``,
workspace over user, falling back to ``editor.fontFamily``), and classic conhost
(``GetCurrentConsoleFontEx`` — trusted only when no ConPTY host is detected, because
the hidden conhost under one reports a stub face, not what's on screen). The face is
matched against :data:`RECOMMENDED_FONTS` — the list MeshTerm recommends to users —
with the alias spellings Nerd Fonts ship under (``Hack Nerd Font Mono`` / ``Hack
NFM``), plus a generic "any Nerd Font" rule, since every Nerd Font patch carries the
full powerline block.
* **The terminal's own renderer**: several terminals draw the core triangles
regardless of font — Windows Terminal falls back to its bundled Cascadia Code NF for
the symbol ranges (≥ 1.22), VS Code's terminal draws them as custom glyphs
(``terminal.integrated.customGlyphs``, default on), and kitty / WezTerm / Alacritty
ship built-in powerline glyphs. Those count as ``core`` support even when the
configured font matches nothing.
Coverage comes in two levels: ``full`` (a Nerd Font patch — triangles *and* the
extended block: rounded caps, slants) and ``core`` (just U+E0B0–U+E0B3, which many
stock coder fonts — Fira Code, JetBrains Mono, Source Code Pro — include natively).
The path widget only uses the core triangle, so ``core`` is enough to switch it on;
``full`` is headroom for fancier chrome. When nothing can be known — an ssh session
(the font lives on the far client's machine), an unrecognised terminal — the verdict
is honestly ``unknown`` and callers should keep the plain-arrow rendering.
Every probe is best-effort: unreadable settings, missing registry keys, or a failed
Win32 call degrade the verdict, never raise. The result is a *hint that picks a
default*, not a gate the user has to fight.
"""
from __future__ import annotations
import json
import os
import re
import sys
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from typing import Callable, Iterable, Mapping, Optional
#: Coverage levels a verdict (or a recommended font) can carry, strongest first.
FULL = "full"
CORE = "core"
NONE = "none"
UNKNOWN = "unknown"
@dataclass(frozen=True)
class RecommendedFont:
"""One font MeshTerm recommends for powerline path rendering.
Attributes:
name: The canonical display name (what a recommendation screen shows).
aliases: Normalised name prefixes that identify the family — every spelling a
terminal config might hold, lower-case with collapsed spaces (Nerd Fonts
deliberately ship several: ``hack nerd font mono`` and ``hack nfm`` are the
same file).
coverage: :data:`FULL` when the family carries the whole powerline block
(Nerd Font patches), :data:`CORE` when it ships just the four triangles.
"""
name: str
aliases: tuple[str, ...]
coverage: str
#: The fonts MeshTerm recommends, full-coverage Nerd Fonts first — order matters, the
#: first alias hit wins, so ``JetBrainsMono Nerd Font`` must match its ``full`` entry
#: before the plain ``JetBrains Mono`` prefix could claim it as ``core``.
RECOMMENDED_FONTS: tuple[RecommendedFont, ...] = (
RecommendedFont(
"MesloLGM Nerd Font",
("meslolgm nerd font", "meslolgs nerd font", "meslolgl nerd font",
"meslolgm nf", "meslolgs nf", "meslolgl nf",
"meslo lgm nerd font", "meslo lgs nerd font"),
FULL,
),
RecommendedFont(
"Hack Nerd Font",
("hack nerd font", "hack nf", "hack nfm", "hack nfp"),
FULL,
),
RecommendedFont(
"CaskaydiaCove Nerd Font",
("caskaydiacove nerd font", "caskaydiacove nf", "caskaydiacove nfm",
"caskaydiamono nerd font", "caskaydiamono nf", "caskaydia cove nerd font"),
FULL,
),
RecommendedFont(
"Cascadia Code NF",
("cascadia code nf", "cascadia mono nf"),
FULL,
),
RecommendedFont(
"FiraCode Nerd Font",
("firacode nerd font", "firacode nf", "firacode nfm", "fira code nerd font"),
FULL,
),
RecommendedFont(
"JetBrainsMono Nerd Font",
("jetbrainsmono nerd font", "jetbrainsmono nf", "jetbrainsmono nfm",
"jetbrains mono nerd font"),
FULL,
),
RecommendedFont("Cascadia Code PL", ("cascadia code pl", "cascadia mono pl"), CORE),
RecommendedFont("Fira Code", ("fira code",), CORE),
RecommendedFont("JetBrains Mono", ("jetbrains mono",), CORE),
RecommendedFont("Source Code Pro", ("source code pro", "source code variable"), CORE),
RecommendedFont("Iosevka", ("iosevka",), CORE),
)
#: The catch-all for a patched font the explicit list doesn't spell out: any family
#: whose name carries the Nerd Font branding — the long ``… Nerd Font [Mono|Propo]``
#: or the v3 short suffixes ``NF``/``NFM``/``NFP`` — has the full powerline block.
NERD_FONT_GENERIC = RecommendedFont("Nerd Font (patched)", (), FULL)
_NERD_RE = re.compile(r"\bnerd font\b|\bnf[mp]?\b")
@dataclass(frozen=True)
class FontDetection:
"""The configured terminal font, and which terminal it was read from.
Attributes:
face: The font family name as configured (unnormalised).
source: ``"windows-terminal"``, ``"vscode"``, or ``"conhost"``.
"""
face: str
source: str
@dataclass(frozen=True)
class PowerlineSupport:
"""The verdict: how confidently this terminal can draw powerline separators.
Attributes:
level: :data:`FULL`, :data:`CORE`, :data:`NONE`, or :data:`UNKNOWN`.
source: What decided it — ``"env"`` (the override), ``"font:<terminal>"``
(a configured face matched, or definitively didn't), ``"renderer:<terminal>"``
(the terminal draws the glyphs itself), ``"ssh"`` (the font lives on the far
client, unknowable), or ``"unknown"``.
face: The configured face when one was found, for the curious.
matched: The recommended-list entry the face matched, if any.
"""
level: str
source: str
face: Optional[str] = None
matched: Optional[RecommendedFont] = None
@property
def capable(self) -> bool:
"""Whether the core triangle (all the path widget needs) will draw."""
return self.level in (FULL, CORE)
# --- name matching ---------------------------------------------------------------
def primary_family(value: str) -> str:
"""The first family of a CSS-style font list, unquoted.
VS Code stores ``"'Hack Nerd Font Mono', monospace"``; the first family is the
one the renderer tries first, so it is the one we judge.
Args:
value: A font-family setting value (single name or comma list).
Returns:
The first family with quotes and outer whitespace stripped.
"""
first = value.split(",")[0]
return first.strip().strip("'\"").strip()
def normalize_face(face: str) -> str:
"""Fold a face name to its matchable form: lower-case, single-spaced, unquoted.
Args:
face: A font family name as a config file spells it.
Returns:
The normalised name (may be empty).
"""
return " ".join(face.strip().strip("'\"").lower().split())
def match_recommended(face: Optional[str]) -> Optional[RecommendedFont]:
"""Match a configured face against the recommended-font list.
A face matches an entry when it *is* one of the entry's aliases or extends one as
a longer family name (``hack nerd font mono`` extends ``hack nerd font``); the
generic Nerd Font rule (:data:`NERD_FONT_GENERIC`) catches patched families the
explicit list doesn't spell out.
Args:
face: The configured font family (single name or comma list); ``None`` is a
polite no-op.
Returns:
The matched :class:`RecommendedFont`, or ``None`` when the face is unknown.
"""
if not face:
return None
norm = normalize_face(primary_family(face))
if not norm:
return None
for spec in RECOMMENDED_FONTS:
if any(norm == alias or norm.startswith(alias + " ") for alias in spec.aliases):
return spec
if _NERD_RE.search(norm):
return NERD_FONT_GENERIC
return None
# --- config-file reading ---------------------------------------------------------
def _strip_jsonc(text: str) -> str:
"""Strip ``//`` and ``/* */`` comments from JSON-with-comments, string-safely."""
out: list[str] = []
i, n = 0, len(text)
in_string = False
while i < n:
ch = text[i]
if in_string:
out.append(ch)
if ch == "\\" and i + 1 < n: # keep the escaped char verbatim
out.append(text[i + 1])
i += 2
continue
if ch == '"':
in_string = False
i += 1
continue
if ch == '"':
in_string = True
out.append(ch)
i += 1
continue
if ch == "/" and i + 1 < n and text[i + 1] == "/":
while i < n and text[i] != "\n":
i += 1
continue
if ch == "/" and i + 1 < n and text[i + 1] == "*":
i += 2
while i + 1 < n and not (text[i] == "*" and text[i + 1] == "/"):
i += 1
i += 2
continue
out.append(ch)
i += 1
return "".join(out)
def _read_jsonc(path: Path) -> Optional[dict]:
"""Parse a JSONC settings file (comments, trailing commas), ``None`` on any failure.
Both Windows Terminal and VS Code write JSON-with-comments; a strict parser dies
on the first ``//``. Trailing commas are swept with a best-effort regex after the
string-aware comment strip (a *string* containing ``", }"`` could theoretically be
clipped — no font config holds one).
"""
try:
text = path.read_text(encoding="utf-8-sig")
data = json.loads(re.sub(r",\s*([}\]])", r"\1", _strip_jsonc(text)))
return data if isinstance(data, dict) else None
except Exception:
return None
def _profile_face(profile: object) -> Optional[str]:
"""A Windows Terminal profile's configured face — new ``font.face`` or legacy
``fontFace`` — or ``None`` when the profile leaves it to the defaults chain."""
if not isinstance(profile, dict):
return None
font = profile.get("font")
if isinstance(font, dict):
face = font.get("face")
if isinstance(face, list) and face: # defensive: an array face takes its head
face = face[0]
if isinstance(face, str) and face.strip():
return face
legacy = profile.get("fontFace")
if isinstance(legacy, str) and legacy.strip():
return legacy
return None
def _wt_settings_paths(environ: Mapping[str, str]) -> list[Path]:
"""Candidate Windows Terminal ``settings.json`` locations (packaged, preview,
unpackaged), existing files only."""
local = environ.get("LOCALAPPDATA")
if not local:
return []
base = Path(local)
candidates: list[Path] = []
try:
packages = base / "Packages"
if packages.is_dir():
candidates.extend(
sorted(packages.glob("Microsoft.WindowsTerminal*/LocalState/settings.json"))
)
except OSError:
pass
candidates.append(base / "Microsoft" / "Windows Terminal" / "settings.json")
return [p for p in candidates if p.is_file()]
def _windows_terminal_face(environ: Mapping[str, str]) -> str:
"""The face Windows Terminal is rendering this session with.
Resolved the way the terminal itself does: the ``WT_PROFILE_ID`` profile's font,
else ``profiles.defaults``, else the built-in default ``Cascadia Mono``. An
unreadable settings file lands on the built-in default too — a pristine install
is exactly that.
"""
guid = (environ.get("WT_PROFILE_ID") or "").strip().lower()
for path in _wt_settings_paths(environ):
data = _read_jsonc(path)
if data is None:
continue
profiles = data.get("profiles")
plist: list = []
defaults: object = {}
if isinstance(profiles, dict):
plist = profiles.get("list") or []
defaults = profiles.get("defaults") or {}
elif isinstance(profiles, list): # the ancient flat-list schema
plist = profiles
profile = next(
(
p for p in plist
if isinstance(p, dict) and str(p.get("guid", "")).strip().lower() == guid
),
None,
) if guid else None
face = _profile_face(profile) or _profile_face(defaults)
if face:
return face
break # a parsed settings file with no face set → the built-in default
return "Cascadia Mono"
def _vscode_settings_paths(
environ: Mapping[str, str], start: Optional[Path]
) -> list[Path]:
"""VS Code settings files in precedence order: nearest workspace, then user
(stable and Insiders, per-platform locations), existing files only."""
paths: list[Path] = []
try:
here = (start or Path.cwd()).resolve()
for folder in (here, *here.parents):
candidate = folder / ".vscode" / "settings.json"
if candidate.is_file():
paths.append(candidate)
break
except OSError:
pass
roots: list[Path] = []
appdata = environ.get("APPDATA")
if appdata:
roots.append(Path(appdata))
home = environ.get("HOME") or environ.get("USERPROFILE")
if home:
roots.append(Path(home) / ".config")
roots.append(Path(home) / "Library" / "Application Support")
for root in roots:
for flavour in ("Code", "Code - Insiders"):
candidate = root / flavour / "User" / "settings.json"
if candidate.is_file():
paths.append(candidate)
return paths
def _vscode_default_face() -> str:
"""VS Code's per-platform default editor font family's first name."""
if sys.platform == "win32":
return "Consolas"
if sys.platform == "darwin":
return "Menlo"
return "Droid Sans Mono"
def _vscode_face(environ: Mapping[str, str], start: Optional[Path] = None) -> str:
"""The face VS Code's integrated terminal is rendering with.
``terminal.integrated.fontFamily`` wins over ``editor.fontFamily`` (VS Code's own
fallback), and within each key the workspace file wins over the user file. The
stored value is a CSS-style comma list; the first family is judged.
"""
paths = _vscode_settings_paths(environ, start)
settings = [data for data in (_read_jsonc(p) for p in paths) if data is not None]
for key in ("terminal.integrated.fontFamily", "editor.fontFamily"):
for data in settings:
value = data.get(key)
if isinstance(value, str) and value.strip():
return primary_family(value)
return _vscode_default_face()
def _conhost_face() -> Optional[str]:
"""The classic-console face via ``GetCurrentConsoleFontEx``, or ``None``.
Only meaningful on a *genuine* conhost — the caller gates on the absence of ConPTY
markers first, because the hidden conhost under Windows Terminal / VS Code reports
a stub face, not the glyphs on screen.
"""
if sys.platform != "win32":
return None
try:
import ctypes
import ctypes.wintypes as wintypes
kernel32 = ctypes.windll.kernel32
if not kernel32.GetConsoleWindow():
return None
class _COORD(ctypes.Structure):
_fields_ = [("X", wintypes.SHORT), ("Y", wintypes.SHORT)]
class _CONSOLE_FONT_INFOEX(ctypes.Structure):
_fields_ = [
("cbSize", wintypes.ULONG),
("nFont", wintypes.DWORD),
("dwFontSize", _COORD),
("FontFamily", wintypes.UINT),
("FontWeight", wintypes.UINT),
("FaceName", ctypes.c_wchar * 32),
]
info = _CONSOLE_FONT_INFOEX()
info.cbSize = ctypes.sizeof(_CONSOLE_FONT_INFOEX)
handle = kernel32.GetStdHandle(-11) # STD_OUTPUT_HANDLE
if not kernel32.GetCurrentConsoleFontEx(handle, False, ctypes.byref(info)):
return None
return info.FaceName or None
except Exception:
return None
# --- the ladder -------------------------------------------------------------------
def detect_terminal_font(
environ: Optional[Mapping[str, str]] = None,
*,
cwd: Optional[Path] = None,
conhost_probe: Callable[[], Optional[str]] = _conhost_face,
) -> Optional[FontDetection]:
"""Identify the terminal and read the font it is configured to render with.
The ladder, most to least certain: Windows Terminal (``WT_SESSION`` names the app,
``WT_PROFILE_ID`` the exact profile), VS Code (``TERM_PROGRAM=vscode``), then a
genuine classic conhost (no ConPTY markers, no ``TERM`` — a set ``TERM`` means some
other emulator is hosting the console) asked directly via Win32. Anything else —
an unrecognised terminal, an ssh session — is ``None``: the face is unknowable
from here.
Args:
environ: The environment to inspect (defaults to ``os.environ``).
cwd: Where the VS Code workspace walk starts (defaults to the process cwd).
conhost_probe: The classic-console face reader (injectable for tests).
Returns:
The configured face and its source, or ``None`` when no source applies.
"""
env = os.environ if environ is None else environ
if env.get("WT_SESSION"):
return FontDetection(_windows_terminal_face(env), "windows-terminal")
if (env.get("TERM_PROGRAM") or "").lower() == "vscode":
return FontDetection(_vscode_face(env, cwd), "vscode")
if not env.get("TERM") and not env.get("TERM_PROGRAM"):
face = conhost_probe()
if face:
return FontDetection(face, "conhost")
return None
def _renderer_backed(environ: Mapping[str, str]) -> Optional[str]:
"""The terminal id when the terminal draws the core triangles itself.
Windows Terminal falls back to its bundled Cascadia Code NF for the powerline
symbol ranges (≥ 1.22), VS Code's terminal draws them as custom glyphs
(``terminal.integrated.customGlyphs``, default on), and kitty / WezTerm /
Alacritty ship built-in powerline glyphs — on those, the separators render
whatever the configured font holds.
"""
if environ.get("WT_SESSION"):
return "windows-terminal"
term_program = (environ.get("TERM_PROGRAM") or "").lower()
if term_program == "vscode":
return "vscode"
if term_program == "wezterm" or environ.get("WEZTERM_EXECUTABLE"):
return "wezterm"
if environ.get("KITTY_WINDOW_ID") or environ.get("TERM") == "xterm-kitty":
return "kitty"
if environ.get("ALACRITTY_WINDOW_ID") or environ.get("ALACRITTY_SOCKET") \
or environ.get("TERM") == "alacritty":
return "alacritty"
return None
def _powerline_support(
environ: Optional[Mapping[str, str]] = None,
*,
cwd: Optional[Path] = None,
conhost_probe: Callable[[], Optional[str]] = _conhost_face,
) -> PowerlineSupport:
"""The uncached verdict (see :func:`powerline_support` for the ladder)."""
env = os.environ if environ is None else environ
override = (env.get("MESHTERM_POWERLINE") or "").strip().lower()
if override in {"0", "off", "no", "none", "false"}:
return PowerlineSupport(NONE, "env")
if override in {"1", "on", "yes", "true", "full"}:
return PowerlineSupport(FULL, "env")
if override == "core":
return PowerlineSupport(CORE, "env")
detected = detect_terminal_font(env, cwd=cwd, conhost_probe=conhost_probe)
matched = match_recommended(detected.face) if detected else None
if detected and matched:
return PowerlineSupport(
matched.coverage, f"font:{detected.source}", detected.face, matched
)
backed = _renderer_backed(env)
if backed:
return PowerlineSupport(
CORE, f"renderer:{backed}", detected.face if detected else None
)
if detected: # a face we could read, matching nothing, on a terminal with no fallback
return PowerlineSupport(NONE, f"font:{detected.source}", detected.face)
if env.get("SSH_CONNECTION") or env.get("SSH_TTY") or env.get("SSH_CLIENT"):
return PowerlineSupport(UNKNOWN, "ssh")
return PowerlineSupport(UNKNOWN, "unknown")
@lru_cache(maxsize=1)
def powerline_support() -> PowerlineSupport:
"""The session's powerline verdict, decided once and cached.
The ladder: the ``MESHTERM_POWERLINE`` override, then the configured font matched
against :data:`RECOMMENDED_FONTS`, then a renderer that draws the glyphs itself,
then an honest ``none`` (face read, no match, no fallback) or ``unknown`` (ssh, or
a terminal we can't identify). Cached because the environment and settings files
don't change mid-session; tests exercise :func:`_powerline_support` directly.
Returns:
The :class:`PowerlineSupport` verdict.
"""
return _powerline_support(os.environ)
def powerline_enabled() -> bool:
"""Whether path lines should default to powerline separators (see ``.capable``)."""
return powerline_support().capable
# --- installed-font scan ----------------------------------------------------------
def _installed_families() -> Iterable[str]:
"""Every installed font family name this platform will admit to, best-effort."""
if sys.platform == "win32":
try:
import winreg
for root in (winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER):
try:
key = winreg.OpenKey(
root, r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"
)
except OSError:
continue
with key:
for i in range(winreg.QueryInfoKey(key)[1]):
try:
name = winreg.EnumValue(key, i)[0]
except OSError:
break
# "Hack Nerd Font Mono (TrueType)" → "Hack Nerd Font Mono"
yield name.split(" (")[0]
except Exception:
return
return
try:
import subprocess
result = subprocess.run(
["fc-list", ":", "family"], capture_output=True, text=True, timeout=2
)
for line in result.stdout.splitlines():
for family in line.split(","):
if family.strip():
yield family.strip()
except Exception:
return
def installed_recommended() -> Optional[RecommendedFont]:
"""The best recommended font *installed* on this machine, selected or not.
The weaker, always-available check that a future recommendation screen splits its
message on: "not installed — here's the list" versus "installed but not selected
in your terminal's profile" versus "active". Prefers :data:`FULL` coverage over
:data:`CORE`; returns ``None`` when nothing recommended is installed (or the
platform offers no way to ask).
"""
best: Optional[RecommendedFont] = None
for family in _installed_families():
matched = match_recommended(family)
if matched is None:
continue
if matched.coverage == FULL:
return matched
best = best or matched
return best
+229
View File
@@ -0,0 +1,229 @@
"""Terminal-font detection tests: the recommended list, the ladder, the verdict.
Everything runs against injected environments and temp settings files — no test
touches the real registry, the real Win32 console, or the machine's actual fonts —
so the suite decides the same verdicts on any box.
"""
from __future__ import annotations
import json
from pathlib import Path
from meshterm.ui.termfont import (
CORE,
FULL,
NONE,
UNKNOWN,
NERD_FONT_GENERIC,
RecommendedFont,
_powerline_support,
_read_jsonc,
_vscode_default_face,
_vscode_face,
_windows_terminal_face,
detect_terminal_font,
installed_recommended,
match_recommended,
normalize_face,
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:
"""``JetBrainsMono Nerd Font`` must hit its full entry, never the plain
``JetBrains Mono`` core prefix — first match wins, so 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:
"""``terminal.integrated.fontFamily`` beats ``editor.fontFamily``; within a key
the workspace file beats the user file; the value's head family is 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:
"""WT markers first, then VS Code, then a genuine conhost; TERM disqualifies
the conhost probe (some other emulator is hosting the console)."""
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 on WT still earns core (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:
"""Glyph-drawing terminals earn core by marker; ssh and strangers stay unknown
(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_installed_recommended_never_raises() -> None:
"""The machine scan is best-effort context for a future nudge screen — whatever
the platform answers, it is a RecommendedFont or None, never an exception."""
result = installed_recommended()
assert result is None or isinstance(result, RecommendedFont)