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>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5 committed 2026-08-26 00:38:53 -04:00
1 parent bbd7ab362c
commit ad2b852018
4 files changed
+57 -13

No files matched your search

File renamed without changes.
File renamed without changes.
+38 -11
View File
@@ -1,18 +1,20 @@
"""The MeshTerm wordmark for the startup splash, loaded from the assets folder.
The art itself lives in ``meshterm/assets`` as pre-coloured ``.ans`` files — the classic
ANSI-art extension, kept short because the editors that draw this stuff are old and
particular about 8.3 names. Each mark is drawn by hand in one of those editors and needs
The art itself lives in ``meshterm/assets/splash`` as pre-coloured ``.ans`` files — the
classic ANSI-art extension — one per canvas width, each named for the width it was drawn
at: ``logo.71.ans``, ``logo.53.ans``. Each mark is drawn by hand in an art editor and needs
no source but itself: there is nothing here that generates a wordmark, and nothing to
re-run after editing one. Files are read fresh on every call, so the mark can be re-styled
by editing the art alone: no code change and no restart of the design loop.
by editing the art alone: no code change and no restart of the design loop — and a new size
is a file dropped in the folder, since the ladder is read from the names (:func:`_variants`)
rather than listed here.
Three things separate a real ``.ans`` from a text file with colour in it, and :func:`_rows`
handles them all so the art stays authorable in the tools that drew it:
* **Codepage 437.** The blocks and box-drawing (``█▓▒░`` and ``╔═╗``) are single bytes in
DOS's codepage, not UTF-8. We try UTF-8 first and fall back, so an older mark stored as
UTF-8 (the 53-column one still is) and one exported from an art editor both read.
DOS's codepage, not UTF-8. We try UTF-8 first and fall back, so a mark saved as UTF-8 and
one exported from an art editor both read.
* **Auto-wrap.** An art editor omits the line break on a row that fills the canvas and
lets the terminal wrap it, so the file's newlines are *not* the picture's rows. The
canvas width comes from the SAUCE record on the end of the file — which has to be
@@ -21,6 +23,13 @@ handles them all so the art stays authorable in the tools that drew it:
DOS console dutifully leaves it blank. Rich measures it as nothing at all, so every one
swallowed a column and slid the rest of its row leftwards.
A fourth trap belongs to the *art* rather than the loader: codepage 437's first 32 bytes
are pictures on a DOS console (``►◄‼``) and control characters everywhere else, so a mark
that draws with them measures short here and, at ``0x13``, sends XOFF to a real console.
There is nothing to decode there — the byte has to become a glyph in the file — so it is
held by a test instead (``test_theme16``), alongside the one that keeps the narrow mark
inside the console font.
There is more than one mark, at different widths, and the *screen* picks — not the
platform. A 53-column PicoCalc console and a desktop terminal dragged narrow have the same
problem, so :func:`load_logo` answers the only question that matters: which is the widest
@@ -38,11 +47,13 @@ from rich.text import Text
#: Where the art lives — a sibling of the code, not mixed into it, and inside the package
#: so it ships with an installed wheel.
_ASSETS = Path(__file__).resolve().parent.parent / "assets"
_ASSETS = Path(__file__).resolve().parent.parent / "assets" / "splash"
#: Every wordmark, widest first. :func:`load_logo` walks this and takes the first that
#: fits, so adding a size is dropping a file in and naming it here.
_VARIANTS: tuple[str, ...] = ("logo.ans", "logo_53.ans")
#: ``logo.<width>.ans`` — a mark's file is named for the canvas it was drawn on, so the
#: folder listing *is* the size ladder and :func:`_variants` needs to read nothing else.
#: (The old 8.3 spelling is gone with it: the editors that draw this stuff manage a second
#: dot, and a name that states its width is worth more than a name DOS could have opened.)
_NAMED_WIDTH = re.compile(r"logo\.(\d+)\.ans$")
#: A colour change and nothing else. Every escape these marks use is an SGR, so re-breaking
#: a row only ever has to carry a *colour* across the seam — never a cursor move.
@@ -68,6 +79,22 @@ _SAUCE_LEN = 128
_BLANK_CELLS = {0x00: " ", 0xA0: " "}
def _variants() -> list[str]:
"""Every mark in the folder, widest first.
Adding a size is dropping the file in — nothing here lists them. The width in the name
only *orders* the ladder; which mark fits is still settled by measuring the art itself
(see :func:`load_logo`), so a mark whose name overstates it is caught rather than
trusted. A file that doesn't spell a width is not a mark and is passed over.
"""
named = []
for path in _ASSETS.glob("logo.*.ans"):
found = _NAMED_WIDTH.search(path.name)
if found:
named.append((int(found.group(1)), path.name))
return [name for _, name in sorted(named, reverse=True)]
def _split_sauce(raw: bytes) -> tuple[bytes, Optional[int]]:
"""The art alone, and the canvas width SAUCE claims for it (``None`` when unsigned).
@@ -198,7 +225,7 @@ def load_logo(max_cols: Optional[int] = None) -> list[str]:
draws no banner at all rather than a torn one, and a missing file degrades the
same way rather than raising.
"""
for name in _VARIANTS:
for name in _variants():
rows = _rows(name)
if rows and (max_cols is None or logo_width(rows) <= max_cols):
return rows
+19 -2
View File
@@ -388,9 +388,9 @@ def test_every_wordmark_draws_one_cell_per_cell() -> None:
"""
from rich.text import Text
from meshterm.ui.logo import _VARIANTS, _rows
from meshterm.ui.logo import _rows, _variants
for name in _VARIANTS:
for name in _variants():
rows = _rows(name)
assert rows, f"{name} should be readable"
for i, row in enumerate(rows):
@@ -402,6 +402,23 @@ def test_every_wordmark_draws_one_cell_per_cell() -> None:
)
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.