043cc692ac
Rounds out project mode, where the workspace is the project root and one subfolder holds the manuscript proper: - Characters and Outline windows, backed by new `characters` and `outline` modules that read the cast from character sheets and measure how much of the snowflake outline is actually written. - Edit ▸ Changes… diffs the open file against its last committed version. - Revision status, per-file and project word counts, an archive action and hidden folders in the file panel. - Chapter-file export alongside the ODT master, richer header parsing, and a project word list for names and invented terms. - The export path now follows the workspace: opening a project points it at that project root, keeping a file name you chose yourself and re-deriving one that merely echoed the folder it sat in. - Clicking an issue in the grammar/spelling panel takes the editor to it, selecting the words and centring them; applying a suggestion jumps to the rewritten text as well. README covers the new windows and workflows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bSn3Xijp8GofZVUnRX4oq
656 lines
32 KiB
Markdown
656 lines
32 KiB
Markdown
# 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.
|
||
* **Start a new project** from a cookiecutter template (**File ▸ New project…**),
|
||
which builds the whole folder structure and opens its drafting folder.
|
||
|
||
Built in Rust with [`egui`](https://github.com/emilk/egui)/`eframe`. The ODT
|
||
writer is native (no `pandoc`/LibreOffice needed at runtime).
|
||
|
||
## Build
|
||
|
||
```sh
|
||
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:
|
||
|
||
```sh
|
||
sudo apt install libgl1 libxkbcommon0 libwayland-client0 libx11-6
|
||
```
|
||
|
||
(Only needed to *run* the GUI; building does not require them.)
|
||
|
||
## Install
|
||
|
||
```sh
|
||
install -Dm755 target/release/md-manuscript ~/.local/bin/md-manuscript
|
||
```
|
||
|
||
Optionally add a desktop launcher at
|
||
`~/.local/share/applications/md-manuscript.desktop`:
|
||
|
||
```ini
|
||
[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, add a remote yourself:
|
||
```sh
|
||
cd ~/Manuscript
|
||
git remote add origin <url>
|
||
git push -u origin main
|
||
```
|
||
After that, the **⟳ Sync (git)** button commits all changes, `pull --rebase`s,
|
||
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.
|
||
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](#organising-with-folders))
|
||
or **+ New from template**
|
||
(no name needed — see [New files from a template](#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 |
|
||
|
||
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):
|
||
|
||
```markdown
|
||
## 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.
|
||
|
||
## 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** (`06-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](#characters-tools--characters), [Outline](#outline-tools--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 `06-First Draft` — and
|
||
the setting is global, so every project on this machine uses the same layout.
|
||
If your writing actually lives in `05-Plot`, point it there.
|
||
|
||
### 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.
|
||
|
||
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.
|
||
|
||
### Hiding the header while you write
|
||
|
||
A scene card'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 away everything above the draft marker, leaving the prose alone
|
||
in the editor. A single line above the editor says so — `▸ header hidden (8
|
||
fields)` — and clicking it flips 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.
|
||
|
||
Files with no draft marker have 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.
|
||
|
||
### 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.
|
||
|
||
Beats generated by [the Mistral tool](#plot-beats-mistral) 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.
|
||
|
||
### 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](#hidden-folders), 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 and author, 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.
|
||
|
||
### 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 a title page 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 a cookiecutter template
|
||
|
||
**File ▸ ✨ New project…** scaffolds a whole manuscript project from a
|
||
[cookiecutter](https://cookiecutter.readthedocs.io/) template and then opens its
|
||
drafting folder as the workspace, so you go from nothing to editing chapters in
|
||
one step.
|
||
|
||
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 template whose hooks talk to the network doesn't freeze the editor.
|
||
|
||
When it finishes, the app opens the project's **`06-First Draft`** subfolder as
|
||
the workspace — that is where the snowflake template keeps the chapter files, in
|
||
`Act 1/`, `Act 2/` and `Act 3/` subfolders that the
|
||
[folder tree](#organising-with-folders) shows directly. If the template didn't
|
||
produce that folder, the project root is opened instead and the status bar says
|
||
so.
|
||
|
||
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.
|
||
|
||
### Settings ▸ New project…
|
||
|
||
| Setting | Meaning |
|
||
|---|---|
|
||
| **Template** | the folder holding the template's `cookiecutter.json`. Defaults to `~/Documents/Cookiecutters/snowflake`; the dialog confirms whether a `cookiecutter.json` is actually there |
|
||
| **Open subfolder** | which folder of the generated project becomes the workspace (default `06-First Draft`; blank opens the project root) |
|
||
| **cookiecutter path** | leave blank to search `PATH` and then the usual per-user Python prefixes (`~/.anaconda3/bin`, `~/miniconda3/bin`, `~/.local/bin`, …), which is what a desktop launcher needs since it does not inherit a conda `PATH`. The dialog shows which executable was found |
|
||
| **Run the template's hooks** | whether to run the template's pre/post-generation scripts (on by default) |
|
||
| **Gitea** | server URL, user and token, handed to the hooks as `GITEA_URL` / `GITEA_USER` / `GITEA_TOKEN` |
|
||
|
||
Cookiecutter is a Python program and is **not bundled** — install it with
|
||
`pip install cookiecutter` or `conda install cookiecutter` if the settings window
|
||
reports it missing.
|
||
|
||
### Hooks and publishing
|
||
|
||
The snowflake template's `hooks/post_gen_project.py` creates a repository on a
|
||
Gitea server and pushes the new project to it. It only does that when all three
|
||
Gitea settings are filled in; with any of them blank it prints a note and skips,
|
||
and **the project is still generated either way**. The token is passed to the
|
||
push command alone and is not written into the project's `.git/config`.
|
||
|
||
The token is stored in `config.json` in plain text, the same as the Mistral API
|
||
key.
|
||
|
||
## 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 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 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`](https://crates.io/crates/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
|
||
`06-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](http://wordlist.sourceforge.net/) 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](https://mistral.ai/) 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](https://languagetool.org/) 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):
|
||
```sh
|
||
# 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 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:
|
||
|
||
```markdown
|
||
# 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](#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.
|