Splash: the wordmark states its own copyright, and blank is a space
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) <noreply@anthropic.com>
This commit is contained in:
1 parent
c439dec289
commit
ed06492bd0
7 files changed
+27
-23
No files matched your search
@@ -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
|
||||
Binary file not shown.
@@ -24,7 +24,6 @@ from typing import TYPE_CHECKING, Awaitable, Callable, Mapping, Optional
|
||||
from rich.cells import cell_len
|
||||
from rich.text import Text
|
||||
|
||||
from .. import copyright_notice
|
||||
from ..core.config import DeviceProfile
|
||||
from ..core.connection import DeviceAuthenticationError, DeviceCommandError
|
||||
from ..core.device_store import DeviceStore, RememberedDevice
|
||||
@@ -204,7 +203,6 @@ async def _smoke_test(
|
||||
verify(chosen, pin),
|
||||
title="Checking companion",
|
||||
banner=load_logo(),
|
||||
footnote=copyright_notice(),
|
||||
)
|
||||
except DeviceAuthenticationError:
|
||||
# The device answered the scan but won't connect until it's bonded (or the last PIN
|
||||
@@ -214,7 +212,6 @@ async def _smoke_test(
|
||||
error=pin_error,
|
||||
help_text="The 6-digit code shown on the device or in the MeshCore app",
|
||||
banner=load_logo(),
|
||||
footnote=copyright_notice(),
|
||||
)
|
||||
if entered is None:
|
||||
return None # the user gave up → back to the device list
|
||||
@@ -228,7 +225,7 @@ async def _smoke_test(
|
||||
notice.append(str(exc), style="warn")
|
||||
notice.append("\nChoose another device.")
|
||||
await ui.notify_startup(
|
||||
notice, title="Can't connect yet", banner=load_logo(), footnote=copyright_notice()
|
||||
notice, title="Can't connect yet", banner=load_logo()
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -252,7 +249,6 @@ async def _smoke_test(
|
||||
),
|
||||
title="Not a MeshCore device",
|
||||
banner=load_logo(),
|
||||
footnote=copyright_notice(),
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -317,7 +313,6 @@ async def prompt_device(
|
||||
items,
|
||||
default=default,
|
||||
banner=load_logo(),
|
||||
footnote=copyright_notice(),
|
||||
)
|
||||
# Esc (``None``) and the Quit row both mean "leave the picker" — surface that to the
|
||||
# caller as ``None`` so it can exit the program instead of continuing device-less.
|
||||
@@ -500,7 +495,6 @@ async def _add_network_device(
|
||||
validate=_validate,
|
||||
help_text=f"host or host:port — the port defaults to {DEFAULT_TCP_PORT}",
|
||||
banner=load_logo(),
|
||||
footnote=copyright_notice(),
|
||||
)
|
||||
if entered is None:
|
||||
return None # cancelled → back to the device list
|
||||
@@ -550,7 +544,6 @@ async def _remove_network_device(
|
||||
title="Remove network device",
|
||||
confirm_label="Remove",
|
||||
banner=load_logo(),
|
||||
footnote=copyright_notice(),
|
||||
backdrop_items=backdrop_items,
|
||||
backdrop_default=device,
|
||||
)
|
||||
|
||||
+12
-3
@@ -8,8 +8,8 @@ an ANSI-art editor arrives already finished and has no source but itself. Files
|
||||
fresh on every call, so the wordmark can be re-styled by editing the art alone: no code
|
||||
change and no restart of the design loop.
|
||||
|
||||
Two things separate a real ``.ans`` from a text file with colour in it, and :func:`_rows`
|
||||
handles both so the art stays authorable in the tools that drew it:
|
||||
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 a mark we generated
|
||||
@@ -18,6 +18,9 @@ handles both so the art stays authorable in the tools that drew it:
|
||||
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
|
||||
trimmed off in the same breath, being metadata rather than art.
|
||||
* **Blank cells that aren't spaces.** An editor writes an untouched cell as a NUL, and a
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -51,6 +54,11 @@ _SGR = re.compile(r"\x1b\[[0-9;]*m")
|
||||
_EOF_MARK = b"\x1a"
|
||||
_SAUCE_LEN = 128
|
||||
|
||||
#: Cells a DOS console draws blank but a modern renderer would swallow or mis-measure: NUL
|
||||
#: is how an art editor spells "nothing here", and codepage 437's 0xff is a hard space.
|
||||
#: Both become a plain space, so the column they hold survives into the frame.
|
||||
_BLANK_CELLS = {0x00: " ", 0xA0: " "}
|
||||
|
||||
|
||||
def _split_sauce(raw: bytes) -> 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:
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
|
||||
|
||||
+8
-7
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user