chat join logs in to a room server and prints what it allowed: member,
admin or read-only. It uses --password, or the password MeshTerm
remembers for the room. A room says nothing to a wrong password, so a
join that hears nothing fails (exit 4) and names both possible reasons.
send --to a room posts to it, and its receipt says kind "room". A room's
history names each post's author where a direct transcript names the
peer, drawn the way a route draws a hop: "Alice (d4e5f6a7)", or the bare
hash when nothing names the key. The document carries the author on
every row, null outside a room, so history answers in one shape for
every kind of conversation; listen streams it too. chat list calls a
room a room.
The CLI manual, the features guide and the README describe rooms, and
CLAUDE.md adds room and post to the lexicon, 📌 to the icons, and ^L to
the chord list.
Refs: #24
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>
docs/ was one flat folder: the user guides, the CLI manual, the device
manuals, the code notes and an old survey side by side. It is now
guide/ (features, configuration, connecting, terminals), cli/ (the
manual as its README, the cookbook), devices/ (which manual is yours
as its README, then one per device) and development/ (architecture,
the survey); screenshots/ stays put. The index follows the folders.
The device manuals are named for the device they are about: the
PicoCalc's is picocalc-lyra.md, since MeshTerm runs on the PicoCalc's
Luckfox Lyra core and on none of the others, and the uConsole's stays
uconsole.md, since its CM4 and CM5 run it alike. A new cardputer-zero.md
opens on a warning that the device is not supported yet, says what has
been built and tested only in the simulator, and how to try it there.
Every relative link was resolved from where it was written and rewritten
from where it now lives, and the repo-root spellings in code, config and
the release skill moved with them; the changelog's GitHub URLs too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SignPath Foundation's application asks for a download page that says the
project uses them for signing, and their terms want a "Code signing policy"
section on it: their credit line, who holds which role, and a privacy
statement. The download note now says Windows signing is on its way instead
of that signing costs money, and the new section says how a build gets
signed, that one person holds all three roles, and what MeshTerm sends
anywhere: map tiles from OpenFreeMap when a map opens, and on the radio only
what you ask it to send (the weekly advert is off by default).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three pull requests against toy issues arrived from agents that farm open
issues for contribution stats. None had said a word to the project, and
none disclosed that AI wrote them.
CONTRIBUTING.md now says, in plain words, how MeshTerm was made and who
it wants: people who use it, AI-assisted or not. Before writing code, a
contributor says hello in the new #contributing channel on Discord and
waits for a yes; the pull request carries their Discord username and a
link to that yes. AI use has to be disclosed in the commits and the pull
request; undisclosed or unattended agent output is closed without review.
The PR template, the README's two notes on pull requests, and the docs
index all said small fixes could skip the conversation, and now match.
The release skill's commit carries the AI trailer its seven release
commits went out without.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every list of three or more takes the serial comma: the one-liner in
pyproject.toml, meshterm/__init__.py, and --help ("USB, Bluetooth, or TCP",
"Windows, macOS, and Linux"), the README, the changelog, the manuals, the
issue and PR templates, and six strings the app shows (the region-name
rule, a repeater setting's help, the diagnostics tool's description, and
the archive confirms). The Windows-console notice is rewrapped so its
comma keeps it inside 72 columns.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A new page, docs/connecting.md, collects every connection failure MeshTerm
reports, with its cause and remedy: who pairs a Bluetooth companion on each
system (MeshTerm on Windows and Linux, macOS's own dialog there, where
--ble-pin does nothing), the terminal's Bluetooth permission on macOS and
its refusal over SSH, reading a screenless companion's PIN over USB,
forgetting a pairing on each system, and the dialout group and ModemManager
for USB on Linux. The README, the docs index, hardware.md, and the CLI
manual's failure section link to it, and --ble-pin says it has no effect
on macOS. The CLI manual's no-radio example shows today's message.
The changelog covers the Linux Bluetooth pairing and link fixes and the
per-step failure messages. configuration.md shows db_path, and a few lists
gain their Oxford comma, WiFi becomes Wi-Fi, and "never-before-seen"
becomes "heard for the first time".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The menu's This node / Other nodes / This app sections were called scopes; they now name whose a setting is, and scope is left to a flood's region alone. CLAUDE.md's lexicon says so.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
configuration.md now takes a non-AIO board from its meshtasticd config
to a working profile: the key-by-key mapping, a whole example, how to
check it, and what the profile deliberately doesn't hold (radio
settings, the preamble, identity). It warns that a key left out keeps
the AIO's value, unlike meshtasticd where it means off. --spi joins the
option lists in cli.md, the README, and the cookbook, radio/ joins the
state-files table, and config.example.toml carries a full wiring example.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A LoRa chip on the host's own SPI bus (the uConsole AIO) is now a
transport like serial, BLE, and TCP: --spi, an SPI profile, or the
picker's row for /dev/spidev1.0. MeshTerm starts a software node as a
child process under whichever Python has openhop-core, talks to it over
a loopback port, and ends it on disconnect; the node also exits with its
parent, so the pins are free whenever MeshTerm is not running.
The node keeps identity, prefs, channels, and contacts under
~/.meshterm/radio/<spidev>/, and on first use copies the bridge's
identity, contacts, and radio settings so it stays the same node.
Board wiring lives in a profile's spi table; radio settings are the
node's own, edited on Device config. Closes#21.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The top of the README showed a still that linked to meshterm.net, because
GitHub strips <video> from markdown and only plays a file uploaded to
GitHub itself. The recording is now uploaded, so the README carries it on a
line of its own and GitHub puts an inline player there. A logged-out
visitor gets the player too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four different one-liners were in circulation: the repository's "interactive, scriptable
CLI toolkit", the README's "full-screen terminal companion", the About page's "client that
lives in your terminal", and the package summary's "TUI-based client ... companion
connection". JP picked one:
A full-featured TUI MeshCore client for your terminal
It now opens the README, the About page, the package summary, meshterm.__doc__ and the
`meshterm --help` header, which cli.py already says must stay the same as the summary.
The summary's second sentence loses its grammar slip on the way: "Connects to a companion
over USB, Bluetooth or TCP." CLAUDE.md opens with the same line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A sweep of the README, CONTRIBUTING and the 0.9.0 changelog entry against the running app.
- Not every screen has a subcommand: the map, dashboard, live feed, watchtower, mesh walk
and time machine live in the menu only. "Most screens".
- Not every command speaks --json: specimen refuses it. "Nearly every".
- On Windows the download is meshterm-windows-x64.exe, not a file called meshterm.
- Apple silicon is M1 and later; Intel Macs were sold until 2023.
- The global options list gains --ble-pin, --absolute and --platform, which it missed.
- The meshcore library speaks TCP too.
- The standalone builds are single files, so their licence texts are bundled inside, not
in an archive to extract.
- 64-bit ARM covers the uConsole; the PicoCalc is 32-bit and installs from a checkout.
- --mock keeps its own history database; the caches it shares are not "none of it".
The README, CONTRIBUTING and the PR template disagreed on when a pull request needs an
issue first. All three now say what CONTRIBUTING said: small fixes can come straight in,
anything bigger waits for a yes. CONTRIBUTING drops "or discussion", since Discussions
are off, and points bug reporters at the form, the diagnostics file and Discord. The bug
form link goes to the form rather than its YAML.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The recording is the highest-leverage thing the front page can carry, and it now sits
above the screenshots rather than waiting for launch day.
It is a link, not a player, and that is forced rather than chosen. GitHub strips <video>
out of markdown and renders an mp4 behind an image as a broken image; the only thing that
plays inline is a user-attachments URL, which cannot be created until the repository is
public. A linked still works today, works while the repository is still private, and goes
on working afterwards -- camo fetches the image from meshterm.net, which has been public
all along.
The picture is the poster frame with a play mark and the running time on it, so it reads
as a video rather than as a sixth screenshot, and so a reader knows it is under two
minutes before they commit to a click. The screenshots stay underneath: a still is what
someone skimming actually reads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_CATEGORY_ORDER lists six: Message, Watch, Explore, This node, Other nodes,
This app. Every table we have ever published stopped at five. So Preferences,
Diagnostics and the four written pages -- About, About the author, Join
Discord, Support -- were documented nowhere, while the CLI has answered
`meshterm about` and the rest all along.
Folding the changelog is what surfaced it: writing diagnostics into the 0.9.0
entry meant naming where it lives, and where it lives is a section the
features table did not have.
The framing is fixed too, and the fix comes from base.py's own comment rather
than from me. "Five sections named for the doing" was never quite right --
three of them name a doing (Message, Watch, Explore) and three name the scope
a setting belongs to: the radio in your hand, someone else's over the mesh,
and the program in front of you. That is why This app sorts last, and it now
reads that way in both the README summary and the catalogue.
Every command in the new table was checked against `meshterm --help` rather
than inferred from a tool's name attribute, `diagnostics --out` and the four
`preferences` subcommands included.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It had grown to 580 lines, and most of what a newcomer scrolled past was
reference material: the five feature tables, the CLI cookbook with both
output faces and the exit statuses, the configuration reference, and the
package layout. None of that is what somebody reads while deciding whether
to install this.
It is 375 lines now and it does onboarding and install -- what MeshTerm is,
the screenshots, how to get it running, a quick start, and how the project
is run. The rest moved under docs/ behind a new docs/README.md index, which
GitHub renders when you browse to the folder. The index also lists the two
code surveys, which the README's own table never did.
The prose moved by extracting line ranges rather than retyping, so anything
that did not need rewording is byte-identical. All 255 relative links across
the 13 markdown files resolve, anchors included.
Two changes are not relocations. "## Development" is deleted rather than
moved, because CONTRIBUTING.md already carried every command in it plus the
blame-ignore config and the 72-column gallery gate. And the Windows console
section is summarised in Install with the full explanation in
docs/terminals.md, since a Windows user meeting empty boxes cannot afford to
miss it.
Licensing stays where it was. Acknowledgements, the map and font credits, the
dependency table and the licence are all still in the README, which is where
a redistributor looks and where SignPath Foundation will look for its credit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A browser download is the one route that does not work on a Mac, and the
README said so in a note *below* the commands -- which is no help to
somebody who followed the commands. macOS now leads the download section
with curl, states why in its first sentence, and Linux follows in the same
three lines.
Those three lines name no version, because the release now attaches a
version-less copy of every binary beside the stamped one. That is what
makes releases/latest/download/meshterm-macos-arm64 resolve, so the README
points at whichever release is newest instead of at one that was current
when somebody last remembered to grep the old number out of it. The
stamped files stay for anyone keeping several builds around.
And the release page itself now opens with those commands written out for
its own version, above the changelog, built from the tag -- so the links in
the example are the assets attached to the page printing them.
The release skill loses the step that re-pointed the README by hand and
gains the asset count to check instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The quarantine flag that stops an unsigned binary is set by the browser
that downloaded it, not by macOS. curl sets nothing, so the same file
fetched that way simply runs -- no dialog, no xattr, no signature. For a
program whose users are already at a terminal that is the normal way to
install it, so it leads the note now.
The old advice was also going stale: Sequoia removed the right-click ->
Open bypass, and since Catalina a quarantined binary started from Terminal
-- which is how the README says to start MeshTerm -- gets the same block
rather than escaping it. Both are named, xattr stays for anyone who already
used a browser, and pipx is pointed at as the route that downloads no
binary at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs/hardware.md shrinks to a router: which guide for which device, and the
two platform flavours. Everything the guides were still reaching into it for
moved home — the bridge's environment knobs into uconsole.md, and the
verified/unverified lists into each manual's closing section, split by
device (the PicoCalc's gains the MX1.25 pin order and the 5 V path, both
inferred rather than measured on a Lyra).
The PicoCalc manual gets the Wi-Fi kick back — it works and stays; only the
join script is going. The uConsole manual opens on the same kind of
disclaimer as the PicoCalc's, minus the hardware-handling parts: a LoRa
transmitter, a Linux shell, the guide may be wrong or stale, your call.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs/picocalc.md starts from a stock PicoCalc with its Pico and ends with a
Linux handheld running MeshTerm with a LoRa radio soldered inside: a priced
shopping list, the Lyra swap, writing Calculinux, the serial console, Wi-Fi,
the setup script phase by phase, then PlatformIO, the firmware build with the
patch explained hunk by hunk and a by-hand route for when it stops applying,
flashing, the four wires, the Lyra setup, day-to-day use and troubleshooting.
The README gains a Documentation table and points the handheld paragraph at
the two manuals; the script indexes and the changelog follow. hardware.md
stays as the reference behind both.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Dashboard, map, trace and the Message paths dialog two by two, each with an alt text that
says what it shows, and the PicoCalc photo alone beneath them, downscaled to 1200 px wide so
the README does not load a 7 MB camera frame. The placeholder comment is gone with its job.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
docs/cli.md led with "the CLI is meant to be read by awk" and made every rule
follow from it. That promise moved to --json, so the manual now opens on the
split — the plain face for a person at a prompt, the JSON face for a program —
and documents both per command: what each prints, and what shape and fields come
back. Every sample is captured from a real --mock run in a throwaway
MESHTERM_HOME, not written.
Three sections that were missing:
- Where MeshTerm keeps its state. --db moves the database and nothing else;
the outbox, the caches, the stored passwords and the remembered device stay
in the config directory. $MESHTERM_HOME is what actually isolates a run and
was documented nowhere.
- About the examples. They come from the simulator, which opens a fresh radio
per invocation, never fills PKTS, and invents hop names on a device-routed
trace. And --mock records into the real history unless given its own home.
- When it goes wrong. The exit scheme was explained and never once shown.
Corrected: quoting is gone from listings, times are ages with --absolute behind
the cli_time_format preference, routes take arrows where paths keep commas,
contacts gained HASH and LOCATION, preferences regained DESCRIPTION, ack and
closing messages now reach stderr rather than being dropped, and the written
pages wrap. Exit 3 now covers a --port that will not open, and the usage class
covers a missing --yes, a bad --sort or --category, an unknown --profile and a
bad courier id. The awk -F'"' recipe split on quotes that no longer exist, so
the recipes go through jq; the cron entry flooded the mesh every 30 minutes,
six times tighter than the cadence picker's own floor, and is now
config advert-cadence with an absolute path for what still belongs in cron.
README: chat is not interactive, trace-path is its own command, and the exit
table is prose plus a link instead of a third copy to keep in sync.
about.md: everything-in-the-TUI-is-in-the-CLI was never true — the map,
dashboard, live feed, watchtower and mesh walk have no command — so it says
nearly, names them, and points at the JSON the claim is now worth making about.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs/cli.md documents every command and option, what each one prints, and
what it returns -- with the output conventions and the exit-status table
up front, since those are the two things a script is written against.
Every command, option and flag in it was checked against the running app;
the sample output is captured from the simulator rather than typed out.
Writing it turned up two places the CLI still did the thing the last
commit was about. A destructive command refused for want of --yes printed
its refusal in red on *stdout* and exited 1: a colour and a sentence that
is not the command's answer, in whatever was reading the command's answer.
A missing confirmation is a bad invocation, so it now goes through the
parser like any other -- nothing on stdout, the reason on stderr, exit 2.
The README keeps its cookbook and its summary of the conventions, and
points at the manual for the rest.
Co-Authored-By: Claude Opus 5 (1M context) <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>
JP saw the PicoCalc's wordmark on a desktop session. The splash picks the
widest mark that fits, the desktop one is 71 columns, and below that it swaps
to the 53 drawn for the handheld -- so the app was announcing itself as the
handheld and truncating every menu description onto the pager underneath.
The window was 64 columns. reopen() passed no window argument, so wt.exe put
the session in a *tab* of whatever window happened to be open and it inherited
that size; with no window open it would instead have taken the reader's global
launch size, which is a setting about their shell and nothing to do with this
app. Either way the app could land somewhere too small for it, and the failure
was quiet and cosmetic rather than anything that announced itself.
It now asks for a window of its own at the platform's stated minimum plus a
margin -- 80x30 on the desktop, derived from readable_cols/readable_rows so it
follows them. Windows Terminal clamps to what the display can show, so a small
screen degrades exactly as it did before.
The tab is titled MeshTerm too. It was showing the executable's full path,
which on a downloaded build is a line of Downloads folder. The icon is not ours
to set -- Windows Terminal takes that from a profile and a bare command line is
not one.
Release 0.2.8.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The classic Windows console cannot draw a single MeshTerm icon whatever font
it is given, so "reopen in Windows Terminal?" had exactly one sensible answer
-- and was being put to someone who had not seen the app yet and so had nothing
to weigh it against, at the one moment nothing had been drawn.
So it moves. It still says what it is doing and still names the way to stop it,
because a program that opens a window you did not ask for should say so.
Installing a font keeps its consent, and the split is deliberate: moving window
costs nothing and undoes itself by closing, while a font is a file written to
someone's machine that Windows will not let go of until their next logon.
The console_setup preference reads auto/off now rather than ask/off, and its
help says what it does. An older stored "ask" is not "off", so it keeps working
and simply means auto.
JP's call, and it is the right one -- the reference experience should be what
an end user gets, not what they think to ask for.
Release 0.2.7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by testing the release artifact rather than the repo. A PyInstaller
one-file build unpacks itself and re-runs itself, the halves coordinating
through _PYI* environment variables. reopen() handed the whole environment to
the new copy, so its bootloader concluded it was stage two of a launch nobody
had made, checked that its parent was the same executable, found wt.exe, and
aborted:
[PYI-33368:ERROR] Security validation failure: parent process has
different executable!
[process exited with code 4294967295]
A new window holding a message nobody can act on, with the window that offered
the move already gone. Strictly worse than not offering.
child_environment() strips them, and restores the loader paths the bootloader
parks aside. Verified against a locally frozen build: before, the bootloader
error; after, the session comes up in Windows Terminal with its emoji and its
charts.
The source build could never have shown this. Same shape as 0.2.2's empty tool
registry -- the packaged build is a platform of its own, and the only way to
test it is to run it.
Release 0.2.6.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The OFL permits bundling a font with any software, on conditions we now meet
explicitly. The file is byte-identical to Microsoft's own release (sha256
528f9214...), so it is not a Modified Version and the Reserved Font Name
clause does not apply. Condition 2 wants the copyright notice and the licence
in every copy: CascadiaMono-OFL.txt ships beside the .ttf and travels into the
bundle with the rest of the assets. Condition 5 wants the font under that
licence and no other, which the Apache LICENSE at the root would otherwise
have appeared to claim -- so NOTICE and the README now carve it out by name.
Also says plainly that Microsoft does not endorse MeshTerm, since condition 4
allows acknowledgement and nothing more.
The README's font section predated the Windows Terminal offer and named a
preference that has since been renamed; it now leads with the terminal, as the
app does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every timeline in MeshTerm is made of braille characters, and almost no
monospace font has them. Reading the cmap of every font on this machine:
Consolas, Lucida Console, Hack Nerd Font, JetBrains Mono, Fira Code and
Source Code Pro all carry zero of the 256 -- and so does DejaVu Sans *Mono*,
which is where the widespread "install DejaVu for braille" advice falls down.
DejaVu *Sans*, proportional, has all 256. That is the whole trick: terminals
fall back to a proportional face for the block, and the charts have always
been drawn by a font nobody chose.
Which is fine everywhere except the one host that does no fallback. In the
classic Windows console -- what a double-clicked build gets -- Consolas draws
57 of the 122 characters we use and none of the charts.
So MeshTerm now bundles Cascadia Mono PL (SIL OFL, licence beside it) and
offers it there. Cascadia is one of two mainstream coder fonts with the block;
the PL build adds the powerline separators the path lines want, and at 723KB
covers exactly what the 2.4MB Nerd Font build does. A machine that already has
Cascadia -- every Windows 11, anyone with Windows Terminal -- is only offered
the switch.
Both halves are verified on Windows 10 22H2, and neither needs admin: a font
installs for one user under %LOCALAPPDATA% with AddFontResourceW and an HKCU
registration, and SetCurrentConsoleFontEx selects a face the console's own
properties dialog will not even list -- that dialog offers only what is under
the machine-wide Console\TrueTypeFont key, which here is Lucida Console and
Consolas, so without that API the font could be installed and still unpickable.
Declining is answered with what declining looks like -- charts as rows of
boxes -- and asked once more, because a reader who has never seen the app
cannot picture what it costs. A second no is remembered in the console_font
preference and never raised again.
Windows locks a font file once loaded: it cannot be deleted even elevated,
even after restarting the font cache, until the next logon. So nothing here
offers to uninstall one, and the install is written to be repeatable instead
of reversible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MeshTerm died building its screen on Windows with a ctypes type error, and on a
double-clicked build the traceback and the window it printed to were destroyed
together — which is why this needed a log file before it could be looked at.
`ctypes.windll.kernel32` is a cache. Every caller in the process receives the same
object and each function on it is a shared attribute, so declaring `.argtypes` is
not configuration, it is a process-wide mutation where the last writer wins. The
emoji-width probe declared `GetConsoleScreenBufferInfo` as taking a pointer to its
own copy of the screen-buffer struct; prompt_toolkit then called that same function
object with a pointer to its own copy — same layout, different class — and ctypes
refused it.
Nothing here reaches for `ctypes.windll` any more. Five call sites take private
handles from `core/win32dll.py`, built by constructing `WinDLL` directly so each
one gets its own function cache and a declaration made for one purpose cannot be
seen by anything else. Each call returns a fresh handle, because two parts of
MeshTerm disagreeing about a signature is the same bug in miniature.
The regression test was checked against the defect rather than assumed to cover
it: with the old line restored it fails, and with the fix it passes. A test that
passes either way would only have recorded that somebody thought about the
problem.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The build to diagnose a double-click with: a window that waits after a failure, a
log that keeps what happened, and a level you can turn up before reproducing it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first build that contains the whole app. 0.2.1's binaries start and then
offer you nothing, so this replaces them rather than adding to them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Windows build launched, printed help, drew the specimen card, and showed a
menu containing one item: Quit. Not a permissions problem and nothing to do with
Bluetooth — the tools were simply not in the bundle.
`meshterm/tools/__init__.py` finds every tool with `pkgutil.iter_modules` and
imports it so its `@register` decorator fires. Nothing static ever names a tool
module, so PyInstaller had no reason to include one, and the registry came up
empty: no menu entries, no CLI subcommands, an app that starts and does nothing.
`collect_submodules("meshterm.tools")` fixes it, which is the same treatment
`bleak.backends` already had for the same reason — the spec knew about other
people's dynamic imports and not about ours.
The worse half is that CI said this was fine. The smoke test ran `--help` and
`specimen`, and a build with no tools at all passes both. It now runs a
tool-provided subcommand, so an empty registry fails the pipeline instead of
reaching a release page. Green tests that cannot see the defect are worse than no
tests, because they are believed.
`MESHTERM_HOME` comes out of the same session. Every copy of MeshTerm reads and
writes `~/.meshterm`, so trying a downloaded build meant pointing it at the same
34MB history database as the checkout it was built from, with no way to keep them
apart. The variable moves the whole directory. An empty or whitespace value falls
back rather than resolving to the current directory, because a shell exporting it
blank is common and scattering someone's history into their downloads folder is a
poor way to find that out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pushing a version tag now builds the binaries and puts them on the Releases page,
with a combined SHA256SUMS and notes lifted out of the changelog section for that
version. It stays inside GitHub — while the repository is private the release is
private too — and it is emphatically not the PyPI publish, which is `release.yml`
and still needs a person to run it on purpose.
`installers.yml` gained a `workflow_call` trigger so the release calls it instead
of keeping a second copy of the matrix. Two matrices would agree for about a month
and then quietly stop, and the files people download should be built exactly the
way the ones CI checks on every push are.
The README now leads its install section with the download rather than with pip,
because most people who want MeshTerm have a radio and a terminal and not a Python
toolchain. It also says plainly that the first launch will be blocked: these builds
are unsigned, macOS will refuse them until the quarantine attribute is cleared, and
Windows will show its SmartScreen warning. Signing costs real money on both
platforms, so the honest move is to say so and give people the checksum file rather
than let them think something is wrong.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bug form now asks what JP's Discord post asks: how often it happens, whether
there's a workaround, what hardware you're on, and which companion device — with
the note that "a Xiao" doesn't narrow anything down. Three things his post didn't
cover got added: the MeshTerm version, whether it still happens under `--mock`
(which splits an app bug from a radio bug before anyone spends an evening on it),
and whether you're on the regular or PicoCalc look, since those draw differently
enough that a bug in one often doesn't exist in the other.
The README gains the part that was missing: what to expect from a maintainer
doing this on evenings and weekends. Issues welcome, ask before opening a pull
request, no promised timelines, and a nudge on a quiet issue is fine. Report on
GitHub or on Discord, whichever you have — the real ones get moved across either
way, and being told beats filing it tidily.
Install is rewritten for someone who wants to run MeshTerm rather than work on
it. It said `pip install -e ".[dev]"`, which is a contributor's command and puts
pytest and ruff on the machine of somebody who just wants to talk to a radio. Now
it's one pip line, a pipx alternative, `--mock` for people without a radio yet,
and an honest "coming" list for installers and PyPI.
Badges now include CI, the licence and Discord, so they say something checkable
rather than only decorating. The two survey documents get a line at the top
saying they're a snapshot of a date, not a to-do list.
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>
Both README links and the About page's attribution pointed at
meshcore.co.uk. The project's home is meshcore.io; a credit is worth
nothing if it sends the reader somewhere other than to the people it
names.
The About page's line moves with the page's other uncommitted edits.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>