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 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>
Navigation was almost a stack already, and the survey in docs/survey-navigation.md
catalogues where it wasn't. This is the machinery for the strict version, plus
the one shortcut past it.
A screen that owns a loop can now keep itself pushed for the whole visit:
async with session.stay(screen) as visit:
while True:
chosen = await visit.result()
One push, one pop, and the *object* survives its sub-screens — so the cursor,
the sort, the scroll offset and any live filter are simply still there on the
way back, rather than being approximated by a `default=` restore that can only
recover the cursor and only when its row still exists. It also retires the
pop-then-re-push-as-a-backdrop workaround: a screen that never left the stack
already is one.
^W unwinds every frame at once, back to the main menu. It resolves the top
screen with POP_ALL, which each navigation boundary turns into PopToMenu, and
every frame pops itself in a finally on the way out — the same single-pop
discipline applied N times. PopToMenu derives from BaseException so it crosses
the `except Exception` guards that stop a broken tool from taking the menu down;
those must not report an unwind as a tool failure. The flag it sets matters
independently of the future: pressed during a device read there is nothing
armed to carry the sentinel, so the next screen the flow tries to open refuses
to open instead.
It declines to fire over anything modal — an unanswered dialog, a progress bar,
a busy splash — which is what `modal` is for. `floating` is only about being
drawn as a box; a select list floats and is not modal, the busy splash is modal
and does not float.
^Q quits from anywhere. Both chords are deliberately unadvertised: a global verb
has no screen to belong to, so a footer atom or an F-key chip would ride on
every screen, and the lane only has three free slots to begin with. A test
walks every string literal in the package to keep them out of the UI.
Quitting mid-screen used to abandon the coroutine driving the app: the chord
exits the application straight from the key handler, so `run_async` returned
while main() was still parked on a screen, and its finally — which closes the
history and chat runs, stops the services and arms the exit watchdog — never
ran. It is cancelled and awaited now.
The menu is the root: built once, kept for the session, and the one frame that
catches the unwind.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Home and End are on the PicoCalc's keyboard. Paging is not. So the
jump-to-either-end pair was never the unreachable thing that earned it
F1/F2 -- it only ever wanted a chip for consistency with the pager it
belongs to. Now it rides that pager: Shift+F5 is Top because F5 is
Page up, Shift+F4 is Bottom because F4 is Page down. One axis, one pair
of keys, the second-grade verb sitting behind the first.
That frees F1-F3 on every screen, and the lane is the only place the
device can advertise anything -- there is no hint line to read a chord
off. So the freed slots went to the affordances that were invisible
there:
select lists Sect down/up, the section jumps. Ctrl+PgUp on a
keyboard with no PgUp is the one binding in the app
that was physically unreachable. Plus Delete, lit on
exactly the rows the key acts on.
map Region (Home, renamed -- it frames the ground the mesh
covers, it does not "reset" anything), You (new: Ctrl+L,
recentre on our own node at the zoom you chose), and
Frame for the find matches.
node page the tab it would switch you to.
time machine the window span it would take you to -- w was
undiscoverable on the device.
The map is the one exception to the new rule: Region answers "where
should I be looking", not "how far down the body am I", so it keeps F1
beside the other two destinations rather than hiding behind a pager it
has nothing to do with.
Screens that never scroll (every dialog, the busy splash, the reorder
list) now say so with an empty lane instead of offering a pager that
does nothing. Screens whose Home/End mean something else say what:
Latest/Oldest in a chat, Newest/Oldest in the packet viewer, You on the
walk (where End is bound to nothing, so its slot stays blank).
The map's hint line finally fits 72: its basemap status was never a key
hint, so it moved to the title's status chain where the standards put
those -- which is what took the map off the gallery's pre-existing
overflow list.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replace the between-screens ring spinner with a titled skeleton card: a
heading, the one-cell working chip beside a "reading from <device>…"
caption, and a two-row braille LED bar in place of the old ring.
The bar is a Knight-Rider scanner — a red head sweeps left↔right and
bounces off both ends over a dark-gray track, dragging a persistence-of-
vision comet trail (per-tick decay) with a white-hot core. It rides the
same tick that spins the chip and fades in from black with the card, so
the wait reads as work rather than a frozen bar.
Thread an optional title/caption through busy_overlay so each gap names
the screen being fetched and the companion it's reading from. Give the
CLI progress dialog the same liveness: a spinning chip on every row and a
slim braille meter, with no dead track on indeterminate tasks. Drop the
now-unused RingSpinner.
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>