Parse per-file Word Count Target header and show progress
Header parsing now recognises a `Word Count Target:` line at any heading level (## included) in addition to `# Title:`/`# Slug:`, and accepts one or more leading `#`. The value parses a single number or a range — "1500 - 2000", "1500-2000", "1,500 to 2,000" — into a min/max WordGoal (stripped from the exported document like the other header lines). When a target is set, the status bar shows a progress bar of the file's prose word count (the body below the draft marker) against it: amber under the target, green in range, blue over. Aliases "Word Count Goal:" and "Word Count:" also work. Adds preprocess tests for the range forms, ## heading level, alias, and absence; 44 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N9kRuP7JvXoUGdNNeg5ZSs
This commit is contained in:
+52
@@ -523,6 +523,16 @@ impl App {
|
||||
resolve_chapter_title(None, header.title.as_deref(), idx, self.index_pad_width())
|
||||
}
|
||||
|
||||
/// The current file's word-count target (from its `Word Count Target:`
|
||||
/// header) paired with its current prose word count, if a target is set.
|
||||
/// Prose = the body below the draft marker, so header metadata isn't counted.
|
||||
fn current_goal(&self) -> Option<(crate::preprocess::WordGoal, usize)> {
|
||||
self.selected?;
|
||||
let header = crate::preprocess::parse(&self.buffer, &self.config.draft_marker);
|
||||
let goal = header.goal?;
|
||||
Some((goal, count_words(&header.body)))
|
||||
}
|
||||
|
||||
fn export_odt(&mut self) {
|
||||
self.save_current();
|
||||
let marker = self.config.draft_marker.clone();
|
||||
@@ -1205,6 +1215,7 @@ impl App {
|
||||
let dirty = if self.dirty { " • unsaved" } else { "" };
|
||||
ui.label(format!("{}{dirty}", self.status));
|
||||
|
||||
let goal = self.current_goal();
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if let Some(idx) = self.selected {
|
||||
let name = &self.files[idx];
|
||||
@@ -1224,6 +1235,23 @@ impl App {
|
||||
.on_hover_text(
|
||||
"Words in the current file · net change since this session opened",
|
||||
);
|
||||
|
||||
// Progress toward the file's Word Count Target, if set.
|
||||
if let Some((goal, prose)) = goal {
|
||||
let (frac, color, text) = goal_progress(goal, prose);
|
||||
ui.separator();
|
||||
ui.add(
|
||||
egui::ProgressBar::new(frac)
|
||||
.desired_width(150.0)
|
||||
.fill(color)
|
||||
.text(egui::RichText::new(text).small()),
|
||||
)
|
||||
.on_hover_text(
|
||||
"Prose words vs the file's “Word Count Target” header \
|
||||
(counts the body below the draft marker). Amber = under \
|
||||
target, green = in range, blue = over.",
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -2564,6 +2592,30 @@ fn count_words(s: &str) -> usize {
|
||||
s.split_whitespace().count()
|
||||
}
|
||||
|
||||
/// Progress toward a word-count target: the bar fraction (0..=1), a fill colour
|
||||
/// (amber under the range, green within it, blue over it), and a label like
|
||||
/// "1,234 / 1,500–2,000".
|
||||
fn goal_progress(goal: crate::preprocess::WordGoal, prose: usize) -> (f32, egui::Color32, String) {
|
||||
let frac = if goal.max == 0 {
|
||||
0.0
|
||||
} else {
|
||||
(prose as f32 / goal.max as f32).clamp(0.0, 1.0)
|
||||
};
|
||||
let color = if prose < goal.min {
|
||||
egui::Color32::from_rgb(0xC8, 0x8A, 0x2A) // amber: below target
|
||||
} else if prose <= goal.max {
|
||||
egui::Color32::from_rgb(0x3F, 0x9E, 0x4F) // green: in range
|
||||
} else {
|
||||
egui::Color32::from_rgb(0x3B, 0x82, 0xF6) // blue: over target
|
||||
};
|
||||
let target = if goal.min == goal.max {
|
||||
thousands(goal.max)
|
||||
} else {
|
||||
format!("{}–{}", thousands(goal.min), thousands(goal.max))
|
||||
};
|
||||
(frac, color, format!("{} / {}", thousands(prose), target))
|
||||
}
|
||||
|
||||
/// Format a non-negative integer with comma thousands separators (e.g. 12345 -> "12,345").
|
||||
fn thousands(n: usize) -> String {
|
||||
let digits = n.to_string();
|
||||
|
||||
Reference in New Issue
Block a user