No description
  • Go 95%
  • Just 5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Claudia 9cec46b1bb
All checks were successful
ci/woodpecker/push/build Pipeline was successful
chore: sync the files wrangle manages
.woodpecker/release.yml
2026-10-20 21:23:18 +02:00
.just chore: sync the files wrangle manages 2026-10-20 12:19:59 +02:00
.woodpecker chore: sync the files wrangle manages 2026-10-20 21:23:18 +02:00
internal feat: draw the prompt in zsh from the segments the config places 2026-10-20 12:19:59 +02:00
.gitignore feat: draw the prompt in zsh from the segments the config places 2026-10-20 12:19:59 +02:00
CHANGELOG.md docs: changelog for v0.1.1 2026-10-20 20:35:01 +02:00
config.example.toml feat: draw the prompt in zsh from the segments the config places 2026-10-20 12:19:59 +02:00
go.mod feat: declare the config as the hook's input, so a shell may cache it 2026-10-20 20:23:29 +02:00
go.sum feat: declare the config as the hook's input, so a shell may cache it 2026-10-20 20:23:29 +02:00
justfile feat: draw the prompt in zsh from the segments the config places 2026-10-20 12:19:59 +02:00
main.go feat: declare the config as the hook's input, so a shell may cache it 2026-10-20 20:23:29 +02:00
main_test.go feat: declare the config as the hook's input, so a shell may cache it 2026-10-20 20:23:29 +02:00
README.md feat: draw the prompt in zsh from the segments the config places 2026-10-20 12:19:59 +02:00

cairn

A small, fast zsh prompt. Like starship, but doing far less: a handful of segments, no per-language modules, and no process on any prompt.

[default] ⚓ ~/dev/vlv/moor%                                        ✘1 4s main

Install

just install          # to $XDG_BIN_HOME, or ~/.local/bin

Then, in ~/.zshrc, after moor's hook:

PS1='%~%# '
(( $+commands[cairn] )) && eval "$(cairn hook zsh)"

The plain PS1 is the prompt for a machine without cairn: the hook only ever replaces it, so a missing binary or a broken config leaves exactly that, and nothing on stderr. cairn status says which config it read and why it would not load.

cairn sets PS1 and RPS1 itself, before every prompt, so it replaces a promptinit theme rather than adding to one.

Segments

Segment Draws When
sandbox [profile], yellow inside a claudia sandbox ($CLAUDIA_PID)
anchor ⚓, red when outside the domain's root a moor domain is active
host %m over ssh ($SSH_TTY)
dir %~ always
status ✘N, red the last command failed
duration 4s, 1m5s, 1h2m, yellow the last command ran min_duration or longer
jobs &N, cyan background jobs exist
branch the branch, or the short hash detached, magenta inside a git checkout or worktree

A segment with nothing to say draws nothing, its separator included. The left side ends in %#. Placement is the whole config — see config.example.toml:

left  = ["sandbox", "anchor", "host", "dir"]
right = ["status", "duration", "jobs", "branch"]
min_duration = 3

Those are the defaults, and the left side of them is the prompt cairn replaced. The anchor used to be drawn by moor itself; moor now only exports the state (MOOR_DOMAIN, MOOR_ROOT), and drawing it is cairn's.

Why it is fast

The binary runs once per shell, when ~/.zshrc evals cairn hook zsh. That is also the only time the config is read: the hook it prints is plain zsh, one function per segment the config places and none for the rest, and a precmd that assembles them.

From then on a prompt is zsh alone. Everything a segment needs is already in the shell — $PWD, $?, the job table, moor's exported variables — except the branch, which is a walk up to the nearest .git (a stat per directory) and one read of HEAD. git itself is never run.

What that buys, measured on the default prompt in a repo inside a domain:

Per prompt Cost
cairn, every default segment ~37 µs
starting any Go binary, doing nothing ~570 µs
git symbolic-ref for the branch ~1.6 ms
just bench

The benchmark drives one zsh through many prompts and times it from inside, so start-up is not counted. TestAPromptSpawnsNothing holds the other half: the whole default prompt draws with PATH empty.

The branch is drawn, not run

A branch name comes from the repo, and git allows one called $(anything). With promptsubst set, zsh runs whatever substitution it finds in PS1 at draw time — so a branch placed in PS1 as text would run when you cd into a clone. cairn never places it there: under promptsubst, PS1 is ${_cairn_ps1}, and zsh does not re-expand what an expansion produced; without it, the value goes in directly and nothing is expanded. Either way a % from outside (a branch, a profile name) is doubled, so it is drawn rather than read as an escape.

Not yet

A dirty-tree marker. It is the one segment that needs a process — git status grows with the repo — so it would run in the background and redraw the prompt when done, rather than hold it up. The design leaves room for it; nothing here does it yet.