1#!/usr/bin/env bash 2# Redraw the pictures in the READMEs from a real Claude Code session. 3# 4# tools/with-secrets.sh tools/screenshots.sh (mise run screenshots) 5# 6# Nothing here is drawn by hand. A session runs in a detached tmux with the plugin loaded, against a 7# daemon of its own (its own state and config directories, so the memory it "measures" is a number in a 8# file this script writes). Each scene sends a prompt, waits for a line to appear on the screen, and 9# turns the screen into an SVG with freeze. A scene whose line never appears fails the run and prints 10# the screen: a picture of the wrong moment is worse than none. 11# 12# The session reads no user settings (`--setting-sources project,local`), so the pictures show Claude 13# Code as it comes, not the status line of whoever ran this. 14# 15# Needs: TYPESAFE_API_KEY in the environment, a signed-in `claude` (CLAUDE names another binary), the 16# 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)" 24 25# Short on purpose: the daemon's socket lives under it, and a socket's path has 103 bytes. 26work="$(mktemp -d "${TMPDIR:-/tmp}/jevshots.XXXXXX")" 27[ -d "$work" ] || die "no scratch directory" 28export XDG_STATE_HOME="$work/state" XDG_CONFIG_HOME="$work/config" 29# A tmux server of this run's own: another run's cleanup must not be able to end this one's session. 30t() { tmux -L "jevshots-$$" "$@"; } 31cleanup() { 32 t kill-server 2>/dev/null || true 33 "$root/plugin/bin/jevhooks" stop >/dev/null 2>&1 || true 34 rm -rf "$work" 35} 36trap cleanup EXIT 37 38mkdir -p "$XDG_CONFIG_HOME/jevhooks" "$work/demo" 39printf 'available_memory_command = "cat %s/free"\nchoose_model = true\n' "$work" > "$XDG_CONFIG_HOME/jevhooks/config.toml" 40echo 64000 > "$work/free" 41 42# A small project with a little history, for the session to work in. `git -C` and not `cd`: a `cd` 43# 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" 54 55screen() { t capture-pane -p -t shots; } 56 57# wait_for <seconds> <regex>: until the screen shows it. Fails with the screen when it never does. 58wait_for() { 59 local until=$((SECONDS + $1)) 60 while ! screen | grep -Eq -- "$2"; do 61 [ "$SECONDS" -lt "$until" ] || { screen >&2; die "the screen never showed: $2"; } 62 sleep 0.2 63 done 64} 65 66# 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; } 68 69# Rows Claude Code draws about the account of whoever runs this (how much of a plan's limit is used, 70# 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' 72 73# shot <file> <rows>: the last <rows> rows of the screen that have something on them, as an SVG. The 74# font is a list, not an embedded file: a browser takes each character from the first font that has it, 75# 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} 85 86# idle: the turn is over (the band shows its end). 87idle() { wait_for 120 'turn end: '; sleep 1; } 88 89t new-session -d -s shots -x 120 -y 28 -c "$demo" "$claude --plugin-dir '$root/plugin' --setting-sources project,local --permission-mode manual" 90t set-option -g focus-events on # or the TUI spends a row of every picture asking for it 91wait_for 60 'trust this folder|for shortcuts' 92# The scratch project is new, so Claude Code asks whether to trust it, with "No, exit" selected. Enter 93# is pressed only once the screen shows the cursor on "Yes": a Down the dialog was not ready for would 94# 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 104# 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 112 113# 1. A prompt is given to a model. The band says which, until the turn's end replaces the line. 114say 'What branch am I on? Answer in three words.' 115wait_for 30 'prompt: Jev: ' 116shot crates/jevhooks-daemon/src/model.svg 9 117idle 118 119# 2. An ordinary command runs without a prompt, and the band says what Jev made of it. 120say 'Run exactly this shell command, then reply with the one word done: mkdir -p build && echo hello > build/out.txt' 121wait_for 60 'command: Jev: .*allowed' 122shot plugin/hooks/band.svg 12 123idle 124 125# 3. 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 134 135# 4. A command that is hard to undo is put to the user, with Jev's reason in the dialog. 136dialog='Do you want to proceed|requires confirmation' 137say 'I want my last commit and every untracked file gone. Run exactly this shell command: git reset --hard HEAD~1 && git clean -fdx' 138wait_for 30 'prompt: Jev: ' 139wait_for 120 "$dialog|turn end: " 140# A careful model asks before it tries; say yes, and the command reaches the hook. 141if ! screen | grep -Eq -- "$dialog"; then 142 say 'Yes, run it now.' 143 wait_for 120 "$dialog" 144fi 145sleep 1 146shot plugin/hooks/ask.svg 22 147t send-keys -t shots Escape