diff --git a/breadman/src/main.rs b/breadman/src/main.rs index 7e07a4e..48430d7 100644 --- a/breadman/src/main.rs +++ b/breadman/src/main.rs @@ -108,13 +108,15 @@ mod args { // ── AppState ────────────────────────────────────────────────────────────────── +type ErrorLog = Rc, String)>>>; + /// Shared UI state, cheap to clone (all fields are Rc/Arc). #[derive(Clone)] struct AppState { store: Arc, notes: Rc>>, cfg: Rc>, - errors: Rc, String)>>>, + errors: ErrorLog, active_view: Rc>, stack: gtk4::Stack, window: gtk4::ApplicationWindow, @@ -579,7 +581,7 @@ fn build_note_list( .build(); let mut sorted: Vec = notes.iter().filter(|n| !n.done).cloned().collect(); - sorted.sort_by(|a, b| b.created.cmp(&a.created)); + sorted.sort_by_key(|n| std::cmp::Reverse(n.created)); if sorted.is_empty() { let action = empty_new_type.map(|nt| views::row::new_note_action(nt, state.window.clone(), state.clone())); diff --git a/breadman/src/views/errors.rs b/breadman/src/views/errors.rs index 1be64e0..cedb9ff 100644 --- a/breadman/src/views/errors.rs +++ b/breadman/src/views/errors.rs @@ -35,7 +35,7 @@ pub fn build(entries: &[(DateTime, String)]) -> gtk4::ScrolledWin .build(); let time_label = gtk4::Label::builder() - .label(&ts.format("%H:%M:%S").to_string()) + .label(ts.format("%H:%M:%S").to_string()) .width_chars(10) .xalign(0.0) .css_classes(["dim-label"]) diff --git a/breadpad-shared/src/ai.rs b/breadpad-shared/src/ai.rs index 7bb69ad..24f2bd4 100644 --- a/breadpad-shared/src/ai.rs +++ b/breadpad-shared/src/ai.rs @@ -73,7 +73,7 @@ impl OllamaClient { let classification: OllamaClassification = extract_json(&ollama_resp.response) .ok_or_else(|| anyhow::anyhow!( "no JSON object found in response — raw: {:?}", - &ollama_resp.response + ollama_resp.response ))?; let note_type = classification diff --git a/breadpad-shared/src/config.rs b/breadpad-shared/src/config.rs index 9d81e5e..e0f9bcf 100644 --- a/breadpad-shared/src/config.rs +++ b/breadpad-shared/src/config.rs @@ -135,7 +135,7 @@ impl Default for RemindersConfig { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct CalendarConfig { #[serde(default = "default_calendar_enabled")] pub enabled: bool, @@ -150,17 +150,6 @@ pub struct CalendarConfig { pub password: String, } -impl Default for CalendarConfig { - fn default() -> Self { - CalendarConfig { - enabled: false, - url: String::new(), - username: String::new(), - password: String::new(), - } - } -} - #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Config { #[serde(default)] diff --git a/breadpad-shared/src/parser.rs b/breadpad-shared/src/parser.rs index cc03b5e..27f63b9 100644 --- a/breadpad-shared/src/parser.rs +++ b/breadpad-shared/src/parser.rs @@ -324,6 +324,67 @@ pub fn parse_rule_based(text: &str, default_morning: &str) -> ClassificationResu } } +fn infer_type(text: &str, has_time: bool, has_rrule: bool) -> NoteType { + let lower = text.to_lowercase(); + if has_rrule || has_time { + return NoteType::Reminder; + } + if lower.contains("buy ") + || lower.contains("pick up") + || lower.contains("clean ") + || lower.starts_with("call ") + || lower.starts_with("email ") + || lower.starts_with("fix ") + || lower.starts_with("check ") + || lower.starts_with("finish ") + || lower.starts_with("write ") + || lower.starts_with("update ") + || lower.starts_with("prepare ") + || lower.starts_with("schedule ") + || lower.starts_with("organize ") + || lower.starts_with("deploy ") + || lower.starts_with("install ") + || lower.starts_with("send ") + || lower.starts_with("submit ") + || lower.starts_with("create ") + || lower.starts_with("setup ") + || lower.starts_with("restore ") + || lower.starts_with("archive ") + || lower.starts_with("export ") + || lower.starts_with("import ") + || lower.starts_with("approve ") + || lower.starts_with("configure ") + || lower.starts_with("refactor ") + || lower.starts_with("review ") + { + return NoteType::Todo; + } + if lower.starts_with("what if ") + || lower.starts_with("idea:") + || lower.contains("could ") + || lower.contains("maybe ") + || lower.starts_with("should we ") + { + return NoteType::Idea; + } + if lower.starts_with("why ") + || lower.starts_with("how ") + || (lower.starts_with("what ") && !lower.starts_with("what if ")) + || lower.starts_with("when ") + || lower.starts_with("where ") + || lower.starts_with("who ") + || lower.starts_with("will ") + || lower.starts_with("is ") + || lower.starts_with("are ") + || lower.starts_with("did ") + || lower.starts_with("does ") + || lower.ends_with('?') + { + return NoteType::Question; + } + NoteType::Note +} + #[cfg(test)] mod tests { use super::*; @@ -490,7 +551,7 @@ mod tests { let r = p("take a break in 30 minutes"); let t = r.time.unwrap(); let delta = (t - before).num_seconds(); - assert!(delta >= 29 * 60 && delta <= 31 * 60, "delta was {}s", delta); + assert!((29 * 60..=31 * 60).contains(&delta), "delta was {}s", delta); } #[test] @@ -498,7 +559,7 @@ mod tests { let before = Utc::now(); let r = p("ping in 1 minute"); let delta = (r.time.unwrap() - before).num_seconds(); - assert!(delta >= 55 && delta <= 65, "delta was {}s", delta); + assert!((55..=65).contains(&delta), "delta was {}s", delta); } #[test] @@ -506,7 +567,7 @@ mod tests { let before = Utc::now(); let r = p("review PR in 2 hours"); let delta_min = (r.time.unwrap() - before).num_minutes(); - assert!(delta_min >= 119 && delta_min <= 121, "delta was {}min", delta_min); + assert!((119..=121).contains(&delta_min), "delta was {}min", delta_min); } #[test] @@ -514,7 +575,7 @@ mod tests { let before = Utc::now(); let r = p("follow up in 3 days"); let delta_h = (r.time.unwrap() - before).num_hours(); - assert!(delta_h >= 71 && delta_h <= 73, "delta was {}h", delta_h); + assert!((71..=73).contains(&delta_h), "delta was {}h", delta_h); } // ---- Time extraction: tomorrow ---- @@ -717,7 +778,7 @@ mod tests { let before = Utc::now(); let r = p("check on the server in an hour"); let delta_min = (r.time.unwrap() - before).num_minutes(); - assert!(delta_min >= 59 && delta_min <= 61, "delta was {}min", delta_min); + assert!((59..=61).contains(&delta_min), "delta was {}min", delta_min); } #[test] @@ -737,7 +798,7 @@ mod tests { let before = Utc::now(); let r = p("in a couple of hours remind me to check the oven"); let delta_min = (r.time.unwrap() - before).num_minutes(); - assert!(delta_min >= 119 && delta_min <= 121, "delta was {}min", delta_min); + assert!((119..=121).contains(&delta_min), "delta was {}min", delta_min); } #[test] @@ -758,7 +819,7 @@ mod tests { let before = Utc::now(); let r = p("in half an hour submit the report"); let delta_min = (r.time.unwrap() - before).num_minutes(); - assert!(delta_min >= 29 && delta_min <= 31, "delta was {}min", delta_min); + assert!((29..=31).contains(&delta_min), "delta was {}min", delta_min); } // ---- Tonight / this evening ---- @@ -847,64 +908,3 @@ mod tests { assert!(rule.as_str().contains("BYHOUR=16"), "rule: {}", rule.as_str()); } } - -fn infer_type(text: &str, has_time: bool, has_rrule: bool) -> NoteType { - let lower = text.to_lowercase(); - if has_rrule || has_time { - return NoteType::Reminder; - } - if lower.contains("buy ") - || lower.contains("pick up") - || lower.contains("clean ") - || lower.starts_with("call ") - || lower.starts_with("email ") - || lower.starts_with("fix ") - || lower.starts_with("check ") - || lower.starts_with("finish ") - || lower.starts_with("write ") - || lower.starts_with("update ") - || lower.starts_with("prepare ") - || lower.starts_with("schedule ") - || lower.starts_with("organize ") - || lower.starts_with("deploy ") - || lower.starts_with("install ") - || lower.starts_with("send ") - || lower.starts_with("submit ") - || lower.starts_with("create ") - || lower.starts_with("setup ") - || lower.starts_with("restore ") - || lower.starts_with("archive ") - || lower.starts_with("export ") - || lower.starts_with("import ") - || lower.starts_with("approve ") - || lower.starts_with("configure ") - || lower.starts_with("refactor ") - || lower.starts_with("review ") - { - return NoteType::Todo; - } - if lower.starts_with("what if ") - || lower.starts_with("idea:") - || lower.contains("could ") - || lower.contains("maybe ") - || lower.starts_with("should we ") - { - return NoteType::Idea; - } - if lower.starts_with("why ") - || lower.starts_with("how ") - || (lower.starts_with("what ") && !lower.starts_with("what if ")) - || lower.starts_with("when ") - || lower.starts_with("where ") - || lower.starts_with("who ") - || lower.starts_with("will ") - || lower.starts_with("is ") - || lower.starts_with("are ") - || lower.starts_with("did ") - || lower.starts_with("does ") - || lower.ends_with('?') - { - return NoteType::Question; - } - NoteType::Note -} diff --git a/breadpad-shared/src/scheduler.rs b/breadpad-shared/src/scheduler.rs index ff5ff26..580e247 100644 --- a/breadpad-shared/src/scheduler.rs +++ b/breadpad-shared/src/scheduler.rs @@ -176,7 +176,7 @@ pub(crate) fn parse_next_from_rrule(rrule_str: &str, default_morning: &str) -> O } else { (now.date_naive() + chrono::Duration::days(1)).and_time(fire_time) }; - return Some(local_naive_to_utc(naive)); + Some(local_naive_to_utc(naive)) } "WEEKLY" => { use chrono::Datelike; @@ -204,7 +204,7 @@ pub(crate) fn parse_next_from_rrule(rrule_str: &str, default_morning: &str) -> O }; let target_date = (now.date_naive() + chrono::Duration::days(days_ahead)).and_time(fire_time); - return Some(local_naive_to_utc(target_date)); + Some(local_naive_to_utc(target_date)) } _ => None, } diff --git a/breadpad-shared/src/store.rs b/breadpad-shared/src/store.rs index bc8b86a..a8f52c8 100644 --- a/breadpad-shared/src/store.rs +++ b/breadpad-shared/src/store.rs @@ -118,11 +118,11 @@ impl Store { Ok(()) } - fn rewrite_notes(&self, mut f: F) -> Result<()> + fn rewrite_notes(&self, f: F) -> Result<()> where F: FnMut(Note) -> Note, { - let notes: Vec = self.load_all()?.into_iter().map(|n| f(n)).collect(); + let notes: Vec = self.load_all()?.into_iter().map(f).collect(); self.write_all(&self.notes_path, ¬es) } @@ -145,7 +145,7 @@ impl Store { let notes = self.load_all()?; let (to_archive, keep): (Vec, Vec) = notes .into_iter() - .partition(|n| n.done && n.completed.map_or(false, |c| c < cutoff)); + .partition(|n| n.done && n.completed.is_some_and(|c| c < cutoff)); if to_archive.is_empty() { return Ok(0); diff --git a/breadpad-shared/src/theme.rs b/breadpad-shared/src/theme.rs index e6fa63e..670b711 100644 --- a/breadpad-shared/src/theme.rs +++ b/breadpad-shared/src/theme.rs @@ -278,7 +278,7 @@ mod tests { #[test] fn css_defines_bg_color() { let css = build_css(&Palette::default(), None); - assert!(css.contains("@define-color bg #1e1e2e"), "css missing bg: {}", &css[..300]); + assert!(css.contains("@define-color bg #0c0c0c"), "css missing bg: {}", &css[..300]); } #[test] @@ -323,9 +323,11 @@ mod tests { #[test] fn css_reflects_custom_palette_colors() { - let mut p = Palette::default(); - p.background = "#deadbe".into(); - p.color4 = "#cafe00".into(); + let p = Palette { + background: "#deadbe".into(), + color4: "#cafe00".into(), + ..Default::default() + }; let css = build_css(&p, None); assert!(css.contains("@define-color bg #deadbe"), "css: {}", &css[..300]); assert!(css.contains("@define-color blue #cafe00"), "css: {}", &css[..300]); diff --git a/breadpad-shared/src/types.rs b/breadpad-shared/src/types.rs index 5be0b54..e1d5729 100644 --- a/breadpad-shared/src/types.rs +++ b/breadpad-shared/src/types.rs @@ -15,6 +15,9 @@ pub enum NoteType { } impl NoteType { + // Not std::str::FromStr — infallible, returns Self directly rather than + // Result, and used across 30+ call sites as NoteType::from_str(..). + #[allow(clippy::should_implement_trait)] pub fn from_str(s: &str) -> Self { match s.to_lowercase().as_str() { "todo" => NoteType::Todo, diff --git a/breadpad-shared/tests/config.rs b/breadpad-shared/tests/config.rs index 6e057e5..462afe6 100644 --- a/breadpad-shared/tests/config.rs +++ b/breadpad-shared/tests/config.rs @@ -279,15 +279,19 @@ fn resolved_ort_dylib_empty_returns_none() { #[test] fn resolved_ort_dylib_whitespace_only_returns_none() { - let mut m = ModelConfig::default(); - m.ort_dylib_path = " ".into(); + let m = ModelConfig { + ort_dylib_path: " ".into(), + ..Default::default() + }; assert!(m.resolved_ort_dylib_path().is_none()); } #[test] fn resolved_ort_dylib_set_returns_some() { - let mut m = ModelConfig::default(); - m.ort_dylib_path = "/usr/lib/libonnxruntime.so".into(); + let m = ModelConfig { + ort_dylib_path: "/usr/lib/libonnxruntime.so".into(), + ..Default::default() + }; assert_eq!( m.resolved_ort_dylib_path().unwrap().to_str().unwrap(), "/usr/lib/libonnxruntime.so" diff --git a/breadpad-test/src/main.rs b/breadpad-test/src/main.rs index 54a7d03..4c96a43 100644 --- a/breadpad-test/src/main.rs +++ b/breadpad-test/src/main.rs @@ -389,7 +389,7 @@ fn cmd_show(index: usize, corpus_path: &Path, tier: &TierArg) -> Result<()> { println!(); let sep = "─".repeat(62); - println!("{:<14} {:<26} {}", "field", "expected", "actual"); + println!("{:<14} {:<26} actual", "field", "expected"); println!("{sep}"); println!( "{:<14} {:<26} {}", diff --git a/breadpad/src/main.rs b/breadpad/src/main.rs index 4949963..b284cb8 100644 --- a/breadpad/src/main.rs +++ b/breadpad/src/main.rs @@ -468,7 +468,7 @@ fn build_reminder_window( let local: chrono::DateTime = t.into(); header.append( >k4::Label::builder() - .label(&local.format("%H:%M").to_string()) + .label(local.format("%H:%M").to_string()) .css_classes(["reminder-time"]) .build(), );