chore: rustfmt + clippy, enforced in CI - #150
Merged
Merged
Conversation
roost was hand-formatted for its whole life and enforced nothing mechanically, so both were one careless commit away from drifting. This adds the two tools, tuned rather than imposed, and a CI job that makes them binding. **rustfmt.** The naive `max_width = 100` rewrites 1,632 hunks; adding `use_small_heuristics = "Max"` cuts that to 556, and `max_width = 110` is worse again at 773. So the tree was already written to ~100 columns with everything that fits kept on one line — `rustfmt.toml` is that finding written down, not a new house style. Stable options only: this crate pins `rust-version = "1.89"` and CI runs stable, where a nightly-only key is *silently ignored*, which is worse than no rule because it reads like one. The reformat itself is the next commit, and is listed in `.git-blame-ignore-revs`. **Lints in `Cargo.toml`, not only in CI.** `[lints.rust]`/`[lints.clippy]` apply to every local `cargo check`, so a rule is visible in the editor the moment it is broken rather than twenty minutes later on a runner. Every lint enabled was verified to already hold — this pins discipline the codebase has, it does not schedule a cleanup. Three candidates were measured and rejected, which is most of the value here: - `unreachable_pub` — 582 hits. roost is a *binary*, so every `pub` is unreachable by definition, and almost all of them are in `tests/harness/mod.rs` where `pub` is how a shared test module offers anything at all. A lint that is right 0% of the time trains people to ignore the ones that are right. - `clippy::str_to_string` — 184 hits, all asking `"x".to_string()` to become `.to_owned()`. Same allocation, same meaning, 184-line diff. - `trivial_casts` — fires only on the FFI reference-to-pointer casts (`&mut info as *mut _ as *mut c_void`, a fn item to a fn pointer for `libc::signal`) that make those call sites read like the C prototypes they are calling. Its numeric sibling has no such false-positive class, so that half stays. What the survivors found, all fixed here: - **`clippy::undocumented_unsafe_blocks`: 10 unsafe blocks with no `SAFETY:` comment.** Every other one in the tree has one — this was a convention, and it had lapsed in ten places (`kill(2)` in the pane sweep, `geteuid`, two `mem::zeroed` FFI structs, `localtime_r`, and the counting allocator in `tests/scrollback_memory.rs`). Now a rule, and the ten are written. - `elided_lifetimes_in_paths` (via `rust_2018_idioms`): 32 sites, all `&mut Frame` / `Vec<Line>` / `Vec<Span>` in the renderer, now spelled `<'_>` so the borrow is visible. - `trivial_numeric_casts`: one `1 as PaneId` where `PaneId` is `u64`. **CI** gets a `lint` job — once, on ubuntu, not inside the OS matrix: formatting and lints are properties of the source, not of the platform, and a run that will fail `cargo fmt --check` should say so in twenty seconds rather than after two full test matrices. It is deliberately not added to `main`'s required checks by this commit; that is a repo-settings change for whoever administers branch protection. `vendor/vt100` is untouched throughout: it is a path dependency rather than a workspace member, so neither tool reaches it. That is the intent — reformatting vendored code would bury roost's own patches to it the next time it is diffed against upstream. CLAUDE.md's "roost is NOT rustfmt-formatted — never run `cargo fmt`" rule is reversed in the same commit, since following it would now fail CI.
Mechanical. `cargo fmt` with the `rustfmt.toml` added in the previous commit, and nothing else — no logic, no renames, no comment rewrites. Verified by re-running the whole suite (1,047 tests) and clippy (clean) against the reformatted tree. That verification is not a formality here. Three of roost's gates read its own source text rather than its behaviour — theme.rs's §2 colour ban, C34's chord-literal ban, and `srcscan::production`'s test-module cut — and all three depend on where a line ends and which trailing comment sits on it (`// chrome-gate-exempt: program output` is load-bearing punctuation). A reformat is exactly the change that could have moved a marker off its line. It did not. Add this commit's *post-squash* hash to `.git-blame-ignore-revs` once it lands, then `git config blame.ignoreRevsFile .git-blame-ignore-revs` locally; GitHub reads the file on its own.
The first CI run of the new `lint` job failed, and the failure is the
argument for this commit: four findings that do not exist in the clippy
this branch was developed against (0.1.89 — a drifted local `stable`),
so they were unreproducible locally and would have been equally
unreproducible for anyone whose toolchain sat still.
`-D warnings` against a moving `stable` means a clippy release can turn
a green branch red with no code change. So the lint job pins its
toolchain, and **only** the lint job: a repo-wide `rust-toolchain.toml`
would also pin what `cargo build`, the test matrix and — worst —
`release.yml`'s four-target build compile with, and the release path is
the one thing here that cannot be rehearsed. Lint determinism is worth a
pin; build determinism is not worth that risk.
The four, all real:
- `clippy::sort_by_key` x2 — `sort_by(|a, b| b.0.cmp(&a.0))` is a
hand-rolled reverse sort; `sort_by_key(|(mtime, _)| Reverse(*mtime))`
says "newest first" in the code instead of in the comment beside it.
- `clippy::needless_return` x2, **Linux only** — `infra::qos`'s two
promote functions guard with `if !enabled() { return; }` and then run
a `#[cfg(target_os = "macos")]` block. Off macOS that block vanishes
and the whole body is a bare `return;`. Both are now gated as a whole
(`#[cfg(target_os = "macos")] if enabled()`), which is the same
condition — `enabled()` is `cfg!(macos) && ...` — spelled where it can
be read.
Verified against the pinned toolchain on both of CI's platforms:
`cargo +1.96.1 clippy --all-targets` clean on aarch64-apple-darwin *and*
on x86_64-unknown-linux-gnu (the half of `qos.rs` a macOS-only check can
never see), `cargo +1.96.1 fmt --check` clean, 1,047 tests green.
rustfmt 1.96.1 produces a zero-line diff against the tree formatted by
1.89 — worth recording, since it means the reformat in this PR is not
version-sensitive the way the lints turned out to be.
navbytes
added a commit
that referenced
this pull request
Aug 21, 2026
#150 reformatted 52 files. Without this, `git blame` on ~3,300 lines answers "the formatting commit" instead of whoever actually wrote them, which on a codebase whose comments carry most of its reasoning is the real cost of adopting rustfmt at all. The hash could not be filled in before the merge: this repository squash-merges, so the SHA on the PR branch is not the SHA in history. It is also not a *pure* formatting commit, for the same reason — the squash collapsed the reformat together with the config and the 43 lint fixes the new rules turned up. The file says so rather than pretending otherwise, and the rule it states is relaxed to match what a squash-merging repo can actually produce: "overwhelmingly mechanical, and say plainly what else is in it". Verified: `git blame` over `src/core/app.rs` with the file configured attributes lines to their 2026-07-20 authors again instead of to #150. GitHub reads this file on its own; locally it needs one command, which the file's own header gives: git config blame.ignoreRevsFile .git-blame-ignore-revs
navbytes
added a commit
that referenced
this pull request
Aug 21, 2026
…e request (#155) v0.1.10 shipped at 01:48 UTC today, before any of this session's work landed, so everything below is unreleased. Touching `.github/release-request` dispatches Release, which builds four targets, creates the `v0.1.11` tag from Cargo.toml itself, and publishes with SHA256SUMS.txt. **Two silent data-loss bugs.** - `Alt+w` on the last pane — whose own prompt calls it "quit roost" — removed the pane and *then* quit, so `shutdown` saved a tab whose layout named a pane its map no longer had. The repair minted a blank `shell` spec on next launch, and the session id, cwd, title and note of the pane you quit on were gone. `Alt+q` on the same pane kept all of them. (#147) - A recycled pane id inherited the dead pane's session-detection clock. `last_shell_seen` and `pending_detect` were the only two `PaneId`-keyed maps `close_pane_id` did not prune, so a pane taking that id could scan hours back and resume a stranger's conversation, permanently. (#141) **Idle cost down ~13x.** 2.75% CPU and 888 B/s of terminal traffic, to 0.2–0.4% and ~62 B/s, flat in the pane count at every step because it was all fixed per-frame chrome: a `String` allocated per cell per frame (#140); the quick-launch picker's `$PATH` probe running ~1,500 `stat(2)` a second with no dialog on screen, plus the whole help table, every frame (#144); an unconditional 30 fps repaint (#146); and 12 fps even with nothing animating (#149). **Desktop notifications changed channel, and this one is user-visible.** They used to fork `osascript`, which macOS attributes to **Script Editor** — there is no flag for that; a CLI cannot post under its own name. roost now asks the terminal instead, with a bell and an `OSC 9`, which is the mechanism SPEC-parity P2 already named. Ghostty, iTerm2, WezTerm and kitty raise a real notification from it, attributed to themselves, and it survives ssh. **A host that ignores OSC 9 (Apple Terminal, Alacritty) now gets only the bell.** (#152) And a pane's notification no longer arrives twice: there were two emitters, invisible as a duplicate only while one of them was the `osascript` fork. The named one won, so one banner, saying which pane, and silence for the pane you are already looking at. (#153) **Three new fuzzers** over input surfaces nothing reached before — the mouse router, the key router, and paste — each walking a layout being reshaped underneath it, drawing a real frame and re-checking the layout fuzzer's own invariants after every step. The mouse one found #147 on seed 15. (#147, #148) **rustfmt and clippy, enforced in CI** (#150), tuned to the style the tree already had rather than imposed: the config was picked by measuring churn (556 hunks vs 1,632 for the naive setting). The lint set is in `Cargo.toml`'s `[lints]` tables so it binds locally too, and three candidate lints were measured and rejected. It found ten `unsafe` blocks missing the `SAFETY:` comment every other one in the tree has. Plus: the reformat kept out of `git blame` (#151), and three test gates that blamed roost for the machine being out of ptys (#154). Verified before cutting: 1,048 tests, clippy clean at `-D warnings`, `fmt --check` clean. Both agent adapters checked end to end against live binaries (`pi --session …`, `claude --resume …`), the session-resolution table across all five adapters, the 45s status decays timed against the clock, and two 15-minute soaks (~78k random keystrokes each, ~1,800 panes) with no orphan, no leak and no unreadable workspace. Note for after the merge: `HOMEBREW_TAP_TOKEN` is still unset, so release.yml will skip the tap sync again (it did for v0.1.10). The formula needs the usual hand-sync.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two commits, deliberately separate so the mechanical one can be read as
mechanical:
chore: rustfmt and clippy, configured to the style the tree already had—
rustfmt.toml,Cargo.toml's[lints]tables, a CIlintjob,.git-blame-ignore-revs, and the 43 lint fixes the new rules turned up.chore: apply rustfmt—cargo fmtand nothing else. 52 files,+1991/-1299.
rustfmt, tuned rather than imposed
roost was hand-formatted for its whole life. The config was chosen by
measuring the churn of each candidate against the existing tree, not by
taste:
max_width = 100max_width = 100+use_small_heuristics = "Max"max_width = 110+use_small_heuristics = "Max"So the tree was already written to ~100 columns with everything that fits
kept on one line.
rustfmt.tomlis that finding written down. Stableoptions only — this crate pins
rust-version = "1.89"and CI runs stable,where a nightly-only key is silently ignored, which is worse than no rule
because it reads like one.
Lints in
Cargo.toml, not only in CI[lints.rust]/[lints.clippy]apply to every localcargo check, so arule is visible in the editor the moment it is broken rather than twenty
minutes later on a runner.
-D warningsin CI is what makes them binding.Every lint enabled was verified to already hold. Three candidates were
measured and rejected, which is most of the value here:
unreachable_pub— 582 hits. roost is a binary, so everypubisunreachable by that lint's definition, and almost all of them are in
tests/harness/mod.rswherepubis how a shared test module offersanything at all. A lint that is right 0% of the time trains people to
ignore the ones that are right.
clippy::str_to_string— 184 hits, all asking"x".to_string()tobecome
.to_owned(). Same allocation, same meaning, 184-line diff.trivial_casts— fires only on the FFI reference-to-pointer caststhat make those call sites read like the C prototypes they are calling.
Its numeric sibling has no such false-positive class, so that half stays.
What the survivors found
clippy::undocumented_unsafe_blocks: 10 unsafe blocks with noSAFETY:comment. Every other one in the tree has one — this was aconvention and it had lapsed in ten places (
kill(2)in the pane sweep,geteuid, twomem::zeroedFFI structs,localtime_r, and the countingallocator in
tests/scrollback_memory.rs). Now a rule, and the ten arewritten.
elided_lifetimes_in_paths(viarust_2018_idioms): 32 sites, all&mut Frame/Vec<Line>/Vec<Span>in the renderer.trivial_numeric_casts: one1 as PaneIdwherePaneIdisu64.Verification
1,047 tests and a clean clippy against the reformatted tree. Not a
formality: three of roost's gates read its own source text rather than its
behaviour — theme.rs's §2 colour ban, C34's chord-literal ban, and
srcscan::production's test-module cut — and all three depend on where aline ends and which trailing comment sits on it (
// chrome-gate-exemptisload-bearing punctuation). A reformat is exactly the change that could have
moved a marker off its line. It did not.
Two follow-ups for the maintainer
lintjob is not inmain's required checks (those are the twomatrix jobs). Add it in repo settings to make it blocking.
.git-blame-ignore-revsneeds the reformat's post-squash hash, whichcannot be known before merge. I'll push that one-liner immediately after
this lands.
vendor/vt100is untouched throughout — it is a path dependency, not aworkspace member, so neither tool reaches it. Deliberate: reformatting
vendored code would bury roost's own patches the next time it is diffed
against upstream. CLAUDE.md's old "never run
cargo fmthere" rule isreversed in commit 1, since following it would now fail CI.