Files
MeshTerm/tests/test_logo.py
T
Jean-Pierre MartineauandClaude Opus 5 f962baec58 Two files the formatter wanted, so CI is green again
2dbae7a went red on lint, and it was never ruff check -- that passed. It was
ruff format --check, on two files from the last few commits: a nested def with no blank
line above it in the session, and a sum() in test_logo that fits on one line once
joined. Both pure layout; neither touches a hand-aligned table, and ruff check stays
clean after, which is the thing this repository has been bitten by before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 04:01:28 -04:00

117 lines
5.2 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""The splash art's colour rewriting: brightness said as a colour, never as bold.
The art is DOS-era ANSI, where `1m` lifts the foreground into the bright bank and every
span after it inherits that. Neither half of that survives the trip here: rows are handed
to Rich one at a time, so nothing inherits, and on macOS Terminal bold asks for a heavier
face and leaves the colour alone. So `_state_intensity` restates brightness as the
aixterm bright bank (`9N`), which can only ever mean a colour.
These pin the part that is easy to get subtly wrong — a sequence that changes the
intensity while naming no colour at all, which is exactly how the art announces a bright
run. Both splash files are full of bare `1m`, and dropping one silently draws a whole run
in the dim bank: two spans meant to read dark then light both come out dark.
"""
from __future__ import annotations
import re
from pathlib import Path
from meshterm.ui.logo import _state_intensity
_SPLASH = Path(__file__).resolve().parent.parent / "meshterm" / "assets" / "splash"
_SGR = re.compile(r"\x1b\[([0-9;]*)m")
def _params(text: str) -> list[list[str]]:
"""Every SGR sequence in `text`, split into its parameters.
Matched as whole tokens rather than by substring: a pattern like ``[0-9;]*1`` also
matches the trailing digit of ``31``, which would read a perfectly ordinary red as a
request for bold.
"""
return [found.group(1).split(";") for found in _SGR.finditer(text)]
def _brights(text: str) -> int:
"""How many sequences in `text` name a bright-bank foreground."""
return sum(any(p.isdigit() and 90 <= int(p) <= 97 for p in parts) for parts in _params(text))
# -- a bare intensity change ----------------------------------------------------------
def test_bare_bright_lifts_the_colour_already_in_force() -> None:
"""`1m` on its own brightens the standing foreground, not just the next one named."""
out = _state_intensity("\x1b[0;37mdim\x1b[1mbright")
assert "\x1b[97m" in out, out.replace("\x1b", "ESC")
def test_bare_bright_carries_a_background_without_losing_the_foreground() -> None:
"""`1;41m` names a background and no foreground; the standing one still brightens."""
out = _state_intensity("\x1b[0;32mgreen\x1b[1;41mbright on red")
assert "\x1b[41;92m" in out, out.replace("\x1b", "ESC")
def test_returning_to_dim_restates_the_colour_in_the_dim_bank() -> None:
"""`22m` has the same problem in reverse: the run would stay in the bright bank."""
out = _state_intensity("\x1b[0;31mred\x1b[1mbright\x1b[22mdim again")
assert out.endswith("dim again")
assert "\x1b[22;31m" in out, out.replace("\x1b", "ESC")
def test_a_reset_takes_the_standing_colour_with_it() -> None:
"""After `0m` there is no colour in force, so a later `1m` has nothing to restate."""
out = _state_intensity("\x1b[0;36mcyan\x1b[0m\x1b[1mplain")
tail = out.split("plain")[0].split("\x1b[0m")[-1]
assert _brights(tail) == 0, out.replace("\x1b", "ESC")
def test_a_named_foreground_is_still_rewritten_in_place() -> None:
"""The original behaviour: `1;33m` becomes `93m`, with the bold dropped."""
out = _state_intensity("\x1b[0m\x1b[1;33myellow")
assert "\x1b[93m" in out, out.replace("\x1b", "ESC")
# -- the art itself -------------------------------------------------------------------
def test_no_splash_row_asks_a_terminal_for_bold() -> None:
"""Bold is a font weight on macOS Terminal, so none may reach the screen."""
for art in sorted(_SPLASH.glob("*.ans")):
raw = art.read_bytes().decode("cp437", errors="replace")
for number, line in enumerate(raw.split("\n"), start=1):
for parts in _params(_state_intensity(line)):
assert "1" not in parts, f"{art.name}:{number} still asks for bold"
def test_no_bright_run_in_the_art_is_silently_dropped() -> None:
"""A `1m` with a colour standing must leave a bright-bank foreground behind.
Counting brights across a file cannot catch this -- one `1m` brightens every later
sequence, so the totals stay comfortably high while individual runs go missing. The
invariant is per sequence: walk the source keeping track of whether a colour is in
force, and every sequence that turns brightness on while one is must produce a
bright-bank emission in the rewritten row.
"""
for art in sorted(_SPLASH.glob("*.ans")):
raw = art.read_bytes().decode("cp437", errors="replace")
for number, line in enumerate(raw.split("\n"), start=1):
standing, owed = None, 0
for parts in _params(line):
for part in parts:
if part in ("", "0"):
standing = None
elif re.fullmatch(r"3[0-7]", part):
standing = part
if "1" in parts and standing is not None:
owed += 1
if owed:
assert _brights(_state_intensity(line)) >= owed, (
f"{art.name}:{number} turns brightness on {owed} time(s) with a "
f"colour standing, but the rewrite emits "
f"{_brights(_state_intensity(line))} bright foreground(s)"
)