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:
+44
-38
@@ -1,5 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Persistent application configuration (which workspace to open, plus UI prefs).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -85,16 +85,15 @@ pub struct Config {
|
||||
/// and `{{date}}` to today's UTC date. Blank = the built-in default.
|
||||
#[serde(default = "default_new_file_template")]
|
||||
pub new_file_template: String,
|
||||
/// Directory holding the cookiecutter template used by **File ▸ New
|
||||
/// project…** (the folder containing `cookiecutter.json`).
|
||||
/// Directory holding the template used by **File ▸ New project…** — the
|
||||
/// folder containing `cookiecutter.json`. Rendered by the built-in
|
||||
/// MiniJinja engine; the manifest keeps the cookiecutter name so existing
|
||||
/// templates still work.
|
||||
#[serde(default = "default_project_template")]
|
||||
pub project_template: PathBuf,
|
||||
/// Path to the `cookiecutter` executable. Blank means look on `PATH` and in
|
||||
/// the usual per-user Python prefixes.
|
||||
#[serde(default)]
|
||||
pub cookiecutter_bin: String,
|
||||
/// Subfolder of a freshly generated project to open as the workspace. Blank
|
||||
/// (or a folder the template did not create) opens the project root.
|
||||
/// Which folder of a project holds the book — the manuscript folder. A
|
||||
/// generated project always opens at its root; this names the folder inside
|
||||
/// it whose files are ordered, numbered and exported.
|
||||
#[serde(default = "default_project_open_subdir")]
|
||||
pub project_open_subdir: String,
|
||||
/// Whether to run the template's pre/post-generation hooks. The snowflake
|
||||
@@ -184,39 +183,46 @@ pub fn default_show_toolbar() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Default cookiecutter template for **File ▸ New project…**.
|
||||
/// Uses the bundled template from the project's templates/snowflake directory.
|
||||
/// Where the bundled project template lives, for **File ▸ New project…**.
|
||||
///
|
||||
/// Walks up from the executable and then tries the working directory, which
|
||||
/// covers `cargo run`, a release build run in place, and an installed binary
|
||||
/// with the templates beside it.
|
||||
///
|
||||
/// Returns a path even when nothing is found, rather than failing: this is the
|
||||
/// default for a config field, so it is reached on any machine that has no
|
||||
/// `config.json` yet — and an installed binary without the templates folder
|
||||
/// beside it is an ordinary way to arrive here, not a reason to refuse to
|
||||
/// start. The path that comes back simply has no `cookiecutter.json` in it,
|
||||
/// which the settings dialog reports ("✖ no cookiecutter.json in that folder")
|
||||
/// and project generation refuses with a message naming the folder.
|
||||
pub fn default_project_template() -> PathBuf {
|
||||
// Find the bundled template relative to the executable's location
|
||||
if let Ok(exe_path) = std::env::current_exe() {
|
||||
// Walk up from the executable's directory looking for templates/snowflake/cookiecutter.json
|
||||
// This handles various build configurations (release, debug, cargo run, installed binary)
|
||||
let mut current = exe_path.parent().unwrap_or_else(|| exe_path.as_path());
|
||||
loop {
|
||||
// Check if current/templates/snowflake/cookiecutter.json exists
|
||||
let candidate = current.join("templates/snowflake");
|
||||
if candidate.join("cookiecutter.json").is_file() {
|
||||
return candidate;
|
||||
}
|
||||
// Try going up one level
|
||||
match current.parent() {
|
||||
Some(parent) => current = parent,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
// Also try relative to current working directory
|
||||
if let Ok(cwd) = std::env::current_dir() {
|
||||
let candidate = cwd.join("templates/snowflake");
|
||||
const BUNDLED: &str = "templates/snowflake";
|
||||
let exe_dir = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|exe| exe.parent().map(Path::to_path_buf));
|
||||
|
||||
if let Some(dir) = exe_dir.as_deref() {
|
||||
for ancestor in dir.ancestors() {
|
||||
let candidate = ancestor.join(BUNDLED);
|
||||
if candidate.join("cookiecutter.json").is_file() {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we can't find the bundled template, panic with a clear error
|
||||
// The bundled template should always be present in a properly built release
|
||||
panic!("Bundled template not found. Please ensure templates/snowflake/cookiecutter.json exists relative to the executable.");
|
||||
if let Ok(cwd) = std::env::current_dir() {
|
||||
let candidate = cwd.join(BUNDLED);
|
||||
if candidate.join("cookiecutter.json").is_file() {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing found: name where the templates are expected to be, so the
|
||||
// message the user eventually sees points at a useful folder.
|
||||
match exe_dir {
|
||||
Some(dir) => dir.join(BUNDLED),
|
||||
None => PathBuf::from(BUNDLED),
|
||||
}
|
||||
}
|
||||
|
||||
/// Default subfolder opened after generating a project: the snowflake
|
||||
@@ -262,7 +268,9 @@ pub fn default_new_file_template() -> String {
|
||||
"---",
|
||||
"title: {{name}}",
|
||||
"slug: ",
|
||||
"status: draft",
|
||||
"pov: ",
|
||||
"characters: []",
|
||||
"word_count_target: ",
|
||||
"---",
|
||||
"",
|
||||
@@ -344,7 +352,6 @@ impl Default for Config {
|
||||
mistral_base_url: default_mistral_base_url(),
|
||||
new_file_template: default_new_file_template(),
|
||||
project_template: default_project_template(),
|
||||
cookiecutter_bin: String::new(),
|
||||
project_open_subdir: default_project_open_subdir(),
|
||||
project_run_hooks: default_project_run_hooks(),
|
||||
projects_dir: default_projects_dir(),
|
||||
@@ -560,7 +567,6 @@ mod tests {
|
||||
// Credentials are absent until set; blank is what tells a hook to skip.
|
||||
assert!(cfg.gitea_url.is_empty());
|
||||
assert!(cfg.gitea_token.is_empty());
|
||||
assert!(cfg.cookiecutter_bin.is_empty());
|
||||
}
|
||||
|
||||
/// The folder opened after generating a project. It is the snowflake
|
||||
|
||||
Reference in New Issue
Block a user