Commit Graph
712 Commits
Author SHA1 Message Date
Jean-Pierre MartineauandClaude Fable 5.1 bca1c83dcd MeshTerm 0.3.4
Installers / windows-x64 (push) Canceled after 0s
Installers / macos-x64 (push) Canceled after 0s
Installers / macos-arm64 (push) Canceled after 0s
Installers / linux-x64 (push) Canceled after 0s
Installers / linux-arm64 (push) Canceled after 0s
GitHub release / build (push) Canceled after 0s
GitHub release / create the release (push) Canceled after 0s
The macOS installers of 0.3.3 never built: the notices step refused pyobjc-core and
pyobjc-framework-libdispatch, whose wheels ship no licence text. Their MIT text is vendored
now, and a test checks every vendored path on every platform.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
v0.3.4
2026-09-13 15:50:53 -04:00
Jean-Pierre Martineau 48f975b33b MeshTerm 0.3.3
Installers / windows-x64 (push) Canceled after 0s
Installers / macos-x64 (push) Canceled after 0s
Installers / macos-arm64 (push) Canceled after 0s
Installers / linux-x64 (push) Canceled after 0s
Installers / linux-arm64 (push) Canceled after 0s
GitHub release / build (push) Canceled after 0s
GitHub release / create the release (push) Canceled after 0s
v0.3.3
2026-09-13 15:44:44 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 8a1bb875a7 On a bordered frame the map's credit moves into the bottom rule, out of the map
The credit was stamped over the right end of the drawing's own last row on both
platforms. That is the traditional corner and it is where a frame with no bottom
border has to put it, but where there *is* a bottom border there is already a rule
under the body doing nothing except closing the box — and a caption set into it
costs the picture nothing at all. JP, looking at the desktop map: "that way it's
not in the map."

So a screen may now hand the frame a bottom_caption, which _panel_box sets into the
bottom rule right-justified through Rich's own subtitle machinery — landing as
"└──… © OpenStreetMap ─┘", one rule cell before the corner, the mirror of the way a
title sits in the top rule. The rule's other occupant keeps its place: the clip
arrows still lead, centred when they are alone and joined by the caption, right, when
both are wanted, because dropping either would be the frame deciding something
neither screen asked of it. The caption joins the base box memo's key as well — the
credit collapses on a keystroke that need not change a single body line, and a memo
blind to it would go on serving a rule the reader has already grown out of.

The map asks attribution for both halves and tests nothing itself: rule_caption gives
the caption where the platform's frame has a rule, map_body stamps the drawing where
it hasn't, and exactly one of them draws. Which it is, is bound once at platform-switch
time rather than asked per paint.

Nothing about the PicoCalc changes — same strings, same corner, same collapse, and the
minimap still stamps on both platforms, having no frame of its own to set anything
into. JP read that form on the device's own panel on 13 September and signed it off as
it stands; the docstring now says so, so nobody goes tidying it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 13:47:28 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 8916544bfb CLAUDE.md states what every file and every build carries: the SPDX line, the map credit, and what NOTICE is for
The three rules the licensing pass settled, pinned where the other binding standards live so
a new file, a new map surface or a new NOTICE paragraph is held to them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 13:21:13 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 3e80074dbf The standalone builds carry their own licences, and everyone else's
The five installers bundled Cascadia's font licence and nothing else --
not MeshTerm's own LICENSE and NOTICE, not the MIT/BSD/PSF notices
Apache-2.0 and those licences all want kept with a redistributed copy.
packaging/notices.py walks the mesh-term distribution's actual runtime
dependency closure through importlib.metadata (markers evaluated for
this platform, the dev extra excluded), gathers each package's licence
text, and fails the build loudly if one is missing rather than shipping
a silent gap -- which is exactly what it found: pyserial and the nine
winrt-* packages bleak's Windows backend pulls in all declare a licence
but ship no licence file in their wheel at all. Their real text, pulled
from each project's own repository, is vendored under
packaging/vendored-licenses/ for those two documented, verified gaps;
anything else missing still stops the build.

The spec generates THIRD-PARTY-NOTICES.txt fresh into the build
directory on every run and bundles it at the archive root alongside
LICENSE and NOTICE. installers.yml uploads the same three files once,
from the linux-x64 leg, so they land beside the downloads on the
release page too, without extracting an installer just to read them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 13:18:35 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 6d7be118b3 The map credits OpenStreetMap on the map, and shrinks the credit once it is used
Only the About page and the README said where the streets came from, which is not
where anyone looking at a map would look for it. OSMF's Attribution Guideline asks
for the credit in a corner of the map itself, visible without having to interact
with it, and lets an interactive map collapse it once the reader pans or zooms so
long as the licence stays findable — an "About option in a menu" is its own worked
example, and we have one.

So the full-screen map is arrived at with "© OpenMapTiles · Data from
OpenStreetMap" — OpenFreeMap's required line, less the half it calls optional — in
its bottom-right corner, and the first key it handles collapses that to "©
OpenStreetMap", which OSMF names as an acceptable form outright. The OSM credit
therefore never actually goes away; the collapse only spends OpenMapTiles' half of
the line. It costs the app no chrome at all: it is stamped over the right end of
the drawing's own bottom row at paint time, so no title atom, no footer character,
no body line, and no reveal key to buy a chip on a full F-key lane. Where the
PicoCalc echoes a find query over that same row the two share it, query left and
credit right, and the query is the half that gives way.

The node page's location preview takes the short form throughout: it is a picture
on somebody else's page rather than a map they went to look at, it has no pan or
zoom for a collapse to trigger on, and the short form is complete standing still.

The About page and the README now name OpenMapTiles too, which neither did, and
the page says out loud that the URL it prints is where the map's corner points —
nothing on a framebuffer console is clickable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 13:15:19 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 e7fb14bed0 The SPDX gate fails on a file it cannot read instead of skipping it, and the changelog says licence
A silent skip would let an unmarked file through, which is the one thing the test exists to
catch. The two new changelog lines now read in the file's own voice, and a docstring in the
logging tests is wrapped under the line limit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 13:15:06 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 ea5b6b2b62 Changelog entries for the MeshCore notice, the dual-licensed generator and the SPDX headers
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 13:09:19 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 37cb6c7c84 The pull-request template says out loud that opening a PR accepts CONTRIBUTING's terms
The relicensing grant binds by conduct only if the contributor had a fair chance to see it, and the template mentioned CONTRIBUTING only inside an HTML comment the rendered PR never shows. One visible checkbox at the top of the checklist.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 13:09:19 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 c9f5a71ce7 Every Python file names its licence with an SPDX identifier
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>
2026-09-13 13:09:18 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 f16a7c4738 The shared font generator is offered under GPL-2.0-only too, so the 6x8 script may run it
The GPL-2.0-only 6x8 script executes the generator block out of the Apache-2.0 6x12 script, and GPLv2 and Apache-2.0 do not mix inside one program. Both files are the author's, so the block between the BEGIN/END markers is additionally available under GPL-2.0-only for that use; both headers and the hardware manual say so. No code changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 13:09:18 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 d078e2379f The XIAO firmware patch carries MeshCore's MIT notice beside it
meshcore-uart1.patch quotes upstream lines and build-firmware.sh produces a MeshCore binary, so the licence those copies travel under now sits beside them as scripts/xiao-radio/LICENSE.MeshCore, verbatim from upstream, and is named from the bench card and the hardware manual.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 13:09:17 -04:00
Jean-Pierre MartineauandClaude Opus 5 266aca08fb Preferences are kept in TOML, like every other file a person edits by hand
preferences.yaml becomes preferences.toml: config.toml and the device-config
backups were already TOML, and tomli/tomli-w already dependencies, so PyYAML
existed for this one file and is dropped. The file keeps its shape - flat
keys, only the overrides, grouped under comment headings with each entry's
help and default - and one name, PREFERENCES_FILENAME, for both call sites.

TOML refuses a whole document over one bad line, so a refused file is read
again a line at a time: a valid line keeps its typed value, a refused one
hands its right-hand side to parse_value as text. One broken hand edit still
costs only its line, and a word left unquoted (log_level = DEBUG) still reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 12:45:22 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 ec21d03c03 The SPI bridge drives openhop_core, and stands its shims down where the library caught up
pymc_core was renamed openhop_core in 2026 and the current meshcore-uconsole
package depends on the new name, so a bridge that only knew the old one failed
its own preflight on a fresh install. It now finds either runtime, newest first
(an interpreter with openhop_core beats an earlier candidate with pymc_core;
MESHTERM_PYMC_PYTHON still overrides), imports through the name it found, and
lets two of its three compat shims stand down under openhop_core, which accepts
long ACK payloads and pushes completed trace replies itself; pushing a trace
twice was the alternative. The raw RX-log push is still the bridge's.

What the port found on the device shaped the rest. The 1.12.0 .deb is built for
trixie against Python 3.13 and cannot run on a bookworm uConsole, so the bridge
looks first in a venv of its own (~/.local/share/meshterm-spi-bridge/venv, made
with openhop-core[hardware]) and, without the GUI's helpers there, configures
the radio itself with the uConsole AIO preset rather than the runtime's
Waveshare defaults (TX-enable on pin 6, no DIO2 switch, no TCXO -- deaf on the
AIO), every MESHCORE_* knob the GUI reads honoured and passed only where the
constructor accepts it. It also loads the GUI's identity key first, so a bridge
in its own venv is still the same node. Verified on the uConsole: openhop-core
1.1.3, radio up, 159 contacts restored, info and contacts answered over TCP,
and the boot service now runs from that venv. The manual and the index say all
of this instead of the rename warning.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 01:29:28 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 097d2ef495 Under --mock, devices lists the simulator and scans nothing
--mock promises a session with no real hardware, and a scan was the one thing
the devices tool did regardless: it walked the serial ports and switched the
Bluetooth radio on, which broke the promise on the first command a stranger
without a radio would try, and which the test sweep had to work around with
--no-ble. Now the simulator is the inventory under --mock: one row with the
flag itself as its target, since that is how this device is selected, and a
stable id of its own. The sweep's workaround is gone and a contract test
replaces both discovery entry points with ones that fail if reached.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 01:16:27 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 356ca6a94a The widgets CLAUDE.md names as THE widgets have public names
A leading underscore says "internal to this file, don't import me", and the
column-age formatter and the node glyph table wore one while seventeen modules
imported them and CLAUDE.md named them as the one place each concept lives.
format_age sits beside format_ago now (prose age, column age), NODE_GLYPHS and
DEFAULT_GLYPH beside them, age_seconds with the ages. A mechanical rename, 140
references across 24 files, CLAUDE.md's three lines with them so the guide and
the code move as one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 01:13:18 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 c974085384 The README holds four screenshot slots, named for what each one shows
Four image links under docs/screenshots/ (dashboard, map, trace, picocalc)
with a comment above them saying what to capture, so taking the pictures is
the only step left. They render as soon as the files exist.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 00:58:10 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 646a0b7b7c Picking a console font is its own preview, and a discard puts the saved one back
The Preferences page loads the font the moment its value is picked, rather than
after Apply: a VT has one font for the whole screen, so the only honest way to
show what the 6x8 build looks like is to load it, and the page is the preview,
repainting at the new row count on the kernel's SIGWINCH. The page tracks what
the console is drawn in right now; staging the saved value again reloads it at
once, Apply keeps whatever is showing (the tool's re-apply after the write then
finds nothing to change), and leaving with a discard reloads the saved font.
Off the handheld the service says no once at page open and is asked nothing
more. Four tests drive the page through each path with the service stubbed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 00:40:55 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 3b67c6813e The PicoCalc's console font is a preference, and the shell gets its own back
A Linux VT loads one font for the whole console, so asking for the 6x8 build
(53x40, fourteen rows more) changes what the shell is drawn in too. The new
console_font preference (6x12 default, 6x8 the alternative, offered on the
PicoCalc only, round-tripping through preferences.yaml on every platform)
therefore comes with services/consolefont.py: remember the font the shell had
with `setfont -O`, apply the one the preference names, and restore it on every
way out -- the menu's finally and an atexit hook, since ^Q and an unhandled
error leave by other doors. Verified on the device that setfont -O captures
the current font with its unicode table and that feeding it back reproduces it
exactly, with no sudo, because the VT is the process's own controlling
terminal. Only the interactive menu switches; a scripted run never touches the
console it was invited into. Changing the preference on its page applies live.

Shipping both fonts needed the licence untangled: the 6x8 base is the Linux
kernel's font_6x8 bitmap, GPL-2.0, so its build moves into
calculinux-console-font-6x8.sh under that licence with the GPLv2 text beside it
in scripts/LICENSE.GPL-2.0. The shared generator stays in the 6x12 script
between BEGIN/END markers and the 6x8 script executes that block, so the marks,
donors and aliases the font contract pins exist in one place. The produced PSF
is a data file the kernel console reads; MeshTerm never links or bundles it, and
NOTICE and the hardware manual say so. PrefSpec gains a platforms gate for the
page; the labels use an ASCII x because U+00D7 is not in the console font.
The 6x8 base has no Cyrillic; the manual says so and the owner accepts it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 23:52:54 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 ffd2649a35 Shared logic sits in the layer that reads it, and the CLI answers --version
A pre-release architecture and QA pass, applied as one change because the moves
share files and only the whole set was gate-tested (2480 passed, ruff clean).

Three new modules pull logic down out of layers that should not own it:
core/channel_probe.py (ChannelSlot, read_channel_slots, probe_channel_slots)
so services/device_state.py no longer imports a screen; services/markers.py
(gather_markers, with MapMarker a core/models dataclass) so two screens stop
importing a tool; core/tracing.py (trace_timeout, path_hash_flags) so
core/connection.py no longer imports services. The routing-prefix width is
DeviceState.routing_prefix_bytes(), replacing five private cross-module imports.

One definition per thing: is_path_hash, the NodeResolver/NameKeyResolver
aliases, TILE_RETRY_SECONDS, haversine_km, marks.SELF_MARK, MENTION; nine
private names that crossed module boundaries got public ones and menus.__all__
lists what other modules import. A preference's default is written once
(ALERT_CAP, DONE_CAP, DEFAULT_VIEW_FRACTION read the registry), the stranded
PREFERENCES doc comment sits on its subject, and three docstrings stop naming
questionary, a library dropped long ago.

The command line gains --version (eager, exit 0, documented, tested) and drops
the -p short form on trace, trace-path and tx-optimize: the root lifts -p as
--profile before a leaf ever sees it, so the leaf's could never work, and a
derived test now checks every leaf against every root short option.
tx-optimize with a one-hop --path is a usage error, exit 2, because nothing
transmitted is not a device failure. Log handlers are closed before they are
cleared (197 unclosed-file warnings gone), chat's fire-and-forget tasks keep a
reference, coroutines use get_running_loop(), the never-awaited MapScreen._load
in a test is awaited, and the CLI sweep passes --no-ble to devices so a test
run stops turning on the Bluetooth radio three times (the suite is 40 % faster).
The about page lists every dependency, and the release skill bumps the README's
download names, which still said 0.2.8.

apply_ops stays in tools/: it and its helpers build report blocks and open two
screens, so moving it to core/ would invert core over ui. The widgets CLAUDE.md
names by their private spellings (_format_age, _NODE_GLYPHS) keep them until
CLAUDE.md is edited alongside.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 19:19:45 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 8b80a88f03 The sdist is the package's source, not the repository's
Hatchling's default sdist is everything git does not ignore, which packed the
Claude skills, the GitHub configuration, the PyInstaller spec, the device-side
scripts and the developer environment template into an archive whose only job
is to build and test the wheel. An exclude list keeps the package, the tests
and their fixture, the manual and the files a build reads. The build backend
floor is hatchling 1.26, the first to write the PEP 639 licence fields into the
metadata; an older one built a wheel that said nothing about the licence.
Workflow comments that reasoned from a private repo's metered minutes are
reworded for the public one, a skill's usage line stops naming one machine's
venv, and the scheduler's lock file is ignored in the tree rather than only in
one clone's info/exclude.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 19:02:02 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 8d3f5d8ed3 MeshTerm on hardware: one manual for the PicoCalc, the XIAO radio and the SPI bridge
docs/hardware.md is the single place a reader goes to put MeshTerm on a
PicoCalc or a uConsole, written in the shape of the command-line manual: what
the three targets are and which exclude each other, why the console font is
built on the device, every phase of the Calculinux bring-up and its knobs, the
XIAO UART radio from parts to a verified link, the SPI bridge with an honest
note that it was written against pymc_core 1.0.x while today's uConsole stack
depends on the renamed openhop-core, and a closing list of what has only ever
been proven on one bench. scripts/README.md and xiao-radio/README.md become
short indexes pointing at it; the old ones were stale in four places (trace
through the bridge works, the firmware env is _serial, two scripts were
unlisted). README links the manual, gains the two dependency rows it lacked
(markdown-it-py, PyYAML) and a fixed fonts anchor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 19:02:02 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 9ebc4bd2be The device scripts say what they do, and the bring-up no longer needs a deploy key
A read-through of scripts/ ahead of the public release, applying only what could
be verified from the code. The setup script clones over public HTTPS when no
deploy key sits at KEY_PATH and keeps the SSH path when one does, and its
timezone is a TIMEZONE knob rather than a hardcoded city. The flasher recognises
the plain XIAO nRF52840's bootloader (PID 0x0044) as well as the Sense's
(0x0045), both from Adafruit's board.h. The mux unit was After= the very target
that WantedBy= pulls it into, an ordering cycle; it now orders on basic.target,
which still wants a check on the device. The console-font header stopped
claiming a default run installs /etc/vtrgb (it removes the custom remap), and
states that the font it builds is an OFL-1.1 Terminus derivative that must not
be called Terminus and that the optional 6x8 candidate embeds a GPL-2.0 kernel
bitmap. The radio-bridge header describes the four phases the script actually
runs instead of a pin-routing phase it never contained. Personal remarks in
comments are reworded impersonally.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 19:02:02 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 72fc0665c5 A record's header reads as a sentence, and the bar says only Record
"🛣 Longest distance — 5th place: 112.0 km": the discipline written the way its board
heading writes it — discipline_label, the mark padded to the measured lane so a
one-cell 🛣 and a two-cell 🎯 start their titles in the same cell — then the standing
and the metric. The bare mark-and-number it replaces was dry, and it had to lean on the
title bar for the discipline; now the header names the record and the bar names the
kind of page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 13:38:40 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 6e84c7fe54 A record's metric heads the page, above the tabs, under its discipline's mark
The node page's identity header, for a record: one line above the strip carrying the
discipline's own mark (the one its board heading wears) and the score in the
discipline's unit, which is the one thing the record is — the title bar already names
the discipline and the standing. The score lane that repeated it on Info is gone, and
the metric's spelling is one function, record_score, shared with the board's score
lane so a partial Longest-distance walk reads ≥ in both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 13:22:45 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 aae3d3f0fe A record's tabs are Info, Route and Area, organized like the node page
Info is the record's stats with the page's actions at its foot, the cursor opening on
Trace; Route is the walk two ways — THE route graph, its key, the route line — with
Trace this path as its one row, so Enter there walks the route on show, the node page's
rule for its Routes tab; Area is the ground drawing, unchanged. Each tab's stage is
composed once per width and cached, and the footer names only what the tab answers: a
lone action row earns no ↑↓ atom. The gallery gains the Route tab as a specimen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 13:13:01 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 ea8ae61812 A record is a tabbed page: Stats, then the walk's ground on an Area tab of its own
The node page's shape (widgets.tab_strip, Tab/Shift+Tab, the next tab on F3): Stats is
the record read — its stats, identity first, then the route two ways — with the action
rows at its foot; Area is the ground drawing across the whole stage the viewport leaves
under the strip, so the shape gets every row and column the page has rather than the
block a stacked layout could spare it. The Area tab is offered only for a walk with a
drawable shape; a record without one keeps a lone-tab heading and nothing to switch. A
page too narrow to draw on says so instead of squeezing.

widgets.tab_air is the strip's platform-bound air, hoisted from the node page so both
tabbed screens spend it the same way. The gallery gains the Area tab as a specimen on
both platforms.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 04:00:22 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 197af90c8e A share code is centred as a block, never justified — the bottom finder was skewed
Rich's centre justify strips each row's trailing spaces before it pads it, so a
code row whose right edge was light modules lost cells and landed a column off
its neighbours; the bottom-left finder square came out one cell skewed, and a
camera could not lock on. The About pages' codes were never centred, which is
why only the full-screen share codes broke.

A code is now moved across a line one way only: the same indent in front of
every row, computed once from the fitted code's width. The test pins that all
rows share a width and every finder starts in the same column.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 03:29:46 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 4b605c1da7 The record page reads by its sections, and the ground drawing spends the page's width
Three body headings — Stats, Area walked, Route — each with the note that used to be a
faint caption under its block, so a page this tall can be skimmed by its landmarks. The
record's identity leads its stats: the spec walked and when it was set, then what the
walk measured. The area drawing takes the page's whole width and rows in step with it,
a quarter of the columns (half as tall as wide in braille dots), so a wider page is a
bigger shape rather than a padded one; rows a flat shape leaves blank are still
trimmed, so a tall box costs it nothing.

widgets.body_heading is THE form for a heading inside a page's body — accent title,
muted note — hoisted from the Time Machine's private copy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 03:10:01 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 b56987b24b A QR code is white on black, and the whole screen when it is the answer
A camera reads contrast, and a light-on-dark code is what a scanner expects
of a screen, so every code MeshTerm draws is now pure white ink on a pure
black field (the theme's `qr` style, both ends named on both themes), never
black on white.

The share views — a channel's share link, a contact card, the CLI's share
commands run in the menu — are no longer a titled popup with an instruction
over the code. They are `QrScreen`, a **bare frame**: `Screen.bare` and
`frame.compose_bare` draw the body alone on blank rows, with no header, no
footer, no title bar, no box, no hint and no F-key lane, and nothing on it
but the code and the URL it encodes, sat a little above centre. Esc leaves.

The code fits itself to the frame every paint, width and rows both: the
standard fit first, then a lighter error level, then a narrower quiet zone,
before it would ever be cut — a contact card is 57 cells and 29 rows at the
standard fit, the PicoCalc is 53 across and a regular terminal 24 tall. When
code and URL can't share the frame the code is whole at the top and the URL
a page down. The frame states its height before it asks for the body, which
is what lets the screen size to it.

A `qr` fence in a written page is the one exception and keeps its place in
the prose — the About pages' codes flip to white on black like the rest.

Found on the way: the PicoCalc's SGR quantizer matched a colour only when it
was a whole sequence, and Rich writes a style's foreground and background as
one sequence (`ESC[38;2;…;48;2;…m`), so a white-on-black module reached the
console in truecolor. It now walks every sequence's parameter list.

The standard is written into CLAUDE.md under Screens and lists; the gallery
carries the share screen as a specimen on both platforms.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 03:05:34 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 121f75c2a0 A record's ground is drawn under the stats, each pin labelled with its hash byte
The area drawing rode beside the stats in a third of the width, which squeezed the
shape and left no cell for a label on any pin. It is a block of its own now, between
the stat lanes and the route graph — page-wide up to a cap, a fixed eight rows, north
up, the shape against the left edge under the left-aligned stats — and every hop's pin
carries its first hash byte, placed the way the map places a name (beside the pin,
clear of the drawn lines where it can, dropped rather than overprinting), so a pin and
the graph node under it match by eye. The star stays bare: it already says who that is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 02:50:50 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 07a566d49d A trophy case record is a page, not a card floating over the list
The reader is in a record — reading its stats, scrolling the route, arming a trace from
it — so it fills the frame like the browser under it and Esc reads back. It used to
float as a 62-column card, which read as a question over the list rather than as the
page it is. RecordDialog is RecordScreen, full-frame, and the reading width it asked for
as a dialog goes with the name.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 02:35:52 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 6bdfa26160 The trace target picker is a popup, gone before the trace screen opens
The third lead-in picker kept pushed as a hub, and the one the ledger said would stay:
a contact list under the trace screen, so Esc out of a trace landed on the list — two
places where the trace is the whole visit. It is a question, and asks like the other
two: the contact list floats over the menu as a one-shot (run_dialog, public now; the
list stays a full-screen place everywhere else), the trace screen opens over the menu,
and Esc from it lands there. The typed fallback floats the same way instead of filling
the frame.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 02:25:17 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 83e2aa80ae The trophy case fills the frame whichever way in
A bare SelectScreen floats by default, and the browser was full-frame from the menu
only because nothing sat under it; opened from a trace, over the still-pushed trace
screen, the same page came up as a content-sized box. It is a place, not a question,
and declares so like the contact list does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 02:21:42 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 1f34d35d2a Survey ledger: the base under a popup is the menu
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 02:16:13 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 b1dbbe5a5a A popup with nothing under it floats over the menu, never over an empty frame
The menu is popped while a tool runs, so a lead-in question — the TX optimize link, the
node to manage, the password that follows it — had a blank base pushed under it to draw
as a box at all. A dialog is smaller than the frame, and what shows around it should be
the page it was reached from: the root the menu already declares for ^W is now the base
_floated pushes on an empty stack, and the reconnect dialog draws over it too, so the
menu loop stops forgetting its root on the way out. A session with no root declared
keeps the blank fallback.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 02:16:01 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 13f0c647de The navigation standard says what a popup is: a question, not a place
Two flows were rebuilt on it today and nothing had it written down — the stack is for
places, a popup informs, confirms or asks and is then gone, and a question draws as a
box even as the only frame on the stack. The entry-chain rule gains its list form,
run_wizard, one popup turning its page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 02:05:49 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 a8b69efa73 The repeater admin's node picker is a popup, gone before the admin page opens
The same shape TX optimize just lost, with one popup instead of two: the node list was
kept pushed as a hub under the password prompt and the admin page, so Esc from the page
landed on the list it was picked from — two places where there is one. A popup asks and
is gone: the pick is now a floating one-shot (session.select gains text's floating
flag, so a lead-in question draws as a box even as the only frame on the stack), the
admin page opens over the main menu, and a refused login re-asks the question with the
same node highlighted. admin_node_visit is deleted; nothing keeps a popup pushed now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 02:05:21 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 222679c5a3 The stepped dialog draws as a popup, not as the frame
The menu is popped while a tool runs, so a floating screen that is the only frame on the
stack was painted as the full-frame background — the TX optimize link dialog opened as
a page and only its sub-prompts looked like popups. The blank base _run_dialog_screen
puts under a one-shot dialog is now one context manager, and stay(dialog=True) puts the
same base under a popup's whole visit, which is what run_wizard asks for.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 01:58:38 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 930596130f The TX optimize link is picked in one dialog that turns its page, gone before the sweep
Two pickers stacked one over the other, each kept pushed under the sweep as a hub, read
as two places to be — and a popup is not a place: it asks, confirms or informs, then it
is gone. The node to tune and the node to measure at are now one floating list that turns
its page ("… · step 1 of 2"): Esc on the second step turns back to the first with the
picked node still highlighted, Esc on the first leaves, and the box is popped before the
sweep screen opens, so Esc from the sweep lands on the main menu.

menus.run_wizard is the dialog form of run_steps — the same stack semantics drawn in one
box — and SelectScreen.turn_page is what it turns on: the opposite claim from
replace_items, since a page turned is a new question and neither the typed filter nor the
highlight rides along. A step with nothing to list (the typed hex prefix when the tuned
node is the only contact) runs as an awaitable floated over the box's last page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 01:52:37 -04:00
Jean-Pierre MartineauandClaude Opus 5 e24e41178a The delay settings round to one decimal, on the repeater admin and device config
TX delay, direct TX delay and RX delay show, stage and send at one place instead
of two. Device config's RX delay had no rounding at all, so SettingSpec gains
the same decimals field the remote catalog carries, applied on parse and display.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 01:19:30 -04:00
Jean-Pierre MartineauandClaude Opus 5 09956c764c MeshTerm 0.3.2
Installers / windows-x64 (push) Canceled after 0s
Installers / macos-x64 (push) Canceled after 0s
Installers / macos-arm64 (push) Canceled after 0s
Installers / linux-x64 (push) Canceled after 0s
Installers / linux-arm64 (push) Canceled after 0s
GitHub release / build (push) Canceled after 0s
GitHub release / create the release (push) Canceled after 0s
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
v0.3.2
2026-09-11 04:29:19 -04:00
Jean-Pierre MartineauandClaude Opus 5 7afd231bc7 A read-logs skill turns the log into incidents before anyone reasons about it
The log misleads read raw: a count hides its span (204 identical records that
were three seconds), a failed CLI run is logged twice, the suite's runs from
before 59f10f9 sit in the real file, and the runs table keeps UTC while the
log keeps local time. logscan.py groups tracebacks, warnings and the DEBUG
failures that lost their stack, each with first and last; folds the CLI's
reports onto the run they repeat; hides fixture and other-database runs and
says how many; and joins the runs table by run id. SKILL.md is the method:
pin the question, fetch the log (this machine, the PicoCalc, an attachment),
summary then around/run/show, the reading traps, and checking HEAD before
calling a finding live.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 04:29:19 -04:00
Jean-Pierre MartineauandClaude Opus 5 10f48dbffc A relay chain opens and closes on the chevron, since its ends are not the route's
A packet's via field names the repeaters that forwarded it and neither the
node that sent it nor the one that heard it, yet its ribbon opened and closed
square, the mark that says the route began and ended there. PathLine takes
from_origin / to_destination: a cleared end opens on the notch or closes on
the point, and arrow mode spells it with a bare leading or trailing arrow.
The packet viewer's via chain and the TX sweep's composed relays draw both
ends open. Under the chain, the "reception describes..." note gives way to
the hop count atom the node page and trace scenarios already carry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 04:29:19 -04:00
Jean-Pierre MartineauandClaude Opus 5 e5231687de The location picker opens at once instead of waiting on the radio for contacts
Pick location on map gathered its surrounding nodes before opening, and that
meant reading the contact list from the radio whenever the session hadn't
cached it. A companion refusing that read (ERR_CODE_BAD_STATE, which the log
shows all session) held the picker closed through every retry, twenty-odd
seconds, only to open with no contacts anyway. Our own position waited the
same way after Device config dropped its cached self-info.

The picker now opens on what is already in hand: cached contacts and
self-info (DeviceState.peek_contacts / peek_self_info) and our own history,
via gather_markers(wait=False). Whatever the cache lacked is fetched behind
the open map and joins it through MapScreen.set_markers when the radio
answers. The read is never cancelled, so a fetch another screen shares
survives the picker closing. The Map tool and the node page keep waiting,
unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 03:58:46 -04:00
Jean-Pierre MartineauandClaude Opus 5 74eabff25f A repeater setting its firmware lacks reads n/a, not ?
v1.15 matches get keys by prefix, so a key it lacks falls into a shorter
sibling's branch: get radio.fem.rxgain answers with the get radio line, and
gps on a board without its receiver answers "Can't find GPS". Neither is an
error string and neither parses, and an unparseable reply left the cache
alone, so those rows stayed on ?, which says never asked. Any reply that
can't be the setting's value now marks it n/a; only no reply keeps ?.

The fold itself rode along in 564ac0e; this adds the regression test, the
catalog's note on prefix-shadowed replies, and reflows a docstring line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 03:42:43 -04:00
Jean-Pierre MartineauandClaude Opus 5 564ac0e6ac Both editor pages pick a location on the map above separate coordinate rows
Device config folded latitude and longitude into one Location row behind a
Pick / Type / Clear dialog. It now lists them as two rows, as repeater admin
always did. Above them, on both pages, a "Pick location on map…" row opens
the map picker and stages both coordinates at once. The map opens on the
position as staged, or as the device (or node) last reported it. It opens on
the mesh where there is no fix (0, 0 or unread), via map_screen.coords_or_none.

On repeater admin a picked coordinate is stored the way a CLI read would spell
it, so picking the spot the node already holds unstages rather than queueing a
no-op set. Typing a pair and the Clear button went with the dialog. Each
coordinate is typed on its own row now, and 0 in both still means no fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 03:39:39 -04:00
Jean-Pierre MartineauandClaude Opus 5 f27492b2e4 Device config carries the device actions and every setting the firmware has
The Device actions menu entry is gone: clock sync, backup/restore, the identity
key, reboot and factory reset are an Actions section on the Device config page,
which now takes the repeater admin page's shape. It is full-screen, titled with
the node's name and a staged count, and Apply sends in place, one value at a
time; a value the device refuses stays staged with its reason. Read settings
re-reads the device, and unread and blank values read ? and empty.

The settings were checked against the companion firmware (MyMesh.cpp). New:
client repeat, the radio command's trailing byte. Every radio edit now restates
it, because the firmware reads a missing byte as off and a retune silently
stopped a relaying companion. Also new: the auto-add hop limit, read off the raw
frame the library half-parses, and a row for each custom variable the device
reports. The firmware's own bounds now apply: a PIN of 0 or six digits,
path-hash modes 0-2, telemetry deny / by contact / allow all, 150-2500 MHz,
7-500 kHz, and TX power from -9 dBm.

Neither editor page slides its rows sideways any more. The Actions rows pin a
head block, which on its own turned on horizontal scrolling for repeater admin's
whole list and slid its setting labels out of view. SelectScreen(hscroll=False)
now holds against the rows, and both pages use it, so long rows end at the edge
the way Device config's always did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 03:23:21 -04:00
Jean-Pierre MartineauandClaude Opus 5 c9325d0c5d Repeater admin is a full-screen page, not a popup over the node picker
The editor floated as a content-sized box over the picker it was opened from,
spending the frame's width on a backdrop while the reader worked through a
whole node's catalog. It now fills the frame like the local Device config
editor it mirrors; the picker, value prompts, confirms and progress dialogs
still float. The gallery gains a repeater_admin case at both platforms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 02:40:08 -04:00
Jean-Pierre MartineauandClaude Opus 5 61dc9dc110 Every icon-led list measures its icon column, so no label starts a column early
The terminal draws some icons in one cell (↻ ⌨ 🗑 ✓ ✗ ⚠) and most emoji in two, so
a row written icon-plus-space started its words a column left of its siblings. The
fix kept landing one screen at a time. menus.align_icons now pads every icon in a
list out to the widest one, and menu_rows applies it itself, so a list built there
cannot misalign.

Hand-built lists are brought along: the courier outbox and entry actions, the
watchtower actions and rule popover, the record card's actions, the main menu's
Quit row and the device splash's action rows. The repeater admin's regression
test, dropped by a concurrent rewrite of its file, is restored.

tests/test_icon_alignment.py sweeps every gallery screen on both platforms and
fails, naming the rows and their start cells, when a list's icon-led rows start
their words in different cells.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 02:02:05 -04:00