Each test docstring states the behaviour that the test proves, in the
simple present. No assertion message, test id, or other string literal
changed. Four statements now agree with the code: the Trace highlight
stays on the last row, a menu count, and two others.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
M5Stack makes three Cardputers and only the Zero runs Linux; the PicoCalc
takes several cores and only the Luckfox Lyra runs MeshTerm. So the bare
names were ambiguous, and every reference to the two devices now carries
the variant: the --platform values and MESHTERM_PLATFORM, the Platform
constants (PICOCALC_LYRA, CARDPUTER_ZERO), their lane decks and every
screen's picocalc_lyra_lane / cardputer_zero_lane, the font inventories,
the lane chips' theme styles, the scripts/ folders, the two chrome test
files, and the docs and CLAUDE.md that name them.
There is no alias: --platform picocalc is refused with the three real
choices listed, rather than falling back to the desktop layout. The
PicoCalc is auto-detected from its device tree, so it needs nothing.
What names hardware or a user's data keeps its name: the PicoCalc
keyboard's input-device name, its battery's sysfs supply, and the serial
profile the Lyra setup script writes. .gitignore follows the firmware's
folder, and the changelog keeps the words each release used.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A muted line under the Weekly advert row on the Preferences page, set in
the value lane: "next advert in 4d" while the week runs, "due — waiting
for a packet" and then "due — after 30 s of quiet" once it is due, "no
device connected", and "the week starts on Apply" while it is only staged
on. The line is a width-aware separator that reads the scheduler's new
status() on every paint, from state the loop already keeps, so it follows
the wait live without touching the file. It is drawn, even blank, for as
long as the preference is on, so the rows below never shift; every message
fits the PicoCalc's 53 columns, and a gallery specimen holds it there.
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>
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>
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>
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>
The CLI had grown the menu's manners -- colour, boxes, section headings,
glyph columns, a closing check mark -- and every one of them is in the way
of the thing it is actually for. It now prints like a standard Unix
utility: plain text, no escape sequences at all, no frames, one record per
line, and nothing wrapped, so a long line runs off the right instead of
folding its fields onto a second one.
meshterm/ui/script.py is the whole vocabulary. Listings are an uppercase
header and space-aligned records; key/value blocks are sysctl's shape, and
a `get` prints the bare value alone. Names are quoted wherever they share a
line, because a name can hold a space or a comma and a bare one is not a
field. A route reads "Origin" (3d),"Relay" (f2),"Us" (a1) -- the hash
outside the quotes, our own node a hop like any other rather than a star,
since the hash is what carries the identity that colour carries on screen.
Times are absolute local ISO-8601 and "-" is the one token for absent.
The return value is now half the report: 0 ok, 1 failure, 2 usage, 3 no
device, 4 the device was reached and the operation failed, 5 nothing to
report. That last one is the point -- a command that ran fine and found
nothing prints nothing and says so in $?, so a script never has to count
output lines to tell an empty mesh from a full one. Errors and progress
moved to stderr, and an expected failure no longer arrives as a traceback.
Two things lose their scripted face rather than get a degraded one. The map
is a picture whose nodes are told apart by colour: monochrome it would be
unreadable, coloured it could not be piped anywhere useful, and the nodes
themselves are still in `contacts`. The QR codes are a second rendering of
a link the output already prints.
ui.ack joins ui.note to draw the line the change needed: a note is output,
an acknowledgement is reassurance a person wants and a script must filter
out, so the menu prints it and the CLI drops it.
Co-Authored-By: Claude Opus 5 (1M context) <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>
Down off the last row wrapped to the first on a good third of the app's
lists, and the select screen shipped that as its default -- twenty-one
call sites had already opted out one at a time, each writing wrap=False
for its own reason. The reasons were all the same one, so the flag is
gone and clamping is simply what a cursor does.
The window follows the highlight, so a roll doesn't move a pointer: it
hauls the whole list back to the other end and flips its edge markers,
which reads as the screen changing under you rather than as one step.
Held-down arrows now settle at an end instead of looping past what you
were trying to reach.
The hand-rolled cursors came along: the trophy case's, the trace and TX
optimize action rows, the reorder list -- where the ungrabbed cursor
wrapped while the grabbed one clamped, so the same two keys meant
opposite things in the two modes -- a prompt's completion list, and a
dialog's buttons.
Tab is the exception, and it stays a ring: it has no reverse of its own,
so on the last chip it comes round or it dead-ends. Same for the three
picocalc F-lane chips that are single-key advancers -- Sort, the node
page's tab strip, the Time Machine's window -- where the lane offers no
way back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The descriptions were written for someone who already knew the answer. A user
reading "Airtime budget: duty cycle = 100 / (factor + 1) %" or "Per-hop
path-hash size; larger resists collisions, adds overhead" has to ask, which is
the opposite of what a description is for.
So both registries now answer the reader's actual question -- what does this do
to me -- in as few words as it takes. "Wider is faster, narrower reaches
further" instead of naming the unit twice. "Caps how much of the air we use;
0 = no cap" instead of the formula. "How loud this radio transmits."
Labels went with them where the name was ours to choose: Sweep floor and Sweep
ceiling are Lowest power and Highest power, Opening frame is Map fit, Silence
alarm is Alert after, Basemap source is Map tiles. The radio's own names stay --
someone matching Spreading factor against their phone app should find the words
they came for. History kept is just History.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Background adverts came off it. The cadence is the *companion's*, remembered per
public key and set on Device config, so a preference could only ever be the value
a radio starts on before you give it one -- a default behind a default, and a row
that says "newly seen device" while meaning "your own radio, the first time".
adverts.json answers for it again; the store is back to the constants it had.
Transmit cooldown now defaults to 5s rather than 1s. It paces every burst we are
allowed to make, and on a shared mesh the polite number is the larger one.
The last-column choices answer their own row: it asks whether to use the column a
terminal hides, so the values are auto, yes, no instead of naming the mechanism a
second time. YAML 1.1 reads a bare yes/no as a boolean, so a hand-edited file that
writes the obvious thing is understood rather than silently dropped -- we still
quote ours on the way out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three kinds of "setting" were tangled into two files and one of them had no
screen at all. The radio's settings live on the radio and are edited on Device
config. Machine setup -- where the database is, which companion to talk to --
stays in config.toml, a text editor's job. Everything in between was app
behaviour: six keys sitting in config.toml with nothing to open them, and a
dozen more that were only module constants a reader could never reach.
Those are preferences now, and they have a page. core/preferences.py is a
PrefSpec registry in the shape of the device-config one -- group, type, bounds,
help, and a default -- and the page is built from the same parts as the device
editor, deliberately: one grouped lane-aligned list, current -> new staged in
warn, Apply/Back appearing only once something is staged, the shared discard
confirm on the way out. Two editors that stage values should not be two screens
to learn. What it adds is a Defaults section holding one row that stages every
changed preference back to its built-in value; it is drawn only while a reset
would do something, and it stages rather than writes, so it is as reviewable and
as Esc-discardable as any other change and the file is still only ever written
by the one action at the bottom. Its rows scroll sideways where the device
editor's don't: the explanation is the widest thing on the row and the only part
that runs past the edge, so <-> slide it under a pinned setting and value.
The file is preferences.yaml and holds only the values you disagree with the
code about -- grouped and commented, each entry above its help and its default.
That is what lets a later change to a default reach an install that never asked
to be exempt; delete a line and the code takes over again. A hand edit gone
wrong costs that line, never the session.
Every preference is wired to something that reads it. ctx.preferences is the
explicit path; preferences.current() is for the render layer, which has no
context to reach through, the same reason get_platform() exists.
The About pages come along into a This app section, the third scope after this
node and other nodes, with Preferences leading it -- the one row there that
changes MeshTerm rather than describing it. A section of one would have broken
the menu's own no-bucket-of-one rule, and the app's scope is the honest home for
both halves anyway.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>