Commit Graph

14 Commits

Author SHA1 Message Date
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 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 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