---
title: "The demo.toml DSL — DemoStage docs | UniverLab"
source: "https://univerlab.org/demostage/docs/demo-toml/"
---

[← DemoStage](/demostage/)Documentation

# The `demo.toml` DSL

Last updated 2026 · Sol 270 · 11:35 UTC (2026-09-27T11:35Z)

A score is a TOML document with four areas: metadata, the (optional) sandbox env, the typing parameters, the canvas layout, and the timeline.

## `[demo]`

    [demo]
    name = "my-demo"          # used for the output filename
    output_dir = "./dist"     # default
    speed = "2x"              # how this demo is published; omit for 1x
    targets = ["gif", "mp4"]  # formats to build; omit for the defaults (gif, mp4; svg is opt-in)
    # prompt = "\[\e[32m\]❯\[\e[0m\] "   # custom prompt; omit for the default `$ `

`speed` and `targets` say **how this demo is meant to be exported**, so the next person to rebuild it gets the same result without knowing which flags you used. A demo is usually recorded at a comfortable pace and published faster, and without this the multiplier survives nowhere: the published file’s duration is the only remaining evidence of it.

Both are defaults, not locks — `--speed` and a positional target still win:

    demo export demo.rec              # 2x and gif+mp4, per the score above
    demo export gif demo.rec          # 2x, gif only
    demo export demo.rec --speed 1x   # the recorded pace, both formats

`prompt` is bash `PS1` syntax, so colours (`\[\e[36m\]…\[\e[0m\]`) and escapes like `\w` work. The built-in default is the plain Linux `$` . Export always forces this prompt over your rc files, so a demo never leaks `user@host`. The pixel targets (gif/mp4) render a green-arrow `❯` and a handful of common prompt symbols, so a custom prompt rasterizes too; very exotic glyphs may not.

## `[env]` (optional)

A sandbox for an isolated, reproducible run. Scripts run **before** the clock starts (setup) and **after** the timeline (teardown), so their output is not captured.

    [env]
    isolated = true
    setup_script = "mkdir -p /tmp/demo-sandbox && cd /tmp/demo-sandbox"
    teardown_script = "rm -rf /tmp/demo-sandbox"
    requires = ["GITHUB_TOKEN"]   # env vars export needs; values come from your shell

`requires` lists environment variables the export run needs — typically a token that lets a flow skip a secret prompt (so the demo stays reproducible without storing the secret). The values come from whoever runs `export`; the score never holds them. `demo record` validates the score before running and fails if a required variable is unset.

## `[typing]` (optional)

Controls the humanized per-character jitter applied to `type` steps that set `human_salt = true`.

    [typing]
    base_ms = 80     # base speed, ms per character
    salt_ms = 15     # max jitter ±, ms
    seed = 42        # omit for a random feel; set for reproducible output

## `[[sources]]` (optional)

The content sources a demo can show: the terminal (`main`) and any browser pages (repo, docs, localhost, a local PDF). The `demo capture` wizard writes them; during a capture, `demo focus <source>` reveals them. Authoring them by hand works too.

    [[sources]]
    id = "main"
    type = "terminal"
    
    [[sources]]
    id = "docs"
    type = "browser"
    url = "https://docs.example.com"
    # theme = "dark"        # emulated colour scheme: "light" | "dark"

## `[layout]` and `[[layout.panes]]`

The canvas (pixels) and the panes placed on it.

    [layout]
    width = 1920
    height = 1080
    fps = 15               # 15, 24, or 30 — set at capture via --fps
    line_height = 1.0      # gif/mp4 line spacing × font size; 1.0 connects box-drawing
    background = "#0b0f14"
    
      [[layout.panes]]
      id = "console"        # unique id, referenced by the timeline
      type = "terminal"     # "terminal" | "browser"
      x = 0
      y = 0
      width = 960
      height = 1080
      font_family = "monospace"   # terminal only
      font_size = 16              # terminal only
    
      [[layout.panes]]
      id = "preview"
      type = "browser"
      x = 960
      y = 0
      width = 960
      height = 1080
      url = "file:///tmp/demo-sandbox/output.pdf"   # browser requires a url
      # theme = "dark"       # emulated colour scheme: "light" | "dark"
      # reveal_at = 3.5      # visibility window (seconds): overlay shows from here…
      # hide_at = 8.0        # …until here (omit either for "from the start"/"to the end")

`demo record` validates the score before it runs: pane ids are unique, panes fit the canvas, and browser panes have a `url`.

`reveal_at`/`hide_at` (browser panes) window a pane in time: the terminal is the always-on background and each browser pane overlays it only inside its window — this is what a live `demo focus` records, and how switching views (or going back to the terminal) renders.

`ignore_speed` (browser panes, optional, default `false`) exempts a pane from the export speed multiplier. A PDF pane with `ignore_speed = true` pans at the 1x cap (1200 px/s) regardless of `--speed`, while the rest of the demo still accelerates. Use it for a document that should remain readable at any export speed.

      [[layout.panes]]
      id = "pdf-r1"
      type = "browser"
      x = 0
      y = 0
      width = 960
      height = 1080
      url = "file:///tmp/demo-sandbox/output.pdf"
      ignore_speed = true   # pan at the 1x cap even when the demo is exported at 2x

`line_height` (optional, default `1.2`) is the line spacing as a multiple of the font size. `1.0` makes box-drawing characters (`│ ─ ┌ ┘ …`) join into continuous lines for TUIs; raise it (e.g. `1.25`) for airier, prose-style spacing.

## `[[timeline]]`

Steps share one timeline, each tagged by `action`:

| action | Fields | Meaning |
| --- | --- | --- |
| focus | pane | Make a pane the active input target. |
| type | text, human_salt? | Type into the focused terminal. |
| keypress | key | Press a named key — enter, tab, esc, up, ctrl+c, … |
| wait | duration_ms | Hold for a fixed time. |
| wait_for_stdout | match, pane? | Block until a substring appears in the raw output. |
| wait_for_quiet | quiet_ms, max_ms? | Block until the output has been silent this long. |
| wait_for_screen | match, timeout_ms? | Block until a pattern is visible on the rendered screen. |
| caption | text | Show an on-canvas step label (empty text clears it). Rendered on gif/mp4. |
| secret | prompt | Re-supply a secret at this point on demo record (the value is asked for, never stored). |
| scroll | direction, velocity?, duration_ms, pane? | Scroll a browser pane. direction is up or down (default down); up starts at the bottom and pans to the top. velocity is constant (linear, default) or ease_in_out (smooth acceleration/deceleration). When several scroll steps target the same pane, the first wins. |
| terminate | — | End the demo. |

    [[timeline]]
    action = "focus"
    pane = "console"
    
    [[timeline]]
    action = "type"
    text = "cargo build --release"
    human_salt = true
    
    [[timeline]]
    action = "keypress"
    key = "enter"
    
    [[timeline]]
    action = "wait_for_stdout"
    match = "Finished"
    
    [[timeline]]
    action = "terminate"