Commit Graph
9 Commits
Author SHA1 Message Date
Jean-Pierre MartineauandClaude Opus 5 8ac0be0928 The battery gauge is a block, filled in eighths
One braille cell was four coarse levels lit in one colour. It is now a
filled block whose two channels answer two different questions.

Colour answers "roughly how much" from across the room: a lit
foreground over its own darker ground of the same hue — light green on
green over half, yellow on brown over a quarter, light red on red
below. The braille fill inside the block answers "exactly how much" on
a second look: each dot row lights left before right, so one cell
carries eight rungs, one per 12.5% of charge. The band holds still
while the dots climb through it, which is what keeps the two readable
as two.

The charging sweep keeps its five whole-row frames and does not colour
itself — a frame of an animation is not a reading, so the band goes on
answering for the real charge underneath it, exactly as the number
beside it does.

That leaves the alarm nothing to say in hue, so it says it in ground:
under 6.25% — half the last dot's worth of charge — the cell alternates
black dots on red with the light red on the bare page. Where every
other cell is filled, a beat with no fill at all is the loudest thing
the palette has. It is drawn a frame at a time because a terminal's own
blink only ever toggles the foreground, and the PicoCalc's framebuffer
console ignores SGR 5 outright — device-checked, along with its want of
bright backgrounds, which is why the grounds are all dim-bank slots.

Neither animation sits behind Platform.effects any more. The header is
rebuilt on the idle tick regardless, so both cost a colour swap in a
frame already being painted, and the one platform the alarm is *for*
was the one platform never drawing it. effects keeps the braille
spinner, which is what it was really gating.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 21:14:07 -04:00
Jean-Pierre MartineauandClaude Opus 5 c66e2c86b6 Frames: the border is one colour again, so drop the corner light
Every framed box was post-processed on the way to the screen: the top-left
corner blended toward white and the highlight decayed along the top and left
edges, so a box read as lit from that corner. It reads as a gradient laid over
the chrome rather than as the box being a box. The borders draw flat now.

The pass goes with it — ui/tui/glow.py, and the three composers that ran it
(base, startup, dialog), each of which also carried the platform gate that kept
it off the 16-slot console where it could only ever hand back the same lines.
Nothing else called it, and its memo went with the module. Text colour is
untouched: the pass only ever recoloured box-drawing glyphs, and titles and
subtitles were already skipped.

The four glow tests become one that pins the outcome instead of the mechanism —
every box glyph in a composed frame, nested panels and dialogs included, carries
the border's own colour and no other. Verified against the old pass: with it
back, that set holds sixty-five.

Platform.effects keeps its other two claims (the header blink, the braille
spinner); it just no longer mentions a pass that no longer exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 01:18:29 -04:00
Jean-Pierre MartineauandClaude Opus 5 3376a18271 PicoCalc: a command row's icon lane is the first thing to go
Every icon on the console is a one-glyph stand-in for an emoji, and the
menu is where that pinches hardest: the main menu drew @ for both Map and
Mesh walk, … for both Live feed and Time machine, and ?? for Trace path,
in a lane costing three of fifty-three columns on every row (JP,
on-device).

So drop the lane there, mark and emoji alike. A command row's icon is
decoration — it names the action's family while the label names the
action — so where it can only be approximated it is worth less than the
cells it takes, and the label and its description get them back. This is
a platform choice (Platform.menu_icons), not a per-emoji one: a glyph
that carries *data* is not an icon in this sense and never goes, which is
why the Channels page keeps its #/🌐/🔒 openness mark, the feed keeps the
class icon it shows in place of the class word at 53 columns, and node
types, status marks and the mute mark in the channel lane all stay put.

Three funnels do the work, so a screen never asks the platform itself:
command_label for a label carrying its own icon, command_icon for a row
that builds its own lane, and marked_label where the icon carried a tint
— a delete row's red moves onto its words rather than leaving with the 🗑.
Section headings go through the same cut (a stand-in beside the ── rule
is pure noise), which also retires the ?? that No revisits was drawing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 02:08:41 -04:00
Jean-Pierre MartineauandClaude Opus 5 ce86e86693 Packet card: two columns back from the gutter, and rssi holds its line
On the PicoCalc the viewer's reception row folded the bare word "rssi"
onto a line of its own. Two things were spending the cells it needed.

The dialog gutter: every floating box left six columns to the backdrop it
sits over. Three a side reads as floating at 72; at 53 the same gutter is
a twelfth of the whole display, and it costs content. How wide the gutter
is worth being is now the platform's call (Platform.dialog_margin), and
the console's is four — still floating, two columns back.

The separator: the row chained its two atoms on the roomy "  ·  " no
matter the lane. That is the header's problem too, and the header already
solved it — roomy by default, compact when the cells run out — so the row
now takes the same trade, sizing itself at render time like the via path
beside it. Two cells alone would have left a two-digit SNR beside a
three-digit RSSI still folding; together they fit with a cell to spare.
The roomy/compact pair moves to ui/menus.py, one definition for both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 17:01:51 -04:00
Jean-Pierre MartineauandClaude Opus 5 935b4fb3b6 PicoCalc: nodes colour by key, like everywhere else
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>
2026-08-07 20:08:31 -04:00
Jean-Pierre MartineauandClaude Fable 5 2f8f77be49 Review round: the standard palette, by JP's call
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>
2026-08-01 14:53:31 -04:00
Jean-Pierre MartineauandClaude Fable 5 84caae53f8 P3 rework: the PicoCalc visual language, done properly
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>
2026-08-01 12:48:07 -04:00
Jean-Pierre MartineauandClaude Opus 5 63cd4e5f96 Performance: SQLite WAL, lighter startup, per-platform tick rates
PicoCalc plan phase P2, driven by measurements taken on the Lyra as a
paired A/B (git archive HEAD vs the working tree, run side by side
against the same venv).

The headline is the database. Every packet the monitor hears is one
durable commit, and on the SD card each was costing 33.78 ms of fsync.
db._tune() opens in WAL with temp_store=MEMORY: 0.59 ms per commit, a
54x cut. synchronous=NORMAL is requested only once WAL is confirmed
engaged - the pragma returns the mode it settled on rather than raising
when a filesystem refuses it - because under WAL it risks the last
transactions to a power cut, and under the rollback journal it risks the
file itself. This device runs on a battery pack.

Startup: bleak/dbus-fast were already lazy; the real finds were two
cheap *values* dragging heavy subtrees. core/connection.py imports one
constant from core/channels.py, which imported PyCryptodome at module
level (224 ms of CPU-feature probing per run) - now deferred into the
two functions that decrypt. And tools/map.py's register_cli, which runs
for every tool at boot, reached into ui/map_screen.py for a --fraction
default, dragging the basemap's urllib.request -> http.client -> ssl
behind it; DEFAULT_VIEW_FRACTION moves to core/geo.py (pure arithmetic,
already on the boot path) and is re-exported so existing importers are
untouched. 26 modules and 316 ms of import self-time gone.

Per-frame: the corner glow only ever recolours truecolor foregrounds, so
on a 16-slot console it scanned every line of every frame to hand back
the identical list - skipped outright where effects are off, along with
the battery blink and the braille spinner cycle. _has_wide_glyph answers
from the platform when no emoji can be drawn instead of scanning the
composed frame (6.5-9.1 ms per repaint on its own). The header built its
segments twice to measure the compact separator, on exactly the narrow
terminals that take that branch every frame; it now builds once and
measures arithmetically. Together: 101.2 -> 73.2 ms per frame at 53x26,
151.8 -> 110.4 at 53x40, and 72x24 unregressed at 91.7.

That finally gave numbers to set the clocks against. refresh_interval
becomes per-platform tick_s (2.0 s on picocalc: a 1 Hz idle tick alone
was 7-11% of a core doing nothing, and only the header's per-minute
pulse drifts, since screens push their own repaints via invalidate).
The 0.12 s spinner was untenable - 84% of a core at 53x26, and at 53x40
it did not fit in its own interval at all. Five duplicate
_SPINNER_INTERVAL = 0.12 constants across the UI collapse into one
platform-aware spinner_interval() beside the Spinner it paces.

The header refactor was verified equivalent over 540 renders, plain text
and style spans both. The span check earned its keep: the first cut
passed style= to the Text constructor, which makes it the object's base
style and blankets everything appended after, so the version string
would have rendered brand-under-muted. test_header.py only asserted on
.plain and never saw it; it now pins the styling too.

tests/test_performance.py pins each contract as behaviour rather than
timing, including a subprocess guard that fails if Crypto, the network
stack, or the map screen ever creep back onto the startup import graph.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 03:40:42 -04:00
Jean-Pierre MartineauandClaude Sonnet 5 8f1fba135e Platform seam: meshterm/platforms.py, --platform flag, dual-platform gallery
One frozen Platform spec (REGULAR/PICOCALC), resolved once at boot
(--platform flag > MESHTERM_PLATFORM env > /proc/device-tree/model
auto-detect > REGULAR) and read via get_platform()/set_platform() -
never a stale `from platforms import PLATFORM`, which the module
docstring calls out. `meshterm platform` reports the resolution and
everything it looked at.

Wires the two correctness bindings that matter before any flavour
work lands: width_reclaim off on PicoCalc's exact-width console
(session._reclaim_last_column, env override still wins), and the
emoji-width calibration probe skipped outright when a platform never
draws emoji.

tests/test_gallery.py renders all 16 screens the interactive menu
opens against REGULAR (72x24) and PICOCALC (53x26 live floor, 53x40
lux case), asserting no line overflows its platform - 35 passing
cases, 13 xfailed as a real P6 worklist (contacts, map, walk,
livefeed, remote_cli, path_composer). Also caught and left alone: the
map screen's footer hint already overflowed the *existing* 72-col
standard before this seam existed, unrelated to picocalc - xfailed
rather than fixed as a drive-by. tests/test_platforms.py unit-tests
the module directly; conftest.py gained an autouse fixture resetting
the platform between tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-01 02:18:22 -04:00