Development
Contributions are welcome: bug reports, ideas and pull requests. For anything bigger than a small fix, open an issue first so you can agree on the approach. Questions and half-formed ideas are welcome on Discord. The full guidelines are in CONTRIBUTING.md, and the rules for every change are in AGENTS.md and every design decision is explained in design/, one file per area, listed in DESIGN.md.
Set up
Section titled “Set up”You need the same tools as for building from source: Linux or macOS, rustup, Zig 0.15.2 on your PATH, and git.
- The Rust toolchain is pinned in
rust-toolchain.toml, version and components (clippy,rustfmt). rustup installs it on the first build, and CI uses exactly the same one. - The first build of each profile clones Ghostty and builds its terminal core with Zig: about 100 seconds and 530 MB in
target/.GHOSTTY_SOURCE_DIRcan point at a local Ghostty checkout instead. .cargo/config.tomlbuilds Ghostty withLIBGHOSTTY_VT_SYS_OPTIMIZE=ReleaseFast, because a debug build of it is too slow to use.
Run your build
Section titled “Run your build”cargo run # build and attach, starting a server if neededcargo run -- kill-server # stop that serverA client never attaches to a server from another build, so after every rebuild cargo run asks to restart the old one: answer y, or run cargo run -- kill-server first. Debug builds never check for updates, and cargo run -- update refuses unless CORNERCASE_RELEASES_URL is set, as the end-to-end tests do.
cargo run builds and attaches to this checkout’s own server, so kill-server stops only that one. Each checkout and worktree gets its own server, session, log, stats, TODOs and queue in ~/.local/state/cornercase-dev/<key>/ (8 hex digits from the checkout’s path; a source file names the checkout). Settings, tokens, flows and plugins live in ~/.config/cornercase-dev/, shared by every checkout and empty at first: copy your config.json there. Release builds (cargo run --release, cargo install --path .) use your usual folders and server.
CORNERCASE_SOCKET still overrides all of it, for a second server from one checkout. Everything then lives next to that socket:
CORNERCASE_SOCKET=/tmp/cc-dev/server.sock cargo runCORNERCASE_SOCKET=/tmp/cc-dev/server.sock cargo run -- kill-serverIf you work on cornercase from a terminal inside cornercase, start the window with env -u CORNERCASE cargo run, and never run your installed cornercase kill-server there.
Check it in a real terminal
Section titled “Check it in a real terminal”To drive the app from a script, or from a terminal that already runs cornercase, start it in a separate tmux server and send it keys and mouse events:
T() { tmux -L cctest "$@"; }T new-session -d -s t -x 100 -y 20 env -u CORNERCASE ./target/debug/cornercaseT send-keys -t t 'echo hi' EnterT send-keys -t t -l $'\e[<0;6;5M' # mouse press at column 6, row 5 (1-based)T capture-pane -p -t tT kill-serverChecks
Section titled “Checks”CI runs all of these on Linux and macOS; run them before opening a pull request:
cargo fmtcargo clippy --all-targets --all-features --locked -- -D warningscargo test --lockedcargo-machete # unused dependencies: cargo install cargo-machetenpx -y jscpd@4.3.0 # copy-paste detector- Clippy runs with
alldenied andpedanticwarned, configured inCargo.toml. Silence a lint only locally, with#[expect(clippy::…, reason = "…")]. - jscpd fails above 1 % duplication (clones of 50 tokens or more) in every file of
src/andtests/. It’s a ratchet: extract the shared code rather than raising the threshold. - tests/layers.rs, part of
cargo test, checks which modules production code uses and keeps every Rust file insrc/andtests/at most 1,000 lines. The exceptions are existing violations; the lists only shrink, and an entry that no longer applies fails until it is removed. - cargo-machete is a text search; a false positive goes in
[package.metadata.cargo-machete] ignored.
- Unit tests sit next to the code, in a
modnamed after the unit of work, with names that read as sentences, likeclose_button_closes_its_entry. Tables of cases userstestwith named#[case::…]. - Where tests live: the tests of
foo.rs(orfoo/mod.rs) go infoo/tests.rs, and a file over 1,000 lines keeps its helpers and puts each group infoo/tests/<group>.rs. They stay child modules, so they reach private items. - UI tests render a
Viewinto ratatui’sTestBackend:instasnapshots for layouts, cell styles for things like hover colours. - Terminal and app tests spawn real
/bin/shPTYs, never your own shell, and wait withtest_util::wait_until, never with sleeps. - End-to-end tests in
tests/e2e/run the real binary in a PTY, parse its output withvt100, and send raw bytes and SGR mouse sequences. Each test gets its own server through aSession, which runskill-serverwhen dropped. Wait for output that proves the previous step finished before sending the next one. - Nothing calls the real
gh, GitLab, Shortcut, Linear, Jira, Plane or Asana: tests usefake_gh,FakeHttpand a fake agent script. The shared helpers live intest_util; each agent’s fake lives inagents/{claude,codex,gemini,opencode}/fake.rs. A test added as a safety net must fail without the code it protects.
Snapshots
Section titled “Snapshots”When a change alters what’s drawn, cargo test writes its .snap.new in the snapshots/ folder next to the test’s file (src/ui/tests/snapshots/ for the ui tests) and fails. Look at the new render; if it’s right, accept it:
INSTA_UPDATE=always cargo testDelete the snapshots of tests you renamed or removed by hand.
Code style
Section titled “Code style”- Everything in English: identifiers, test names, messages and interface text.
- No comments, doc comments included. If a decision needs explaining, it goes in its area’s file in
design/. - Errors use
crate::error::Error(thiserror) in the library;anyhowonly insrc/main.rs. - No
unwrap()outside tests; in tests preferexpect("what"). - Mutexes are
parking_lot::Mutex. - Imports in three groups:
std, external crates, thensuperandcrate. rustfmt.tomlsetsmax_width = 120.- Where code goes is in
design/code-layout.md: by domain, with tests in files of their own and at most 1,000 lines per file.cargo testchecks the imports and sizes throughtests/layers.rs; its exception lists only shrink. pubonly for whatsrc/main.rsor the end-to-end tests use; everything else ispub(crate)or narrower. Theunreachable_publint enforces it, so the compiler’sdead_codewarnings then point at code nothing uses: delete it, or put it under#[cfg(test)]when only tests use it.
Pull requests
Section titled “Pull requests”Keep each pull request to one change. Describe what changed and why, how you tested it, and add a screenshot or a recording for anything visible. Update the area’s file in design/ when you change a design decision, design/architecture.md when you add a module, and AGENTS.md when you change a rule. Contributions are dual licensed under MIT and Apache-2.0, like the rest of the project.
Versions and releases
Section titled “Versions and releases”Every pull request that changes the app (src/, Cargo.toml, Cargo.lock, the toolchain or .cargo/) releases a new version, and CI checks it:
- bump
versioninCargo.tomlabove the last release; - add a
## <version>section toCHANGELOG.mdsaying what changed, written for users: it becomes the release notes on GitHub and in the app’s update dialog.
Merging it to main publishes the release automatically: CI tags the version, builds the binaries for Linux and macOS, and publishes the GitHub Release, the install script’s installer and the Homebrew formula. Running cornercase then offers the update within a day.
This website
Section titled “This website”The website and these docs live in site/, built with Astro and Starlight, and published to GitHub Pages by GitHub Actions.
-
Install the dependencies and start the development server:
Terminal window cd sitenpm cinpm run dev -
Open
http://localhost:4321/. -
Before you push, make sure the production build passes:
Terminal window npm run build
- The docs are Markdown and MDX files in
site/src/content/docs/docs/. Links between pages are relative and end with a slash, so the site works under any base path. - The terminal screens in the docs are real:
site/src/screens/*.ansiaretmux capture-pane -e -p -Ndumps of cornercase, turned into SVG when the site is built.site/capture/capture.shmakes every one of them: it builds a fake home (the same projects as the landing page, fromsite/src/lib/demo/data.ts), runs the debug binary in its own tmux server with stand-ins forclaude,codex,gh,ssh,nvim,npmandcargo, clicks what each screen needs and captures it. Run it aftercargo buildwhenever the interface changes (capture.shalone does every screen,capture.sh overviewone); add a screen by adding ascreen_<name>function tosite/capture/screens.shand using it with<Screen name="…" alt="…" />. - The live terminal on the home page is a simulation written in TypeScript that mirrors
ui.rsandui/; a change to the interface should be mirrored there too.