import json
import os
import platform
import re
import shutil
import subprocess
import urllib.request
from dataclasses import dataclass
from datetime import datetime, timedelta
from pathlib import Path
from .themes import THEMES, format_theme_for_vhs
REAL_HOME = Path.home()
FIXTURES_DIR = Path(__file__).parent / "fixtures"
DEPS_DIR = Path(__file__).parent.parent / ".deps"
_GCS_BUCKET = "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases"
_ZELLIJ_PLUGIN_URL = "https://github.com/Cynary/zellij-tab-name/releases/download/v0.4.1/zellij-tab-name.wasm"
_VHS_FORK_REPO = "https://github.com/max-sixty/vhs.git"
_VHS_FORK_BRANCH = "keypress-overlay"
def _detect_platform() -> str:
system = platform.system().lower()
machine = platform.machine().lower()
if system == "darwin":
os_name = "darwin"
elif system == "linux":
os_name = "linux"
else:
raise RuntimeError(f"Unsupported OS: {system}")
if machine in ("x86_64", "amd64"):
arch = "x64"
elif machine in ("arm64", "aarch64"):
arch = "arm64"
else:
raise RuntimeError(f"Unsupported architecture: {machine}")
if os_name == "linux" and arch == "x64":
try:
result = subprocess.run(
["ldd", "--version"], capture_output=True, text=True, check=False
)
if "musl" in result.stderr.lower() or "musl" in result.stdout.lower():
return "linux-x64-musl"
except FileNotFoundError:
pass
return f"{os_name}-{arch}"
def _download_file(url: str, dest: Path) -> None:
dest.parent.mkdir(parents=True, exist_ok=True)
print(f"Downloading {dest.name}...")
temp = dest.with_suffix(f".{os.getpid()}.tmp")
try:
urllib.request.urlretrieve(url, temp)
if not dest.exists():
temp.rename(dest)
finally:
temp.unlink(missing_ok=True)
def _ensure_claude_binary() -> Path:
claude_binary = DEPS_DIR / "claude"
if claude_binary.exists():
return claude_binary
plat = _detect_platform()
print(f"Fetching Claude Code for {plat}...")
with urllib.request.urlopen(f"{_GCS_BUCKET}/stable") as resp:
version = resp.read().decode().strip()
print(f"Claude Code version: {version}")
_download_file(f"{_GCS_BUCKET}/{version}/{plat}/claude", claude_binary)
claude_binary.chmod(0o755)
result = subprocess.run(
[str(claude_binary), "--version"], capture_output=True, text=True, check=False
)
if result.returncode != 0:
raise RuntimeError(
f"Claude binary downloaded but --version failed: {result.stderr or result.stdout}"
)
print(f"✓ Claude Code {version} ready")
return claude_binary
def _ensure_zellij_plugin() -> Path:
plugin_path = DEPS_DIR / "zellij-tab-name.wasm"
if plugin_path.exists():
return plugin_path
_download_file(_ZELLIJ_PLUGIN_URL, plugin_path)
print(f"✓ Zellij plugin ready")
return plugin_path
def ensure_vhs_binary() -> Path:
vhs_dir = DEPS_DIR / "vhs"
vhs_binary = vhs_dir / "vhs"
if vhs_binary.exists():
return vhs_binary
if not shutil.which("go"):
raise RuntimeError(
"Go is required to build VHS. Install from https://go.dev/dl/"
)
if not vhs_dir.exists():
print(f"Cloning VHS fork...")
DEPS_DIR.mkdir(parents=True, exist_ok=True)
result = subprocess.run(
["git", "clone", "-b", _VHS_FORK_BRANCH, "--depth=1", _VHS_FORK_REPO, str(vhs_dir)],
capture_output=True,
text=True,
)
if result.returncode != 0:
raise RuntimeError(f"Failed to clone VHS fork: {result.stderr}")
print(f"Building VHS...")
result = subprocess.run(
["go", "build", "-o", "vhs", "."],
cwd=vhs_dir,
capture_output=True,
text=True,
)
if result.returncode != 0:
raise RuntimeError(f"Failed to build VHS: {result.stderr}")
result = subprocess.run(
[str(vhs_binary), "--version"],
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
raise RuntimeError(f"VHS built but --version failed: {result.stderr}")
print(f"✓ VHS ready")
return vhs_binary
VALIDATION_RS = """//! Input validation utilities.
/// Validates that a number is positive.
pub fn is_positive(n: i32) -> bool {
n > 0
}
/// Validates that a string is not empty.
pub fn is_non_empty(s: &str) -> bool {
!s.trim().is_empty()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_positive() {
assert!(is_positive(1));
assert!(!is_positive(0));
assert!(!is_positive(-1));
}
}
"""
@dataclass
class DemoEnv:
name: str
out_dir: Path
repo_name: str = "worktrunk"
@property
def root(self) -> Path:
return self.out_dir / f".demo-{self.name}"
@property
def home(self) -> Path:
return self.root
@property
def work_base(self) -> Path:
return self.home / "w"
@property
def repo(self) -> Path:
return self.work_base / self.repo_name
@property
def bare_remote(self) -> Path:
return self.root / "remote.git"
def run(cmd, cwd=None, env=None, check=True, capture=False):
result = subprocess.run(
cmd, cwd=cwd, env=env, check=check, capture_output=capture, text=True
)
return result.stdout if capture else None
def git(args, cwd=None, env=None):
run(["git"] + args, cwd=cwd, env=env)
def render_tape(template_path: Path, replacements: dict, repo_root: Path) -> str | None:
if not template_path.exists():
print(f"Warning: {template_path} not found, skipping VHS recording")
return None
rendered = template_path.read_text()
def inline_source(match):
source_path = repo_root / match.group(1).strip().strip('"')
return source_path.read_text()
rendered = re.sub(r"^Source\s+(.+)$", inline_source, rendered, flags=re.MULTILINE)
for key, value in replacements.items():
rendered = rendered.replace(f"{{{{{key}}}}}", str(value))
return rendered
def record_vhs(
tape_path: Path, vhs_binary: str = "vhs", expected_output: Path = None
):
run([vhs_binary, str(tape_path)], check=True)
if expected_output and not expected_output.exists():
raise RuntimeError(
f"VHS exited 0 but {expected_output.name} was not created. "
"Check ffmpeg output above — likely missing libass support."
)
def build_wt(repo_root: Path):
print("Building wt binary...")
run(["cargo", "build", "--quiet"], cwd=repo_root)
def commit_dated(repo: Path, message: str, offset: str, env_extra: dict = None):
now = datetime.now()
if offset.endswith("d"):
delta = timedelta(days=int(offset[:-1]))
elif offset.endswith("H"):
delta = timedelta(hours=int(offset[:-1]))
else:
raise ValueError(f"Unknown offset format: {offset}")
date_str = (now - delta).strftime("%Y-%m-%dT%H:%M:%S")
env = os.environ.copy()
env["GIT_AUTHOR_DATE"] = date_str
env["GIT_COMMITTER_DATE"] = date_str
env["SKIP_DEMO_HOOK"] = "1"
if env_extra:
env.update(env_extra)
git(["-C", str(repo), "commit", "-qm", message], env=env)
def prepare_base_repo(env: DemoEnv, repo_root: Path):
shutil.rmtree(env.root, ignore_errors=True)
env.root.mkdir(parents=True, exist_ok=True)
env.work_base.mkdir(parents=True, exist_ok=True)
env.repo.mkdir(parents=True, exist_ok=True)
run(["git", "init", "--bare", "-q", str(env.bare_remote)])
git(["-C", str(env.repo), "init", "-q"])
git(["-C", str(env.repo), "config", "user.name", "Worktrunk Demo"])
git(["-C", str(env.repo), "config", "user.email", "demo@example.com"])
git(["-C", str(env.repo), "config", "commit.gpgsign", "false"])
git(["-C", str(env.repo), "config", "worktrunk.hints.worktree-path", "true"])
(env.repo / "README.md").write_text("# Acme App\n\nA demo application.\n")
git(["-C", str(env.repo), "add", "README.md"])
commit_dated(env.repo, "Initial commit", "7d")
git(["-C", str(env.repo), "branch", "-m", "main"])
git(["-C", str(env.repo), "remote", "add", "origin", str(env.bare_remote)])
git(["-C", str(env.repo), "push", "-u", "origin", "main", "-q"])
(env.repo / "Cargo.toml").write_text(
'[package]\nname = "acme"\nversion = "0.1.0"\nedition = "2021"\n\n[workspace]\n'
)
(env.repo / "src").mkdir()
shutil.copy(FIXTURES_DIR / "lib.rs", env.repo / "src" / "lib.rs")
(env.repo / ".gitignore").write_text("/target\n")
git(["-C", str(env.repo), "add", ".gitignore", "Cargo.toml", "src/"])
commit_dated(env.repo, "Add Rust project with tests", "6d")
run(["cargo", "build", "--release", "-q"], cwd=env.repo, check=False)
git(["-C", str(env.repo), "add", "Cargo.lock"])
commit_dated(env.repo, "Add Cargo.lock", "6d")
git(["-C", str(env.repo), "push", "-q"])
bin_dir = env.home / ".local" / "bin"
bin_dir.mkdir(parents=True, exist_ok=True)
bat_wrapper = bin_dir / "cat"
bat_wrapper.write_text("""#!/bin/bash
# Use bat for syntax highlighting if file is toml
if [[ "$1" == *.toml ]]; then
exec bat --style=plain --paging=never "$@"
else
exec /bin/cat "$@"
fi
""")
bat_wrapper.chmod(0o755)
build_wt(repo_root)
wt_bin = repo_root / "target" / "debug" / "wt"
install_env = os.environ.copy()
install_env["HOME"] = str(env.home)
run([str(wt_bin), "config", "shell", "install", "fish", "--yes"], env=install_env)
fish_config_dir = env.home / ".config" / "fish"
fish_config_dir.mkdir(parents=True, exist_ok=True)
color_vars = {
"fish_color_autosuggestion": "brblack",
"fish_color_command": "yellow", "fish_color_comment": "brblack",
"fish_color_end": "normal",
"fish_color_error": "red",
"fish_color_escape": "cyan",
"fish_color_keyword": "yellow",
"fish_color_normal": "normal",
"fish_color_operator": "normal",
"fish_color_param": "normal",
"fish_color_quote": "green",
"fish_color_redirection": "normal",
}
lines = [
"# This file contains fish universal variable definitions.",
"# VERSION: 3.0",
]
lines.extend(f"SETUVAR {k}:{v}" for k, v in color_vars.items())
lines.extend([
"SETUVAR fish_color_cancel:\\x2d\\x2dreverse",
"SETUVAR fish_color_search_match:\\x2d\\x2dbackground\\x3d111",
"SETUVAR fish_color_selection:\\x2d\\x2dbackground\\x3dbrblack",
"SETUVAR fish_color_valid_path:\\x2d\\x2dunderline",
"SETUVAR fish_greeting:",
])
(fish_config_dir / "fish_variables").write_text("\n".join(lines) + "\n")
color_lines = [f"set -g {k} {v}" for k, v in color_vars.items()]
(fish_config_dir / "colors.fish").write_text("\n".join(color_lines) + "\n")
config_dir = env.home / ".config" / "worktrunk"
config_dir.mkdir(parents=True)
(env.repo / ".config").mkdir(exist_ok=True)
def setup_claude_code_config(
env: DemoEnv,
worktree_paths: list[str],
allowed_tools: list[str] = None,
) -> None:
api_key_suffix = (
os.environ.get("ANTHROPIC_API_KEY", "")[-20:]
if os.environ.get("ANTHROPIC_API_KEY")
else ""
)
projects_config = {}
for path in worktree_paths:
resolved_path = str(Path(path).resolve())
projects_config[resolved_path] = {
"allowedTools": [],
"hasTrustDialogAccepted": True,
}
claude_json = env.home / ".claude.json"
claude_json.write_text(
json.dumps(
{
"numStartups": 100,
"installMethod": "global",
"theme": "light",
"firstStartTime": "2025-01-01T00:00:00.000Z",
"hasCompletedOnboarding": True,
"hasCompletedClaudeInChromeOnboarding": True,
"claudeInChromeDefaultEnabled": False,
"sonnet45MigrationComplete": True,
"opus45MigrationComplete": True,
"thinkingMigrationComplete": True,
"hasShownOpus45Notice": {},
"hasShownOpus46Notice": {},
"opusProMigrationComplete": True,
"opus46FeedSeenCount": 100,
"sonnet1m45MigrationComplete": True,
"lastReleaseNotesSeen": "99.0.0",
"lastOnboardingVersion": "99.0.0",
"oauthAccount": {
"displayName": "wt",
"emailAddress": "demo@example.com",
},
"customApiKeyResponses": {
"approved": [api_key_suffix] if api_key_suffix else [],
"rejected": [],
},
"officialMarketplaceAutoInstalled": True,
"effortCalloutDismissed": True,
"lspRecommendationDisabled": True,
"tipsHistory": {
"new-user-warmup": 100,
"terminal-setup": 100,
"theme-command": 100,
"fast-mode-2026-02-01": 100,
"adaptive-thinking-2026-01-28": 100,
"prompt-caching-scope-2026-01-05": 100,
"plan-mode-for-complex-tasks": 100,
"memory-command": 100,
"todo-list": 100,
"stickers-command": 100,
"status-line": 100,
"custom-commands": 100,
"custom-agents": 100,
"permissions": 100,
"git-worktrees": 100,
},
"projects": projects_config,
},
indent=2,
)
)
local_bin = env.home / ".local" / "bin"
local_bin.mkdir(parents=True, exist_ok=True)
claude_binary = _ensure_claude_binary()
shutil.copy(claude_binary, local_bin / "claude")
claude_dir = env.home / ".claude"
claude_dir.mkdir(exist_ok=True)
settings = {
"permissions": {"allow": allowed_tools or [], "deny": [], "ask": []},
"model": "claude-opus-4-6",
"statusLine": {
"type": "command",
"command": "wt list statusline --format=claude-code",
},
}
(claude_dir / "settings.json").write_text(json.dumps(settings, indent=2))
def setup_zellij_config(env: DemoEnv, default_cwd: str = None) -> None:
zellij_config_dir = env.home / ".config" / "zellij"
zellij_config_dir.mkdir(parents=True, exist_ok=True)
zellij_plugins_dir = zellij_config_dir / "plugins"
zellij_plugins_dir.mkdir(exist_ok=True)
plugin_path = _ensure_zellij_plugin()
shutil.copy(plugin_path, zellij_plugins_dir / "zellij-tab-name.wasm")
default_cwd_line = f'default_cwd "{default_cwd}"' if default_cwd else ""
plugin_dest = zellij_plugins_dir / "zellij-tab-name.wasm"
if platform.system() == "Darwin":
zellij_cache_dir = env.home / "Library" / "Caches" / "org.Zellij-Contributors.Zellij"
else:
zellij_cache_dir = env.home / ".cache" / "zellij"
zellij_cache_dir.mkdir(parents=True, exist_ok=True)
permissions_file = zellij_cache_dir / "permissions.kdl"
permissions_file.write_text(f'''"{plugin_dest}" {{
ReadApplicationState
ChangeApplicationState
}}
''')
zellij_config = zellij_config_dir / "config.kdl"
zellij_config.write_text(f"""// Demo Zellij config
default_shell "fish"
{default_cwd_line}
pane_frames false
show_startup_tips false
show_release_notes false
theme "warm-gold"
// Load the tab-name plugin
load_plugins {{
"file:{zellij_plugins_dir}/zellij-tab-name.wasm"
}}
// Warm gold theme to match the demo aesthetic
themes {{
warm-gold {{
fg "#1f2328"
bg "#FFFDF8"
black "#f5f0e8"
red "#d73a49"
green "#22863a"
yellow "#d29922"
blue "#0969da"
magenta "#8250df"
cyan "#1b7c83"
white "#57534e"
orange "#d97706"
}}
}}
keybinds clear-defaults=true {{
normal {{
bind "Ctrl Space" {{ SwitchToMode "tmux"; }}
}}
tmux {{
bind "o" {{ SwitchToMode "pane"; }}
bind "p" {{ SwitchToMode "pane"; }}
bind "t" {{ SwitchToMode "tab"; }}
bind "q" {{ Quit; }}
}}
tab {{
bind "n" {{ NewTab; SwitchToMode "Normal"; }}
bind "h" "Left" {{ GoToPreviousTab; SwitchToMode "Normal"; }}
bind "l" "Right" {{ GoToNextTab; SwitchToMode "Normal"; }}
bind "1" {{ GoToTab 1; SwitchToMode "Normal"; }}
bind "2" {{ GoToTab 2; SwitchToMode "Normal"; }}
bind "3" {{ GoToTab 3; SwitchToMode "Normal"; }}
bind "4" {{ GoToTab 4; SwitchToMode "Normal"; }}
}}
pane {{
bind "n" {{ NewPane; SwitchToMode "Normal"; }}
}}
shared_except "locked" {{
bind "Ctrl t" {{ NewTab; }}
bind "Ctrl n" {{ NewPane; }}
}}
shared_except "normal" {{
bind "Ctrl Space" "Ctrl c" {{ SwitchToMode "normal"; }}
bind "Esc" {{ SwitchToMode "normal"; }}
}}
}}
""")
def setup_fish_config(env: DemoEnv, wsl_create: bool = False) -> None:
fish_config_dir = env.home / ".config" / "fish"
fish_config_dir.mkdir(parents=True, exist_ok=True)
wsl_cmd = (
"wt switch --execute=claude --create"
if wsl_create
else "wt switch --execute=claude"
)
fish_config = fish_config_dir / "config.fish"
fish_config.write_text(f"""# Demo fish config
# wsl abbreviation: switch to worktree and launch Claude
abbr --add wsl '{wsl_cmd}'
starship init fish | source
# Pre-load wt completions (VHS doesn't trigger lazy loading reliably)
source ~/.config/fish/completions/wt.fish 2>/dev/null
# Disable cursor blinking for VHS recording
set fish_cursor_default block
# Send escape sequences to disable cursor blink
printf '\\e[?12l' # Disable cursor blink mode
printf '\\e[2 q' # Set steady block cursor (non-blinking)
# Auto-rename Zellij tabs based on git branch (for demo)
function __zellij_tab_rename --on-variable PWD
if set -q ZELLIJ
# Get git branch name, fallback to directory basename
set -l branch (git rev-parse --abbrev-ref HEAD 2>/dev/null)
if test -n "$branch"
zellij action rename-tab $branch
end
end
end
""")
def setup_mock_clis(env: DemoEnv) -> None:
bin_dir = env.home / ".local" / "bin"
bin_dir.mkdir(parents=True, exist_ok=True)
npm_mock = bin_dir / "npm"
npm_mock.write_text("""#!/bin/bash
if [[ "$1" == "install" ]]; then
echo "added 847 packages in 3.2s"
elif [[ "$1" == "run" && "$2" == "build" ]]; then
echo "vite v5.4.2 building for production..."
echo "✓ 142 modules transformed"
echo "dist/index.js 45.2 kB │ gzip: 14.8 kB"
elif [[ "$1" == "run" && "$2" == "dev" ]]; then
# Extract port from args if provided (e.g., npm run dev -- --port 3001)
port=3000
for arg in "$@"; do
if [[ "$prev" == "--port" ]]; then
port="$arg"
fi
prev="$arg"
done
echo ""
echo " VITE v5.4.2 ready in 342 ms"
echo ""
echo " ➜ Local: http://localhost:$port/"
echo " ➜ Network: http://192.168.1.42:$port/"
fi
""")
npm_mock.chmod(0o755)
docker_mock = bin_dir / "docker"
docker_mock.write_text("""#!/bin/bash
if [[ "$1" == "compose" && "$2" == "up" ]]; then
echo "[+] Running 1/1"
echo " ✔ Container postgres Started"
fi
""")
docker_mock.chmod(0o755)
flyctl_mock = bin_dir / "flyctl"
flyctl_mock.write_text("""#!/bin/bash
if [[ "$1" == "scale" ]]; then
echo "Scaling app to 0 machines"
fi
""")
flyctl_mock.chmod(0o755)
llm_mock = bin_dir / "llm"
llm_mock.write_text(r"""input=$(cat)
if echo "$input" | grep -qi "summary"; then
if echo "$input" | grep -q "utils\.rs"; then
echo "Add utility functions module with string and math helpers"
elif echo "$input" | grep -q "notes\.txt"; then
echo "Add TODO notes for caching improvements"
elif echo "$input" | grep -q "multiply\|subtract\|math"; then
echo "Add math operations and consolidate tests"
elif echo "$input" | grep -q "User settings"; then
echo "Add user settings module placeholder"
else
echo "Expand README with contributing and license sections"
fi
else
sleep 0.5
echo "feat: add user settings module"
echo ""
echo "Add placeholder module for user profile settings."
fi
""")
llm_mock.chmod(0o755)
cargo_mock = bin_dir / "cargo"
cargo_mock.write_text(r"""if [[ "$1" == "nextest" && "$2" == "run" ]]; then
sleep 0.3
echo " Finished \`test\` profile [unoptimized + debuginfo] target(s) in 0.02s"
echo " Starting 2 tests across 1 binary"
echo " PASS [ 0.001s] acme::tests::test_add"
echo " PASS [ 0.001s] acme::tests::test_add_zeros"
echo "------------"
echo " Summary [ 0.002s] 2 tests run: 2 passed, 0 skipped"
fi
""")
cargo_mock.chmod(0o755)
def prepare_demo_repo(env: DemoEnv, repo_root: Path, hooks_config: str = None):
prepare_base_repo(env, repo_root)
setup_mock_clis(env)
if hooks_config is None:
hooks_config = '[pre-merge]\ntest = "cargo nextest run"\n'
(env.repo / ".config" / "wt.toml").write_text(hooks_config)
claude_md_dir = env.repo / ".claude"
claude_md_dir.mkdir(exist_ok=True)
(claude_md_dir / "CLAUDE.md").write_text("# Acme App\n\nRust project. Run `cargo test` for tests.\n")
git(["-C", str(env.repo), "add", ".config/wt.toml", ".claude/CLAUDE.md"])
commit_dated(env.repo, "Add project hooks", "5d")
git(["-C", str(env.repo), "push", "-q"])
bin_dir = env.home / ".local" / "bin"
gh_mock = bin_dir / "gh"
shutil.copy(FIXTURES_DIR / "gh-mock.sh", gh_mock)
gh_mock.chmod(0o755)
git(["-C", str(env.repo), "branch", "docs/readme"])
git(["-C", str(env.repo), "branch", "spike/search"])
_create_branch_beta(env)
readme = env.repo / "README.md"
readme.write_text(
readme.read_text() + "\n## Development\n\nSee CONTRIBUTING.md for guidelines.\n"
)
(env.repo / "notes.md").write_text("# Notes\n")
git(["-C", str(env.repo), "add", "README.md", "notes.md"])
commit_dated(env.repo, "docs: add development section", "1d")
git(["-C", str(env.repo), "push", "-q"])
_create_branch_alpha(env)
_create_branch_hooks(env)
def _create_branch_alpha(env: DemoEnv):
branch = "alpha"
path = env.work_base / f"acme.{branch}"
git(["-C", str(env.repo), "checkout", "-q", "-b", branch, "main"])
(env.repo / "README.md").write_text("""# Acme App
A demo application for showcasing worktrunk features.
## Features
- Fast worktree switching
- Integrated merge workflow
- Pre-merge test hooks
- LLM commit messages
## Getting Started
Run `wt list` to see all worktrees.
""")
git(["-C", str(env.repo), "add", "README.md"])
commit_dated(env.repo, "docs: expand README", "3d")
readme = env.repo / "README.md"
readme.write_text(readme.read_text() + "\n## Contributing\n\nPRs welcome!\n")
git(["-C", str(env.repo), "add", "README.md"])
commit_dated(env.repo, "docs: add contributing section", "3d")
readme.write_text(readme.read_text() + "\n## License\n\nMIT\n")
git(["-C", str(env.repo), "add", "README.md"])
commit_dated(env.repo, "docs: add license", "3d")
shutil.copy(FIXTURES_DIR / "alpha-utils.rs", env.repo / "src" / "utils.rs")
lib_rs = env.repo / "src" / "lib.rs"
lib_content = lib_rs.read_text()
lib_rs.write_text("pub mod utils;\n\n" + lib_content)
git(["-C", str(env.repo), "add", "src/utils.rs", "src/lib.rs"])
commit_dated(env.repo, "feat: add utility functions module", "3d")
git(["-C", str(env.repo), "push", "-u", "origin", branch, "-q"])
git(["-C", str(env.repo), "checkout", "-q", "main"])
git(["-C", str(env.repo), "worktree", "add", "-q", str(path), branch])
readme = path / "README.md"
readme.write_text(readme.read_text() + "## FAQ\n\n")
git(["-C", str(path), "add", "README.md"])
commit_dated(path, "docs: add FAQ section", "3d")
shutil.copy(FIXTURES_DIR / "alpha-readme.md", path / "README.md")
(path / "scratch.rs").write_text("// scratch\n")
def _create_branch_beta(env: DemoEnv):
branch = "beta"
path = env.work_base / f"acme.{branch}"
git(["-C", str(env.repo), "checkout", "-q", "-b", branch, "main"])
git(["-C", str(env.repo), "push", "-u", "origin", branch, "-q"])
git(["-C", str(env.repo), "checkout", "-q", "main"])
git(["-C", str(env.repo), "worktree", "add", "-q", str(path), branch])
(path / "notes.txt").write_text("# TODO\n- Add caching\n")
git(["-C", str(path), "add", "notes.txt"])
def _create_branch_hooks(env: DemoEnv):
branch = "hooks"
path = env.work_base / f"acme.{branch}"
git(["-C", str(env.repo), "checkout", "-q", "-b", branch, "main"])
shutil.copy(FIXTURES_DIR / "lib-hooks.rs", env.repo / "src" / "lib.rs")
git(["-C", str(env.repo), "add", "src/lib.rs"])
commit_dated(env.repo, "feat: add math operations, consolidate tests", "2H")
git(["-C", str(env.repo), "checkout", "-q", "main"])
git(["-C", str(env.repo), "worktree", "add", "-q", str(path), branch])
lib_rs = path / "src" / "lib.rs"
lib_rs.write_text(lib_rs.read_text() + "// Division coming soon\n")
git(["-C", str(path), "add", "src/lib.rs"])
lib_rs.write_text(lib_rs.read_text() + "// TODO: add division\n")
def check_dependencies(commands: list[str]):
for cmd in commands:
if not shutil.which(cmd):
raise SystemExit(f"Missing dependency: {cmd}")
def check_ffmpeg_libass():
if not shutil.which("ffmpeg"):
raise SystemExit(
"Missing dependency: ffmpeg\n"
"Install with: HOMEBREW_NO_INSTALL_FROM_API=1 brew install --build-from-source ffmpeg"
)
result = subprocess.run(
["ffmpeg", "-filters"],
capture_output=True,
text=True,
)
if " ass " not in result.stdout:
raise SystemExit(
"ffmpeg missing libass support (required for keystroke overlay).\n"
"Install with: HOMEBREW_NO_INSTALL_FROM_API=1 brew install --build-from-source ffmpeg"
)
def setup_demo_output(out_dir: Path) -> Path:
out_dir.mkdir(parents=True, exist_ok=True)
starship_config = out_dir / "starship.toml"
shutil.copy(FIXTURES_DIR / "starship.toml", starship_config)
return starship_config
def record_text(
demo_env: DemoEnv,
tape_path: Path,
output_txt: Path,
replacements: dict,
repo_root: Path,
vhs_binary: str = "vhs",
) -> None:
rendered = render_tape(tape_path, replacements, repo_root)
if not rendered:
raise RuntimeError(f"Failed to render tape: {tape_path}")
temp_txt = (demo_env.out_dir / ".text-output.txt").resolve()
rendered = re.sub(
r'^Output\s+"[^"]+"', f'Output "{temp_txt}"', rendered, flags=re.MULTILINE
)
rendered = re.sub(r"^Set Width .*$", "Set Width 120", rendered, flags=re.MULTILINE)
rendered = re.sub(
r"^Set Height .*$", "Set Height 120", rendered, flags=re.MULTILINE
)
for setting in ["FontSize", "Theme", "Padding"]:
rendered = re.sub(rf"^Set {setting} .*$\n?", "", rendered, flags=re.MULTILINE)
tape_rendered = (demo_env.out_dir / ".text-rendered.tape").resolve()
tape_rendered.write_text(rendered)
try:
run([vhs_binary, str(tape_rendered)], check=True)
finally:
tape_rendered.unlink(missing_ok=True)
if not temp_txt.exists():
raise RuntimeError(f"VHS succeeded but output file not created: {temp_txt}")
shutil.copy(temp_txt, output_txt)
temp_txt.unlink()
def extract_commands_from_tape(
tape_path: Path, repo_root: Path, command_prefixes: tuple[str, ...] = ("wt", "git")
) -> list[str]:
rendered = render_tape(tape_path, {}, repo_root)
if not rendered:
return []
commands = []
in_visible_section = False
lines = rendered.split("\n")
i = 0
while i < len(lines):
line = lines[i].strip()
if line == "Show":
in_visible_section = True
elif line == "Hide":
in_visible_section = False
if in_visible_section and line.startswith("Type "):
match = re.match(r'Type\s+["\'](.+)["\']', line)
if match:
cmd = match.group(1)
j = i + 1
while j < len(lines):
next_line = lines[j].strip()
if not next_line:
j += 1
continue
if next_line.startswith("Sleep "):
j += 1
continue
if next_line == "Enter":
if any(cmd.startswith(prefix) for prefix in command_prefixes):
commands.append(cmd)
break
i += 1
return commands
def record_snapshot(
demo_env: "DemoEnv",
tape_path: Path,
output_snap: Path,
repo_root: Path,
) -> None:
commands = extract_commands_from_tape(tape_path, repo_root)
if not commands:
raise RuntimeError(f"No snapshotable commands found in {tape_path.name}")
env = os.environ.copy()
env.update(
{
"HOME": str(demo_env.home),
"XDG_CONFIG_HOME": str(demo_env.home / ".config"),
"PATH": f"{repo_root / 'target' / 'debug'}:{demo_env.home / '.local' / 'bin'}:{os.environ.get('PATH', '')}",
"TERM": "xterm-256color",
"LANG": "en_US.UTF-8",
"LC_ALL": "en_US.UTF-8",
"GIT_PAGER": "", }
)
script_lines = [
"# Initialize shell integration",
"wt config shell init fish | source",
"source ~/.config/fish/completions/wt.fish",
f"cd {demo_env.repo}",
"",
]
for i, cmd in enumerate(commands):
script_lines.append(f"echo '$ {cmd}'")
script_lines.append(f"{cmd} 2>&1")
if i < len(commands) - 1:
script_lines.append("echo ''")
script_content = "\n".join(script_lines)
script_path = demo_env.out_dir / ".snapshot-script.fish"
script_path.write_text(script_content)
result = subprocess.run(
["fish", str(script_path)],
env=env,
capture_output=True,
text=True,
)
output = (result.stdout + result.stderr).rstrip()
temp_path = str(demo_env.out_dir)
temp_path_real = str(demo_env.out_dir.resolve())
output = output.replace(temp_path_real, "<DEMO_DIR>")
output = output.replace(temp_path, "<DEMO_DIR>")
output_snap.parent.mkdir(parents=True, exist_ok=True)
output_snap.write_text(output + "\n")
@dataclass
class DemoSize:
width: int
height: int
fontsize: int
SIZE_SOCIAL = DemoSize(width=1200, height=700, fontsize=26) SIZE_DOCS = DemoSize(width=1600, height=900, fontsize=24)
def build_tape_replacements(demo_env: DemoEnv, repo_root: Path) -> dict:
starship_config = (demo_env.out_dir / "starship.toml").resolve()
return {
"DEMO_REPO": demo_env.repo.resolve(),
"DEMO_HOME": demo_env.home.resolve(),
"REAL_HOME": REAL_HOME,
"STARSHIP_CONFIG": starship_config,
"TARGET_DEBUG": (repo_root / "target" / "debug").resolve(),
"ANTHROPIC_API_KEY": os.environ.get("ANTHROPIC_API_KEY", ""),
"GIT_PAGER": "", }
def record_all_themes(
demo_env: "DemoEnv",
tape_template: Path,
output_gifs: dict[str, Path],
repo_root: Path,
vhs_binary: str = "vhs",
size: DemoSize = None,
):
if size is None:
size = SIZE_DOCS
tape_rendered = demo_env.out_dir / ".rendered.tape"
base_replacements = build_tape_replacements(demo_env, repo_root)
for theme_name, output_gif in output_gifs.items():
theme = THEMES[theme_name]
delta_flags = "delta --paging=never"
if theme_name == "light":
delta_flags += " --light"
replacements = {
**base_replacements,
"OUTPUT_GIF": output_gif,
"THEME": format_theme_for_vhs(theme),
"WIDTH": size.width,
"HEIGHT": size.height,
"FONTSIZE": size.fontsize,
"GIT_PAGER": delta_flags,
}
rendered = render_tape(tape_template, replacements, repo_root)
if not rendered:
continue
tape_rendered.write_text(rendered)
print(f"\nRecording {theme_name} GIF...")
record_vhs(tape_rendered, vhs_binary, expected_output=output_gif)
tape_rendered.unlink(missing_ok=True)
print(f"GIF saved to {output_gif}")