diff --git a/README.md b/README.md index 8f2b77b..e3b4eee 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ A small desktop application for Debian that treats a directory of markdown files as an ordered manuscript. * **Left pane** — every `*.md` file in the workspace directory, in a manual order - you set by **dragging the `⠿` handle** up and down. + you set by **dragging the `⠿` handle** up and down. **Hover a file** to see its + `# Slug:` line as a tooltip — a handy one-line synopsis of each chapter. * **Right pane** — a markdown text editor for the selected file (optional live preview via the *Preview* checkbox). * **Order + contents are saved and version-controlled with git.** The order lives diff --git a/src/app.rs b/src/app.rs index 9e98d71..4200a33 100644 --- a/src/app.rs +++ b/src/app.rs @@ -74,6 +74,10 @@ pub struct App { /// Each file's word count captured when the workspace was opened, used as the /// per-file baseline for the "this session" delta. session_start_counts: HashMap, + /// Cached `# Slug:` value per file (name -> slug) for the file-list hover + /// tooltip. Filled when the workspace opens and refreshed on save; the + /// selected file is read live from the buffer instead. + slugs: HashMap, /// Grammar/spelling issues from the last LanguageTool check. lt_matches: Vec, /// The exact buffer text the current `lt_matches` were computed against; @@ -159,6 +163,7 @@ impl App { repo_root: None, pending_repo: None, session_start_counts: HashMap::new(), + slugs: HashMap::new(), lt_matches: Vec::new(), lt_checked_text: String::new(), lt_status: String::new(), @@ -216,6 +221,7 @@ impl App { self.pending_delete = false; self.clear_lt(); self.session_start_counts = self.snapshot_counts(); + self.slugs = self.snapshot_slugs(); if !self.files.is_empty() { self.select(0); } @@ -289,6 +295,19 @@ impl App { .collect() } + /// Read every file and return its `# Slug:` header value (only for files that + /// have one), for the file-list hover tooltip. + fn snapshot_slugs(&self) -> HashMap { + self.files + .iter() + .filter_map(|name| { + let text = std::fs::read_to_string(self.path_for(name)).unwrap_or_default(); + let header = crate::preprocess::parse(&text, &self.config.draft_marker); + header.slug.map(|slug| (name.clone(), slug)) + }) + .collect() + } + fn persist_order(&self) { let _ = order::write_order(self.workspace(), &self.files); } @@ -321,12 +340,26 @@ impl App { fn save_current(&mut self) { if let Some(idx) = self.selected { if self.dirty { - if let Some(name) = self.files.get(idx) { - let path = self.path_for(name); + if let Some(name) = self.files.get(idx).cloned() { + let path = self.path_for(&name); match std::fs::write(&path, &self.buffer) { Ok(_) => { self.dirty = false; self.status = format!("Saved {name}"); + // Keep the hover-tooltip slug cache in step. + let slug = crate::preprocess::parse( + &self.buffer, + &self.config.draft_marker, + ) + .slug; + match slug { + Some(s) => { + self.slugs.insert(name, s); + } + None => { + self.slugs.remove(&name); + } + } } Err(e) => self.status = format!("Save failed: {e}"), } @@ -412,6 +445,7 @@ impl App { self.files.remove(idx); self.titles.remove(&name); self.session_start_counts.remove(&name); + self.slugs.remove(&name); self.persist_titles(); self.persist_order(); self.selected = None; @@ -463,6 +497,9 @@ impl App { if let Some(words) = self.session_start_counts.remove(&old_name) { self.session_start_counts.insert(new_name.clone(), words); } + if let Some(slug) = self.slugs.remove(&old_name) { + self.slugs.insert(new_name.clone(), slug); + } self.persist_order(); self.status = format!("Renamed to {new_name}"); } @@ -1288,6 +1325,15 @@ impl App { for idx in 0..self.files.len() { let name = self.files[idx].clone(); let selected = self.selected == Some(idx); + // The selected file's slug is read live from the + // buffer (so unsaved edits show); others come from the + // cache filled on open/save. + let slug = if selected { + crate::preprocess::parse(&self.buffer, &self.config.draft_marker) + .slug + } else { + self.slugs.get(&name).cloned() + }; let row = ui .horizontal(|ui| { ui.dnd_drag_source( @@ -1299,13 +1345,14 @@ impl App { ); }, ); - if ui - .add_sized( - [ui.available_width(), 20.0], - egui::SelectableLabel::new(selected, &name), - ) - .clicked() - { + let mut label = ui.add_sized( + [ui.available_width(), 20.0], + egui::SelectableLabel::new(selected, &name), + ); + if let Some(slug) = &slug { + label = label.on_hover_text(slug); + } + if label.clicked() { clicked = Some(idx); } })