Terminal-native Markdown slide decks, written in Go.
mddeck renders presentations directly in your terminal using character cells and ANSI styling. The source file is plain Markdown with minimal slide semantics — it remains readable as a normal document.
go install github.com/miskun/mddeck/cmd/mddeck@latestOr build from source:
git clone https://github.com/miskun/mddeck.git
cd mddeck
go build -o mddeck ./cmd/mddeck/# Audience mode (default)
mddeck slides.mddeck
# Presenter mode with timer, notes, and next-slide preview
mddeck --present slides.mddeck
# Watch for file changes and reload
mddeck --watch slides.mddeck
# Auto-advance slides every 30 seconds
mddeck --auto-advance 30s slides.mddeck
# Auto-advance with looping (kiosk mode)
mddeck --auto-advance 1m --loop slides.mddeckmddeck [flags] <file>
Both .mddeck and .md files are accepted — the parser operates on content, not file extension.
| Flag | Description |
|---|---|
--present, -p |
Start in presenter mode |
--theme <name> |
Override theme (default, dark, light) |
--safe-ansi |
Force safe ANSI mode (strip non-SGR sequences) |
--unsafe-ansi |
Disable safe ANSI mode |
--start <n> |
Start at slide number (1-based) |
--watch |
Reload on file change |
--auto-advance <dur> |
Auto-advance slides after duration (e.g. 30s, 1m, 1m30s) |
--loop |
Loop back to first slide when auto-advance reaches the end |
--version |
Show version |
| Flag | Description |
|---|---|
--dump |
Dump slide data to stdout and exit (no TUI) |
--format <fmt> |
Dump format: text (default) or json |
--slide <n> |
Dump only slide N (1-based, 0=all) |
--width <w> |
Virtual terminal width (0=auto) |
--height <h> |
Virtual terminal height (0=auto) |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Runtime error |
- Encoding: UTF-8
- Line endings: LF preferred, CRLF accepted and normalized automatically
A deck is a sequence of slides. Each slide contains:
- Slide body — a subset of Markdown
- Optional frontmatter — YAML metadata
- Optional speaker notes — hidden text for the presenter
A slide break is a line containing exactly --- with at least one blank line before and at least one blank line after.
This preserves compatibility with standard Markdown horizontal rules.
Valid slide break:
Some text.
---
# Next slideNot a slide break (no blank lines):
Some text.
---
# Same slide (this --- renders as a horizontal rule)When a file contains no --- slide breaks, mddeck automatically splits on headers instead (similar to patat):
- The deepest heading level used becomes the split level
- Each heading at or above that level starts a new slide
- Headings below the split level stay within the current slide
For example, if the deepest heading in the file is ##, then every # and ## starts a new slide, while ### and below remain part of the current slide.
A per-slide YAML frontmatter block (--- / YAML / ---) also starts a new slide, even in header-split mode. This lets you mix header-based splitting with layout-specific slides:
## Regular Slide
Content split by headers.
---
layout: title-cols-2
ratio: "50/50"
---
## Left Column
Left content.
## Right Column
Right content.
## Back to Normal
This slide splits on the header again.The parser automatically absorbs the correct number of headers based on region count. For example, title-body absorbs 2, title-cols-2 and title-rows-2 absorb 3, title-grid-4 absorbs 5. Single-region layouts (title, section, blank) absorb 1 header. Custom layouts compute regions as cols × rows.
To group multiple headings onto a single slide, use autosplit: false in per-slide frontmatter. All content is absorbed until the next frontmatter block:
---
autosplit: false
---
## All Heading Levels
# Heading 1
## Heading 2
### Heading 3
All on one slide.
---
autosplit: true
---
## Next Slide
Normal splitting resumes here.The autosplit: true block acts as a resume marker — it produces no visible slide itself.
If the file begins with ---, the first YAML block is interpreted as deck-level metadata.
---
title: "My Talk"
theme: "default"
wrap: true
tabSize: 2
slideWidth: 80
---| Field | Type | Default | Description |
|---|---|---|---|
title |
string | "" |
Deck title |
theme |
string | "default" |
Color theme |
wrap |
bool | true |
Enable paragraph wrapping |
tabSize |
int | 2 |
Tab expansion width |
slideWidth |
int | 80 |
Slide stage width in characters (see Slide Dimensions) |
slideHeight |
int | -1 (auto) |
Slide stage height in characters (see Slide Dimensions) |
safeAnsi |
bool | true |
Strip non-SGR ANSI sequences |
incrementalLists |
bool | true |
Reveal list items one at a time |
aspect |
string | "16:9" |
Target aspect ratio (e.g. "16:9", "4:3") |
padding |
object | all 1 |
Global padding for all layouts (see Padding) |
footer |
object | {} |
Footer bar configuration (see below) |
layouts |
map | {} |
Custom layout definitions |
Unknown fields are silently ignored.
Two parameters — slideWidth and slideHeight — control the content stage size. The stage is always centered in the terminal window. The footer sits outside the stage at full terminal width.
Each accepts three kinds of values:
| Value | Meaning |
|---|---|
> 0 |
Explicit size in characters |
0 |
Fill the terminal (no padding on that axis) |
-1 |
Auto-calculate from the other dimension + aspect ratio |
Default: slideWidth: 80, slideHeight: -1 (auto). This creates an 80-character-wide stage whose height is derived from the 16:9 aspect ratio.
Common configurations:
slideWidth: 80 # 80 chars wide, height from aspect (default)
slideWidth: 0 # fill terminal width, height from aspect
slideWidth: 100
slideHeight: 30 # explicit 100×30, aspect ignored
slideWidth: -1
slideHeight: 25 # height 25, width from aspect
slideWidth: 0
slideHeight: 0 # fill entire terminal, no paddingWhen both are -1 (auto), the slide fills the terminal constrained by the aspect ratio.
The footer bar spans the bottom row of each slide. Configure it with up to three sections:
---
footer:
left: "Company Name"
center: "Confidential"
right: "Custom text"
---| Field | Type | Default | Description |
|---|---|---|---|
left |
string | "" |
Left-aligned text |
center |
string | "" |
Centered text |
right |
string | slide counter | Right-aligned text (defaults to N / M) |
Individual slides may begin with YAML frontmatter (after a slide break).
---
layout: title-cols-2
ratio: "60/40"
align: top
---| Field | Type | Default | Description |
|---|---|---|---|
layout |
enum | "auto" |
Layout mode |
ratio |
string | "" |
Column ratio for title-cols-2 (e.g. "60/40") |
align |
enum | "top" |
Vertical alignment |
title |
string | "" |
Slide title |
class |
string | "" |
Style class |
autosplit |
bool | true |
Enable header-based splitting within this slide |
incrementalLists |
bool | inherited | Override deck-level incrementalLists for this slide |
Layout values: auto, title, section, title-body, title-cols-2, title-rows-2, title-grid-4, blank
Align values: top, middle, bottom
Unknown fields are silently ignored.
Speaker notes begin after a line containing exactly ???.
# Architecture
- Access boundary
- Authorization-aware AI
???
Mention Datadog comparison here.Rules:
???must be on its own line- Only the first
???per slide is recognized - Everything after
???belongs to notes - Notes are hidden in audience mode, shown in presenter mode
Slide content can be revealed incrementally with each keypress.
Insert a . . . line (three dots separated by spaces) to split a slide into reveal steps:
# Architecture
First point is visible immediately.
. . .
Second point appears on the next keypress.
. . .
Third point appears after another keypress.Blocks before the first . . . are visible immediately. Each . . . advances the step counter so subsequent blocks require additional keypresses to appear. The slide counter shows [step/total] during stepped slides.
By default, list items are revealed one at a time — each item requires a keypress. This applies to unordered, ordered, and task lists.
- First item visible immediately
- Second item on next keypress
- Third item on another keypressDisable this at the deck level:
---
incrementalLists: false
---Or override per slide:
---
incrementalLists: false
---When combined with . . . markers, incremental list items count as additional steps after the pause marker.
| Element | Syntax |
|---|---|
| Headings | #, ##, ### |
| Paragraphs | Plain text separated by blank lines |
| Unordered lists | - item or * item (supports nesting via indentation) |
| Ordered lists | 1. item, 2. item (supports nesting via indentation) |
| Task lists | - [ ] unchecked, - [x] checked |
| Blockquotes | > text |
| Alerts/Callouts | > [!NOTE], > [!TIP], > [!WARNING], > [!IMPORTANT], > [!CAUTION] |
| Tables | Pipe-delimited | col | col | with header separator |
| Fenced code blocks | ``` with optional language tag |
| Horizontal rules | ---, ***, ___ (when not a slide break) |
| Element | Syntax | Rendering |
|---|---|---|
| Bold | **text** |
Bold weight |
| Italic | *text* |
Italic style |
| Strikethrough | ~~text~~ |
Struck-through text |
| Inline code | `code` |
Colored text |
| Links | [text](url) |
Underlined accent text |
| Hard line break | Trailing \ or two spaces |
Forces a new line |
Indent list items by 2 spaces per level for nesting:
- Top level
- Second level
- Third levelUnordered lists use distinct bullets per depth (•, ◦, ▪). Ordered lists maintain per-depth numbering.
Trailing \ works as a hard line break within list items — the continuation text appears on a new line, indented under the bullet:
* **Bold heading** \
Continuation text on the next line.Pipe-delimited tables render with Unicode box-drawing characters:
| Feature | Status |
|---------|--------|
| Tables | Done |The separator row (|---|---|) is required between the header and body rows. Column widths auto-size to content and shrink proportionally when the terminal is narrow. Cells that overflow are truncated with an ellipsis (…).
Additional separator rows within the table body render as horizontal dividers:
| Name | Team |
|-------|---------|
| Alice | Alpha |
|-------|---------|
| Bob | Beta |To render a table without a bold header row, start with a separator line:
|-------|---------|
| Alice | Alpha |
| Bob | Beta |All rows render as plain text with no header emphasis.
GitHub-flavored alert syntax inside blockquotes:
> [!NOTE]
> Additional context the reader should know.
> [!WARNING]
> Something that could cause problems.Supported types: NOTE, TIP, IMPORTANT, WARNING, CAUTION. Each type renders with a distinct icon and color.
mddeck treats certain fenced code blocks as first-class art:
| Language tag | Type | Description |
|---|---|---|
ansi |
ANSI art | Colored text with escape sequences |
ascii |
ASCII art | Plain monospace art |
braille |
Braille art | Unicode braille patterns |
| Any other | Code block | Standard syntax-highlighted code |
```ansi
\033[32mPASS\033[0m All tests passed
\033[31mFAIL\033[0m Connection timeout
```In ansi blocks, these literal sequences are converted to actual escapes:
\033→ ESC\e→ ESC\x1b/\x1B→ ESC
Art blocks (ansi, ascii, braille) are nowrap by default. Overflow is cropped.
Default: safeAnsi = true
When safe mode is enabled, only ANSI SGR sequences (colors, bold, underline, reset) are allowed. All other sequences are stripped:
- Cursor movement
- Screen clearing
- OSC sequences (clipboard, hyperlinks)
- Alternate screen switching
When safeAnsi = false, additional sequences pass through, but the runtime never executes commands, accesses the filesystem, or performs network operations.
All layouts — built-in and custom — use the same grid engine. Built-in layouts are simply pre-defined grid configurations that save you from writing them out each time.
All built-in layouts use the same defaults: padding of 1 on all sides, gutterX of 2 (horizontal gap between columns), and gutterY of 1 (vertical gap between rows). No special-case overrides — they behave identically to custom layouts.
| Layout | Structure | Description |
|---|---|---|
auto |
(detected) | Automatically detected from content |
title |
1 region, centered | Centered title slide (h1 + optional subtitle) |
section |
1 region, centered | Section header (centered, like title) |
title-body |
title row + 1 body | Title row + single content area |
title-cols-2 |
title row + 2 cols | Title row + two columns |
title-rows-2 |
title row + 2 rows | Title row + two stacked rows |
title-grid-4 |
title row + 2×2 | Title row + four quadrants |
blank |
1 region, top-aligned | No title, single region, full space |
When layout: auto (the default), the layout is chosen as follows:
| Condition | Layout |
|---|---|
| No blocks | blank |
| H1 present + minimal text (≤3 blocks, headings + ≤1 paragraph) | title |
| No H1 + minimal text (≤3 blocks, headings + ≤1 paragraph) | section |
| Single code/art block (≤2 blocks) | blank |
| Two major blocks (top-level headings) | title-cols-2 |
| Otherwise | title-body |
The title-cols-2 layout defaults to a 50/50 column split. Override per slide:
---
layout: title-cols-2
ratio: "60/40"
---The default aspect ratio is 16:9. Override it in deck frontmatter:
---
aspect: "4:3"
---The aspect ratio controls centering of the content stage within the terminal. When the terminal is wider than the target ratio, horizontal centering padding (pillarbox) is added. When taller, vertical centering padding (letterbox) is added. Terminal character cells are approximately 1:2 (width:height), so the computation accounts for this cell ratio.
Layout padding (configurable via the padding field or per-layout padX/padY/padTop/padBottom/padLeft/padRight) is applied inside the content stage, independent of aspect ratio centering.
By default the area outside the slide content (centering margins from aspect ratio) uses the terminal's own background — the slide blends seamlessly into the terminal.
To visually distinguish the slide canvas from the surrounding terminal, set the PadBg field in a theme. This paints the area outside the content stage with a background color, making the slide boundary visible.
Built-in themes leave PadBg empty (transparent). To enable it, override the theme in code or define a custom theme with PadBg set to an ANSI background escape (e.g. ansi.BgRGB(28, 28, 34) for a subtle dark border).
Define custom grid layouts in deck frontmatter under layouts. Custom layouts use the exact same parameters as built-in layouts — columns, rows, gutterX, gutterY, padding.
---
layouts:
hero:
columns: [40, 60]
gutterX: 4
dashboard:
columns: [33, 34, 33]
rows: [60, 40]
---Reference a custom layout per slide:
---
layout: hero
---The parser automatically absorbs the correct number of headings per slide based on region count (cols × rows).
These fields apply to both custom layouts and built-in overrides:
| Field | Type | Default | Description |
|---|---|---|---|
columns |
[]int |
[100] |
Column widths as percentages |
rows |
[]int |
[100] |
Row heights as percentages |
gutterX |
int | 2 |
Horizontal gap between columns in characters |
gutterY |
int | 1 |
Vertical gap between rows in lines |
padX |
int | 2 |
Horizontal padding (sets both left and right) |
padY |
int | 1 |
Vertical padding (sets both top and bottom) |
padTop |
int | 1 |
Top padding |
padBottom |
int | 1 |
Bottom padding |
padLeft |
int | 2 |
Left padding |
padRight |
int | 2 |
Right padding |
align |
enum | "top" |
Content alignment within cells |
Padding controls the inset between the content stage and the layout regions. It is resolved in priority order (lowest to highest):
- Hard-coded default: top=1, bottom=1, left=2, right=2
- Deck-level
padding:in deck frontmatter (global override for all layouts) - Layout-level
padX/padYconvenience fields (set both sides at once) - Layout-level
padTop/padBottom/padLeft/padRight(most specific)
Deck-level global padding:
---
padding:
top: 2
bottom: 2
left: 3
right: 3
---Per-layout override (in deck frontmatter layouts: section):
layouts:
blank:
padTop: 0
padBottom: 0
title:
padX: 2
padTop: 0Regions are created in row-major order (left-to-right, top-to-bottom). Content blocks are distributed across regions by splitting at H1/H2 headings (major blocks) and assigning them round-robin.
Example: a columns: [50, 50] + rows: [50, 50] layout creates 4 regions. With 4 major blocks (## A, ## B, ## C, ## D), they map to:
| Region | Position | Content |
|---|---|---|
| 0 | top-left | ## A |
| 1 | top-right | ## B |
| 2 | bottom-left | ## C |
| 3 | bottom-right | ## D |
Override any built-in layout's parameters by defining it in your deck frontmatter. Unspecified fields keep their defaults:
---
layouts:
title-body:
padX: 10
padY: 3
title-cols-2:
gutterX: 4
---All styling uses ANSI SGR sequences:
- Title headings (on
title/sectionslides) — bold + title color (TitleStyle) - Slide title headings (title row on grid layouts) — bold + slide title color (SlideTitleStyle)
- Content headings (H1-H3 in body regions) — bold + accent color
- Content headings (H4-H6) — bold + body foreground
- Inline code — colored text
- Blockquotes — muted color +
│indicator - Lists — accent-colored bullets (
•) and numbers
- Paragraphs wrap when
wrap = true(default) - Lists use hanging indentation
- Art/code blocks never wrap
- Tabs expanded using
tabSize(default: 2) - Fenced blocks preserve whitespace exactly
When content exceeds its region:
- Horizontal overflow → truncated with
… - Vertical overflow → truncated with
↓indicator
No scaling in v1.
| Key(s) | Action |
|---|---|
Space, Enter, →, PgDn, n |
Next slide |
Backspace, ←, PgUp, p |
Previous slide |
| Home | First slide |
| End | Last slide |
| Key | Action |
|---|---|
t |
Toggle presenter mode |
? |
Toggle help overlay |
| Space (auto-advance) | Pause / resume auto-advance |
q, Ctrl+C |
Quit |
Displays:
- Current slide
- Next slide preview
- Speaker notes
- Elapsed timer
- Slide index (e.g.
3 / 18)
Three built-in themes:
| Theme | Description |
|---|---|
default |
Cyan accent on default background |
dark |
Steel blue accent for dark terminals |
light |
Blue accent for light terminals |
Themes define: base foreground/background, accent color, muted color, heading styles, title styles, and heading margins.
Override via CLI (--theme dark) or deck frontmatter (theme: "dark").
Slide titles use dedicated style tokens that are visually distinct from content headings:
| Token | Purpose | Fallback |
|---|---|---|
TitleStyle |
Main heading on title and section centered slides |
H1Style |
SlideTitleStyle |
Heading in the title row of grid layouts (title-body, title-cols-2, etc.) |
H2Style |
Content headings (H1-H3) use a uniform accent color (H1Style/H2Style/H3Style), while H4-H6 use bold body foreground.
Each heading level has a configurable margin-bottom (number of blank lines after the heading). All default to 1.
| Token | Purpose |
|---|---|
TitleMargin |
After main title on centered slides |
SlideTitleMargin |
After title-row heading on grid layouts |
H1Margin – H6Margin |
After content headings at each level |
| Condition | Behavior |
|---|---|
| YAML parse error | Exit non-zero with message |
| Unknown YAML keys | Silently ignored |
Invalid ratio |
Fallback to default (50/50) |
| Invalid UTF-8 | Replaced with � |
---
title: "My Talk"
footer:
left: "Acme Corp"
center: "Internal"
---
# Hello, World!
A terminal-native presentation.
???
Opening remarks go here.
## Key Features
- **Fast** — renders instantly
- **Portable** — runs in any terminal
- **Simple** — just Markdown
---
layout: title-cols-2
ratio: "50/50"
---
## Title
### Left Side
Content for the left column.
### Right Side
Content for the right column.
# Thank You!- HTML rendering
- Inline images (Kitty, Sixel, etc.)
- Embedded scripting
- Component DSLs
- Region markers (e.g.
:::left) - Fenced block options (e.g.
{fit=width}) - Scaling logic
- Animation
The .mddeck file must remain readable and meaningful as a normal Markdown document.
MIT