Finish bundled-template migration; simplify frontmatter; de-duplicate toolbar/menu

Completes the move off the external cookiecutter tool: drops
src/cookiecutter.rs and the cookiecutter_bin/Template-path setting now
that scaffolding always runs through the built-in MiniJinja engine,
and rewrites default_project_template() to walk up from the executable
without panicking when the bundled template can't be found. Rounds
out the snowflake template with the remaining 06-Revisions,
07-Final, 08-Covers and 09-Archive folders.

Simplifies the scene/chapter YAML frontmatter (drops type/word_count_min/
word_count_max in favor of status + a flat characters list) and updates
the new-file template, header parsing, outline, file list and Story
Bible editor to match.

Separately, de-duplicates the toolbar and menu bar: settings that only
existed as always-visible toolbar controls (draft marker, zero-pad,
spell-check toggle, dictionary picker) move into their Settings
windows, toolbar buttons that exactly duplicated a menu item are
dropped, and toolbar-only actions with no menu path (open project
folder, git sync/init) gain one under File. Also fixes two ambiguous
menu labels: the Story Bible editor now reads "Story Bible editor…"
next to "Story Bible…" in Tools instead of sharing a label with Edit,
and Settings' "New project…" is now "New-project settings…" so it
doesn't read like File's actual create action.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 10:35:52 -05:00
parent c61a7b605a
commit 0f144737a5
23 changed files with 1199 additions and 1091 deletions
+45 -128
View File
@@ -6,8 +6,14 @@ use super::*;
impl App {
// ---- UI ----------------------------------------------------------------
/// The toolbar — the Workspace, Export, Grammar and Spelling rows — and the
/// status bar along the bottom, which is rendered here too.
/// The toolbar — the Workspace, Export and Grammar rows — and the status
/// bar along the bottom, which is rendered here too.
///
/// Anything with a menu equivalent (opening the workspace, syncing git,
/// toggling a panel, LanguageTool settings, …) lives only in the menu bar;
/// this panel keeps just the controls that need a live value at hand —
/// the workspace/export paths and the grammar-check trigger — so hiding it
/// (View ▸ Toolbar, or Ctrl+D) never costs you a feature, only convenience.
///
/// The toolbar collapses through `show_animated`, which reserves no height
/// when hidden and slides rather than snapping. Note that it guards only the
@@ -22,9 +28,6 @@ impl App {
egui::TextEdit::singleline(&mut self.workspace_input)
.desired_width(300.0),
);
if ui.button("📂").on_hover_text("Browse for workspace folder").clicked() {
self.browse_workspace();
}
if ui.button("Open").clicked()
|| (resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)))
{
@@ -33,23 +36,6 @@ impl App {
self.config.save();
self.open_workspace();
}
ui.separator();
if self.is_repo {
let hover = match &self.repo_root {
Some(root) if root.as_path() != self.workspace() => {
format!("Commit & push to the repository at {}", root.display())
}
_ => "Commit & push this workspace's repository".to_string(),
};
if ui.button("⟳ Sync (git)").on_hover_text(hover).clicked() {
self.git_sync();
}
} else if ui.button("Init git").clicked() {
self.git_init();
}
if ui.button("Log").clicked() {
self.show_log = !self.show_log;
}
});
ui.add_space(2.0);
ui.horizontal(|ui| {
@@ -78,48 +64,6 @@ impl App {
{
self.export_manuscript();
}
// Resolved first: the hover text borrows the path, and the click
// handler needs `self` mutably.
let project = self.project_root().display().to_string();
if ui
.button("📁")
.on_hover_text(format!("Open the project folder {project}"))
.clicked()
{
self.open_project_folder();
}
ui.separator();
if ui
.checkbox(&mut self.config.show_preview, "Preview")
.changed()
{
self.config.save();
}
ui.separator();
ui.label("Draft marker:")
.on_hover_text(
"Text above this line in each file (its header) is dropped on export; \
# Title: / # Slug: lines become the chapter heading and caption. \
Leave blank to export files whole.",
);
let marker_resp = ui.add(
egui::TextEdit::singleline(&mut self.config.draft_marker)
.desired_width(140.0),
);
if marker_resp.lost_focus() {
self.config.save();
}
ui.separator();
if ui
.checkbox(&mut self.config.zero_pad_index, "Zero-pad #")
.on_hover_text(
"When a chapter title defaults to its number, pad it with \
leading zeros (e.g. 03. of 12).",
)
.changed()
{
self.config.save();
}
});
ui.add_space(2.0);
ui.horizontal(|ui| {
@@ -143,58 +87,10 @@ impl App {
{
self.clear_lt();
}
if ui
.button("⚙")
.on_hover_text("LanguageTool settings")
.clicked()
{
self.show_settings = true;
}
if !self.lt_status.is_empty() {
ui.label(egui::RichText::new(&self.lt_status).weak());
}
});
ui.add_space(2.0);
ui.horizontal(|ui| {
ui.label("Spelling:").on_hover_text(
"Offline spell check (Hunspell dictionaries). Underlines misspellings \
as you type when LanguageTool results aren't current; right-click a \
word for suggestions.",
);
if ui
.checkbox(&mut self.config.spell_check, "Check as I type")
.on_hover_text("Underline misspellings live using the dictionary below")
.changed()
{
self.config.save();
if self.config.spell_check {
// Re-check the current buffer straight away.
self.spell_dirty = true;
self.spell_last_edit = None;
}
}
ui.label("Dictionary:");
let current = self.config.spell_language.clone();
let mut pick: Option<String> = None;
egui::ComboBox::from_id_salt("spell_dict")
.selected_text(current.clone())
.show_ui(ui, |ui| {
for d in &self.spell_dicts {
if ui
.selectable_label(current == d.id, &d.label)
.clicked()
{
pick = Some(d.id.clone());
}
}
});
if let Some(id) = pick {
self.set_spell_language(&id);
}
if !self.spell_status.is_empty() {
ui.label(egui::RichText::new(&self.spell_status).weak());
}
});
ui.add_space(4.0);
});
@@ -271,7 +167,8 @@ impl App {
if ui
.button("✨ New project…")
.on_hover_text(
"Scaffold a project from the cookiecutter template and open its drafting folder",
"Scaffold a project from the bundled template and open \
its drafting folder",
)
.clicked()
{
@@ -283,6 +180,26 @@ impl App {
ui.close_menu();
self.browse_workspace();
}
if ui.button("📁 Open project folder").clicked() {
ui.close_menu();
self.open_project_folder();
}
if self.is_repo {
let hover = match &self.repo_root {
Some(root) if root.as_path() != self.workspace() => {
format!("Commit & push to the repository at {}", root.display())
}
_ => "Commit & push this workspace's repository".to_string(),
};
if ui.button("⟳ Sync (git)").on_hover_text(hover).clicked() {
ui.close_menu();
self.git_sync();
}
} else if ui.button("Init git").clicked() {
ui.close_menu();
self.git_init();
}
ui.separator();
if ui.button(self.export_label()).clicked() {
ui.close_menu();
self.export_manuscript();
@@ -321,10 +238,6 @@ impl App {
ui.close_menu();
self.open_find(true);
}
if ui.button("📖 Story Bible…").clicked() {
ui.close_menu();
self.open_story_bible_editor();
}
if ui.button("📸 Snapshots…").clicked() {
ui.close_menu();
self.show_snapshots = true;
@@ -343,6 +256,10 @@ impl App {
ui.close_menu();
self.show_story_bible = true;
}
if ui.button("📖 Story Bible editor…").clicked() {
ui.close_menu();
self.open_story_bible_editor();
}
ui.separator();
let busy = self.beats_rx.is_some();
if ui
@@ -360,7 +277,7 @@ impl App {
if ui
.checkbox(&mut self.config.show_toolbar, "Toolbar")
.on_hover_text(
"The Workspace, Export, Grammar and Spelling rows \
"The Workspace, Export and Grammar rows \
under the menu bar",
)
.clicked()
@@ -432,7 +349,7 @@ impl App {
ui.close_menu();
self.show_template_settings = true;
}
if ui.button("New project…").clicked() {
if ui.button("New-project settings…").clicked() {
ui.close_menu();
self.show_project_settings = true;
}
@@ -444,7 +361,7 @@ impl App {
ui.close_menu();
self.show_manuscript_settings = true;
}
if ui.button("Word list…").clicked() {
if ui.button("Spelling…").clicked() {
ui.close_menu();
self.show_wordlist = true;
}
@@ -459,9 +376,9 @@ impl App {
// a control that hides the toolbar cannot live inside it.
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let (icon, hint) = if self.config.show_toolbar {
("⬆", "Hide the toolbar (Workspace, Export, Grammar, Spelling)")
("⬆", "Hide the toolbar (Workspace, Export, Grammar)")
} else {
("⬇", "Show the toolbar (Workspace, Export, Grammar, Spelling)")
("⬇", "Show the toolbar (Workspace, Export, Grammar)")
};
if ui.button(icon).on_hover_text(hint).clicked() {
self.config.show_toolbar = !self.config.show_toolbar;
@@ -666,11 +583,11 @@ impl App {
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()
{
// Nothing to say about a file with no header to fold away. Both forms
// count: asking only about the draft marker meant a frontmatter file
// never offered the toggle, because it has no marker to find — the fold
// itself has handled frontmatter all along.
if !collapsed && !self.header_is_foldable() {
return;
}
ui.horizontal(|ui| {
@@ -678,7 +595,7 @@ impl App {
let n = self.hidden_field_count();
(
format!("▸ header hidden ({n} field{})", if n == 1 { "" } else { "s" }),
"Show the fields above the draft marker",
"Show this file's fields again",
)
} else {
(