Contributing to Scope
July 30, 2026 · View on GitHub
First off — thank you for taking the time to contribute! Scope is a
community-driven serial monitor, and every bug report, idea, plugin, and pull
request helps make it better.
This guide explains how to get involved, set up a development environment, and get your changes merged. It's meant to be read top to bottom the first time, and skimmed afterwards.
Table of contents
- Code of conduct
- Project philosophy
- Ways to contribute
- Before you start
- Reporting bugs
- Requesting features
- Development setup
- Project layout
- Coding conventions
- Testing and the quality checklist
- Commit and branch conventions
- Opening a pull request
- Contributing plugins
- Documentation and demo GIFs
- License
- Getting help
Code of conduct
This project and everyone participating in it is governed by the Code of Conduct. By participating, you are expected to uphold it. Please report unacceptable behavior to the maintainer at matheuswhite1@protonmail.com.
Project philosophy
Scope is guided by five pillars (see
Project Goals for the full text). Keep them in mind
when proposing or reviewing changes:
- Intuitive usage — behavior should follow the conventions of popular
tools (e.g.
Up/Downnavigating history like a shell). - Compactness and orthogonality — prefer small, composable features over large, overlapping ones.
- User-centric development — deliver value to users first; prioritize critical, user-reported bugs over new features.
- Multiplatform — every release must work on Linux, Windows, and macOS.
- Extensible — favor user-scriptable extension points (Lua plugins) over hard-coding niche behavior.
Ways to contribute
You don't have to write Rust to help:
- Report bugs you hit while using
Scope. - Request features or share use cases we haven't considered.
- Improve the documentation — the README, this guide, or the Plugins Developer Guide.
- Write or share plugins (see Contributing plugins).
- Test on your platform / hardware — different OSes, serial adapters, and RTT probes surface bugs the maintainers can't reproduce.
- Fix bugs or implement features with a pull request.
Before you start
- Browse the open issues and the roadmap project to see what's already planned or in progress, and to avoid duplicating work.
- For anything larger than a small fix, open an issue first (or comment on an existing one) to discuss the approach before you write code. It saves everyone time and avoids surprises at review.
- Small, self-contained fixes (typos, obvious bugs) can go straight to a pull request.
Reporting bugs
Open a bug report and include as much of the following as you can:
- What happened and what you expected to happen.
- Steps to reproduce — the exact command you ran (e.g.
scope serial /dev/ttyUSB0 115200) and what you typed. - Environment — your OS and version, the
Scopeversion (scope --version), and whether you were using a serial port or RTT. - Logs or a screenshot/GIF of the TUI, if relevant. You can save the
session with
Ctrl+Sand attach the.txt.
Requesting features
Open a feature request describing the problem you're trying to solve (not just the solution you have in mind), who it helps, and how it fits the project philosophy.
Development setup
Prerequisites:
- Rust
1.92.0or newer (the crate uses edition 2024). Install via rustup. - Linux only: the
libudevdevelopment headers —sudo apt-get install libudev-devon Debian/Ubuntu. - For the RTT interface: a debug probe supported by
probe-rs(J-Link, ST-Link, CMSIS-DAP, …). Not needed for serial-only work.
Build and run:
git clone https://github.com/matheuswhite/scope-rs
cd scope-rs
cargo build # debug build -> target/debug/scope
cargo run --bin scope -- list # list available serial ports
cargo run --bin scope -- serial /dev/ttyUSB0 115200 # open a serial port
cargo run --bin scope -- rtt STM32F303 0 # attach to an RTT target
Project layout
Scope (crate scope-monitor) is a binary-only crate — there is no library
target, so use cargo test --bin scope, not cargo test --lib. It's built as a
multi-threaded actor system; the main subsystems live under src/:
| Path | Responsibility |
|---|---|
src/main.rs | Wires up the tasks and CLI (app_serial / app_rtt). |
src/interfaces/ | Owns the serial port / RTT connection. |
src/inputs/ | The command bar: keystrokes, history, search. |
src/graphics/ | Renders the TUI, scrollback, selection, session saving. |
src/plugin/ | Hosts the Lua plugin engine. |
src/infra/ | Shared plumbing: tasks, channels, logger, tags, config. |
For a deeper architectural tour (tasks, the MPMC data buses, command-bar
syntax), see CLAUDE.md. For the plugin API, see the
Plugins Developer Guide.
Coding conventions
- Format your code with
cargo fmt --allbefore committing. CI runscargo fmt --all -- --checkand fails on any diff. - Keep the tree warning-clean.
src/main.rshas#![deny(warnings)], so any compiler warning fails the build. - Stay cross-platform. Guard platform-specific code with
cfgand don't break Linux, Windows, or macOS. If you can't test all three locally, CI will — but call out in your PR what you were able to verify. - Add tests for new behavior (see below).
- Match the surrounding code in naming, structure, and comment density.
Testing and the quality checklist
Unit tests live in #[cfg(test)] mod tests blocks inside the files they cover.
End-to-end TUI tests are in tests/tui_e2e.rs (Unix only): they drive the real
binary in a PTY and assert on the reconstructed screen.
cargo test --bin scope # unit tests
cargo test --bin scope <substring> # a single test, e.g. cargo test --bin scope test_rhs
cargo test --test tui_e2e # end-to-end TUI tests (Unix only)
cargo test --test tui_e2e -- --ignored # includes the platform-dependent serial-RX test
Before opening a pull request, run the same checks CI does so it passes on the first try (CI runs these on Linux, Windows, and macOS):
cargo fmt --all -- --check
cargo test --locked
cargo build --locked --release
You can also drive and eyeball the running TUI without hardware using the
test-tui helper described in CLAUDE.md (virtual serial port via
socat, keystroke injection via tmux).
Commit and branch conventions
-
Branch off
mainand name your branch after the work, e.g.feat/45-contributing-guide,fix/123-reconnect-windows, ordocs/.... -
Write Conventional Commits. Use a type prefix and an imperative summary:
feat: add flow-control command to the serial interface fix: keep auto-reconnect alive after a port replug on Windows docs: document the config.toml resolution orderCommon types:
feat,fix,docs,refactor,test,chore. -
Reference the issue in the commit body or the PR (e.g.
(#45)orCloses #45). -
Do not bump the version in
Cargo.toml. Releasing is a maintainer action: the maintainer bumps the version and pushes a matchingvX.Y.Ztag, which triggers CI (cargo-dist) to build the binaries and installers and a companion workflow to publish to crates.io. A CI check (version-guard) will fail your pull request if it changes the version field.
Opening a pull request
- Push your branch and open a PR against
main. - Fill in the pull-request template: what changed, why, how you tested it, and
the issue it closes (
Closes #NN). - Make sure CI is green (build + tests on all three OSes, and
rustfmt). - Be responsive to review feedback — small follow-up commits are fine; the maintainer will squash/merge as appropriate.
Keep pull requests focused: one logical change per PR is much easier to review than a large mixed one.
Release security
Cutting a release is a maintainer-only action, enforced in depth so that no pull request (and no non-owner collaborator) can trigger one:
- A release only fires on a
vX.Y.Zgit tag. A pull request never publishes — the distrelease.ymlrunsdist planon PRs (gated bypublishing: !github.event.pull_request), andpublish-crates.ymltriggers on tags only. - Only admins can create tags. The
release-tagsrepository ruleset restricts creating/updating/deleting any tag to admins, so a collaborator with push access cannot push av*tag to start a release. - The tag comes from the release PR, not from a manual push.
release-tag.ymltagsvX.Y.Zwhen a version bump lands onmain, so the release is one maintainer-gated flow (review + merge) instead of a merge plus a rememberedgit tag && git push. It refuses to tag unless the pusher is the repository owner, the commit belongs to a merged PR labelledrelease, and the version actually changed; it never moves an existing tag, and it publishes nothing itself. It triggers onpushrather thanpull_requeston purpose: apushrun always uses the workflow definition frommain, so a pull request branch cannot edit the file that holds the tagging token. - crates.io publishing is reviewer-gated.
publish-crates.ymldeploys through thecratesenvironment (required reviewer: the owner) and is additionally guarded byif: github.actor == github.repository_owner; it also refuses to publish when the tag doesn't matchCargo.toml. - The guardrails themselves are protected.
mainrequires a PR, a passing build on all three OSes, and code-owner review for the release-critical paths listed in.github/CODEOWNERS(workflows,dist-workspace.toml,wix/,build.rs,installer/,Cargo.toml, and the security tests).tests/release_security.rsparses the workflows and fails CI if any of these invariants regress.
Maintainer release flow: open a PR that bumps version in Cargo.toml, label it
release, and merge it once CI is green. The merge tags vX.Y.Z, which builds
the binaries and installers and publishes to crates.io (approve the crates
deployment when prompted). No manual tagging step.
One-time setup for automatic tagging
release-tag.yml pushes the tag with a maintainer PAT, exposed as the
RELEASE_TAG_TOKEN secret of a release-tag environment. A PAT is required
rather than the built-in GITHUB_TOKEN for two reasons: a GITHUB_TOKEN push
does not trigger further workflow runs, so release.yml would never fire;
and tag creation is admin-only per the release-tags ruleset. Using the owner's
PAT also keeps github.actor on the tag push equal to the owner, so
publish-crates.yml's owner guard still applies.
- Create a fine-grained PAT owned by the repository owner, scoped to this
repository only, with
Contents: read and write— nothing else — and the shortest expiry you're willing to rotate. - Create a
release-tagenvironment (Settings → Environments) and add the PAT as the secretRELEASE_TAG_TOKEN. Limit its deployment branches tomain. Adding a required reviewer there is optional; it gives you a second OK before any release goes out, at the cost of one approval click.
Rotate the PAT when it expires. Without it the tagging job fails loudly with a pointer to this section rather than silently skipping a release.
Testing the installers before a release
PRs only run dist plan, so no .msi is produced by default. To get one
without cutting a release, temporarily add pr-run-mode = "upload" under
[dist] in dist-workspace.toml, regenerate the workflow (dist generate) and
push: the PR then builds the same archives and installers a tag would and
attaches them to the Actions run.
gh run list --branch <your-branch> --workflow release.yml --limit 1
gh run download <run-id> # or grab the artifacts from the PR's Actions tab
The Windows installer lands as scope-monitor-x86_64-pc-windows-msvc.msi
(inside the artifacts-build-local-* artifact). Install it on a Windows machine
and check the PATH entry, the four Start-Menu shortcuts and their icons, an
upgrade over an existing install, and a clean uninstall. Revert the
pr-run-mode line once you are done, so ordinary PRs don't pay for a full
three-platform release build.
Contributing plugins
Extensibility is a core pillar, and plugins are a great first contribution.
Plugins are Lua scripts that hook into Scope's lifecycle and I/O events. See
the Plugins Developer Guide for the API, and the existing
scripts under plugins/ for working examples. If you build something broadly
useful, feel free to propose adding it (or a link to it) via an issue or PR.
Documentation and demo GIFs
Documentation changes are very welcome. If your change affects behavior shown in the README, note it in your PR.
The animated GIFs in the README are generated headlessly — no real hardware
and no manual screen recording. Each demo is a videos/NNN_name/steps.sh script
driven by socat + tmux + asciinema + agg. To add or update one, see
videos/README.md.
License
By contributing to Scope, you agree that your contributions will be dual
licensed under the project's MIT and Apache-2.0
licenses, without any additional terms or conditions.
Getting help
- Questions, bugs, and ideas: open an issue.
- If you'd like to support the project, there's a Ko-fi link.
Thanks again for contributing! 🎉