Skip to content
smorinlabsPublic

About

Vaporware demos as code: render scripted terminal sessions to asciicast v3 (and GIF via agg)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

mockcast

CI

Vaporware demos as code. Describe an imaginary CLI in a YAML script, and mockcast renders it into a real asciicast v3 .cast file — a scripted, fake terminal session you can play with the asciinema player and share. No real product required, no terminal recorded.

It ships with a headline demo for an imagined product, acme ai (a tool that manages AI coding harnesses — Claude Code, Codex, Gemini CLI — plus profiles and skills).

mockcast rendering the acme ai demo

(Above: media/hero.gif, generated by mockcast from examples/acme-ai-day.yaml. See also media/styled.gif — the ANSI styling showcase.)

maya@laptop ~/acme $ acme ai profile install acme-team
Resolving acme-team…
✔ claude-code   installed
✔ codex         already up to date
✔ gemini-cli    installed
✔ synced 7 skills · logged in as maya

Why generate instead of record?

A recording embeds wall-clock time and human timing, so it can't be tested or reproduced. mockcast emits the v3 format directly from a definition, which makes the output a pure function of its script:

  • Deterministic — a seeded RNG drives typing "jitter," and the optional timestamp header is omitted, so the same YAML always renders byte-identical bytes.
  • Diffable & reviewable — the demo lives in version control as readable YAML.
  • Reshootable — tweak one line of timing or output and re-render; you never have to perform the demo again.

Requirements

  • Python ≥ 3.12 and uv
  • asciinema ≥ 3.0 for playback (v3 is an asciinema 3.x format; the legacy 2.x CLI can't play it)
  • agg — only for the gif command (brew install agg)

Check your toolchain:

make check

Install

uv sync

This installs the package and the mockcast console script into the project environment (run it via uv run mockcast …).

Usage

# Schema-check a demo script (friendly errors, no output written)
uv run mockcast validate examples/acme-ai-day.yaml

# Render to a .cast file
uv run mockcast render examples/acme-ai-day.yaml -o hero.cast

# Render to a temp file and play it in your terminal
uv run mockcast play examples/acme-ai-day.yaml

# Render to an animated GIF (via agg)
uv run mockcast gif examples/acme-ai-day.yaml -o hero.gif
uv run mockcast gif examples/styled-output.yaml -o styled.gif --speed 1.5 --theme dracula

gif accepts --speed <factor> and --theme <name> (passed through to agg; themes include asciinema, dracula, monokai, nord, solarized-dark, …).

Then watch the real thing — timed playback with navigable chapter markers:

asciinema play hero.cast        # press the marker keys to jump between chapters

Non-interactive sanity check (replays the timed stream to plain text — proves asciinema accepts the file):

asciinema convert -f txt hero.cast -

In asciinema 3.x, cat is a concatenation command (needs ≥2 files); use convert to dump a single cast to text.

Writing a demo script

A demo is global settings (meta/vars) plus an ordered list of scenes, each holding steps. Each scene name becomes a navigable chapter marker (when auto_markers is on).

meta:
  title: "acme ai — a day in the life"
  term: { cols: 100, rows: 30, type: xterm-256color }
  typing: { cps: 22, jitter: 0.35 }        # chars/sec + randomness
  prompt: "{user}@{host} {cwd} $ "
  pauses: { after_command: 0.5, between_scenes: 1.2 }
  idle_time_limit: 2.0
  seed: 42                                  # makes jitter reproducible
  auto_markers: true                        # one chapter marker per scene

vars: { user: maya, host: laptop, cwd: "~/acme" }

scenes:
  - name: onboard
    steps:
      - type: "acme ai profile install acme-team"
      - output: |
          Resolving acme-team…
          ✔ claude-code   installed
        stream: lines                       # reveal line-by-line
      - pause: 1.0

  - name: teammate                          # the two-machine trick
    vars: { user: sam, host: workstation }  # swaps the prompt identity
    prompt: "{user}@{host} $ "
    steps:
      - banner: "─── Sam's machine ───"
      - type: "acme ai search recipe"

Step vocabulary

Step Behavior
type: "<cmd>" renders the prompt, then "types" the command char-by-char (cps + seeded jitter), then Enter + an after_command pause
output: "<text>" prints text; modifier stream: instant|lines|chars (default instant), optional delay: seconds before
pause: <sec> idle gap (think time)
banner: "<text>" prints a styled scene-divider line (set dressing)
clear: true clears the screen
marker: "<label>" drops a v3 m event → a navigable chapter marker

Scene-level vars: / prompt: override the globals — that's how the "Sam's machine" identity swap works. You can embed raw ANSI escapes in output/banner strings for color.

How it maps to asciicast v3

  • Header (line 1): {"version": 3, "term": {"cols", "rows", "type"}, …}. timestamp is omitted for reproducibility.
  • Events: [interval, code, data], where interval is relative seconds since the previous event (the first event's interval is 0.0), rounded to 3 decimals. Only two codes are emitted: o (output — every keystroke and printout) and m (marker).

Architecture

Three I/O-free units behind a thin CLI:

demo.yaml ──▶ [models.py] ──▶ [engine.py] ──▶ [writer.py] ──▶ out.cast ──▶ asciinema
 (author)      validate         render          serialize                   (play)
  • models.py — pydantic v2 schema + validation (what a valid demo is).
  • engine.py — pure render(Demo) -> Cast: all timing, typing, prompt, and marker logic. No filesystem access (so it's golden-testable).
  • writer.py — serializes a Cast to newline-delimited v3 JSON.
  • cli.py — render / validate / play; owns all I/O.
src/mockcast/{__init__,models,engine,writer,cli}.py
tests/{test_models,test_engine,test_writer,test_cli}.py
examples/acme-ai-day.yaml      # the 10-beat hero cut
docs/superpowers/specs/        # design spec
docs/superpowers/plans/        # implementation plan

The hero cut

examples/acme-ai-day.yaml is one continuous "day in the life" of acme ai: onboard → verify → launch → usage → search → author a skill → ship → promote → Sam's machine (pull) → status. It renders to ~374 events with 10 chapter markers, and a teammate scene swaps the prompt from maya@laptop to sam@workstation to prove the producer→consumer loop in a single cast.

Styling with ANSI

examples/styled-output.yaml is a second demo showing bold, color, symbols, and an ★ Insight box — the rich-terminal look. Styling is just raw ANSI in output strings; the engine passes it straight through. Two rules:

  1. Use double-quoted YAML so \e becomes a real ESC byte (block scalars | keep text literal and won't interpret \e).
  2. Instant output adds no trailing newline — end standalone lines with \n, or use stream: lines (which adds \r\n per line and reveals them one at a time). Keep each line self-contained (open and \e[0m-close its color on the same line).
uv run mockcast play examples/styled-output.yaml     # see it in color

Development

just all        # format, lint, typecheck, test
just test       # pytest only

Tooling: uv (deps/packaging), ruff (format + lint), ty (typecheck), pytest (tests), just (command runner). The test suite covers schema validation, the engine's timing/marker/identity logic, deterministic output (jitter: 0 exact intervals + render-twice byte equality), v3 serialization, and the CLI.

License

MIT © Steve Morin

About

Vaporware demos as code: render scripted terminal sessions to asciicast v3 (and GIF via agg)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages