npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dvdrw

v1.4.1

Published

Create beautiful animated SVGs from terminal recordings

Readme

dvdrw is the Node library behind dvd-cli — call it from your own code, your build pipeline, or your service. Output is a single self-contained animated SVG. No ffmpeg, no headless browser, no shelling out.

npm install dvdrw
import dvd from 'dvdrw';

const { svg } = await dvd(`
  Type "echo hello world"
  Enter
  Sleep 800ms
`, { theme: 'dracula', template: 'macos' });

Contents

Guide

Reference

Meta


Why the library?

The CLI is great when you have a .cd script in a file. The library is for everything else:

  • Programmatic content — render the output of a real test run, a real deploy, or a real benchmark, with frames built from runtime data.
  • Raw stdout capture — feed any ANSI byte stream straight in ({ raw }). Spinners, progress bars, chartscii, lolcat, anything that animates on the terminal.
  • Build-pipeline integration — fully async, optional onProgress callback, no temp files, no subprocesses by default.
  • Embeddable — drop into a docs generator, a service, an Electron app, a serverless function. The output is a string.
  • Composable — the parser, terminal emulator, coalescer, emitter and animator are all exported and usable independently.

If you want a single command on the CLI that takes a .cd file and writes a .svg, use dvd-cli. If you want to call into the engine, you're in the right place.


Inputs

dvd(input, options) accepts four input shapes. Pick whichever matches the data you already have.

| Input | Shape | Use when | |---|---|---| | Script string | string | You're writing the demo by hand | | Steps | CDCommand[] | The content is computed at runtime | | Raw output | { raw, totalDuration? } | You already captured stdout | | Pre-parsed | { script } | You parsed and transformed a script yourself |

1. CD script string

The fastest path. Same syntax as dvd-cli, just inlined.

const { svg } = await dvd(`
  Type "npm install dvdrw"
  Sleep 400ms
  Enter
  Sleep 800ms
`, { theme: 'dracula', template: 'macos', title: 'quick-start' });

Type + Enter runs the line through a real shell and records its actual output. Set lines inside the script are applied as settings — see script settings.

2. Programmatic steps

When the content of your animation is computed at runtime — a generated test report, a stream of deploy events, a templated demo — skip the script and pass an array.

// Each Type+Enter is sent through a real shell, so wrap any styled output
// in `echo -e "..."` rather than typing raw ANSI as a command.
const steps = [
  { type: 'Type', text: 'echo -e "\\x1b[2m$\\x1b[0m npm test"' },
  { type: 'Key', key: 'Enter' },
  { type: 'Sleep', duration: 600 },
  ...tests.flatMap((t) => [
    { type: 'Type', text: `echo -e "\\x1b[32m  ✓\\x1b[0m ${t.name} \\x1b[2m(${t.ms}ms)\\x1b[0m"` },
    { type: 'Key', key: 'Enter' },
    { type: 'Sleep', duration: 200 },
  ]),
];

const { svg } = await dvd(steps, { theme: 'tokyoNight', template: 'macos', title: 'test runner' });

Settings are not read from a steps array — a { type: 'Set', ... } entry is ignored. Pass them as the second argument to dvd() instead.

Full source: examples/02-programmatic-steps.ts

3. Raw terminal output

Capture stdout from any command and hand the bytes over. dvd auto-detects the animation pattern (terminal reset, cursor restore, cursor up, clear line) and splits into frames automatically.

import dvd from 'dvdrw';
import { spawnSync } from 'node:child_process';

const r = spawnSync('myscript.sh', { encoding: 'buffer' });
const raw = r.stdout.toString('binary');

const { svg } = await dvd({ raw, totalDuration: 2400 }, {
  theme: 'catppuccinMocha',
  template: 'macos',
  title: 'spinner capture',
});

totalDuration is the wall-clock time the capture took, in ms. Without it, frames are spaced at a flat 30fps.

Full source: examples/03-raw-output.ts

4. Pre-parsed script

If you've already parsed a CD script (e.g., for validation or transformation), pass the AST directly:

import dvd, { parseCDScript } from 'dvdrw';

const script = parseCDScript(scriptText);
// ...mutate, validate, splice frames...
const { svg } = await dvd({ script }, { theme: 'nord' });

Seeding the terminal

seed pre-fills the screen with text that was never typed and never executed. Every frame starts with it already on screen, and the prompt begins on the line below.

Use it to give a recording context it would otherwise waste time earning — earlier commands in the session, a banner, a fake motd, a chart the demo is about to update.

const { svg } = await dvd(`
  Type "npm run deploy"
  Sleep 400ms
  Enter
  Sleep 1200ms
`, {
  seed: [
    '\x1b[95m❯\x1b[0m npm test',
    '\x1b[32m  ✓\x1b[0m 56 passing \x1b[2m(1.7s)\x1b[0m',
    '',
  ],
  theme: 'catppuccinMocha',
  template: 'macos',
});

The seed is content, not chrome — it goes through the same terminal emulator as everything else:

  • ANSI works. Colors, bold, dim, background fills — anything the emulator understands.
  • Auto-sizing accounts for it. A wide or tall seed grows the SVG, same as recorded output.
  • clear wipes it. A clear in the script resets the screen exactly like a real terminal, seed included.
  • It is not a prompt. Seed lines are printed verbatim, with no prompt prefix. Include your own ❯ if you want the seed to look like past commands.

Accepted as a string or an array of lines — the array form avoids escaping and makes blank lines explicit:

seed: 'line one\nline two'     // a single trailing \n is ignored
seed: ['line one', 'line two'] // same result

All three input shapes support it. With { raw }, the seed is prepended to every detected frame, so it stays on screen while the capture animates underneath it.

From a script, use Set Seed — backticks span multiple lines:

Set Seed `❯ git status
On branch master
nothing to commit, working tree clean`

Type "npm run deploy"
Enter

Set Seed runs the value through the same escape handling as Set PromptPrefix: \e / \x1b, \n, \t and $VAR are expanded. The seed option, being a real JS string, is taken literally.

Full source: examples/11-seed.ts


Themes

37 built-in themes, all exported from themes. Pass by name or as a full Theme object.

await dvd(script, { theme: 'tokyoNight' });

The full list: a11yDark, base16Dark, base16Light, blackboard, catppuccinMocha, cobalt, dark, dracula, draculaPro, duotoneDark, githubDark, githubLight, gruvboxDark, gruvboxLight, hopscotch, lucario, material, monokai, night3024, nord, oceanicNext, oneDark, oneLight, pandaSyntax, paraisoDark, seti, shadesOfPurple, solarizedDark, solarizedLight, synthwave84, terminal, tokyoNight, twilight, verminal, vscode, yeti, zenburn.

For custom palettes, pass a Theme object directly:

import dvd, { type Theme } from 'dvdrw';

const retroGreen: Theme = {
  name: 'retro',
  background: '#0a0a0a',
  foreground: '#00ff00',
  cursor: '#00ff00',
  // ...the 16 ANSI colors
};

await dvd(script, { theme: retroGreen });

Source: examples/04-themes-gallery.ts


Templates

Window chrome — macos / windows / minimal.

Source: examples/09-templates.ts


Loop styles

Animations loop by default. Choose how the loop behaves at the boundary:

await dvd(script, {
  loopStyle: 'reverse',  // 'loop' | 'reverse' | 'rewind' | 'fade'
  loopPause: 600,
  rewindSpeed: 6,        // for 'rewind'
  fadeDuration: 1200,    // for 'fade'
});

Source: examples/05-loop-styles.ts


Branded output

Gradient backgrounds, watermarks, custom borders — for docs sites and landing pages where the SVG carries product weight.

const { svg } = await dvd(script, {
  theme: 'tokyoNight',
  template: 'macos',
  background: 'gradient(#7c5fff, #ff6ec7:diagonal)',
  backgroundPadding: 48,
  borderRadius: 12,
  watermark: 'made with dvd',
});

Backgrounds accept solid colors (#1a1a2e) or gradients in the form gradient(<color>, <color>[:vertical|horizontal|diagonal]) with as many stops as you need.

Source: examples/06-branding.ts


Progress tracking

Wire dvd into your build pipeline or TUI. The onProgress callback fires for every step the executor runs.

await dvd(script, {
  onProgress: (current, total, description) => {
    const pct = Math.round((current / total) * 100);
    process.stdout.write(`\r[${pct}%] ${description ?? ''}`);
  },
});

The returned result also carries metadata — frame count, total duration, effective FPS — for logging and CI annotations:

const result = await dvd(script);
console.log(result.metadata); // { duration: 2118, frameCount: 33, fps: 15.6 }

Source: examples/07-progress.ts


Low-level API

Skip the executor when you want to render arbitrary terminal state directly — for static badges, dashboards, CI annotations, or content that doesn't fit the script model.

import { coalesce, createGridState, emit, processInput, themes } from 'dvdrw';

const fontSize = 16;
const theme = themes.draculaPro;

let state = createGridState(44, 9);
state = processInput(state, '\x1b[1;38;5;213m  dvdrw\x1b[0m · terminal recordings as svg');

const spans = coalesce(state, theme);

const { svg } = emit(spans, state.cursor, false, {
  theme,
  template: 'minimal',
  width: 460,
  height: 240,
  fontSize,
  lineHeight: fontSize * 1.4,
  charWidth: fontSize * 0.6,
  padding: 16,
});

The exposed building blocks:

| Module | Symbols | |---|---| | Terminal emulator | createGridState, processInput, applyCommand, applyCommands, parseInput | | Text processing | coalesce | | Emission | emit, emitAnimated, emitFilmstripAnimated | | Animation | createAnimatedSVG, createFilmstripSVG, optimizeSvg | | Raw output | processRawOutput, detectAnimationType, splitIntoFrames | | Cast files | parseCastFile, RecordingPlayer, generateFramesFromRecording, optimizeFrames | | Script parsing | parseCDScript, CDParseError | | Executor | CDExecutor | | Frame sequencing | planVideo, resolveQuality, autoFps, VIDEO_QUALITY | | Seeding | resolveSeedLines, resolveSeedText | | Misc | themes, getCharWidth |

Source: examples/08-low-level-api.ts


Video frame sequencing

dvdrw emits SVG and only SVG. It does not rasterize and it does not encode — no ffmpeg, no canvas, no browser.

What it does give you is planVideo(): a constant-rate frame plan built from the same emitter options the animation used, so every still renders identically to the animation it came from. Rasterizing and encoding that plan is your job.

import dvd, { planVideo } from 'dvdrw';

const { frameData, emitter } = await dvd(script);
const plan = planVideo(frameData, { emitter, quality: 'high' });

for (const frame of plan.frames()) {
  // frame.svg — a standalone static SVG for this instant
  // frame.index, frame.timestampMs, frame.sourceIndex
  // frame.repeatsPrevious — true when the raster is identical to the last one
}

plan.frames() is a lazy generator; plan.render(index) gives you one frame on its own. Quality tiers are low / medium / high (VIDEO_QUALITY holds their scale, crf, bitsPerPixel and fps), and fps: 'auto' on the high tier asks the recording how fast it actually moves.

Rasterize at plan.frameWidth × plan.frameHeight — the SVG's own intrinsic size, which is larger than emitter.width/height whenever background padding or a wide watermark grows the canvas. Then pad (never scale) to reach the even-numbered plan.width × plan.height the encoder wants.


Rendering modes: filmstrip vs SMIL

Two animation engines are available. The default (filmstrip) is what you almost always want.

| | Filmstrip (default) | SMIL (smil: true) | |---|---|---| | Engine | CSS @keyframes over a deduped row pool | Native SVG <animate> per frame | | File size | Smaller — scales with unique rows, not total frames | Larger — scales with total frames | | Best for | README embeds, docs, long recordings, mostly-static content | Short animations, high-FPS smoothness on iOS Safari / 120Hz |

await dvd(script, { smil: true });

Options reference

Every option is optional.

Content

| Option | Type | Default | | |---|---|---|---| | seed | string \| string[] | — | Text pre-filled on screen before anything runs — see Seeding | | title | string | — | Window title text | | watermark | string | — | Text or SVG markup pinned under the content |

Window chrome

| Option | Type | Default | | |---|---|---|---| | theme | Theme \| string | dark | Name or full Theme object | | template | 'macos' \| 'windows' \| 'minimal' | minimal, or macos for { raw } input | Window chrome | | borderRadius | number | 8 | | | borderColor | string | — | | | borderWidth | number | — | |

Dimensions

| Option | Type | Default | | |---|---|---|---| | width / height | number | auto | Omit to size from content (including the seed) | | fontSize | number | 14 | | | lineHeight | number | 1.4 | Multiplier, minimum 1 | | letterSpacing | number | 0 | | | fontFamily | string | system mono stack | | | padding | number | 16 | Inside the terminal window |

Background (outside the window)

| Option | Type | Default | | |---|---|---|---| | background | string \| Gradient | — | '#hex' or 'gradient(#a, #b[:horizontal\|vertical\|diagonal])' | | backgroundPadding | number \| string | 0 | CSS-style shorthand: 48, '40 64', '10 20 30 40' | | backgroundRadius | number | 12 | |

Header / footer

| Option | Type | | |---|---|---| | headerHeight, headerBackground | number, string | Header bar | | headerBorder, headerBorderColor, headerBorderWidth | boolean, string, number | Header rule | | footerHeight, footerBackground | number, string | Footer bar | | footerBorder, footerBorderColor, footerBorderWidth | boolean, string, number | Footer rule |

Cursor

| Option | Type | Default | | |---|---|---|---| | cursorStyle | 'block' \| 'bar' \| 'underline' | block | | | cursorColor | string | theme cursor | | | cursorBlink | boolean | true | |

Animation

| Option | Type | Default | | |---|---|---|---| | loop | boolean | true | | | loopStyle | 'loop' \| 'reverse' \| 'rewind' \| 'fade' | loop | | | loopPause | number | 0 | ms between cycles | | pauseAtEnd | number | 1000 | ms held on the last frame | | fadeDuration | number | 1500 | ms, fade style only | | rewindSpeed | number | 5 | multiplier, rewind style only | | playbackSpeed | number | 1 | 2 = 2×, 0.5 = half speed | | fps | number | — | Reserved. Frame timing comes from recorded timestamps; this is accepted but not yet applied |

Renderer

| Option | Type | Default | | |---|---|---|---| | smil | boolean | false | false = filmstrip, true = SMIL | | optimize | boolean | true | SVGO post-pass | | customGlyphs | boolean | true | Box-drawing characters as geometric shapes |

Callbacks

| Option | Signature | |---|---| | onFrame | (frame: TerminalFrame) => void | | onProgress | (current: number, total: number, description?: string) => void |


Result reference

const result = await dvd(input, options);

| Field | Type | | |---|---|---| | result.svg | string | The animated SVG | | result.metadata.duration | number | Total ms | | result.metadata.frameCount | number | Number of frames | | result.metadata.fps | number | Effective fps, derived from frame timestamps | | result.frames | TerminalFrame[] | Per-frame SVG + terminal state | | result.frameData | FrameData[] | Raw row spans — the input to a custom emitter | | result.emitter | EmitterOptions | Fully resolved render options: auto-detected size, resolved theme, defaulted font metrics. Feed this back into emit() or planVideo() to re-render single frames identically |


Steps reference

When using the programmatic-steps input, each entry conforms to one of these shapes:

| Type | Fields | Example | |---|---|---| | Type | text, optional speed (ms/char) | { type: 'Type', text: 'hello', speed: 50 } | | Key | key, optional count | { type: 'Key', key: 'Enter' } | | Sleep | duration (ms) | { type: 'Sleep', duration: 1000 } | | Shortcut | key + ctrl / alt / shift / cmd flags, optional count | { type: 'Shortcut', ctrl: true, key: 'c' } | | Screenshot | path | { type: 'Screenshot', path: 'frame.svg' } | | Copy / Paste | text (Copy only) | { type: 'Copy', text: 'hi' } |

Keys: Enter, Backspace, Tab, Space, Left, Right, Up, Down.

The parser also accepts Set, Env, Output, Require, Wait, Hide, Show and Source, but the executor does not act on them — Set only takes effect through a script string or a pre-parsed script, and the rest are currently inert.


Script settings reference

Settings usable as Set <Name> <value> inside a CD script string. Anything not listed is ignored.

| Group | Settings | |---|---| | Content | Seed, Title, Watermark, WatermarkStyle, PromptPrefix | | Size | Width, Height, FontSize, LineHeight, LetterSpacing, CharWidthRatio, Padding, Scroll | | Font | FontFamily, EmbedFont (path to a font file, inlined as base64) | | Chrome | Template, Theme, BorderColor, BorderWidth, BorderRadius | | Header | HeaderHeight, HeaderBackground, HeaderBorder, HeaderBorderColor, HeaderBorderWidth | | Footer | FooterHeight, FooterBackground, FooterBorder, FooterBorderColor, FooterBorderWidth | | Cursor | CursorStyle, CursorColor, CursorBlink | | Background | Background, BackgroundPadding, BackgroundRadius | | Timing | TypingSpeed, AnimationSpeed, PlaybackSpeed, LoopStyle, LoopPause, FadeDuration, RewindSpeed | | Shell | Shell, WorkingDirectory ($PWD or an absolute path) |

Values can be bare, "double quoted" (with \n, \t, \" escapes) or `backticked` (literal, may span lines). Options passed to dvd() override the script's own Set lines.


Examples

All runnable. Each writes its SVG into examples/svgs/.

npx ts-node -P tsconfig.dev.json examples/01-quick-start.ts
npx ts-node -P tsconfig.dev.json examples/02-programmatic-steps.ts
npx ts-node -P tsconfig.dev.json examples/03-raw-output.ts
npx ts-node -P tsconfig.dev.json examples/04-themes-gallery.ts
npx ts-node -P tsconfig.dev.json examples/05-loop-styles.ts
npx ts-node -P tsconfig.dev.json examples/06-branding.ts
npx ts-node -P tsconfig.dev.json examples/07-progress.ts
npx ts-node -P tsconfig.dev.json examples/08-low-level-api.ts
npx ts-node -P tsconfig.dev.json examples/09-templates.ts
npx ts-node -P tsconfig.dev.json examples/11-seed.ts

# or render them all in one go:
npx ts-node -P tsconfig.dev.json examples/10-render-all.ts

Comparison

| | dvdrw | VHS | asciinema | | ----------------- | :------------: | :----------: | :----------: | | Output | SVG | GIF / MP4 | asciicast | | Native API | TypeScript lib | CLI | JSON + player | | Dependencies | none | ffmpeg, ttyd | player embed | | Scalable | yes | no | yes | | GitHub README | perfect | works | embed only | | Editable | yes (XML) | no | yes (JSON) | | Offline | yes | yes | no | | Loop styles | 4 modes | basic | basic | | Programmatic | yes | limited | yes |


Related

  • dvd-cli — the CLI front-end (.cd scripts, pipe mode, rec / render sub-commands)
  • shellfie — terminal screenshots in code
  • shellfie-cli — terminal screenshots CLI
  • shellfied — terminal screenshots web service

License

MIT