The console that can't draw an emoji says so before we draw one
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>
This commit is contained in:
1 parent
d062b5b706
commit
ea5f644d73
8 files changed
+418
-14
No files matched your search
@@ -17,6 +17,19 @@ allowed to change behaviour, not just add to it.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Every icon was a box in the classic Windows console.** That console — which is what a
|
||||
double-clicked build still gets — draws no emoji at all, whatever font it is set to, so
|
||||
the menu came up as rows of replacement characters.
|
||||
|
||||
MeshTerm now works out which console it is running in and, on that one, draws its icons
|
||||
through the compact single-character table it already keeps for the PicoCalc: `☼` for an
|
||||
advert, `¶` for a message, `★` for the trophy case. Nothing else about the desktop
|
||||
layout changes — same width, same colours, same accented names. Running in Windows
|
||||
Terminal or VS Code's terminal keeps the emoji, and `MESHTERM_EMOJI=1` or `=0` forces
|
||||
the matter either way. `meshterm platform` reports which it chose and why.
|
||||
|
||||
## [0.2.4] — 2026-09-07
|
||||
|
||||
### Fixed
|
||||
|
||||
+24
-2
@@ -28,9 +28,10 @@ from .core.preferences import PreferenceError, Preferences
|
||||
from .core.selection import DeviceSelectionError
|
||||
from .persistence.logging import configure_logging, get_logger, level_from_name, log_path
|
||||
from .persistence.repository import Repository
|
||||
from .platforms import Resolution, resolve, set_platform
|
||||
from .platforms import Resolution, resolve, set_platform, without_emoji
|
||||
from .tools import all_tools
|
||||
from .tools.base import Tool, ToolResult
|
||||
from .ui.termfont import emoji_support
|
||||
from .ui.theme import make_console
|
||||
|
||||
|
||||
@@ -138,7 +139,14 @@ def main_callback(
|
||||
_platform_resolution = resolve(platform)
|
||||
except ValueError as exc:
|
||||
raise typer.BadParameter(str(exc), param_hint="--platform") from exc
|
||||
set_platform(_platform_resolution.platform)
|
||||
# A terminal that cannot draw emoji is asked about here rather than in resolve(),
|
||||
# which answers "which flavour" from the flag, the env and the device tree and should
|
||||
# stay that pure — this is the *terminal's* limit, and it applies whichever flavour
|
||||
# those three chose. See meshterm.ui.termfont.emoji_support.
|
||||
active = _platform_resolution.platform
|
||||
if not emoji_support().supported:
|
||||
active = without_emoji(active)
|
||||
set_platform(active)
|
||||
|
||||
settings = Settings.load()
|
||||
if db_path is not None:
|
||||
@@ -244,6 +252,10 @@ def platform_command() -> None:
|
||||
flavour a given invocation would run as, and why — the ``--platform``/
|
||||
``MESHTERM_PLATFORM`` inputs, what ``/proc/device-tree/model`` reports (``None`` off
|
||||
the actual hardware), and which of those decided it.
|
||||
|
||||
It also reports the terminal's own emoji verdict, which is a separate question from
|
||||
the flavour and the usual reason a Windows session looks plainer than the screenshots
|
||||
(see :func:`meshterm.ui.termfont.emoji_support`).
|
||||
"""
|
||||
assert _platform_resolution is not None # set by the callback that always runs first
|
||||
r = _platform_resolution
|
||||
@@ -253,6 +265,16 @@ def platform_command() -> None:
|
||||
console.print(f" MESHTERM_PLATFORM: {r.env or '(not set)'}")
|
||||
console.print(f" /proc/device-tree/model: {r.detected_model or '(unavailable)'}")
|
||||
|
||||
emoji = emoji_support()
|
||||
verdict = "yes" if emoji.supported else "no — icons use the compact glyphs"
|
||||
console.print(f"icons: [accent]{verdict}[/accent] (source: {emoji.source})")
|
||||
if not emoji.supported and emoji.source == "console":
|
||||
console.print(
|
||||
" this is the classic Windows console, which draws no emoji whatever font\n"
|
||||
" it is set to. Windows Terminal and VS Code's terminal both do — running\n"
|
||||
" MeshTerm in one gets the icons back. MESHTERM_EMOJI=1 overrides this."
|
||||
)
|
||||
|
||||
|
||||
@app.command(name="specimen")
|
||||
def specimen_command() -> None:
|
||||
|
||||
+21
-1
@@ -19,7 +19,7 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -342,10 +342,30 @@ def resolve(flag: str | None = None) -> Resolution:
|
||||
return Resolution(flag, env, model, "default", REGULAR)
|
||||
|
||||
|
||||
def without_emoji(platform: Platform) -> Platform:
|
||||
"""The same platform with its icons routed through the compact glyph table.
|
||||
|
||||
A terminal that cannot draw emoji is not a different *flavour* — the classic Windows
|
||||
console has the desktop's width, colour depth and keyboard, and wants every other
|
||||
thing :data:`REGULAR` says. Only the one flag moves, which is what the seam's flags
|
||||
being independent is for: ``ascii_fold`` stays off, so accents and the rest of the
|
||||
BMP still come through.
|
||||
|
||||
Args:
|
||||
platform: The resolved platform.
|
||||
|
||||
Returns:
|
||||
``platform`` itself when its icons are already compact, else a copy with
|
||||
``emoji`` cleared.
|
||||
"""
|
||||
return platform if not platform.emoji else replace(platform, emoji=False)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Platform",
|
||||
"REGULAR",
|
||||
"PICOCALC",
|
||||
"without_emoji",
|
||||
"PLATFORM",
|
||||
"get_platform",
|
||||
"set_platform",
|
||||
|
||||
+185
-1
@@ -1,4 +1,25 @@
|
||||
"""Terminal font detection — can this terminal draw powerline path separators?
|
||||
"""Terminal capability detection — what can this terminal actually draw?
|
||||
|
||||
Two questions, both answered out-of-band because a terminal cannot be asked in-band: a
|
||||
glyph it does not have still occupies its cell, so the screen looks the same whether the
|
||||
character arrived or not.
|
||||
|
||||
**Powerline separators**, below, are a *font* question — read the terminal's configured
|
||||
face and match it against fonts known to carry the block.
|
||||
|
||||
**Emoji** (:func:`emoji_support`) are a *host* question, and only on Windows. The classic
|
||||
console — ``conhost``, which is what a double-clicked program still gets — rasterises
|
||||
through GDI with the console font and nothing behind it, so an emoji lands as a
|
||||
replacement box however the font is configured. Everything that replaced it draws them
|
||||
fine. So the verdict identifies the host, structurally rather than from the environment
|
||||
(``WT_SESSION`` is inherited across process launches, so a program started from Windows
|
||||
Terminal into a console of its own still claims to be in one), and a ``False`` routes
|
||||
every icon through the compact single-glyph table :func:`meshterm.ui.theme.glyph` already
|
||||
keeps for the PicoCalc — that whole vocabulary is BMP, so it draws wherever the plain
|
||||
status marks already do.
|
||||
|
||||
Both verdicts are hints that pick a default, and both have an override for the reader who
|
||||
knows better than the probe: ``MESHTERM_POWERLINE`` and ``MESHTERM_EMOJI``.
|
||||
|
||||
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
|
||||
@@ -677,3 +698,166 @@ def installed_recommended() -> RecommendedFont | None:
|
||||
return matched
|
||||
best = best or matched
|
||||
return best
|
||||
|
||||
|
||||
# --- emoji support ----------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EmojiSupport:
|
||||
"""Whether this terminal can show an emoji at all, and what decided that.
|
||||
|
||||
Attributes:
|
||||
supported: ``True`` when an emoji icon reaches the screen as itself.
|
||||
source: ``"env"`` (the ``MESHTERM_EMOJI`` override), ``"platform"`` (not Windows,
|
||||
so not the classic console's problem), ``"console"`` (the host was identified
|
||||
— see :func:`_is_classic_console`), or ``"no-console"`` (nothing to identify:
|
||||
output is redirected, or the call failed).
|
||||
"""
|
||||
|
||||
supported: bool
|
||||
source: str
|
||||
|
||||
|
||||
def _is_classic_console() -> bool | None:
|
||||
"""Whether this process is drawing into a genuine ``conhost`` window.
|
||||
|
||||
Which is the whole question, because that host renders through GDI with the console
|
||||
font and no emoji font behind it, while everything that replaced it — Windows
|
||||
Terminal, VS Code's terminal, anything over ssh — draws emoji as a matter of course.
|
||||
|
||||
It cannot be asked in band. A missing glyph still occupies its cell, so the screen
|
||||
reads back exactly as it would have if the character had drawn: measured on Windows 10
|
||||
22H2, a classic console stores ``📡`` intact and advances two cells while showing a
|
||||
single replacement box, and a ConPTY (which does draw it) reads back as U+FFFD because
|
||||
the buffer behind it is updated asynchronously. A write-and-read-back probe answers
|
||||
both hosts *backwards*, which is the trap this docstring exists to record.
|
||||
|
||||
Nor can the environment be trusted alone: ``WT_SESSION`` is inherited by child
|
||||
processes, so a program launched from Windows Terminal into a console of its own still
|
||||
carries it and would answer for the wrong host.
|
||||
|
||||
So the host is identified structurally, from two independent facts, and both must
|
||||
agree before anything is dimmed (the costly mistake is stripping icons from a terminal
|
||||
that could draw them):
|
||||
|
||||
* **The console font has a real pixel width.** A ConPTY's hidden conhost reports a
|
||||
stub — face ``Consolas``, cell width ``0`` — because nothing is rasterised there.
|
||||
* **The buffer is taller than its window.** A classic console owns its scrollback
|
||||
(9001 rows by default); under a ConPTY the terminal owns it, so the buffer is
|
||||
exactly the window.
|
||||
|
||||
Returns:
|
||||
``True`` for a genuine classic console, ``False`` for anything else drawing to a
|
||||
console, and ``None`` when there is no console on stdout (redirected output, not
|
||||
Windows, or the call failed) — an unknown is never a reason to degrade.
|
||||
"""
|
||||
if sys.platform != "win32":
|
||||
return None
|
||||
try:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
class _COORD(ctypes.Structure):
|
||||
_fields_ = [("X", wintypes.SHORT), ("Y", wintypes.SHORT)]
|
||||
|
||||
class _SMALL_RECT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("Left", wintypes.SHORT),
|
||||
("Top", wintypes.SHORT),
|
||||
("Right", wintypes.SHORT),
|
||||
("Bottom", wintypes.SHORT),
|
||||
]
|
||||
|
||||
class _CSBI(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("dwSize", _COORD),
|
||||
("dwCursorPosition", _COORD),
|
||||
("wAttributes", wintypes.WORD),
|
||||
("srWindow", _SMALL_RECT),
|
||||
("dwMaximumWindowSize", _COORD),
|
||||
]
|
||||
|
||||
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),
|
||||
]
|
||||
|
||||
# Safe to declare: this handle is ours alone (see meshterm.core.win32dll). The
|
||||
# explicit restype matters — ctypes would assume a 32-bit int and truncate the
|
||||
# 64-bit HANDLE, turning every later call into a silent failure.
|
||||
kernel32 = win32dll.kernel32()
|
||||
kernel32.GetStdHandle.argtypes = [wintypes.DWORD]
|
||||
kernel32.GetStdHandle.restype = wintypes.HANDLE
|
||||
kernel32.GetConsoleScreenBufferInfo.argtypes = [wintypes.HANDLE, ctypes.POINTER(_CSBI)]
|
||||
kernel32.GetConsoleScreenBufferInfo.restype = wintypes.BOOL
|
||||
kernel32.GetCurrentConsoleFontEx.argtypes = [
|
||||
wintypes.HANDLE,
|
||||
wintypes.BOOL,
|
||||
ctypes.POINTER(_CONSOLE_FONT_INFOEX),
|
||||
]
|
||||
kernel32.GetCurrentConsoleFontEx.restype = wintypes.BOOL
|
||||
|
||||
handle = kernel32.GetStdHandle(wintypes.DWORD(-11).value) # STD_OUTPUT_HANDLE
|
||||
info = _CSBI()
|
||||
# Fails when stdout is a pipe or a file — nothing is being drawn, so there is
|
||||
# nothing to answer, and this is what keeps redirected and CI runs out of it.
|
||||
if not kernel32.GetConsoleScreenBufferInfo(handle, ctypes.byref(info)):
|
||||
return None
|
||||
|
||||
font = _CONSOLE_FONT_INFOEX()
|
||||
font.cbSize = ctypes.sizeof(_CONSOLE_FONT_INFOEX)
|
||||
if not kernel32.GetCurrentConsoleFontEx(handle, False, ctypes.byref(font)):
|
||||
return None
|
||||
|
||||
rasterised = font.dwFontSize.X > 0
|
||||
owns_scrollback = info.dwSize.Y > (info.srWindow.Bottom - info.srWindow.Top + 1)
|
||||
return rasterised and owns_scrollback
|
||||
except Exception: # pragma: no cover - a probe that fails is an unknown, not a crash
|
||||
return None
|
||||
|
||||
|
||||
def _emoji_support(
|
||||
environ: Mapping[str, str] | None = None,
|
||||
*,
|
||||
console_probe: Callable[[], bool | None] = _is_classic_console,
|
||||
system: str | None = None,
|
||||
) -> EmojiSupport:
|
||||
"""The uncached verdict (see :func:`emoji_support` for the ladder)."""
|
||||
env = os.environ if environ is None else environ
|
||||
override = (env.get("MESHTERM_EMOJI") or "").strip().lower()
|
||||
if override in {"0", "off", "no", "none", "false"}:
|
||||
return EmojiSupport(False, "env")
|
||||
if override in {"1", "on", "yes", "true"}:
|
||||
return EmojiSupport(True, "env")
|
||||
|
||||
if (sys.platform if system is None else system) != "win32":
|
||||
return EmojiSupport(True, "platform")
|
||||
classic = console_probe()
|
||||
if classic is None:
|
||||
return EmojiSupport(True, "no-console")
|
||||
return EmojiSupport(not classic, "console")
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def emoji_support() -> EmojiSupport:
|
||||
"""Whether emoji icons can be drawn here, decided once and cached.
|
||||
|
||||
The ladder: the ``MESHTERM_EMOJI`` override, then everything that isn't Windows (where
|
||||
a UTF-8 terminal draws them as a matter of course), then :func:`_is_classic_console`.
|
||||
Cached because a session does not change host halfway through.
|
||||
|
||||
Returns:
|
||||
The :class:`EmojiSupport` verdict.
|
||||
"""
|
||||
return _emoji_support(os.environ)
|
||||
|
||||
|
||||
def emoji_enabled() -> bool:
|
||||
"""Whether icons should render as emoji rather than through the compact table."""
|
||||
return emoji_support().supported
|
||||
@@ -283,13 +283,20 @@ def _has_wide_glyph(text: str) -> bool:
|
||||
``>= 0x1100`` guard skips the ASCII/Latin bulk of a frame before the width lookup, which
|
||||
matters because this runs over the whole composed frame on every repaint.
|
||||
|
||||
On a platform that draws no emoji the answer is ``False`` by construction — its console
|
||||
font is a fixed 512-glyph set with no wide glyph in it, so nothing a frame can contain
|
||||
would return ``True``. Answering from the platform instead of the text skips a
|
||||
per-character scan of the entire frame on every repaint, and with it the scrub machinery
|
||||
that a ``True`` would arm, leaving the cheap differential paint permanently in play.
|
||||
On a platform that folds its frames to a fixed font the answer is ``False`` by
|
||||
construction — that font is a 512-glyph set with no wide glyph in it, so nothing a
|
||||
composed frame can contain would return ``True``. Answering from the platform instead
|
||||
of the text skips a per-character scan of the entire frame on every repaint, and with
|
||||
it the scrub machinery that a ``True`` would arm, leaving the cheap differential paint
|
||||
permanently in play.
|
||||
|
||||
The fold is the guarantee, not the icon table: a terminal that merely can't draw
|
||||
*emoji* (the classic Windows console — see :func:`meshterm.ui.termfont.emoji_support`)
|
||||
still renders the rest of the BMP, and a contact named in CJK is two cells wide there
|
||||
like anywhere else. Reading ``emoji`` here instead would have skipped the scan on a
|
||||
frame that genuinely needed it, and smeared the row.
|
||||
"""
|
||||
if not get_platform().emoji:
|
||||
if get_platform().ascii_fold:
|
||||
return False
|
||||
return any(ord(ch) >= 0x1100 and get_cwidth(ch) == 2 for ch in text)
|
||||
|
||||
|
||||
+58
-1
@@ -6,12 +6,20 @@ 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
|
||||
from meshterm.platforms import (
|
||||
PICOCALC,
|
||||
REGULAR,
|
||||
get_platform,
|
||||
resolve,
|
||||
set_platform,
|
||||
without_emoji,
|
||||
)
|
||||
|
||||
|
||||
def test_regular_is_todays_behaviour() -> None:
|
||||
@@ -142,3 +150,52 @@ def test_resolve_auto_detect_is_conservative(monkeypatch: pytest.MonkeyPatch) ->
|
||||
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
|
||||
+86
-3
@@ -1,8 +1,9 @@
|
||||
"""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.
|
||||
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
|
||||
@@ -17,6 +18,8 @@ from meshterm.ui.termfont import (
|
||||
NONE,
|
||||
UNKNOWN,
|
||||
RecommendedFont,
|
||||
_emoji_support,
|
||||
_is_classic_console,
|
||||
_powerline_support,
|
||||
_read_jsonc,
|
||||
_vscode_default_face,
|
||||
@@ -252,3 +255,83 @@ def test_installed_recommended_never_raises() -> 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)
|
||||
@@ -150,6 +150,24 @@ def test_every_glyph_map_target_is_in_the_console_font() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_every_glyph_map_target_stays_inside_the_bmp() -> None:
|
||||
"""The table has a second consumer that the console font cannot speak for.
|
||||
|
||||
The classic Windows console keeps one 16-bit code unit per cell, so a character above
|
||||
U+FFFF is replaced by U+FFFD before its font is ever consulted — which is why MeshTerm
|
||||
routes icons through this table there too (see
|
||||
:func:`meshterm.ui.termfont.emoji_support`). A substitute "improved" into an emoji
|
||||
would draw fine on the PicoCalc's own font and silently break every Windows console,
|
||||
so the plane is asserted here rather than left to the font whitelist to imply.
|
||||
"""
|
||||
for emoji, compact in theme._GLYPH_MAP.items():
|
||||
for ch in compact:
|
||||
assert ord(ch) <= 0xFFFF, (
|
||||
f"{emoji!r} maps to {compact!r}; {ch!r} is above U+FFFF and no classic "
|
||||
f"Windows console can hold it"
|
||||
)
|
||||
|
||||
|
||||
def test_glyph_map_targets_never_widen_their_icon() -> None:
|
||||
"""A compact form at most matches its emoji's measured width (the fold pads the rest)."""
|
||||
for emoji, compact in theme._GLYPH_MAP.items():
|
||||
|
||||
Reference in new issue
Block a user