The Oxford comma, always, the one-line description included

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>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5.5 committed 2026-09-25 21:28:00 -04:00
1 parent 5d0bf52717
commit b7a1ad47c2
21 files changed
+46 -46

No files matched your search

+1 -1
View File
@@ -94,7 +94,7 @@ body:
description: >
Only if you are running on something the diagnostics file cannot name. It knows a
PicoCalc from a regular terminal, but a uConsole and a laptop look much the same to
it. Leave blank on a desktop, laptop or Mac.
it. Leave blank on a desktop, laptop, or Mac.
placeholder: "ClockworkPi uConsole (CM4)"
- type: input
+1 -1
View File
@@ -13,6 +13,6 @@
- [ ] I've read [CONTRIBUTING.md](../blob/main/CONTRIBUTING.md) and agree its licensing terms apply to this contribution
- [ ] `python -m pytest -q` passes, including the dual-platform gallery gate
- [ ] `ruff check .` and `ruff format --check .` are clean
- [ ] New screens, rows and hints go through the helpers in `ui/menus.py`, and follow the
- [ ] New screens, rows, and hints go through the helpers in `ui/menus.py`, and follow the
lexicon and UX standards in `CLAUDE.md`
- [ ] Anything user-visible has a line in `CHANGELOG.md` under `[Unreleased]`
+1 -1
View File
@@ -3,7 +3,7 @@
# Split in two on purpose. Style is a property of the source, not of the machine, so
# `lint` runs once and its failure is unambiguous. Behaviour is a property of the
# machine, so `test` runs everywhere we claim to work — the description promises
# Windows, macOS and Linux, and until this file existed that promise had only ever been
# Windows, macOS, and Linux, and until this file existed that promise had only ever been
# checked on one of them.
name: CI
+2 -2
View File
@@ -123,7 +123,7 @@ what the mesh is doing.
- **You can see where everyone is.** There is a street map, drawn in the terminal, with the
radios on it. You can pan it, zoom it, and search it. There is also a Time Machine: charts of
everything you have ever overheard, so you can see when a radio usually talks and how
well you have heard it, over a day, a week, a month or all of it.
well you have heard it, over a day, a week, a month, or all of it.
- **You can talk to people.** Group channels and one-to-one messages, both live. If someone
is out of range right now, the courier holds your message and delivers it when they come
@@ -158,7 +158,7 @@ what the mesh is doing.
### Getting it
One file to download, with nothing else to install, for Windows, macOS (both Intel and
Apple silicon) and Linux (including 64-bit ARM, which covers the uConsole). If you would
Apple silicon), and Linux (including 64-bit ARM, which covers the uConsole). If you would
rather use Python, `pipx install` works too, on Python 3.10 to 3.14. The README has the
exact commands.
+7 -7
View File
@@ -133,7 +133,7 @@ Whichever you took, you now have a single file: `meshterm` on macOS and Linux,
### If your terminal draws empty boxes
MeshTerm is drawn with emoji icons, braille charts and powerline path chips, and whether
MeshTerm is drawn with emoji icons, braille charts, and powerline path chips, and whether
you see them is up to your *terminal* rather than your font. Windows Terminal, VS Code's
terminal, and anything modern on macOS and Linux all draw them correctly.
@@ -247,9 +247,9 @@ connect](docs/connecting.md)** covers Bluetooth pairing on Windows, Linux, and m
## What it does
The menu asks one question — *what would you like to do?* — and answers it in two halves.
Three sections name a doing: **Message** is chat, channels, the courier outbox and
contacts; **Watch** is the dashboard, the live feed, the watchtower and the time machine;
**Explore** is the map, the mesh walk, tracing and the trophy case. Three name whose it is:
Three sections name a doing: **Message** is chat, channels, the courier outbox, and
contacts; **Watch** is the dashboard, the live feed, the watchtower, and the time machine;
**Explore** is the map, the mesh walk, tracing, and the trophy case. Three name whose it is:
**This node** is the radio in your hand, **Other nodes** is someone else's over the mesh,
and **This app** is MeshTerm itself — preferences, diagnostics, and the written pages.
@@ -338,7 +338,7 @@ are decoded with a small built-in reader for the
Both map surfaces carry the credit themselves. The full-screen map opens with
`© OpenMapTiles · Data from OpenStreetMap` in its bottom-right corner and collapses it to
`© OpenStreetMap` once you pan, zoom or type — set into the panel's bottom border rule on
`© OpenStreetMap` once you pan, zoom, or type — set into the panel's bottom border rule on
a desktop terminal, so the drawing itself keeps every cell, and on the map's own last row
on the PicoCalc, whose frame has no bottom rule. The node page's location preview shows the
short form throughout. The About page inside the app spells out the licence URL.
@@ -368,7 +368,7 @@ respective project for the authoritative terms):
| [prompt_toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) | Full-screen interactive TUI | BSD-3-Clause |
| [markdown-it-py](https://github.com/executablebooks/markdown-it-py) | CommonMark parser behind the written About pages | MIT |
| [Segno](https://github.com/heuer/segno) | Pure-Python QR codes (channel share links) | BSD-3-Clause |
| [tomli](https://github.com/hukkin/tomli) / [tomli-w](https://github.com/hukkin/tomli-w) | TOML config, preferences and device-config backups | MIT |
| [tomli](https://github.com/hukkin/tomli) / [tomli-w](https://github.com/hukkin/tomli-w) | TOML config, preferences, and device-config backups | MIT |
## License
@@ -388,7 +388,7 @@ is Microsoft's Cascadia Mono PL, redistributed unmodified under the SIL Open Fon
[CascadiaMono-OFL.txt](meshterm/assets/fonts/CascadiaMono-OFL.txt) and stays its only
license.
Each standalone build is a single file with `LICENSE`, `NOTICE` and a generated
Each standalone build is a single file with `LICENSE`, `NOTICE`, and a generated
`THIRD-PARTY-NOTICES.txt` (every dependency's own license text) bundled inside it. The same
three files are also attached to each release on the
[releases page](https://github.com/jpmartineau/MeshTerm/releases), so you can read them
+4 -4
View File
@@ -113,7 +113,7 @@ of two directories from the same shell.
**`--db` alone does not isolate a run.** It moves the database, which is most of the
weight but none of the identity: the contact and channel caches, the outbox, the stored
admin passwords and the remembered device all live beside it in the config directory and
admin passwords, and the remembered device all live beside it in the config directory and
would still be the ones you use every day. To run against a scratch state — a test, a
demo, a second radio — set `MESHTERM_HOME`, and set `--db` inside it if you want the
database somewhere else again. The same holds for the database `--mock` picks for itself:
@@ -147,8 +147,8 @@ Three things the simulator does that a radio does not, all visible in the sample
> **`--mock` records into its own history, not yours — but it still writes the caches.**
> The simulator is a fake radio, not a fake MeshTerm: what it adverts is written down like
> anything else. The database it writes is `meshterm-mock.db`, so its invented nodes stay
> out of your mesh walk, dashboard and map; naming a database with `--db` overrides that
> choice. The contact and channel caches, the outbox and the remembered devices are still
> out of your mesh walk, dashboard, and map; naming a database with `--db` overrides that
> choice. The contact and channel caches, the outbox, and the remembered devices are still
> the ones you use every day, so a demo that should touch nothing at all gets a home of
> its own: `MESHTERM_HOME=... meshterm --mock …`.
@@ -266,7 +266,7 @@ fields; a paragraph has no fields, and unwrapped it is a 600-cell line no termin
read.
**stdout is the answer; everything else is stderr** — errors (`meshterm: what went
wrong`), progress bars, log records, acknowledgements and closing messages:
wrong`), progress bars, log records, acknowledgements, and closing messages:
```console
$ meshterm config set radio_sf 9
+2 -2
View File
@@ -7,7 +7,7 @@ shell loops.
Nearly every menu option is also a subcommand — ideal for scripting, cron, and bots. The
live pictures stay in the menu, where their meaning is (the map, dashboard, live feed,
watchtower and mesh walk); everything else has a command, and every command speaks
watchtower, and mesh walk); everything else has a command, and every command speaks
`--json`. **[`docs/cli.md`](cli.md) is the full manual**: every command and option,
what each one prints on both faces, and the exit statuses. A taste:
@@ -70,7 +70,7 @@ allowed to be comfortable about it: listings are `ps`-style aligned records with
route is drawn with arrows — `MockCompanion (00) → Yagi-Repeater (a1) → Alice (d4)`.
`--absolute` swaps every age back for an ISO-8601 instant. A *path*, the spec `--path`
takes back, stays comma-separated hex. `-` is the one token for absent. Errors,
acknowledgements and progress all go to stderr, so a redirect catches only the answer.
acknowledgements, and progress all go to stderr, so a redirect catches only the answer.
The **JSON face** is the machine contract, and `--json` works on **every** command:
+2 -2
View File
@@ -64,8 +64,8 @@ pages close it.
| Feature | What it does | Scripted |
| --- | --- | --- |
| **⚙ Preferences** | How MeshTerm behaves, in eight groups: Device, Sending, TX optimize, Watchtower, Map, History, Display and Diagnostics. Grouped and staged, saved by one action at the bottom, and written to `preferences.toml` as only the values you changed. [Configuring MeshTerm](configuration.md) has the detail. | `meshterm preferences show / set / reset` |
| **🩺 Diagnostics** | Everything a bug report opens with, in one block: which build this is and how it was installed, the OS, the terminal and its size, the boot-time verdicts that show on no screen (icons, powerline, platform flavour and why), the radio and its link, and how much history is stored. Nothing private — no keys, passwords, positions or contact names; the mesh is described as per-table counts and the span they cover. `s` saves it beside the log for attaching. | `meshterm diagnostics [--out PATH]` |
| **⚙ Preferences** | How MeshTerm behaves, in eight groups: Device, Sending, TX optimize, Watchtower, Map, History, Display, and Diagnostics. Grouped and staged, saved by one action at the bottom, and written to `preferences.toml` as only the values you changed. [Configuring MeshTerm](configuration.md) has the detail. | `meshterm preferences show / set / reset` |
| **🩺 Diagnostics** | Everything a bug report opens with, in one block: which build this is and how it was installed, the OS, the terminal and its size, the boot-time verdicts that show on no screen (icons, powerline, platform flavour, and why), the radio and its link, and how much history is stored. Nothing private — no keys, passwords, positions, or contact names; the mesh is described as per-table counts and the span they cover. `s` saves it beside the log for attaching. | `meshterm diagnostics [--out PATH]` |
| **📖 About MeshTerm** | What MeshTerm is, where it came from, and the terms it ships under — including the map and font credits and the licence URL. | `meshterm about` |
| **👤 About the author** | The person behind MeshTerm, on the mesh and off it. | `meshterm about-author` |
| **🔗 Join Discord** | The community server: one invite link, and a QR of it for a phone to read. | `meshterm discord` |
+1 -1
View File
@@ -1,7 +1,7 @@
# MeshTerm on hardware
MeshTerm talks to a **node** — a MeshCore radio that speaks the companion protocol over
serial, Bluetooth LE or TCP. Plug a companion board into USB, or pair one over Bluetooth,
serial, Bluetooth LE, or TCP. Plug a companion board into USB, or pair one over Bluetooth,
and none of this page applies: MeshTerm finds it, and the [README](../README.md) and the
[command line manual](cli.md) are all you need.
+6 -6
View File
@@ -25,8 +25,8 @@ a radio in the meantime.
> errors, and parts of it will go out of date as boards, images and firmware change. It is
> up to you to check every fact in it against your own hardware and the current sources
> before you act on it, and every decision along the way is yours. The author accepts no
> responsibility for any damage, loss or injury that results from following this guide,
> and nothing in it is a warranty that your parts, your tools or your hands will behave the
> responsibility for any damage, loss, or injury that results from following this guide,
> and nothing in it is a warranty that your parts, your tools, or your hands will behave the
> way the author's did. If you are not comfortable with any step, stop and get help from
> someone who is.
@@ -213,7 +213,7 @@ machine it describes. Read this section before you open the shell.
Follow ClockworkPi's own
[assembly guidelines](https://github.com/clockworkpi/PicoCalc/blob/master/Clockwork_PicoCalc_Assembly_Guidelines.pdf)
(a PDF in the [PicoCalc repository](https://github.com/clockworkpi/PicoCalc), which also
has a wiki) to put the kit together: mainboard, screen, keyboard, speakers and shell. Insert your 18650
has a wiki) to put the kit together: mainboard, screen, keyboard, speakers, and shell. Insert your 18650
battery (or two), watching the `+`/`-` marks in the battery compartment.
⚠ Two of the warnings in [Three ways to break it](#three-ways-to-break-it) apply here: tape
@@ -223,7 +223,7 @@ there is no reason to make the screws tight now.
Power it on once with the **stock Pico still in place**. You should see the stock BASIC
firmware boot on the screen, and the keyboard should respond. This confirms the screen,
keyboard and battery are all good before you start swapping boards.
keyboard, and battery are all good before you start swapping boards.
Power it back off.
@@ -756,7 +756,7 @@ those. Keep the iron on each pin only as long as it takes, and don't press down
board. Attach the LoRa antenna to the Wio-SX1262's u.FL connector before you
power anything back on — running the radio without an antenna attached can damage it.
Wrap the XIAO + Wio-SX1262 stack in Kapton tape so no pad can touch the Lyra, the
mainboard or a battery, then tuck it into the shell wherever it fits without straining the
mainboard, or a battery, then tuck it into the shell wherever it fits without straining the
wires; a strip of foam tape holds it in place against the inside of the case.
### Step 15 — Set up the Lyra
@@ -914,7 +914,7 @@ The rest is one bench's findings, and a second device may disagree:
on one board, not read from a reference manual.
- **That the Calculinux kernel permits those `/dev/mem` writes**, and that the mux oneshot
really runs at every boot.
- **The header pin numbers** 6, 7, 8 and 36 for GP4, GP5, GND and 3V3. The Pico-side
- **The header pin numbers** 6, 7, 8, and 36 for GP4, GP5, GND, and 3V3. The Pico-side
numbering is right; the carrier's wiring is a bench observation.
- **The MX1.25 socket's pin order.** Taken from the wire colours of Luckfox's own cable,
which is why [Step 3](#step-3--attach-the-wi-fi-dongle) has you check pin 1 with a meter.
+1 -1
View File
@@ -3,7 +3,7 @@
Why MeshTerm looks right in some terminals and draws empty boxes in others, what it does
about it on Windows, and how to turn all of that off.
MeshTerm is drawn with emoji icons, braille charts and powerline path chips. Whether you
MeshTerm is drawn with emoji icons, braille charts, and powerline path chips. Whether you
see them is up to your *terminal*, not really your font: a modern terminal, asked for a
character its font doesn't have, quietly borrows it from another font on the machine.
That's why the app looks right in Windows Terminal, in VS Code's terminal, and on macOS
+5 -5
View File
@@ -15,7 +15,7 @@ That's [further down](#always-on-the-bridge) — most people want the section ab
> user to hardware groups, and then keys a LoRa transmitter. It assumes you are comfortable
> at a Linux shell and know which frequency plan and power limits apply where you live —
> what the radio does on air is your responsibility, not MeshTerm's. There is a real risk
> of leaving the system, the radio or the mesh around you in a worse state if a step goes
> of leaving the system, the radio, or the mesh around you in a worse state if a step goes
> wrong. You do this at your own risk.
>
> **Read the whole manual through once before you start.** Which install route you take
@@ -23,10 +23,10 @@ That's [further down](#always-on-the-bridge) — most people want the section ab
> steps.
>
> This is a guide, not a prescription. Despite the author's best efforts it may contain
> errors, and parts of it will go out of date as packages, runtimes and boards change. It
> errors, and parts of it will go out of date as packages, runtimes, and boards change. It
> is up to you to check every fact in it against your own hardware and the current sources
> before you act on it, and every decision along the way is yours. The author accepts no
> responsibility for any damage, loss or injury that results from following this guide.
> responsibility for any damage, loss, or injury that results from following this guide.
- [What you need](#what-you-need)
- [Step 1 — Prepare the system](#step-1--prepare-the-system)
@@ -194,7 +194,7 @@ frees the GPIO lines and SPI handle the moment its process does. The trade-off i
this implies: **the node is off the mesh while MeshTerm is closed.** If you want it on all
the time, that's what [the bridge](#always-on-the-bridge) is for.
**Where its state lives.** Each radio keeps its identity, preferences, channels and
**Where its state lives.** Each radio keeps its identity, preferences, channels, and
contacts under `~/.meshterm/radio/spidev1.0/` — `identity.key`, `prefs.json`,
`channels.json`, `contacts.json` — plus a running log, `node.log` (the previous run's is
kept as `node.log.1`). All of it survives a restart.
@@ -557,7 +557,7 @@ Then `systemctl --user restart meshterm-spi-bridge.service`.
| Variable | What it sets |
| --- | --- |
| `MESHCORE_BUS_ID`, `MESHCORE_CS_ID`, `MESHCORE_CS_PIN` | which SPI bus, device and chip-select pin the radio is on |
| `MESHCORE_BUS_ID`, `MESHCORE_CS_ID`, `MESHCORE_CS_PIN` | which SPI bus, device, and chip-select pin the radio is on |
| `MESHCORE_RESET_PIN`, `MESHCORE_BUSY_PIN`, `MESHCORE_IRQ_PIN` | the three control lines |
| `MESHCORE_FREQUENCY`, `MESHCORE_TX_POWER` | frequency in Hz, power in dBm |
| `MESHCORE_SPREADING_FACTOR`, `MESHCORE_BANDWIDTH`, `MESHCORE_CODING_RATE` | the modem preset — all three must match the mesh you are joining. Whole numbers only: bandwidth in Hz (`62500`), coding rate as the denominator (`5` for 4/5). A decimal or `4/5` stops the bridge at startup. |
+1 -1
View File
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
"""MeshTerm is a full-featured TUI MeshCore client for your terminal.
Connects to a companion over USB, Bluetooth or TCP. For Windows, macOS and Linux.
Connects to a companion over USB, Bluetooth, or TCP. For Windows, macOS, and Linux.
"""
from datetime import datetime
+5 -5
View File
@@ -127,8 +127,8 @@ app = typer.Typer(
# `meshterm.__doc__` — if this drifts from that, one of them is lying.
help=(
"MeshTerm is a full-featured TUI MeshCore client for your terminal. "
"Connects to a companion over USB, Bluetooth or TCP. "
"For Windows, macOS and Linux."
"Connects to a companion over USB, Bluetooth, or TCP. "
"For Windows, macOS, and Linux."
),
epilog=EXIT_STATUS_EPILOG,
)
@@ -593,9 +593,9 @@ def _report_no_windows_console(console: Console) -> None:
console.print()
console.print("That usually means one of two things:")
console.print()
console.print(" • You are in Git Bash, MSYS or Cygwin. Those are not Windows consoles.")
console.print(" Open [accent]Windows Terminal[/accent] or [accent]PowerShell[/accent]")
console.print(" and run it from there.")
console.print(" • You are in Git Bash, MSYS, or Cygwin, which are not Windows")
console.print(" consoles. Open [accent]Windows Terminal[/accent] or")
console.print(" [accent]PowerShell[/accent] and run it from there.")
console.print(" • The output is piped or redirected somewhere. The full-screen menu")
console.print(" needs the terminal itself.")
console.print()
+1 -1
View File
@@ -513,7 +513,7 @@ def validate_new_name(name: str, taken: Iterable[str] = ()) -> str:
bad = sorted({ch for ch in bare if not _is_name_char(ch)})
if bad:
raise RegionNameError(
f"a repeater refuses {' '.join(bad)} in a region name — letters, digits and - only"
f"a repeater refuses {' '.join(bad)} in a region name — letters, digits, and - only"
)
if bare in set(taken):
raise RegionNameError(f"{bare} is already a region here")
+1 -1
View File
@@ -543,7 +543,7 @@ REPEATER_SETTINGS: tuple[RemoteSetting, ...] = (
label="Bridge type",
category="Bridge",
writable=False,
help="The bridge this firmware was built with: none, rs232 or espnow",
help="The bridge this firmware was built with: none, rs232, or espnow",
),
RemoteSetting(
key="bridge.enabled",
+1 -1
View File
@@ -55,7 +55,7 @@ class DiagnosticsTool(Tool):
name = "diagnostics"
title = "Diagnostics"
icon = "🩺"
help = "Version, host, terminal and device facts for a bug report"
help = "Version, host, terminal, and device facts for a bug report"
category = "This app"
order = 7 # after Preferences (the row that changes MeshTerm), before the pages
+1 -1
View File
@@ -1598,7 +1598,7 @@ async def _archive_contact(ctx: AppContext, contact: Contact, self_key: str, lab
)
prompt.append(
"? It comes off this device's contact list, freeing a slot for a new one. MeshTerm "
"keeps it — with its key, reception history and messages — and you can restore it "
"keeps it — with its key, reception history, and messages — and you can restore it "
"at any time.",
style="warn",
)
+1 -1
View File
@@ -629,7 +629,7 @@ async def _add_region(
prompt=f"Name of the new region, {inside}:",
default=values[slot] or "",
validate=check,
help_text="letters, digits and -, up to 30 bytes",
help_text="letters, digits, and -, up to 30 bytes",
floating=True,
)
return typed.strip() if typed else None
+1 -1
View File
@@ -661,7 +661,7 @@ async def _sweep(ctx: AppContext, self_key: str, victims: list[ScoredContact]) -
if not await ctx.ui.dialog(
f"Archive {_count_desc(len(victims))}? They come off this device's contact list, "
"freeing space for new ones. MeshTerm keeps them — with their keys, reception "
"history and messages — under Archived contacts, and any of them can be restored.",
"history, and messages — under Archived contacts, and any of them can be restored.",
[("Cancel", False), ("Archive", True)],
title="Archive contacts",
default=1,
+1 -1
View File
@@ -12,7 +12,7 @@ build-backend = "hatchling.build"
# wheel shipping the `meshterm/` package under this different name.
name = "mesh-term"
dynamic = ["version"]
description = "MeshTerm is a full-featured TUI MeshCore client for your terminal. Connects to a companion over USB, Bluetooth or TCP. For Windows, macOS and Linux."
description = "MeshTerm is a full-featured TUI MeshCore client for your terminal. Connects to a companion over USB, Bluetooth, or TCP. For Windows, macOS, and Linux."
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Jean-Pierre Martineau" }]