aboutsummaryrefslogtreecommitdiff
path: root/docs/screenshots.sh
diff options
context:
space:
mode:
Diffstat (limited to 'docs/screenshots.sh')
-rwxr-xr-xdocs/screenshots.sh198
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>"