From 4cad1009c9d07cdfdab28fe9e073ccb35b54dfd7 Mon Sep 17 00:00:00 2001 From: Igor Soarez Date: Tue, 10 Nov 2026 17:30:26 +0200 Subject: readme: lead with what dingger shows, with real renders of made-up agents; move the details to docs/reference.md; dingger status: fit the agent column to its longest name --- docs/screenshots.sh | 198 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100755 docs/screenshots.sh (limited to 'docs/screenshots.sh') 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" <"$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
" -- cgit v1.2.3