przn 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +496 -66
- data/default_theme.yml +98 -18
- data/exe/przn +8 -2
- data/lib/przn/audience_link.rb +3 -3
- data/lib/przn/code_highlighter.rb +165 -0
- data/lib/przn/controller.rb +506 -14
- data/lib/przn/echoes_client.rb +20 -0
- data/lib/przn/file_watcher.rb +69 -0
- data/lib/przn/image_util.rb +65 -2
- data/lib/przn/kitty_text.rb +2 -1
- data/lib/przn/mermaid_renderer.rb +88 -0
- data/lib/przn/parser.rb +417 -35
- data/lib/przn/prawn_pdf_exporter.rb +133 -15
- data/lib/przn/presentation.rb +43 -0
- data/lib/przn/presenter_renderer.rb +22 -8
- data/lib/przn/renderer.rb +2411 -305
- data/lib/przn/screenshot_pdf_exporter.rb +173 -5
- data/lib/przn/slide.rb +4 -2
- data/lib/przn/terminal.rb +10 -0
- data/lib/przn/theme.rb +85 -10
- data/lib/przn/version.rb +1 -1
- data/lib/przn.rb +11 -5
- data/sample/ruby-logo-LICENSE +15 -0
- data/sample/ruby.jpg +0 -0
- data/sample/ruby.png +0 -0
- data/sample/sample.md +285 -28
- data/sample/shapes.md +55 -0
- metadata +91 -77
- data/sample/doge.jpg +0 -0
- data/sample/doge.png +0 -0
data/README.md
CHANGED
|
@@ -3,30 +3,34 @@
|
|
|
3
3
|
A terminal-based presentation tool written in Ruby.
|
|
4
4
|
Renders Markdown slides with [Kitty text sizing protocol](https://sw.kovidgoyal.net/kitty/text-sizing-protocol/) support for beautifully scaled headings.
|
|
5
5
|
|
|
6
|
+
For the full feature set, it is **strongly recommended** to run przn inside [Echoes](https://github.com/amatsuda/echoes). Features that depend on Echoes' OSC 7772 extensions, proportional-font measurement, or vector-capture pipeline are marked _(Echoes only)_ below; everything else works on any Kitty-graphics-compatible terminal (silently degrading where the protocol isn't available).
|
|
7
|
+
|
|
6
8
|
## Installation
|
|
7
9
|
|
|
8
10
|
```
|
|
9
|
-
gem install przn
|
|
11
|
+
% gem install przn
|
|
10
12
|
```
|
|
11
13
|
|
|
12
14
|
## Usage
|
|
13
15
|
|
|
14
16
|
```
|
|
15
|
-
przn your_slides.md
|
|
17
|
+
% przn your_slides.md
|
|
16
18
|
```
|
|
17
19
|
|
|
18
20
|
To open the presentation directly at a specific slide, append `@N` (1-based):
|
|
19
21
|
|
|
20
22
|
```
|
|
21
|
-
przn your_slides.md @42
|
|
23
|
+
% przn your_slides.md @42
|
|
22
24
|
```
|
|
23
25
|
|
|
24
26
|
Out-of-range numbers are clamped to the last slide, so `@9999` jumps to the end.
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
The repository ships with `sample/sample.md` — a 30-slide demo deck that exercises every feature documented below (slide splitting, lists, code blocks, tables, sized text, colours, fonts, alignment, backgrounds, absolute-position text, images, shapes, layouts, step builds, actions, animation, comments, notes). Clone the repo and run `przn sample/sample.md` to page through it; each section below has a slide you can jump to with `@N` for hands-on reference.
|
|
29
|
+
|
|
30
|
+
### Extended-display presenter mode _(Echoes only)_
|
|
27
31
|
|
|
28
32
|
```
|
|
29
|
-
przn --present your_slides.md
|
|
33
|
+
% przn --present your_slides.md
|
|
30
34
|
```
|
|
31
35
|
|
|
32
36
|
On a setup with a secondary display (projector / external monitor) and running inside [Echoes](https://github.com/amatsuda/echoes), `--present` auto-spawns an **audience window** on the second display showing the clean current slide, while the laptop pane becomes the **presenter view**:
|
|
@@ -34,7 +38,7 @@ On a setup with a secondary display (projector / external monitor) and running i
|
|
|
34
38
|
- Current slide rendered as normal
|
|
35
39
|
- Speaker notes (`{::note}` / `<note>` markup) shown in a side strip — stripped from the audience view
|
|
36
40
|
- Next slide's title hint
|
|
37
|
-
- Elapsed-time clock (or, when `
|
|
41
|
+
- Elapsed-time clock (or, when `counter.duration` is set in the theme, the runner-bar visualization)
|
|
38
42
|
|
|
39
43
|
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.
|
|
40
44
|
|
|
@@ -42,18 +46,24 @@ Implementation: the two `przn` processes coordinate over a Unix socket. The pres
|
|
|
42
46
|
|
|
43
47
|
### PDF export
|
|
44
48
|
|
|
45
|
-
Two flavors:
|
|
46
|
-
|
|
47
49
|
```
|
|
48
|
-
przn --export your_slides.md
|
|
49
|
-
przn --export pdf your_slides.md
|
|
50
|
-
przn --export pdf -o output.pdf your_slides.md
|
|
51
|
-
|
|
52
|
-
przn --export prawn your_slides.md # Prawn (headless fallback)
|
|
53
|
-
przn --export prawn -o output.pdf your_slides.md
|
|
50
|
+
% przn --export your_slides.md
|
|
51
|
+
% przn --export -o output.pdf your_slides.md
|
|
54
52
|
```
|
|
55
53
|
|
|
56
|
-
**`--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
|
|
54
|
+
**`--export pdf`** (default) _(Echoes only)_ 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 text remains selectable and scales infinitely. Requires running inside a terminal that implements the OSC 7772 `capture` command to a `.pdf` path (currently [Echoes](https://github.com/amatsuda/echoes)). The slides flicker through the visible pane during export.
|
|
55
|
+
|
|
56
|
+
Echoes embeds `<img>` content at its source resolution (a 24-megapixel phone photo lands in the PDF as 24 megapixels), so image-heavy decks balloon. If [Ghostscript](https://ghostscript.com/) is on `$PATH` przn pipes the merged file through `gs -dPDFSETTINGS=…` to downsample-and-recompress those rasters as JPEG — typically 5-20× smaller on photo-bearing decks. Tune with `--pdf-quality`:
|
|
57
|
+
|
|
58
|
+
| Value | gs preset | Use for |
|
|
59
|
+
|-------|-----------|---------|
|
|
60
|
+
| `lossless` | (skip gs) | Image fidelity matters more than file size; keeps Echoes' originals byte-for-byte |
|
|
61
|
+
| `low` | `/screen` (~72 DPI) | Email-friendly, presentations viewed on phones |
|
|
62
|
+
| `medium` (default) | `/ebook` (~150 DPI) | General-purpose; visually indistinguishable at projection distances |
|
|
63
|
+
| `high` | `/printer` (~300 DPI) | Print handouts |
|
|
64
|
+
| `max` | `/prepress` | Maximum fidelity, color preservation |
|
|
65
|
+
|
|
66
|
+
Without `gs` on `$PATH` the file is left at its lossless-only size and a one-line install hint is printed on stderr (`brew install ghostscript` on macOS). Set `PRZN_GS=/custom/path/gs` to point at a non-default install, or `PRZN_GS=` (empty) / `--pdf-quality lossless` to opt out explicitly.
|
|
57
67
|
|
|
58
68
|
**`--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.
|
|
59
69
|
|
|
@@ -61,19 +71,24 @@ przn --export prawn -o output.pdf your_slides.md
|
|
|
61
71
|
|
|
62
72
|
| Key | Action |
|
|
63
73
|
|-----|--------|
|
|
64
|
-
| `→` `↓` `l` `j` `Space` | Next slide |
|
|
65
|
-
| `←` `↑` `h` `k` | Previous slide |
|
|
74
|
+
| `→` `↓` `l` `j` `Space` / left-click | Next slide |
|
|
75
|
+
| `←` `↑` `h` `k` / right-click | Previous slide |
|
|
66
76
|
| `g` | First slide |
|
|
67
77
|
| `G` | Last slide |
|
|
78
|
+
| `r` | Reload the deck (and `theme.yml`) from disk, keeping the current slide index |
|
|
68
79
|
| `q` `Ctrl-C` | Quit |
|
|
69
80
|
|
|
70
|
-
|
|
81
|
+
Clicks step through `<wait/>` builds exactly like the keys. With the 🐇 / 🐢 runner bar on (`counter.duration`), grab the 🐇 and drag it along the bottom row to scrub through the deck — the slide follows the rabbit as you drag. With mouse reporting on, most terminals still let you select text by holding a modifier while dragging (Shift in kitty / Ghostty / xterm, Option in iTerm2).
|
|
82
|
+
|
|
83
|
+
### Hot reload
|
|
71
84
|
|
|
72
|
-
`
|
|
85
|
+
The deck file and the sibling `theme.yml` are watched in the background; saving either re-renders automatically. A source save lands you on the slide you just edited (the first chunk whose markdown differs from the prior snapshot); a theme save re-renders in place. Bad syntax mid-edit is swallowed silently — the previous slide stays on screen until the next save parses cleanly. Pass `--no-watch` to disable; the manual `r` binding still works in either mode.
|
|
73
86
|
|
|
74
87
|
## Markdown format
|
|
75
88
|
|
|
76
|
-
przn's Markdown format is
|
|
89
|
+
przn's Markdown format is based on [Rabbit](https://rabbit-shocker.org/)'s Markdown mode with some extensions with HTML-ish tags.
|
|
90
|
+
|
|
91
|
+
**HTML-ish tag attributes** — every `<tag attr=value>` block below (`<bg>`, `<at>`, `<img>`, `<font>`) accepts three value forms: double-quoted `attr="value"`, single-quoted `attr='value'`, and unquoted `attr=value` (HTML5-ish — anything that isn't whitespace, `=`, `<`, `>`, a quote, or backtick). Self-closing tags need a space before `/>` when the last attribute is unquoted (`<img src=foo.png />`).
|
|
77
92
|
|
|
78
93
|
### Slide splitting
|
|
79
94
|
|
|
@@ -115,15 +130,17 @@ Long lines wrap at whitespace boundaries (not mid-word) for English-style text.
|
|
|
115
130
|
|
|
116
131
|
### Code blocks
|
|
117
132
|
|
|
118
|
-
Fenced code blocks:
|
|
133
|
+
Fenced code blocks render on a dim gray background and are syntax-highlighted via [rouge](https://github.com/rouge-ruby/rouge) when a language is set on the fence — Ruby, Python, JavaScript, Go, Rust, HTML, JSON, YAML, shell, and ~200 others all work:
|
|
119
134
|
|
|
120
135
|
````markdown
|
|
121
136
|
```ruby
|
|
122
|
-
|
|
137
|
+
def hello
|
|
138
|
+
puts "world" # greet
|
|
139
|
+
end
|
|
123
140
|
```
|
|
124
141
|
````
|
|
125
142
|
|
|
126
|
-
Indented code blocks (4 spaces) with optional kramdown IAL:
|
|
143
|
+
Indented code blocks (4 spaces) with an optional kramdown IAL pick up highlighting too:
|
|
127
144
|
|
|
128
145
|
```markdown
|
|
129
146
|
def hello
|
|
@@ -132,6 +149,65 @@ Indented code blocks (4 spaces) with optional kramdown IAL:
|
|
|
132
149
|
{: lang="ruby"}
|
|
133
150
|
```
|
|
134
151
|
|
|
152
|
+
Blocks without a language fall back to the same gray-on-dim plain text. The color scheme is fixed (tuned for the dim background) — comments dim, strings green, keywords cyan, numbers magenta, function / type names yellow / green. ` ```diff ` / ` ```patch ` blocks follow `git diff`: removed lines red, added lines green, file headers yellow.
|
|
153
|
+
|
|
154
|
+
A kramdown-style IAL right after the language overrides the theme's `code.*` defaults for that one block. Most useful for shrinking a long snippet down to a smaller size without touching the deck-wide default:
|
|
155
|
+
|
|
156
|
+
````markdown
|
|
157
|
+
```ruby {size=1}
|
|
158
|
+
class Slide
|
|
159
|
+
attr_reader :title, :body, :notes
|
|
160
|
+
def initialize(title:, body: "", notes: [])
|
|
161
|
+
# …
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
````
|
|
166
|
+
|
|
167
|
+
`size` accepts the same forms as `<size=…>` (numeric `1`–`7` or named `xx-small` … `xxxx-large`). `family`, `color`, and `bg` are also recognized and override `theme.code.family` / `theme.code.color` / `theme.code.bg` for that one block. Multi-attr forms work with either separator: `{size: small, family: Menlo}` and `{size=2, color=cyan}` both parse.
|
|
168
|
+
|
|
169
|
+
**Per-line stepping via `lines=…`** _(Echoes only for the dim effect)_ — pipe-separated step groups walk through which line range is highlighted as the user advances:
|
|
170
|
+
|
|
171
|
+
````markdown
|
|
172
|
+
```ruby {lines=1-2|3|4}
|
|
173
|
+
class Slide
|
|
174
|
+
attr_reader :title, :body
|
|
175
|
+
def initialize(title:, body: "")
|
|
176
|
+
@title, @body = title, body
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
````
|
|
181
|
+
|
|
182
|
+
On the slide's initial reveal, lines 1-2 are at full opacity and the rest are dimmed (via OSC 7772 `cell-alpha`); space steps the highlight to line 3, then line 4. Each pipe-separated extra group adds a synthetic step to the slide (the deck's normal step counter walks them with the existing `<wait/>` infrastructure). Comma-separated ranges inside a group need quoting because `,` is the multi-attr separator at the IAL level: `lines="1-2,4|3,5"`. `lines=all` or omitting the directive turns dimming off. Non-Echoes terminals drop the OSC silently — the steps still tick but every line renders at full opacity.
|
|
183
|
+
|
|
184
|
+
### Mermaid diagrams
|
|
185
|
+
|
|
186
|
+
A `mermaid` fence renders to an inline image via the [mermaid CLI](https://github.com/mermaid-js/mermaid-cli):
|
|
187
|
+
|
|
188
|
+
````markdown
|
|
189
|
+
```mermaid
|
|
190
|
+
graph TD
|
|
191
|
+
A[Idea] --> B{Demo?}
|
|
192
|
+
B -->|yes| C[Run live]
|
|
193
|
+
B -->|no| D[Static slide]
|
|
194
|
+
```
|
|
195
|
+
````
|
|
196
|
+
|
|
197
|
+
The rendered PNG is cached for the session in a tmpdir keyed by SHA256 of the source, so the same diagram repeated across slides (or after a reload) shells out exactly once. Per-block IAL sizes the placement — `height` / `width` / `relative_height` / `relative_width` work the same as on `<img>`:
|
|
198
|
+
|
|
199
|
+
````markdown
|
|
200
|
+
```mermaid {height=70%}
|
|
201
|
+
…
|
|
202
|
+
```
|
|
203
|
+
````
|
|
204
|
+
|
|
205
|
+
Requirements:
|
|
206
|
+
- `mmdc` on `PATH` — install via `npm i -g @mermaid-js/mermaid-cli`.
|
|
207
|
+
- Puppeteer's headless Chrome — `mmdc` shells out to it. If you see a "Could not find Chrome" error, run `npx puppeteer browsers install chrome-headless-shell` once.
|
|
208
|
+
|
|
209
|
+
The diagram is rendered with `-b transparent` so the slide background (theme bg / gradient / `<bg>`) shows through. If `mmdc` is missing or fails, the source falls back to a plain fenced block so the slide still shows the diagram text.
|
|
210
|
+
|
|
135
211
|
### Block quotes
|
|
136
212
|
|
|
137
213
|
```markdown
|
|
@@ -156,46 +232,41 @@ term
|
|
|
156
232
|
|
|
157
233
|
### Text sizing
|
|
158
234
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
```markdown
|
|
162
|
-
{::tag name="x-large"}Big text{:/tag}
|
|
163
|
-
{::tag name="7"}Maximum size{:/tag}
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
An XML-style alternative is also accepted:
|
|
235
|
+
Supported size names: `xx-small`, `x-small`, `small`, `large`, `x-large`, `xx-large`, `xxx-large`, `xxxx-large`, and numeric `1`-`7`.
|
|
167
236
|
|
|
168
237
|
```markdown
|
|
169
238
|
<size=x-large>Big text</size>
|
|
170
239
|
<size=7>Maximum size</size>
|
|
171
240
|
```
|
|
172
241
|
|
|
242
|
+
Rabbit-compatible kramdown form is also accepted: `{::tag name="x-large"}Big text{:/tag}`.
|
|
243
|
+
|
|
173
244
|
On [Kitty](https://sw.kovidgoyal.net/kitty/)-compatible terminals, sized text is rendered using the OSC 66 text sizing protocol. On other terminals, the markup is silently ignored.
|
|
174
245
|
|
|
175
246
|
### Color
|
|
176
247
|
|
|
177
|
-
Named ANSI colors (`red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, plus `bright_*` variants) and 6-digit hex
|
|
248
|
+
Named ANSI colors (`red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, plus `bright_*` variants) and 6-digit hex (with or without a leading `#`). The value is forgiving about quoting — bare, single-quoted, or double-quoted — and the same engine resolves color names for the standalone `<color>` tag and for the `color` attribute on `<font>`, so the two stay in sync.
|
|
178
249
|
|
|
179
250
|
```markdown
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
<font color="red">warning</font>
|
|
184
|
-
<font color="ff5555">custom hex</font>
|
|
251
|
+
<color=red>warning</color>
|
|
252
|
+
<color=ff5555>custom hex</color>
|
|
253
|
+
<color="#ff5555">CSS-style hex</color>
|
|
185
254
|
```
|
|
186
255
|
|
|
256
|
+
For combined styling, the `color` attribute on `<font>` works too (see [Font](#font)). Rabbit-compatible kramdown form is also accepted: `{::tag name="red"}warning{:/tag}`.
|
|
257
|
+
|
|
187
258
|
### Font
|
|
188
259
|
|
|
189
|
-
|
|
260
|
+
HTML4-style `<font>` tag with `face`, `size`, and `color` attributes. Any subset, in any order. The `face` attribute is _(Echoes only)_ — other terminals silently ignore it and fall back to the body font.
|
|
190
261
|
|
|
191
262
|
```markdown
|
|
192
263
|
<font face="Helvetica Neue">Title</font>
|
|
193
264
|
<font face="Menlo" size="3">code</font>
|
|
194
265
|
<font face="Menlo" size="3" color="red">flagged</font>
|
|
195
|
-
|
|
196
|
-
{::font name="Helvetica Neue"}Title{:/font}
|
|
197
266
|
```
|
|
198
267
|
|
|
268
|
+
Rabbit-compatible kramdown form is also accepted: `{::font name="Helvetica Neue"}Title{:/font}` (`name` maps to `face`).
|
|
269
|
+
|
|
199
270
|
`face` requires a terminal that honors the OSC 66 `f=` extension (e.g. [Echoes](https://github.com/amatsuda/echoes)). For PDF export, the family is registered with Prawn via fontconfig — families that can't be found fall through to the default font.
|
|
200
271
|
|
|
201
272
|
### Alignment
|
|
@@ -217,7 +288,7 @@ XML form (single-line, paragraph-level):
|
|
|
217
288
|
|
|
218
289
|
### Slide background
|
|
219
290
|
|
|
220
|
-
Set a per-slide background — solid color
|
|
291
|
+
Set a per-slide background — solid color, linear gradient, or image — via a self-closing block-level directive. Solid color and gradient backgrounds are _(Echoes only)_; image backgrounds work on any Kitty-graphics terminal.
|
|
221
292
|
|
|
222
293
|
```markdown
|
|
223
294
|
# Title
|
|
@@ -231,12 +302,198 @@ content...
|
|
|
231
302
|
<bg from="#1a1a2e" to="#16213e" angle="90"/>
|
|
232
303
|
|
|
233
304
|
content...
|
|
305
|
+
|
|
306
|
+
# Third slide
|
|
307
|
+
|
|
308
|
+
<bg image="cover.png"/>
|
|
309
|
+
|
|
310
|
+
content layered on top of the image...
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
- `color` / `from` / `to` / `angle` use [Echoes](https://github.com/amatsuda/echoes)' OSC 7772 extension; other terminals ignore the escape sequence.
|
|
314
|
+
- `image` (PNG, path relative to the deck) uses the Kitty Graphics Protocol at `z: -1` so text and `<img>` content layer on top. Works on every kitty-graphics terminal (Kitty, Ghostty, Wezterm, Echoes…); silently no-ops elsewhere. `image` wins when set alongside `color` / `from` / `to`.
|
|
315
|
+
- For a deck-wide default, set `background.image:` (or `color:` / gradient keys) in `theme.yml`.
|
|
316
|
+
|
|
317
|
+
The previous slide's background — color, gradient, and image placement — is cleared on every navigation, and on `przn` exit, so your shell isn't left tinted or covered.
|
|
318
|
+
|
|
319
|
+
### Absolute-position text
|
|
320
|
+
|
|
321
|
+
Place text at an arbitrary `(column, row)` on the slide, escaping the normal top-down paragraph flow:
|
|
322
|
+
|
|
323
|
+
```markdown
|
|
324
|
+
# Layout test
|
|
325
|
+
|
|
326
|
+
<at x="10" y="5">top-left ish</at>
|
|
327
|
+
<at x="40" y="15"><size=3>BIG</size></at>
|
|
328
|
+
<at x="80" y="25"><color=red>warn</color></at>
|
|
329
|
+
<at x="50%" y="50%">dead center</at>
|
|
234
330
|
```
|
|
235
331
|
|
|
236
|
-
|
|
332
|
+
Rabbit-compatible kramdown form is also accepted: `{::at x="10" y="20"}content{:/at}`.
|
|
333
|
+
|
|
334
|
+
- `x` / `y` accept five forms:
|
|
335
|
+
- **Plain integer** — 1-based terminal cells (cells are the default for `<at>` since it places text). `x="1" y="1"` is the very top-left of the slide pane.
|
|
336
|
+
- **`Nc`** — same as plain integer; the `c` is explicit cells, useful when sitting next to an `<img>` that defaults the other way.
|
|
337
|
+
- **`Npx`** — pixels. Converted to the cell containing that pixel (text can only land on cell boundaries, so a px value is rounded to its enclosing cell).
|
|
338
|
+
- **`N%`** — percent of the terminal's current width / height. Auto-adjusts when the pane is resized.
|
|
339
|
+
- **Alignment keywords** — `x="left"` / `"center"` / `"right"`, `y="top"` / `"center"` / `"bottom"` (`middle` is accepted as a synonym of `center` on both axes). The position resolves against the content's own rendered footprint — `x="center"` measures the widest `<br>`-split line at its actual rendered cell width and centres the whole block; `y="bottom"` sums each line's height (respecting inline `<size>` / `<font size>`) and pins the last line to the bottom edge. Mix and match: `<at x="center" y="bottom">caption</at>` drops the caption centered on the bottom row.
|
|
340
|
+
- Content is parsed inline, so all the usual styling works inside an `<at>` — `<size>`, `<color>`, `<font>`, `**bold**`, `*italic*`, etc.
|
|
341
|
+
- The block doesn't take up vertical space in the slide's layout — paragraphs around it render in their normal positions and the absolute placement layers on top. Useful for overlaying labels on a `<bg .../>` gradient or pinning annotations to specific cells.
|
|
342
|
+
- Out-of-range coordinates clamp into the visible area; missing / unparseable coordinates skip silently.
|
|
343
|
+
|
|
344
|
+
### Image
|
|
345
|
+
|
|
346
|
+
Embed an image with the standard markdown form, or the `<img>` XML form when you want to absolute-position it. Both produce identical output — `<img>` just opens the door to extra attributes like `x` / `y`.
|
|
347
|
+
|
|
348
|
+
```markdown
|
|
349
|
+

|
|
350
|
+
<img src="ruby.png"/>
|
|
351
|
+
|
|
352
|
+
<img src="ruby.png" relative_height="70"/>
|
|
353
|
+
<img src="ruby.png" x="200" y="100" relative_height="40"/> <!-- 200 px from left, 100 px from top -->
|
|
354
|
+
<img src="ruby.png" x="5c" y="3c" relative_height="40"/> <!-- cell column 5, row 3 -->
|
|
355
|
+
<img src="ruby.png" x="50%" y="50%" height="40%"/>
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
- `src` is required; `alt` and `title` are accepted and ignored at render time (kept for accessibility / future use).
|
|
359
|
+
- By default the image renders at its **intrinsic size** — no auto-shrink to fit. If it's bigger than the visible pane, the overflowing edge is clipped (same as a tall list of paragraphs scrolling off the bottom).
|
|
360
|
+
- `relative_height="N"` caps the image at N % of the terminal height (no default — without it, intrinsic size). Aspect ratio is preserved. `relative_width="N"` is the same for the horizontal dimension. Caps shrink, never grow.
|
|
361
|
+
- `height="N%"` / `width="N%"` are short-form aliases for `relative_height` / `relative_width` (both forms — `<img>` and `![]{:...}` — accept the alias). An explicit `relative_*` on the same block wins.
|
|
362
|
+
- `height="N"` / `width="N"` (plain integer, with optional `px` suffix) target an exact pixel size on that axis — aspect ratio is preserved, and the other axis is derived from it. Unlike the `relative_*` caps, pixel values can scale the image **up** past intrinsic size as well as down. Setting both pixel attrs fits the image inside the smaller of the two scales. `relative_*` caps still apply on top of a pixel target (`width="500" relative_width="40"` shrinks the 500-pixel result if it would exceed 40 % of the terminal).
|
|
363
|
+
- `x` / `y` (optional) anchor the image's top-left. Same suffix vocabulary as [`<at>`](#absolute-position-text), but `<img>` flips the default: a bare number means **pixels**, since that's the image's native unit.
|
|
364
|
+
- **Plain integer** — pixels (`x="200"` = exactly 200 px from the left edge of the slide pane). True 1-px precision via Kitty Graphics' `X=` / `Y=` sub-cell offsets — `x="201"` and `x="202"` land at distinct pixels, not snapped to the cell grid. Backend-dependent: Echoes honors sub-cell offsets reliably; pure-Kitty implementations vary.
|
|
365
|
+
- **`Npx`** — same as plain integer; the `px` is explicit.
|
|
366
|
+
- **`Nc`** — 1-based terminal cell, when you want grid alignment instead of pixel alignment.
|
|
367
|
+
- **`N%`** — percent of the terminal's current width / height.
|
|
368
|
+
- **Alignment keywords** — `x="left"` / `"center"` / `"right"` and `y="top"` / `"center"` / `"bottom"` (with `middle` as a synonym of `center` on either axis) resolve against the image's *rendered* cell footprint (after `relative_*` / `width` / `height` sizing). `<img src="logo.png" x="center" y="bottom"/>` drops the rendered logo centered on the bottom row of the pane; `x="right"` anchors flush to the rightmost column. Sub-cell pixel offsets reset to 0 for keyword axes (they're cell-grid choices by nature).
|
|
369
|
+
- **Either / both axes pin** — setting `x` only pins the horizontal column (vertical falls back to the flow row); setting `y` only pins the row (horizontal falls back to the centered flow position); setting both pins both. As soon as either is set, the image contributes 0 to the layout flow — paragraphs around it render in their normal positions, exactly like `<at>`. With neither `x` nor `y`, the image stays horizontally centered and takes up its natural height in the flow.
|
|
370
|
+
- **Z-order**: `z="N"` lets you put the image above or below cell text. A pinned `<img x y/>` defaults to `z="-1"` (behind text) so paragraphs and headings layered on the same cells stay readable; flow `<img>` (no `x` / `y`) stays at the Kitty default of `z=0` (on top of cells) because that's almost always what a standalone image wants. Pass `z="0"` / `z="1"` etc. to put a pinned image on top.
|
|
371
|
+
- Rendering backend: Kitty Graphics Protocol on terminals that support it (PNG uploaded once and reused; JPG goes through `kitten icat`), Sixel as a fallback. Other terminals show nothing in place of the image.
|
|
372
|
+
|
|
373
|
+
### Shapes and Lines
|
|
374
|
+
|
|
375
|
+
Keynote-style vector shapes — `<rect>`, `<circle>`, `<ellipse>`, `<line>`, `<polyline>`, `<polygon>`, `<arrow>`, `<path>` — drawn natively by the terminal. Each tag is self-closing, absolute-positioned (contributes 0 to the layout flow), and accepts geometry attrs in slide cells or `N%` of the terminal width / height.
|
|
376
|
+
|
|
377
|
+
```markdown
|
|
378
|
+
<line x1="10" y1="5" x2="70" y2="5" stroke="white" stroke-width="0.3"/>
|
|
379
|
+
<arrow x1="10" y1="10" x2="70" y2="10" stroke="cyan" stroke-width="0.5"/>
|
|
380
|
+
<rect x="10" y="13" width="20" height="6" rx="1" fill="tomato"/>
|
|
381
|
+
<circle cx="50%" cy="15" r="5" fill="cyan"/>
|
|
382
|
+
<ellipse cx="50" cy="15" rx="20" ry="6" fill="none" stroke="gold" stroke-width="0.5"/>
|
|
383
|
+
<polyline points="10,5 30,15 50,5 70,15" stroke="lime" stroke-width="0.4"/>
|
|
384
|
+
<polygon points="50,2 60,15 40,15" fill="gold"/>
|
|
385
|
+
<path d="M 10 20 C 30 12 50 12 70 20" stroke="orange" stroke-width="0.4"/>
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
- Geometry attrs per shape:
|
|
389
|
+
- `<rect>`: `x`, `y`, `width`, `height`, optional `rx`, `ry` for rounded corners.
|
|
390
|
+
- `<circle>`: `cx`, `cy`, `r` (radius is a length, resolved against terminal width when given as `N%`).
|
|
391
|
+
- `<ellipse>`: `cx`, `cy`, `rx`, `ry`.
|
|
392
|
+
- `<line>`, `<arrow>`: `x1`, `y1`, `x2`, `y2`. The arrow grows a filled triangular head at `(x2, y2)`; the head sizes scale with `stroke-width` (length 4×, width 3×). The head's color defaults to `stroke`; an explicit `fill="..."` recolors only the head (handy for two-tone arrows).
|
|
393
|
+
- `<polyline>` / `<polygon>`: `points="x1,y1 x2,y2 ..."` (space- or comma-separated; each coord can be `N%`).
|
|
394
|
+
- `<path>`: `d="..."` — SVG path data using `M / L / H / V / C / S / Q / T / A / Z` commands (uppercase = absolute, lowercase = relative). Coordinates are in slide cells exactly like every other shape (`<path d="M 10 5 L 70 5"/>` runs a line from cell `(10, 5)` to `(70, 5)`); under the hood the renderer rewrites `d` into pixel coords so the stroke renders crisply at the cell aspect ratio. Percents inside `d` aren't supported (only plain numbers); the bbox is computed from every endpoint and control point in `d`, which slightly over-estimates curves (control points often sit outside the visible curve) — safe but means the placement reserves a few extra cells in that direction.
|
|
395
|
+
- Paint attrs pass through to SVG verbatim: `fill`, `stroke`, `stroke-width`, `opacity`, `fill-opacity`, `stroke-opacity`, `stroke-linecap`, `stroke-linejoin`, `stroke-dasharray`, `stroke-miterlimit`, `fill-rule`, `transform`.
|
|
396
|
+
- Colors: the full 147-name CSS / SVG color set is supported — `red`, `tomato`, `gold`, `lavender`, `rebeccapurple`, etc. The renderer expands every named color to `#rrggbb` before shipping the SVG, so Echoes' built-in named-color list (which is smaller) doesn't matter. Hex codes (`#rrggbb` / `#rgb`) and `rgb(...)` / `rgba(...)` pass through unchanged; `none`, `currentColor`, `transparent` work too.
|
|
397
|
+
- Defaults: closed shapes (`rect`, `circle`, `ellipse`, `polygon`) fill `white`; open shapes (`line`, `polyline`, `arrow`, `path`) stroke `white` at `stroke-width="0.2"` (a cell-width hairline). Override via explicit `fill=` / `stroke=` when your slide background is light. `<path>` can be either open (default) or closed via `Z` plus an explicit `fill="..."`.
|
|
398
|
+
- Coordinate semantics: positional attrs (`x`, `y`, `cx`, `cy`, `x1`, `y1`, `x2`, `y2`, `points`) are 1-indexed slide cells, matching `<at>` and `<img>` (so `x="10" y="5"` lands at column 10, row 5). Size attrs (`width`, `height`, `rx`, `ry`) are cell counts on the respective axis. The shape is composed in pixel coords behind the scenes using the terminal's actual cell pixel size, so a `circle r="5"` renders as a true circle even though terminal cells are typically ~1:2 wide-to-tall.
|
|
399
|
+
- Stroke widths are in **cell-widths** (typically ~12 px each) — `stroke-width="0.3"` is roughly a 3–4 pixel hairline regardless of cell aspect.
|
|
400
|
+
- Stroke is rendered inside the shape's padded bounding box, so the stroke won't get clipped.
|
|
401
|
+
- Z-order: shapes are registered **before** any text in the slide so that subsequent text writes re-fill the cell buffer at the cells the placement overlapped. Where the SVG is transparent (e.g., outside a thin `<line>` stroke or an unfilled outline), text shows through; where the SVG is opaque (a `<rect fill="..."/>`, a filled `<circle>`, etc.) the shape still covers any text underneath. Note: stock Echoes (today) ignores the `z=` parameter and always draws placements on top of cells — until z-index support lands upstream, solid-filled shapes that overlap text cells will occlude that text. `z="N"` is still accepted on the tag for forward compatibility.
|
|
402
|
+
- Rendering backend: Kitty Graphics Protocol direct-data mode. Echoes content-sniffs the payload and rasterizes via its native CoreGraphics fast path (sub-millisecond, synchronous) — these shapes always hit the fast path. On terminals that don't speak Kitty graphics the shape silently renders nothing.
|
|
403
|
+
|
|
404
|
+
### Slide layouts
|
|
405
|
+
|
|
406
|
+
Layouts let a slide carve itself into named regions ("slots") — title across the top, two columns underneath, image-and-caption side-by-side, etc. Without a layout, slides render in the existing top-down flow (the default behavior is unchanged).
|
|
407
|
+
|
|
408
|
+
**Picking a layout.** Use an h1 IAL with the layout name. Three interchangeable spellings:
|
|
409
|
+
|
|
410
|
+
```markdown
|
|
411
|
+
# Two columns {layout=two-column} ← plain HTML-attr style
|
|
412
|
+
# Two columns {:layout=two-column} ← kramdown IAL marker
|
|
413
|
+
# Two columns {layout: two-column} ← YAML / JSON flow style
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
`=` and `:` are interchangeable separators. Values may be unquoted (`name`), single-quoted (`'name'`), or double-quoted (`"name"`).
|
|
417
|
+
|
|
418
|
+
**Filling slots.** The layout's first `title` slot is auto-filled from the h1. Remaining slots fill in declaration order; a block-level `<slot/>` advances to the next slot, and `<slot name="right"/>` jumps to a specific slot by name.
|
|
419
|
+
|
|
420
|
+
```markdown
|
|
421
|
+
# Two columns {layout=two-column}
|
|
422
|
+
|
|
423
|
+
left column content
|
|
424
|
+
- bullet A
|
|
425
|
+
- bullet B
|
|
426
|
+
|
|
427
|
+
<slot/>
|
|
428
|
+
|
|
429
|
+
right column content
|
|
430
|
+
- bullet C
|
|
431
|
+
- bullet D
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**Defining layouts.** Layouts live under the `layouts:` key in `theme.yml`. Each layout's value is an ordered list of slots; each slot has `name`, `x`, `y`, `width`, `height`, and optional styling: `size` (OSC 66 scale: `1`-`7` or named `xx-small` … `xxxx-large`), `family` _(Echoes only)_, and `color` (named ANSI or 6-digit hex). Coordinates use the same vocabulary as `<at>` and `<img x y>` — numeric cells, `N%`, `Nc`, `Npx`, or the alignment keywords `left` / `center` / `right` on `x` and `top` / `center` / `bottom` on `y` (with `middle` accepted as a synonym of `center` on either axis). When `x:` is a keyword the slot is positioned so its content lands at the requested alignment AND blocks routed into the slot inherit the same alignment, replacing the old separate `align:` key. When `y:` is a keyword the renderer **measures the slot's actual content height** (via a dry-render pass) and positions the content so its top / centre / bottom edge falls at the slide's top / centre / bottom — regardless of `slot.height`. That means `y: center` centres the **text content** vertically no matter how many lines it has; a single h1 and a six-line paragraph both land on the slide's centre line.
|
|
435
|
+
|
|
436
|
+
```yaml
|
|
437
|
+
layouts:
|
|
438
|
+
title-content:
|
|
439
|
+
- {name: title, x: center, y: 3, width: 90%, height: 6}
|
|
440
|
+
- {name: content, x: 5, y: 10, width: 90%, height: 80%}
|
|
441
|
+
cover:
|
|
442
|
+
- {name: title, x: center, y: 35%, width: 100%, height: 25%, size: xxx-large}
|
|
443
|
+
- {name: subtitle, x: center, y: 80%, width: 100%, height: 15%}
|
|
444
|
+
two-column:
|
|
445
|
+
- {name: title, x: center, y: 3, width: 90%, height: 6}
|
|
446
|
+
- {name: left, x: 5, y: 10, width: 45%, height: 80%}
|
|
447
|
+
- {name: right, x: 50%, y: 10, width: 45%, height: 80%}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
`x: center` on a slot is what makes h1 titles (and any other content routed into the slot) horizontally centered — there's no implicit "h1 always centers" magic. Per-block `<center>` / `{:.center}` directives and inline tags (`<size>`, `<font>`, `<color>`) still override the slot's defaults per-segment. The legacy `align: center` key on a slot is still accepted as a back-compat alias for `x: center`, so existing custom themes keep working until you migrate them.
|
|
451
|
+
|
|
452
|
+
**Built-in layouts** (shipped in `default_theme.yml`):
|
|
453
|
+
|
|
454
|
+
- `default` — theme-wide fallback for slides without an `{layout=...}` IAL. Shipped identical to `title-content`: centered title band across the top, content below. Override in your own `theme.yml` to give every plain slide a different layout.
|
|
455
|
+
- `cover` — auto-applied to slide 0 when it has no `{layout=...}` IAL. Roughly emulates Keynote's "Title" slide: heading centered near the middle (slot `title`, y=35%), a smaller subtitle near the bottom (slot `subtitle`, y=80%). Put the deck title in the h1 and any author/date line in a paragraph after it.
|
|
456
|
+
- `title-only` — one slot, vertically centered. For section dividers.
|
|
457
|
+
- `takahashi` — [Takahashi-method](https://en.wikipedia.org/wiki/Takahashi_method) (高橋メソッド) slides: a single very-large phrase per slide, no decoration. Just the words, dropped at the slide's exact centre regardless of phrase length via `x: center, y: center`.
|
|
458
|
+
- `title-content` — title across the top, content below.
|
|
459
|
+
- `two-column` — title across the top, two side-by-side columns.
|
|
460
|
+
- `photo-caption` — title across the top, image on the left, caption on the right.
|
|
461
|
+
|
|
462
|
+
Both `default` and `cover` are picked automatically when no IAL is set; `cover` only on slide 0, `default` everywhere else. To opt out of `cover` on slide 0, write `# Title {layout=default}` (or any other explicit layout) on the first slide. To remove the auto-cover behavior deck-wide, delete `cover` from your theme.
|
|
463
|
+
|
|
464
|
+
**Theme-wide default.** Override `layouts.default` in your own `theme.yml` to apply a different layout to every plain slide. For example, a full-bleed single-slot layout instead of the shipped title-band default:
|
|
465
|
+
|
|
466
|
+
```yaml
|
|
467
|
+
layouts:
|
|
468
|
+
default:
|
|
469
|
+
- {name: content, x: 1, y: 2, width: 100%, height: 100%}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
A single slide can opt back out of the `default` layout with `{layout=none}`:
|
|
473
|
+
|
|
474
|
+
```markdown
|
|
475
|
+
# This one uses flow rendering {layout=none}
|
|
476
|
+
|
|
477
|
+
normal top-down content, no slot routing.
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
**Overflow.** Content past a slot's `height` spills downward; there's no clipping or shrink-to-fit. Slot height is a layout guide, not a hard cap — easier to debug while authoring. If you need tighter control, shrink the content or grow the slot.
|
|
237
481
|
|
|
238
482
|
### Comments
|
|
239
483
|
|
|
484
|
+
HTML-style comments — single-line or multi-line — are stripped at parse time.
|
|
485
|
+
|
|
486
|
+
```markdown
|
|
487
|
+
<!-- single-line note to self, hidden from the deck -->
|
|
488
|
+
|
|
489
|
+
<!--
|
|
490
|
+
multi-line
|
|
491
|
+
hidden block
|
|
492
|
+
-->
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Rabbit-compatible kramdown form is also accepted:
|
|
496
|
+
|
|
240
497
|
```markdown
|
|
241
498
|
{::comment}
|
|
242
499
|
This text is hidden from the presentation.
|
|
@@ -246,10 +503,11 @@ This text is hidden from the presentation.
|
|
|
246
503
|
### Notes
|
|
247
504
|
|
|
248
505
|
```markdown
|
|
249
|
-
Visible text {::note}(speaker note){:/note}
|
|
250
506
|
Visible text <note>(speaker note)</note>
|
|
251
507
|
```
|
|
252
508
|
|
|
509
|
+
Rabbit-compatible kramdown form is also accepted: `{::note}(speaker note){:/note}`.
|
|
510
|
+
|
|
253
511
|
### Escaping `<`, `>`, `&`
|
|
254
512
|
|
|
255
513
|
To show literal markup characters that would otherwise be interpreted as a tag, use HTML-style entity references:
|
|
@@ -260,17 +518,181 @@ To show literal markup characters that would otherwise be interpreted as a tag,
|
|
|
260
518
|
A & B renders as: A & B
|
|
261
519
|
```
|
|
262
520
|
|
|
263
|
-
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
|
|
521
|
+
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=...>`, `<color=...>`, `<font ...>`, `<note>`, `<wait/>`, `<br>`, `<center>`, `<right>`, `<at ...>`, `<bg .../>`, `<img .../>`, shape tags like `<rect/>` / `<circle/>`, or `<!-- ... -->`).
|
|
522
|
+
|
|
523
|
+
### Step builds — `<wait/>`
|
|
524
|
+
|
|
525
|
+
A `<wait/>` on its own line is a **step boundary**: blocks before it are visible immediately, blocks after it stay hidden until you press Space (or Right / Down / `j` / `l`). The layout reserves space for the unrevealed blocks so the slide doesn't reflow upward as builds appear — Keynote-style staged reveal.
|
|
526
|
+
|
|
527
|
+
```markdown
|
|
528
|
+
# Builds
|
|
529
|
+
|
|
530
|
+
First idea — visible right away.
|
|
531
|
+
|
|
532
|
+
<wait/>
|
|
533
|
+
|
|
534
|
+
Second idea — revealed on the first Space press.
|
|
535
|
+
|
|
536
|
+
<wait/>
|
|
537
|
+
|
|
538
|
+
Conclusion — revealed on the second Space press.
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
- Each `<wait/>` adds one step. A slide with N waits has N+1 steps; the last Space press flips to the next slide. Left / Up walk backward, with the previous slide re-entered at its **last** step so backtracking shows full content first.
|
|
542
|
+
- A leading `<wait/>` on a list item or paragraph counts the same way — the item / paragraph appears at the next step. Lists keep their list semantics (no broken numbering):
|
|
543
|
+
|
|
544
|
+
```markdown
|
|
545
|
+
- foo
|
|
546
|
+
- <wait/>bar
|
|
547
|
+
- <wait/>baz
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
- Mid-text `<wait/>` (inside prose, not at the start of a block) is still consumed as a silent no-op — splitting a sentence mid-flow has no defined rendering semantics yet. Prose that mentions the marker literally should wrap it in backticks (`` `<wait/>` ``) like the rest of this section.
|
|
551
|
+
- Reload (`r`) rewinds the current slide to step 0 so a re-parsed deck always starts collapsed.
|
|
552
|
+
- Both forms are accepted: `<wait/>` (XML self-closing), `<wait></wait>` (paired), and the kramdown spelling `{::wait/}`.
|
|
553
|
+
|
|
554
|
+
### Slide transition — `{transition=fade}` _(Echoes only)_
|
|
555
|
+
|
|
556
|
+
A slide opts into a fade transition via the h1 IAL:
|
|
557
|
+
|
|
558
|
+
```markdown
|
|
559
|
+
# Intro {transition=fade}
|
|
560
|
+
|
|
561
|
+
…content…
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
The slide carrying the attribute fades out when you leave it and fades in when you arrive at it. The two halves are independent — `A → B` fades out from A iff `A` has `{transition=fade}` and fades in to B iff `B` does. If only one side opts in, the other snaps. Set `theme.transition.duration` (ms) to control per-half speed; default is 200ms.
|
|
565
|
+
|
|
566
|
+
- Echoes-only: drives OSC 7772 `cell-alpha` paint state under the hood. Other terminals skip the animation entirely and snap.
|
|
567
|
+
- Text-only in v1: paragraphs, headings, lists, code blocks, tables, and `<at>` text all fade. Kitty-graphics placements (background images, `<img>`, shapes) stay opaque — Kitty's per-placement opacity isn't wired yet.
|
|
568
|
+
- Acts on slide-boundary navigation (→, ←, Space, `j`/`k`, etc., plus `g` / `G`). `<wait/>` step builds within a slide are independent and not affected.
|
|
569
|
+
- Reload (`r`) skips the fade — it's a refresh, not navigation.
|
|
570
|
+
|
|
571
|
+
### Hiding the counter — `{counter=hide}`
|
|
572
|
+
|
|
573
|
+
A slide can opt out of the bottom-row counter via the h1 IAL — handy for the title page:
|
|
574
|
+
|
|
575
|
+
```markdown
|
|
576
|
+
# My Talk {counter=hide}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
Both forms are suppressed on that slide: the plain `N / M` counter and the 🐇/🐢 runner bar (`counter.duration`). The slide still counts toward the total, so the next slide shows `2 / M`. `--export prawn` leaves the page number off that page too.
|
|
580
|
+
|
|
581
|
+
### Fragment animations — `fragment="fade-..."`
|
|
582
|
+
|
|
583
|
+
A positioned block can opt into a one-shot directional reveal via `fragment=`. The block appears at the *next* step boundary, sliding in from a direction while fading from alpha 0 to alpha 1:
|
|
584
|
+
|
|
585
|
+
```markdown
|
|
586
|
+
<at x="10" y="5" fragment="fade-up">A callout</at>
|
|
587
|
+
<at x="40" y="5" fragment="fade-right">Another</at>
|
|
588
|
+
<img src="chart.png" x="20" y="10" fragment="fade-in"/>
|
|
589
|
+
<rect x="5" y="5" width="20" height="6" fill="tomato" fragment="fade-in"/>
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
Styles: `fade-up`, `fade-down`, `fade-left`, `fade-right`, `fade-in`. Each `fragment=` block adds one step boundary (matches reveal.js's `.fragment` semantics) — three fragments become a three-step build with no explicit `<wait/>` between them. Mix freely with `<wait/>`; each contributes a step independently.
|
|
593
|
+
|
|
594
|
+
Settings & limits:
|
|
595
|
+
- Accepted on `<at>`, `<img>`, and shape tags (`<rect>`, `<circle>`, `<ellipse>`, `<line>`, `<arrow>`, `<polyline>`, `<polygon>`, `<path>`).
|
|
596
|
+
- `theme.fragment.duration` (ms) controls the animation length; default 300ms.
|
|
597
|
+
- The hardcoded slide-in distance is 2 cells vertical, 4 cells horizontal.
|
|
598
|
+
- Non-numeric coords (`x="center"`, `y="40%"`, alignment keywords) fall back to fade-only — the positional offset is skipped.
|
|
599
|
+
- The alpha portion is Echoes-only (OSC 7772 `cell-alpha`); on other terminals the position still moves but the fade snaps.
|
|
600
|
+
- `id="..."` is auto-generated (`__frag_N`) when not supplied so the existing action / state machinery can target the block. An explicit `id=` is preserved.
|
|
264
601
|
|
|
265
|
-
###
|
|
602
|
+
### Cross-slide reference — `<ref id="..."/>`
|
|
266
603
|
|
|
267
|
-
|
|
604
|
+
Give any positioned block an `id="..."` and then re-render it on another slide (or twice on the same slide) by writing `<ref id="..."/>`. The reference looks up the block whose original declaration carries that id anywhere in the deck and dispatches to the same render path, so `<at>` text, `<img>` placements, and shapes all reappear at their original spot:
|
|
268
605
|
|
|
269
606
|
```markdown
|
|
270
|
-
|
|
607
|
+
# Slide 1
|
|
608
|
+
|
|
609
|
+
<at id="lyric" x="center" y="50%">Stand by for Exciter</at>
|
|
610
|
+
|
|
611
|
+
# Slide 5
|
|
612
|
+
|
|
613
|
+
<ref id="lyric"/> <!-- same text, same position -->
|
|
614
|
+
<ref id="lyric" y="40%"/> <!-- same text, moved to y=40% -->
|
|
615
|
+
<ref id="lyric" y="80%"/> <!-- same text, moved to y=80% -->
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
- Same-tag attrs on the `<ref>` (typically `x` / `y`, but anything the source's renderer reads) override the source's. Attrs you omit ride through from the source.
|
|
619
|
+
- The synthetic clone the ref renders has no `id`, so an `<action target="..."/>` (see below) on the *ref* slide can't accidentally bind to it. Giving the ref instance its own animatable id is out of scope for v1.
|
|
620
|
+
- Actions stay per-slide. An `<action target="lyric"/>` on the source slide does not animate the reference; the reference shows the source's declared (not animated) state.
|
|
621
|
+
- First declaration wins on duplicate ids across the deck. Refs are skipped when building the lookup index, so a chain of refs (`<ref>` → `<ref>` → real source) doesn't resolve transitively in v1.
|
|
622
|
+
- Unknown id → silent no-op (no crash, no writes), same fallback as an unknown `<action target="...">`.
|
|
623
|
+
- PDF parity: refs to flow content (paragraph / list / code / blockquote / table / image) render in the Prawn PDF fallback; refs to `<at>` or shapes inherit the source's existing PDF limitation (those positioned types aren't drawn in PDF regardless).
|
|
624
|
+
|
|
625
|
+
### Composite reuse — `<group id="..."> ... </group>`
|
|
626
|
+
|
|
627
|
+
`<ref>` re-renders one source block. When the thing you want to reuse is a *bundle* — a heading + backing rectangle + caption, say — wrap it in a `<group id="..."> ... </group>` and `<ref id="..."/>` replays the whole bundle:
|
|
628
|
+
|
|
629
|
+
```markdown
|
|
630
|
+
# Slide 1
|
|
631
|
+
|
|
632
|
+
<group id="callout">
|
|
633
|
+
<rect x="10" y="20" width="40" height="6" fill="tomato" opacity="0.3"/>
|
|
634
|
+
<at x="30c" y="22c">Watch this part →</at>
|
|
635
|
+
A caption beneath, in flow.
|
|
636
|
+
</group>
|
|
637
|
+
|
|
638
|
+
# Slide 5
|
|
639
|
+
|
|
640
|
+
<ref id="callout"/>
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
- Children can be anything that already works at slide-body scope — paragraphs, `<at>`, `<img>`, shapes, even nested `<group>`s.
|
|
644
|
+
- Each child's own id stays addressable deck-wide. `<ref id="<inner-id>"/>` resolves to a child of a group from anywhere; `<action target="<inner-id>"/>` on the **source** slide can animate it.
|
|
645
|
+
- When a slide ref's the group, every descendant's id is stripped from the synthetic clone, so per-slide `<action target="..."/>` on the *ref* slide can't bind to any child. (Renaming a child inside a ref'd group is a v1 non-goal.)
|
|
646
|
+
- The opening `<group ...>` and closing `</group>` tags each have to be alone on their own line — same line-level constraint the rest of the block-level dispatch uses. A missing close tag or a missing `id=` silently drops the group; the rest of the slide parses normally.
|
|
647
|
+
- `<wait/>` inside a group does not drive sub-steps in v1. The slide-level step counter only sees waits at slide top level, so all children of a ref'd (or in-place) group render together. Authors who need incremental reveals can use slide-level waits between groups.
|
|
648
|
+
|
|
649
|
+
### Actions — `<action target="..."/>`
|
|
650
|
+
|
|
651
|
+
The same `id="..."` attribute introduced for `<ref>` (above) also lets `<action>` push attribute overrides onto a target block at a later step within a slide. The most common use is moving an element across the slide as builds advance:
|
|
652
|
+
|
|
653
|
+
```markdown
|
|
654
|
+
# Move demo
|
|
655
|
+
|
|
656
|
+
<rect id="box" x="5c" y="5c" width="20" height="6" fill="tomato"/>
|
|
657
|
+
|
|
658
|
+
Now press Space →
|
|
659
|
+
|
|
271
660
|
<wait/>
|
|
661
|
+
|
|
662
|
+
<action target="box" x="50c" y="20c"/>
|
|
663
|
+
|
|
664
|
+
… and again →
|
|
665
|
+
|
|
666
|
+
<wait/>
|
|
667
|
+
|
|
668
|
+
<action target="box" x="80%" y="80%"/>
|
|
272
669
|
```
|
|
273
670
|
|
|
671
|
+
- Any attr on `<action>` that isn't `target` overrides the same attr on the target block. `x` / `y` move it (the position resolver — the same one used by `<img>` and `<at>` — re-runs at each step); `z`, `width`, `height`, shape `fill` / `stroke` / `opacity`, etc. all work the same way.
|
|
672
|
+
- Actions fire on the step at which they appear in the block sequence (positional, same model as element reveals). The latest action against each target wins at the current step, so a sequence of `<action>`s walks the target through positions.
|
|
673
|
+
- An action with no `target=` is silently dropped. An action targeting an id that doesn't exist is a no-op (no crash). An action against an id whose element hasn't been revealed yet still sets the state — when the element later appears, it lands at the action's position.
|
|
674
|
+
- Layer interaction: the target block's flow vs. positioned status doesn't change. A pinned `<img x y>` stays out of the flow whether moved or not; a flow `<img>` stays in flow.
|
|
675
|
+
- `duration="500ms"` (also `"0.5s"` or plain `"500"` for ms) animates the action smoothly over that span when its step is revealed, instead of snapping. Numeric attrs blend at 30fps from the target's prior value to the new value. Both sides of each interpolated attr must share a unit — `x1="15"` → `x1="50"` blends fine, but `x1="15"` → `x1="50%"` snaps because cells and percentages can't be averaged. Key presses during the animation queue and process after it settles.
|
|
676
|
+
- **Fade in / fade out** _(Echoes only)_ — `opacity` is one of the attrs `<action>` can drive, so a fade is just `<action target="id" opacity="0" duration="500ms"/>` (fade-out) or, paired with an initial `opacity="0"` on the target block, `<action target="id" opacity="1" duration="500ms"/>` (fade-in). The renderer wraps the faded block in OSC 7772 `cell-alpha` so Echoes paints its text at the lerped alpha against whatever's underneath. Scope: every text block fades — paragraphs, lists, code blocks, tables, blockquotes, h1 titles, the runner-bar emojis, and `<at>` positioned text. `<img>` blocks (Kitty Graphics placements) don't fade in v1 — Kitty has its own opacity param that the renderer doesn't drive yet. Non-Echoes terminals ignore the OSC, so the block snaps in / out instead of fading.
|
|
677
|
+
|
|
678
|
+
```markdown
|
|
679
|
+
<at id="bullet">A fading point</at>
|
|
680
|
+
<wait/>
|
|
681
|
+
<action target="bullet" opacity="0" duration="500ms"/>
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### Line break
|
|
685
|
+
|
|
686
|
+
Force a line break inside a paragraph with `<br>`, `<br/>`, or `<br />`. Each break closes the current line — surrounding inline styling (`<color>`, `<font>`, `<center>`, etc.) carries across, so a centered paragraph with explicit breaks stays centered on every line. Two `<br>`s in a row insert a blank line between the chunks.
|
|
687
|
+
|
|
688
|
+
```markdown
|
|
689
|
+
<center>title in the middle<br>subtitle on the next line</center>
|
|
690
|
+
|
|
691
|
+
first line<br><br>second line, after a blank
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
Paragraphs that would overflow the slide width wrap automatically — `<br>` is only for the cases where you want a break at a specific spot.
|
|
695
|
+
|
|
274
696
|
## Theming
|
|
275
697
|
|
|
276
698
|
Theme resolution:
|
|
@@ -282,43 +704,51 @@ Theme resolution:
|
|
|
282
704
|
All keys are optional — anything you don't set falls back to the bundled defaults.
|
|
283
705
|
|
|
284
706
|
```yaml
|
|
285
|
-
font:
|
|
286
|
-
family: #
|
|
287
|
-
size:
|
|
288
|
-
color: #
|
|
707
|
+
font: # body text typography (paragraphs, lists, h2–h6, code, tables, …)
|
|
708
|
+
family: # font family; Echoes only — OSC 66 f= extension
|
|
709
|
+
size: # OSC 66 scale: numeric (1–7) or named (xx-small … xxxx-large); default 2 (small)
|
|
710
|
+
color: # named ANSI or 6-digit hex; falls back to terminal default fg when unset
|
|
289
711
|
|
|
290
712
|
title: # h1 typography (slide titles)
|
|
291
|
-
family: # font family
|
|
713
|
+
family: # font family; Echoes only — OSC 66 f= extension
|
|
292
714
|
size: # OSC 66 scale: numeric (1–7) or named (xx-small … xxxx-large); default x-large
|
|
293
715
|
color: # named ANSI or 6-digit hex
|
|
294
716
|
|
|
295
717
|
bullet: # unordered-list marker; also h2–h6 prefix
|
|
296
718
|
text: "・" # the glyph
|
|
297
719
|
size: # OSC 66 scale (1–7) for the bullet; default = body text's scale
|
|
720
|
+
color: # named ANSI or 6-digit hex; falls back to body text color when unset
|
|
298
721
|
|
|
299
|
-
|
|
722
|
+
code: # fenced code block (and inline `<code>`) typography
|
|
723
|
+
family: # font family (Echoes only); falls back to font.family
|
|
724
|
+
size: # OSC 66 scale (1–7); default = body text's scale
|
|
725
|
+
color: # fg for ALL code text — fenced blocks and inline; named ANSI or 6-digit hex; falls back to body fg
|
|
726
|
+
bg: # block background (fenced only); named / hex / ANSI; default = dim gray (ANSI 256-color 236)
|
|
727
|
+
|
|
728
|
+
background: # default slide background; Echoes only — OSC 7772
|
|
300
729
|
color: # solid, e.g. "#1a1a2e"
|
|
301
730
|
from: # gradient endpoint
|
|
302
731
|
to: # gradient endpoint
|
|
303
732
|
angle: # gradient angle in degrees
|
|
304
733
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
dim: "6c7086"
|
|
311
|
-
inline_code: "a6e3a1"
|
|
734
|
+
counter: # bottom-of-screen slide counter; runner-bar opt-in via duration
|
|
735
|
+
color: # named ANSI or 6-digit hex; default = dim
|
|
736
|
+
duration: # "30m", "1h30m", "1800s", or plain integer seconds; opt in to the 🐇/🐢 runner bar
|
|
737
|
+
rabbit: # raw escape sequence written at the slide-progress runner position
|
|
738
|
+
turtle: # raw escape sequence written at the elapsed-time runner position
|
|
312
739
|
```
|
|
313
740
|
|
|
314
741
|
Notes:
|
|
315
742
|
|
|
316
|
-
- **`font
|
|
317
|
-
- **`
|
|
318
|
-
- **`font.
|
|
319
|
-
- **`
|
|
320
|
-
- **`
|
|
321
|
-
- **`
|
|
743
|
+
- **`font`** — body text typography for everything that isn't an h1: paragraphs, lists, h2–h6, blockquotes, code blocks, tables, definition lists. Mirrors `title`'s three knobs:
|
|
744
|
+
- **`font.family`** _(Echoes only)_ — applied via OSC 66 `f=`; PDF: registered via fontconfig. Inline `<font face="...">` runs override it per-segment.
|
|
745
|
+
- **`font.size`** — OSC 66 scale; controls the visual size of body text (numeric `1`–`7` or named `xx-small` … `xxxx-large`). Default `2` (small). Inline `<size=...>` runs override it per-segment.
|
|
746
|
+
- **`font.color`** — deck-wide default text color. Inline `<color=...>` / `<font color="...">` runs still win per-segment.
|
|
747
|
+
- **`bullet`** — `bullet.text` is the character; `bullet.size` is the OSC 66 scale used to render it. When `bullet.size` is smaller than the body text scale, the bullet is rendered with fractional scaling and vertical centering so it still aligns with the body line. `bullet.color` sets a dedicated foreground color for the marker (named ANSI or 6-digit hex); when unset, the bullet inherits the body text color.
|
|
748
|
+
- **`title`** — h1 typography. Same three knobs as `font` (`family`, `size`, `color`) but each is independent: `title.family` _(Echoes only)_ does **not** inherit `font.family`, `title.color` does **not** inherit `font.color`. `title.size` defaults to x-large (OSC 66 `s=4`). When `title.family` is set, h1 lines emit as one slot-spanning multicell with halign=2 so Echoes' proportional-font measurement centers the text pixel-precisely instead of using the cell-grid estimate. h2–h6 stay body text.
|
|
749
|
+
- **`code`** — fenced code block (and inline `<code>`) typography. `code.family` _(Echoes only)_ sets the OSC 66 `f=` face for code (falls back to `font.family` when unset, so a single body-font override usually styles code too); `code.size` is the OSC 66 scale (default = `font.size`); `code.color` is the foreground colour for **all** code text — fenced blocks and inline `<code>` alike — falling back to the body fg when unset; `code.bg` is the **fenced-block** background — accepts the same forms as `font.color` (CSS / ANSI named or 6-digit hex; default is dim gray, ANSI 256-color 236). Inline `<code>` segments in the **terminal** keep the historical gray-background styling for the bg; only fenced blocks read `code.bg`. `code.highlight` picks the syntax-highlight colour scheme: any Rouge theme name works — common picks are `monokai`, `gruvbox`, `github.dark`, `base16.solarized.dark`, `tulip`. Unset (the default) uses przn's built-in ANSI palette, which maps tokens to ANSI colour names and so picks up the user's terminal palette (handy on light terminals where absolute monokai-style hex would clash). Unknown / mistyped theme names silently fall through to the ANSI palette.
|
|
750
|
+
- **`background`** _(Echoes only)_ — the deck-wide default background. A per-slide `<bg .../>` directive overrides it for that slide. The Prawn fallback paints the PDF page in `background.color` when set; otherwise it leaves the page Prawn's default (white).
|
|
751
|
+
- **`counter`** — bottom-of-screen slide counter (hide it on a single slide with `# Title {counter=hide}`). Without `counter.duration`, przn shows a plain `N / M` counter at the bottom-right. Set `counter.duration` to opt into the Rabbit-style runner bar: current slide # at the very left, total at the very right, 🐇 running between them tracking slide progress and 🐢 tracking elapsed time against the goal. Drag the 🐇 with the mouse to jump to another slide. `counter.color` (named ANSI or 6-digit hex) styles both the plain counter and the runner-bar anchor numbers; default is dim ANSI. `counter.rabbit` and `counter.turtle` are the **raw escape sequences** the renderer writes at the two runner positions — the default theme ships an Echoes-private OSC 7772 `;multicell` with `flip=h` so the emojis face rightward (towards the right anchor); terminals that don't speak OSC 7772 silently drop the sequence and no runner glyph appears. To stay portable, override these with a plain glyph or a standard OSC 66 multicell — at the cost of the flip on Echoes.
|
|
322
752
|
|
|
323
753
|
## License
|
|
324
754
|
|