A plain-text playwriting format with a simple way to write, preview, and export PDF manuscripts.
Downstage is a plaintext markup language for writing stage plays, inspired by
Fountain (for screenplays) and the archived
TheatreScript spec. It
gives you three clear ways to start: the Web Editor, the VS Code
extension, or the command line. Files use the .ds extension.
If you just want to start, open the Web Editor.
Read the Syntax Guide.
The Pages site is built with Eleventy from the
templates in site/.
npm install
npm --prefix web install
npm run build:site
npm run serve:site# The Example Play
Author: Jane Smith
Date: 2025
Draft: First
## Dramatis Personae
HAMLET - Prince of Denmark
HORATIO - Friend to Hamlet
### Courtiers
ROSENCRANTZ - A courtier
GUILDENSTERN - A courtier
## ACT I
### SCENE 1
> The battlements of Elsinore Castle. Night.
HORATIO
Who's there?
HAMLET
(aside)
A piece of work is man, how **noble** in reason,
how *infinite* in faculty.
In form and moving, how express
and admirable; in action, how like
an angel.
HORATIO
They're here.
HAMLET ^
Then let them come.
// A line comment
> Enter GHOST
===
### SCENE 2
ROSENCRANTZ
Good my lord!
SONG 1: The Wanderer's Lament
HAMLET
O, that this too, too solid flesh
Would melt, thaw, and resolve itself
Into a dew.
SONG END
- Readable plaintext format — scripts look natural without markup noise
- Inline formatting:
*italic*,**bold**,***bold italic***,_underline_,~strikethrough~ - Verse support via indentation (2+ spaces)
- Dual dialogue with trailing
^on the second cue - Songs with
SONG/SONG ENDblocks - Comments:
// lineand/* block */ - Forced elements:
@characterand.headingfor edge cases - Character aliases:
HAMLET/HAMor[HAMLET/HAM] - Page breaks:
=== - LSP server with:
- Semantic syntax highlighting
- Document outline (acts/scenes/characters)
- Hover info on character names (shows description from dramatis personae)
- Go-to-definition (jump to character's dramatis personae entry)
- Rename Symbol for characters (updates dramatis personae entry, aliases, and matching cues — refuses on prose mentions)
- Context-aware completion for character cues
- Diagnostics (parse errors, unknown character warnings, Dramatis Personae hygiene, cue hygiene)
- Code actions (quick fixes for unknown characters, unnumbered or misnumbered acts/scenes, duplicate DP entries, and inserting a missing Dramatis Personae section)
- Neovim integration out of the box (0.11+)
- CLI tools for parsing and validation
If you are new to Downstage, you probably want the Web Editor or the VS Code extension before you want installation steps.
go install github.com/jscaltreto/downstage@latest
brew tap jscaltreto/tap
brew install downstage
downstage parse play.ds # Output AST as JSON
downstage validate play.ds # Check for errors
downstage stats play.ds # Report word/dialogue/runtime stats
downstage render play.ds # Render to PDF (default)
downstage render -f html play.ds # Render to HTML
downstage revisions play_v2.ds --against play_v1.ds # Render revision pages
downstage revisions play.ds --from HEAD~1 # ...against a git ref
downstage lsp # Start LSP server (stdio)
downstage version # Print version info
Use -v or --verbose to enable debug logging on any command.
downstage validate exits non-zero on parse errors. downstage parse prints
parse errors to stderr but still emits the AST JSON. downstage render exits
non-zero if parsing fails.
downstage render supports PDF (default) and HTML output via --format:
downstage render play.ds # PDF, manuscript
downstage render --format html play.ds # HTML, manuscript
downstage render --format html --style condensed play.ds
downstage render --format html -o play.html play.ds # explicit output file
Both formats support two styles via --style:
standard(default, Manuscript) — Traditional manuscript format. Character names centered above dialogue, generous margins.condensed(Acting Edition) — Acting edition format designed for rehearsal use. Character names inline with dialogue (e.g.HAMLET. To be or not...), tighter spacing.
PDF also supports --page-size letter (default) and --page-size a4.
Manuscript output renders on the selected sheet size. Acting edition output
derives its logical page from that sheet size: half-letter for Letter, A5 for
A4.
Acting edition can be imposed for print via --pdf-layout:
single(default) — one logical page per sheet2up— two logical pages per landscape sheetbooklet— duplex booklet order, padded to a multiple of 4. Print double-sided, then fold in half.--gutter <measurement>sets the inside gap (default0.125in; acceptsinandmm).
HTML produces a self-contained document with embedded CSS using semantic
.downstage-* class names for custom styling.
downstage revisions produces a small PDF containing only the pages that changed
between two drafts of a play — the Hollywood "revision pages" workflow. Print
the result and slot the pages into a v1 binder by their A/B/C-suffixed numbers;
changed blocks get a right-margin asterisk and each page has a top-margin note
indicating where it inserts or replaces in the v1 print.
downstage revisions v2.ds --against v1.ds # explicit prior version
downstage revisions v2.ds --from HEAD~1 # prior version from git
downstage revisions v2.ds --against v1.ds -o rev.pdf # custom output path
Useful flags:
--page-numbers v1-labels|natural|none— defaultv1-labels(A/B/C suffix off v1 page numbers).naturalnumbers from 1,nonesuppresses the footer.--mark-changes / --no-mark-changes— right-margin asterisks (default on).--anchor-window N— merge change hunks separated by ≤ N equal blocks into a single region (default 4). Lower for tighter regions, higher for fewer.--removed-marker / --no-removed-marker— emit a REMOVED placeholder page when a revision shortens v1's pagination (default on).
Exit codes: 0 success, 1 parse/render error, 2 no differences detected, 3 git-ref resolution failed.
Block-level granularity: when one word changes inside a long dialogue cue, the whole cue is marked. Per-visual-line marking is a planned follow-up.
downstage stats reports manuscript metrics derived from the parsed AST:
word counts, dialogue breakdown, per-character speeches and lines, scene/act
counts, and a rough runtime estimate.
downstage stats play.ds # Human-readable summary
downstage stats play.ds --format json # Machine-readable output
downstage stats play.ds --rate slow # Use the 110 wpm preset
downstage stats play.ds --wpm 140 --pause 0 # Custom rate, no pause overhead
Runtime is based on spoken dialogue word count divided by a speaking rate, plus a small pause factor for pacing:
spoken_minutes = dialogue_words / words_per_minute
estimated_minutes = spoken_minutes * (1 + pause_factor)
Presets: slow (110 wpm), standard (130 wpm, default), conversational
(150 wpm). The default pause factor is 10%. Stage directions and prose do
not contribute to the runtime estimate. Treat the output as a rough guide,
not a prediction.
Install downstage.nvim with your plugin manager:
-- lazy.nvim
{ "jscaltreto/downstage.nvim", ft = "downstage" }This provides filetype detection, buffer settings, and LSP integration for .ds
files. The downstage binary must be on your PATH.
If you use nvim-cmp, enable the plugin's optional completion integration to
limit Downstage buffers to LSP-driven cue completions:
-- lazy.nvim
{
"jscaltreto/downstage.nvim",
ft = "downstage",
opts = {
cmp = true,
},
}Any LSP-compatible editor can use the Downstage language server. The server communicates over stdio using JSON-RPC 2.0 (LSP 3.17). Point your editor's LSP client at the server:
{
"command": ["downstage", "lsp"],
"filetypes": ["downstage"],
"rootPatterns": [".git"]
}The editors/vscode/ extension provides full Downstage
support: LSP-powered completions, diagnostics, and folding; live PDF preview;
render-to-PDF commands; snippets; and TextMate syntax highlighting.
See the extension README for details.
For local development:
cd editors/vscode
npm install
npm run compileThen open the editors/vscode folder in VS Code and press F5 to launch an
Extension Development Host.
Alpha / pre-release. The desktop app is under active development. No pre-built binaries are published yet, the on-disk config format may still change, and some workflows (drafts, cross-device sync, auto-update) are unimplemented. Build from source and expect rough edges.
cmd/downstage-write/ is a native desktop build —
the same Vue editor wrapped in a Wails shell with a
single-library workflow: open a directory of .ds files, edit in place,
snapshot revisions with git, and restore older versions from a native
menu + command palette.
- Go 1.23+
- Node 20+
- Wails v2 CLI:
go install github.com/wailsapp/wails/v2/cmd/wails@latest - Frontend deps:
npm --prefix web install(one-time)
Run wails doctor after installing the CLI — it lists anything else your
system is missing.
Linux additionally needs webkit2gtk 4.1
(sudo apt install libwebkit2gtk-4.1-dev on Debian/Ubuntu; equivalents
via dnf/pacman on other distros). macOS has no extra system packages.
make desktop-dev # live-reloading dev build
make desktop-build # release build → cmd/downstage-write/build/bin/make desktop-dev (via wails dev) serves the real desktop entry
(desktop.html → AppDesktop.vue) with HMR; it's independent of the
browser-app dev flow at make web-dev, which keeps serving the Vue
web editor at /editor/.
Windows uses WebView2, which ships with Windows 10 20H1+ and Windows 11. On older Windows, install the Evergreen runtime from Microsoft. No other system packages are needed.
Since GNU make isn't part of Windows, use the bundled PowerShell wrapper (from the repo root):
.\scripts\build-desktop.ps1 # release build
.\scripts\build-desktop.ps1 -Mode dev # hot-reload dev modeThe binary lands at cmd\downstage-write\build\bin\downstage-write.exe.
See internal/desktop/AGENTS.md for the
backend architecture and web/src/desktop/AGENTS.md
for the frontend layer.
Start writing in the Web Editor, a modern browser-based editor built with Vue 3, Tailwind CSS v4, and CodeMirror 6. It features live preview, adaptive syntax highlighting, completions and quick-fix code actions sourced from the Downstage LSP, spellcheck with a per-script dictionary, and PDF export. No install required; the entire pipeline runs client-side via WebAssembly.
For local development:
npm --prefix web install
make web # Build the editor for Pages output
make web-dev # Run the editor dev server with ViteSee web/README.md for details.
A Downstage document is organized around top-level # sections:
- Play Header — each play starts with
# Title, followed by optionalKey: Valuemetadata lines such asAuthor:orDraft: - Dramatis Personae — an optional
## Dramatis Personae,## Cast of Characters, or## Characterssection inside that play, with character entries and optional###subgroup headings; rendered output keeps the chosen wording - Body — the play itself: acts (
## ACT), scenes (### SCENE), dialogue (ALL CAPS character name followed by speech text), stage directions (>prefixed lines), callouts (>>prefixed lines), verse (indented 2+ spaces), songs, and comments
To mark simultaneous dialogue, put ^ at the end of the second character cue.
HORATIO
They're here.
HAMLET ^
Then let them come.
The renderer places the two dialogue blocks side by side when they fit on the page. If they do not fit cleanly, rendering falls back to sequential dialogue instead of producing broken columns.
See SPEC.md for the complete language specification.
git clone https://github.com/jscaltreto/downstage.git
cd downstage
make
To embed version information:
go build -ldflags "\
-X github.com/jscaltreto/downstage/cmd.version=1.0.0 \
-X github.com/jscaltreto/downstage/cmd.commit=$(git rev-parse HEAD) \
-X github.com/jscaltreto/downstage/cmd.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-o downstage .
Release PRs and changelog updates are managed by Release Please. Merging a
release PR creates the Git tag and GitHub release, and GoReleaser then builds
artifacts for macOS, Linux, and Windows on amd64 and arm64.
For local release validation:
make release-check
make release-snapshot
These targets require goreleaser to be
installed locally.
Publishing the Homebrew formula is handled by the release workflow, which
updates jscaltreto/homebrew-tap after GoReleaser publishes release assets.
This requires a repository secret named HOMEBREW_TAP_GITHUB_TOKEN with push
access to jscaltreto/homebrew-tap.
Release Please requires a repository secret named RELEASE_PLEASE_TOKEN with
enough access to create release PRs, tags, and releases in
jscaltreto/downstage.
Downstage source code is licensed under the MIT License.
Bundled fonts in internal/render/pdf/fonts/
remain under the SIL Open Font License 1.1; see the included OFL.txt and
OFL-LibreBaskerville.txt files.
- Fountain — screenplay markup language that inspired Downstage's plaintext philosophy
- TheatreScript — archived stage play markup spec that Downstage builds on
