1#!/usr/bin/env bash
Redraw the pictures in the READMEs from a real Claude Code session.
tools/with-secrets.sh tools/screenshots.sh (mise run screenshots)
Nothing here is drawn by hand. A session runs in a detached tmux with the plugin loaded, against a daemon of its own (its own state and config directories, so the memory it "measures" is a number in a file this script writes). Each scene sends a prompt, waits for a line to appear on the screen, and turns the screen into an SVG with freeze. A scene whose line never appears fails the run and prints the screen: a picture of the wrong moment is worse than none.
The session reads no user settings (--setting-sources project,local), so the pictures show Claude
Code as it comes, not the status line of whoever ran this.
Needs: TYPESAFE_API_KEY in the environment, a signed-in claude (CLAUDE names another binary), the
daemon built (mise run daemon), and tmux, freeze and git, which mise install brings.
17set -euo pipefail 18root="$(cd "$(dirname "$0")/.." && pwd)" 19claude="${CLAUDE:-claude}" 20die() { echo "screenshots: $*" >&2; exit 1; } 21for tool in tmux freeze git; do command -v "$tool" >/dev/null || die "$tool is not installed (mise install)"; done 22[ -n "${TYPESAFE_API_KEY:-}" ] || die "TYPESAFE_API_KEY is not set (see fnox.toml)" 23[ -x "$root/plugin/bin/jevhooks" ] || die "the daemon is not built (mise run daemon)"
Short on purpose: the daemon's socket lives under it, and a socket's path has 103 bytes.
A tmux server of this run's own: another run's cleanup must not be able to end this one's session.
A small project with a little history, for the session to work in. git -C and not cd: a cd
that failed would leave every command after it running in this repository.
44demo="$work/demo" 45git -C "$demo" init -q -b main 46git -C "$demo" config user.name demo 47git -C "$demo" config user.email demo@example.com 48printf 'fn main() {\n println!("hello");\n}\n' > "$demo/main.rs" 49git -C "$demo" add -A 50git -C "$demo" commit -q -m "hello" 51printf '# demo\n' > "$demo/README.md" 52git -C "$demo" add -A 53git -C "$demo" commit -q -m "a readme"
55screen() { t capture-pane -p -t shots; }
wait_for <seconds> <regex>: until the screen shows it. Fails with the screen when it never does.
say <prompt>: types it and presses Enter (a separate keypress, or the TUI takes it as a newline).
67say() { t send-keys -t shots -l "$1"; sleep 0.5; t send-keys -t shots Enter; }
Rows Claude Code draws about the account of whoever runs this (how much of a plan's limit is used, and when it resets, in their timezone). They are no part of the plugin and do not belong in a README.
71private='weekly limit|usage-credits|session limit'
shot <file> <rows>: the last <rows> rows of the screen that have something on them, as an SVG. The font is a list, not an embedded file: a browser takes each character from the first font that has it, which is what gets the TUI's symbols drawn everywhere.
76shot() { 77 t capture-pane -e -p -t shots \ 78 | awk '{ rows[NR] = $0 } /[^[:space:]]/ { last = NR } END { for (i = 1; i <= last; i++) print rows[i] }' \ 79 | grep -Ev "$private" | tail -n "$2" > "$work/shot.ansi" 80 freeze "$work/shot.ansi" --language ansi --output "$root/$1" --window=false --padding 20 --background "#171717" \ 81 --font.family "JetBrains Mono, Cascadia Code, DejaVu Sans Mono, Menlo, Consolas, monospace, Noto Color Emoji, Apple Color Emoji, Segoe UI Emoji" \ 82 </dev/null >/dev/null # freeze reads a piped stdin in place of its file, and waits on it for ever 83 echo "screenshots: wrote $1" 84}
idle: the turn is over (the band shows its end).
87idle() { wait_for 120 'turn end: '; sleep 1; }
The scratch project is new, so Claude Code asks whether to trust it, with "No, exit" selected. Enter is pressed only once the screen shows the cursor on "Yes": a Down the dialog was not ready for would otherwise make Enter mean "exit", and the session would be gone.
95if screen | grep -q 'trust this folder'; then 96 for _ in $(seq 1 20); do 97 screen | grep -q '❯ Yes, I trust' && break 98 t send-keys -t shots Down 99 sleep 0.5 100 done 101 screen | grep -q '❯ Yes, I trust' || { screen >&2; die "could not select Yes in the trust dialog"; } 102 t send-keys -t shots Enter 103fi
The session starts the daemon; wait until it answers.
105for _ in $(seq 1 300); do "$root/plugin/bin/jevhooks" status >/dev/null 2>&1 && break; sleep 0.2; done 106"$root/plugin/bin/jevhooks" status >/dev/null 2>&1 || { 107 screen >&2 108 cat "$XDG_STATE_HOME/jevhooks/daemon.log" >&2 2>/dev/null || true 109 die "the session's daemon never answered" 110} 111sleep 2
- A prompt is given to a model. The band says which, until the turn's end replaces the line.
- An ordinary command runs without a prompt, and the band says what Jev made of it.
- A command with no room waits. The memory "available" drops to 1 GB, then comes back.
126echo 1000 > "$work/free" 127sleep 6 # the daemon reads the figure at most every 5 s 128say 'Run exactly this shell command, then reply with the one word done: cargo build --release' 129wait_for 60 'waiting for memory' 130sleep 3 131shot crates/jevhooks-daemon/src/waiting.svg 12 132echo 64000 > "$work/free" 133idle
- A command that is hard to undo is put to the user, with Jev's reason in the dialog.