From f05ee655c35179028fe38364238cdf459d5e28f4 Mon Sep 17 00:00:00 2001 From: landon Date: Sun, 26 Jul 2026 14:59:13 -0500 Subject: [PATCH] Add zero-pad option for numbered chapter titles A "Zero-pad #" checkbox pads chapter titles that default to their position number with leading zeros to a uniform width (the digit count of the largest chapter number, e.g. 03. of 12). Padding never applies to "# Title:" headers or manual overrides. The setting persists in config. Co-Authored-By: Claude Opus 4.8 --- README.md | 4 +++- src/app.rs | 59 ++++++++++++++++++++++++++++++++++++++++++--------- src/config.rs | 5 +++++ 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 7c376ac..540a5d8 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,9 @@ Terminal=false auto-derived one (the field shows the automatic title as a hint). Leaving it blank falls back to a `# Title:` line in the header, and if there is none, to the chapter's **position in the left pane** followed by a period (`1.`, `2.`, - …). Titles are saved to `titles.json` and sync with git. + …). Tick **Zero-pad #** in the top bar to pad those numbered titles with + leading zeros to a uniform width (e.g. `03.` when there are 12 chapters). + Titles are saved to `titles.json` and sync with git. 5. Set the **Export** path (type it, or click **📂** to choose the `.odt` file in a native save dialog) and press **Export ODT** to produce the concatenated document. Each file becomes a chapter headed by its title, optionally followed diff --git a/src/app.rs b/src/app.rs index 074a7a3..4e7b523 100644 --- a/src/app.rs +++ b/src/app.rs @@ -294,17 +294,28 @@ impl App { }; } + /// Digits to zero-pad a defaulted chapter number to: the width of the largest + /// chapter number when padding is enabled, otherwise 1 (no padding). + fn index_pad_width(&self) -> usize { + if self.config.zero_pad_index { + self.files.len().to_string().len().max(1) + } else { + 1 + } + } + /// The title a chapter at `idx` would get without a manual override: its /// `# Title:` header, or its 1-based position (e.g. "3."). Shown as the hint /// in the chapter-title field. fn auto_title(&self, idx: usize, markdown: &str) -> String { let header = crate::preprocess::parse(markdown, &self.config.draft_marker); - resolve_chapter_title(None, header.title.as_deref(), idx) + resolve_chapter_title(None, header.title.as_deref(), idx, self.index_pad_width()) } fn export_odt(&mut self) { self.save_current(); let marker = self.config.draft_marker.clone(); + let pad_width = self.index_pad_width(); let mut chapters = Vec::new(); for (i, name) in self.files.iter().enumerate() { let raw = std::fs::read_to_string(self.path_for(name)).unwrap_or_default(); @@ -318,6 +329,7 @@ impl App { self.titles.get(name).map(String::as_str), header.title.as_deref(), i, + pad_width, ); chapters.push(Chapter { title, @@ -471,6 +483,17 @@ impl App { 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(4.0); }); @@ -815,18 +838,20 @@ fn render_preview(ui: &mut egui::Ui, markdown: &str) { } /// Resolve a chapter's title: a non-empty manual `override_title` wins, then the -/// `# Title:` header value, otherwise the chapter's 1-based position (e.g. "3."). +/// `# Title:` header value, otherwise the chapter's 1-based position followed by +/// a period (e.g. "3."), zero-padded to `pad_width` digits (`1` = no padding). fn resolve_chapter_title( override_title: Option<&str>, header_title: Option<&str>, index: usize, + pad_width: usize, ) -> String { override_title .map(str::trim) .filter(|t| !t.is_empty()) .or_else(|| header_title.map(str::trim).filter(|t| !t.is_empty())) .map(|t| t.to_string()) - .unwrap_or_else(|| format!("{}.", index + 1)) + .unwrap_or_else(|| format!("{:0width$}.", index + 1, width = pad_width)) } /// Count words in a string: runs of non-whitespace separated by whitespace. @@ -873,19 +898,33 @@ mod tests { #[test] fn chapter_title_falls_back_to_position() { - // No override, no header title -> 1-based index with a period. - assert_eq!(resolve_chapter_title(None, None, 0), "1."); - assert_eq!(resolve_chapter_title(None, None, 2), "3."); + // No override, no header title -> 1-based index with a period (width 1). + assert_eq!(resolve_chapter_title(None, None, 0, 1), "1."); + assert_eq!(resolve_chapter_title(None, None, 2, 1), "3."); // Header title is used when present. - assert_eq!(resolve_chapter_title(None, Some("The Gate"), 4), "The Gate"); + assert_eq!(resolve_chapter_title(None, Some("The Gate"), 4, 1), "The Gate"); // Override wins over everything, even a header title. assert_eq!( - resolve_chapter_title(Some("My Override"), Some("The Gate"), 4), + resolve_chapter_title(Some("My Override"), Some("The Gate"), 4, 1), "My Override" ); // Blank/whitespace override or header title are ignored. - assert_eq!(resolve_chapter_title(Some(" "), None, 1), "2."); - assert_eq!(resolve_chapter_title(Some(""), Some(" "), 6), "7."); + assert_eq!(resolve_chapter_title(Some(" "), None, 1, 1), "2."); + assert_eq!(resolve_chapter_title(Some(""), Some(" "), 6, 1), "7."); + } + + #[test] + fn chapter_number_zero_pads_to_width() { + // Width 2 pads single digits; wider numbers are unaffected. + assert_eq!(resolve_chapter_title(None, None, 0, 2), "01."); + assert_eq!(resolve_chapter_title(None, None, 8, 2), "09."); + assert_eq!(resolve_chapter_title(None, None, 11, 2), "12."); + assert_eq!(resolve_chapter_title(None, None, 4, 3), "005."); + // Padding never applies to a real title. + assert_eq!( + resolve_chapter_title(None, Some("The Gate"), 0, 3), + "The Gate" + ); } #[test] diff --git a/src/config.rs b/src/config.rs index 82209dd..d43fbfb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,6 +16,10 @@ pub struct Config { /// An empty value disables header splitting. #[serde(default = "default_marker")] pub draft_marker: String, + /// When a chapter title defaults to its position number, pad it with leading + /// zeros to the width of the largest chapter number (e.g. "03." of 12). + #[serde(default)] + pub zero_pad_index: bool, } /// Default header/body separator, matching the manuscript drafting convention. @@ -32,6 +36,7 @@ impl Default for Config { export_path, show_preview: false, draft_marker: default_marker(), + zero_pad_index: false, } } }