From ed06492bd0a3052bca270705d5e947454637376e Mon Sep 17 00:00:00 2001 From: Jean-Pierre Martineau Date: Tue, 25 Aug 2026 01:15:46 -0400 Subject: [PATCH] Splash: the wordmark states its own copyright, and blank is a space MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mark's art signs itself now, so no splash adds a line under it — the picker, the smoke-test spinner, the PIN prompt, the failure notices, the scan and the settings offer all drop the footnote they were passing, and nothing draws the notice twice. The written About pages keep their {copyright} colophon: that is a page of prose, not a screen with the mark on it. Moebius spells an untouched cell as a NUL, which a DOS console draws blank and Rich measures as nothing at all — so every one swallowed a column and slid the rest of its row leftwards, eighteen of them on the worst line. They are spaces in the file now, and the loader translates any that come back (with codepage 437's hard space) so re-exporting the art can't reintroduce the drift. Those NULs were also the only reason git guessed the file was binary. With them gone, .gitattributes has to say so outright, before core.autocrlf starts rewriting CRLFs that are part of the canvas. Co-Authored-By: Claude Opus 5 (1M context) --- .gitattributes | 6 ++++++ meshterm/assets/logo.ans | Bin 2418 -> 2377 bytes meshterm/ui/device_picker.py | 9 +-------- meshterm/ui/logo.py | 15 ++++++++++++--- meshterm/ui/menu.py | 3 --- meshterm/ui/settings_offer.py | 2 -- tests/test_tui.py | 15 ++++++++------- 7 files changed, 27 insertions(+), 23 deletions(-) diff --git a/.gitattributes b/.gitattributes index 5b549a2..89d67ac 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,3 +11,9 @@ # Patch files are fed to `git apply` against LF sources; a CR sneaking into a context line # is how a patch that applied yesterday stops applying on somebody else's checkout. *.patch text eol=lf + +# ANSI art is bytes, not text: codepage 437 rather than UTF-8, a SAUCE record on the end +# whose FileSize counts them, and CRLFs that are part of the canvas. Git guessed "binary" +# only because the art carried NULs for its blank cells; those are spaces now, so say it +# outright before core.autocrlf starts rewriting a wordmark's line endings. +*.ans -text diff --git a/meshterm/assets/logo.ans b/meshterm/assets/logo.ans index 75d9c9ea4b130ab50e9ead50d112e62820cb6e0d..74a03cb160ee9df9b9a84cced28a133715fd8f38 100644 GIT binary patch delta 373 zcmew)bW&)89}}xVu7ZNXWP4`G$$y!+Cx2j)0Ai8JiiKswskAa}QPG)NSrLWZ@O1H!a6p1hVdTpp_2*fe+JMxX*~pz@Pv z&+dWgn5@Z`%Ma8E6g~kGH8#&xm|VvuHhBU&`{Y|}ypzwfg=&F>txcd-SR0#Ko0#U_ zxdYM!65Rzc3uL|Z#ppov}VCR6HYCZWOyL1BFP^kS7KFDCx+}j|X lKnuVQ18anO;KW%l*aMep^hs!g9}|bMc`gG3gLL#{MP`x7f0=kD_cMu1e#;~=c`?&uPHSTWYZKGl+qWmn zFiTFhVG(5kDW4nwR9e8ovbmXAi%|%!MmpNi+SuIM#9;Df7IQ^ovs?uQ1?gy@hWj7@ zWEte{29h9gknm)7)@D8w%_fs4v4&5+z$(Ul^6c3?P^FVsu_{kiXY=C+x)Er?39tmn z5ryoclV`GJvERN8ws`VcHc2)p(~w tuple[bytes, Optional[int]]: """The art alone, and the canvas width SAUCE claims for it (``None`` when unsigned). @@ -121,7 +129,8 @@ def _rows(name: str) -> list[str]: except OSError: return [] art, width = _split_sauce(raw) - lines = [line.rstrip("\r") for line in _decode(art).split("\n")] + text = _decode(art).translate(_BLANK_CELLS) + lines = [line.rstrip("\r") for line in text.split("\n")] if lines and lines[-1] == "": # drop the trailing newline's empty row lines.pop() if width: diff --git a/meshterm/ui/menu.py b/meshterm/ui/menu.py index 5132d2a..2c9f8d5 100644 --- a/meshterm/ui/menu.py +++ b/meshterm/ui/menu.py @@ -578,14 +578,11 @@ async def _startup(ctx: AppContext) -> bool: baudrate = ctx.profile.baudrate if ctx.profile else 115200 # Enumerate serial ports (instant) and scan for BLE companions (a few seconds), behind # the splash spinner so the wait reads as intentional rather than a hang. - from .. import copyright_notice - devices = await ctx.ui.busy_startup( "Scanning for companion devices…", discover_all(ble=True), title="Select a companion device", banner=load_logo(), - footnote=copyright_notice(), ) # The smoke test opens the radio; on success we keep that live connection and reuse # it for the session rather than reopening (boards often reset on each open). diff --git a/meshterm/ui/settings_offer.py b/meshterm/ui/settings_offer.py index d487e5c..5704edc 100644 --- a/meshterm/ui/settings_offer.py +++ b/meshterm/ui/settings_offer.py @@ -122,7 +122,6 @@ async def offer_remembered_settings(ctx: AppContext) -> None: async def _prompt(ctx: AppContext, drifted: list[SettingDrift]) -> object: """Show the startup offer and return the chosen action, or ``None`` if dismissed.""" - from .. import copyright_notice from .logo import load_logo count = len(drifted) @@ -160,7 +159,6 @@ async def _prompt(ctx: AppContext, drifted: list[SettingDrift]) -> object: items, default=_RESTORE, banner=load_logo(), - footnote=copyright_notice(), ) diff --git a/tests/test_tui.py b/tests/test_tui.py index a4a5808..8da95dd 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -1140,8 +1140,8 @@ def test_device_picker_builds_aligned_columns(tmp_path) -> None: asyncio.run(prompt_device(_Ui(), devices, store, _never)) # The banner (wordmark) is passed through so the splash can draw it. assert captured["banner"] and any("█" in row for row in captured["banner"]) - # A copyright footnote rides along for the splash to render beside the logo. - assert captured["footnote"] == copyright_notice() + # No footnote: the wordmark carries its own copyright, so the splash adds none. + assert captured["footnote"] is None # Each device row's port sits at the same column, proving the name column is padded. rows = [ it.label.plain if hasattr(it.label, "plain") else it.label @@ -1562,8 +1562,8 @@ def test_device_picker_smoke_tests_and_reprompts(tmp_path) -> None: assert store.is_known(devices[0]) -def test_device_picker_shows_copyright_on_every_splash(tmp_path) -> None: - """The copyright rides along the logo on every splash — picker, spinner, and notices.""" +def test_device_picker_leaves_the_copyright_to_the_wordmark(tmp_path) -> None: + """No splash states the copyright itself — the wordmark's own art already carries it.""" from meshterm.core.device_store import DeviceStore from meshterm.core.discovery import DiscoveredDevice from meshterm.ui.device_picker import prompt_device @@ -1591,10 +1591,11 @@ def test_device_picker_shows_copyright_on_every_splash(tmp_path) -> None: return results.pop(0) asyncio.run(prompt_device(_Ui(), devices, store, verify)) - # Every splash draws the logo, so every splash carries the copyright beside it — the - # opening picker, the smoke-test spinner, the failure notice, and the re-opened picker. + # Every splash draws the logo, and the logo is where the copyright lives now — so none + # of them adds a line of its own: not the opening picker, the smoke-test spinner, the + # failure notice, nor the re-opened picker. assert len(footnotes) > 1 - assert all(footnote == copyright_notice() for _kind, footnote in footnotes) + assert all(footnote is None for _kind, footnote in footnotes) def test_device_picker_prompts_and_retries_ble_pin(tmp_path) -> None: