The star of our node had the hue of our key, set from the self-info at run
time. It is now the white of "you" on each surface: the map, the routes,
the lists, the legends, and the chat. The run-time colour, the device-state
hook, and the style generation of the render cache are removed. A faded
star keeps its grey.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The star of our node was the yellow of the map. SELF_MARK now names a
theme style, you.mark, and the theme writes our key's hue into it when the
device-state cache reads the self-info. The context makes that connection,
so the services still do not import the UI. The map, the routes, the lists,
the legends, and the chat all draw it, and the render cache keys on a style
generation so no old bytes stay. A faded star keeps its grey.
In a chat, a sender or a mention with no key had the light blue keyless
chip. It is now quiet: the grey of plain text on that grey darkened by the
chip rule. Routes keep the keyless chip, arrow mode keeps node.unknown, and
the PicoCalc keeps the keyless pair.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Our sender chip is our name, turned over, with the star on the page to its
right. A mention of us has the turned-over chip and no star: the message
that names us gets the star at the left of its time, so every time moves
two cells right, and the Messages list puts it at the left of the row's
age. A mention chip drops its @ where chips are drawn.
The compose line draws each complete @[Name] as its chip and treats it as
one atom: the arrows step over it, and Backspace or Delete removes all of
it. A reply inserts its mention at the cursor, so one message can name
several nodes. The prompt and the typed text are white.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A message goes from its author toward the nodes it names, so the two chips are
the two ends of one route that is not drawn. name_chip takes a role (ORIGIN or
DESTINATION); arrow mode ignores it, so a degraded terminal draws exactly what it
did before. A chip in a message body never breaks across lines: render_hanging
holds each whole run together, spaces included. _sender_style lost its last
caller and is gone; its tests now read the rendered chips.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
The same rewrite as for the core package. A navigation entry is now
"a screen on the stack", the visible part of a page is its viewport, and
the visible rows of a ListWindow are its list window. Two statements now
agree with the theme: the selected chip style is a grey fill.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A URL too long for a message's hanging lane steps out to the timestamp's
column on a line of its own, whole, since a terminal opens a link, and a
reader copies one, only while it sits on one line. Only a URL wider than
the screen still folds. ^U on a picked message opens its links on the
share screen, one code at a time, and with several, the arrows step
through them, the URL line flanked by a lit or dim arrow each way. On the
PicoCalc the lane carries it as QR, behind Day down.
CLAUDE.md also states the shortcut rule: a bare letter only on a screen
that takes no typing, a Ctrl chord everywhere letters are text.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Apache-2.0 does not require per-file headers, but a file lifted out of the tree carried no marking at all. One comment line, first (or right after a shebang), on every .py under meshterm/, tests/, packaging/ and scripts/, plus the extensionless SPI bridge; tests/test_spdx.py keeps a new file from landing without one.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`ruff format .` across the repo: 153 files, no behaviour change. This is the
second gate CONTRIBUTING.md promises, and until now it reported 154 files needing
reformat — so a first contributor ran the command the guide told them to run and
could not tell their own diff from the background.
Landed on its own, with nothing else in it, because a whole-repo reformat is only
safe to skip past in blame if it is provably nothing but layout. It was checked
that way rather than assumed: every changed file was formatted in memory and
compared by full `ast.dump()`, and 152 of 153 come out structurally identical
(the one exception was a docstring opening on a quote character, fixed in the
commit before this one). The string and comment token streams were diffed
separately, so a quote flip or an edited comment could not hide inside a
re-layout — five quote-style normalisations and sixteen implicit-concatenation
joins, all value-identical, and one commented-out line gaining the space after
its `#`. `meshterm specimen` is byte-identical on both platforms, and the suite
is 1888 green, gallery gate and `test_theme16` included.
What actually changed, in order of how much of the diff it is: wrapped calls and
signatures rejoin onto one line where they fit inside the hundred columns; the
magic trailing comma expands what it is there to expand; complex slices gain
their spaces (`[len(noise) :]`); a few redundant parens go. The tables that are
laid out to be read were fenced in the previous commit and are untouched here.
The aligned trailing-comment column is gone from about ninety lanes that were not
fenced — including the `heat.*` ladder in `MESH_THEME`, which cannot be fenced
where it sits. Every comment stays on the entry it describes, so nothing there
reads wrong; it just no longer lines up.
Add this commit to `.git-blame-ignore-revs`, which follows immediately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`ruff check --fix` over the whole tree, ahead of putting the linter in CI:
1977 mechanical fixes, overwhelmingly `Optional[X]` → `X | None` (1186 in the
package alone) and the quoted annotations that `from __future__ import
annotations` already made unnecessary, plus import sorting and unused imports.
Nothing here changes behaviour — the suite is 1885 green before and after.
Four findings were real rather than stylistic and are fixed by hand: a dead
`cells` range in `MapCanvas`, a dead slot dict in the heat-ladder test, a
`ProgressScreen` task handle nobody read, and a tab loop binding a `label` it
never used. The two `F821`s were quoted forward references to names imported
inside their function bodies — now declared under `TYPE_CHECKING`, which keeps
`test_performance`'s local import (it guards the startup import graph) while
letting the linter resolve the name.
412 findings remain and are deliberately untouched: they need a decision about
the rule, not an edit to the code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every node in the suite was named after hardware on JP's desk: YUL-Cartierville
and YUL-Poly are repeaters he runs, two more were the BLE
companions, another his own handle, and a long tail of YUL-Far / YUL-Me / bare
YUL besides. A stranger reading the tests learned his callsign, his city and his
gear before learning what the tests check.
They join the family the suite had already invented for exactly this — Alice,
Lounge, Yagi-Repeater, hub-1: Hilltop-Repeater, Lakeside, Waymarker, Homestead,
and Hub for the 3d-keyed node. Hub is not a new name; test_device_config already
called that contact Hub, so the suite was carrying two names for one fixture.
Replacements are the same length as what they replace, so every width budget,
truncation assertion and gallery line is unchanged. The two map tests that type
a literal query picked `yu`/`yul` to match the old name; they type `hi`/`hil`
now, same length, same single match.
Left alone on purpose: the About page and the donate link, where the handle and
the repeaters are the point, the hand-drawn splash art, and the XIAO README
reporting what JP's own radio advertises.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A picked row folds its node hues to the cursor white so the row reads as one
thing rather than as a name arguing with its own selection. On a path line that
is wrong: there the hue is not decoration on a name, it is what tells one hop
from the next — and what a route graph above the row is cross-referenced by,
its labels being a marker and one byte of hash. Chips were never folded (their
fills are outside the fold's vocabulary), so the bug was the arrow form: every
path line on the PicoCalc, and on any terminal without the powerline glyphs.
PathLine now stamps every shape it mints with PATH_INK, a style that draws
nothing; Text.append_text carries the stamp into the row as a span over the
hops, and the fold skips identity spans lying inside one. So the Message paths
row keeps each hop in the colour its graph label wears, and so do the TX
screen's "Route — via", the trophy board's walk and the feed's sender → recipient
lane. The node detail's Routes tab, which bought this by hand (no cursor base at
all), is untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Teal was still answering three questions it had no business answering. The wordmark's
colour is the app's identity, and it is also a node hue — so a cyan-keyed node could
vanish into the very highlight pointing at it.
- A node's key-derived hue folds to the cursor white on the cursor row. A row is a
column of identities right up until it is the row you are on, and then it is
answering a different question; a hue on it argues with its own selection. Folded
once at the render boundary for any Text whose *base* style is `cursor` (which is how
every list and every screen drawing its own rows already marks it), so no screen has
to remember the rule. `theme.is_identity_style` is node_style read backwards — every
hue a key can mint, on both platforms — and only those fold: the heat of an age, an
SNR reading, a red badge, a path line's chip fills all keep what they were given. So
does the grey of a node no key could place: the highlight knows where you are, not who.
- The reverse-video chip — a dialog's committing button, a running action's Abort, the
arrow-mode composer slot, the editor cursor — is grey. A chip marks where a press
lands. It is chrome, and chrome does not get a hue. Light grey slot 7 on the VT: the
one grey a reverse can put behind text there, and the fill the F-key lane already uses.
- The active sort column's heading and triangle take the same white as the cursor row,
being the same claim one axis over. They were a bare `#22d3ee`, which put a selection
cue back on the node spectrum and, on the console, straight onto the brand's own slot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The map and the mesh walk echo the query you are typing in their footer
hint. The PicoCalc draws no footer hint -- the F-key lane took that row
-- so on the device both screens silently filtered themselves while the
reader had no way to see what they had typed.
The app already had an answer: the select list and the path composer
each draw the query as a warn-styled /query line above the rows it
narrows. That is now render.query_line, one definition, and the map and
the walk draw it too -- but only where the footer isn't, so the desktop
is untouched and no screen ever shows the query twice.
The row it costs comes from what the query narrows: the walk's link
list, the map's canvas. The map rebuilds its viewport a row shorter
through the ordinary resize path, so beginning a find loses a strip of
ground along the bottom rather than pushing the last row out of frame.
base_body_size was still mirroring only compose_base's bordered branch,
so a screen sizing itself against it left one row of a borderless frame
permanently blank -- which is the row the echo now wants back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every repaint re-rasterized every row and re-ran every layout; every
screen open re-scanned the whole history. Now work is done once per
change instead of once per frame:
Render pipeline — a content-keyed ANSI cache in render_to_ansi (every
Text/str row app-wide), memoized corner glow, and panel-wrap memos for
the base frame and stacked dialogs, all dropped on a platform switch or
emoji recalibration. Idle-frame compose falls 5-100x per screen
(contacts 5.6->0.1 ms, record card 8.5->0.07 ms, trace 7.6->0.6 ms).
Repository — an observed_at index turns every since-window query into a
range walk (recent_observations 7.5->1 ms); heard_nodes and first_seen
aggregate in one streaming pass by ISO-string comparison instead of
hydrating dataclasses and sorting; node_names groups in SQL (NOT
INDEXED past a slow planner choice).
Per packet — livefeed no longer repaints on events that drew nothing,
the channel list's stats TTL rises above the tick so the repository is
not queried per frame, watch-store alerts batch through the throttled
flush instead of a full-state disk write per new node, and the header's
360-bucket histogram/flags tuples are memoized per minute and packet.
Per open — the resolvers, topology canonical()/display_name(), and
Yen's route search memoize over their frozen inputs; timemachine, chat,
and livefeed drop duplicate queries; the config editor, device actions,
info, advert, and tx-optimize reuse the devstate session cache instead
of re-asking the radio.
Per frame, per screen — dashboard folds five window scans into one
digest; walk caches everything derived from its frozen graph (the
onward-count quadratic included); packet viewer, record dialog, trophy
rows, trace stats/log, and the node-detail route fan memoize on their
frozen inputs, invalidated by revision counters where the input can
move.
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>
The five "pre-existing" test failures (four corner-glow, one message-
paths muted-hash assertion) were all one thing: under a NO_COLOR
environment, Rich's console honoured it inside the TUI's headless
render bridge and stripped every named-style colour — but that console
is a rasterizer whose ANSI prompt_toolkit re-parses, not terminal
output, and the TUI's hues are semantics (node identity, recency
heat), not decoration. The bridge now pins no_color=False; the
scripted CLI's real console (theme.build_console) keeps honouring
NO_COLOR, where it belongs. Nothing was wrong with the tests or the
hash-widget commit they seemed to implicate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ^P view grows from a static scroll window into its own dialog. Up
top, every distinct path the message took drawn in the atlas's edge
style — origin left, us right, the final relay → us link coloured by
that path's reception SNR, earlier links slate — with markers for every
node but labels only along the currently selected path, so a many-path
graph stays readable; the selected path draws bright, the rest dim.
Beneath, one row per arrival with a selection cursor: ↑↓ move it (the
graph follows), and a row longer than the dialog scrolls horizontally
with ←→ — the whole line shifts under an edge …, and snaps back the
moment the selection moves on. crop_cells in tui.render is the new
cell-exact window primitive behind that.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two rounds of interactive-polish fixes across the dashboard, Time Machine,
chat, and the menu popups.
Dashboard
- Feed name lane names what a row actually is: a channel message's parsed
sender, or a relayed packet's payload class (channel text / trace / …),
instead of a bare "?" or a redundant "ch N". The class is persisted so
history-seeded rows carry it too (observations.payload_typename, schema v10).
- PgUp/PgDn walk the feed selection so the highlight travels with the view.
Packet viewer
- Takes a live source and re-locates the viewed packet by identity, so packets
arriving after it opens become reachable; the n/total title stays current.
- Decrypts overheard channel text straight from stored history (crypto fields
persisted: chan_hash/cipher_mac/crypted, schema v9).
Time Machine
- Day charts label their bars with column ticks: each date sits under its own
bar with a ┬ notch, shortens (month only on the first tick and on rollover),
and thins to fit. axis_chart grows an optional ticks path for columnar charts.
- Node history charts render through the shared axis_chart widget; the mesh
Rhythm is 96 fifteen-minute slices at 4x width, "now/newest" hints dropped.
Chat
- The byte counter pins to the right edge of the input's last line, so it stays
bottom-right when the compose wraps instead of trailing the cursor.
Menu popups
- The per-node Rules popover gets an explicit Close row.
- The Send advert popup drops the emoji from its floating title (a terminal that
paints it narrow left the content-sized dialog's border short, bleeding the
backdrop through); the menu row keeps its icon.
625 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrapped message bodies now align their continuation lines under the body
of the first line instead of falling back to the timestamp gutter, via a
new render_hanging helper. And an @[Name] token primed by the reply flow
renders as a bare @Name in that sender's stable hue, so a mention reads
cleanly and is tied to the person it names.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename the package directory, imports, brand strings, env-var prefixes,
console-script entry point, and the config/data dir (~/.config/meshterm).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>