The file list's "+ New" needs a typed name and seeds only '# <name>',
so every new chapter started from a blank header. Add a second button,
"+ New from template", that takes no name: it picks the first free
untitled-N.md (reusing a gap left by a deleted file), seeds it from a
configurable template, appends it to the manuscript order and selects it.
The template lives in config.json and is edited under Settings > New-file
template..., alongside the existing LanguageTool and Mistral dialogs.
Three placeholders expand at creation time: {{name}} (file stem),
{{marker}} (the configured draft marker, so a template keeps working if
the marker changes) and {{date}} (today's UTC date). Blanking the box
falls back to the built-in default, which seeds the header fields the app
already understands followed by the draft marker.
The date is computed with a local civil-from-days conversion rather than
a date crate, keeping the binary self-contained - ldd still shows only
libc/libgcc/libm.
"+ New" is unchanged; both paths now share insert_new_file. Covered by
13 new tests: placeholder expansion, untitled-N gap reuse, civil date
conversion against known dates, config.json written before this field
still deserializing (Config::load falls back to Default on a parse error,
which would otherwise discard the user's workspace and API keys), and a
round trip proving the seeded header parses back into Title/Slug/POV/goal
via the app's own preprocess::parse.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBWj9TphFMCoh7VHaSRnvQ
md-manuscript
A small desktop application for Debian that treats a directory of markdown files as an ordered manuscript.
- Left pane — every
*.mdfile in the workspace directory, in a manual order you set by dragging the⠿handle up and down. Hover a file to see a tooltip built from its header — thePOV: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.jsoninside the workspace, so it syncs along with the files. - Export the whole manuscript as a single concatenated
.odtfile, with a chapter heading before each file's content.
Built in Rust with egui/eframe. The ODT
writer is 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
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
-
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. -
Press Init git once to make the workspace a git repository. To sync with another machine, add a remote yourself:
cd ~/Manuscript git remote add origin <url> git push -u origin mainAfter that, the ⟳ Sync (git) button commits all changes,
pull --rebases, and pushes.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.
-
Create files with + New (type a name first) 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. 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 + Bbold ( **…**)Ctrl/Cmd + Iitalic ( *…*)Ctrl/Cmd + Einline codeCtrl/Cmd + Shift + Xstrikethrough(~~…~~)Ctrl/Cmd + Klink — [selection](url), withurlselected to replacePress
Ctrl/Cmd + F(or Edit ▸ Find / Replace…) to open the find/replace bar above the editor;Ctrl/Cmd + Hopens 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. -
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 totitles.jsonand sync with git. -
Set the Export path (type it, or click 📂 to choose the
.odtfile 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.
New files from a template
The file list has two create buttons:
| Button | Name | Contents |
|---|---|---|
| + New | the name you type beside it | # <name> and a blank line |
| + New from template | auto-assigned untitled-N.md |
the configured template |
+ New from template takes no typed name — it picks the first free
untitled-N.md (reusing a gap if you have deleted one), seeds it from the
template, appends it to the manuscript order and selects it. Rename it later with
the Rename box, or set a # Title: header and let the chapter title come
from that.
Edit the template under Settings ▸ New-file template…. The built-in default is:
# Title: {{name}}
# Slug:
# POV:
# Word Count Target:
### Rough Draft:
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 (default ### Rough Draft:) |
{{date}} |
today's date in UTC, as YYYY-MM-DD |
Anything else in double braces is left alone. Using {{marker}} rather than
typing the marker literally keeps the template working if you change the marker
later. 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 matchingxx_YY.aff+xx_YY.dicpair in there (e.g. from your distro'shunspell-de-depackage) 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 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.
- Open Settings ▸ Mistral… once and paste your API key (from
console.mistral.ai). You can also set the Model (defaultmistral-large-latest) and the Base URL (defaulthttps://api.mistral.ai, handy if you route through a proxy). The settings are remembered. - 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.
- 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.mdinto the workspace (auto-numbered if that name is taken) and opens it in the editor, or - ⧉ Copy — copies the beats to the clipboard.
- 💾 Save as new file — writes
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.
-
Run a LanguageTool server. The standalone server listens on
http://localhost:8010by 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 '*' -
Open Settings ▸ LanguageTool… (from the menu bar, or the ⚙ button on the Grammar row) and set the connection:
- Scheme —
httpfor a local server,httpsfor 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: Bearerheader for a server behind an auth proxy, or a premium API key. Leave blank for an unauthenticated local server. - Language —
autoto detect, or a code likeen-US,en-GB,de-DE.
Press Test connection to confirm the settings reach a working server. All fields are remembered in the config.
- Scheme —
-
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 header block — everything above the Draft marker line (set in the top bar; default
### Rough Draft:) — is dropped. From that 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 |
<workspace>/order.json |
the manual ordering (committed to git) |
<workspace>/titles.json |
chapter-title overrides (committed to git) |
~/.config/md-manuscript/config.json |
last workspace, export path, prefs, new-file template |
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.