βŒ¨οΈβ€‚Terminal

How do I put a terminal demo in a deck, a README or a video, and find out when the tool it demonstrates stops saying what the demo says?

@getnarro/terminal is a terminal session as text. Not a video of one, not a screen recording: a file that names the commands, the pauses and the moments worth naming, plus β€” once you have run it β€” the bytes the commands actually printed.

That one decision is what everything else follows from. Text diffs, so CI can tell you the demo went stale. Text has no resolution, so the same file renders a GIF, an SVG, a still, a slide and a shot in a video. Text carries palette indices rather than colours, so one recording is re-themable. And a screen is a pure function of the frame number, which is what lets a video renderer’s parallel workers each draw frame 500 without having drawn the 499 before it.

npx narro-terminal render demo.term.json          # a GIF
npx narro-terminal render demo.term.json --svg    # vector, no browser needed
npx narro-terminal still  demo.term.json          # one frame, at the poster mark
npx narro-terminal check  demo.term.json          # what will be wrong before you render it
npx narro-terminal record demo.term.json          # run it for real, and write down what happened
npx narro-terminal record demo.term.json --check  # …and fail when the output changed

The one rule

Rendering never executes anything. A playbook names commands; drawing one draws them being typed. Only narro-terminal record runs them, it prints them and asks first, and it requires --yes when nothing is attached to the terminal.

This is a security boundary, not an implementation detail. A deck arrives by download, from a template, from a marketplace, from a model. If narro build spawned a shell for a fence, opening a document would be a way to execute its author’s commands, and the blast radius would be every user of the CLI. So: in markdown, only authored mode exists. narro build, narro dev, narro check and narro export draw text and never spawn a process.

The ladder

Four ways to write the same object. Every one goes through the same validator, and nothing downstream can tell them apart.

A transcript you already wrote

The floor. A ```bash block with prompts in it becomes a terminal by changing one word:

```term
$ npm create narro@latest my-deck
βœ” Scaffolded my-deck in 8.4s
$ cd my-deck && npm run dev
  ➜  Local:   http://localhost:5173/
```

Lines beginning with the prompt were typed; everything else is what came back. ANSI colour survives, because a pasted transcript has it. It is drawn at build time as positioned HTML β€” no emulator in the browser, no measurement pass, nothing to load β€” which is what makes it work in single-file mode, in a PDF and in a PPTX.

A tape, when you need marks, waits and keys

The rung above, and it reads VHS .tape syntax, because thousands of demos are already written in it:

```tape {window=mac poster=ready}
Set TypingSpeed 50ms
Type "npm create narro@latest my-deck"
Enter
Sleep 2s
Wait /Scaffolded/
Mark ready
```

A tape says what happens; a transcript says what was printed. Narro adds four directives VHS has no equivalent for β€” Mark (a named moment), Mask (what varies between runs), Cue (narration alignment) and Expect β€” and reports every directive that did not map instead of dropping it. Source is refused inside a fence: a fence is a document, and a document does not get to read ~/.ssh.

FenceAttributes
termtheme, prompt, cols, rows, window, title, width
tapetheme, cols, rows, window, title, width, poster

A playbook file

demo.term.json beside the deck, shared by a slide, a README, a docs page and a video:

{
  "cols": 92,
  "rows": 24,
  "steps": [
    { "id": "install", "type": "npm create narro@latest my-deck", "enter": true },
    { "id": "done", "output": "βœ” Scaffolded my-deck in 8.4s\r\n$ ", "mask": ["\\d+\\.\\d+s"] },
    { "id": "ready", "expect": "Scaffolded" }
  ]
}

One action per step, from this set: type, key, paste, output, wait, expect, clear, resize, mark, hide, show. A key step takes Enter, Tab, Backspace, Delete, Space, Escape, Up, Down, Left, Right, Home, End, PageUp, PageDown, Insert, with Ctrl+, Alt+ and Shift+ in front of them.

TypeScript, for the programmatic case

import { defineTerminal } from "@getnarro/terminal";

export default defineTerminal({
  steps: [{ id: "run", type: "npm run build", enter: true }],
});

expect may be written as a regular expression here and is stored as a string, because a playbook has to survive a trip through JSON.

Recording, and the check that keeps a demo honest

record runs the playbook in a real terminal β€” a script(1) pty, so nothing native to install β€” and writes demo.capture.json beside it: the bytes, with the second each arrived.

Then the point of the exercise:

npx narro-terminal record demo.term.json --check

It re-records, compares against the stored capture without writing, and exits 1 when a step’s output changed:

βœ” Β«beforeΒ»   matches
✘ install    output changed
    stored  βœ” Scaffolded my-deck in β€Ήmaskedβ€Ί
    now     βœ” Created my-deck in β€Ήmaskedβ€Ί

This is what nothing else in this field can do. A .cast carries what a terminal emitted and not what the author meant; a VHS .tape carries the intent and not the reality. Neither can tell you that the tool being demonstrated stopped saying what the demo claims it says. A playbook plus its capture carries both β€” and because the capture is text, the answer is a diff a person reads.

Two things make it survivable in CI:

  • Output is attributed to a command by the shell’s own echo of it, not by timestamp. Two recordings of one session are never timed alike, and time-based attribution reports jitter as a change on every run.
  • Masks are session-wide. A duration, a version, a temp path and a port change every run and mean nothing. A mask is written on the step whose expect it relates to and applies to every step, because output arrives while the previous step is still running.

check reports a mask that matches nothing, because a mask that hides nothing means --check will report the very difference it was written to absorb.

Output

CommandWhat you get
renderAn animated GIF. Differenced frames, a counted palette, per-frame delays, and a --max-mb ladder that retries down to a size you asked for
render --svgAn animated SVG: vector, usually smaller, and it needs no browser at all
stillOne PNG, at a mark, a second or a frame
<narro-terminal>The embed β€” 14.7 KB gzipped, no framework, playable and seekable
<TerminalScene>A terminal in a @getnarro/video composition, aligned to a narration cue
```term / ```tapeA terminal on a slide, drawn at build time

The SVG does not animate in a GitHub README. GitHub serves README images through its camo proxy as <img>, and animation inside an <img>-loaded SVG is unreliable across browsers β€” reported broken in Firefox while working in Chrome. Use the GIF there, and the SVG on a docs site, a blog post or a slide. The CLI says so every time it writes one.

Themes: narro-dark, narro-light, asciinema, dracula, monokai, nord, solarized-dark, solarized-light, github-dark, github-light. A cell stores a palette index, not a colour, which is what makes one recording re-themable β€” the same session in a docs site’s light theme and a launch video’s dark one, from one file.

Coming from somewhere else

npx narro-terminal import session.cast --explain   # asciicast v2 or v3
npx narro-terminal import demo.tape                # a VHS tape
npx narro-terminal import old.ttyrec               # a ttyrec
npx narro-terminal export demo.term.json --cast-version 3

The format is decided by what is in the file, not by its name. What an import cannot recover is intent: a cast records what a terminal emitted, so it arrives as output and wait steps β€” honestly a transcript β€” and everything downstream works on it unchanged. Export goes the other way, including from an authored playbook, which produces a recording of a session that never happened, typing and all, that asciinema play plays.

What check reports

`unsupported-sequence`, `unsupported-sgr`, `expect-never-matched`, `output-truncated`, `line-too-wide`, `mask-never-matches`, `long-idle-gap`, `session-very-long`, `typing-faster-than-frames`, `glyph-not-in-face`, `font-not-embedded`

--glyphs adds the two that need a browser. Every glyph in a monospace face advances by exactly one cell, so a glyph whose advance is anything else came from a fallback face β€” and a fallback glyph is what drifts a row off the grid. It measures in the same engine that will do the render, with the same face embedded.

npx narro-terminal render demo.term.json --font ./JetBrainsMono.woff2

Without --font the render uses whatever monospace face the machine has, and two machines produce different pixels. check says so rather than leaving it to be found by a frame diff.

Closed sets

Everything above is published as data in catalog.json, under its terminal key: the commands, the step actions, the key names, the themes, the findings, the tape directives and settings, the fences and their attributes, and every entry point. A name that is not in that file does not exist.

Directives: Output, Require, Env, Set, Source, Type, Sleep, Hide, Show, Screenshot, Copy, Paste, Wait, Mark, Mask, Cue, Expect.

Settings: Set Shell, Set Theme, Set TypingSpeed, Set Width, Set Height, Set Cols, Set Rows, Set FontSize, Set FontFamily, Set LineHeight, Set LetterSpacing, Set Padding, Set Framerate, Set PlaybackSpeed, Set LoopOffset, Set WindowBar, Set WindowBarSize, Set BorderRadius, Set Margin, Set MarginFill, Set CursorBlink, Set WaitTimeout, Set WaitPattern.

Entry points: @getnarro/terminal, @getnarro/terminal/react, @getnarro/terminal/player, @getnarro/terminal/hast, @getnarro/terminal/renderer, @getnarro/terminal/record, @getnarro/terminal/convert, @getnarro/terminal/video, @getnarro/terminal/embed.

IntroductionWhat is Narro, and should I be writing markdown or React?
InstallationWhat do I install, and what does the project look like afterwards?
First DeckWhat does a working React deck look like, end to end?
Markdown ModeHow do I split a slide, style one word, or reveal a line β€” without leaving the markdown file?
Writing AI PromptsWhat do I tell an AI so the deck it writes actually builds?
Rules for AI AgentsWhat do I paste into my repo so an agent stops writing decks that build wrong?
React APIWhich component or hook do I import, and what does it take?
CLIWhich command do I run, and what are its flags?
Markdown APIHow do I read, edit, or validate a deck file from my own code instead of by hand?
Component ReferenceWhat props does this component take, and which package do I import it from?
Verifying a deckHow do I know my deck is actually correct?
AnimationHow do I reveal a list one line at a time, or move between slides with something other than a cut?
NavigationHow does the audience move through the deck, and how do I present it?
Canvas & PositioningHow do I put something at an exact position instead of in the flow?
Images & MediaHow do I use an image as a background, tint it, or embed a video?
ArchitectureWhich package owns what, and why is the seam where it is?
Zooming canvasHow do I zoom and pan across one big canvas instead of cutting between slides?
Transform ModeHow do I zoom and pan across one big canvas instead of cutting between slides?
Import & ExportHow do I get this deck out as PPTX, PDF, or one file I can email?
VideoHow do I turn a story into a video, and keep it right when the words, the product or the source changes?
Recording a web appHow do I put a demo of my product in a deck or on a landing page without shipping a video of it?
Embedding a replayHow do I embed a recorded demo of my app in my React app, my Astro site, or any other page?
TerminalHow do I put a terminal demo in a deck, a README or a video, and find out when the tool it demonstrates stops saying what the demo says?
AI IntegrationHow do I wire an AI assistant up to Narro so it can write and build decks?
ThemingHow do I change the colours, fonts, and overall look of a deck?
Deck TemplatesHow do I define one house style with named layouts my slides can reference, like a PowerPoint master?
TroubleshootingSomething is wrong with my deck. What is it, and how do I fix it?
LimitationsWhat can't Narro do, and what do I do instead?