aboutsummaryrefslogtreecommitdiff
path: root/docs/screenshots.sh
blob: ffe7291de257cdfe336be2fe04e2e653702d9ef9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
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>"