Skip to content

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.

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_DIR can point at a local Ghostty checkout instead.
  • .cargo/config.toml builds Ghostty with LIBGHOSTTY_VT_SYS_OPTIMIZE=ReleaseFast, because a debug build of it is too slow to use.
Terminal window
cargo run # build and attach, starting a server if needed
cargo run -- kill-server # stop that server

A 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:

Terminal window
CORNERCASE_SOCKET=/tmp/cc-dev/server.sock cargo run
CORNERCASE_SOCKET=/tmp/cc-dev/server.sock cargo run -- kill-server

If 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.

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:

Terminal window
T() { tmux -L cctest "$@"; }
T new-session -d -s t -x 100 -y 20 env -u CORNERCASE ./target/debug/cornercase
T send-keys -t t 'echo hi' Enter
T send-keys -t t -l $'\e[<0;6;5M' # mouse press at column 6, row 5 (1-based)
T capture-pane -p -t t
T kill-server

CI runs all of these on Linux and macOS; run them before opening a pull request:

Terminal window
cargo fmt
cargo clippy --all-targets --all-features --locked -- -D warnings
cargo test --locked
cargo-machete # unused dependencies: cargo install cargo-machete
npx -y jscpd@4.3.0 # copy-paste detector
  • Clippy runs with all denied and pedantic warned, configured in Cargo.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/ and tests/. 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 in src/ and tests/ 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 mod named after the unit of work, with names that read as sentences, like close_button_closes_its_entry. Tables of cases use rstest with named #[case::…].
  • Where tests live: the tests of foo.rs (or foo/mod.rs) go in foo/tests.rs, and a file over 1,000 lines keeps its helpers and puts each group in foo/tests/<group>.rs. They stay child modules, so they reach private items.
  • UI tests render a View into ratatui’s TestBackend: insta snapshots for layouts, cell styles for things like hover colours.
  • Terminal and app tests spawn real /bin/sh PTYs, never your own shell, and wait with test_util::wait_until, never with sleeps.
  • End-to-end tests in tests/e2e/ run the real binary in a PTY, parse its output with vt100, and send raw bytes and SGR mouse sequences. Each test gets its own server through a Session, which runs kill-server when 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 use fake_gh, FakeHttp and a fake agent script. The shared helpers live in test_util; each agent’s fake lives in agents/{claude,codex,gemini,opencode}/fake.rs. A test added as a safety net must fail without the code it protects.

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:

Terminal window
INSTA_UPDATE=always cargo test

Delete the snapshots of tests you renamed or removed by hand.

  • 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; anyhow only in src/main.rs.
  • No unwrap() outside tests; in tests prefer expect("what").
  • Mutexes are parking_lot::Mutex.
  • Imports in three groups: std, external crates, then super and crate.
  • rustfmt.toml sets max_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 test checks the imports and sizes through tests/layers.rs; its exception lists only shrink.
  • pub only for what src/main.rs or the end-to-end tests use; everything else is pub(crate) or narrower. The unreachable_pub lint enforces it, so the compiler’s dead_code warnings then point at code nothing uses: delete it, or put it under #[cfg(test)] when only tests use it.

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.

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 version in Cargo.toml above the last release;
  • add a ## <version> section to CHANGELOG.md saying 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.

The website and these docs live in site/, built with Astro and Starlight, and published to GitHub Pages by GitHub Actions.

  1. Install the dependencies and start the development server:

    Terminal window
    cd site
    npm ci
    npm run dev
  2. Open http://localhost:4321/.

  3. 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/*.ansi are tmux capture-pane -e -p -N dumps of cornercase, turned into SVG when the site is built. site/capture/capture.sh makes every one of them: it builds a fake home (the same projects as the landing page, from site/src/lib/demo/data.ts), runs the debug binary in its own tmux server with stand-ins for claude, codex, gh, ssh, nvim, npm and cargo, clicks what each screen needs and captures it. Run it after cargo build whenever the interface changes (capture.sh alone does every screen, capture.sh overview one); add a screen by adding a screen_<name> function to site/capture/screens.sh and using it with <Screen name="…" alt="…" />.
  • The live terminal on the home page is a simulation written in TypeScript that mirrors ui.rs and ui/; a change to the interface should be mirrored there too.