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:
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
@@ -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
@@ -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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user