For installation, configuration and CLI usage, see the README.
Use macOS 15 or newer on Apple Silicon or Intel, with the Xcode Command Line Tools and Rust installed through rustup. Cargo selects the toolchain pinned in rust-toolchain.toml, including rustfmt and Clippy.
Run these commands from the repository root:
make build
target/debug/runon check -c examples/config.kdl
target/debug/runon run -c examples/config.kdlEdit the example or use your own configuration before running it: its commands are examples, not bundled programs. Foreground mode stops with Ctrl-C. Use target/debug/runon events to inspect native events without executing actions.
To install a release build in ~/.local/bin without sudo:
make installAdd that directory to your shell's PATH. Installation does not start the service; run runon start after configuring it, or runon restart to apply a binary update to an existing service.
make checkThis runs rustfmt, Clippy with warnings treated as errors, shell syntax checks and workspace tests. Use make lint or make test to run those parts separately. Tests cover parsing, matching, scheduling, subprocesses, logging, the CLI and the installer.
The platform-independent crates can also be tested outside macOS:
cargo test --locked -p runon-core -p runon-configNative integration checks require a real logged-in Mac:
cargo test --locked -p runon --test service -- --ignored --nocapture
cargo run --locked -p runon-macos --example native_smokeThe ignored service test creates a temporary LaunchAgent with a unique label. The native smoke test opens a temporary app without activating it and checks subscriptions, snapshots and event delivery. Neither replaces physical display, audio, lock, wake or power testing. Follow the hardware acceptance checklist for event-source changes.
CI runs make check, packaging and example-config validation on macOS 15 ARM64/Intel and macOS 26 ARM64. Local validation records and outstanding hardware checks are in docs/validation.md.
| Crate | Responsibility | Internal dependencies |
|---|---|---|
runon-core |
Typed events and clock-controlled scheduler; no platform APIs, unsafe code or external dependencies | None |
runon-config |
Config types, KDL loading and validation, event-selector formatting | runon-core |
runon-macos |
Native event sources, Dispatch/RunLoop/signal ownership, system paths and LaunchAgent integration | runon-config, runon-core |
runon-runtime |
Matching, action execution, process supervision and scheduler queue | runon-config, runon-core, runon-macos |
runon |
CLI, logging and component wiring | All four libraries |
The root Cargo.toml owns shared dependency versions, package metadata, lint policy and release settings. Every crate uses workspace lints; the workspace shares Cargo.lock and target/. Tests and executable examples live with their owning crates; user-facing KDL examples live in examples/.
Keep dependencies flowing toward the libraries: they never depend on the CLI. Native sources emit typed events through a callback without depending on the runtime. runon-config loads an explicitly supplied path and formats events with format_selector; runon-macos::paths owns home/config discovery and the command search path. The scheduler takes the parallelism limit and debounce durations without depending on the config parser. Process supervision stays private to runon-runtime, whose public entry points are Runtime and Report.
Configuration is parsed once into typed rules, then the KDL document is released. The main thread runs AppKit's event loop. Only configured sources are subscribed, plus wake notifications needed to refresh device and power snapshots. Snapshot comparisons suppress duplicate changes; native refresh requests are coalesced.
Runtime::submit matches events and updates the scheduler under a short mutex. The scheduler owns batch replacement and readiness order, keeping at most one pending batch per group. Ready groups run in readiness order; replacing a waiting zero-debounce batch preserves its position.
A serial DispatchQueue drives process lifecycle through native data, process, pipe, timer and signal sources, without an async runtime, polling or idle timer. Report callbacks execute on that queue outside state locks and must not block; the CLI forwards diagnostics to a log worker.
Each step owns a process group. Timeout or shutdown sends SIGTERM, allows two seconds for cleanup, then sends SIGKILL before reaping the leader and releasing its slot. The grace period also covers descendants when the leader exits first. Normal completion kills leftover descendants before the next step. Deadlines use monotonic awake time, excluding sleep.
Both output streams are drained without blocking and retain only their last 64 KiB for failure reports. Diagnostic writes use a separate thread with a 16-record queue. A full queue drops new records and reports the count when writing resumes; exit waits at most 100 ms for queued diagnostics. Logging must not delay process timeouts or shutdown.
make measureThis builds the release binary, measures idle behavior for five minutes through an external Python 3 sampler, then runs the launch-latency and event-flood benchmark. It starts a separate daemon without installing a service. Use a quiet, logged-in Mac; see measurement methods, budgets and recorded results.
make build-releaseLocal packaging builds the host architecture and writes these files to dist/ without publishing:
runon-macos-arm64.tar.gzorrunon-macos-x86_64.tar.gzSHA256SUMSnotes.md, extracted from the matchingvVERSIONsection inCHANGELOG.md
Archives include the binary, license, README, KDL examples and docs/. Packaging verifies the checksum and enforces a 5 MiB binary limit. Release settings use size optimization, LTO, one codegen unit, symbol stripping and abort-on-panic. The deployment target is macOS 15.0; the toolchain and dependency lockfile are pinned.
For a release, update the workspace version and lockfile, add the matching vVERSION changelog entry, and run make check and make build-release. Validate the packaged version's examples:
target/release/runon check -c examples/config.kdl
target/release/runon check -c examples/editors.kdlThe release workflow builds natively on macOS 15 for ARM64 and Intel and combines their checksums. Pushing a v* tag matching the binary version publishes both archives and SHA256SUMS as a GitHub release. A manual workflow run only builds artifacts.