Files
MeshTerm/tests/test_theme16.py
T
Jean-Pierre MartineauandClaude Opus 5 ad2b852018 Wordmark: a mark's file says how wide it is, and the folder is the ladder
The two marks sat beside the written pages in assets/ under names that spelled
their widths differently and only by accident — logo.ans and logo_53.ans, where
the first is 71 columns and says so nowhere. They move into assets/splash/ as
logo.71.ans and logo.53.ans: one file per canvas, each named for the canvas.

So the size ladder is now the folder listing. _variants() reads the widths out
of the names and walks them widest-first, which means a new size is a file
dropped in rather than a file dropped in *and* a tuple edited. The name only
orders the ladder — which mark fits is still settled by measuring the art, so a
name that overstates its canvas can't draw a torn mark. A test keeps the two
honest with each other: every mark is as wide as its name says.

The old 8.3 spelling goes with the rename. It was there for art editors that
predate long filenames, which manage a second dot fine, and a name that states
its width is worth more than a name DOS could have opened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:38:53 -04:00

492 lines
22 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""The PicoCalc visual-language contracts: palette, glyph map, fold, and bindings.
P3's deliverables are promises about *output* — only 16 palette slots, only glyphs the
console font holds, layout that survives the translation — enforced screen-by-screen by
the gallery. These tests pin the machinery itself: the two themes stay name-compatible,
the 16-slot theme obeys the VT's bold-brightness and background rules, the deploy
script's palette matches the canonical table, the font build script and the frozen
inventory move together, and the platform bindings actually rebind.
"""
from __future__ import annotations
import re
from pathlib import Path
from rich.cells import cell_len
from rich.default_styles import DEFAULT_STYLES
from meshterm.platforms import PICOCALC, REGULAR, set_platform
from meshterm.ui import theme
from meshterm.ui.fontset import FONT_CODEPOINTS
from meshterm.ui.theme import MESH_THEME, MESH_THEME_16, fold_text, glyph, name_style
_FONT_SCRIPT = Path(__file__).resolve().parent.parent / "scripts" / "calculinux-console-font.sh"
# -- the two themes -------------------------------------------------------------------
def test_both_themes_define_exactly_the_same_style_names() -> None:
"""A style name a screen asks for must resolve on either platform."""
assert set(MESH_THEME.styles) == set(MESH_THEME_16.styles)
def _our_styles(theme_obj):
"""The style entries we declared (Rich merges its own DEFAULT_STYLES into a Theme)."""
return {
name: style
for name, style in theme_obj.styles.items()
if name not in DEFAULT_STYLES
}
def test_theme16_uses_only_the_sixteen_slots() -> None:
"""Every 16-slot style is built from ``color(0..15)`` — never hex, never the cube."""
for name, style in _our_styles(MESH_THEME_16).items():
for color in (style.color, style.bgcolor):
if color is None:
continue
assert color.number is not None and 0 <= color.number <= 15, (
f"{name!r} uses {color!r}, outside the 16 VT slots"
)
def test_theme16_bold_never_jumps_a_dim_slot_to_an_unrelated_bright_one() -> None:
"""The VT draws bold as brightness: bold on slots 0-7 lands on N+8.
Slots 5 and 6 carry semantics whose bright partners mean something *else* here
(5 purple = the repeater mark vs 13 pink = the node mark; 6 cyan = hint.brand vs
14 = brand), so no style may combine bold with them.
"""
for name, style in _our_styles(MESH_THEME_16).items():
if style.bold and style.color is not None and style.color.number in (5, 6):
raise AssertionError(
f"{name!r} is bold on slot {style.color.number}; the VT would draw "
f"slot {style.color.number + 8} instead"
)
def test_theme16_dim_slot_styles_all_answer_the_bold_question() -> None:
"""A dim-slot style must state its intent about bold — it cannot stay silent.
Rich merges a base style into every span it wraps, so a span left at ``bold=None``
inside a selected (bold) row inherits it, and the VT then draws slot N as N+8: the
style silently changes colour depending on what it lands in. Each one is therefore
pinned ``not bold`` (keep the declared colour) or ``bold`` (the promotion is intended).
"""
silent = [
name
for name, style in _our_styles(MESH_THEME_16).items()
if style.color is not None and style.color.number is not None
and style.color.number <= 7 and style.bold is None
]
assert not silent, (
f"dim-slot styles that would inherit a row's bold and change colour: {silent}"
)
def test_unknown_node_grey_survives_a_selected_row() -> None:
"""THE reported bug: an unnamed hop's hash inside a selected row read as white "you".
``node.unknown`` is a dim slot, so an inherited bold would promote it to 15 — the
``you`` white. It must render as plain light grey (37) either way, on both platforms.
"""
from io import StringIO
from rich.console import Console
from rich.text import Text
set_platform(PICOCALC)
console = Console(
theme=MESH_THEME_16, width=20, file=StringIO(),
force_terminal=True, color_system="standard", highlight=False,
)
for base in (None, "cursor"): # unselected row, then the bold cursor row
row = Text(" ")
row.append("3d", style="node.unknown")
if base:
row.style = base
with console.capture() as capture:
console.print(row, end="")
assert "\x1b[37m3d" in capture.get(), (base, capture.get())
def test_theme16_backgrounds_stay_in_the_dim_bank() -> None:
"""The VT has no bright backgrounds — SGR 40-47 only."""
for name, style in _our_styles(MESH_THEME_16).items():
if style.bgcolor is not None:
assert style.bgcolor.number <= 7, (
f"{name!r} background on slot {style.bgcolor.number}; the VT stops at 7"
)
def test_deploy_script_carries_the_archived_custom_palette() -> None:
"""The script's **opt-in** block matches ``theme.vtrgb_lines()`` byte for byte, and
its no-kbd OSC fallback spells the same sixteen RGB values — so the archived custom
palette stays one environment variable away, in sync with `_VT_SLOTS_CUSTOM`."""
script = _FONT_SCRIPT.read_text(encoding="utf-8")
assert "MESHTERM_CUSTOM_PALETTE" in script, "the opt-in gate vanished"
assert theme.vtrgb_lines() in script, "scripts/calculinux-console-font.sh vtrgb drifted"
for index, (_, _, hex_) in enumerate(theme._VT_SLOTS_CUSTOM):
sequence = f"\\033]P{index:x}{hex_.lstrip('#').lower()}"
assert sequence in script, f"OSC fallback missing slot {index}: {sequence}"
# -- the compact glyph map ------------------------------------------------------------
def test_every_glyph_map_target_is_in_the_console_font() -> None:
"""The map may only hand out characters the 512-glyph font can draw."""
for emoji, compact in theme._GLYPH_MAP.items():
for ch in compact:
assert ord(ch) in FONT_CODEPOINTS, (
f"{emoji!r} maps to {compact!r}; {ch!r} is not in the console font"
)
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():
assert cell_len(compact) <= cell_len(emoji), (
f"{emoji!r} ({cell_len(emoji)} cells) maps to wider {compact!r}"
)
def test_glyph_is_identity_on_regular_and_compact_on_picocalc() -> None:
set_platform(REGULAR)
assert glyph("📡") == "📡"
set_platform(PICOCALC)
assert glyph("📡") == "☼"
assert glyph("★") == "★" # node/status marks pass through — they are font-native
# -- the render-boundary fold ---------------------------------------------------------
def test_fold_is_identity_on_regular_even_after_picocalc_used_it() -> None:
"""The bound impls may not leak cached picocalc folds into a regular render."""
set_platform(PICOCALC)
assert fold_text("café") == "cafe"
set_platform(REGULAR)
assert fold_text("café") == "café"
def test_fold_strips_accents_and_preserves_cell_widths() -> None:
set_platform(PICOCALC)
for text in ("café ⚠", "Ĉu vi paroläs", "📡 Advert", "🗑 Clear", "…", "npo Waymarker 🇨🇦"):
folded = fold_text(text)
assert cell_len(folded) == cell_len(text), (text, folded)
for ch in folded:
assert ord(ch) in FONT_CODEPOINTS or ord(ch) < 0x20, (text, folded, ch)
def test_fold_replaces_the_unmappable_at_width() -> None:
"""CJK and unmapped emoji leave as ``?`` at their own width — never as tofu."""
set_platform(PICOCALC)
assert fold_text("你好") == "????"
assert cell_len(fold_text("🦕")) == cell_len("🦕")
def test_fold_keeps_ansi_sequences_intact() -> None:
set_platform(PICOCALC)
line = "\x1b[1;93mwarn é\x1b[0m\nnext"
folded = fold_text(line)
assert folded == "\x1b[1;93mwarn e\x1b[0m\nnext"
def test_fold_quantizes_embedded_truecolor_to_the_slots() -> None:
"""Canvas-emitted truecolor (the braille rasters) lands on the nearest palette slot."""
set_platform(PICOCALC)
folded = fold_text("\x1b[38;2;148;163;184m○\x1b[0m")
assert "38;2;" not in folded
# #94a3b8 → nearest stock slot is 7 (#aaaaaa); a dim-bank slot states its intensity.
assert "\x1b[22;37m" in folded
background = fold_text("\x1b[48;2;94;234;212mX\x1b[0m")
assert "48;2;" not in background
assert re.search(r"\x1b\[4[0-7]m", background), background # bg clamps to the dim bank
eight_bit = fold_text("\x1b[38;5;201mX\x1b[0m")
assert "38;5;" not in eight_bit
def test_fold_drops_zero_width_machinery() -> None:
set_platform(PICOCALC)
assert fold_text("🕸️") == fold_text("🕸")
assert "‍" not in fold_text("a‍b")
# -- name colouring -------------------------------------------------------------------
def test_names_colour_by_key_on_both_platforms() -> None:
"""One rule everywhere: the key picks the hue, and only its first byte does."""
for platform in (REGULAR, PICOCALC):
set_platform(platform)
hue = name_style("YUL-Cartierville", "3d63c6429436")
assert hue.startswith("bold #"), platform.name
# Any prefix of the key agrees, a rename does not move the colour, and a sender
# we could not place falls to the unknown-node grey — colour is reserved for
# keyed identities.
assert name_style("YUL-Cartierville", "3d") == hue
assert name_style("renamed", "3d63c6429436") == hue
assert name_style("nameless", None) == "node.unknown"
def test_picocalc_node_hues_land_on_their_own_palette_slots() -> None:
"""The quantized wheel spends only the six chromatic bright slots, evenly.
Each returned hex is a slot's own RGB, so every downsample on the way out — Rich's,
and the fold's — lands on that exact slot instead of guessing a neighbour.
"""
set_platform(PICOCALC)
slots = {hex_: slot for slot, _, hex_ in theme._VT_SLOTS}
landed = [name_style("n", f"{byte:02x}").removeprefix("bold ") for byte in range(256)]
assert set(landed) == set(theme._NODE_SLOT_HEXES)
assert {slots[hex_] for hex_ in landed} == {9, 10, 11, 12, 13, 14}
assert min(landed.count(h) for h in set(landed)) >= 256 // 8 # no starved sector
def test_route_graph_resolves_a_named_marker_colour_to_rgb() -> None:
"""A relay we can't name keeps its marker's colour — including a *typed* marker.
The type marks carry theme style names, so the graph's label colour has to resolve
them rather than assume a literal hex (it raised ValueError when it did).
"""
from meshterm.ui.widgets import route_graph_style
for platform in (REGULAR, PICOCALC):
set_platform(platform)
_glyph_of, _label_of, label_rgb_of = route_graph_style(
resolve=lambda hop: None, self_name="Me", source="Alice", type_of=lambda hop: 2,
)
assert label_rgb_of("3d63") == theme.mark_rgb("type.repeater"), platform.name
def test_node_type_marks_stay_distinct_on_the_console() -> None:
"""Each type mark owns a slot — a naive downsample would grey the repeater's violet."""
set_platform(PICOCALC)
marks = ("type.node", "type.repeater", "type.room", "type.sensor")
rgbs = [theme.mark_rgb(name) for name in marks]
assert len(set(rgbs)) == len(marks)
assert theme.mark_rgb("type.repeater") != theme.mark_rgb("muted")
def test_heat_ladder_walks_jps_seven_slots_in_order() -> None:
"""The heard-age scale, pinned to its spec: seven steps on plain human boundaries.
White under 5 minutes, then yellow, light red, brown, red, light grey, and one cold
grey shared by "over a year" and "never heard".
"""
from meshterm.ui.widgets import _recency_style
set_platform(PICOCALC)
slots = {hex_: slot for slot, _, hex_ in theme._VT_SLOTS}
expected = [
(60, 15), (299, 15), # under 5 minutes — white
(300, 11), (3599, 11), # 5 minutes — yellow
(3600, 9), (86399, 9), # 1 hour — light red
(86400, 3), (604799, 3), # 1 day — brown
(604800, 1), (2591999, 1), # 1 week — red
(2592000, 7), (31535999, 7), # 1 month — light grey
(31536000, 8), (None, 8), # 1 year, and never — dark grey
]
for secs, slot in expected:
style = MESH_THEME_16.styles[_recency_style(secs)]
assert style.color is not None and style.color.number == slot, (secs, style)
assert not style.bold or slot >= 8, f"bold on dim slot {slot} would jump a rung"
# The regular platform answers the same question with a continuous hue instead.
set_platform(REGULAR)
assert _recency_style(1200).startswith("#")
def test_canvas_drops_emphasis_where_bold_means_brightness() -> None:
"""A braille canvas may not embolden on the console: the VT would recolour the run.
The canvas quantizes its own truecolour at the fold, so a bold run has no way to know
which bank it landed in — an unknown label (slot 7) would arrive as white (15).
"""
from meshterm.ui.mapcanvas import MapCanvas
for platform, expect_bold in ((REGULAR, True), (PICOCALC, False)):
set_platform(platform)
canvas = MapCanvas(6, 1)
canvas.marker(0, 0, "x", (148, 163, 184)) # markers always draw emboldened
rendered = "".join(canvas.to_ansi_lines())
assert ("\x1b[1m" in rendered) is expect_bold, (platform.name, repr(rendered))
# And the colour it was given survives intact — a dim-bank slot with its intensity
# stated, so the span can't inherit brightness from the raster cell before it.
assert "\x1b[22;37m" in rendered
# -- the font build script stays mirrored ---------------------------------------------
def test_font_script_marks_and_fontset_move_together() -> None:
"""Every codepoint the script draws or aliases is in the frozen inventory, and every
donor it consumes is out — the two files must change in the same commit."""
script = _FONT_SCRIPT.read_text(encoding="utf-8")
marks = {int(m, 16) for m in re.findall(r"^ (0x[0-9A-Fa-f]{4}): art\(", script, re.M)}
assert marks, "could not parse MARKS out of the font script"
for cp in marks:
assert cp in FONT_CODEPOINTS, f"script draws U+{cp:04X} but fontset lacks it"
aliases = {
int(m, 16) for m in re.findall(r"^ (0x[0-9A-Fa-f]{4}): 0x[0-9A-Fa-f]{4},", script, re.M)
}
for cp in aliases:
assert cp in FONT_CODEPOINTS, f"script aliases U+{cp:04X} but fontset lacks it"
donors_match = re.search(r"DONORS = \[(.*?)\]", script, re.S)
assert donors_match is not None
# Donors are consumed front to back, one per mark; everything consumed must be out
# of the inventory. The spares at the tail legitimately remain in it.
donor_list = re.findall(r"0x[0-9A-Fa-f]{4}", donors_match.group(1))
assert len(donor_list) >= len(marks), "fewer donors than marks — the build would fail"
for cp_hex in donor_list[: len(marks)]:
cp = int(cp_hex, 16)
assert cp not in FONT_CODEPOINTS, (
f"donor U+{cp:04X} is consumed by a mark but still listed in the fontset"
)
# -- the specimen card ----------------------------------------------------------------
def test_specimen_renders_clean_on_picocalc() -> None:
"""`meshterm specimen` under picocalc obeys every contract it demonstrates:
≤53 cells per line, 16-slot SGR only, console-font characters only."""
from io import StringIO
from rich.console import Console
from meshterm.ui.specimen import specimen_lines
set_platform(PICOCALC)
console = Console(
theme=MESH_THEME_16, width=53, file=StringIO(),
force_terminal=True, color_system="standard", highlight=False,
)
with console.capture() as capture:
for line in specimen_lines():
console.print(line)
for i, line in enumerate(capture.get().splitlines()):
plain = re.sub(r"\x1b\[[0-9;]*m", "", line)
assert cell_len(plain) <= 53, f"specimen line {i} is {cell_len(plain)} cells: {plain!r}"
assert "[38;2;" not in line and "[48;2;" not in line, f"truecolor on line {i}: {line!r}"
for ch in plain:
assert ord(ch) in FONT_CODEPOINTS, f"line {i} char {ch!r} outside the font"
def test_every_wordmark_draws_one_cell_per_cell() -> None:
"""No mark holds a byte the terminal won't draw as a glyph.
An art editor writes straight to video memory, where codepage 437's first 32 bytes are
pictures (►, ◄, ‼) rather than commands. Nothing downstream reads them that way: the
codecs decode them as the control characters they nominally are, Rich measures each as
no cells at all — so every one swallows a column and slides the rest of its row, the
same wound NUL cells leave (see :mod:`meshterm.ui.logo`) — and one of them, ``0x13``,
is XOFF on a real console. So this asks the question that catches all of it at once:
does every character the mark draws occupy exactly the one cell it was drawn in?
"""
from rich.text import Text
from meshterm.ui.logo import _rows, _variants
for name in _variants():
rows = _rows(name)
assert rows, f"{name} should be readable"
for i, row in enumerate(rows):
plain = Text.from_ansi(row).plain
odd = [ch for ch in plain if cell_len(ch) != 1 and ch != ""]
assert not odd, (
f"{name} row {i} holds {[hex(ord(c)) for c in odd]}, which the terminal "
"draws in something other than the one cell it was drawn in"
)
def test_every_mark_is_as_wide_as_its_name_says() -> None:
"""A mark's file is named for its canvas, and that name orders the size ladder.
:func:`~meshterm.ui.logo.load_logo` walks the marks widest-first by the width in the
name and takes the first that *measures* small enough to fit, so a name that overstates
its art doesn't draw a torn mark — it just sends a mark that would have fitted to the
back of the queue, and the splash quietly steps down a size. Cheaper to catch here.
"""
from meshterm.ui.logo import _NAMED_WIDTH, _rows, _variants, logo_width
marks = _variants()
assert marks, "the splash folder should hold at least one mark"
for name in marks:
declared = int(_NAMED_WIDTH.search(name).group(1))
assert logo_width(_rows(name)) == declared, f"{name} is not {declared} cells wide"
def test_the_mark_the_console_picks_stays_inside_its_font() -> None:
"""The wordmark obeys the glyph contract every other screen does.
The PicoCalc's console font is a 512-glyph inventory, and a character outside it is a
test failure rather than a tofu box found on-device. The mark is art rather than
chrome, so it never passed through the gallery's sweep — and it is the very first thing
the device draws.
"""
from rich.text import Text
from meshterm.ui.logo import load_logo
set_platform(PICOCALC)
rows = load_logo(53)
assert rows, "the 53-column mark should fit a 53-column console"
for i, row in enumerate(rows):
for ch in Text.from_ansi(row).plain:
assert ord(ch) in FONT_CODEPOINTS, (
f"mark row {i} draws {ch!r} (U+{ord(ch):04X}), which the console font "
"has no glyph for"
)
def test_narrow_wordmark_keeps_its_dim_rows_off_the_bright_bank() -> None:
"""Every dim-bank span of the wordmark states its own intensity.
An art editor spells brightness the DOS way — a ``1m`` lifts the bank and every span
after it inherits that — which only reads correctly while the escapes stay one unbroken
stream. A row reaches the console on its own, and bold *is* the bright bank there, so a
span that never mentions intensity is read against whatever happened to be set: the
narrow mark's letter row came out part red (slot 1), part light red (slot 9), splitting
at every span that followed a bevel. The art is redrawn from time to time, so this
pins the rule rather than the picture — no row index, no particular colour.
"""
from meshterm.ui.logo import load_logo
from meshterm.ui.tui.frame import _banner_lines
set_platform(PICOCALC)
rows = _banner_lines(load_logo(53), 53)
assert rows, "the 53-column mark should fit a 53-column console"
dim = [
sgr
for row in rows
for sgr in re.findall(r"\x1b\[([0-9;]*)m", row)
if any(re.fullmatch(r"3[0-7]", part) for part in sgr.split(";"))
]
# Without this the loop below would pass on a mark that never touches the dim bank.
assert dim, "the mark paints on the dim bank somewhere, or this check says nothing"
for sgr in dim:
parts = sgr.split(";")
assert any(p in {"0", "1", "2", "22"} for p in parts), f"unstated intensity: {sgr!r}"
# -- relocated marks stay importable from their old homes -----------------------------
def test_mark_constants_reexport_from_their_old_hosts() -> None:
"""The P3 relocation to ``ui.marks`` left the old names bound in the old modules."""
from meshterm.ui.map_render import _NODE, _REPEATER, _SELF, _UNKNOWN
from meshterm.ui.mapcanvas import RGB, parse_hex
from meshterm.ui.marks import NODE_MARK, REPEATER_MARK, SELF_MARK, UNKNOWN_MARK
from meshterm.ui.pathgraph import DST_NODE, SRC_NODE, GlyphOf, LabelOf, LabelRgbOf
assert (_SELF, _REPEATER, _NODE, _UNKNOWN) == (
SELF_MARK, REPEATER_MARK, NODE_MARK, UNKNOWN_MARK,
)
assert parse_hex("#facc15") == (0xFA, 0xCC, 0x15)
assert RGB is not None and SRC_NODE and DST_NODE
assert GlyphOf is not None and LabelOf is not None and LabelRgbOf is not None