przn
A terminal-based presentation tool written in Ruby. Renders Markdown slides with Kitty text sizing protocol support for beautifully scaled headings.
Installation
gem install przn
Usage
przn .md
To open the presentation directly at a specific slide, append @N (1-based):
przn your_slides.md @42
Out-of-range numbers are clamped to the last slide, so @9999 jumps to the end.
Extended-display presenter mode
przn --present your_slides.md
On a setup with a secondary display (projector / external monitor) and running inside Echoes, --present auto-spawns an audience window on the second display showing the clean current slide, while the laptop pane becomes the presenter view:
- Current slide rendered as normal
- Speaker notes (
{::note}/<note>markup) shown in a side strip — stripped from the audience view - Next slide's title hint
- Elapsed-time clock (or, when
rabbit:is themed, the runner-bar visualization)
If only one display is attached or Echoes isn't the host terminal, --present falls back to today's mirror mode with a one-line warning on stderr.
Implementation: the two przn processes coordinate over a Unix socket. The presenter forwards every slide navigation as a goto message; the audience renders and otherwise stays silent. Notes are not transmitted to the audience side.
PDF export
Two flavors:
przn --export your_slides.md # vector capture (default)
przn --export pdf your_slides.md
przn --export pdf -o output.pdf your_slides.md
przn --export prawn your_slides.md # Prawn (headless fallback)
przn --export prawn -o output.pdf your_slides.md
--export pdf (default) drives the live renderer for each slide and asks the terminal to save the rendered pane as a one-page vector PDF, then concatenates the per-slide PDFs into a single multi-page PDF. Output is an exact match of what's on screen — gradients, proportional fonts, OSC 66 sized text, custom bullets, all show up exactly as you'd see them — but vector, so the file stays small, scales infinitely, and text remains selectable. Requires running inside a terminal that implements the OSC 7772 capture command to a .pdf path (currently Echoes). The slides flicker through the visible pane during export.
--export prawn is the headless fallback: it renders the deck directly into a vector PDF via Prawn, without touching the terminal. Useful for CI or environments where Echoes isn't available, but diverges from the on-screen rendering for any feature the live renderer adds (OSC 66 sized text, OSC 7772 backgrounds, proportional fonts). Requires a TrueType font (with glyf outlines) for proper rendering — Prawn does not support CFF-based fonts (most .otf files). Fonts are auto-detected in this order: NotoSansJP TTF, HackGen, Arial Unicode.
Key bindings
| Key | Action |
|---|---|
→ ↓ l j Space |
Next slide |
← ↑ h k |
Previous slide |
g |
First slide |
G |
Last slide |
q Ctrl-C |
Quit |
Selecting and copying text
przn doesn't capture mouse events, so drag-to-select and the terminal's own copy shortcut (Kitty: Cmd+C on macOS, Ctrl+Shift+C on Linux) work normally on a slide. Mouse-tracking modes that may have leaked from a previously crashed program are explicitly disabled on entry, so drag selection is reliable.
Markdown format
przn's Markdown format is compatible with Rabbit's Markdown mode.
Slide splitting
Slides are separated by # (h1) headings.
# Slide 1
content
# Slide 2
more content
Text formatting
*emphasis*
**bold**
~~strikethrough~~
`inline code`
Long lines wrap at whitespace boundaries (not mid-word) for English-style text. A single word that's longer than the line — a URL, a class name — still wraps at the character it has to. CJK runs without inter-character whitespace fall back to per-character splitting.
Lists
* item 1
* item 2
* nested item
- also works as bullets
1. ordered
2. list
Code blocks
Fenced code blocks:
```ruby
puts "hello"
```
Indented code blocks (4 spaces) with optional kramdown IAL:
def hello
puts "world"
end
{: lang="ruby"}
Block quotes
> quoted text
> continues here
Tables
| Header 1 | Header 2 |
|----------|----------|
| cell 1 | cell 2 |
Definition lists
term
: definition
Text sizing
Uses Rabbit-compatible {::tag} notation. Supported size names: xx-small, x-small, small, large, x-large, xx-large, xxx-large, xxxx-large, and numeric 1-7.
{::tag name="x-large"}Big text{:/tag}
{::tag name="7"}Maximum size{:/tag}
An XML-style alternative is also accepted:
<size=x-large>Big text</size>
<size=7>Maximum size</size>
On Kitty-compatible terminals, sized text is rendered using the OSC 66 text sizing protocol. On other terminals, the markup is silently ignored.
Color
Named ANSI colors (red, green, yellow, blue, magenta, cyan, white, plus bright_* variants) and 6-digit hex. Use {::tag name="..."} (kramdown form) or the color attribute on <font> (see Font).
{::tag name="red"}warning{:/tag}
{::tag name="ff5555"}custom hex{:/tag}
<font color="red">warning</font>
<font color="ff5555">custom hex</font>
Font
HTML 4-style <font> tag with face, size, and color attributes. Any subset, in any order. The kramdown shape is also accepted.
<font face="Helvetica Neue">Title</font>
<font face="Menlo" size="3">code</font>
<font face="Menlo" size="3" color="red">flagged</font>
{::font name="Helvetica Neue"}Title{:/font}
face requires a terminal that honors the OSC 66 f= extension (e.g. Echoes). For PDF export, the family is registered with Prawn via fontconfig — families that can't be found fall through to the default font.
Alignment
{:.center}
centered text
{:.right}
right-aligned text
XML form (single-line, paragraph-level):
<center>centered <size=3>text</size></center>
<right>right-aligned</right>
Slide background
Set a per-slide background — solid color or linear gradient — via a self-closing block-level directive. Uses the Echoes OSC 7772 extension; other terminals ignore the escape sequence.
# Title
<bg color="#1a1a2e"/>
content...
# Second slide
<bg from="#1a1a2e" to="#16213e" angle="90"/>
content...
The previous slide's background is cleared on every navigation, and on przn exit, so your shell isn't left tinted.
Comments
{::comment}
This text is hidden from the presentation.
{:/comment}
Notes
Visible text {::note}(speaker note){:/note}
Visible text <note>(speaker note)</note>
Escaping <, >, &
To show literal markup characters that would otherwise be interpreted as a tag, use HTML-style entity references:
<note> renders as: <note>
2 < 3 renders as: 2 < 3
A & B renders as: A & B
A bare < not followed by a recognized tag name renders literally as well, so most accidental < characters are fine. The entities are only needed when you'd otherwise hit one of the tag patterns (<size=...>, <font ...>, <note>, <wait/>, <center>, <right>, <bg .../>).
Wait marker
Self-closing presentation flow marker, consumed at parse time:
{::wait/}
<wait/>
Theming
Theme resolution:
theme.ymlin the deck's directory — loaded automatically if present. No flag needed.--theme path/to/your.yml— overrides step 1 with any other file you point to.default_theme.yml(the file bundled with the gem) — used when neither of the above is found.
All keys are optional — anything you don't set falls back to the bundled defaults.
font:
family: # body text font; terminal: OSC 66 f=, PDF: Prawn font
size: 18 # base PDF font size in pt
color: # body text color; named ANSI or 6-digit hex
title: # h1 typography (slide titles)
family: # font family
size: # OSC 66 scale: numeric (1–7) or named (xx-small … xxxx-large); default x-large
color: # named ANSI or 6-digit hex
bullet: # unordered-list marker; also h2–h6 prefix
text: "・" # the glyph
size: # OSC 66 scale (1–7) for the bullet; default = body text's scale
background: # default slide background (Echoes OSC 7772)
color: # solid, e.g. "#1a1a2e"
from: # gradient endpoint
to: # gradient endpoint
angle: # gradient angle in degrees
# rabbit: # opt into the 🐇 / 🐢 bottom progress indicator
# duration: "30m" # "1h30m", "1800s", or plain integer seconds; turtle hides when unset
colors:
code_bg: "313244"
dim: "6c7086"
inline_code: "a6e3a1"
Notes:
font.color— deck-wide default text color (terminal: ANSI fg; PDF: Prawn fg). Inline<color=...>/<font color="...">runs still win per-segment.bullet—bullet.textis the character;bullet.sizeis the OSC 66 scale used to render it. Whenbullet.sizeis smaller than the body text scale, the bullet is rendered with fractional scaling and vertical centering so it still aligns with the body line.font.family— applied to body text (terminal: via OSC 66f=, requires Echoes; PDF: registered via fontconfig). Inline<font face="...">runs override it per-segment.title— h1 typography. Each attribute is independent fromfont:title.familydoes not inheritfont.family,title.colordoes not inheritfont.color.title.sizedefaults to x-large (OSC 66s=4). Whentitle.familyis proportional, every h1 OSC 66 sequence is emitted withh=2so a terminal that honors centered horizontal alignment (Echoes) keeps the title visually centered against its reserved cell block. h2–h6 stay body text.background— the deck-wide default background. A per-slide<bg .../>directive overrides it for that slide. The Prawn fallback paints the PDF page inbackground.colorwhen set; otherwise it leaves the page Prawn's default (white).rabbit— opt-in Rabbit-style bottom-row progress indicator. With the key absent, przn shows the simpleN / Mcounter at the bottom-right. With the key present, the bottom row becomes: current slide # at the very left, total at the very right, 🐇 running between them tracking slide progress. Setrabbit.durationto also show 🐢 tracking elapsed time against the goal; without a duration the turtle stays hidden. Inside Echoes the emojis are emitted via OSC 7772;multicellwithflip=hso they face rightward; outside Echoes they fall back to standard OSC 66 and render unflipped (left-facing).
License
The gem is available as open source under the terms of the MIT License.