Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b2c5527f8 | ||
|
|
141698678e | ||
|
|
8cc1cae213 | ||
|
|
2ea8f9fb83 | ||
|
|
eb61c55f22 | ||
|
|
0c12bd4fcf | ||
|
|
ebb3c80f15 | ||
|
|
4f60f44512 | ||
|
|
ab8ef2aa23 | ||
|
|
628b665420 | ||
|
|
117b958edf | ||
|
|
365df236b9 |
No files matched your search
@@ -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"
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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]`
|
||||
@@ -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
|
||||
@@ -0,0 +1,119 @@
|
||||
# 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
|
||||
# A tool-provided subcommand, not just --help: the tools are discovered at
|
||||
# runtime, so a broken package still answers --help perfectly well.
|
||||
/tmp/fresh/bin/meshterm contacts --help > /dev/null
|
||||
echo "the wheel installs, the console script runs, and its tools registered"
|
||||
@@ -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"
|
||||
@@ -0,0 +1,109 @@
|
||||
# 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
|
||||
# Reads the asset files, so a bundle missing its pages fails here.
|
||||
"$GITHUB_WORKSPACE/$bin" specimen > /dev/null
|
||||
# And a tool-provided subcommand, which is the check that matters. Every tool is
|
||||
# found at runtime by `pkgutil.iter_modules`, so a bundle that omits them still
|
||||
# starts, still prints help, still draws the specimen — and has an empty menu
|
||||
# with nothing in it but Quit. `--help` passing proves almost nothing on its own.
|
||||
"$GITHUB_WORKSPACE/$bin" contacts --help > /dev/null
|
||||
echo "runs, finds its assets, and its tools registered"
|
||||
|
||||
- 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
|
||||
@@ -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
|
||||
@@ -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/
|
||||
|
||||
+73
-2
@@ -7,8 +7,75 @@ numbers follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — wi
|
||||
SemVer itself makes for a leading zero: while the major version is `0`, a **minor** bump is
|
||||
allowed to change behaviour, not just add to it.
|
||||
|
||||
<!--
|
||||
At 0.9.0 — the first public release — everything below collapses into a single entry
|
||||
reading "first public release", with the headline features under it. Nobody arriving at
|
||||
a project on its launch day wants a changelog of the fortnight before it: the versions
|
||||
under 0.9.0 were never released anywhere, and their entries are notes to ourselves about
|
||||
getting ready. Keep the dates and the tags; replace the prose.
|
||||
-->
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.2] — 2026-09-07
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The downloadable builds were missing every tool.** They started, printed help and drew
|
||||
the specimen card, and had a menu with nothing in it but Quit — no contacts, no map, no
|
||||
trace, no chat. Tools are found at runtime with `pkgutil.iter_modules`, which finds
|
||||
nothing inside a frozen bundle, so none of them registered. The packaging now collects
|
||||
them, and the smoke tests run a tool-provided subcommand instead of only `--help`, which
|
||||
a gutted build passes perfectly well.
|
||||
|
||||
### Added
|
||||
|
||||
- `MESHTERM_HOME` points config and data somewhere other than `~/.meshterm`. Every copy of
|
||||
MeshTerm shared one directory, so trying a downloaded build meant letting it open the
|
||||
same history database as the one you use — the valuable half of an install, with no way
|
||||
to keep a second copy away from it. Also useful for a portable install, or two radios
|
||||
kept apart.
|
||||
|
||||
## [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 +98,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 +110,7 @@ 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.2...HEAD
|
||||
[0.2.2]: https://github.com/jpmartineau/MeshTerm/releases/tag/v0.2.2
|
||||
[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
|
||||
@@ -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
|
||||
@@ -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.:
|
||||

|
||||
@@ -48,19 +55,84 @@ your mesh's history is worth.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
git clone <your-fork-url> meshterm
|
||||
cd meshterm
|
||||
pip install -e ".[dev]"
|
||||
### Download a build
|
||||
|
||||
**[⬇ Latest release](https://github.com/jpmartineau/MeshTerm/releases/latest)** — one file,
|
||||
no Python needed. Windows, macOS (Intel and Apple silicon), and Linux (x64 and ARM64, so
|
||||
the uConsole is covered).
|
||||
|
||||
MeshTerm is a terminal program, so **open a terminal and run it from there.** You can
|
||||
double-click it and it will work, but you'll get whatever console your system picks, and
|
||||
if anything goes wrong at startup the window closes before you can read why.
|
||||
|
||||
**Windows** — open Windows Terminal or PowerShell, `cd` to your downloads, then:
|
||||
|
||||
```powershell
|
||||
.\meshterm-0.2.2-windows-x64.exe
|
||||
```
|
||||
|
||||
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 .`).
|
||||
**macOS and Linux** — open Terminal, `cd` to your downloads, then:
|
||||
|
||||
```bash
|
||||
python -m pytest -q # run the test suite
|
||||
chmod +x meshterm-0.2.2-*
|
||||
./meshterm-0.2.2-*
|
||||
```
|
||||
|
||||
Rename it to something you don't mind typing and put it somewhere on your `PATH`, and it
|
||||
becomes just `meshterm` from anywhere.
|
||||
|
||||
> **Your system will complain the first time, and it's right to.** These builds aren't
|
||||
> code-signed — signing costs real money on both platforms and this is a free side
|
||||
> project.
|
||||
>
|
||||
> - **macOS**: *"cannot be opened because the developer cannot be verified"*. Clear it with
|
||||
> `xattr -d com.apple.quarantine meshterm-0.2.2-*` and run it again.
|
||||
> - **Windows**: *"Windows protected your PC"*. Click **More info → Run anyway**.
|
||||
>
|
||||
> Every release ships a `SHA256SUMS` file if you'd rather check the download first.
|
||||
|
||||
> **Trying a build without touching your real data.** MeshTerm keeps everything in
|
||||
> `~/.meshterm` — your history database, contacts, channel keys — and *every* copy of
|
||||
> MeshTerm uses that same folder. Set `MESHTERM_HOME` to try one in isolation:
|
||||
>
|
||||
> ```bash
|
||||
> MESHTERM_HOME=~/meshterm-test ./meshterm-0.2.2-* # macOS, Linux
|
||||
> ```
|
||||
> ```powershell
|
||||
> $env:MESHTERM_HOME = "$HOME\meshterm-test"; .\meshterm-0.2.2-windows-x64.exe
|
||||
> ```
|
||||
>
|
||||
> Same variable if you run two radios and want them kept apart.
|
||||
|
||||
### 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 +325,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
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.2"
|
||||
__author__ = "Jean-Pierre Martineau"
|
||||
|
||||
#: The year MeshTerm was first published; the copyright span starts here.
|
||||
|
||||
+7
-1
@@ -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.
|
||||
|
||||
+19
-2
@@ -15,6 +15,7 @@ keys used to sit in this file with no screen behind them.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
@@ -26,15 +27,31 @@ else: # pragma: no cover - exercised only on 3.10
|
||||
import tomli as tomllib
|
||||
|
||||
|
||||
#: Points MeshTerm's config and data somewhere other than the home directory. For trying a
|
||||
#: build without letting it near a real history, for a portable install on a stick, and for
|
||||
#: running two radios out of two directories. Read every time rather than cached, so a test
|
||||
#: can move it between cases.
|
||||
CONFIG_DIR_ENV = "MESHTERM_HOME"
|
||||
|
||||
|
||||
def default_config_dir() -> Path:
|
||||
"""Return the directory MeshTerm uses for config and data.
|
||||
|
||||
Resolves to ``.meshterm`` under the OS-defined home directory (``%USERPROFILE%``
|
||||
on Windows, ``$HOME`` on Unix), as reported by :meth:`Path.home`.
|
||||
``$MESHTERM_HOME`` wins if it is set and not empty. Otherwise this resolves to
|
||||
``.meshterm`` under the OS-defined home directory (``%USERPROFILE%`` on Windows,
|
||||
``$HOME`` on Unix), as reported by :meth:`Path.home`.
|
||||
|
||||
The override exists because the database is the valuable part of an install — a
|
||||
running record of everything the radio has overheard — and there was no way to point a
|
||||
second copy of MeshTerm at a different one. A downloaded build would happily open the
|
||||
same 34MB file as the checkout it was built from.
|
||||
|
||||
Returns:
|
||||
The resolved configuration directory path (not guaranteed to exist).
|
||||
"""
|
||||
override = os.environ.get(CONFIG_DIR_ENV, "").strip()
|
||||
if override:
|
||||
return Path(override).expanduser()
|
||||
return Path.home() / ".meshterm"
|
||||
|
||||
|
||||
|
||||
@@ -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())
|
||||
@@ -0,0 +1,72 @@
|
||||
# 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 = [
|
||||
# Every tool module. `meshterm.tools` finds these with `pkgutil.iter_modules` at
|
||||
# import time, so nothing static ever names one, and a frozen build without this line
|
||||
# starts with an empty registry: no menu entries but Quit, and no CLI subcommands.
|
||||
# The app launches and looks like it works, which is the worst way for this to fail.
|
||||
*collect_submodules("meshterm.tools"),
|
||||
# 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
@@ -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"]
|
||||
|
||||
+30
-1
@@ -8,7 +8,9 @@ from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from meshterm.core.config import Settings
|
||||
import pytest
|
||||
|
||||
from meshterm.core.config import CONFIG_DIR_ENV, Settings, default_config_dir
|
||||
|
||||
|
||||
def test_connect_on_start_defaults_true() -> None:
|
||||
@@ -42,3 +44,30 @@ def test_tcp_profile_defaults_port(tmp_path: Path) -> None:
|
||||
config.write_text('[profiles.wifi]\nhost = "meshcore.local"\n', encoding="utf-8")
|
||||
profile = Settings.load(config).profiles["wifi"]
|
||||
assert profile.is_tcp and profile.tcp_endpoint == "meshcore.local:5000"
|
||||
|
||||
|
||||
def test_config_dir_defaults_to_the_home_directory(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""With no override set, config and data live in ``.meshterm`` under the user's home."""
|
||||
monkeypatch.delenv(CONFIG_DIR_ENV, raising=False)
|
||||
assert default_config_dir() == Path.home() / ".meshterm"
|
||||
|
||||
|
||||
def test_config_dir_follows_the_override(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
"""``$MESHTERM_HOME`` moves the whole directory, so a build can run beside a real one.
|
||||
|
||||
This is what keeps a downloaded binary from opening the same database as the checkout
|
||||
it was built from — the history is the valuable half of an install, and there was no
|
||||
way to point a second copy somewhere else.
|
||||
"""
|
||||
monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path / "elsewhere"))
|
||||
assert default_config_dir() == tmp_path / "elsewhere"
|
||||
|
||||
|
||||
def test_config_dir_ignores_an_empty_override(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""An unset-but-present or whitespace value falls back rather than resolving to nowhere.
|
||||
|
||||
A shell that exports the variable empty is common enough that treating "" as "put the
|
||||
data in the current directory" would be a nasty way to scatter someone's history.
|
||||
"""
|
||||
monkeypatch.setenv(CONFIG_DIR_ENV, " ")
|
||||
assert default_config_dir() == Path.home() / ".meshterm"
|
||||
Reference in new issue
Block a user