presentypst #
Point it at a Typst file and it serves the presentation over HTTP: a clean fullscreen display to cast to a screen, and a presenter console to drive it from.
$ presentypst talk.typ
presentypst — 14 slides
display http://192.168.1.24:8080/
presenter http://localhost:8080/present
Cast the display URL (with castt, a browser, a TV, whatever), open the presenter
URL on your laptop, and drive the talk from there. Navigation is authoritative on
the server, so both views stay in lockstep and either one can drive.
Writing a deck #
#import "@presentypst/lib:0.1.0": *
#show: presentation.with(
title: "My talk",
author: "Me",
date: datetime.today(),
)
#title-slide()
= A section
== A slide
First point #pause
Second point
#notes[Say the thing about the second point.]
== With a video
#video("clips/demo.mp4", start: "0:30", end: "0:55", width: 70%)
The library is built on touying, so #pause,
#meanwhile, #only, #uncover and the rest work as documented there. Nothing
needs installing: the @presentypst package namespace is served from inside the
binary.
Slides #
presentation(title:, subtitle:, author:, date:, aspect-ratio:, primary:) |
show rule setting up the deck |
title-slide() |
opening slide, built from the deck's front matter |
slide[…] |
a content slide (also what a == heading produces) |
centered-slide[…] |
content centred both ways |
focus-slide[…] |
full-bleed, one statement |
notes[…] |
speaker notes, visible only in the presenter console |
A = heading starts a section and gets a divider slide; a == heading titles a
slide. Both show up in the presenter console's slide list.
Speaker notes keep their markup: lists, *emphasis*, *bold*, raw, links and
headings all render as such in the console, so a note can be structured rather
than one long paragraph.
Video #
Typst cannot render video, so #video reserves a correctly-sized box and records
where it landed on the page. The server reads that geometry and floats a real HTML
<video> over the slide at exactly that rectangle — placement stays authored in
Typst, playback is the browser's.
#video(
"https://www.youtube.com/watch?v=…", // or a path relative to the deck
start: "1:30", // optional; see "Timestamps" below
end: "2:00", // optional
width: 70%, // height defaults to 16:9
loop: true, // restart at `start` on reaching `end`
poster: "still.png", // optional
name: "The demo", // shown in the presenter console
)
Timestamps. Typst has no 30s literal, so start and end take a number of
seconds (start: 90), a clock string (start: "1:30" or "1:02:03"), or a
duration (start: duration(minutes: 1, seconds: 30)).
A video counts as one overlay step. Landing on the slide shows the first frame and nothing plays. The next Space starts it; the press after that moves on and stops it. Stepping back onto the slide re-arms it. With several videos on a slide they arm in document order.
Scrubbing. The presenter console's copy of the clip has ordinary player
controls, and driving them drives the display too: seek, pause or resume there and
the cast screen lands on the same frame. Pausing holds the position rather than
rewinding — only leaving the slide, or stopping the clip, sends it back to the
start. Scrubbing outside a clipped start/end window suspends the loop, so you
can look at the rest of the footage; playing it from the top restores it.
Enlarging. v, or the presenter console's enlarge button, blows the
current slide's video up to fill the whole screen, and again puts it back. It is
driven from the presenter and applies to the display, so the audience sees the clip
full-size without you touching that screen. Leaving the slide, or stopping the
clip, restores it on its own.
YouTube URLs are fetched with yt-dlp in the background and cached under
$XDG_CACHE_HOME/presentypst/media, then served locally — so once a clip is
cached, presenting needs no network. Clips are downloaded whole and trimmed in the
browser, so adjusting start/end never costs another download.
When YouTube breaks yt-dlp. It does, regularly, and the symptom on the slide is
HTTP Error 403: Forbidden or Requested format is not available. presentypst
already asks through player clients that work around the current breakage, but that
default will rot. Update yt-dlp first; if that is not enough, override the
arguments:
$ presentypst talk.typ \
--yt-dlp-arg --extractor-args --yt-dlp-arg youtube:player_client=web_safari
--yt-dlp-arg is repeatable and replaces the built-in default entirely. To find a
client that works, try yt-dlp --extractor-args youtube:player_client=X -F <url>
by hand.
Controls #
Both views answer to the same keys.
| → ↓ Space PgDn Enter | next step |
| ← ↑ PgUp Backspace | previous |
| Home / End | first / last |
| b | blank the display |
| v | enlarge this slide's video to fill the screen |
| f | fullscreen the browser window |
| r | reset the talk timer (presenter) |
The display view waits for one gesture — any key, click or scroll — before it starts. That is not decoration: browsers refuse to play audio on a page nobody has interacted with, so without it a video started from the presenter console would come up silent.
A presentation remote sends ordinary key events, so clicking it once clears the prompt without anyone touching the cast screen. That first press only dismisses the prompt — it does not advance, so the talk starts on slide one rather than skipping straight past it.
Live reload #
The deck and everything it imports are watched. On save it recompiles and pushes the new slides to both views, holding your position. A deck that fails to compile leaves the previous one on screen and shows the diagnostic in a box — a typo mid-talk costs nothing.
Pass --no-watch to compile once and serve.
Options #
presentypst <DECK>
--root <DIR> resolve imports and media against DIR (default: the deck's directory)
--host <ADDR> bind address (default: 0.0.0.0, so other devices can reach the display)
-p, --port <PORT> (default: 8080)
--no-watch compile once, do not watch for changes
--yt-dlp-arg <ARG> extra argument for yt-dlp; repeatable, replaces the default
Building #
$ nix build # the packaged binary, with yt-dlp and the Typst packages wired in
$ nix run . -- examples/demo.typ
$ nix develop # a shell with the toolchain, typst, yt-dlp and ffmpeg
The flake vendors the Typst Universe packages the library needs as fixed-output
derivations, so nix build is hermetic and the packaged binary resolves
@preview/… imports without network access.
When something looks wrong #
$ RUST_LOG=presentypst=debug,tower_http=debug presentypst talk.typ
That logs every request, so when a slide does not appear on the cast screen you can see whether that screen actually asked for it — which is almost always the first thing worth knowing.
How it fits together #
The typst crates are embedded, so there is no typst subprocess and no temp
files — a recompile happens in memory. Metadata (speaker notes, slide titles,
video geometry) is read straight out of the compiled document's introspector
rather than by running a second query pass over it.
typst/presentypst/ |
the Typst library, embedded into the binary |
src/world.rs |
the Typst World: bundled-package namespace, fonts, dependency tracking |
src/compile.rs |
compile → SVG pages + metadata |
src/state.rs |
navigation rules, including the video-as-overlay-step rule |
src/media.rs |
local files and yt-dlp downloads |
src/server.rs |
HTTP routes and the websocket |
web/ |
the two views |