Documentation

Everything deckhand does, in detail. For a gentler intro, start with getting started.

Deck formats

Decks come in two flavors that share all their powers: a single markdown file with separators, or a JSON manifest that lays out one markdown file per slide.

Single-file decks

Two separators give the deck its two dimensions:

# first topic          ← column 1, top

---

# second topic         ← column 2, top

--

## the weeds           ← column 2, one level deeper

A deck can open with frontmatter carrying a title and theme:

---
title: my talk
theme:
  border_type: rounded
  accent: magenta
---

The leading block is only treated as frontmatter when it parses as a YAML mapping — a bare --- at the top still just separates columns.

Presenter notes

Everything after a bare ??? line is that slide's presenter notes: invisible when presenting, streamed to deckhand notes.

Embedded terminals

A fenced code block whose info string is terminal becomes a live, interactive PTY on the slide:

```terminal rows=10
python3 -q
```

Syntax highlighting

Fenced code blocks with a language tag get lightweight highlighting — keywords, strings, comments, numbers/constants, called functions, and Capitalized types — for rust, python, javascript/typescript, go, c/c++, sh, json, yaml, toml, and sql. Unknown languages render plain. Colors come from the theme (code_keyword, code_string, code_comment, code_literal, code_function, code_type), defaulting to a One Dark-flavored truecolor palette — RGB, so terminal color schemes can't remap it. It's a small built-in tokenizer, not a grammar engine, so the wasm presenter stays light and the browser gets the same colors.

QR codes

A fenced block whose info string is qr renders its body — a URL, some text — as a scannable QR code:

```qr
https://deckhand.sh
```

It draws black-on-white whatever the terminal palette, so phone cameras actually read it. Works right here in the browser presenter, too — try it in the playground.

Images as ASCII art

A markdown image alone on its own line renders as colored ASCII art, scaled to the slide and resolved against the deck's directory:

![the architecture](diagrams/arch.png)

PNG, JPEG, GIF, and WebP. Images referenced mid-sentence stay inline text, and the browser presenter shows a placeholder — pixels need a filesystem.

Side-by-side layout

A fenced block whose info string is row lays its cells out side-by-side, splitting the slide width equally; || on its own line separates cells. A cell stacks anything a slide can hold — markdown, code blocks, terminals, QR codes, images — except another row. Make the outer fence longer than any fence inside the cells:

````row
```qr
https://deckhand.sh
```
||
```terminal rows=10
python3 -q
```
````

Cells are top-aligned and the row is as tall as its tallest cell. rows=fill terminals inside a row fall back to their fixed height (default 12) — "the rest of the slide" isn't a height a side-by-side cell can claim.

Per-slide themes

A theme fence anywhere on a slide overrides the theme for that slide only (margins, width, borders, colors — the status bar keeps the deck theme):

```theme
margin: 0
accent: red
```

JSON manifests

For bigger talks, point deckhand at a manifest: deckhand deck.json.

{
  "title": "my talk",
  "theme": { "accent": "magenta" },
  "columns": [
    "slides/intro.md",
    ["slides/topic.md", "slides/topic-deep.md"],
    { "terminal": { "command": "htop" }, "title": "live demo" },
    { "file": "slides/fin.md", "notes_file": "slides/fin-notes.md" }
  ]
}
{ "panes": [
    "slides/context.md",
    { "terminal": { "command": "python3 -q", "rows": 8 } },
    { "terminal": {} }
] }

Theming

Layout, borders, and colors merge field by field from four sources, each later one winning:

  1. built-in defaults (solarized-safe indexed grays)
  2. ~/.config/deckhand/theme.json (or under $XDG_CONFIG_HOME) — your personal defaults, applies to every deck and the notes view
  3. the deck's theme — manifest "theme" or markdown frontmatter
  4. the slide's theme — manifest slide "theme" or a theme fence

All fields, all optional:

{
  "max_width": 84,
  "max_height": 30,
  "margin": 2,
  "margin_y": 1,
  "align_x": "center",
  "align_y": "top",
  "pin_title": true,
  "title_gap": 1,
  "border_type": "rounded",
  "accent": "magenta",
  "muted": 244,
  "term_border": "gray",
  "snapshot_border": "yellow",
  "status_bg": "#3a3a3a",
  "status_fg": 250,
  "h1_style": "banner",
  "h1": "yellow",
  "h2": "light-yellow",
  "bullet": "magenta",
  "quote": "green",
  "link": "light-blue",
  "inline_code": "yellow",
  "code_bg": 235,
  "code_fg": 252,
  "code_keyword": "#c678dd",
  "code_string": "#98c379",
  "code_comment": "#7f8490",
  "code_literal": "#d19a66",
  "code_function": "#61afef",
  "code_type": "#e5c07b",
  "qr_dark": "#102040",
  "qr_light": "#fdf6e3"
}

Colors take a name ("cyan", "light-blue"), hex ("#rrggbb"), or a 0–255 palette index. qr_dark/qr_light color QR blocks (modules / background, quiet zone included), defaulting to true black on true white — restyle at your own risk: scanners need a dark code on a light background with strong contrast, so dark navy on cream scans fine but an inverted or low-contrast pairing often won't. snapshot_border styles terminals showing a baked-in snapshot, superseding term_border for those — set it in a slide's theme fence to mark that slide's captures distinctly; unset, snapshots use term_border like any terminal. border_type is plain, rounded, double, or thick.

Layout

One content box, two axes, the same three questions per axis — and three rules: margins always win, caps bound everything drawn, alignment only places what's left.

axis air (min) cap (max) leftover space
x margin_x (default 2) max_width (default 96) align_x: left / center / right
y margin_y (default 0) max_height (default unlimited) align_y: top / center / bottom

margin sets both axes at once; the per-axis keys win over it. Units are terminal cells (columns / rows). max_height caps everything the slide draws — fill terminals and a pinned title included.

pin_title anchors a slide-leading heading to the top of the content area (i.e. at row margin_y), with title_gap blank rows (default 1) below it; the body lays out in the remaining space, still following align_y. That's how a title sits on the same row on every slide while the body stays centered.

h1_style: "banner" draws level-1 headings as large block glyphs, three rows tall (A–Z, 0–9, light punctuation — case-insensitive). A heading the font can't render, or that won't fit the width, falls back to a normal H1 — try it on the title slide in the playground.

Tip: prefer palette indexes (16–255) or hex over ANSI names for grays. Schemes like solarized remap the 16 ANSI colors, turning ANSI "dark gray" nearly invisible — deckhand's own defaults use indexed grays for exactly this reason.

Keys

key action
/h /l previous / next column
/j /k deeper / shallower
space, n next slide (walks every column depth-first)
shift-space, backspace, p previous slide
g, G first / last column
o overview — type a slide's jump code, or hjkl + enter, or click
19 select a terminal pane (marked with ▸)
t, enter run (after y) / focus the selected terminal
Ctrl-q release terminal focus
shift-pgup/pgdn scroll terminal history (also shift-↑/)
s / R stop / restart the selected terminal
? help
q quit

While a terminal is focused, every key except Ctrl-q and the shift-scroll keys goes to the PTY. The mouse works too: click a terminal to focus it, click outside to release, scroll-wheel through history, click a slide in the overview to jump.

On slides with several terminals, each pane shows its number and ▸ marks the one t will focus.

Live reload

Local decks live-reload while presenting: deckhand watches the deck file — and for manifests, every referenced slide and notes file, plus your user theme — and hot-swaps the deck when anything changes. Your position is kept (clamped if the deck shrank), live terminals survive when the deck's terminal blocks are unchanged, and a mid-edit parse error keeps the current deck with the error shown on the status bar. Disable with --no-watch; remote decks aren't watched.

Presenter notes over a socket

deckhand present listens on a unix socket (default $TMPDIR/deckhand.sock) and broadcasts the current slide, its notes, the next slide's title, and a talk timer. deckhand notes renders that from any other terminal and reconnects automatically.

Running two decks at once? Give each a socket:

deckhand present talk.md --socket /tmp/talk.sock
deckhand notes --socket /tmp/talk.sock

Presenting from URLs and gists

Any deck argument can be a URL:

deckhand https://example.com/talks/deck.md
deckhand https://gist.github.com/you/abc123

The play page does the same resolution in your browser.

Compiling to one file

deckhand compile flattens any deck — including manifests and remote decks — into a single markdown file that presents identically:

deckhand compile deck.json -o talk.md
deckhand compile https://gist.github.com/you/abc123 -o talk.md

The deck title and deck theme are preserved as frontmatter, per-slide themes as theme fences. Only per-slide title overrides have no single-file syntax; bare ---/-- lines inside slide bodies are rewritten to *** so they can't split slides on re-parse.

Snapshots: real terminal output on the web

If a deck has terminal blocks, compiling requires choosing --snapshots or --no-snapshots — capturing executes the deck's commands on your machine, so deckhand won't guess. With --snapshots, each block runs in a real PTY, output settles (--snapshot-wait-ms, default 1500), and the screen is captured into the compiled file at --snapshot-cols width (default 80). --snapshot-root picks the working directory captures run in — it controls what shell prompts display — defaulting to the deck's directory. The capture lives inside the terminal fence as a %%snapshot COLSxROWS marker plus base64-encoded ANSI.

Contexts that can't spawn PTYs — this site's presenter, the playground, play — replay the capture through the same terminal emulator the native TUI uses, colors and all, marked with a snapshot hint on the border. Presenting natively always runs the command live and ignores the snapshot.

Architecture: presenters and providers

The presentation core (navigation, layout, theming, drawing) is platform-independent; each context plugs in a terminal provider that supplies the live innards of terminal blocks. The native TUI's provider spawns real PTYs; the browser build on this site provides placeholders. The TerminalProvider trait is public — a provider could just as well run commands over ssh, in a container, or replay a recording.

Limitations