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>
The row `❯` points at borrowed `brand` — so the app's identity ink and
"the row you're on" were the same hue, and worse, teal/cyan is itself a
node hue: a cyan-keyed node's name vanished into its own highlight.
Give the highlight its own name. `cursor` is white on both platforms
(slot 15 on the VT, the top of the bright bank, so the row's bold cannot
promote it further and every dim-slot `not bold` pin inside a highlighted
row goes on meaning what it meant). White sits outside the per-node
spectrum, so it can never collide with an identity; it does share ink
with `you`, which is the accepted cost — spans that set their own colour
keep it over the base, so only unstyled text goes white, and only on the
row already under the reader's eye.
Every surface that draws its own cursor row follows: the select list and
its reorder mode, the feed, chat, routes and actions on the node page,
the composer, records, trace, tx, the walk, the paths screen, the prompt's
suggestions. The composer's insertion-slot chip takes the same white — its
docstring already tied it to "the colour the list cursor below it wears",
and the chip ink was already picked to stay readable on white.
Reverse-video chips keep `selected`: on the VT a reversed white lands on
the light grey the F-key lane already owns, so a white default button
would stop reading as the committing verb.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The 53-column mark's fourth row is the only row in either wordmark whose
colour lands on the dim bank (185;28;28 -> slot 1), and on the console it
came out part red, part light red: the M's right leg, E, S, H and the R's
right leg promoted, the M's left leg, T, E and the R's left leg not.
The split follows the bevels. The mark alternates letter blocks with slate
bevel spans, the bevel is slot 8 -- spelled 90, which on the VT sets the
intensity bit -- and the quantizer handed back a bare 31 for the blocks
after it. Bare, it keeps the bit and renders 91. Every leg that happened to
follow a bevel went light red; every leg that followed one of the row's few
resets stayed red. The .ansi art is one colour throughout; the promotion is
entirely on the way out.
So the fold's quantizer now states its intent the way the theme's dim-slot
styles already must: a dim slot leaves as 22;3N, never a bare 3N. Adjacent
art spans reset nothing between them, so this is the only place that can
know. It fixes the same latent promotion in a map raster, where two
neighbouring cells are exactly this pattern.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JP's spec for the heard-age scale, replacing the four-step ladder: white
under 5 minutes, then yellow at 5m, light red at 1h, brown at 1d, red at
1w, light grey at 1mo, and one cold grey for over a year — which is also
"never heard", since past a year the distinction stops earning a colour.
The `heat.*` styles are renamed for what they describe: a node heard
eight minutes ago is `heat.minutes`, one heard eight days ago is
`heat.days`. The old hot/warm/cool/cold read as temperature but had to
carry an age, and at seven rungs that stopped being sayable. The steps
themselves move into a table, so the ladder is one thing to read.
Three of the seven land in the VT's dim bank, so they say `not bold` — a
selected row would otherwise jump them a rung (brown to yellow, red to
light red), which is the one way a heat colour can lie about an age.
The splash wordmark's vertical ramp becomes white / yellow / light red /
red / light black, each picked to quantize onto exactly one palette slot
(15, 11, 9, 1, 8) while staying a warm fade in truecolour. Its rows are
art rather than renderables, so they had never passed through the fold
and the console was downsampling them on its own — they do now, which is
what makes the slot choice hold.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two reports on the PicoCalc's message paths, one root cause each.
The graph drew an unnamed hop's hash — and the sender's name — as white,
the "you" colour. MapCanvas emits its own bold for markers and labels,
and its colours arrive already quantized to a palette slot; the kernel VT
then draws bold as *brightness*, so anything that landed in the dim bank
was promoted to its bright partner. Light grey became white. A purple
repeater would have become pink the same way. The canvas now drops
emphasis where bold is brightness — the colour is the load-bearing part.
The same trap reaches the theme through inheritance: Rich merges a row's
base style into every span, so a dim-slot span left silent about bold
changes colour inside a selected row. Every dim-slot style now says which
it means — `not bold` to keep its colour, `bold` where the promotion is
the point (only title.muted) — and a test forbids staying silent.
The pathline drew the same hop dark grey, because `muted` collapses onto
the console's dark grey while the desktop's is light. That conflated two
things: `muted` is chrome and may recede a step, but a node we can't
identify is content you can still act on. It gets its own name,
`node.unknown` — light grey, identical to today's desktop muted, so
nothing moves there — and it is what `name_style` returns keyless, what
an unlit `highlighted_hash` wears, and what the `○` ring is.
Also fixes a crash from the previous commit: the graph's label colour
resolved a marker through `parse_hex`, which the type marks' theme names
are not. A repeater relay we couldn't name would have raised.
The gallery's node-detail header spelled a marker hex by hand; it goes
through the app's own styles now, so a palette bug can't hide behind it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A platform's ceiling may change a rule's resolution, never the rule. P3
had picocalc colour node names by *type* instead of by key, so a name's
colour there meant something different than it does on the desktop — the
node's kind, not its identity. Both platforms now run one rule: the key's
first byte picks the hue. The console just spends fewer of them.
node_style gains the binding the heat scale already had. Regular keeps
the full 256-hue wheel; picocalc snaps the same hue to its sixth of it —
the six chromatic bright slots, ~43 first bytes each. It returns the
slot's own hex rather than a color(N), so every downsample on the way out
(Rich's, and the fold's quantizer) lands on that exact slot instead of
guessing a neighbour, and the callers that read an RGB back out of a
style string keep working.
The type marks needed the opposite treatment: their hues are fixed, not
derived, and a naive nearest-RGB match drained the repeater's violet to
grey — a repeater read as an unknown node. They move to the theme's
type.* entries so each platform picks its slot deliberately, and a
braille raster resolves the same entry through theme.mark_rgb, so the
canvas marker and its Rich-drawn twin can't drift.
With names off the type registry, nothing reads it: Platform.name_colour
and core/nodetypes.py go, along with the three reception-layer feeders.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The console keeps its stock kernel palette - black background, classic
PC hues - instead of the custom setvtrgb remap. MESH_THEME_16 is
re-tuned against the stock slots, which hands back the map's own
language for free: clients wear bright pink (13), repeaters purple (5),
rooms cyan (6), sensors brown (3). The cost is one dark grey - muted,
faint and track all collapse onto slot 8. Heat quantizes on plain human
units (15 min / 1 h / 1 day). The custom design is archived, not
deleted: theme._VT_SLOTS_CUSTOM + MESHTERM_CUSTOM_PALETTE=1 on the font
script reinstalls it, with the parity test keeping the two in sync.
Undoing the remap on the device exposed a trap: setvtrgb overwrites the
kernel's DEFAULT colormap, so the OSC palette-reset "resets" straight
back to the custom values - the script's default mode now programs the
stock palette explicitly (verified live; specimen screenshot in
.claude/plans/p3-specimen-stock.png).
The header also follows JP's spec: the handheld brands the app -
"MeshTerm vX" - instead of naming a soldered port that never changes,
and the activity pulse takes whatever room remains.
Also root-causes the intermittent suite flake: Rich memoizes a parsed
Style's ANSI codes on first render (Style.parse is globally cached), so
a raw hex style first rendered by a truecolor console replays 38;2
verbatim on a standard console later in the process. The specimen's
nodes row now wears the type.* theme styles - which is also just
correct: the marks now match the names row. Six consecutive clean
full-suite runs.
Deferred at JP's request: the glyph substitution map (he will redo it),
the F-key lane design (awaits his explicit spec), the CLI fold gap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first P3 (a84abf3) ran on the wrong model and its palette never
reached the TUI: MESH_THEME_16 was written in 256-colour cube indices
instead of the 16 VT slots, and the platform theme pick was wired into
the scripted-CLI console while the real rasterizer stayed hardcoded to
truecolor. Replace it end to end:
- theme._VT_SLOTS: the canonical slot->RGB design (conventional hue
families kept; slots 5/6 become the track/faint slates), MESH_THEME_16
rewritten against color(0..15) with the VT's bold-as-brightness and
dim-bank-background rules pinned by tests. /etc/vtrgb + a boot oneshot
install it (console-font script), OSC fallback when kbd is absent.
- platforms.on_platform: set_platform now re-runs registered bindings,
so theme, rasterizer and widgets swap implementations once per switch
and hot paths read module globals - no per-call get_platform().
- The render-boundary fold (theme.fold_text): one str.translate pass
(NFKD accent folds - the Terminus base has no accented Latin - plus
symbol stand-ins and the width-padded emoji map), an embedded-SGR
quantizer for canvas truecolor, and a ?-at-own-width catch-all that
makes the no-wide-glyph short-circuit true by construction. Applied at
the only two places every visible byte passes: render_to_ansi and
MapCanvas.to_ansi_lines.
- ui/fontset.py freezes the installed font's inventory (dumped live);
the gallery now asserts rendered picocalc ANSI carries no truecolor/
256-colour SGR and no character outside it - the parity gate the
first attempt faked by asserting a dataclass field.
- Font build: nine new 6x12 marks (ellipsis, warn, backspace, shift,
gear, refresh, clock, crosshair, padlock) drawn into donor slots, with
donor misses now fatal and a keeper canary (donating pi once erased
Cyrillic pe - shared slot). Table re-dumped on-device: identical to
fontset.py, 519 = 519.
- Type-based name colouring: registry moved to core/nodetypes (services
no longer import ui), fed from monitor observations, heard_nodes and
merge_contacts. Heat gradient quantizes to heat.* steps on picocalc.
- ui/marks.py: the P2-deferred constant relocation - widgets no longer
drags map_render/mapcanvas/pathgraph for tuples and aliases.
- meshterm specimen: the visual language on one card through the real
funnels - the on-device acceptance screen (JP's request).
- tools/chat: delivered/no-ack state uses the standard check/query
marks, not the banned emoji pair.
Verified on the Lyra: font+palette installed, specimen framebuffer
captures in .claude/plans/. 1259 tests green, 13 xfailed (P6 list).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>