Bundle the project template into the binary; add outline-to-draft
links, a sprint timer, placeholder scanning, corkboard, and a quick-capture inbox - Compile templates/snowflake into the binary via include_dir, so New project… never depends on files installed beside the executable; Settings ▸ Template becomes an optional external override, and a broken saved override now falls back to the bundled template instead of failing project generation. - Outline beats now link to a matching chapter for a one-click jump. - Sprint window: a daily whole-manuscript word goal plus a Pomodoro-style writing sprint timer, with a compact countdown in the status bar while in Distraction-free mode. - Placeholders window: find [TODO]/[TK]/[FIXME]/[XXX]/[NOTE]/[???] markers left in the draft and jump straight to any of them. - Snowflake progress strip in the file panel: a nudge-only readout of which snowflake stage folders actually have content. - Corkboard window: drag-reorderable synopsis cards, one per chapter. - Quick-capture popup (Ctrl+Shift+N): jot a stray idea straight into the project's Scratch Pad without losing your place in the draft. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
//! A heuristic, nudge-only progress strip for the Snowflake-staged project
|
||||
//! template (`00-Scratch Pad` … `09-Archive`): how much each stage folder
|
||||
//! actually holds, so the planning → draft handoff is visible at a glance.
|
||||
//! Tolerant of custom/non-snowflake templates — a missing stage folder is
|
||||
//! simply left out, never an error.
|
||||
|
||||
use super::*;
|
||||
|
||||
/// The bundled snowflake template's stage folders, in order. A project built
|
||||
/// from a custom template may not have all — or any — of these; each is
|
||||
/// only shown if it actually exists.
|
||||
const STAGES: &[&str] = &[
|
||||
"00-Scratch Pad",
|
||||
"01-Premise",
|
||||
"02-Synopsis",
|
||||
"03-Story-Bible",
|
||||
"04-Outline",
|
||||
"05-First Draft",
|
||||
"06-Revisions",
|
||||
"07-Final",
|
||||
"08-Covers",
|
||||
"09-Archive",
|
||||
];
|
||||
|
||||
/// Word count under which a stage counts as merely "started" rather than
|
||||
/// "solid" — past template boilerplate, short of a full stage's worth.
|
||||
const SOLID_THRESHOLD: usize = 200;
|
||||
|
||||
/// Total prose words under each stage folder that actually exists in
|
||||
/// `workspace`, in template order. A missing folder is left out entirely.
|
||||
pub(super) fn stage_word_counts(workspace: &Path) -> Vec<(String, usize)> {
|
||||
STAGES
|
||||
.iter()
|
||||
.filter_map(|stage| {
|
||||
let dir = workspace.join(stage);
|
||||
if !dir.is_dir() {
|
||||
return None;
|
||||
}
|
||||
let words: usize = markdown_files_under(&dir)
|
||||
.iter()
|
||||
.map(|path| count_words(&std::fs::read_to_string(path).unwrap_or_default()))
|
||||
.sum();
|
||||
Some((stage.to_string(), words))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// A compact "○/◐/● stage name" line per snowflake stage folder found in
|
||||
/// the workspace. Collapsed by default and draws nothing at all when no
|
||||
/// stage folder is present, so a custom-template project isn't left with
|
||||
/// an empty header.
|
||||
pub(super) fn snowflake_progress_section(&self, ui: &mut egui::Ui) {
|
||||
let counts = stage_word_counts(self.workspace());
|
||||
if counts.is_empty() {
|
||||
return;
|
||||
}
|
||||
egui::CollapsingHeader::new("Snowflake progress")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
for (stage, words) in &counts {
|
||||
let icon = if *words == 0 {
|
||||
"○"
|
||||
} else if *words < SOLID_THRESHOLD {
|
||||
"◐"
|
||||
} else {
|
||||
"●"
|
||||
};
|
||||
ui.label(
|
||||
egui::RichText::new(format!("{icon} {stage}"))
|
||||
.small()
|
||||
.weak(),
|
||||
)
|
||||
.on_hover_text(format!("{words} words"));
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
|
||||
fn temp_dir(name: &str) -> PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"md-manuscript-snowflake-progress-test-{name}-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_missing_stage_folder_is_left_out() {
|
||||
let ws = temp_dir("missing");
|
||||
assert!(stage_word_counts(&ws).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_stage_folder_counts_zero_words() {
|
||||
let ws = temp_dir("empty");
|
||||
fs::create_dir_all(ws.join("01-Premise")).unwrap();
|
||||
let counts = stage_word_counts(&ws);
|
||||
assert_eq!(counts, vec![("01-Premise".to_string(), 0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_populated_stage_folder_sums_words_across_its_files() {
|
||||
let ws = temp_dir("populated");
|
||||
let stage = ws.join("05-First Draft");
|
||||
fs::create_dir_all(stage.join("Act 1")).unwrap();
|
||||
fs::write(stage.join("Act 1").join("ch01.md"), "one two three").unwrap();
|
||||
fs::write(stage.join("ch02.md"), "four five").unwrap();
|
||||
let counts = stage_word_counts(&ws);
|
||||
assert_eq!(counts, vec![("05-First Draft".to_string(), 5)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stages_are_reported_in_template_order_not_filesystem_order() {
|
||||
let ws = temp_dir("order");
|
||||
fs::create_dir_all(ws.join("09-Archive")).unwrap();
|
||||
fs::create_dir_all(ws.join("01-Premise")).unwrap();
|
||||
let counts = stage_word_counts(&ws);
|
||||
assert_eq!(
|
||||
counts.iter().map(|(s, _)| s.as_str()).collect::<Vec<_>>(),
|
||||
vec!["01-Premise", "09-Archive"]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user