Add nested folders to the file panel and File ▸ New project

Two features that arrived together, since the second depends on the first
to show what it generates.

Nested folders
--------------
`App::files` now holds workspace-relative paths (`part-1/ch-03.md`) rather
than bare names, and the workspace scan recurses eight levels, skipping
dot-directories, `target/` and `node_modules/`. `order::tree_order`
normalises the flat order so every folder's files are contiguous and each
folder sits where its earliest-ordered file put it — which is what keeps
the panel and the export in agreement: the export concatenates the tree
read top to bottom.

The panel draws a collapsible tree. Dragging within a folder reorders as
before; dropping onto a folder header, or among another folder's files,
moves the file on disk and carries its title override, session word
baseline and cached header info with it. Emptied folders are pruned. New
files take a path (`part-1/ch-01`) to create folders, and the Rename box
now holds the whole relative path, so editing its folder part moves the
file.

File ▸ New project
------------------
Scaffolds a project from a cookiecutter template and opens its drafting
subfolder (`06-First Draft`) as the workspace. `cookiecutter.rs` resolves
the executable from PATH and the usual per-user Python prefixes — a
desktop launcher inherits neither a conda PATH nor the tools a template's
hooks shell out to, so the resolved binary's directory is prepended for
the child — builds the non-interactive command line, and identifies the
result by diffing the output directory, which works whatever a template
names its root.

Generation runs off the UI thread because hooks can reach the network.
Settings ▸ New project… covers the template path, the subfolder to open,
the cookiecutter path, a hooks toggle, and the Gitea credentials passed
to hooks as GITEA_URL / GITEA_USER / GITEA_TOKEN.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZGoPiDuZ7vmryNCJWjYSD
This commit is contained in:
2026-08-23 15:18:22 -05:00
parent b71ffc4f20
commit 41c3f088cd
12 changed files with 2243 additions and 148 deletions
+107 -14
View File
@@ -3,16 +3,20 @@
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 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 — the `POV:` line and the `# Slug:` synopsis —
a quick at-a-glance summary of each chapter.
* **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.
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).
@@ -71,10 +75,12 @@ Terminal=false
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) or ** New from template**
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. Use the **Zoom** slider
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
@@ -139,18 +145,105 @@ 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.
## 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/`.
## 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 | `# <name>` and a blank line |
| ** 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), seeds it from the
template, appends it to the manuscript order and selects it. Rename it later with
`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.
@@ -328,10 +421,10 @@ only triggers in the header region, never in the prose below the marker.
| 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 |
| `<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) |
| `~/.config/md-manuscript/config.json` | last workspace, export path, prefs, new-file template, project/Gitea settings |
## Markdown supported in ODT export