diff options
Diffstat (limited to 'docs/screenshots.sh')
| -rwxr-xr-x | docs/screenshots.sh | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/docs/screenshots.sh b/docs/screenshots.sh new file mode 100755 index 0000000..ffe7291 --- /dev/null +++ b/docs/screenshots.sh @@ -0,0 +1,198 @@ +#!/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" <<EOF +{"saved_at": $saved, "agents": 22, "interrupted": 2, "sessions": [ + {"name": "api", "windows": 6, "panes": 9, "agents": 7}, {"name": "web", "windows": 4, "panes": 6, "agents": 5}, + {"name": "infra", "windows": 3, "panes": 4, "agents": 3}, {"name": "docs", "windows": 3, "panes": 3, "agents": 2}, + {"name": "cli", "windows": 4, "panes": 5, "agents": 3}, {"name": "scratch", "windows": 2, "panes": 2, "agents": 0}, + {"name": "notes", "windows": 1, "panes": 1, "agents": 0}, {"name": "ops", "windows": 2, "panes": 3, "agents": 2}, + {"name": "0", "windows": 1, "panes": 1, "agents": 0}]} +EOF +app --snapshot-panel "$WORK/panel-restore.png" +rm "$WORK/state/offer.json" + +# --- a terminal: tmux with `dingger status` and the status line --------------- +prompt='\033[1;34m~/src/api\033[0m \033[35m❯\033[0m' +cat >"$WORK/term.sh" <<EOF +#!/bin/sh +clear +printf '$prompt dingger status\n' +DINGGER_TMUX_SOCKET=$SOCK $DINGGER status +printf '\n$prompt ' +exec sleep 100000 +EOF +chmod +x "$WORK/term.sh" +t new-session -d -s main -n api -c / "$WORK/term.sh" +t set -g status-style "fg=colour244,bg=default" +t set -g status-left "#[fg=blue,bold] #S #[default]" +t set -g status-left-length 20 +t set -g window-status-format " #I:#W " +t set -g window-status-current-format "#[fg=default,bold] #I:#W* #[default]" +t set -g status-right-length 80 +grep '^set -g status-right' "$ROOT/packaging/tmux/dingger.tmux" >"$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'<span class="{" ".join(cls)}" style="{style}">{html.escape(s)}</span>' +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" <<EOF +$(crop panel "$WORK/panel.png") +$(crop panelfull "$WORK/panel-full.png") +$(crop panelrestore "$WORK/panel-restore.png") +$(crop search "$WORK/search.png") +* { box-sizing: border-box; margin: 0 } +body { font-family: -apple-system, "SF Pro Text", sans-serif; } +.wall { position: relative; overflow: hidden; + background: radial-gradient(1200px 700px at 15% 10%, #4f5d95 0%, transparent 60%), + radial-gradient(1000px 800px at 90% 100%, #8a4f7d 0%, transparent 55%), + linear-gradient(160deg, #1d2140, #2d2346 55%, #1b1f33); } +.shadow { border-radius: 12px; box-shadow: 0 18px 50px rgba(0,0,0,.45), 0 0 0 .5px rgba(255,255,255,.12); } +.menubar { position: absolute; top: 0; left: 0; right: 0; height: 26px; background: rgba(30,30,46,.55); + backdrop-filter: blur(20px); color: #e6e6ef; font-size: 13px; display: flex; align-items: center; + padding: 0 14px; gap: 18px; } +.menubar .right { margin-left: auto; display: flex; gap: 16px; align-items: center; } +.menubar .item { font-weight: 500; font-variant-numeric: tabular-nums; padding: 1px 7px; border-radius: 5px; background: rgba(255,255,255,.14) } +.r { color: #f38ba8 } .t { color: #94e2d5 } .y { color: #f9e2af } +.term { border-radius: 11px; overflow: hidden; } +.term .bar { height: 30px; display: flex; align-items: center; gap: 8px; padding: 0 12px; font-size: 12.5px; } +.term .dot { width: 12px; height: 12px; border-radius: 50%; } +.term .title { flex: 1; text-align: center; margin-right: 52px; opacity: .7 } +.term pre { font: 13px/1.38 "SF Mono", Menlo, monospace; padding: 10px 14px 6px; white-space: pre; } +.b { font-weight: 700 } .d { opacity: .6 } +/* Catppuccin Mocha */ +.dark { background: #1e1e2e; color: #cdd6f4 } .dark .bar { background: #181825; color: #cdd6f4 } +.dark .c0{color:#45475a}.dark .c1{color:#f38ba8}.dark .c2{color:#a6e3a1}.dark .c3{color:#f9e2af}.dark .c4{color:#89b4fa}.dark .c5{color:#f5c2e7}.dark .c6{color:#94e2d5}.dark .c7{color:#bac2de} +/* PaperColor Light */ +.light { background: #eeeeee; color: #4d4d4c } .light .bar { background: #e2e2e2; color: #4d4d4c } +.light .c0{color:#ededed}.light .c1{color:#d7005f}.light .c2{color:#718c00}.light .c3{color:#d75f00}.light .c4{color:#4271ae}.light .c5{color:#8959a8}.light .c6{color:#3e999f}.light .c7{color:#4d4d4c} +.dots { display: contents } .dot.a { background: #ff5f57 } .dot.b2 { background: #febc2e } .dot.c { background: #28c840 } +.label { color: rgba(255,255,255,.7); font-size: 12px; margin: 0 0 7px 4px } +.caption { color: rgba(255,255,255,.75); font-size: 13px; text-align: center; margin-top: 12px } +EOF +termwin() { # theme title + echo "<div class=\"term shadow $1\"><div class=\"bar\"><span class=\"dot a\"></span><span class=\"dot b2\"></span><span class=\"dot c\"></span><span class=\"title\">$2</span></div><pre>$TERM_HTML</pre></div>" +} +page() { # name width height body + printf '<!doctype html><meta charset="utf-8"><link rel="stylesheet" href="style.css"><body style="width:%spx;height:%spx">%s</body>' "$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="<div class=\"menubar\"><b>Ghostty</b><span>Shell</span><span>Edit</span><span>View</span><span>Window</span> +<div class=\"right\"><span class=\"item\"><span class=\"r\">×2</span> <span class=\"t\">✓3</span> <span class=\"y\">◐3</span></span><span>$clock</span></div></div>" +page hero 1320 500 "<div class=\"wall\" style=\"width:1320px;height:500px\">$menubar +<div style=\"position:absolute;left:30px;top:58px;width:822px\">$(termwin dark 'tmux — api')</div> +<div class=\"panel shadow\" style=\"position:absolute;right:30px;top:42px;border-radius:12px\"></div></div>" + +page panel 1000 690 "<div class=\"wall\" style=\"width:1000px;height:690px;display:flex;gap:40px;padding:44px 40px;align-items:flex-start\"> +<div><div class=\"panel shadow\"></div><div class=\"caption\">Only when agents are waiting</div> +<div class=\"panelrestore shadow\" style=\"margin-top:34px\"></div><div class=\"caption\">After a restart: restore everything</div></div> +<div><div class=\"panelfull shadow\"></div><div class=\"caption\">Always, with the full list</div></div></div>" + +page search 700 360 "<div class=\"wall\" style=\"width:700px;height:360px;padding:50px 60px\"><div class=\"search shadow\" style=\"border-radius:14px\"></div></div>" + +page statusline 900 250 "<div class=\"wall\" style=\"width:900px;height:250px;padding:22px 30px\"> +<div class=\"label\">a dark theme (Catppuccin Mocha)</div><div class=\"term shadow dark\"><pre>$TERM_TAIL</pre></div> +<div class=\"label\" style=\"margin-top:18px\">a light theme (PaperColor Light)</div><div class=\"term shadow light\"><pre>$TERM_TAIL</pre></div></div>" |
