91 Commits
Author SHA1 Message Date
rich 233dfc813c Merge upstream MeshTerm 0.11.5
CI / lint (push) Waiting to run
CI / test macos-latest py3.14 (push) Waiting to run
CI / test ubuntu-24.04 py3.10 (push) Waiting to run
CI / test ubuntu-latest py3.11 (push) Waiting to run
CI / test ubuntu-latest py3.12 (push) Waiting to run
CI / test ubuntu-latest py3.13 (push) Waiting to run
CI / test ubuntu-latest py3.14 (push) Waiting to run
CI / test windows-latest py3.10 (push) Waiting to run
CI / test windows-latest py3.14 (push) Waiting to run
CI / build (push) Waiting to run
Issue trailers / check (push) Waiting to run
Bring in the 65 upstream commits since 0.11.1, with ^K lock and the self chip.
Both sides kept their features. React gives way to Lock in two places. On the
PicoCalc lane, Lock keeps the Shift half of F2 and React is ^E only. In the
hint, ^E react is the first atom to go when ^K lock has no room.

The long-message code now refills the compose line with MentionEditor, which
replaced LineEditor upstream.
2026-10-22 16:53:37 -04:00
Jean-Pierre MartineauandClaude Haiku 5.5 8dff5bbb29 The selected chat message whitens its author chip and its unknown mentions
The author chip of the selected message's group now turns its label white,
as the message's text does, and goes back when the selection leaves the
group. A mention of an unknown node turns white too: its grey fill already
says the node is unknown, and white reads better on it than black.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-19 23:59:45 -04:00
Jean-Pierre MartineauandClaude Haiku 5.5 7264a71104 The mention chips of a selected chat message go white with its text
In a channel, a DM, or a room, the selected message's text is white, and
now its mention chips follow: the label turns white and the fill stays, so
the chips still tell one node from the next. Arrow mode whitens the label
the same way. A mention that no key can place keeps its grey, as on every
selected row.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-19 23:48:59 -04:00
Jean-Pierre MartineauandClaude Haiku 5.5 59273a6b29 A locked author's sender chip carries the padlock, as ours carries the star
In a channel, a DM, or a room, the chip of a sender that is a locked
contact reads 🔒 Name (⚿ on the PicoCalc), in the place of the ★ on our
own chip. The transcript cache now compares the locks, so a ^K or a lock
from the node page redraws the chips on the next paint.

The padlock is one constant, marks.LOCK_ICON, shared with the Contacts list.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-19 23:40:32 -04:00
Jean-Pierre MartineauandClaude Haiku 5.5 daa26be5df ^K in a conversation locks the author of the selected message
In a channel, a DM, or a room, ^K toggles the lock of the contact that wrote
the selected message, so the archive sweep never takes it. The hint says
^K lock or ^K unlock, but only where it fits without dropping another atom,
and the status line confirms what the key did.

On the handhelds, Lock (or Unlock) rides Shift+F2 in a channel and a DM. A
room keeps Log in there, so the Author page that a conversation opens now
carries the lock row, and no other management verb.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-19 23:18:57 -04:00
Jean-Pierre MartineauandClaude Haiku 5.5 eb2d314551 Section headings carry no icon
Chat's Channels, Rooms, and Direct headings and the trophy case's
discipline headings drop their emoji, for a cleaner look. The trophy
marks stay on the delete list's rows and the record page's header.
section_heading no longer strips icons, since no heading has one.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-15 23:56:36 -04:00
Jean-Pierre MartineauandClaude Haiku 5.5 820740c9a0 A blank line before each section is one preference, app-wide
"Section spacing" (section_spacing_min_rows, in the Display group)
replaces "Author spacing" and "Spacing from". It is the smallest
terminal that gets the line: any height, 20, 24, 30, or 40 rows, or
never (default 24). Each section heading of a list gets the line, and
the chat keeps it between authors and before a new day.

The frame records the terminal rows on the screen before the body
renders (note_terminal_rows), and Screen.section_spacing is the one
test. section_heading marks its row as spaced, and the select list
adds the line, never at the top, after a blank line, or under a
heading. The hand spacers before headings are gone, except the seam
between the region table and its actions, which is two lists.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-15 22:28:23 -04:00
richandClaude Haiku 5.5 a323342971 Add emoji reactions in the wire format of MeshCore One
Select a message in a channel or a DM and press ^E (Shift+F2 on the handhelds) to
react with an emoji. A reaction is a usual message on the wire: MeshTerm writes the
format of MeshCore One and reads it and the two formats of meshcore-open. The
transcript folds each reaction under the message that it names, with a count, or
the names of the nodes while that message is selected. A reaction does not raise
the unread badge, and the Messages list previews it as "reacted 👍".

A reaction names its target by the sender's timestamp. Thus MeshTerm now gives the
device the timestamp of each message it sends and stores exactly that second. A
retry of a DM keeps the timestamp and counts the try number, as the protocol
specifies. The emoji that ^E offers are the reaction_emoji preference. Rooms have
no reactions.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-15 07:47:13 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 eeafbb18f6 Node marks are white, and the border is darker
Every node-type mark is white now: ● ▲ ■ ◉ were pink, violet, white,
and orange, and the shape already says the type. Each keeps its own
type.* style, so one can take a colour back in one place; the unknown ○
keeps its grey. On the map a crowd in one cell used to glow toward
white, which a white marker cannot do, so that signal is gone.

The border grey goes down to #7c7c7c, which JP picked on a ladder. That
sits almost on faint, so the title bar's live clip arrow is now lit like
the title instead of taking the border's grey. On the PicoCalc the
border is the dark grey slot.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 00:30:12 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 796ff94dad Our star is white
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>
2026-10-14 00:18:59 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 feea5a89db In a chat, our chip is white on our fill and unknown names are black
Our chip had our hue as a light fill under dark text. It now has the fill
that each known sender gets, with a white label, and the PicoCalc can draw
that too: white on the dim twin of the hue. The hop flag is white_ink now.

A chat chip with no key keeps its grey fill, and its label is black. On the
PicoCalc it is black on the light grey of the keyless chip.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 00:06:28 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 b680b172d7 Our star takes the hue of our key, and unknown chat names go grey
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>
2026-10-13 23:46:39 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 3fdc638812 In a chat, a star in our hue marks a message that names us
The point of our chip did not read well. The mark at the left of the time
is now the star, in the hue of our key: the colour of the mention chip in
the body. It is the same on each platform, with chips or without them. The
Messages list uses the same star at the left of the age.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 23:23:06 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 691170d342 In a chat, the point of our chip alone marks a message that names us
The notch and the point together were too heavy. The mark is now one cell,
the point of our chip in its fill, then a space before the time. Without
chips, it is ">" in our hue.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 23:05:12 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 68d5d03324 In a chat, the ends of our chip mark a message that names us
The star at the left of the time becomes two cells in the fill of our
chip: the notch, then the point, with no label between them. Thus the mark
has the colour of the mention chip in the body. Without chips, it is ">>"
in our hue. The Messages list puts the same mark at the left of the age.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 22:42:28 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 427bee836b In a chat, our sender chip has its star inside again
Our messages go under "★ Name" on the turned-over chip, as before the last
change. A mention of us keeps its chip without a star, and its message
keeps the star at the left of the time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 22:28:47 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 8043881d3c In a chat, the star leaves the chip and a mention is a chip as you type
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>
2026-10-13 21:07:06 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 0c3a8b5b5c The space between authors in a chat is a preference
Two preferences in the Display group: "Author spacing" (auto, on, off;
default auto) and "Spacing from" (the smallest terminal, in rows, that gets
the blank line with auto; default 24). The test reads the real rows of the
terminal before each paint, so a small desktop window drops the line as the
Cardputer Zero does, and a change applies at the next paint. The transcript
cache stores the spacing, so a change draws the lines again. The CLI
manual's sample of `preferences show` is the real output again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 18:16:15 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 03b3c0926b In a chat, a blank line comes after the messages of each author
The transcript asked for a gap between sender groups, but an empty Text
renders as no line at all, so the gap never showed. A blank line now
closes each author's messages, also before a new day's divider, where
the platform has 20 rows or more. The 14 rows of the Cardputer Zero keep
the transcript dense. A cached slice that starts with the gap pins its
day divider one line down.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 02:00:31 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 a19eadd81b In a chat, our messages go under our name
A channel, a direct chat, a room, and the Messages list show our messages
under "★ Name" instead of the bare star. The chip has our key's hue turned
over: dark ink on a light fill, where each other sender is light on dark.
A mention of our name takes the same chip, as "★ @Name". The PicoCalc
cannot draw a light background, so there the chip keeps the normal
colours and the star marks it. Path lines keep the bare star.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 01:56:13 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 2d2128718a ^O opens the author's node page, and ^G logs in to a room
In a channel, a DM, or a room, ^O on a selected message opens the node page
of its author, the same page the Contacts list opens: our own for a message
of ours, the contact the author's key finds otherwise (ContactPages, the twin
of the map's Locator). It opens to look, without the management verbs, which
belong to the list a contact is in. The hint names ^O only where it acts; the
handheld chip is Author, on Shift+F3 behind Map, since F1-F3 are full.

The room's login moves from ^O to ^G.

The Unreleased changelog now also says where the Map chip went, and covers
the shared lane and the route-end chips.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 00:25:35 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 00a7b82347 The conversations' F2 chip reads URL→QR
It names what the key does with the selection: its link, shown as a code.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 23:17:18 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 444b0ea669 A channel, a DM, and a room share one F-key lane, and the day jump leaves it
F1-F3 hold what the three conversations share, in the hint's order: Paths,
QR, Map. The Shift bank holds each screen's own verbs: ^R on Shift+F1 (Retry
in a DM and a room, Resend in a channel) and the room's Log in on Shift+F2.
F4/F5 stay the pager with Latest/Oldest behind Shift.

Day ↑ and Day ↓ are gone from the lane; the desktop keeps the day jump on
^PgUp/^PgDn.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 22:06:50 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 f07bb9e1dd The ends of a route are origin and destination chips in four more places
The Messages list's preview draws its author as an origin chip (no colon: the
point says it) and its mentions as destination chips, as the transcript does.
The packet viewer's from rows close on the point and its to row opens on the
notch, around the via chain that already opens and closes on both. The live
feed's addressed subject is an origin chip and a destination chip facing each
other across one cell of page (widgets.route_ends): a seam would claim the two
are neighbours. A pair too wide for the lane cracks instead of eliding. The
courier's recipient is a destination chip, in the hue of its key while it
waits and muted once finished.

Arrow mode reads exactly as before everywhere. A lone hop there carries no
PATH_INK stamp, so a highlighted list row whitens it like any other name.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 17:45:24 -04:00
Jean-Pierre MartineauandClaude Haiku 5.5 87baa480a9 A message's author chip closes on the point, and a mention is a chip that opens on the notch
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>
2026-10-10 17:22:59 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 125ddb4003 ^L shows a message's author or a contact on the map, and ^O logs in to a room
locate moves from ^Y to ^L, and the room's login from ^L to ^O, under the
chord rule of 3675226. On the map and in the mesh walk, ^L still goes to
our node. In a chat and on a room's board, ^L opens the map on the author
of the selected message (our own message: our node); on Contacts, on the
highlighted node. map_screen.Locator holds the markers that
gather_markers returns when the screen opens and answers find() at each
paint, so the "^L map" hint atom and the Shift+F1 Map chip show only
where the key acts. A room keeps Log in on Shift+F1, so ^L has no chip
on a room's board yet.

The chat's hints for a selected message go through _fit_hint, which
drops ↑↓ and then ^P only when the line passes 72 cells; the room now
reuses them. LaneDeck.action_for returns None for a slot that has only
a Shift companion.

Right Ctrl with a letter chord still does nothing on the Canadian
Multilingual layout: JP chose not to repair that.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 17:20:42 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 11b5274b31 Docstrings and comments of the tests are in ASD-STE100
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>
2026-10-06 21:36:50 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 44e60c4054 Acks get the wait a flood needs, and a late one still counts
Only 24 of 144 direct messages and posts in the history were ever marked
delivered. Three faults, each fixed:

- The wait was the radio's own estimate times 1.2: about 5.7 s for a
  flood, while a flooded round trip from a few hops out takes ten (a
  room's answer from five hops landed at 10.1 s on hardware). It is now
  the login's patience, the estimate stretched a quarter plus a round
  trip sized to the route, about 15 s for a flood.
- An ack that landed after the wait was dropped. The radio pushes one
  whenever it arrives, for any of its last eight sends, but nothing was
  listening, so a delivered message stayed failed for good. The history
  shows ACK and PATH packets heard seconds after messages recorded as
  failed. The send now returns a Delivery carrying the ack code, and the
  chat service keeps the last sixteen messages still waiting. A late ack
  flips one to delivered, in history and in the message the chat screen
  holds, and every code a retried message went out under counts.
- The Courier trusted the failure and sent the message again: a
  duplicate for the recipient whenever the ack was only late. A late ack
  now marks the entry delivered.

The ack listener is also armed before the send rather than after it,
so a neighbour's ack quicker than the send's own return is not missed.
The debug log names how a message went out and the radio's own
round-trip time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 03:00:48 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 b6e7cd5c16 The handhelds are named for their variant: picocalc-lyra and cardputer-zero
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>
2026-10-04 11:52:41 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 149eb0cd90 The chat list's columns say LAST and MESSAGE; the live feed keeps TIME
The conversation list now reads CONVERSATION, NEW, LAST, MESSAGE: LAST
over the last message's age, MESSAGE over the message, shortening to MSG
on a narrow terminal. The live feed's timestamp lane is TIME again, and
the channel list keeps NEW.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 19:44:28 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 fedd5f9608 Column headings say NEW and WHEN, not UNREAD and TIME
The chat picker's unread count is headed NEW and its last-message age
WHEN; the channel list's unread count is NEW; the live feed's timestamp
lane is WHEN. Short words over a badge and an age, which UNREAD outgrew
by a cell and had to borrow the gap for. The command line's columns keep
their names: they are a documented contract of their own.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 17:13:21 -04:00
Jean-Pierre Martineau a2e2500efe Merge branch 'main' into cardputer-zero
# Conflicts:
#	tests/test_chat.py
2026-10-02 00:58:32 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 810232cfb6 A link written without a scheme is found too, by www. or a real TLD
meshterm.net/map and www.example.org are links now, as surely as their
https forms: a host starting www., or ending in a top-level domain IANA
has delegated, read from the registry itself, kept verbatim in
assets/tlds.txt. So file.txt stays text, txt being no TLD, and so do a
decimal, an abbreviation, an address, and a path. A scheme-less link
opens, and its QR code encodes, as https://. Link finding moves out of
the chat screen into core/links.py.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 00:55:59 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 cd68498feb On the Cardputer, a link's code is on ^U, as on the desktop
Its 14 rows can't spare a code under every message that carries a link,
so it keeps url_codes off and reaches a link's code the desktop's way,
on the share screen. Its braille code is small enough that a link and its
code share the panel. A test pins all three: no code under the message,
QR on the lane's Shift bank, and the share screen inside 53x14.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 00:18:33 -04:00
Jean-Pierre Martineau 8755adc6f8 Merge branch 'main' into cardputer-zero
# Conflicts:
#	CLAUDE.md
#	meshterm/ui/chat.py
#	meshterm/ui/qr.py
#	meshterm/ui/walk_screen.py
#	tests/test_chat.py
#	tests/test_gallery.py
2026-10-02 00:15:20 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 c6c80b4c1f A chat link is never cut where the screen can hold it, and ^U shows its code
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>
2026-10-01 23:07:51 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 9b215172d0 On the PicoCalc, a chat message's links hang as QR codes under it
Every URL in a channel or direct message, any scheme (a meshcore://
share as surely as a web link), now gets a braille code under the
message, hung at the body's indent. Several sit side by side and wrap
onto a new band when the line is full, so a link read on the handheld
opens on a phone instead of being typed out. Platform.url_codes keeps
it to the PicoCalc: half-block codes would swamp a desktop transcript,
and the Cardputer's 14 rows can't spare 9 under every link.

A picked message now keeps its whole block in view, codes included,
rather than only its first row, so walking the pick down onto a link
no longer leaves its code below the fold.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 00:37:33 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 ec850d8e93 On the PicoCalc, a chat message's links hang as QR codes under it
Every URL in a channel or direct message, any scheme (a meshcore://
share as surely as a web link), now gets a braille code under the
message, hung at the body's indent. Several sit side by side and wrap
onto a new band when the line is full, so a link read on the handheld
opens on a phone instead of being typed out. Platform.url_codes keeps
it to the PicoCalc: half-block codes would swamp a desktop transcript,
and the Cardputer's 14 rows can't spare 9 under every link.

A picked message now keeps its whole block in view, codes included,
rather than only its first row, so walking the pick down onto a link
no longer leaves its code below the fold.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 00:36:50 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 6eb7d8f6bd Each handheld deals its own F-key lane, and the Cardputer Zero joins as a platform
A lane's keyboard-bound half is now a LaneDeck named by Platform.lane_deck: which
keycodes drive the slots, where the chips sit, how they are drawn, and which of a
screen's lane definitions it reads. Screens define picocalc_lane; cardputer_lane
follows it until a screen overrides it, so the two can part ways one screen at a
time. The Cardputer deck is Fn+4..8, Shift with them as F16-F20, eight-cell chips
centred over their keys in the fn key's orange-red and the Shift blue.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 19:33:44 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 bd222105ae Two tests stop racing CI: the chat waits for its send, the BlueZ fixture leaves os alone
The BlueZ fixture patched os.getpid to return a string, and bluez.os is the
global os module, so Python 3.14's event loop (which opens a pidfd for its own
pid at startup) failed before any test ran. The agent path is now derived from
the real pid instead.

The end-to-end chat test piped Esc beside Enter, so a slow runner left the
screen and cancelled the spawned send before the row was written. It now sends
Esc only once the message is recorded.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 04:16:26 -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 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 Opus 5 9341c4c1d9 Twenty-two cells reserved for a name most people spell in five
The chat picker's conversation lane was a flat 22 cells, and every cell
of it past the longest name in the list was padding in front of a short
one — taken straight out of the last-message preview, which is the only
run on the row with something to say. On the PicoCalc's 53 columns that
left the message 12 cells: enough for a sender's name and a colon.

It is measured now, against the names the list actually holds, floored
at the header word and capped at the shape of a repeater-style name.
The preview keeps what it saves: 31 cells to 37 on a 72-column
terminal, 12 to 18 on the console.

What the cap costs is the tail of a long name, so the row gained the way
to read past its own edge that every other lane-and-run list in the app
already had: ←→ slide the message under the pinned columns, one row at a
time (Choice.hscroll_from). The preview had to stop pre-truncating
itself at 40 cells to make that worth pressing — a cut made before the
terminal's own leaves nothing to reveal.

The marker lane was measured too, and was wrong. A contact's dot was
padded to "a channel's double-cell glyph", which the channel glyph is on
regular and is not in the console font: on the PicoCalc every channel
row sat a cell to the left of every direct row, header over neither. It
now pads to what the glyph reports.

The footer had to give a cell back for the scroll atom, since it can
coincide with the delete atom on any thread with history — the same
arithmetic the purge preview does in its own comment. Enter opens a
screen here, so its verb is `open` rather than the default `select`,
which pays for most of it; `Del delete history` spends the rest on an
object the highlighted row is already naming.

The picker joins the gallery, which is what would have caught the marker
lane.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 23:34:15 -04:00
Jean-Pierre MartineauandClaude Opus 5 92d31a3191 Every file is spelled the way the formatter spells it
`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>
2026-09-07 01:07:19 -04:00
Jean-Pierre MartineauandClaude Opus 5 40ecf5151d The tests' docstrings lead with a sentence too
The last 122 `D205` sites, which takes `ruff check .` to zero.

Same shape as the package pass and just as additive: these were a paragraph
with no summary, so each one gains a plain sentence saying what is being
asserted, and every word that was there moves down into the description. The
dense ones benefit most — `test_pathline`'s 23, where "A seam is one cell, not
two" now stands alone and the chevron mechanics explain it underneath, and
`test_tui`'s wide-glyph repaint test, whose seven lines on prompt_toolkit's
relative cursor now sit under the one line that says what the test pins.

Four module docstrings (`test_advert`, `test_gallery`, `test_platforms`,
`test_repeater_admin`) opened with "X tests: a, b, and c" and now name the
subject on the first line with the list below it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 20:37:51 -04:00
Jean-Pierre MartineauandClaude Opus 5 4545173c65 Every public callable says what it is for
The 46 missing docstrings, written rather than generated.

Most of the test ones already carried the explanation — as a comment sitting on
the first line of the body. That prose is the docstring; it was only in the
wrong place, so the config editor's PIN and lane tests keep their own words and
lose the duplicate comment. The rest are new: the two `repo` fixtures, the
counting `FakeDevice` in `test_device_state` (each method now says what it
answers *and* that it counts the call), the PicoCalc chrome and 16-slot theme
tests, and three one-offs.

In the package: `PathLine` and `FastRenderer`'s constructors point at the class
docstring that already documents their arguments rather than repeating it,
`SelectItem.__post_init__` and `courier_screen`'s opener say what they cache and
why, and `emoji_width.create_content` explains the memo it wraps the line lookup
in. `flash.py` and `uart1-mux.py` get theirs too, and `tests/__init__.py`
finally says what the package is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 20:28:01 -04:00
Jean-Pierre MartineauandClaude Opus 5 b5006cc415 Every line fits the hundred columns it was configured for
`E501` across 69 lines, wrapped rather than exempted.

The prose ones — module docstrings in `menu`, `node_detail_screen`,
`pathline`, `pathgraph`, `walk_screen`, `trace_runner` and `test_about` — are
reflowed as paragraphs, so the block keeps its shape instead of gaining one
short orphan line. Nothing they say changed.

The code ones mostly gained a line break inside a call. Where a break would
have read worse than a name, the sub-expression got one instead: `reach` in
`topology`'s quality clamp, `plain_cells` in `pathline`, `numbering` in the
trace list, `listed` in `flash.py`'s refusal, and a small `shape`/`is_strip`
helper in two tests that were comparing or matching the same shape twice on one
line. Four docstring summaries lost a word they were not using.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 20:22:52 -04:00
Jean-Pierre MartineauandClaude Opus 5 e9a4f644d8 A line variable is called a line, and a format string is one string
The mechanical tail of the ruff pass, all of it behaviour-preserving.

`E741`: 29 uses of the ambiguous `l`, which reads as a `1` in most fonts.
Twenty-four of them held a rendered line and are now `ln`; three held an MVT
`Layer` and are now `layer`; two were a map label sorted by rank, now `lab`.
Renaming only the flagged line and then re-running `F821` was the check — a
half-renamed comprehension leaves a name nothing defines, so the linter finds
what a careful read might not.

`UP031`: 15 `%`-format strings become `.format` calls. `UP007`: the `CellStyle`
and `LabelT` aliases become `X | Y`; both are annotation-only, so the runtime
value never mattered. `E702`: four semicolon-chained `await` lines in
`test_device_state` get a line each.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 20:14:42 -04:00
Jean-Pierre MartineauandClaude Opus 5 61b6387269 Every zip says whether its sides are the same length
`B905` over 56 sites, and the answer differs by idiom rather than by file.

21 were the sliding window `zip(xs, xs[1:])` — ragged by exactly one, on
purpose, so `strict=` is the wrong question entirely. Those become
`itertools.pairwise`, which is what they always meant and needs no flag: the
walk's SNR stops, the heat ladder, the route chains in `topology`/`records`,
every polyline in `mapcanvas`, and the graph's edge sets in `pathgraph`.

The other 35 pair sequences that must line up, so they now say `strict=True`
and a mismatch raises instead of silently truncating the longer side. Running
them under that assertion is what sorted the two groups: the first pass put
`strict=True` everywhere and 208 tests failed, all of them a sliding window.

One goes the other way. `mvt._decode_feature` reads tags off a network tile,
where an odd-length list means the tile is malformed — and this decoder answers
that with a shrug everywhere else (`if not geom_ints: return None`, bounds-
checked key and value indices). It keeps `strict=False`, now with a comment
saying why, so a bad basemap tile still can't crash the map.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 20:12:07 -04:00
Jean-Pierre MartineauandClaude Opus 5 71b16ec7b7 The annotations, imports, and dead locals answer to ruff
`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>
2026-09-06 17:26:12 -04:00