#!/usr/bin/env bash # The README's images, from made-up agents on a tmux server of their own. # # docs/screenshots.sh # # Needs a release build (cargo build --release), the menu bar app # (target/dingger-menu), and Google Chrome (headless, to compose the # images). The panel and search are drawn by the real app # (--snapshot-panel, --snapshot-search); the terminal is a real tmux client's # screen, colored as two terminal themes would color it. The menu bar strip # and window frames are drawn around them. Writes docs/images/*.png. set -eu ROOT=$(cd "$(dirname "$0")/.." && pwd) OUT="$ROOT/docs/images" WORK=$(mktemp -d /tmp/dingger-shots.XXXXXX) SOCK=dingger-demo-$$ TERMSOCK=dingger-demo-term-$$ CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" DINGGER="$ROOT/target/release/dingger" MENU="$ROOT/target/dingger-menu" trap 'tmux -L "$SOCK" kill-server 2>/dev/null; tmux -L "$TERMSOCK" kill-server 2>/dev/null; [ -n "${SHOTS_KEEP:-}" ] && echo "kept $WORK" || rm -rf "$WORK"' EXIT mkdir -p "$OUT" "$ROOT/docs/demo-agents.sh" "$SOCK" "$WORK/demo" t() { tmux -u -L "$SOCK" "$@"; } app() { env DINGGER_TMUX_SOCKET="$SOCK" DINGGER_STATE_DIR="$WORK/state" "$MENU" "$@"; } # --- the app's own renders ---------------------------------------------------- mkdir -p "$WORK/state" app --snapshot-panel "$WORK/panel.png" app --snapshot-panel "$WORK/panel-full.png" --full matched=$(app --print-search edit | tail -1 | cut -c1-7) [ "$matched" = "4 of 13" ] || { echo "search: 'edit' should match 4 of 13 agents, got: $matched"; exit 1; } app --snapshot-search "$WORK/search.png" "edit" # After a restart: the offer to restore. saved=$(($(date +%s) * 1000 - 3 * 60000)) cat >"$WORK/state/offer.json" <"$WORK/term.sh" <"$WORK/status.tmux" t source-file "$WORK/status.tmux" tmux -u -L "$TERMSOCK" -f /dev/null new-session -d -x 100 -y 20 \ "exec env -u TMUX TERM=xterm-256color tmux -u -L $SOCK attach -t main" sleep 1.5 tmux -u -L "$TERMSOCK" capture-pane -p -e -t 0 >"$WORK/term.ansi" # ANSI to HTML spans: classes for the 16 colors (each theme styles them), # inline colors for the 256-color grays. python3 - "$WORK/term.ansi" "$WORK/term.html" "$WORK/term-tail.html" <<'PY' import html, re, sys lines = open(sys.argv[1]).read().rstrip("\n").split("\n") fg, bold, dim = None, False, False def gray(n): if n < 16: return None if n >= 232: v = 8 + (n - 232) * 10; return f"rgb({v},{v},{v})" n -= 16; return "rgb(%d,%d,%d)" % tuple(0 if c == 0 else 55 + c * 40 for c in (n // 36, n // 6 % 6, n % 6)) def span(s): if not s: return "" cls, style = [], "" if isinstance(fg, int): cls.append(f"c{fg}") elif fg: style = f"color:{fg}" if bold: cls.append("b") if dim: cls.append("d") return f'{html.escape(s)}' def convert(text): global fg, bold, dim fg, bold, dim = None, False, False out = [] for part in re.split(r"(\x1b\[[0-9;]*m)", text): m = re.fullmatch(r"\x1b\[([0-9;]*)m", part) if not m: out.append(span(part)); continue codes = [int(c) if c else 0 for c in m.group(1).split(";")] i = 0 while i < len(codes): c = codes[i] if c == 0: fg, bold, dim = None, False, False elif c == 1: bold = True elif c == 2: dim = True elif c == 22: bold = dim = False elif 30 <= c <= 37: fg = c - 30 elif 90 <= c <= 97: fg = c - 90 + 8 elif c == 39: fg = None elif c == 38 and codes[i + 1] == 5: n = codes[i + 2]; fg = n if n < 16 else gray(n); i += 2 elif c == 38 and codes[i + 1] == 2: fg = "rgb(%d,%d,%d)" % tuple(codes[i + 2:i + 5]); i += 4 elif c in (48,): i += 2 if codes[i + 1] == 5 else 4 i += 1 return "".join(out) open(sys.argv[2], "w").write(convert("\n".join(lines))) # The prompt and the status line. open(sys.argv[3], "w").write(convert("\n".join(lines[-2:]))) PY # --- compose --------------------------------------------------------------------- # A snapshot is the view drawn twice, over white and over black, side by side # (points: 2w+20 wide); the second copy is the one composed here. size() { sips -g pixelWidth -g pixelHeight "$1" | awk '/pixelWidth/{w=$2} /pixelHeight/{h=$2} END{print w/2, h/2}'; } crop() { # name png -> CSS for a div showing the second copy read -r w h < <(size "$2") local half=$(echo "($w - 20) / 2" | bc) echo ".$1{width:${half}px;height:${h}px;background:url('$2') no-repeat;background-size:${w}px ${h}px;background-position:-$((half + 20))px 0}" } TERM_HTML=$(cat "$WORK/term.html") TERM_TAIL=$(cat "$WORK/term-tail.html") cat >"$WORK/style.css" <
$2
$TERM_HTML
" } page() { # name width height body printf '%s' "$2" "$3" "$4" >"$WORK/$1.html" rm -f "$WORK/$1-out.png" "$CHROME" --headless=new --disable-gpu --hide-scrollbars --force-device-scale-factor=2 \ --window-size="$2,$3" --screenshot="$WORK/$1-out.png" "file://$WORK/$1.html" >/dev/null 2>&1 for _ in $(seq 100); do [ -s "$WORK/$1-out.png" ] && break; sleep 0.1; done cp "$WORK/$1-out.png" "$OUT/$1.png" echo "wrote docs/images/$1.png" } clock=$(date "+%a %-d %b %H:%M") menubar="
GhosttyShellEditViewWindow
×2 ✓3 ◐3$clock
" page hero 1320 500 "
$menubar
$(termwin dark 'tmux — api')
" page panel 1000 690 "
Only when agents are waiting
After a restart: restore everything
Always, with the full list
" page search 700 360 "
" page statusline 900 250 "
a dark theme (Catppuccin Mocha)
$TERM_TAIL
a light theme (PaperColor Light)
$TERM_TAIL
"