9 Commits
Author SHA1 Message Date
Jean-Pierre MartineauandClaude Opus 5 2ea8f9fb83 MeshTerm 0.2.1
Installers / windows-x64 (push) Canceled after 0s
Installers / macos-x64 (push) Canceled after 0s
Installers / macos-arm64 (push) Canceled after 0s
Installers / linux-x64 (push) Canceled after 0s
Installers / linux-arm64 (push) Canceled after 0s
GitHub release / build (push) Canceled after 0s
GitHub release / create the release (push) Canceled after 0s
Closes out the changelog's Unreleased section under a date, which is the whole
ceremony: everything since 0.2.0 was written down as it landed, so cutting a
release is renaming a heading rather than reading a week of commits back.

0.2.0's link points at its tag rather than a release page, because 0.2.0 was
never released anywhere — it was tagged locally before there was anything to
build or anywhere to put it. 0.2.1 is the first version that exists outside this
machine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 13:30:11 -04:00
Jean-Pierre MartineauandClaude Opus 5 eb61c55f22 The Intel Mac leg asks for a runner that still exists
`macos-13` is retired. GitHub's image list starts at macOS 14 now, and the
current x86_64 labels are `macos-15-intel` and `macos-26-intel`.

The failure mode is worth writing down, because it is not the one you would
expect: a job asking for a retired label does not fail. It queues, forever,
and holds the entire run open behind it — so the four legs that had already
built sat there under a run that never finished, and anything waiting on that
workflow would have waited with them. That includes the release workflow, which
calls it, so a tag pushed today would have produced no release and no error to
explain why.

`macos-15-intel` rather than `-26-intel`, for the same reason the Python in this
matrix is 3.10: built against an older system, runs on more machines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:30:20 -04:00
Jean-Pierre MartineauandClaude Opus 5 0c12bd4fcf Somewhere to download a build from
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>
2026-09-07 12:25:03 -04:00
Jean-Pierre MartineauandClaude Opus 5 ebb3c80f15 Pin the formatter, and stop it editing the record
The first CI run went red on `ruff format --check`, on a tree that was clean on
this machine. Three separate problems, all found by pushing.

`ruff>=0.4` is not a pin. CI installed 0.16.6 where the laptop had 0.15.18, and
0.16 formats Python code blocks inside Markdown while 0.15 leaves them alone — so
the two disagreed about whether the repository was formatted, which is the one
thing a formatter exists to settle. Pinned to the 0.16 line. Dependabot will
offer the next one, and CI will say whether it is safe.

The file it tripped over is `docs/survey-ui-consistency.md`, which quotes four
lines of a dialog call as it looked on 22 August. That excerpt is a fragment —
two arguments, no call around them — so the formatter parsed it as two statements
and rewrote them into a tuple and an assignment that never existed anywhere. The
surveys are dated records of a past state, so `docs/` is now excluded: a
formatter editing a quotation is not tidying, it is falsifying.

And the installer workflow was firing for branches it had no business building.
Dependabot opened three action bumps within a minute of the push; each one edits
`.github/workflows/installers.yml`, each matched its own path filter, and each
started a five-platform build. `branches: [main]` stops it.

Everything else was green on the first try, including the part that was actually
uncertain: all seven test legs passed, macOS among them, and the installers built
on Windows, macOS on Apple silicon, and Linux on both x64 and arm64 — so the
uConsole build works and the ARM runner was available after all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:01:35 -04:00
Jean-Pierre MartineauandClaude Opus 5 4f60f44512 A build you can download and run without Python
Most people who want MeshTerm have a radio and a terminal, not a Python
toolchain. This is the install for them: one file, download it, run it.

`packaging/meshterm.spec` freezes the app with PyInstaller in console mode —
which is not a detail, since a windowed build of a terminal program starts with
nowhere to draw and exits. The assets land at `meshterm/assets` inside the bundle
so that `Path(__file__).parent.parent / "assets"` keeps resolving exactly as it
does in a checkout, and bleak's per-platform backends are named as hidden imports
because nothing static ever sees the one that actually gets used. UPX is off: a
few megabytes are not worth being flagged by antivirus on a first release.

The build was run here rather than only written. The Windows executable comes out
at 14 MB, runs from a directory with no source tree in sight, and draws the
specimen card — which is the real test, because the specimen reads the asset
files.

`installers.yml` does the same on five machines, because frozen builds cannot be
cross-compiled. Windows, macOS on both Apple silicon and Intel (plenty of people
still have one, and an arm64 build will not run for them), and Linux on x64 and
ARM64 — the last for the uConsole. Each leg runs the binary from a temporary
directory before uploading it, so a build that only works beside its own source
fails on the runner instead of on somebody's laptop, and each one gets a checksum
written beside it. It publishes nothing; attaching these to a release stays a
separate decision.

Also here: `meshterm --help` now opens with the same sentence as the package
summary, rather than a fourth description of the project written in its own way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 05:00:22 -04:00
Jean-Pierre MartineauandClaude Opus 5 ab8ef2aa23 Say how the project is run, and ask better bug questions
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>
2026-09-07 04:56:51 -04:00
Jean-Pierre MartineauandClaude Opus 5 628b665420 The project can be joined, reported to, and argued with
The rest of the community files, which are the half of open-sourcing that is not
code: how someone tells you about a hole, what happens if a thread turns nasty,
and what you will be asked when they file a bug.

SECURITY.md points at GitHub's private vulnerability reporting rather than an
address that will be scraped, and promises only what one person in his spare time
can keep — an acknowledgement inside a week, which is not a fix inside a week, and
a decision inside ninety days. Its scope section is the MeshTerm-specific part: a
channel key or an admin password reaching a log, an export or the screen is a
vulnerability here, a crafted packet corrupting the observation database is one,
and a hole in the MeshCore firmware or in `bleak` is somebody else's to fix. It
also says plainly that the database is not encrypted, because a backup of it
carries a mesh's whole history.

The code of conduct is Contributor Covenant 2.1, taken verbatim from source rather
than paraphrased, with a real address in the enforcement section.

The bug form asks the seven things that otherwise get asked in every thread:
version, flavour, transport, OS and Python, terminal and its size, companion
firmware. Flavour is there because the PicoCalc draws itself in 53 columns and
sixteen colours, so a layout bug in one flavour frequently does not exist in the
other. It asks for the version from the splash screen rather than from
`meshterm --version`, which does not exist yet.

Dependabot is weekly and capped low, with minor and patch grouped into one pull
request: a solo maintainer facing five dependency PRs every Monday stops reading
any of them. Funding points at the three destinations the app's own support page
already offers, so the two cannot drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 04:31:32 -04:00
Jean-Pierre MartineauandClaude Opus 5 117b958edf Everything PyPI needs, and no way to reach it by accident
The distribution is named `mesh-term`, because `meshterm` is on PyPI's prohibited
list and a request to free it is 319 deep in a queue. The name after
`pip install` is the only thing that changes: the import package is still
`meshterm`, the console script is still `meshterm`, and nothing in the code reads
the distribution name, so this is one line rather than a rename. Proven rather
than assumed — the wheel was built, installed into a clean interpreter, and the
command run from outside the source tree.

The rest is what a stranger sees on a package page and what the app never shows
them: project URLs, trove classifiers for the console, the three operating
systems and the four Python versions, and the licence moved to its PEP 639
expression form. No `License ::` classifier, which that form forbids, and no
`Typing :: Typed` either — there is no `py.typed` marker, and a classifier that
claims something untrue is worse than an absent one.

`release.yml` publishes to PyPI and is built to be hard to fire. It has no
`push: tags` trigger, and that absence is the design: a workflow that published
on a `v*` tag would make tagging v0.9.0 on launch day claim a name on a public
index, permanently, as a side effect of an unrelated action — and PyPI has no
rename and no real delete. So a human runs it from the Actions tab and types the
version, the job compares that against the version the tree actually built and
refuses on a mismatch, and a `pypi` environment can hold it for a reviewer.
Trusted Publishing means there is no API token in the repository to leak. It
publishes whatever version is committed; there is no version argument, so it
cannot publish something other than what is there.

CI gains a packaging job that does the same build and clean-install check on every
push, publishing nothing. Packaging breaks quietly, and the alternative is finding
out on the one day that is worst for finding out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 04:00:56 -04:00
Jean-Pierre MartineauandClaude Opus 5 365df236b9 The promises in CONTRIBUTING are checked by a machine
`.github/` starts here: the three commands the contributing guide asks a
contributor to run, run for them on every push and pull request.

Two jobs rather than one. Style is a property of the source, so `lint` runs once
on Linux and a failure means one thing. Behaviour is a property of the machine,
so `test` runs across the systems the description now promises — Windows, macOS
and Linux — which until this file existed had only ever been checked on one of
them, whatever the description said.

The matrix is shaped for a private repository, where minutes are metered and
billed by a multiplier: Linux 1x, Windows 2x, macOS 10x. Linux therefore carries
the whole 3.10 to 3.13 range, and the expensive runners cover its ends, since a
version-dependent break shows up at an end rather than in the middle. Seven legs,
about fifty billed minutes a run. The comment says to widen it to the full
product once the repo is public and standard runners stop costing anything.

Two small decisions worth their lines. `fail-fast` is off: the point of a run is
the whole picture of what broke, and a first red leg cancelling its siblings
hides the rest until the next push. And `concurrency` cancels a superseded run,
because a second push makes the first run's answer obsolete and those are metered
minutes spent on a stale commit.

One line of `.gitignore` came with it. The catch-all `.*/` that hides `.venv`
and the caches was hiding `.github/` too — the same rule `.claude/` already needed
an exception to. It fails silently in the worst way: a workflow that is never
committed looks exactly like a workflow that never runs.

The version risk this was expected to surface is already gone: the suite was run
locally on 3.13 as well as the usual 3.10, and both report 1888 passing. What
remains genuinely unchecked is macOS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 03:49:42 -04:00
22 changed files with 1108 additions and 17 deletions

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
# Kept in step with meshterm/assets/pages/support.md deliberately. If the app offers a
# way to chip in and this file doesn't, the two have drifted and one of them is lying.
buy_me_a_coffee: johnputer
custom:
- "https://www.patreon.com/c/MeshTerm"
- "https://tinyurl.com/SupportMeshTerm"
+172
View File
@@ -0,0 +1,172 @@
name: Bug report
description: Report a problem with MeshTerm
labels: [bug]
body:
- type: markdown
attributes:
value: |
Hopefully you never need this form.
If you do: how much of this you fill in really does decide what happens next. A
report I can act on gets worked on. A vague one usually doesn't, because I can't
do anything with it. If it isn't worth describing properly, it's probably not
worth fixing.
Most of these take ten seconds each.
- type: textarea
id: problem
attributes:
label: What's the problem
description: What happens, and what you expected to happen instead.
validations:
required: true
- type: dropdown
id: how-often
attributes:
label: How often does it happen
options:
- Every single time
- Often, but not always
- Now and then, no pattern I can see
- Once, and I haven't seen it since
validations:
required: true
- type: textarea
id: repro
attributes:
label: How do I make it happen
description: >
The steps, in order, starting from launching MeshTerm. Screens and keys beat
descriptions — "Contacts, Enter on a node, Routes tab, Enter on the second route"
is worth more than "open a trace".
placeholder: |
1.
2.
3.
validations:
required: true
- type: textarea
id: workaround
attributes:
label: Is there a way around it
description: Anything that avoids it, or gets you unstuck. Say so even if it's ugly.
- type: input
id: version
attributes:
label: MeshTerm version
description: It's on the splash screen when you start, and on the About page.
placeholder: "0.2.0"
validations:
required: true
- type: input
id: hardware
attributes:
label: What are you running MeshTerm on
description: >
Desktop, laptop, Mac, or a handheld — and if it's a handheld, which one.
placeholder: "ClockworkPi uConsole (CM4), or: desktop PC"
validations:
required: true
- type: input
id: os
attributes:
label: Which OS, exact version
placeholder: "Windows 11 24H2 / macOS 15.3 / Debian 12"
validations:
required: true
- type: input
id: terminal
attributes:
label: Which terminal, and how big
description: >
PowerShell, Windows Terminal, iTerm2, the PicoCalc console, ssh from something.
Size in columns x rows if you know it — a lot of layout bugs are really terminal
bugs.
placeholder: "Windows Terminal, 120x40"
validations:
required: true
- type: input
id: companion
attributes:
label: Which companion device
description: >
Be precise. There are a lot of these and they don't behave the same. "A Xiao"
doesn't narrow it down — "Seeed XIAO nRF52840 + Wio-SX1262" does. Leave blank if
no radio was involved.
placeholder: "Heltec V3 / RAK4631 / XIAO nRF52840 + Wio-SX1262"
- type: input
id: firmware
attributes:
label: Which firmware version is on it
placeholder: "MeshCore 1.15.0"
- type: dropdown
id: transport
attributes:
label: How is it connected
options:
- USB / serial
- Bluetooth
- TCP
- SPI-to-TCP bridge
- Something else (say so below)
- No radio — simulator / --mock
validations:
required: true
- type: dropdown
id: flavour
attributes:
label: Regular or PicoCalc
description: >
MeshTerm draws itself differently on a PicoCalc — narrower, fewer colours, a
different font. A bug in one often doesn't exist in the other, so this saves a
round of guessing.
options:
- Regular (desktop, laptop, ssh)
- PicoCalc handheld
- Not sure
validations:
required: true
- type: dropdown
id: mock
attributes:
label: Does it still happen with `meshterm --mock`
description: >
`--mock` runs the whole app against a fake radio. If the bug survives that, it's
in MeshTerm and I can chase it without hardware. If it doesn't, it's in the link
or the radio, which is a different hunt. Skip it if the bug is about connecting
in the first place.
options:
- "Yes, still happens with --mock"
- "No, --mock is fine"
- "Haven't tried"
- "Doesn't apply"
- type: textarea
id: error
attributes:
label: The exact error, if there was one
description: >
Copy it as text rather than photographing the screen — text can be searched, and
a photo of a terminal is hard to read. Paste the whole traceback if it crashed.
render: text
- type: textarea
id: extra
attributes:
label: Anything else
description: >
Did it used to work? Did it start after an update? Screenshots of a wrong-looking
screen are genuinely useful here, even though text is better for errors.
+13
View File
@@ -0,0 +1,13 @@
blank_issues_enabled: false
contact_links:
- name: Not sure it's a bug? Ask on Discord
url: https://discord.gg/AZwe5Uvb3S
about: >
Setup trouble, "is it supposed to do this", or anything you want a second opinion
on. The support forum is the place. Someone running your radio has probably hit it.
- name: No GitHub account? Report it on Discord instead
url: https://discord.gg/AZwe5Uvb3S
about: >
Drop reproducible bugs in #bugs and they'll get moved here. Being told beats
filing it tidily.
+30
View File
@@ -0,0 +1,30 @@
name: Feature request
description: Suggest a feature or an improvement
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
MeshTerm is one person's spare time, so the honest answer to a good idea is often
"yes, eventually". Saying what you are actually trying to do — rather than the
feature you have in mind — often finds something that already works.
- type: textarea
id: problem
attributes:
label: What are you trying to do
description: The situation you are in, not the feature. What are you trying to find out about your mesh?
validations:
required: true
- type: textarea
id: idea
attributes:
label: What you had in mind
description: If you have a shape in mind — a screen, a key, a subcommand — describe it.
- type: textarea
id: today
attributes:
label: What you do today instead
description: The workaround, if there is one. This says more about how much the feature is worth than anything else.
+16
View File
@@ -0,0 +1,16 @@
## What this changes, and why
<!-- The why matters more than the what — the diff already says the what. -->
## Related issue
<!-- CONTRIBUTING asks for an issue first on anything non-trivial, so a large PR with no
issue behind it will get a conversation before it gets a review. Link it here. -->
## Before you mark it ready
- [ ] `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
lexicon and UX standards in `CLAUDE.md`
- [ ] Anything user-visible has a line in `CHANGELOG.md` under `[Unreleased]`
+26
View File
@@ -0,0 +1,26 @@
# Weekly, and small on purpose. A solo maintainer reviewing five open dependency PRs
# every Monday stops reviewing them at all, so the caps are low enough to stay readable.
version: 2
updates:
- package-ecosystem: pip
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
groups:
# One PR for the routine noise; anything major still arrives on its own so it
# gets read on its own.
minor-and-patch:
update-types: [minor, patch]
commit-message:
prefix: deps
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 3
commit-message:
prefix: ci
+115
View File
@@ -0,0 +1,115 @@
# The three commands CONTRIBUTING.md asks a contributor to run, run for them.
#
# 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
# checked on one of them.
name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
# Nothing here writes to the repository; say so rather than inherit the default.
permissions:
contents: read
# A second push while the first is still running makes the first answer obsolete.
# Cancel it — on a private repo those are metered minutes spent on a stale commit.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
name: lint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.10"
cache: pip
cache-dependency-path: pyproject.toml
- run: pip install -e ".[dev]"
# Both gates, and the second runs even when the first fails: they report
# different things and seeing only the earlier one wastes a round trip.
- run: ruff check .
- if: ${{ !cancelled() }}
run: ruff format --check .
test:
name: test ${{ matrix.os }} py${{ matrix.python }}
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
# Let every leg finish. The point of a run is the whole picture of what broke,
# and one red leg cancelling its siblings hides the rest until the next push.
fail-fast: false
matrix:
# Shaped for a *private* repo, where minutes are metered and billed by a
# multiplier: Linux 1x, Windows 2x, macOS 10x. So Linux carries the full
# Python range and the costly runners cover the ends of it, which is where a
# version-dependent break actually shows up.
#
# Once the repo is public, standard runners are unmetered: widen this to every
# OS times every version and stop thinking about it.
include:
- { os: ubuntu-latest, python: "3.10" }
- { os: ubuntu-latest, python: "3.11" }
- { os: ubuntu-latest, python: "3.12" }
- { os: ubuntu-latest, python: "3.13" }
- { os: windows-latest, python: "3.10" }
- { os: windows-latest, python: "3.13" }
- { os: macos-latest, python: "3.13" }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
cache: pip
cache-dependency-path: pyproject.toml
- run: pip install -e ".[dev]"
# Includes tests/test_gallery.py, the dual-platform readability gate: every
# screen has to stay readable at 72 columns regular and 53 on the PicoCalc, and
# every PicoCalc case is a hard failure rather than a warning.
- run: python -m pytest -q
# Builds the distributions and checks them the way PyPI would, publishing nothing.
# Worth its minute: packaging breaks silently and is otherwise only discovered on
# the one day you least want to discover it. The wheel is installed into a clean
# environment and the command run from outside the source tree, because "it built"
# and "it works when installed" are different claims.
build:
name: build
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install build twine
- run: python -m build
- run: twine check dist/*
- name: Install the wheel clean and run the command
run: |
python -m venv /tmp/fresh
/tmp/fresh/bin/pip install --quiet dist/*.whl
cd /tmp && /tmp/fresh/bin/meshterm --help > /dev/null
echo "the wheel installs and the console script runs"
+103
View File
@@ -0,0 +1,103 @@
# Puts the downloadable builds on the repository's Releases page.
#
# Nothing here leaves GitHub. While the repository is private the release is private too,
# visible and downloadable only by people who can see the repo. This is *not* the PyPI
# publish — that is `release.yml`, it is a separate act, and it does not run from here.
#
# Triggered by pushing a version tag, which is the moment a release exists in git anyway:
#
# git tag -a v0.3.0 -m "MeshTerm 0.3.0"
# git push origin v0.3.0
#
# The binaries are built by `installers.yml`, called rather than copied, so the files
# people download are made exactly the way the ones CI checks on every push are.
name: GitHub release
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag:
description: "Existing tag to build a release from (e.g. v0.2.0)"
required: true
type: string
prerelease:
description: "Mark as a pre-release (keeps it out of 'Latest')"
type: boolean
default: false
permissions:
contents: read
jobs:
build:
uses: ./.github/workflows/installers.yml
publish:
name: create the release
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
# The one job that needs it: creating a release writes to the repository.
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag || github.ref }}
# Every platform's artifact, flattened into one directory of files to attach.
- uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Gather the binaries and their checksums
id: gather
shell: bash
run: |
mkdir -p release
find artifacts -type f -exec mv {} release/ \;
ls -lh release/
# One combined checksum file is easier to verify against than five loose ones.
(cd release && cat *.sha256 > SHA256SUMS && rm -f *.sha256)
tag="${{ inputs.tag || github.ref_name }}"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "version=${tag#v}" >> "$GITHUB_OUTPUT"
- name: Take the release notes from the changelog
id: notes
shell: bash
env:
VERSION: ${{ steps.gather.outputs.version }}
run: |
# The section for this version, between its own heading and the next one. If the
# changelog has nothing for it, say so plainly rather than inventing notes.
if awk -v v="$VERSION" '
$0 ~ "^## \\[" v "\\]" {found=1; next}
found && /^## \[/ {exit}
found {print}
' CHANGELOG.md | sed '/^$/d' | head -1 | grep -q .; then
awk -v v="$VERSION" '
$0 ~ "^## \\[" v "\\]" {found=1; next}
found && /^## \[/ {exit}
found {print}
' CHANGELOG.md > notes.md
else
echo "No changelog entry for $VERSION." > notes.md
fi
echo "--- notes ---"; cat notes.md
- name: Create it
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.gather.outputs.tag }}
PRERELEASE: ${{ inputs.prerelease && '--prerelease' || '' }}
run: |
gh release create "$TAG" release/* \
--title "MeshTerm ${TAG#v}" \
--notes-file notes.md \
--verify-tag \
$PRERELEASE
echo "https://github.com/${{ github.repository }}/releases/tag/$TAG"
+103
View File
@@ -0,0 +1,103 @@
# Standalone builds — a single file you download and run, with no Python installed.
#
# For most people who want MeshTerm, this is the install. They have a radio and a
# terminal, not a Python toolchain.
#
# Frozen builds cannot be cross-compiled: each one is made on the system it targets, so
# the matrix is the point of this file rather than an optimisation. Everything here
# uploads artifacts and nothing publishes — attaching them to a release stays a separate,
# deliberate act.
name: Installers
on:
workflow_dispatch:
# Callable, so the release workflow builds these binaries rather than keeping a second
# copy of the matrix that would drift from this one.
workflow_call:
# Only when the packaging itself changes. Three operating systems is not something to
# spend on every commit while the repository is private and minutes are metered.
push:
# `main` only. Without this, every Dependabot branch that touches this file — which
# is every action bump — starts a five-platform build, and three of those legs are
# billed at 2x and 10x.
branches: [main]
paths:
- "packaging/**"
- ".github/workflows/installers.yml"
permissions:
contents: read
concurrency:
group: installers-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: ${{ matrix.label }}
runs-on: ${{ matrix.os }}
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
include:
- { os: windows-latest, label: windows-x64, ext: ".exe" }
- { os: macos-latest, label: macos-arm64, ext: "" }
# Apple silicon builds do not run on Intel Macs, and plenty of people still
# have one. `macos-15-intel` rather than `-26-intel` for the same reason the
# Python here is 3.10: built against an older system, runs on more machines.
# (Not `macos-13` — that image is retired, and a job asking for it does not
# fail, it queues forever and holds the whole run open behind it.)
- { os: macos-15-intel, label: macos-x64, ext: "" }
- { os: ubuntu-22.04, label: linux-x64, ext: "" }
# For the uConsole and other 64-bit ARM handhelds. ARM runners are free on
# public repositories; on a private one this leg may sit unclaimed until the
# repo is public. Drop the line if it blocks you.
- { os: ubuntu-22.04-arm, label: linux-arm64, ext: "" }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
# Oldest supported version on purpose: a binary built against an older glibc
# and an older Python runs on more machines than one built against the newest.
python-version: "3.10"
- run: pip install . pyinstaller
- name: Build
working-directory: packaging
run: pyinstaller --clean --noconfirm meshterm.spec
- name: Check it actually runs
shell: bash
run: |
bin="packaging/dist/meshterm${{ matrix.ext }}"
chmod +x "$bin" || true
# Run it from somewhere else entirely, so a build that only works next to the
# source tree fails here rather than on someone's laptop.
cd "$(mktemp -d)"
"$GITHUB_WORKSPACE/$bin" --help > /dev/null
"$GITHUB_WORKSPACE/$bin" specimen > /dev/null
echo "runs, and finds its assets"
- name: Name it after the version and the platform
shell: bash
run: |
version=$(python -c "import meshterm; print(meshterm.__version__)")
out="meshterm-${version}-${{ matrix.label }}${{ matrix.ext }}"
mv "packaging/dist/meshterm${{ matrix.ext }}" "$out"
# A checksum beside the binary, so anyone can tell they got what we built.
if command -v sha256sum > /dev/null; then
sha256sum "$out" > "$out.sha256"
else
shasum -a 256 "$out" > "$out.sha256"
fi
cat "$out.sha256"
- uses: actions/upload-artifact@v4
with:
name: meshterm-${{ matrix.label }}
path: |
meshterm-*
if-no-files-found: error
+99
View File
@@ -0,0 +1,99 @@
# Publishing to PyPI. Deliberately hard to fire.
#
# There is no `push: tags` trigger here, and that absence is the point. A release
# workflow that publishes on a `v*` tag would mean tagging v0.9.0 on launch day
# silently claims a distribution name on a public index, permanently, as a side
# effect of an unrelated action. PyPI has no rename and no true delete, so that
# side effect is not one you can take back.
#
# So: a human runs this, from the Actions tab, on purpose, and types the version
# to prove they meant this version. Three gates before anything leaves the runner:
#
# 1. no automatic trigger at all
# 2. the typed version must equal the version actually built
# 3. the `pypi` environment, which can require a reviewer in repo settings
#
# It publishes whatever version is in the tree. There is no version argument, so
# this cannot publish something other than what is committed.
name: Release
on:
workflow_dispatch:
inputs:
confirm_version:
description: "Version to publish, exactly (e.g. 0.9.0). A mismatch aborts."
required: true
type: string
permissions:
contents: read
jobs:
build:
name: build and check
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install build twine
- run: python -m build
# Renders the README the way PyPI will, and fails on metadata PyPI would reject.
- run: twine check dist/*
# The version the build actually produced, read off the artifact rather than
# trusted from anywhere else.
- id: built
shell: bash
run: |
version=$(ls dist/*.tar.gz | sed -E 's/.*-([0-9][^-]*)\.tar\.gz/\1/')
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "built version: $version"
- name: Confirm the typed version matches what was built
shell: bash
env:
TYPED: ${{ inputs.confirm_version }}
BUILT: ${{ steps.built.outputs.version }}
run: |
if [ "$TYPED" != "$BUILT" ]; then
echo "::error::You typed '$TYPED' but the tree builds '$BUILT'. Nothing published."
exit 1
fi
echo "confirmed: $BUILT"
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
publish:
name: publish to PyPI
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
# Configure this environment in repo settings with a required reviewer, and it
# becomes a fourth gate: the job waits for an approval before it runs. The name
# must also match the environment set on the PyPI trusted publisher, or the OIDC
# exchange fails with an unhelpful error. If the publisher has no environment
# configured, delete this line.
environment:
name: pypi
url: https://pypi.org/p/mesh-term
permissions:
# Trusted Publishing mints a short-lived OIDC token instead of storing an API
# token in the repository. This is the only thing that needs write here.
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
+4
View File
@@ -14,6 +14,10 @@ dist/
.claude/settings.local.json
.claude/plans/
.claude/housekeeping.md
# ...and except the GitHub configuration, which only works where GitHub looks for
# it. The catch-all above hid it silently: a workflow that is never committed is
# indistinguishable from one that never runs.
!.github/
# Other virtual environments
venv/
+45 -2
View File
@@ -9,6 +9,46 @@ allowed to change behaviour, not just add to it.
## [Unreleased]
## [0.2.1] — 2026-09-07
### Fixed
- The formatter is pinned. `ruff>=0.4` meant CI installed a newer ruff than a laptop had,
and the two disagreed about whether the tree was clean — 0.16 formats Python blocks
inside Markdown and 0.15 doesn't, so the first CI run went red on a file that was fine
locally. An unpinned formatter isn't a gate.
- `docs/` is out of ruff's reach. The survey documents quote code as it looked on a past
date, in fragments that were never whole programs; a formatter rewriting a quotation
edits the record.
- The installer builds only run for `main`. Every Dependabot branch that bumped an action
touched the workflow file, matched its path filter, and started a five-platform build.
### Changed
- The distribution is named `mesh-term`. PyPI prohibits `meshterm` (pypi/support#12162),
and the name you install is the only thing that changes: the import package and the
command are both still `meshterm`.
- `pyproject.toml` carries the metadata a stranger sees on a package page — project URLs,
trove classifiers, and the licence as a PEP 639 expression.
### Added
- Continuous integration. Lint once, tests across seven OS and Python combinations, and a
packaging job that installs the built wheel into a clean environment and runs the command
from outside the source tree.
- A release workflow with no automatic trigger, which publishes nothing until a human runs
it on purpose and types the version.
- A security policy, a code of conduct, issue and pull request templates, weekly
dependency updates, and the funding links the app's own support page already offers.
- A Releases page. Pushing a version tag builds the binaries and attaches them, with a
combined `SHA256SUMS` and release notes taken from this file. It stays inside GitHub
and is not the PyPI publish, which remains a separate and deliberate act.
- Standalone builds. A PyInstaller spec and a workflow that builds a single runnable
file for Windows, macOS (Intel and Apple silicon) and Linux (x64 and ARM64), checks
each one runs from outside the source tree, and writes a checksum beside it. Nothing
is published; the files are workflow artifacts until someone attaches them to a
release.
## [0.2.0] — 2026-09-07
The first version to carry a changelog. Everything before it is in the commit log and is
@@ -31,6 +71,8 @@ deliberately not reconstructed here.
which is the only handle a tile operator has on a client.
- `macOS` and `Bluetooth` are spelled one way across the project. `BLE` stays where it is
an identifier — the transport name, the config keys, the CLI.
- `meshterm --help` opens with the same description as the package summary and the
PyPI page, instead of a fourth wording of its own.
### Fixed
@@ -41,5 +83,6 @@ deliberately not reconstructed here.
- Two `TYPE_CHECKING` imports the test suite referenced but never imported, on paths that
happened never to run.
[Unreleased]: https://github.com/jpmartineau/MeshTerm/compare/v0.2.0...HEAD
[0.2.0]: https://github.com/jpmartineau/MeshTerm/releases/tag/v0.2.0
[Unreleased]: https://github.com/jpmartineau/MeshTerm/compare/v0.2.1...HEAD
[0.2.1]: https://github.com/jpmartineau/MeshTerm/releases/tag/v0.2.1
[0.2.0]: https://github.com/jpmartineau/MeshTerm/tree/v0.2.0
+83
View File
@@ -0,0 +1,83 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at johnputer@meshterm.net. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of actions.
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
+86 -8
View File
@@ -14,11 +14,18 @@ radio overhears is recorded to a local SQLite database, so the longer you run it
your mesh's history is worth.
<p>
<a href="https://github.com/jpmartineau/MeshTerm/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/jpmartineau/MeshTerm/actions/workflows/ci.yml/badge.svg"></a>
<img alt="Python 3.10+" src="https://img.shields.io/badge/python-3.10%2B-blue">
<a href="LICENSE"><img alt="License: Apache-2.0" src="https://img.shields.io/badge/license-Apache--2.0-green"></a>
<img alt="For MeshCore" src="https://img.shields.io/badge/for-MeshCore-8A2BE2">
<img alt="Interface: TUI + CLI" src="https://img.shields.io/badge/interface-TUI%20%2B%20CLI-orange">
<a href="https://github.com/jpmartineau/MeshTerm/releases/latest"><img alt="Download" src="https://img.shields.io/badge/download-latest-brightgreen"></a>
<a href="https://discord.gg/AZwe5Uvb3S"><img alt="Discord" src="https://img.shields.io/badge/chat-Discord-5865F2"></a>
</p>
> MeshTerm is a side project, not a product. Bug reports are very welcome — but please
> open an issue before writing a pull request. [More on how it's run](#how-this-project-is-run).
<!--
Screenshots welcome here. Drop PNGs into docs/ and reference them, e.g.:
![Dashboard](docs/dashboard.png)
@@ -48,19 +55,55 @@ your mesh's history is worth.
## Install
```bash
git clone <your-fork-url> meshterm
cd meshterm
pip install -e ".[dev]"
```
### Download a build
Python **3.10+** is required (matching the `meshcore` library). The `[dev]` extra adds
pytest and ruff; drop it for a runtime-only install (`pip install -e .`).
**[⬇ Latest release](https://github.com/jpmartineau/MeshTerm/releases/latest)** — one file,
no Python needed. Windows, macOS (Apple silicon) and Linux (x64 and ARM64, so the uConsole
is covered).
Download it, make it runnable, and run it:
```bash
python -m pytest -q # run the test suite
chmod +x meshterm-* # macOS and Linux only
./meshterm-*
```
> **Your system will complain the first time.** These builds aren't code-signed — signing
> costs money on both platforms and this is a free side project. macOS says *"cannot be
> opened because the developer cannot be verified"*; clear it with
> `xattr -d com.apple.quarantine meshterm-*` and run it again. Windows shows *"Windows
> protected your PC"*; click **More info → Run anyway**. Each release ships a `SHA256SUMS`
> file if you'd rather check the download yourself.
### Or install with pip
You need **Python 3.10 or newer**. That's the only requirement — MeshTerm pulls in
everything else itself.
```bash
pip install git+https://github.com/jpmartineau/MeshTerm
meshterm
```
`meshterm` opens the full-screen menu. That's the whole install.
**Rather not touch your system Python?** [pipx](https://pipx.pypa.io/) puts the app in its
own private environment and still gives you a plain `meshterm` command:
```bash
pipx install git+https://github.com/jpmartineau/MeshTerm
```
**No radio yet?** `meshterm --mock` runs the entire app against a simulated mesh, so you
can have a look around before you buy anything.
### Coming
**`pip install mesh-term`**, once the package is published. It isn't yet — the name
question is still being sorted out ([why](https://github.com/pypi/support/issues/12162)).
Setting up for development instead? That's in [CONTRIBUTING.md](CONTRIBUTING.md).
## Quick start
```bash
@@ -253,6 +296,41 @@ Adding a feature means subclassing `Tool`, decorating it with `@register`, and
implementing `run()`. The same registry builds both the CLI and the menu, and the base
class wraps every execution in a logged `runs` row automatically.
## How this project is run
MeshTerm is one person working evenings and weekends. I'd rather tell you that up front
than have you guess from how long things take.
**Issues are welcome — all of them.** Bugs, questions, "is this supposed to do that". The
[bug form](.github/ISSUE_TEMPLATE/bug.yml) asks for a fair bit, and that's on purpose: how
well a problem is described really does decide whether I can do anything with it. If I can
reproduce it, I'll usually chase it. If I can't, I'm mostly guessing.
**Ask before you write a pull request.** Open an issue first and wait for a yes. I'm not
being precious — MeshTerm has firm house rules about how screens get built (they're in
[CLAUDE.md](CLAUDE.md)), and I'd hate for you to spend a weekend on something I then ask
you to rewrite. A quick conversation first saves us both.
**I can't promise timelines.** Some things get fixed the same night. Some sit for a month
because life happened. If your issue goes quiet, a nudge is completely fine — it's not
rude, it's helpful.
**Where to report things.** Either works:
- **[GitHub issues](https://github.com/jpmartineau/MeshTerm/issues)** if you have an
account. This is where things get tracked and fixed, so it's the shortest path.
- **[Discord](https://discord.gg/AZwe5Uvb3S)** if you don't, or if you're not sure it's a
bug yet. Ask in the support forum, or drop reproducible defects in `#bugs`. I move the
real ones over to GitHub myself.
Don't worry about picking the wrong one. Getting told about a problem beats filing it
tidily.
Also here: [CONTRIBUTING.md](CONTRIBUTING.md) ·
[SECURITY.md](SECURITY.md) ·
[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) ·
[CHANGELOG.md](CHANGELOG.md)
## Development
```bash
+62
View File
@@ -0,0 +1,62 @@
# Security policy
## Reporting a vulnerability
Use GitHub's private vulnerability reporting: go to the **Security** tab of this
repository and choose **Report a vulnerability**. That opens a private thread visible only
to you and the maintainer, so nothing is disclosed while it is being fixed.
If that is unavailable to you for any reason, email <johnputer@meshterm.net> instead.
**Please do not open a public issue for a security problem.**
## What to expect
MeshTerm is maintained by one person in his spare time, so this promises only what can
actually be kept:
- **An acknowledgement within a week.** Not a fix within a week — an answer saying the
report was read and what happens next.
- **A fix or a decision within 90 days** for anything confirmed. If it will take longer,
you will be told why.
- **Credit in the release notes**, unless you would rather not be named.
A slow reply is not a dismissal. If a week passes with silence, a nudge on the same thread
is welcome.
## Supported versions
While the major version is `0`, only the **latest release** is supported. There are no
backports; a fix ships in the next version.
## Scope
In scope — anything in this repository:
- Code execution, path traversal, or file overwrite triggered by data MeshTerm reads:
packets overheard from the mesh, a companion's responses, a config or preferences file,
a downloaded map tile.
- Anything that discloses a **channel key**, a **repeater admin password**, or a node's
private key material — including into the local database, a log, an export, or the
screen when it should not be there.
- Anything that lets a crafted packet corrupt or delete the observation database.
- Weaknesses in how MeshTerm stores credentials on disk.
Out of scope, though still worth telling us about somewhere:
- Vulnerabilities in the **MeshCore firmware** or in the companion device itself. Those
belong upstream at the MeshCore project.
- Vulnerabilities in the `meshcore` Python library, `bleak`, or any other dependency —
report those to their maintainers. If a MeshTerm change can mitigate one, we want to
know.
- The fact that anyone within radio range can hear a public channel. That is how a mesh
works, not a defect.
- Anything requiring an attacker to already have write access to your filesystem or your
companion device.
## A note on what MeshTerm handles
MeshTerm decrypts channel traffic using keys **you** give it, and it records everything it
overhears to a local SQLite database. That database is not encrypted. Treat it as you would
any file holding your mesh's history, and remember that a backup of it carries the same
weight.
+5
View File
@@ -1,5 +1,10 @@
# Navigation survey — every exception to "Esc pops the stack"
> **A historical record.** This was a snapshot of the code on the date below, kept
> because a project that writes down its own problems is easier to trust than one
> that doesn't. Some of what's here has since been fixed and some hasn't; nothing in
> it is a to-do list. Read it as "what this looked like then", not "what is wrong now".
Written 2026-08-22 against `main` @ `4b29248`. A catalogue, not a change: nothing here had
been fixed at the time of writing. Each finding says where it is, what it does, and what
makes it a question.
+5
View File
@@ -1,5 +1,10 @@
# UI consistency survey — what is off, and what turned out to be fine
> **A historical record.** This was a snapshot of the code on the date below, kept
> because a project that writes down its own problems is easier to trust than one
> that doesn't. Some of what's here has since been fixed and some hasn't; nothing in
> it is a to-do list. Read it as "what this looked like then", not "what is wrong now".
Written 2026-08-22 against `main` @ `4b29248`. Companion to
[`survey-navigation.md`](survey-navigation.md), which covers navigation exceptions; this one
covers everything else in the CLAUDE.md UX standard. A catalogue, not a change.
+1 -1
View File
@@ -5,7 +5,7 @@ Supports USB, Bluetooth and TCP companion connection. For Windows, macOS and Lin
from datetime import datetime
__version__ = "0.2.0"
__version__ = "0.2.1"
__author__ = "Jean-Pierre Martineau"
#: The year MeshTerm was first published; the copyright span starts here.
+7 -1
View File
@@ -68,7 +68,13 @@ app = typer.Typer(
# section panels flattened to plain headings by _unframe_help_panels() above — a
# splash of colour, none of the framing.
rich_markup_mode="rich",
help="MeshTerm — a modern toolkit for tuning and exploring your MeshCore mesh",
# The one description, same as the package summary and the PyPI page. See
# `meshterm.__doc__` — if this drifts from that, one of them is lying.
help=(
"MeshTerm is a full-featured TUI-based MeshCore client for your terminal. "
"Supports USB, Bluetooth and TCP companion connection. "
"For Windows, macOS and Linux."
),
)
# The context built by the callback and consumed by subcommands within one process.
+22
View File
@@ -0,0 +1,22 @@
"""Launcher for the frozen builds.
PyInstaller freezes a *script*, not a console-script entry point, so this is the script:
it does what the ``meshterm`` command does and nothing else. Kept in ``packaging/``
rather than in the package, because it exists for the installers and has no business
being importable.
"""
from __future__ import annotations
import multiprocessing
import sys
if __name__ == "__main__":
# Windows and macOS spawn child processes by re-running this executable. Without
# this call a frozen app that ever starts one forks a second copy of the whole UI
# instead, which looks like the app launching itself over and over.
multiprocessing.freeze_support()
from meshterm.cli import main
sys.exit(main())
+67
View File
@@ -0,0 +1,67 @@
# PyInstaller spec for the downloadable builds. Run from the repository root:
#
# pyinstaller packaging/meshterm.spec
#
# One file, console mode. Console is not a detail: MeshTerm *is* a terminal program, and
# a windowed build would start with nowhere to draw and exit immediately.
#
# Built on the machine it targets. There is no cross-compiling here — the Windows build
# comes off a Windows runner, the macOS one off macOS. That is what the workflow is for.
from PyInstaller.utils.hooks import collect_submodules
# The assets have to land at `meshterm/assets`, because `ui/about.py` finds them with
# `Path(__file__).parent.parent / "assets"` and that path has to keep resolving inside
# the bundle exactly as it does in a checkout.
datas = [
("../meshterm/assets", "meshterm/assets"),
]
hiddenimports = [
# bleak picks its backend at runtime by platform, so static analysis never sees the
# one that actually gets used. Collect all of them and let the unused ones sit.
*collect_submodules("bleak.backends"),
# Same story for the companion library's transports.
*collect_submodules("meshcore"),
# Rich and prompt_toolkit both reach for things dynamically.
"rich.console",
"prompt_toolkit.output.win32",
"prompt_toolkit.output.vt100",
]
a = Analysis(
["entry.py"],
pathex=[".."],
binaries=[],
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
# Nothing here is used, and each one drags in a GUI toolkit or a test framework.
excludes=["tkinter", "pytest", "IPython", "matplotlib", "numpy", "PIL"],
noarchive=False,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name="meshterm",
debug=False,
bootloader_ignore_signals=False,
strip=False,
# UPX is off deliberately: it saves a few megabytes and gets the result flagged by
# antivirus often enough that it is not a trade worth making for a first release.
upx=False,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
+38 -5
View File
@@ -3,14 +3,34 @@ requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "meshterm"
# The distribution name only. The import package and the console script both stay
# `meshterm` — PyPI prohibits that name (see pypi/support#12162), and nothing about the
# app changes because of it. `[tool.hatch.build.targets.wheel]` below is what keeps the
# wheel shipping the `meshterm/` package under this different name.
name = "mesh-term"
dynamic = ["version"]
description = "MeshTerm is a full-featured TUI-based MeshCore client for your terminal. Supports USB, Bluetooth and TCP companion connection. For Windows, macOS and Linux."
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Jean-Pierre Martineau" }]
license = { text = "Apache-2.0" }
license = "Apache-2.0"
license-files = ["LICENSE", "NOTICE"]
keywords = ["meshcore", "lora", "mesh", "radio", "cli"]
# No `License ::` entry: PEP 639 forbids one alongside the SPDX expression above.
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: End Users/Desktop",
"Operating System :: MacOS",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Communications :: Ham Radio",
"Topic :: System :: Networking",
]
dependencies = [
"meshcore>=2.3", # companion-device protocol (serial/BLE/TCP)
@@ -31,9 +51,18 @@ dependencies = [
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.23",
"ruff>=0.4",
# Pinned to a minor line, not floated. A formatter that drifts between machines is
# not a gate: 0.16 formats Python inside Markdown and 0.15 does not, so an
# unpinned `ruff>=0.4` had CI and a laptop disagreeing about a clean tree.
"ruff>=0.16.6,<0.17",
]
[project.urls]
Homepage = "https://github.com/jpmartineau/MeshTerm"
Repository = "https://github.com/jpmartineau/MeshTerm"
Issues = "https://github.com/jpmartineau/MeshTerm/issues"
Changelog = "https://github.com/jpmartineau/MeshTerm/blob/main/CHANGELOG.md"
[project.scripts]
meshterm = "meshterm.cli:main"
@@ -48,8 +77,12 @@ packages = ["meshterm"]
[tool.ruff]
line-length = 100
target-version = "py310"
# Helper scripts for the Claude skills; they are not part of the package.
extend-exclude = [".claude"]
# Helper scripts for the Claude skills; they are not part of the package. And `docs/`,
# which holds dated survey documents: ruff formats Python blocks inside Markdown, and the
# code they quote is an excerpt of how a file looked on a past date. Reformatting a
# quotation edits the record — and the excerpts are call fragments, not whole programs, so
# the formatter's idea of "fixed" turns them into something that was never there.
extend-exclude = [".claude", "docs"]
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "D"]