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:
+121
-2
@@ -176,7 +176,7 @@ impl App {
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if let Some(idx) = self.selected {
|
||||
let name = &self.files[idx];
|
||||
let total = count_words(&self.buffer);
|
||||
let total = count_words(&self.document());
|
||||
let baseline =
|
||||
self.session_start_counts.get(name).copied().unwrap_or(0);
|
||||
let delta = total as i64 - baseline as i64;
|
||||
@@ -193,6 +193,21 @@ impl App {
|
||||
"Words in the current file · net change since this session opened",
|
||||
);
|
||||
|
||||
// The whole book's length, which is the number that
|
||||
// actually tells you where the manuscript stands.
|
||||
ui.separator();
|
||||
ui.label(
|
||||
egui::RichText::new(format!(
|
||||
"{} total",
|
||||
thousands(self.manuscript_words())
|
||||
))
|
||||
.weak(),
|
||||
)
|
||||
.on_hover_text(
|
||||
"Prose words across the whole manuscript (reference \
|
||||
files are not counted)",
|
||||
);
|
||||
|
||||
// Progress toward the file's Word Count Target, if set.
|
||||
if let Some((goal, prose)) = goal {
|
||||
let (frac, color, text) = goal_progress(goal, prose);
|
||||
@@ -240,6 +255,17 @@ impl App {
|
||||
ui.close_menu();
|
||||
self.export_odt();
|
||||
}
|
||||
if ui
|
||||
.button("Export chapters + master (.odm)")
|
||||
.on_hover_text(
|
||||
"One .odt per chapter in a chapters/ folder, plus an \
|
||||
.odm master that links them",
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
ui.close_menu();
|
||||
self.export_master();
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Quit").clicked() {
|
||||
ui.close_menu();
|
||||
@@ -247,12 +273,30 @@ impl App {
|
||||
}
|
||||
});
|
||||
ui.menu_button("Edit", |ui| {
|
||||
if ui
|
||||
.button("⇄ Changes since last commit…")
|
||||
.on_hover_text("Diff the open file against its committed version")
|
||||
.clicked()
|
||||
{
|
||||
ui.close_menu();
|
||||
self.open_diff();
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("🔍 Find / Replace…").clicked() {
|
||||
ui.close_menu();
|
||||
self.open_find(true);
|
||||
}
|
||||
});
|
||||
ui.menu_button("Tools", |ui| {
|
||||
if ui.button("☑ Outline…").clicked() {
|
||||
ui.close_menu();
|
||||
self.show_outline = true;
|
||||
}
|
||||
if ui.button("👥 Characters…").clicked() {
|
||||
ui.close_menu();
|
||||
self.show_characters = true;
|
||||
}
|
||||
ui.separator();
|
||||
let busy = self.beats_rx.is_some();
|
||||
if ui
|
||||
.add_enabled(
|
||||
@@ -272,6 +316,30 @@ impl App {
|
||||
{
|
||||
self.config.save();
|
||||
}
|
||||
if ui
|
||||
.checkbox(
|
||||
&mut self.config.show_reference_files,
|
||||
"Show reference files",
|
||||
)
|
||||
.on_hover_text(
|
||||
"List a project's characters, outline and scratch pad in \
|
||||
the file panel alongside the manuscript",
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.config.save();
|
||||
}
|
||||
if ui
|
||||
.checkbox(&mut self.config.collapse_header, "Hide header in editor")
|
||||
.on_hover_text(
|
||||
"Edit the prose alone, with the fields above the draft \
|
||||
marker folded away",
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.apply_header_collapse();
|
||||
self.config.save();
|
||||
}
|
||||
ui.checkbox(&mut self.show_lt_panel, "Grammar panel");
|
||||
ui.checkbox(&mut self.show_log, "Git log");
|
||||
});
|
||||
@@ -292,6 +360,14 @@ impl App {
|
||||
ui.close_menu();
|
||||
self.show_project_settings = true;
|
||||
}
|
||||
if ui.button("Manuscript details…").clicked() {
|
||||
ui.close_menu();
|
||||
self.show_manuscript_settings = true;
|
||||
}
|
||||
if ui.button("Word list…").clicked() {
|
||||
ui.close_menu();
|
||||
self.show_wordlist = true;
|
||||
}
|
||||
});
|
||||
ui.menu_button("Help", |ui| {
|
||||
if ui.button("📝 Markdown cheatsheet").clicked() {
|
||||
@@ -357,7 +433,7 @@ impl App {
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Chapter title:");
|
||||
let auto = self.auto_title(idx, &self.buffer);
|
||||
let auto = self.auto_title(idx, &self.document());
|
||||
let resp = ui.add(
|
||||
egui::TextEdit::singleline(&mut self.title_input)
|
||||
.hint_text(format!("auto: {auto}"))
|
||||
@@ -372,6 +448,7 @@ impl App {
|
||||
.weak(),
|
||||
);
|
||||
});
|
||||
self.header_toggle(ui);
|
||||
ui.separator();
|
||||
|
||||
if self.show_find {
|
||||
@@ -450,6 +527,48 @@ impl App {
|
||||
self.decline_pending_repo();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A single line saying whether the editorial header is folded away, and
|
||||
/// letting you flip it.
|
||||
///
|
||||
/// It exists so a hidden header can never be mistaken for a file that has
|
||||
/// none: the editor is showing less than the file holds, and that has to be
|
||||
/// visible without opening a menu.
|
||||
pub(super) fn header_toggle(&mut self, ui: &mut egui::Ui) {
|
||||
let collapsed = self.header_stash.is_some();
|
||||
// Nothing to say about a file with no marker to fold at.
|
||||
if !collapsed
|
||||
&& crate::preprocess::split_at_marker(&self.buffer, &self.config.draft_marker)
|
||||
.is_none()
|
||||
{
|
||||
return;
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
let (label, hover) = if collapsed {
|
||||
let n = self.hidden_field_count();
|
||||
(
|
||||
format!("▸ header hidden ({n} field{})", if n == 1 { "" } else { "s" }),
|
||||
"Show the fields above the draft marker",
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"▾ header shown".to_string(),
|
||||
"Fold the fields away and edit the prose alone",
|
||||
)
|
||||
};
|
||||
if ui
|
||||
.add(egui::Button::new(egui::RichText::new(label).small().weak()).frame(false))
|
||||
.on_hover_text(hover)
|
||||
.clicked()
|
||||
{
|
||||
self.config.collapse_header = !collapsed;
|
||||
self.apply_header_collapse();
|
||||
self.config.save();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Very small markdown-ish preview (headings emphasised, everything else plain).
|
||||
|
||||
Reference in New Issue
Block a user