#!/usr/bin/env bash # Capture README screenshots against a fixture vault. # # Restarts the Omarchy shell with demo/bin ahead of it on PATH, so the plugin # resolves `bw` to the shim and shows made-up data. Your real vault is never # read, and the shell is restored on the way out -- including if this script # is interrupted. # # ./demo/capture.sh [output-dir] (default: docs/screenshots) set -euo pipefail REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" OUT="${1:-$REPO/docs/screenshots}" IPC=(qs -p /usr/share/omarchy/shell/shell.qml ipc call io.github.elevate08.qs-bitwarden-cli) for tool in grim magick wtype hyprctl quickshell /usr/bin/python3; do command -v "$tool" >/dev/null || { echo "missing required tool: $tool" >&2; exit 1; } done mkdir -p "$OUT" restore() { echo "restoring the real shell..." # The fixture vault's SSH key gets projected to the same directory the real # one uses. The real shell rewrites its own keys on the next load but will # not remove a file it never wrote, so a demo key would sit there for good. rm -f "${XDG_DATA_HOME:-$HOME/.local/share}/qs-bitwarden-cli/ssh/Demo Deploy Key.pub" pkill -f "quickshell -n -p /usr/share/omarchy/shell" 2>/dev/null || true sleep 1 omarchy restart shell >/dev/null 2>&1 || true } trap restore EXIT INT TERM # start_shell # # The fixture shell is restarted per vault state rather than driven between # them: the panel reads `bw status` once on open, so the logged-out screen # cannot be reached from a running unlocked instance. start_shell() { echo "starting shell with the fixture vault ($1)..." pkill -f "quickshell -n -p /usr/share/omarchy/shell" 2>/dev/null || true sleep 1 PATH="$REPO/demo/bin:$PATH" QSBW_DEMO_STATUS="$1" \ nohup quickshell -n -p /usr/share/omarchy/shell >/dev/null 2>&1 & sleep 6 # Park the pointer so hover states and tooltips stay out of the shots. hyprctl dispatch movecursor 100 100 >/dev/null 2>&1 || true } # Crop to the panel itself rather than a fixed box. The panel resizes with its # content, and -- more importantly -- a loose crop would put whatever is behind # it (windows, filenames, terminal scrollback) into the published image. shot() { # shot sleep 1 grim "$OUT/.raw.png" local box err # Keep the locator's own reason. Swallowing it turned a theme change into # six identical "could not locate the panel border" lines and no clue why. if box="$(/usr/bin/python3 "$REPO/demo/find_panel.py" "$OUT/.raw.png" 2>/tmp/find_panel.err)"; then magick "$OUT/.raw.png" -crop "$box" +repage "$OUT/$1.png" echo " wrote $1.png ($box)" else err="$(cat /tmp/find_panel.err)" echo " SKIPPED $1: ${err:-could not locate the panel border}" >&2 fi rm -f /tmp/find_panel.err if [ -n "${QSBW_KEEP_RAW:-}" ]; then mv -f "$OUT/.raw.png" "$OUT/.raw-$1.png" 2>/dev/null || true else rm -f "$OUT/.raw.png"; fi } # open_detail # # Narrows the list to one item and opens it. Typing the name is steadier than # counting Down presses: the list is sorted favourites-first and then by name, # so an added fixture would silently shift every offset. # # The Down is what hands focus back from the search field to the key catcher -- # with one result it clamps to the only row -- and `e` on the list opens the # detail. (`e` again, on the detail, opens the form.) open_detail() { wtype "/" 2>/dev/null; sleep 1 wtype "$1" 2>/dev/null; sleep 2 wtype -k Down 2>/dev/null; sleep 1 wtype "e" 2>/dev/null; sleep 2 } # Back to an empty list from wherever open_detail left us. clear_search() { wtype -k Escape 2>/dev/null; sleep 1 wtype "/" 2>/dev/null; sleep 1 wtype -M ctrl -k a -m ctrl 2>/dev/null wtype -k BackSpace 2>/dev/null; sleep 1 wtype -k Down 2>/dev/null; sleep 1 } # --- SSH signing approval --------------------------------------------------- # # The approval screen exists only while a real signing request is waiting, so # it cannot be navigated to -- it has to be raised. `ssh-add -T` asks the agent # to sign one challenge with one key and nothing else, which is the smallest # request that produces this prompt. # # Everything here is the fixture vault: the key is the throwaway pair in # fixtures.json, and the socket belongs to the fixture shell started above. # The request is denied rather than approved, so no signature is ever made. capture_ssh_approval() { local sock="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/qs-bitwarden-cli/ssh-agent.sock" local pub="${XDG_DATA_HOME:-$HOME/.local/share}/qs-bitwarden-cli/ssh/Demo Deploy Key.pub" # The helper starts with the panel and projects its public keys after the # vault loads, so wait for the file rather than guessing at a delay. local waited=0 while [ ! -S "$sock" ] || [ ! -f "$pub" ]; do sleep 1; waited=$((waited + 1)) if [ "$waited" -ge 30 ]; then echo " skipped 13-ssh-approval: no agent socket or projected key after ${waited}s" >&2 echo " (is 'Act as your SSH agent' enabled in shell.json?)" >&2 return 0 fi done "${IPC[@]}" open >/dev/null 2>&1; sleep 2 # In the background: it blocks until the prompt is answered, which is the # point -- the prompt has to still be on screen when the shot is taken. SSH_AUTH_SOCK="$sock" ssh-add -T "$pub" >/dev/null 2>&1 & local asker=$! sleep "${QSBW_SSH_SETTLE:-4}" shot 13-ssh-approval # Deny it. Escape is the approval screen's own deny, so nothing is signed. wtype -k Escape 2>/dev/null; sleep 1 wait "$asker" 2>/dev/null || true "${IPC[@]}" close >/dev/null 2>&1 || true } # QSBW_ONLY_SSH=1 captures the approval shot alone. It is the only shot that # depends on timing rather than on a keystroke, so it is the one that gets # iterated on, and re-running the whole sequence to retake it costs a minute # and five shells. if [ -n "${QSBW_ONLY_SSH:-}" ]; then start_shell unlocked capture_ssh_approval echo "done -> $OUT" exit 0 fi # --- logged out ------------------------------------------------------------- # No setup shot. The wizard appears only while a required tool is missing, and # it watches for the install and moves on by itself the moment one lands. The # fixture environment has every dependency -- that is what makes the rest of # these shots work -- so the screen correctly advances straight past itself. # Photographing it means deliberately hiding `jq` or `bw` from the shell's # PATH, which breaks the vault reads every other shot depends on. start_shell unauthenticated "${IPC[@]}" open >/dev/null 2>&1; sleep 4 shot 12-login "${IPC[@]}" close >/dev/null 2>&1 || true # --- locked ----------------------------------------------------------------- start_shell locked "${IPC[@]}" open >/dev/null 2>&1; sleep 4 shot 11-locked "${IPC[@]}" close >/dev/null 2>&1 || true # --- unlocked, populated vault ---------------------------------------------- start_shell unlocked "${IPC[@]}" open >/dev/null 2>&1; sleep 4 shot 01-vault-list # One of each item type the panel draws differently. A login has credentials # and a TOTP; a card and an identity have the field blocks added in 1.7.0, and # an identity is the one that shows the address as a single copyable block. open_detail "GitHub" shot 02-login-detail wtype "e" 2>/dev/null; sleep 2 shot 03-edit-item wtype -k Escape 2>/dev/null; sleep 1 clear_search open_detail "Demo Card" shot 04-card-detail clear_search open_detail "Dana Demo" shot 05-identity-detail clear_search wtype "f" 2>/dev/null; sleep 2 shot 06-folder-drawer wtype -k Escape 2>/dev/null; sleep 1 wtype "t" 2>/dev/null; sleep 2 shot 07-type-filter wtype -k Escape 2>/dev/null; sleep 1 wtype "g" 2>/dev/null; sleep 5 shot 08-generator wtype -k Escape 2>/dev/null; sleep 1 wtype -M alt -k s -m alt 2>/dev/null; sleep 3 shot 09-sends wtype -k Escape 2>/dev/null; sleep 1 wtype -M alt -k comma -m alt 2>/dev/null; sleep 3 shot 10-settings "${IPC[@]}" close >/dev/null 2>&1 || true capture_ssh_approval echo "done -> $OUT"