landon 2f3780a578 Give Misc Scenes a stub file so it actually shows in the file panel
The panel infers folders from markdown file paths; a folder with only
a .gitkeep never gets a row. Act 1/2/3 avoid this with their own
scene01.md stubs — Misc Scenes was missing one, so new projects
shipped it invisible.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-30 12:22:14 -05:00

md-manuscript

A small desktop application for Debian that treats a directory of markdown files as an ordered manuscript.

  • Left pane — every *.md file in the workspace, as a collapsible folder tree, in a manual order you set by dragging the ⠿ handle up and down. Drag a file onto a folder to move it there. Hover a file to see a tooltip built from its header — the POV: line and the # Slug: synopsis — a quick at-a-glance summary of each chapter.
  • Right pane — a markdown text editor for the selected file (optional live preview via the Preview checkbox).
  • Order + contents are saved and version-controlled with git. The order lives in order.json inside the workspace, so it syncs along with the files. It records each file's path, so subfolders travel with it.
  • Export the whole manuscript as a single concatenated .odt file, with a chapter heading before each file's content — or, for a screenplay project, as a paginated Fountain-to-PDF script (see Screenplays).
  • Start a new project from a bundled template (File ▸ New project…), which builds the whole folder structure and opens its drafting folder.

Built in Rust with egui/eframe. The ODT and PDF writers are both native (no pandoc/LibreOffice needed at runtime).

Build

cargo build --release
# binary: target/release/md-manuscript

Requires a Rust toolchain. The GUI needs a normal desktop session (X11 or Wayland) with OpenGL — the usual runtime libraries on Debian:

sudo apt install libgl1 libxkbcommon0 libwayland-client0 libx11-6

(Only needed to run the GUI; building does not require them.)

Install

install -Dm755 target/release/md-manuscript ~/.local/bin/md-manuscript

That's it — the File ▸ New project… template is compiled into the binary, so there's nothing else to install alongside it. If you want to use a customized copy of the template instead of the bundled one, point Settings ▸ New project… ▸ Template at a folder of your own containing a cookiecutter.json; leave it blank to keep using the bundled template.

Optionally add a desktop launcher at ~/.local/share/applications/md-manuscript.desktop:

[Desktop Entry]
Type=Application
Name=md-manuscript
Exec=md-manuscript
Categories=Office;TextEditor;
Terminal=false

Use

  1. Set the Workspace path in the top bar (default: ~/Manuscript) — type it, or click 📂 to pick a folder in a native file browser — and press Open. The directory is created if it does not exist.

  2. Press Init git once to make the workspace a git repository. To sync with another machine, point it at a remote through Settings ▸ Git remote…: paste the repository's clone URL into the origin box and press Set origin. The repository has to exist on the server already — this attaches to it, it does not create it. Clearing the box and pressing Set detaches again. The equivalent from a terminal, if you prefer:

    cd ~/Manuscript
    git remote add origin <url>
    

    Either way the URL lives in the repository's own .git/config, not in this app's settings, so every workspace keeps its own.

    The same dialog can make the repository for you. Fill in the Gitea rows under Settings ▸ New project (server, user, token — the token needs write access to repositories), then give the repository a name and press Create and attach. The app calls Gitea's API itself, then points origin at what came back. A name that already exists is attached to rather than treated as an error, so a second attempt after a failure does the right thing. Untick Attach over SSH to use the HTTPS clone URL instead, which asks for a credential on every push.

    The SSH host comes from the server URL you configured, not from the URL Gitea advertises. Gitea builds that from its own SSH_DOMAIN, which is whatever it was told to announce — and a server announcing a public hostname whose SSH port is only reachable on the LAN hands out a remote that connects from nowhere, with the failure surfacing at the first push rather than at creation. The server URL is known to work, because the API call just went over it. The port Gitea reports is kept as-is. So on a tailnet, setting the Gitea server to http://100.x.y.z:4000 gets you ssh://git@100.x.y.z:2228/…, which works on and off the LAN alike.

    After that, the ⟳ Sync (git) button commits all changes, pull --rebases, and pushes (setting the upstream on the first push). Until origin is set it commits locally and says so in the log.

    If you open a folder that is not itself a repository but sits inside one (a parent directory is a git work tree), the app asks whether to use the enclosing repository. Accept it to have ⟳ Sync commit and push to that repo; decline to leave the workspace on its own, where Init git creates a separate repository in the folder.

  3. Create files with + New (type a name first — give it a path like part-1/ch-01 to nest it, see Organising with folders) or + New from template (no name needed — see New files from a template), edit on the right, Ctrl+S (or the Save button) to write to disk. Drag the ⠿ handles to reorder or to move between folders. Use the Zoom slider above the editor to scale the editor text (0% = default; Reset returns to it), and the Contrast slider to brighten dim editor text (0% = theme default; higher pushes the text toward white in dark mode / black in light mode). Text always wraps to the pane width, so there is no horizontal scrolling. Inline markdown has keyboard shortcuts, applied to the current selection (press again on wrapped text to remove the markers):

    Shortcut Effect
    Ctrl/Cmd + B bold (**…**)
    Ctrl/Cmd + I italic (*…*)
    Ctrl/Cmd + E inline code
    Ctrl/Cmd + Shift + X strikethrough (~~…~~)
    Ctrl/Cmd + K link — [selection](url), with url selected to replace
    Ctrl/Cmd + / comment the selected lines out (<!-- … -->), or take it back off

    Ctrl/Cmd + / is the odd one out: it works on whole lines, growing the range out to line boundaries first, and it toggles the block off again only when every non-blank line in it is already commented. Commented lines stay in the file but drop out of the export and the word count, so this is the way to shelve a paragraph without losing it. Blank lines in the range are left as-is so paragraph breaks survive; pressing it on an empty line opens <!-- --> with the caret inside.

    Press Ctrl/Cmd + F (or Edit ▸ Find / Replace…) to open the find/replace bar above the editor; Ctrl/Cmd + H opens it with the replace field focused. Type a search term to shade every match in the editor (the current one brighter); Enter / Shift+Enter (or ▼ / ▲) step through them, wrapping around, and scroll the match into view. Aa toggles case sensitivity. Replace swaps the current match and advances; Replace all swaps every match in the file at once. Esc closes the bar.

  4. Set the Chapter title for the open file if you want to override the auto-derived one (the field shows the automatic title as a hint). Leaving it blank falls back to a # Title: line in the header, and if there is none, to the chapter's position in the left pane followed by a period (1., 2., …). Tick Zero-pad # in the top bar to pad those numbered titles with leading zeros to a uniform width (e.g. 03. when there are 12 chapters). Titles are saved to titles.json and sync with git.

  5. Set the Export path (type it, or click 📂 to choose the .odt file in a native save dialog) and press Export ODT to produce the concatenated document. Each file becomes a chapter headed by its title, optionally followed by a caption from its # Slug: line. Every chapter starts on a new page (the first chapter excepted, so there is no blank leading page).

The status bar shows a live word count for the current file, plus the net words added (or removed) to it since the current session started. The count updates as you type, including unsaved edits.

Per-file word-count targets

Give a file a target by adding a Word Count Target: line to its header (at any heading level):

## Word Count Target: 1500 - 2000

A progress bar then appears in two places, comparing the file's prose word count (the body below the draft marker, so header metadata isn't counted) against the target: amber while under the target, green once inside the range, blue when over it.

  • A compact bar beside the file name in the left pane, so you can scan every chapter's progress at a glance (hover it for the exact words / target). It updates live as you type in the open file, and reflects the last saved state for the others.
  • A larger, labelled bar in the status bar for the file you're currently editing.

A single number (## Word Count Target: 1800) sets a point goal; ranges accept -, –, to, and grouped digits (1,500). Like the other header lines, the target is stripped from the exported document.

YAML frontmatter for planning metadata

A scene keeps what it knows about itself in YAML frontmatter at the top of the file, and its prose below. This is the form + New Scene writes and the form the snowflake template ships:

---
title: "Chapter One: The Beginning"
slug: chapter-one-the-beginning
act: 1
chapter: 1
scene: 1
status: draft
pov: "Bixby"
characters: [Bixby, Mom]
setting: "The Old Reynold's House"
date: ""
time: ""
conflict: ""
theme: ""
tone: ""
word_count_target: 1800-2200
---

The prose begins here.

Everything below the closing --- is prose, and nothing else: that is what the status bar counts, what the progress bars measure, and what the exporter writes into the .odt. Restating the header underneath it — a metadata table, a POV section, a word-count tracker — puts all of that in the word count and in the finished book, so the header is written once, above the line.

Fields the app reads: title (the chapter heading), slug (its caption, and the file-list tooltip), pov (also the tooltip), status (the badge and filter in the file panel), characters (which scenes a character appears in, in the Characters window) and word_count_target (the progress bars). A word_count_min/word_count_max pair works too, and wins over word_count_target when both are given. The rest — act, chapter, scene, setting, date, time, conflict, theme, tone — the app carries but does not act on; they are yours, and anything else you add is kept the same way.

Lists take either YAML form — characters: [Bixby, Mom] or

characters:
  - Bixby
  - Mom

Values may be quoted or bare, an empty value reads as no value at all (an unset pov: "" is not a POV), and # comments are ignored, so the template's notes to itself cost nothing.

The frontmatter can be folded away while you write with View ▸ Hide header in editor, the same as a legacy header.

Legacy files still work. A file with no frontmatter falls back to the older form — # Title:, # Slug:, # POV: lines above a ### Rough Draft: marker, described under Header stripping on export. The two are read through the same code, so a status filter or a character search covers both. Frontmatter wins wherever a file has it.

Project mode

Open a project root — a folder holding the manuscript alongside the characters, outline, premise and the rest — and the app recognises the layout instead of treating every file in it as a chapter.

The trigger is simple: if the folder you open contains a subfolder named like the manuscript folder (05-First Draft by default), it is a project. A plain folder of chapters has no such subfolder, so it behaves exactly as before — everything in it is the manuscript.

In a project:

  • Files under the manuscript folder are the book. They are ordered, numbered, and they are what Export ODT concatenates. Chapter numbering counts only them, so a defaulted title is 3. because it is the third chapter, not the third file in the project.

  • Everything else is reference — character sheets, the outline, the scratch pad — and is not listed in the file panel, which stays about the book. Reference material is reached through the windows that understand it (Characters, Story Bible, Outline), and it stays out of the order and out of the export.

    Turn on View ▸ Show reference files to list it in the tree anyway, dimmed, with the manuscript folder tagged · manuscript.

  • order.json, titles.json and wordlist.json live at the project root, so one word list covers the whole project rather than one folder of it.

  • File ▸ New project… opens the project root, not the drafting folder.

Set which folder is the manuscript under Settings ▸ New project… ▸ Manuscript folder. The number is optional — First Draft matches 05-First Draft — and the setting is global, so every project on this machine uses the same layout.

Hidden folders

An archive of superseded drafts can hold hundreds of files that would swamp the tree, so folders named like Archive are skipped by the scan entirely — the same treatment target/ and node_modules/ already get. The match ignores a leading number, so 10-Archive is covered.

Change the list with hidden_folders in ~/.config/md-manuscript/config.json; set it to [] to see everything.

Organising with folders

The file list is a tree. Any *.md file at or below the workspace is part of the manuscript, so you can group a long book into part-1/, part-2/, or go further and give a chapter its own folder of scenes — nesting goes eight levels deep.

  • Make a folder by giving a new file a path: type part-1/ch-01 beside + New and both the folder and the file appear. There are no standalone empty folders — a folder exists for as long as it holds a file, and is cleaned up automatically once its last file leaves.
  • Collapse or expand a folder by clicking its header. The number beside the name counts every file beneath it, at any depth.
  • Move a file by dragging its ⠿ handle onto a folder header, or in among another folder's files. The file really moves on disk, taking its chapter-title override with it. Drop it on ↥ drop here for the top level to bring it back out of every folder.
  • Move by typing instead, if you prefer: the Rename box holds the file's whole path within the workspace, so editing the folder part of it moves the file just as dragging does.
  • + New from template puts its untitled-N.md in the folder of the file you are editing, so working inside a part doesn't scatter new files at the top.
  • Right-click a file for a menu of the things you can do to it: 📝 Rename…, ⧉ Copy path, 🗄 Archive and 🗑 Delete…. It acts on the file you clicked — right-clicking selects it first — so you do not have to select a file before reaching for the operation. Rename opens a small dialog with the path already filled in and the box focused; it stays open if the name is rejected, so a clash never costs you what you typed. Delete asks first. These are the same operations as the buttons under the list, which stay where they are.

Folders are skipped when they cannot hold manuscript prose: anything starting with a . (so .git is never walked), plus target/ and node_modules/.

Folders and the manuscript order

Order is still yours to set by dragging, and still lives in order.json — as paths (part-1/ch-03.md) rather than bare names. The one rule the app enforces is that a folder's files stay together: a folder sits wherever its earliest-ordered file puts it, and everything under it follows in a block.

That is what keeps the panel honest — the export is the tree read top to bottom, so the order you see down the left is the order the chapters are concatenated in, folders and all. Chapter numbering follows the same sequence, so a defaulted title in part-2/ continues counting from part-1/.

Working on the novel's other parts

A manuscript project is more than its chapters. These read the rest of it.

Distraction-free writing (Ctrl+D)

Ctrl+D folds away everything that is not the page — the toolbar and the file list together — leaving the menu bar, the editor and the status line. Press it again to bring both back. It is also View ▸ 🖹 Distraction-free.

The two panels toggle separately as well, from the pair of buttons at the right of the menu bar or from View ▸ Toolbar and View ▸ File list:

Hides Gives back
⬆ / ⬇ the Workspace, Export, Grammar and Spelling rows about an inch of height
⬅ / ➡ the file list down the left about 260px of width

Both choices are remembered between runs, so the window opens the way you left it. The toggles live in the menu bar rather than in the panels they hide, since a control that hides a panel cannot sit inside it, and the menu bar is the one strip that never goes away. The status bar is unaffected — the word counts stay visible while you write.

The editor grows into whatever the panels give back rather than staying a fixed height, so folding them is worth real writing space and not just grey margin.

Hiding the header while you write

A scene's fields sit at the top of the same file as its prose, and once drafting is under way they are mostly in the way. View ▸ Hide header in editor folds them away, leaving the prose alone in the editor. A single line above the editor says so — ▸ header hidden (15 fields) — and clicking it flips back.

Either kind of header folds: a YAML frontmatter block, delimiters and all, or a legacy block above the draft marker. That line is the quickest way to do it — click ▾ header shown to fold, click ▸ header hidden to bring it back.

While the header is hidden it is genuinely not in the editor's text: the file is put back together on save, so what lands on disk is unchanged. Doing it that way rather than merely scrolling past the header means search, replace, spelling underlines and the caret all address exactly what you can see, with no offsets to go wrong. Everything that wants the whole file — the chapter title hint, the word-count target, the file-list tooltip — still reads it whole.

A file with neither frontmatter nor a draft marker has nothing to fold, so the line does not appear.

Characters (Tools ▸ Characters…)

Reads the project's character sheets — a file whose level-1 heading is the name, as either # Bixby Reynolds or # Name: Jonathan Bronski. For each character it shows:

  • their descriptor, from a Slug: field or the heading beneath the name;
  • which scenes they appear in, by matching the Characters: line of every manuscript file. A first name matches a full name, so Characters: Bixby, Mom finds Bixby Reynolds. Click a scene to open it;
  • ▦ Rebuild grid, which regenerates the project's character_grid.md from the sheets as a markdown table. A hand-kept grid drifts the moment a sheet changes; this one cannot. Long values are trimmed, since a grid is a quick-look summary.

Story Bible (Tools ▸ Story Bible…)

The Story Bible window indexes all Story Bible items from your project's character, location, lore, and other reference directories. It supports:

  • Automatic indexing from the first directory found: 03-Story-Bible (the bundled template's own folder), then 03-Characters, 02-Story Bible, Characters, Locations, Lore, and so on.
  • Automatic linking in the draft — no markup required. A name spelled and capitalized the same way as an item's name: or an aliases: entry is matched wherever it appears in ordinary prose ("Bixby stood at the door"). Two explicit syntaxes still work too, and win any overlap with an automatic match:
    • Wiki-style: [[Character Name]] or [[John Doe]]
    • Mention-style: @CharacterName or @John
  • Purple background highlighting of every match — automatic or explicit — in the editor. Hovering one shows a card with the item's name, type, and a summary (see below), and a link to open its full entry; the card stays put while you move the pointer onto it to click that link.
  • Click items in the Story Bible window to see all references to them in the current file
  • Click reference buttons to jump directly to that location in the editor
  • Metadata extraction from YAML frontmatter (id, name, type, aliases, summary fields) or from filenames
  • Case-insensitive search and lookup
  • Aliases support: References match item IDs, names, and aliases for flexible linking

Character sheets in the Characters folder are automatically indexed, and any markdown file with YAML frontmatter containing id: and name: fields will be included in the index (title: is read too, if a sheet uses that word instead). You can also add aliases: for alternative names, and summary: for the one-line hover card:

---
id: john_smith
name: "John Smith"
type: character
aliases: ["Johnny", "JS"]
summary: "A retired locksmith dragged back for one last job."
---

With aliases defined, references like [[Johnny]], @JS, or a bare "Johnny" in prose will all match the "John Smith" character entry. Every reference in your draft that matches a Story Bible item — by ID, name, or alias — is highlighted and clickable, whether or not you typed any markup to get it there.

Story Bible Editor (Edit ▸ Story Bible…)

For a dedicated editing experience, use the Story Bible breakout editor. This provides a focused workspace for managing your Story Bible items:

  • Tree view (left 1/3 of window): Shows the complete Story Bible directory structure with collapsible folders for Characters, Locations, Lore, and other categories
  • Editor (right 2/3 of window): Full-featured text editor for the selected item
  • Auto-load: Select any file from the tree to load it into the editor
  • Save: Click Save or press Ctrl+S to write changes back to disk
  • Index updates: Saving a file automatically rebuilds the Story Bible index

The editor is ideal for extended Story Bible work, allowing you to browse and edit items without leaving the context of your reference material.

Outline (Tools ▸ Outline…)

The snowflake outline files are scaffolds of HTML comments, one per beat (<!-- Midpoint: A major turning point. -->). The app strips comments, so these files otherwise render and export as blank — there is no way to see how much of the outline exists.

This window reads the scaffold back: every beat it names, and whether prose has been written under it, with a progress bar over the whole outline. A beat counts as written when there is ordinary text between its prompt and the next one.

Heading hierarchy is preserved: markdown headings (#, ##, ###) in the outline files create a nested tree structure in the Outline window. Beats under the same heading are grouped together under a collapsible header showing the progress for that section (3/5 beats written, for example). This makes it easy to navigate large outlines and see progress at every level of your story structure.

Beats generated by the Mistral tool can be appended straight into the matching act file with 📝 Append to act_0N.md, which appears when a run filled exactly one act. It appends under a dated comment rather than replacing, because the prompts are the reason the file is worth keeping.

Auto-Snapshots (Edit ▸ Snapshots…)

Never lose your work again. Enable automatic git commits at regular word-count intervals to create recovery points you can roll back to at any time.

  • Enable/Disable: Turn auto-snapshotting on or off from the Snapshots window
  • Configurable threshold: Set how many words between snapshots (default: 250)
  • Automatic commits: Commits are created automatically when you cross a threshold boundary (250, 500, 750, 1000, etc.)
  • Manual snapshots: Click "Create Manual Snapshot" to force a commit at any time
  • Rollback: Restore your entire workspace to any previous snapshot
  • Live status: See current word count and words until next snapshot

Snapshots appear in the Snapshots window with:

  • Word count at the time of the snapshot
  • Timestamp
  • Commit message
  • Commit hash (shown on hover)

Important: This feature requires the workspace to be a git repository. Snapshots are regular git commits with messages like "Auto-snapshot at N words", so they integrate with your normal git workflow.

Revision status

Give a file a Status: header line and the file panel shows a compact badge for it (initials, so Structural Edit becomes SE) and offers a Status filter above the list. The filter narrows the tree to one stage — every file you have still to proofread, say.

Archive

🗄 Archive moves the selected file into the project's archive folder, keeping its place inside it (Act 1/scene.md → 10-Archive/Act 1/scene.md). It leaves the manuscript order and, since archive folders are hidden, the file panel — but stays on disk. Deleting is for things you want gone; this is for things you have moved past.

Changes since the last commit (Edit ▸ Changes…)

Diffs the open file against its committed version, coloured the way git colours a diff. Revising is comparing, and the repository already holds the answer.

Word counts

The status bar carries the open file's word count, its change this session, and the whole manuscript's total. Reference files are not counted.

Manuscript details (Settings ▸ Manuscript details…)

Title, author and contact details. The title and author are written into exported .odt files as their document properties — what a word processor shows under File ▸ Properties. A blank title uses the project folder's name. Exports previously carried no metadata at all.

Standard manuscript format lays the export out the way an agent or an editor expects a submission to arrive, which is not a matter of taste: a monospaced, double-spaced page is a predictable ~250 words, and the margins leave somewhere for their notes to go. Turning it on gives you

  • 12pt Courier (readers substitute a metric-compatible font where it is absent), double-spaced, with half-inch first-line indents and no blank line between paragraphs — a blank line would read as a scene break;
  • Surname / Title / page flush right on every page but the title page, with the numbering starting at 1 on the first page of text;
  • chapters opening about a third of the way down their page;
  • --- in your markdown rendered as the conventional centred # scene break;
  • a submission title page: contact details in the top-left corner, an approximate word count opposite them in the top-right, and the title a third of the way down with the byline beneath it.

Margins are 1 inch on all four sides of US Letter either way — that part was already standard before the option existed.

Begin exports with a title page puts a page of its own at the front of every export: the title, the author beneath it, and the contact details beneath that, with the first chapter starting on the page after (in standard manuscript format it takes the submission layout described above instead). The contact box is free-form and laid out line for line as typed, so an address, an email and a phone number each land where you put them — including the blank line you leave between them. The option is off by default, and an export with nothing to put on the page skips it rather than opening on a blank sheet.

For a chapters-plus-master export the title page goes on the master, not on each chapter file.

Screenplays (Fountain)

Tick Screenplay (Fountain) in Manuscript details to make the project a screenplay. Two things change, and nothing else does:

  • the draft below each file's marker is read as Fountain rather than markdown, and
  • Export ODT becomes Export PDF, writing a paginated screenplay instead of a word-processor document.

The files stay ordinary .md files with the same editorial header (# Title:, # Slug:, ### Rough Draft:), they list and reorder in the left pane the same way, and the header is stripped on export exactly as it is for prose. One file per scene or sequence is the natural layout, but nothing enforces it — the files are joined into one continuous script, because what divides a screenplay is its scene headings, not its chapters.

Fountain needs no markup for the common elements; the shape of the line is enough:

INT. REYNOLD'S HOUSE - KITCHEN - NIGHT

Rain on the window. Bixby stands at the sink with a plate in
each hand, not washing either of them.

BIXBY
(barely)
I said I'd do it.

MOM (O.S.)
That was Tuesday.

CUT TO:

A line starting INT., EXT., EST., INT./EXT. or I/E. is a scene heading. A line in capitals with something directly beneath it is a character cue, and what follows is dialogue until the next blank line; a line in parentheses inside that block is a parenthetical. A line in capitals ending TO: with blank lines above and below is a transition. Everything else is action, and consecutive lines are one paragraph.

Where the shape is not enough, force it: . for a scene heading, ! for action, @ for a character cue, > for a transition. > text < centres a line, === starts a new page, ~ marks a lyric. *italic*, **bold**, ***both*** and _underline_ work as they look. # sections, = synopses, [[notes]] and /* boneyard */ blocks are for you and never reach the page.

The PDF is the conventional layout, so a page reads as roughly a minute of screen time: 12pt Courier on US Letter, 55 lines a page, scene headings and action at 1.5″, dialogue at 2.5″, parentheticals at 3.1″, cues at 3.7″, transitions flush to the 7.5″ right margin, and page numbers in the top-right from the second page. A speech broken by a page boundary is marked (MORE) and picked up under the cue again with (CONT'D); a scene heading is never left stranded at the foot of a page; a ^ cue sets the two speeches side by side. Courier is one of the fonts every PDF reader is required to provide, so nothing is embedded and the file stays small.

Begin exports with a title page works here too, giving the standard spec title page: the title a third of the way down, the credit beneath it, and the contact details at the foot. Standard manuscript format has no effect on a screenplay — a script is always set in screenplay format.

Exporting chapter files (File ▸ Export chapters + master)

Writes one .odt per chapter into a chapters/ folder, plus an .odm master document that links them — the shape the template's Full Text.odm implies.

The per-chapter files are the reliable part: they are ordinary documents and open anywhere. The master is a shell with the title, the author (or the full title page, if you asked for one) and one linked section per chapter; LibreOffice does not follow those links when it opens the file, so treat it as a starting point to relink rather than as the assembled book. For a single finished document, use Export ODT.

New projects from templates

File ▸ ✨ New project… scaffolds a whole manuscript project from a template and then opens it as the workspace, so you go from nothing to editing chapters in one step.

The app uses built-in MiniJinja templating (no external Python cookiecutter required) to render templates. A default "snowflake" template is compiled into the binary itself — nothing has to be installed alongside it. Its source lives in the templates/snowflake/ folder of this repository, for anyone building from source who wants to customise it (a rebuild picks up the change); at runtime, point Settings ▸ New project… ▸ Template at a folder of your own to use a customised copy instead, without rebuilding.

The dialog asks for a name, author, description and the folder to create it in (prefilled with wherever you put the last one). Those are passed to the template as the project_name, author and description variables. Nothing is written until you press Create; generation runs in the background, so a large template doesn't freeze the editor.

When it finishes, the app opens the project root as the workspace, with the 05-First Draft folder (where the snowflake template keeps the chapter files in Act 1/, Act 2/ and Act 3/ subfolders) automatically expanded in the file list. If the template didn't produce that folder, the status bar notes that nothing is marked as the manuscript.

A folder only appears in the file panel once it holds a .md file — the template's empty acts are held open with .gitkeep, which is not markdown, so they show up as you add scenes to them.

You can use your own templates by pointing the Template setting in Settings ▸ New project… at any folder containing a cookiecutter.json file. The template should contain a subdirectory named with a template expression (e.g., {{ cookiecutter.project_name }}) which becomes the project root after rendering. Leave the setting blank (the default) to use the bundled template; an override that stops resolving to a valid template (folder moved, deleted) silently falls back to the bundled one rather than failing project creation.

Settings ▸ New project…

Setting Meaning
Template an external folder holding a cookiecutter.json to use instead of the bundled template. Blank (the default) uses the template compiled into the binary
Manuscript folder which folder of a project holds the book (default 05-First Draft). Its files are ordered, numbered and exported; the rest of the project is reference. A leading number is optional, so First Draft also matches 05-First Draft. A generated project always opens at its root — this does not change which folder the workspace is
Publish to Gitea after creation when enabled and Gitea credentials are configured below, a new repository will be created and the project pushed to it
Gitea server URL, user and token, used to create repositories when publishing is enabled

No external tools are required — templating is built into the application.

Publishing to Gitea

When Publish to Gitea after creation is enabled and all three Gitea settings (Server URL, User, Token) are filled in, the app will:

  1. Initialize a git repository in the new project
  2. Create a repository on the Gitea server
  3. Set the origin remote to point at the new repository

The token is passed to the push command temporarily and is not written into the project's .git/config. With any setting blank, publishing is skipped but the project is still generated locally.

The token is stored in config.json in plain text, the same as the Mistral API key.

This runs in the app itself — it used to be a Python hook inside the template, and is not one any more — so it applies to projects made through New project and nothing else, and it does nothing if the checkbox is off or any of the three Gitea settings is blank. Whatever the server said is reported in the Git log window.

Settings ▸ Git remote… is the same thing for a workspace you already have. It calls the Gitea API directly, works on any folder rather than only a freshly generated one, and reports what the server actually said (a rejected token, a URL that is not a Gitea API, a name the server would not take).

New files from a template

The file list has two create buttons:

Button Name Contents
+ New the name you type beside it, optionally with folders # <name> and a blank line
+ New from template auto-assigned untitled-N.md the configured template
+ New Scene scene-NN.md in the current act a scene's frontmatter, described under YAML frontmatter

+ New from template takes no typed name — it picks the first free untitled-N.md (reusing a gap if you have deleted one) in the folder of the file you are currently editing, seeds it from the template, adds it to the manuscript order and selects it. Rename it later with the Rename box, or fill in title: and let the chapter title come from that.

Edit the template under Settings ▸ New-file template…. The built-in default is frontmatter, the same shape a scene uses:

---
title: {{name}}
slug:
status: draft
pov:
characters: []
word_count_target:
---

Three placeholders are expanded when the file is created:

Placeholder Expands to
{{name}} the file stem, e.g. untitled-3
{{marker}} the draft marker set in the top bar (empty by default)
{{date}} today's date in UTC, as YYYY-MM-DD

Anything else in double braces is left alone. {{marker}} is there for a legacy-style template — using it rather than typing ### Rough Draft: literally keeps such a template working if you change the marker later; a frontmatter template has no use for it. Emptying the box restores the built-in default, and Reset to default does the same in one click.

The template is stored in ~/.config/md-manuscript/config.json, so it is shared by every workspace on the machine rather than committed with a manuscript.

Spelling (offline)

Spelling is checked live as you type, entirely offline — no server, no network. Misspelled words get a red underline; right-click one for a menu of suggested corrections (click a suggestion to apply it). It uses Hunspell dictionaries read by the pure-Rust spellbook crate, so the binary stays self-contained.

  • Two dictionaries are built in — Canadian English (en-CA, the default) and British English (en-GB). Pick one from the Dictionary dropdown on the Spelling row of the top bar; the choice is remembered.
  • More languages are discovered automatically from the usual Hunspell folders (/usr/share/hunspell, /usr/share/myspell, …) and from a per-user folder, ~/.config/md-manuscript/dictionaries/. Drop a matching xx_YY.aff + xx_YY.dic pair in there (e.g. from your distro's hunspell-de-de package) and it appears in the dropdown.
  • Untick “Check as I type” on the Spelling row to turn the underlines off.
  • Code spans, fenced/indented code blocks and link targets are skipped, and ALL-CAPS initialisms (ODT, HTTP) are left alone, to cut false positives.

When you run a LanguageTool check (below), its richer spelling-and-grammar results take over the underlines until you next edit the text — at which point the live offline checker resumes. In other words, LanguageTool is used when it's available and current; the offline checker is the always-on default.

The word list (names and invented terms)

No Hunspell dictionary knows your characters, so without somewhere to record them a novel's cast is underlined on every line of every scene. Settings ▸ Word list… is that place.

  • Right-click an underlined word in the editor and choose 📗 Add “…” to the word list. The underline goes immediately — there is no wait for the next check.
  • 👤 Add character names reads the project's character sheets and lists every name in them at once. It looks for a Characters folder (a numeric prefix like 03-Characters is fine) beside the workspace and up to three levels above it, so it still finds them while you are editing in 05-First Draft. Both # Bixby Reynolds and # Name: Jonathan Bronski headings are understood, and an unfilled template sheet (# Name, # Protagonist) contributes nothing. Each part of a name is listed separately, since prose says Bixby far more often than Bixby Reynolds.
  • Words can also be typed in directly, and taken back out with ✖.

Matching is case-insensitive, and a listed name covers its possessive and plural — Bixby accepts Bixby's, Bixbys and Bixbys' — because the tokenizer hands the checker Bixby's as a single word.

The list is saved as wordlist.json in the workspace, beside order.json and titles.json, so it is committed to git and travels with the manuscript. It is a plain sorted JSON array, and a corrupt or hand-mangled file is treated as empty rather than stopping the checker.

Note that the list belongs to the workspace, not the whole project — open a different folder of the same project and it has its own list.

The bundled dictionaries live under dictionaries/ and are derived from SCOWL under a permissive license (kept alongside them in each license file).

Plot beats (Mistral)

Tools ▸ ✨ Generate plot beats (Mistral)… turns a rough novel proposal into a structured set of plot beats laid out in the classic three-act structure, using the Mistral chat API.

  1. Open Settings ▸ Mistral… once and paste your API key (from console.mistral.ai). You can also set the Model (default mistral-large-latest) and the Base URL (default https://api.mistral.ai, handy if you route through a proxy). The settings are remembered.
  2. Choose Tools ▸ Generate plot beats… and pick a markdown (or text) file describing the novel. Anything the file states is read and honoured — the title, setting, genre, tone, characters, target age (adult, young adult, …), and a loose plot summary; the beats follow the genre's conventions, keep the tone, and stay age-appropriate. Missing attributes are inferred from the rest. The request runs in the background, so the editor stays responsive.
  3. The result opens in a floating window with the beats grouped under Act I — Setup, Act II — Confrontation, and Act III — Resolution, each a numbered list. You can edit the text in place, then:
    • 💾 Save as new file — writes <proposal> — beats.md into the workspace (auto-numbered if that name is taken) and opens it in the editor, or
    • ⧉ Copy — copies the beats to the clipboard.

Filling in an act you haven't written

If the proposal is already partly beaten out, the generator switches to a different brief: instead of producing a whole sheet, it writes only the acts you are missing and leaves the ones you have written alone.

It counts the numbered beats under each act heading it finds (### Act One:, ## Act 2, # Act III — Resolution — the notation doesn't matter). An act with three or more beats is treated as drafted; anything fewer, or an act you never mention, is treated as still to write. If at least one act is drafted and at least one is not, you get the fill-the-gaps brief.

So a proposal whose Act One and Act Three are beaten out in detail and whose Act Two is a note saying "this is what I need help with" sends this:

It is already partly beaten out:
- Act One: 11 beats already drafted — context only
- Act Two: write this one
- Act Three: 10 beats already drafted — context only

Write only the missing act(s), under exactly these headings:
  ### Act Two

Do not output the drafted acts.

Why it matters: the full-sheet brief tells the model to aim for four to eight beats per act, which would compress an act you had already written to eleven. The fill-the-gaps brief drops that cap, tells the model to match the density of your drafted acts, and treats them as canon — including any reveal that lands in a later act, which it must work backwards from and plant for. It also reuses your own heading text and depth, so what comes back drops straight into your document.

The result is labelled accordingly: the status line says which acts were written, the saved file is named <proposal> — beats Two.md rather than <proposal> — beats.md, and its title says which acts it contains — so a partial sheet is never mistaken for a whole one.

To force a full three-act regeneration anyway, run it against a copy of the proposal with the act headings removed.

The chosen proposal text is sent to Mistral to generate the beats; nothing else in your workspace is transmitted. The API key is stored in the app's config file in plain text. Because the request uses the pure-Rust ureq/rustls stack, the binary stays self-contained.

Grammar & spelling (LanguageTool)

The editor can additionally check the current file against a LanguageTool server — typically a local instance, so your manuscript never leaves your machine.

  1. Run a LanguageTool server. The standalone server listens on http://localhost:8010 by default (the official Docker image uses :8081 — just point the app at whichever you run):

    # Standalone (needs Java):
    java -cp 'languagetool-server.jar' org.languagetool.server.HTTPServer --port 8010
    # or via Docker:
    docker run -d -p 8010:8010 --entrypoint java \
      erikvl87/languagetool -cp languagetool-server.jar \
      org.languagetool.server.HTTPServer --port 8010 --allow-origin '*'
    
  2. Open Settings ▸ LanguageTool… (from the menu bar, or the ⚙ button on the Grammar row) and set the connection:

    • Scheme — http for a local server, https for a remote one (TLS is supported).
    • Host / domain — an IP address or hostname (default localhost).
    • Port — the server's port (default 8010).
    • Token — optional; sent as an Authorization: Bearer header for a server behind an auth proxy, or a premium API key. Leave blank for an unauthenticated local server.
    • Language — auto to detect, or a code like en-US, en-GB, de-DE.

    Press Test connection to confirm the settings reach a working server. All fields are remembered in the config.

  3. Open a file and press ✓ Check. The check runs in the background (the UI stays responsive) and issues appear two ways:

    • Underlines in the editor — red for spelling, blue for grammar/style.
    • A "Grammar & spelling" panel at the bottom listing each issue with its explanation and suggested fixes. Click a suggestion to apply it; the remaining underlines re-align automatically.

Editing the text after a check hides the underlines (their positions would no longer be accurate) and disables the fix buttons until you re-check. The checker sends the raw file text, so occasional markdown tokens (e.g. a # heading marker) may be flagged.

Header stripping on export

Manuscript files often carry editorial notes and metadata above the prose that should not appear in the finished document. On export, each file is cleaned up:

  • HTML comments (<!-- ... -->, including multi-line ones) are removed.

  • The frontmatter, when the file has it — everything down to the closing ---. Otherwise the header block: everything above the Draft marker line set in the top bar. That marker is empty by default, since a new file is written as frontmatter; set it to ### Rough Draft: for files in the older form. From either header:

    • # Title: becomes the chapter heading (unless a manual override is set), and
    • # Slug: becomes an italic caption printed beneath the heading.

    Any other header text is discarded. Files with no marker line are exported whole, but stray # Title: / # Slug: lines are still lifted out. Clear the Draft marker field to disable header splitting entirely.

For example, this source file:

# Title: The Gate
# Slug: in which the door will not open
<!-- remember to foreshadow the key -->
outline: she arrives, she knocks, nothing

### Rough Draft:

The prose that actually gets exported begins here.

exports as a chapter titled The Gate, captioned in which the door will not open, containing only the prose below the marker.

Field-name autocomplete

While typing a header line above the draft marker — #, ##, … followed by the start of a field name — a small popup suggests matching field names. Press Tab or Enter (or click) to insert the field and its ": ", ↑/↓ to change the highlighted suggestion, and Esc to dismiss. The suggestions include common fields (Title, Slug, POV, Word Count Target, Characters, Setting, Conflict, …) plus any field names it learns from the headers of your other files, so your own conventions autocomplete too. It only triggers in the header region, never in the prose below the marker.

Files the app writes

Location Purpose
<workspace>/**/*.md your manuscript files, in folders if you like
<workspace>/order.json the manual ordering, as paths (committed to git)
<workspace>/titles.json chapter-title overrides, keyed by path (committed to git)
<workspace>/wordlist.json names the spell checker accepts (committed to git)
~/.config/md-manuscript/config.json last workspace, export path, prefs, new-file template, project/Gitea settings

In project mode the workspace is the project root, so those three bookkeeping files sit there and cover the whole project.

Markdown supported in ODT export

Headings, paragraphs, bold, italic, both, inline code, fenced/indented code blocks (monospace, whitespace preserved), strikethrough, ordered and unordered (nested) lists, block quotes, horizontal rules, and links. Images are rendered as a text placeholder (not embedded).

A quick reference for all of this — plus the chapter metadata, draft-marker header, and comment conventions — is built in: open Help ▸ Markdown cheatsheet from the menu bar.

Changelog

Latest

  • Heading-based nesting outlines: Markdown headings (#, ##, ###) in outline files now create a hierarchical tree structure in the Outline window. Beats under the same heading are grouped together under collapsible headers with progress counts, making it easy to navigate large outlines and track progress at every level of your story structure.
  • A scene says each thing once: planning metadata lives in the YAML frontmatter and nowhere else. + New Scene used to write 28 keys and then restate nearly all of them below the closing --- as a metadata table, a word-count tracker, POV/characters/setting/conflict/theme/tone sections, a notes block and a checklist — all of it body text, so all of it counted toward the word count and was exported into the .odt. A new scene now starts at the prose. Nine keys the app could not keep true (word_count, session_word_count, total_sessions, version, created, updated, …) are gone with it, along with id and order, which restated the file's own path and order.json.
  • The header fold works on frontmatter: the ▾ header shown line above the editor asked only whether the file had a draft marker, so it never appeared on a frontmatter file — the one form the app writes. Both forms now offer it.
  • Frontmatter is read properly: the status column, the Characters window and the scene card now read frontmatter through the same parser as a legacy ## POV: header. characters: works in either YAML list form (it matched nothing before, or one character called []), keys with underscores are no longer invisible, and prose below the frontmatter is no longer mistaken for metadata — a line of dialogue like Bixby: run used to register as a field. A word_count_min/word_count_max pair now sets the target, as documented.
  • Automatic workspace refresh: File list now automatically refreshes when files change on disk using a file watcher.
S
Description
Draggable markdown manuscript editor with git sync and ODT export (Rust/egui)
Readme 4.3 MiB
Languages
Rust 99.2%
Shell 0.5%
Python 0.3%