The local gate serializes tests because of a native font race. nextest gives each test its own process, so the whole suite can run in parallel while iterating. It is local only: it skips doctests and the isolated render tests, so the gate stays as it is. Add it to the Nix shell.
16 KiB
Contributing to wayscriber
Thanks for your interest in improving wayscriber. This guide covers the supported workspace, architecture boundaries, and validation workflow without trying to inventory every source file.
Contribution principles
Wayscriber is shared as a gift exchange, not a contract. Requests are welcome, but there is no guaranteed timeline or support obligation. See the gift-exchange principles for context.
Changes should preserve foreground/fullscreen safety, serialized config and session compatibility, and the feature-gate behavior described below. Prefer focused changes with tests at the owning module boundary.
Workspace and toolchain
The repository is one Cargo workspace using Rust 1.99.1 and edition 2024.
rust-toolchain.toml pins the development toolchain, including Clippy and rustfmt; CI uses
the same version. The Nix flake selects that version through the rust-overlay input pinned
in flake.lock, using the root package's rust-version for both crates and the development
shell. When raising the version, update both manifests, rust-toolchain.toml, and CI pins;
run nix flake update rust-overlay if the locked overlay does not yet provide it.
- the root
wayscriberpackage owns the overlay, daemon, CLI, shared domain/config/session code, rendering, and integration tests; wayscriber-configuratoris the GTK4/libadwaita (Relm4) GUI package underconfigurator/;- root
Cargo.lockis the single lockfile for both packages.
The workspace default member is only the root package. Use -p wayscriber-configurator or
--workspace when configurator coverage is required. A command such as
cargo build --manifest-path configurator/Cargo.toml still belongs to the root workspace and uses
the root lockfile.
The main package's build.rs only exposes an optional release-version override, embeds the current
Git hash, and tells Cargo which Git metadata should trigger a rebuild. Cargo features use standard
cfg(feature = "...") conditions directly.
Development
GitHub CI uses the C# file app. To run that route locally, build it once with
dotnet build tools/wayscriber.cs, then run commands with
dotnet run tools/wayscriber.cs --no-build -- .... Repository, release, and
packaging checks exist only there, so the complete local gate needs .NET. Nix does
not provide .NET; global.json selects the required SDK when it is installed
separately. Without it, cargo test still runs the ownership guards in
tests/repository_guards, and the build, run, and install scripts stay usable;
the C# checks, including source coverage, need .NET.
The repository uses no Python.
Build both packages without launching a window:
cargo build --workspace
The overlay and configurator are foreground applications. Only launch them when it is safe for them to take focus:
cargo run -- --active
cargo run -p wayscriber-configurator
The CLI parser is implemented manually in src/cli.rs; keep parsing, help/version output, and CLI
integration tests aligned when flags change.
Architecture ownership
Use the codebase overview for the main runtime flow and the configurator README for the editor. The stable ownership map is:
| Area | Owner |
|---|---|
| Entry and CLI | src/main.rs, src/lib.rs, src/app/, and src/cli.rs |
| Stable values | src/domain/ for dependency-light action, tool, color, and board identities |
| Config | src/config/, config.example.toml, docs/CONFIG.md, and configurator mappings |
| Input and boards | src/input/; BoardManager owns ordered BoardState values and their pages |
| Drawing and rendering | src/draw/ for frames, shapes, history, page storage, and Cairo/Pango helpers |
| Wayland runtime | src/backend/wayland/ for protocol setup, handlers, event-loop phases, state, and rendering |
| Daemon | src/daemon/ for control requests, service lifecycle, overlay children, and tray integration |
| Sessions | src/session/ plus Wayland session transactions for snapshots, storage, locks, and recovery |
| Capture/export | src/capture/ and src/canvas_export/ |
| UI and toolbars | src/ui/, src/ui/toolbar/, src/backend/wayland/toolbar/, and src/toolbar_gtk/ |
| Configurator | configurator/src/app/ for state/update/side effects and configurator/src/models/ for typed drafts |
| Packaging and automation | packaging/, tools/, and .github/workflows/ |
Scoped AGENTS.md files record invariants and coupled-change reminders near these owners. They are
more durable than a central list of every module and should be updated when a boundary changes.
Common coupled changes
- Config fields often require core types/defaults/validation,
config.example.toml,docs/CONFIG.md, configurator draft/view/search behavior, schema behavior, and tests. - Actions, tools, and keybindings often require action metadata, default maps, input routing, help and command UI, both toolbar frontends, configurator labels/search, docs, and tests.
- Board/page changes can affect session snapshots, board picker and toolbar UI, persistence config, export behavior, and identity/ordering tests.
- Daemon/service/shortcut changes can affect
src/daemon/, shared service/path helpers, configurator daemon setup, packaging units, and setup docs. - Serialized config or session changes must preserve compatibility or provide an explicit migration with regression fixtures.
Feature gates
Preserve the intent of the root package features:
tablet-input— tablet/stylus protocol support;dbus— shared D-Bus support;portal— portal capture/runtime support, layered ondbus;tray— status tray support, layered ondbus;toolbar-gtk— GTK layer-shell toolbar frontend;config-schema— schema export support.
The configurator has its own tablet-input feature forwarding to the root package. Full CI covers
all features and no default features across the workspace.
Tests and linting
Run focused checks while iterating, for example:
cargo test config::
cargo test session::
cargo test -p wayscriber-configurator
For a fast run of the whole suite, use cargo-nextest, which the Nix shell includes. It gives each test its own process, so it can run in parallel despite the native font race that keeps the local CI entry point serial:
cargo nextest run --workspace --all-features
It skips doctests and the isolated render tests, so it does not replace that entry point.
Before submitting a broad or cross-package change, run the local CI entry point:
./tools/lint-and-test.sh
It needs the pinned .NET SDK and stops without it. It checks release/package metadata, Rust
source coverage, and the shell-tool inventory; builds and runs the C# repository-tool tests,
which also run the package-layout and release-packaging shell contracts; then checks
formatting, strict all-feature Clippy, all-feature tests, and no-default-feature tests. The
Rust tests include the repository guards in tests/repository_guards. The source-coverage
gate uses current rustc dep-info and rejects tracked or unignored .rs files that are outside
the supported Cargo target/feature matrix.
Dependency changes must also satisfy the policy in deny.toml, which CI enforces in its own
job. The Nix shell includes cargo-deny; elsewhere, install it and run:
cargo deny --workspace --locked check
The all-feature portal transport tests require dbus-daemon. Each fixture owns a private
session bus and connects through its explicit address, leaving the desktop session bus alone.
The canonical gate serializes the Rust test harness because parallel rendering tests have
crashed in the native font stack through context-menu, board-picker, and region-capture paths.
This changes scheduling, not test selection; tests may still create their own threads.
The context-menu and board-picker retained-text rendering regressions each run in a separate
process under both feature configurations. Both have crashed inside Cairo/FreeType during
parallel tests. This isolation covers these observed failures; it does not prove that the
native race is fixed. Direct parallel cargo test runs can still hit it. See Releasing for
the full release and website handoff.
For offline work, prefetch dependencies first:
./tools/fetch-all-deps.sh
Freeze, zoom, and capture manual matrix
Geometry unit tests cover crop arithmetic and stale-output rejection, but they do not prove a compositor's protocol behavior. For capture-related changes, exercise every unchecked cell below on real hardware; this checklist is manual evidence, not CI coverage.
| Compositor | 1x | Fractional scale | Mixed DPI |
|---|---|---|---|
| Hyprland | [ ] | [ ] | [ ] |
| Plasma/KWin | [ ] | [ ] | [ ] |
| Niri | [ ] | [ ] | [ ] |
| Sway | [ ] | [ ] | [ ] |
| GNOME | [ ] | [ ] | [ ] |
For each cell, record the selected capture backend from the logs. Verify that Freeze, Zoom, and a PDF desktop backdrop all use the active output's exact pixels. User full-screen screenshots remain whole-desktop captures. Repeat while switching outputs and, where supported, changing scale or transform during capture: the result must either stay exact or fail visibly, never shift, stretch, or reuse another output's image.
For changes to the native region picker, use the same compositor/scale matrix and record the following permission-gated evidence (these checks open or foreground the overlay):
Ctrl+Shift+Cdelivers the selected frozen pixels; the saved PNG dimensions match the picker readout, including while zoomed or panned.- With
[capture].include_drawings = true, committed annotations are already visible over the frozen desktop while selecting and match the delivered PNG; transient strokes, handles, and UI stay hidden. In interactive Review, toggling drawings off/on updates that preview immediately. - Cancelling while pending, armed, or selecting leaves no picker-owned freeze behind, and the next capture action works.
- Output, scale, transform, zoom-level, and pan changes cancel with one visible message.
picker = "slurp"restores the external selector. Native acquisition failure hands off to it, while a dismissed portal request does not.- Pointer, touch, and stylus drags all select when the hardware is available; losing the owning device during a drag rearms the picker.
- Holding Shift before the first press produces a square crop, and the crosshair remains on the physical pointer while the squared frame follows the constrained endpoint.
Ctrl+Aduring a pointer, touch, or stylus drag captures the whole output and consumes the outstanding release without starting a canvas gesture.- Deactivating or aborting zoom while the picker waits cancels quietly; a reported zoom failure produces exactly one message.
- With
picker = "slurp"andformat = "jpg", the legacy path remains JPEG; native selection remains PNG-only. - On Hyprland and Sway,
Spaceenters window mode only after the provider is ready. Verify pointer, touch, and stylus selection, plusSuper+Arrownavigation andEnter, on overlapping tiled and floating windows. The highlighted readout and delivered PNG must have identical pixel bounds. Also switch workspaces during picker startup: candidates intentionally follow the workspace visible at provider-query time, while output/layout mismatches still disable window mode. - On Plasma/KWin, Niri, and GNOME, the window-mode legend and
Spacecontrol stay absent while area selection continues to work. Provider failure or an empty workspace must also fall back quietly.
At 1080p@1, 4K@2, and a fractional-scale 4K setup, capture the debug-level Region picker frame
timings during pointer motion. Also record the process peak-RSS delta while encoding a full-output
Ctrl+A crop. Keep these measurements with the change's manual test evidence; they are intentionally
not collected by CI.
Freeze and Zoom fail when the active output does not advertise a current wl_output mode.
Changes confined to another monitor preserve direct captures and installed active-output sources.
Capture preflight retries once on the selected backend after its required geometry settles,
while preserving the original request and keyboard policy. Direct backends require only active-output
geometry; portal captures require complete desktop geometry and also retry stale desktop snapshots.
Known output identities stay bound; requests started before
output discovery bind to the first output observed during the bounded wait. A second stale capture,
loss of a bound output, or an active-output switch is terminal. Board PDF desktop captures retain a
full-desktop generation check, including changes to other monitors even when the screenshot
dimensions stay the same.
dotnet run tools/wayscriber.cs --no-build -- report code-health reports navigational
maintainability metrics. Its CI artifact is
observational, not a global file/function-size gate; use the report to find code worth understanding,
not as a reason for mechanical splitting.
Documentation and release metadata
- User-visible config behavior belongs in
docs/CONFIG.mdandconfig.example.toml. - Installation, service, and shortcut behavior belongs in
docs/SETUP.mdand packaging docs. - Main-crate architecture belongs in
docs/codebase-overview.md. - Drafts under
docs/temp/are planning material unless explicitly promoted. - Version changes must go through the C#
version bumpcommand; keep both package manifests, rootCargo.lock, packaging metadata, and tag/release policy aligned. - Close a user-visible "I don't have that setting / this build" report only after the change is in
a tagged GitHub release.
mainis not whatarch-install.sh, AURwayscriber-bin, or other packaged installs ship.--versionreports the crate version, not the git hash, so bump withversion bumpin the same change as a user-visible overlay, settings, or config toggle (or immediately before tagging that release). Otherwise two binaries can print the samewayscriber 0.9.xand look identical.
See tools/README.md for build, install, packaging, version, and release helpers.
Run ./tools/lint-and-test.sh for the complete local gate; it needs the .NET SDK
selected by global.json. It builds the C# tools, then runs the same steps as CI's
ci lint-and-test: the C# repository checks, C# formatting, and the C# tests
(which run the retained packaging shell contracts), then lints, builds binaries,
and tests the whole workspace with all features and with no default features. CI runs the equivalent C# command and additionally checks dynamic
and static gtk4-layer-shell linkage and uploads its code-health report.
GTK widget coverage runs separately with ./tools/test-gtk-widgets.sh (Weston,
dbus-run-session, and Mesa software OpenGL required). It creates a private
headless display and requires GTK initialization; an unavailable display fails
this check. The native popup tests use another private Weston with software
OpenGL and a Rust protocol proxy to hold one popup frame callback while its
shared clock paints, then require capture to finish after the popup's fresh
render is acknowledged. They cover a plain GtkPopover and GtkPopoverMenu's
empty proof overlay and menu restoration. Successful bodies print EXECUTED
markers. Ordinary widget tests without a display report an
optional skip; when a display is available, their GTK assertions run. The native
popup tests require the dedicated GTK gate. Neither route proves layer-shell
focus or screen capture behavior on a user's compositor.