#!/usr/bin/env python3 """Regenerate the user guide's terminal screenshots from real vault runs. Every screenshot is the genuine output of the vault binary built from this checkout, run in a throwaway home directory. The cosmetic touches are: the sandbox path and file owner are shown as /home/sam and "sam sam", key lines are highlighted in amber for the reader, and "moment" frames show a prompt with a cursor, exactly as vault prints it before you type. Needs: go, python3, and Chrome or Chromium (set CHROME=/path if not found). Output: docs/guide/img/*.png """ import getpass import glob import grp import html import os import pty import shutil import subprocess import sys import tempfile import time import urllib.request import zipfile HERE = os.path.dirname(os.path.abspath(__file__)) REPO = os.path.abspath(os.path.join(HERE, "..", "..")) IMG = os.path.join(HERE, "img") TOOLS = os.path.join(HERE, ".tools") FONT_URL = "https://github.com/JetBrains/JetBrainsMono/releases/download/v2.304/JetBrainsMono-2.304.zip" SHOWN_HOME = "/home/sam" PROMPT_USER = "sam@laptop" def find_chrome(): if os.environ.get("CHROME"): return os.environ["CHROME"] for name in ("chromium", "chromium-browser", "google-chrome", "google-chrome-stable"): if shutil.which(name): return shutil.which(name) found = sorted(glob.glob(os.path.expanduser("~/.cache/ms-playwright/chromium-*/chrome-linux*/chrome"))) mac = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" if found: return found[-1] if os.path.exists(mac): return mac sys.exit("shots: no Chrome or Chromium found; set CHROME=/path/to/chrome") def font_dir(): target = os.path.join(TOOLS, "jetbrains-mono") if not os.path.exists(os.path.join(target, "JetBrainsMono-Regular.ttf")): os.makedirs(target, exist_ok=True) zpath = os.path.join(TOOLS, "jbm.zip") print("shots: fetching JetBrains Mono") urllib.request.urlretrieve(FONT_URL, zpath) with zipfile.ZipFile(zpath) as z: for n in z.namelist(): base = os.path.basename(n) if n.startswith("fonts/ttf/") and base in ("JetBrainsMono-Regular.ttf", "JetBrainsMono-Bold.ttf"): with open(os.path.join(target, base), "wb") as f: f.write(z.read(n)) os.remove(zpath) return target class Sandbox: """A fake home with vault on PATH. Commands run through bash.""" def __init__(self): self.root = tempfile.mkdtemp(prefix="vault-shots-") self.home = os.path.join(self.root, "sam") self.bin = os.path.join(self.root, "bin") os.makedirs(self.home) os.makedirs(self.bin) subprocess.run(["go", "build", "-o", os.path.join(self.bin, "vault"), "."], cwd=REPO, check=True) def env(self, extra=None): e = { "HOME": self.home, "PATH": self.bin + os.pathsep + os.environ["PATH"], "LANG": "C.UTF-8", "TZ": "Europe/London", } e.update(extra or {}) return e def run(self, command, passphrase=None, extra_env=None): env = self.env(extra_env) if passphrase is not None: env["VAULT_PASSPHRASE"] = passphrase p = subprocess.run(["bash", "-c", command], cwd=self.home, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) return self.tidy(p.stdout) def run_tty(self, command, passphrase, answer): """Run on a pseudo-terminal, typing answer at vault's [y/N] question.""" env = self.env({"VAULT_PASSPHRASE": passphrase}) pid, fd = pty.fork() if pid == 0: os.chdir(self.home) os.execve("/bin/bash", ["bash", "-c", command], env) out = b"" while b"[y/N]" not in out: out += os.read(fd, 1024) os.write(fd, answer.encode() + b"\n") try: while True: chunk = os.read(fd, 1024) if not chunk: break out += chunk except OSError: pass os.waitpid(pid, 0) return self.tidy(out.decode().replace("\r\n", "\n")) def tidy(self, text): owner = " %s %s " % (getpass.getuser(), grp.getgrgid(os.getgid()).gr_name) return text.replace(self.home, SHOWN_HOME).replace(owner, " sam sam ").rstrip("\n") def write(self, rel, content): path = os.path.join(self.home, rel) os.makedirs(os.path.dirname(path), exist_ok=True) mode = "wb" if isinstance(content, bytes) else "w" with open(path, mode) as f: f.write(content) # ---- rendering ------------------------------------------------------------- CSS = """ @font-face { font-family: JBM; src: url('file://%(fonts)s/JetBrainsMono-Regular.ttf'); } @font-face { font-family: JBM; font-weight: bold; src: url('file://%(fonts)s/JetBrainsMono-Bold.ttf'); } html, body { margin: 0; background: transparent; } body { padding: 22px 48px 64px; display: flex; gap: 28px; align-items: flex-start; } .win { width: %(width)dpx; border-radius: 12px; overflow: hidden; background: #0d1326; box-shadow: 0 18px 40px rgba(6, 10, 30, .45), 0 0 0 1px rgba(255,255,255,.06); } .bar { height: 34px; background: linear-gradient(#1b2340, #151c35); display: flex; align-items: center; padding: 0 14px; position: relative; } .dot { width: 12px; height: 12px; border-radius: 50%%; margin-right: 8px; } .r { background: #ff5f57; } .y { background: #febc2e; } .g { background: #28c840; } .title { position: absolute; left: 0; right: 0; text-align: center; color: #8b95b7; font: 12.5px JBM, monospace; pointer-events: none; } pre { margin: 0; padding: 16px 20px 18px; font: 14px/21px JBM, monospace; color: #d6ddf2; white-space: pre; } .u { color: #3ee0c5; font-weight: bold; } .p { color: #7aa2ff; font-weight: bold; } .c { color: #ffffff; font-weight: bold; } .dim { color: #6f7a9c; } .cur { background: #3ee0c5; color: #0d1326; } .hl { color: #ffcc66; } """ def prompt(cwd="~"): return ('%s %s $ ' % (PROMPT_USER, html.escape(cwd))) def window(lines, title, width): body = "\n".join(lines) return ('
%s
',
"style.css": "body{background:url(/img/bg.png)}",
"img/me.png": "png", "img/bg.png": "png"}.items():
os.makedirs(os.path.dirname(os.path.join(site, rel)), exist_ok=True)
with open(os.path.join(site, rel), "w") as f:
f.write(body)
srv = subprocess.Popen([sys.executable, "-m", "http.server", "8000", "--bind", "127.0.0.1"],
cwd=site, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(1)
t = Term(box)
t.cmd("vault mirror http://localhost:8000/ sites", P)
t.cmd("vault get sites/localhost:8000/about.html && cat about.html", P)
shoot("mirror", t)
srv.terminate()
# errors
t = Term(box)
t.cmd("vault list", "hunter2", highlight=("incorrect",))
t.cmd("vault get does-not-exist.txt", P, highlight=("not in",))
t.cmd("vault add notes.md ../escape", P, highlight=("invalid",))
t.cmd("vault list --vault ~/Desktop", P, highlight=("not a vault",))
t.cmd("vault init", highlight=("already",))
shoot("errors", t)
# locking: start a big add, then a second writer while it runs
box.write("big-video.mkv", os.urandom(600 << 20))
first = Term(box, title="terminal 1", width=600)
second = Term(box, title="terminal 2", width=600)
env = box.env({"VAULT_PASSPHRASE": P})
a = subprocess.Popen(["bash", "-c", "vault add big-video.mkv"], cwd=box.home, env=env,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
time.sleep(1.5)
second.cmd("vault add recipes/curry.txt", P, highlight=("waiting",))
first.lines.append(prompt() + 'vault add big-video.mkv')
first.lines += [html.escape(box.tidy(l)) for l in a.communicate()[0].rstrip("\n").split("\n")]
shoot("locking", first, second)
os.remove(os.path.join(box.home, "big-video.mkv"))
# automation
t = Term(box)
t.cmd("export VAULT_PASSPHRASE='correct horse battery staple'", shown="export VAULT_PASSPHRASE=…")
t.cmd("vault list ideas", P)
t.cmd("vault get nope 2>/dev/null; echo \"exit code: $?\"", P)
shoot("scripting", t)
# cp, folder mv, get -r
t = Term(box)
t.cmd("vault cp -r letters archive/2025", P)
t.cmd("vault mv ideas/notes.md archive/", P)
t.cmd("vault ls archive", P)
t.cmd("vault get -r archive -o restored/archive", P)
shoot("cp-mv", t)
# unlock: one passphrase, then commands stop asking until lock
t = Term(box)
t.cmd("vault unlock", P)
t.cmd("vault ls")
t.cmd("vault get archive/notes.md -o /tmp/notes.md")
t.cmd("vault lock")
t.moment("vault ls", "Enter passphrase:")
shoot("unlock", t)
# building, into a temporary OUT so an existing dist/ is left alone
rel = tempfile.mkdtemp(prefix="vault-release-")
targets = "linux/amd64 linux/arm darwin/arm64 windows/amd64"
t = Term(box, title="sam@laptop: ~/src/vault")
out = subprocess.run(["./build.sh"], cwd=REPO, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, env=dict(os.environ, TARGETS=targets, OUT=rel)).stdout
sums = subprocess.run(["sha256sum", "-c", "SHA256SUMS"], cwd=rel,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True).stdout
t.lines.append(prompt("~/src/vault") + 'export TARGETS="%s"' % targets)
t.lines.append(prompt("~/src/vault") + 'OUT=/tmp/release ./build.sh')
t.lines += [html.escape(l.replace(rel, "/tmp/release")) for l in out.rstrip("\n").split("\n")]
t.lines.append(prompt("~/src/vault") + 'cd /tmp/release && sha256sum -c SHA256SUMS')
t.lines += [html.escape(l) for l in sums.rstrip("\n").split("\n")]
shoot("build", t)
shutil.rmtree(rel, ignore_errors=True)
shutil.rmtree(box.root, ignore_errors=True)
if __name__ == "__main__":
main()