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
This commit is contained in:
2026-08-24 19:56:36 -05:00
parent e951d57d45
commit 043cc692ac
23 changed files with 3879 additions and 144 deletions
+31
View File
@@ -123,3 +123,34 @@ fn current_branch(workspace: &Path) -> String {
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "main".to_string())
}
/// The unified diff of one file against the last commit, as raw diff text.
///
/// Revising is comparing: what changed in this scene since the last time it was
/// good? The repository already holds that answer, so the app asks git rather
/// than keeping snapshots of its own.
///
/// `rel` is relative to the *workspace*, which may sit below the repository
/// root, so the path is resolved through the work tree before being handed over.
pub fn diff_file(workspace: &Path, rel: &str) -> Result<String, String> {
let (ok, out) = run_git(
workspace,
&["--no-pager", "diff", "--no-color", "HEAD", "--", rel],
);
if !ok {
// A repository with no commits yet has no HEAD to compare against.
return Err(strip_command_echo(&out));
}
Ok(strip_command_echo(&out))
}
/// Whether the repository has at least one commit, and so something to diff.
pub fn has_commits(workspace: &Path) -> bool {
run_git(workspace, &["rev-parse", "--verify", "HEAD"]).0
}
/// Drop the `$ git …` line `run_git` prefixes for the log window; a diff view
/// wants the diff itself.
fn strip_command_echo(text: &str) -> String {
text.split_once('\n').map_or(text, |(_, rest)| rest).to_string()
}