Commit Graph

65 Commits

Author SHA1 Message Date
landon 341202f87c fix: Improve bundled template path resolution and add tests
- Walk up directory tree from executable to find templates/snowflake
- Handles various build configurations (release, debug, cargo run, installed)
- Add test to verify bundled template path contains 'templates/snowflake'
- Add test to verify template has 05-First Draft (merged) and not old 05-Plot/06-First Draft

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-16 18:38:03 -05:00
landon 2471312f05 feat: Remove fallback to Cookiecutters, use bundled template only
- Remove fallback to ~/Documents/Cookiecutters/snowflake from default_project_template()
- Panic with clear error if bundled template not found
- Update tests to use 05-First Draft instead of 06-First Draft
- Update comments to reflect merged structure (05-First Draft contains planning+draft)

The code now only uses the bundled template at templates/snowflake/ relative to the
executable, ensuring all new projects use the merged 05-First Draft structure.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-16 18:27:54 -05:00
landon aab943de6b manual renumbering of template files 2026-09-16 18:14:17 -05:00
landon 114f4ff697 docs: Update all references from old 05-Plot/06-First Draft to merged 05-First Draft
- Update project_template test to verify old directories are NOT created
- Update hover text and comments in project.rs
- Update comments and code in file_list.rs
- Update comment in spelling.rs
- Add test assertions to verify 05-Plot and 06-First Draft don't exist in new projects

This ensures all code references match the merged directory structure where
planning and draft content are combined in 05-First Draft with YAML frontmatter.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-16 15:34:36 -05:00
landon 0583bc077c feat: Add aliases support to Story Bible for flexible linking
- Add aliases field to StoryBibleItem struct
- Add items_by_alias HashMap to StoryBibleIndex for quick alias lookup
- Add find_by_alias() method and update find_by_partial_name() to search aliases
- Parse aliases from YAML frontmatter (list and comma-separated formats)
- Update find_references() to match aliases in addition to IDs and names
- Fix @mention pattern matching to properly stop at spaces
- Add comprehensive tests for aliases functionality
- Update README.md with aliases documentation and usage examples

This allows Story Bible entries to have alternative names that can be
referenced in draft text using wiki-style [[...]] or mention-style @ syntax.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-16 14:18:57 -05:00
landon a0d1c3c206 feat: Remove ### Rough Draft: marker, use YAML frontmatter only for draft files
- Everything after YAML frontmatter closing --- is now considered the draft content
- No ### Rough Draft: marker needed
- Chapter titles are handled by the program on export based on file order
- Draft content should not include chapter titles
- Updated default_new_file_template to use YAML frontmatter
- Updated preprocess.rs to parse YAML frontmatter for metadata
- Updated all relevant tests
- Updated documentation in help.rs
- Updated template files

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-16 13:33:23 -05:00
landon aafe1786b5 feat: Add auto-snapshot feature with git commit every N words
- Add new module src/app/auto_snapshot.rs for auto-snapshot functionality
- Add configuration options: auto_snapshot_enabled (default: false) and
  auto_snapshot_threshold (default: 250 words)
- Automatically create git commits when word count crosses threshold boundaries
- Add 'Edit -> Snapshots' menu option to open snapshots window
- Snapshots window shows: enable/disable toggle, threshold setting, current
  word count, next snapshot target, list of all snapshots with rollback buttons
- Manual snapshot creation via button in snapshots window
- Rollback to any previous snapshot with one click
- Snapshot info includes: word count, timestamp, commit message, commit hash
- Make run_git in gitsync.rs public for use by auto_snapshot module
- Update README.md with Auto-Snapshots documentation

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-16 12:11:35 -05:00
landon dcb97da518 feat: Add Story Bible breakout editor window
- Add new module src/app/story_bible_editor.rs for dedicated Story Bible editing
- Add 'Edit -> Story Bible' menu option to open the editor
- Tree view (left 1/3) shows Story Bible directory structure with collapsible folders
- Editor (right 2/3) allows editing selected Story Bible items
- Auto-load first file on open, support Ctrl+S to save
- Rebuild Story Bible index after saving to pick up changes
- Update README.md with Story Bible Editor documentation

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-16 11:21:15 -05:00
landon 55bf006216 docs: Update README for merged 05-First Draft with YAML frontmatter
- Update all references from 06-First Draft to 05-First Draft
- Remove outdated reference to 05-Plot
- Add new section explaining YAML frontmatter support for planning metadata

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-16 10:08:20 -05:00
landon 449ad4439f feat: Merge 05-Plot and 06-First Draft into 05-First Draft with YAML frontmatter
- Add YAML frontmatter support with split_yaml_frontmatter() in preprocess.rs
- Update create_scene_pair to create single merged files with YAML frontmatter
- Rename 06-First Draft to 05-First Draft and merge with 05-Plot content
- Update default_project_open_subdir to '05-First Draft'
- Add capitalizes filter to MiniJinja environment for template rendering
- Fix template placeholder escaping in CONSISTENCY.md
- Update all references from 06-First Draft and 05-Plot to 05-First Draft
- Update test expectations to match new folder structure

The merged approach uses YAML frontmatter (--- delimiters) at the top of
each draft file to store planning metadata, allowing it to be collapsed
in the editor while keeping all planning and draft content in one file.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-16 10:06:32 -05:00
landon 477c59cb79 docs: Add Story Bible documentation to README
- Document Story Bible feature in the Tools section
- Add Story Bible to the reference material cross-reference
- Describe reference syntaxes (wiki-style and mention-style)
- Document highlighting and navigation features

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-16 08:58:13 -05:00
landon ca3e476a14 feat: Implement bidirectional Story Bible links with draft text
- Add story_bible.rs module with StoryBibleIndex, StoryBibleItem, and
  StoryBibleReference types for indexing Story Bible content
- Support wiki-style [[...]] and mention-style @... syntax for references
- Highlight Story Bible references in editor with purple background
- Add Story Bible window (View > Tools > Story Bible) to browse items
- Show references from current draft file in Story Bible window
- Allow clicking references to jump to their location in the editor
- Auto-index Story Bible items from common directories (03-Characters,
  02-Story Bible, Characters, etc.)
- Extract metadata from YAML frontmatter or filenames
- Add walkdir dependency for directory scanning
- Fix pre-existing format string errors in workspace.rs

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-16 08:56:28 -05:00
landon 511a7588fa docs: Update README with heading-based nesting outlines and changelog
- Updated Outline section to document heading hierarchy preservation
- Added Changelog section documenting latest features
- Documented linked scene/chapter templates
- Documented automatic workspace refresh

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-15 14:58:48 -05:00
landon cd8663349a feat: Implement heading-based nesting outlines in Outline window
- Add depth and path fields to Beat struct to track heading hierarchy
- Rewrite beats() parser to use heading stack and process_headings() helper
- Add BeatTree enum with Heading and Beat variants for tree structure
- Add build_beat_tree() to convert flat beats to nested tree
- Add render_beat_tree() for recursive tree rendering with collapsible headers
- Add count_beat_tree() helper to count beats in subtree
- Replace flat list rendering with tree-based rendering in outline_window()

The implementation currently groups beats by section, with infrastructure
in place for full hierarchical nesting based on beat.path.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-15 14:56:35 -05:00
landon 921f3e74c5 feat: Add 'New Scene' button to file list menu for creating linked planning+draft files
- Added create_scene_pair() method to workspace.rs
- Added '+ New Scene' button in file_list.rs UI
- Button creates both planning file (05-Plot/act_N/scene_XX.md) and draft file (06-First Draft/Act N/sceneXX.md)
- Files are automatically linked via YAML frontmatter (draft_file and planning_template fields)
- Auto-detects current act from folder context
- Auto-generates next scene number
- Selects the draft file after creation (ready to edit)
- Includes hover text explaining the feature

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-15 14:27:30 -05:00
landon b183237d59 feat: Add linked scene/chapter templates with planning-draft file references
- Updated Plot scene template with YAML frontmatter for metadata tracking
- Updated First Draft scene template with YAML frontmatter
- Added _template.md files in both Plot and First Draft folders
- Added create_scene.py script for automatic file creation and linking
- Planning files reference draft files via 'draft_file' field
- Draft files reference planning files via 'planning_template' field
- Word count targets tracked in planning, current count in draft
- Added comprehensive scripts/README.md with usage guide
- Supports --from-planning and --from-draft for creating missing counterparts

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-15 14:20:44 -05:00
landon d311e87836 feat: Transform 03-Characters into comprehensive Story Bible
- Renamed 03-Characters to 03-Story-Bible with expanded structure
- Added enhanced character templates with YAML frontmatter for metadata
- Created new sections: Locations, Lore, Objects, Relationships, Timeline, Themes
- Added templates for: regions, cities, buildings, historical events, factions
- Added templates for: artifacts, weapons, character relationships
- Added world rules document for magic/technology/society tracking
- Added CONSISTENCY.md for continuity tracking
- Added comprehensive README with usage guide
- Added index files for each section with cross-linking
- Implemented wiki-link syntax [[file]] for cross-references
- Added Mermaid diagram support for relationship maps and timelines
- Tagging system for filtering and organization

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-15 13:58:56 -05:00
landon fbccad3a86 ui: Limit file name display length and ensure left alignment in file list
- Add MAX_DISPLAY_NAME_LEN constant (30 characters)
- Add truncate_display_name helper to truncate with ellipsis
- Apply truncation to folder names and file names
- Apply truncation to context menu file names
- Maintain left alignment through existing layout

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-14 16:13:43 -05:00
landon f64536ee9e feat: Add automatic workspace refresh with file watcher
Implement a polling-based file watcher that automatically refreshes
the file list when markdown files are created, deleted, or renamed.

- Add background thread that polls workspace every 2 seconds
- Watcher starts when workspace opens, stops when app exits
- Uses pure Rust std library (no external dependencies)
- Fixes issue where new folders like Act 2 don't appear until manual refresh

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-13 18:41:57 -05:00
landon cea944779e Add outline popup window for folder context menu
Implement the outline file popup window that opens when a user
right-clicks on a folder in the file list (e.g., Act 1 in 06-First Draft)
and selects 'Outline' from the context menu. The window displays the
corresponding outline file (e.g., 04-Outline/scene_breakdown/act_01.md)
in a read-only view.

Changes:
- Add outline_popup_window() function in file_list.rs
- Add window rendering call in mod.rs update loop
- Popup reads file content on open and displays it in scrollable view

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-13 10:47:11 -05:00
landon 9f575c7c7e feat: Ensure 06-First Draft folder is expanded in file list when opening projects
- Add explicit code in open_workspace to ensure manuscript folder is not collapsed
- Update README to clarify that project root is opened with manuscript folder expanded
- Keep workspace at project root but ensure file list shows 06-First Draft expanded

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-13 09:08:50 -05:00
landon caa0001922 feat: Replace cookiecutter with built-in MiniJinja templating
- Add MiniJinja dependency for Jinja2-compatible templating
- Create new project_template.rs module for template rendering
- Bundle snowflake template in templates/snowflake/ directory
- Remove dependency on external cookiecutter Python tool
- Handle Gitea publishing natively in Rust instead of Python hooks
- Update default template path to use bundled template with fallback
- Update README to reflect new built-in templating
- All tests pass

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-13 08:37:18 -05:00
landon 981cd853b9 Cleanup: Remove test push file
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-10 15:28:17 -05:00
landon e77d29047c Test: Verify push access from Mistral Vibe
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-10 15:28:02 -05:00
landon d6ccf12f51 Don't report a first sync as failed
A repository being pushed for the first time has no upstream, so the
pull leg failed with "There is no tracking information for the current
branch" and dragged the whole sync's status down — the app said "Sync
finished with errors" even though the push had gone through and set the
upstream itself.

The upstream is now checked up front. Without one there is nothing to
rebase onto, so the pull is skipped with a note in the log and the push
sets the upstream directly, rather than being run once to fail and then
retried. The argument choice moves to push_args so it can be asserted
without a repository on disk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBWj9TphFMCoh7VHaSRnvQ
2026-09-09 19:41:21 -05:00
landon c42f11b86c Point a created repo's SSH URL at a host we can reach
Gitea builds ssh_url from its own SSH_DOMAIN, which is whatever the
server was told to advertise. A server announcing a public hostname
whose SSH port is only open on the LAN hands out a remote that connects
from nowhere — and because "Create and attach" does not push, the
failure surfaces later, at the first Sync:

    ssh: connect to host <wan-name> port 2228: Connection refused

The app already knows an address that works, having just made an API
call over it, so the SSH host is taken from the configured server URL
instead. The port Gitea reports is kept, as are the user and path, and
both URL shapes are handled (ssh://user@host:port/path and the scp-like
user@host:path). Setting the server to a tailnet address therefore gets
a remote that works on and off the LAN alike.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBWj9TphFMCoh7VHaSRnvQ
2026-09-09 19:36:15 -05:00
landon c4391155cd Create the Gitea repository from the app, not a template hook
Repository creation was delegated entirely to a cookiecutter template's
post-gen hook. That only ever covered projects made through New project,
and when it went wrong it went wrong inside someone else's Python, where
all this app could report was that a hook exited non-zero.

The app now talks to Gitea itself. Settings > Git remote... gains a
"Create and attach" button: it POSTs to /api/v1/user/repos with the
configured server, user and token, then points origin at the clone URL
that comes back. A name that is already taken is fetched and attached to
rather than raised as an error, so retrying after a failure works.

The call runs on a worker thread with the existing channel-and-poll
pattern, so the UI does not block on the network. Errors are mapped by
status code, because a rejected token, a URL that is not a Gitea API and
a name the server refuses each need a different fix.

Uses ureq's send_string rather than send_json, whose feature this build
does not enable; no new dependency, and ldd still shows only
libc/libgcc/libm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBWj9TphFMCoh7VHaSRnvQ
2026-09-09 18:13:21 -05:00
landon 91718f7487 Let the git remote be set from Settings
Attaching a workspace to an existing repository meant leaving for a
terminal: gitsync could init, commit and push, and could tell whether an
'origin' existed, but had no way to create one, and nothing in the
settings held a remote URL. Sync just logged "no 'origin' remote
configured" and committed locally.

Settings > Git remote... now shows the workspace repository's origin and
lets it be set, repointed or cleared. The URL is read from and written
to the repository, never to config.json, because a remote belongs to the
checkout — so each workspace shows its own, and the box is refreshed
whenever the repository in play changes.

This is separate from the Gitea publishing in New project, which is a
cookiecutter post-gen hook and only ever applies to generated projects.

Also factors out first_output_line, which repo_root and the new
origin_url were both open-coding, and origin_args, so the add-vs-set-url
decision can be tested without a repository on disk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBWj9TphFMCoh7VHaSRnvQ
2026-09-09 17:54:27 -05:00
landon aa050cf411 Add a right-click context menu to file rows
Renaming, archiving and deleting a file all lived in buttons under the
list, and all of them required selecting the file first. The same
operations are now on the row itself: right-click a file for Rename,
Copy path, Archive and Delete.

The menu records what was picked rather than acting on it, because the
list is drawn inside a closure still borrowing `self` — the same
deferred pattern the click and drag-drop handling already use. Right-
clicking selects the row first, so the menu always acts on the file you
clicked; selecting is a no-op when it is already current, so this cannot
reload the buffer out from under an unsaved edit.

Rename opens a focused dialog with the path pre-filled, and stays open
if the name is rejected so a clash does not cost what was typed;
rename_selected reports success for that. Delete asks first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBWj9TphFMCoh7VHaSRnvQ
2026-09-07 11:57:49 -05:00
landon 70046b0382 Add a Ctrl+/ whole-line comment toggle to the editor
The app already treats `<!-- ... -->` as editorial notes: preprocess
strips them from the export and the word count. Ctrl+/ makes that a
one-key move, so a paragraph can be shelved without being deleted.

It grows the selection out to line boundaries first, and only toggles a
block back off when every non-blank line in it is already commented.
Blank lines are passed through so paragraph breaks survive the round
trip, and pressing it on an empty line opens `<!--  -->` with the caret
inside.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBWj9TphFMCoh7VHaSRnvQ
2026-09-07 11:03:02 -05:00
landon a23cd5abfe Lay the preview pane out down the page, not across it
The preview rendered the whole document as one clipped line running off
the right edge.

An egui child `Ui` inherits its parent's layout, and `ScrollArea` passes
that layout straight through to its content. Both columns were allocated
with `allocate_ui` inside `horizontal_top`, so the preview's content ui
was left-to-right: every `ui.label` in `render_preview` was placed beside
the previous one instead of below it. The editor escaped notice because
it is a single `TextEdit` widget, so a horizontal layout looks the same.

Allocate both columns with an explicit top-down layout instead. The
editor did not need it, but leaving one column implicit is how this would
come back.

Verified by running the app against an isolated config before and after:
the old binary shows "Title: The Opening  Slug: opening  Rough Dra..." on
one line; the new one wraps paragraphs to the column and sizes headings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017GLYKcrX18ytCCuKozDXk6
2026-08-26 19:53:27 -05:00
landon 5a5017def8 Add Fountain screenplay projects, exported straight to PDF
A screenplay is its own project rather than a file type mixed in with
prose, so this is one switch in Manuscript details, not a second file
extension threaded through order.rs and the file panel. Files stay `.md`
and keep the same editorial header; only the draft below the marker is
read as Fountain. They list, reorder and header-strip exactly as before.

src/fountain.rs parses the format. Almost nothing in Fountain is marked
up -- what makes a line a character cue is that it is in capitals with
something directly beneath it, and what makes the same words a
transition is a blank line below instead. The forcing characters are all
there for where that is not enough, along with notes, the boneyard and
inline emphasis. Sections and synopses are parsed and kept but never
printed: they are the writer's scaffolding.

src/pdf.rs writes the PDF by hand, for the reason odt.rs writes ODT by
hand -- no pandoc, no LibreOffice at runtime. It costs no dependency
either: a screenplay is set entirely in Courier, which is one of the
fourteen faces every reader must provide, so there is no font to embed
and no metrics to parse. Streams are left uncompressed; a feature script
is a few hundred kilobytes that way and stays readable when something
needs debugging.

src/screenplay.rs does layout. The geometry is the conventional one --
55 lines of 12pt on US Letter, action at 1.5", dialogue 2.5",
parentheticals 3.1", cues 3.7", transitions flush to 7.5" -- because a
page only reads as a minute of screen time if it is. A speech broken by
a page boundary is marked (MORE) and resumed under a repeated cue, a `^`
cue sets two speeches side by side, and a scene heading is never left
stranded at the foot of a page.

Two faults the tests missed and measuring the rendered PDF caught. A
hard-wrapped action paragraph was getting a blank line between every
source line, which on the page reads as a beat the writer never wrote;
consecutive lines are now one paragraph, with the breaks kept. And
reserving one line after a scene heading did not stop it stranding,
because every element that can follow a heading is separated from it by
a blank -- the reserve has to cover both. Both now have tests.

Verified beyond the unit tests: pdffonts confirms base-14 Courier with
nothing embedded, pdftotext -bbox puts every indent within a hundredth
of an inch of standard, and the export was driven through the real UI
against a scratch workspace and an isolated config.

Not built, because they were offered and never asked for: rendering
Fountain in the Preview pane, and exporting a manuscript as a .fountain
file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Bq2fUZNgksdPp3zeHqzSw
2026-08-26 12:02:56 -05:00
landon be6a2d6a8b Give the app an icon, and a script that installs it
The launcher pointed at `Icon=story-editor`, a name that only the Breeze
theme happened to ship, and only at 16 and 22 px -- so anywhere the
desktop wanted a larger icon it fell back to something generic.

assets/md-manuscript.svg is the source: a sheaf of manuscript pages, an
amber chapter-title line over three of prose. Drawn in plain rounded
rectangles because a finer design silts up into a grey smear at 16 px.
It is rasterised to the eight sizes a desktop asks for, and the 256px
PNG is compiled into the binary as the window icon, so the title bar and
the task switcher carry it whether or not anything is installed. That
costs no new dependency: eframe already pulls `image` with png decoding.
A test decodes the bundled PNG, since a rename would otherwise show up
only as a blank icon at runtime.

Installation is now `./assets/install.sh` -- binary, icons, launcher and
both cache refreshes, safe to re-run after a rebuild. The launcher is a
file in the repository rather than a here-document in the instructions:
pasting one into a terminal leaves the shell sat at a `>` prompt waiting
for a terminator that never arrives, which is exactly what happened, and
this way the launcher is versioned along with everything else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYSGPDwkSzhm4qLqjCbqxU
2026-08-25 18:15:21 -05:00
landon f35397b49b Add title pages, standard manuscript format and collapsible panels
Five changes to the export and the window chrome:

* Manuscript details gains a contact field and a "Begin exports with a
  title page" option: the title, the author beneath it, and the contact
  details beneath that, laid out line for line as typed. A chapters +
  master export puts the title page on the master, not on each chapter.

* "Standard manuscript format" lays an export out the way an agent or an
  editor expects a submission: 12pt Courier, double-spaced, half-inch
  first-line indents, a Surname / Title / page header on every page but
  the title page, chapters opening a third of the way down, and `---`
  rendered as the conventional centred `#` scene break. The title page
  becomes the submission kind, with contact top-left and an approximate
  word count top-right. Off by default; margins were already the
  standard 1in on US Letter and are unchanged either way.

* A folder button beside Export ODT opens the project folder in the file
  manager -- the enclosing git work tree, including one still awaiting
  confirmation, since showing a folder is a smaller question than
  choosing which repository to commit to.

* The toolbar and the file list each collapse, from the pair of buttons
  at the right of the menu bar or from the View menu, and Ctrl+D folds
  both away together for distraction-free writing. Both choices persist.

* The editor sizes to its viewport instead of a fixed 30 rows, so the
  height a folded panel gives back reaches the page rather than leaving
  grey space below the text box that did not even take focus.

Two traps worth recording. ODF's style:master-page-name is inherited,
and any paragraph style carrying one forces a page break before every
paragraph that uses it -- a four-line contact block came out as four
pages until Contact_20_Line stopped inheriting from Contact_20_Block;
a test now pins which styles may carry one. And the status bar shares
a function with the toolbar, so guarding that function's top rather
than the top panel alone silently took the word counts away with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYSGPDwkSzhm4qLqjCbqxU
2026-08-25 17:58:59 -05:00
landon 66d3f9817e Let a spelling or grammar issue be dismissed
Not every flag deserves a fix, so give the checkers a way to be told no:

- ✖ beside an issue in the panel, and a matching entry in the editor's
  right-click menu, wave that issue away.
- A dismissal is remembered as the offending text paired with the message
  rather than a byte range, since offsets move as soon as you type. Both
  checkers filter their fresh results against it, so a dismissed complaint
  stays gone across re-checks and repeats of the same phrase in the file.
- The panel header counts the dismissals and ↩ takes them back. The offline
  spell check then rebuilds its own list; LanguageTool's can only come from
  the server, so those are dropped with a nudge towards ✓ Check.

Dismissals belong to the open file and the session — they are cleared when
another file or workspace is opened, and never written to disk. A name or
invented term still belongs in the word list, which persists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011CVsxwa6YukFS2YFUY8W2j
2026-08-24 20:16:28 -05:00
landon 043cc692ac Add project-mode tooling: characters, outline, diff and revision status
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
2026-08-24 19:56:36 -05:00
landon e951d57d45 Fill in only the missing acts when a proposal is part-drafted
The beat generator had one brief, which assumed the proposal was a prose
premise and asked for a whole three-act sheet at "roughly four to eight
beats per act". Handed a proposal that already had acts beaten out past
that, it would regenerate them — compressing the author's own work into a
shorter paraphrase, losing the specific detail that made it worth
keeping, and burying the one act they actually wanted among two they did
not.

`mistral::analyse` now reads a proposal's act headings and counts the
numbered beats under each, in whatever notation the author used. An act
with three or more beats is drafted; fewer, or an act never mentioned, is
still to write. Two stray numbered lines are a note to self, not an act.
When some acts are drafted and some are not, a second briefing is sent.

That briefing asks for the missing acts only, and asks for them under the
author's own heading text and depth, so what comes back drops into their
document. It drops the per-act cap in favour of matching the density of
the drafted acts, and states which acts are context and which to write.
The drafted acts are canon: not to be rewritten, reordered, summarised,
condensed or reproduced, and — since a reveal often lands in a later act
— the model is told to work backwards from those reveals and plant what
they need using setups already placed.

The model is asked not to reproduce the drafted acts at all, rather than
to echo them unchanged. Reproducing twenty-odd beats verbatim is exactly
the drift being avoided; not asking makes it impossible.

Because the output is then partial, it is labelled: the status line names
the acts written, the file is saved as `<proposal> — beats Two.md`, and
its title says what it holds, so a partial sheet is not later mistaken
for a whole one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZGoPiDuZ7vmryNCJWjYSD
2026-08-23 16:12:20 -05:00
landon 41c3f088cd 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
2026-08-23 15:18:22 -05:00
landon b71ffc4f20 Suffix the drop-indicator stroke width to avoid the f32 fallback
egui's Stroke::new takes `width: impl Into<f32>`, so the unsuffixed 2.0
literal had no concrete type to infer from and relied on rustc falling
back to f32. That fallback is being removed (rust#154024), and the
float_literal_f32_fallback lint already warns it will become a hard
error, so spell the type out as 2.0_f32.

No behaviour change - the literal was already compiling as f32. The only
other Stroke::new call site passes an f32 variable and was unaffected.
cargo build is now warning-free; clippy is down to the four long-standing
warnings in odt.rs and order.rs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBWj9TphFMCoh7VHaSRnvQ
2026-08-22 09:47:02 -05:00
landon 8fdb9a4cbd Add a template-backed new-file button to the file list
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
2026-08-22 09:30:32 -05:00
landon 16009e38e4 Ignore the .claude worktree directory
Claude Code creates isolated worktrees under .claude/worktrees/, which
otherwise show up as untracked noise in git status.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBWj9TphFMCoh7VHaSRnvQ
2026-08-22 09:08:14 -05:00
landon 423e93c894 Split app.rs into an app/ module tree
src/app.rs had grown to 3,700 lines, ~2,400 of them a single impl App
block. Move it to src/app/mod.rs and spread the behaviour across eleven
child modules grouped by feature: workspace, grammar, spelling, beats,
find, editor, autocomplete, file_list, ui, style and util.

The new modules are children of app rather than siblings, so they still
reach App's private fields without widening its interface; methods and
free helpers that are now used across module boundaries are marked
pub(super). mod.rs keeps the state types, App::new and the eframe::App
update loop.

This is pure code motion - every non-blank line of the original file
reappears exactly once, and the only edits are the pub(super) markers,
the module scaffolding, and rewrapping five signatures that the added
prefix pushed past 100 columns. Largest file is now editor.rs at 520
lines. Tests still 56/56, and cargo clippy --release reports the same
five warnings as before the split.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBWj9TphFMCoh7VHaSRnvQ
2026-08-22 09:02:19 -05:00
landon 0972148e1a Have the beat prompt read all proposal attributes
Broaden the plot-beat system prompt beyond characters + plot: instruct
the model to read and honour every attribute the proposal states —
title, setting, genre, tone, characters, target age (adult / YA / middle
grade / …), and the loose plot summary. The beats now follow the stated
genre's conventions, keep the tone, and stay appropriate to the target
age; missing attributes are inferred rather than contradicted. Updates
the user-message framing and the README to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDfaEsTgcn61n3wgP62DFF
2026-08-20 14:44:14 -05:00
landon e5eadc08c1 Add Mistral plot-beat generation (Tools menu)
New Tools ▸ "Generate plot beats (Mistral)…" command: pick a novel
proposal markdown file (characters + loose plot summary) and have the
Mistral chat API lay it out as plot beats in the classic three-act
structure. The result opens in a floating window where it can be edited,
saved as a new "<proposal> — beats.md" workspace file, or copied.

- src/mistral.rs: pure-Rust ureq/rustls POST to /v1/chat/completions
  (self-contained; no C bindings). System prompt fixes the three-act
  markdown shape; friendly API-error surfacing. Unit tests for content
  parsing, empty/blank responses, error-message extraction, and the
  missing-key guard.
- config: mistral_api_key / mistral_model / mistral_base_url with
  effective-value helpers and defaults (mistral-large-latest,
  https://api.mistral.ai).
- app: background generation + poll (editor stays responsive), a
  Settings ▸ Mistral… window (key/model/base URL), and the results
  window with save/copy.
- Docs: README "Plot beats (Mistral)" section and help cheatsheet note.

Verified: cargo build --release clean, 56 tests pass (+6 Mistral),
ldd still only libc/libgcc/libm. Clippy: no new warnings in the added
code (the two extra vs. the old 3-warning baseline are toolchain-surfaced
in pre-existing app.rs code).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDfaEsTgcn61n3wgP62DFF
2026-08-20 14:39:08 -05:00
landon 5bc2acad25 Add header field-name autocomplete above the draft marker
Typing a header line above the draft marker (`#`/`##`/… + the start of a
field name) now shows an autocomplete popup: Tab/Enter or click to insert
"Field: ", Up/Down to choose, Esc to dismiss (and stay dismissed until the
partial changes). It never triggers in the prose below the marker.

Suggestions combine a built-in list (Title, Slug, POV, Word Count Target,
Characters, Setting, Conflict, …) with field names learned from every
file's header, refreshed on open and merged on save, so custom fields
autocomplete too.

Implementation: the editor gets a stable id so completions can drive its
cursor/focus; nav keys are consumed before the TextEdit sees them, and the
popup is recomputed from the caret each frame. Pure helpers
(header_completion_context, field_matches, header_field_names) are unit
tested; 50 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kRuP7JvXoUGdNNeg5ZSs
2026-08-14 07:30:48 -05:00
landon 894a0746e3 Add a per-file word-count-target bar in the file list
Each file row now shows a compact progress bar beside its name when the
file sets a Word Count Target, alongside the existing detailed bar in the
status bar for the active file. Same colour scheme (amber under / green in
range / blue over) and a hover tooltip with the exact words / target.

The per-file slug/POV cache is generalised to a FileMeta (slug, POV, goal,
prose word count), filled on open and refreshed on save; the selected
file's row reads live from the buffer so its bar tracks as you type.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kRuP7JvXoUGdNNeg5ZSs
2026-08-14 06:55:09 -05:00
landon c926b6696f Include the POV field in the file-list hover tooltip
Header parsing now also lifts a `POV:` line (any heading level). The
file-list tooltip combines it with the slug — "POV: <name>" then the
slug synopsis — cached per file alongside the slug (HoverInfo) and read
live from the buffer for the selected file. Adds a POV parse test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kRuP7JvXoUGdNNeg5ZSs
2026-08-14 06:49:53 -05:00
landon 9c4498a680 Show each file's slug as a file-list hover tooltip
Hovering a file in the left pane now shows its `# Slug:` line as a
tooltip — a one-line synopsis per chapter. Slugs are cached per file
(name -> slug), filled when the workspace opens (snapshot_slugs) and
refreshed on save; the selected file is parsed live from the buffer so
unsaved slug edits show immediately. The cache is kept consistent on
rename and delete.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kRuP7JvXoUGdNNeg5ZSs
2026-08-14 06:46:44 -05:00
landon 9d2207ba96 Parse per-file Word Count Target header and show progress
Header parsing now recognises a `Word Count Target:` line at any heading
level (## included) in addition to `# Title:`/`# Slug:`, and accepts one
or more leading `#`. The value parses a single number or a range —
"1500 - 2000", "1500-2000", "1,500 to 2,000" — into a min/max WordGoal
(stripped from the exported document like the other header lines).

When a target is set, the status bar shows a progress bar of the file's
prose word count (the body below the draft marker) against it: amber
under the target, green in range, blue over. Aliases "Word Count Goal:"
and "Word Count:" also work.

Adds preprocess tests for the range forms, ## heading level, alias, and
absence; 44 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kRuP7JvXoUGdNNeg5ZSs
2026-08-14 06:39:22 -05:00
landon 19dddf30c7 Add offline live spell checking (Hunspell via spellbook)
Spelling is now checked live as you type, fully offline, using the
pure-Rust `spellbook` crate to read Hunspell .aff/.dic dictionaries (no C
libhunspell — the binary stays self-contained; ldd unchanged).

- Canadian (en-CA, default) and British (en-GB) English dictionaries are
  compiled in; more languages are auto-discovered from system Hunspell
  folders and ~/.config/md-manuscript/dictionaries/. A "Spelling" row in
  the top bar toggles live checking and picks the dictionary.
- Misspellings are underlined in red; right-clicking a word opens a menu
  of suggested corrections. A results-panel and one-click fixes work too.
- Checks run on a background thread, debounced ~400ms after the last edit.
  Prose is extracted with pulldown-cmark so code spans, code blocks and
  link targets are skipped; ALL-CAPS initialisms are ignored.
- LanguageTool still layers on top: when its results are fresh they own
  the underlines (spelling + grammar); once you edit, the offline checker
  resumes. The editor underlines, issues panel and fix-apply logic are
  now shared between the two sources.

Bundled dictionaries are SCOWL-derived under a permissive license (kept
in dictionaries/<lang>/license). Adds spell-check unit tests (tokenizer,
code-block skipping, offset mapping, en-CA/en-GB spelling); 40 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kRuP7JvXoUGdNNeg5ZSs
2026-08-14 06:25:08 -05:00