Finish bundled-template migration; simplify frontmatter; de-duplicate toolbar/menu

Completes the move off the external cookiecutter tool: drops
src/cookiecutter.rs and the cookiecutter_bin/Template-path setting now
that scaffolding always runs through the built-in MiniJinja engine,
and rewrites default_project_template() to walk up from the executable
without panicking when the bundled template can't be found. Rounds
out the snowflake template with the remaining 06-Revisions,
07-Final, 08-Covers and 09-Archive folders.

Simplifies the scene/chapter YAML frontmatter (drops type/word_count_min/
word_count_max in favor of status + a flat characters list) and updates
the new-file template, header parsing, outline, file list and Story
Bible editor to match.

Separately, de-duplicates the toolbar and menu bar: settings that only
existed as always-visible toolbar controls (draft marker, zero-pad,
spell-check toggle, dictionary picker) move into their Settings
windows, toolbar buttons that exactly duplicated a menu item are
dropped, and toolbar-only actions with no menu path (open project
folder, git sync/init) gain one under File. Also fixes two ambiguous
menu labels: the Story Bible editor now reads "Story Bible editor…"
next to "Story Bible…" in Tools instead of sharing a label with Edit,
and Settings' "New project…" is now "New-project settings…" so it
doesn't read like File's actual create action.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 10:35:52 -05:00
parent c61a7b605a
commit 0f144737a5
23 changed files with 1199 additions and 1091 deletions
+122 -64
View File
@@ -17,7 +17,7 @@ as an ordered manuscript.
chapter heading before each file's content — or, for a screenplay project, as
a paginated Fountain-to-PDF script (see
[Screenplays](#screenplays-fountain)).
* **Start a new project** from a cookiecutter template (**File ▸ New project…**),
* **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`](https://github.com/emilk/egui)/`eframe`. The ODT
@@ -43,8 +43,17 @@ sudo apt install libgl1 libxkbcommon0 libwayland-client0 libx11-6
```sh
install -Dm755 target/release/md-manuscript ~/.local/bin/md-manuscript
# The project templates are read from disk, so they travel with the binary:
cp -r templates ~/.local/bin/templates
```
The app looks for `templates/snowflake/cookiecutter.json` beside the executable
and then up through its parent folders, so a release build run from the source
tree finds it without installing anything. Without it the app still starts and
everything else works — only **File ▸ New project…** has nothing to scaffold
from, and says so. You can also point **Settings ▸ New project… ▸ Template** at
a copy anywhere else.
Optionally add a desktop launcher at
`~/.local/share/applications/md-manuscript.desktop`:
@@ -184,11 +193,9 @@ target is stripped from the exported document.
### YAML frontmatter for planning metadata
For projects using the **snowflake template** or similar structured workflows, files
can store planning metadata in **YAML frontmatter** at the top of the file. This keeps
all planning information (target word count, POV, characters, setting, etc.) together with
the draft content in a single file, while allowing the metadata to be collapsed in the
editor.
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:
```markdown
---
@@ -197,31 +204,57 @@ slug: chapter-one-the-beginning
act: 1
chapter: 1
scene: 1
type: chapter
status: draft
word_count_target: 2000
word_count_min: 1800
word_count_max: 2200
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
```yaml
characters:
- Bixby
- Mom
setting: "The Old Reynold's House"
---
# Chapter One: The Beginning
The prose begins here...
### Rough Draft:
Actual draft content...
```
The YAML frontmatter (delimited by `---` lines) is automatically detected and can be
collapsed in the editor using the **Collapse header** toggle in the top bar. The
draft marker (`### Rough Draft:`) is still supported for backward compatibility with
legacy files.
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](#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
@@ -340,11 +373,14 @@ height, so folding them is worth real writing space and not just grey margin.
### 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.
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
@@ -353,7 +389,8 @@ 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.
A file with neither frontmatter nor a draft marker has nothing to fold, so the
line does not appear.
### Characters (Tools ▸ Characters…)
@@ -608,8 +645,8 @@ single finished document, use **Export ODT**.
## New projects from templates
**File ▸ ✨ New project…** scaffolds a whole manuscript project from a
template and then opens its drafting folder as the workspace, so you go from
nothing to editing chapters in one step.
template and then opens it as the workspace, so you go from nothing to editing
chapters in one step.
The app now uses **built-in MiniJinja templating** (no external Python
cookiecutter required) to render templates. A default "snowflake" template is
@@ -620,7 +657,7 @@ 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.
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
@@ -642,8 +679,8 @@ rendering.
| Setting | Meaning |
|---|---|
| **Template** | the folder holding the template's `cookiecutter.json`. Defaults to the bundled `templates/snowflake`; falls back to `~/Documents/Cookiecutters/snowflake` |
| **Open subfolder** | which folder of the generated project becomes the workspace (default `05-First Draft`; blank opens the project root) |
| **Template** | the folder holding the template's `cookiecutter.json`. Defaults to the bundled `templates/snowflake`, found beside the executable or up through its parent folders |
| **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 |
@@ -664,18 +701,16 @@ the project is still generated locally.
The token is stored in `config.json` in plain text, the same as the Mistral API
key.
This path runs entirely inside the template's own hook, so it applies to
projects made through **New project** and nothing else, and it does nothing if
**Run the template's hooks** is off, if any of the three Gitea settings is
blank, or if the template has no such hook. When it fails it fails in someone
else's Python, and all this app can report is that a hook exited non-zero — the
Git log window carries whatever the hook printed.
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… does not go through the template at all.** It calls the
Gitea API directly, works on any workspace, 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). If the hook is not publishing and you would rather not debug it, that
is the way round.
**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
@@ -685,23 +720,26 @@ The file list has two create buttons:
|---|---|---|
| **+ 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](#yaml-frontmatter-for-planning-metadata) |
**+ 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.
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:
Edit the template under **Settings ▸ New-file template…**. The built-in default
is frontmatter, the same shape a scene uses:
```
# Title: {{name}}
# Slug:
# POV:
# Word Count Target:
### Rough Draft:
---
title: {{name}}
slug:
status: draft
pov:
characters: []
word_count_target:
---
```
@@ -710,12 +748,13 @@ 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:`) |
| `{{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. 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**
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
@@ -901,8 +940,11 @@ 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:
* **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.
@@ -970,8 +1012,24 @@ from the menu bar.
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.
- **Linked scene/chapter templates**: New file templates now support linked
planning+draft file pairs. Creating a scene in the draft folder automatically
creates a corresponding planning file in the Plot folder with references.
- **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.