Compare commits

..

No commits in common. "main" and "v0.5.2" have entirely different histories.
main ... v0.5.2

13 changed files with 674 additions and 704 deletions

1094
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,7 @@ members = [
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
version = "0.5.3" version = "0.5.1"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Breadway"] authors = ["Breadway"]
@ -24,10 +24,9 @@ chrono = { version = "0.4", features = ["serde"] }
rrule = "0.12" rrule = "0.12"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
zbus = { version = "4", default-features = false, features = ["tokio"] } zbus = { version = "4", default-features = false, features = ["tokio"] }
# WHY: bread-onnx's Provider enum references every EP type, so those ort ort = { version = "2.0.0-rc.12", default-features = false, features = ["std", "ndarray", "tracing", "api-24", "rocm", "load-dynamic"] }
# features must be on in the consumer even if breadpad only requests MIGraphX. ndarray = "0.16"
ort = { version = "2.0.0-rc.12", default-features = false, features = ["std", "tracing", "api-24", "migraphx", "cuda", "openvino", "vitis", "load-dynamic"] } tokenizers = { version = "0.21", default-features = false, features = ["http", "fancy-regex"] }
tokenizers = { version = "0.23", default-features = false, features = ["http", "fancy-regex"] }
gtk4 = { version = "0.11", features = ["v4_12"] } gtk4 = { version = "0.11", features = ["v4_12"] }
gtk4-layer-shell = "0.8" gtk4-layer-shell = "0.8"
hyprland = "0.4.0-beta.3" hyprland = "0.4.0-beta.3"

View file

@ -15,8 +15,8 @@ breadpad-shared = { path = "../breadpad-shared" }
bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" }
# Shared `--screenshot` pair validation + settle delay (`screenshot_cli`). # Shared `--screenshot` pair validation + settle delay (`screenshot_cli`).
bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" }
# `adw` implies `gtk` (`chip`, `set_chip_active`, `adw::init`). # `adw` implies `gtk`. Local chip/init shims remain in src/theme_widgets.rs.
bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.4", features = ["adw"] } bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["adw"] }
libadwaita = { version = "0.9", features = ["v1_7"] } libadwaita = { version = "0.9", features = ["v1_7"] }
anyhow.workspace = true anyhow.workspace = true
tracing.workspace = true tracing.workspace = true

View file

@ -62,16 +62,16 @@ pub fn open_editor(
let type_row = libadwaita::ActionRow::builder().title("Type").build(); let type_row = libadwaita::ActionRow::builder().title("Type").build();
let type_pill_box = gtk4::Box::builder().orientation(gtk4::Orientation::Horizontal).spacing(4).valign(gtk4::Align::Center).build(); let type_pill_box = gtk4::Box::builder().orientation(gtk4::Orientation::Horizontal).spacing(4).valign(gtk4::Align::Center).build();
let selected_type: Rc<RefCell<String>> = Rc::new(RefCell::new(note.note_type.as_str().to_string())); let selected_type: Rc<RefCell<String>> = Rc::new(RefCell::new(note.note_type.as_str().to_string()));
let type_pills: Vec<(gtk4::Button, &'static str)> = NoteType::all_builtin().iter().map(|&name| (bread_theme::gtk::chip(name), name)).collect(); let type_pills: Vec<(gtk4::Button, &'static str)> = NoteType::all_builtin().iter().map(|&name| (crate::theme_widgets::chip(name), name)).collect();
for (btn, name) in &type_pills { for (btn, name) in &type_pills {
bread_theme::gtk::set_chip_active(btn, *name == selected_type.borrow().as_str()); crate::theme_widgets::set_chip_active(btn, *name == selected_type.borrow().as_str());
let sel = selected_type.clone(); let sel = selected_type.clone();
let name = *name; let name = *name;
let all_btns: Vec<gtk4::Button> = type_pills.iter().map(|(b, _)| b.clone()).collect(); let all_btns: Vec<gtk4::Button> = type_pills.iter().map(|(b, _)| b.clone()).collect();
btn.connect_clicked(move |clicked| { btn.connect_clicked(move |clicked| {
*sel.borrow_mut() = name.to_string(); *sel.borrow_mut() = name.to_string();
for b in &all_btns { bread_theme::gtk::set_chip_active(b, false); } for b in &all_btns { crate::theme_widgets::set_chip_active(b, false); }
bread_theme::gtk::set_chip_active(clicked, true); crate::theme_widgets::set_chip_active(clicked, true);
}); });
type_pill_box.append(btn); type_pill_box.append(btn);
} }

View file

@ -14,6 +14,7 @@ use std::sync::Arc;
mod editor; mod editor;
mod screenshot; mod screenshot;
mod theme_widgets;
mod views; mod views;
// ── Args ───────────────────────────────────────────────────────────────────── // ── Args ─────────────────────────────────────────────────────────────────────
@ -362,7 +363,7 @@ fn build_app_window(
// Needed once before constructing any adw:: widget (see views::settings) — // Needed once before constructing any adw:: widget (see views::settings) —
// also forces dark mode, since bread-theme's palette is a fixed dark base // also forces dark mode, since bread-theme's palette is a fixed dark base
// regardless of the system GTK preference. // regardless of the system GTK preference.
bread_theme::adw::init(); theme_widgets::init_adw();
let store = Arc::new(Store::new()?); let store = Arc::new(Store::new()?);
let notes = store.load_all()?; let notes = store.load_all()?;
@ -373,7 +374,6 @@ fn build_app_window(
.default_width(960) .default_width(960)
.default_height(640) .default_height(640)
.build(); .build();
bread_theme::gtk::bind_window_auto(&window);
let hbox = gtk4::Box::builder() let hbox = gtk4::Box::builder()
.orientation(gtk4::Orientation::Horizontal) .orientation(gtk4::Orientation::Horizontal)
@ -616,7 +616,6 @@ fn show_add_note_window(parent: &gtk4::ApplicationWindow, state: AppState, prese
.modal(true) .modal(true)
.default_width(500) .default_width(500)
.build(); .build();
bread_theme::gtk::bind_window_auto(&win);
let vbox = gtk4::Box::builder() let vbox = gtk4::Box::builder()
.orientation(gtk4::Orientation::Vertical) .orientation(gtk4::Orientation::Vertical)
@ -644,17 +643,17 @@ fn show_add_note_window(parent: &gtk4::ApplicationWindow, state: AppState, prese
let selected_type: Rc<RefCell<NoteType>> = Rc::new(RefCell::new(preselect.clone())); let selected_type: Rc<RefCell<NoteType>> = Rc::new(RefCell::new(preselect.clone()));
let chips: Vec<(gtk4::Button, NoteType)> = NoteType::all_builtin() let chips: Vec<(gtk4::Button, NoteType)> = NoteType::all_builtin()
.iter() .iter()
.map(|&name| (bread_theme::gtk::chip(name), NoteType::from_str(name))) .map(|&name| (theme_widgets::chip(name), NoteType::from_str(name)))
.collect(); .collect();
for (btn, nt) in &chips { for (btn, nt) in &chips {
bread_theme::gtk::set_chip_active(btn, *nt == preselect); theme_widgets::set_chip_active(btn, *nt == preselect);
let sel = selected_type.clone(); let sel = selected_type.clone();
let nt_c = nt.clone(); let nt_c = nt.clone();
let all_btns: Vec<gtk4::Button> = chips.iter().map(|(b, _)| b.clone()).collect(); let all_btns: Vec<gtk4::Button> = chips.iter().map(|(b, _)| b.clone()).collect();
btn.connect_clicked(move |clicked| { btn.connect_clicked(move |clicked| {
*sel.borrow_mut() = nt_c.clone(); *sel.borrow_mut() = nt_c.clone();
for b in &all_btns { bread_theme::gtk::set_chip_active(b, false); } for b in &all_btns { theme_widgets::set_chip_active(b, false); }
bread_theme::gtk::set_chip_active(clicked, true); theme_widgets::set_chip_active(clicked, true);
}); });
chip_box.append(btn); chip_box.append(btn);
} }

View file

@ -0,0 +1,23 @@
//! Local stand-ins for `bread_theme::gtk::{chip, set_chip_active}` and
//! `bread_theme::adw::init`, which are not on bread-theme v0.7.1.
use gtk4::prelude::*;
pub fn chip(label: &str) -> gtk4::Button {
gtk4::Button::builder().label(label).css_classes(["chip"]).build()
}
pub fn set_chip_active(chip: &impl IsA<gtk4::Widget>, active: bool) {
if active {
chip.add_css_class("active");
} else {
chip.remove_css_class("active");
}
}
/// Initializes libadwaita and forces dark mode (bread-theme's palette is a
/// fixed dark base regardless of the system GTK preference).
pub fn init_adw() {
libadwaita::init().expect("failed to initialize libadwaita");
libadwaita::StyleManager::default().set_color_scheme(libadwaita::ColorScheme::ForceDark);
}

View file

@ -92,10 +92,10 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled
let selected_type: Rc<RefCell<String>> = Rc::new(RefCell::new(cfg.settings.default_type.clone())); let selected_type: Rc<RefCell<String>> = Rc::new(RefCell::new(cfg.settings.default_type.clone()));
let type_pills: Vec<(gtk4::Button, &'static str)> = NoteType::all_builtin() let type_pills: Vec<(gtk4::Button, &'static str)> = NoteType::all_builtin()
.iter() .iter()
.map(|&name| (bread_theme::gtk::chip(name), name)) .map(|&name| (crate::theme_widgets::chip(name), name))
.collect(); .collect();
for (btn, name) in &type_pills { for (btn, name) in &type_pills {
bread_theme::gtk::set_chip_active(btn, *name == selected_type.borrow().as_str()); crate::theme_widgets::set_chip_active(btn, *name == selected_type.borrow().as_str());
type_pill_box.append(btn); type_pill_box.append(btn);
} }
general_list.append(&field_row("Default type", None, &type_pill_box)); general_list.append(&field_row("Default type", None, &type_pill_box));
@ -260,8 +260,8 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled
let all_btns: Vec<gtk4::Button> = type_pills.iter().map(|(b, _)| b.clone()).collect(); let all_btns: Vec<gtk4::Button> = type_pills.iter().map(|(b, _)| b.clone()).collect();
btn.connect_clicked(move |clicked| { btn.connect_clicked(move |clicked| {
*sel.borrow_mut() = name.to_string(); *sel.borrow_mut() = name.to_string();
for b in &all_btns { bread_theme::gtk::set_chip_active(b, false); } for b in &all_btns { crate::theme_widgets::set_chip_active(b, false); }
bread_theme::gtk::set_chip_active(clicked, true); crate::theme_widgets::set_chip_active(clicked, true);
apply_now(); apply_now();
}); });
} }

View file

@ -7,8 +7,7 @@ authors.workspace = true
[dependencies] [dependencies]
bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.4", features = ["gtk"] } bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["gtk"] }
gtk4.workspace = true
anyhow.workspace = true anyhow.workspace = true
tracing.workspace = true tracing.workspace = true
serde.workspace = true serde.workspace = true
@ -20,7 +19,7 @@ tokio.workspace = true
zbus.workspace = true zbus.workspace = true
ort.workspace = true ort.workspace = true
tokenizers.workspace = true tokenizers.workspace = true
bread-onnx = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } ndarray.workspace = true
toml.workspace = true toml.workspace = true
dirs.workspace = true dirs.workspace = true
regex.workspace = true regex.workspace = true

View file

@ -2,8 +2,6 @@ use crate::ai::OllamaClient;
use crate::config::OllamaConfig; use crate::config::OllamaConfig;
use crate::parser::parse_rule_based; use crate::parser::parse_rule_based;
use crate::types::{ClassificationResult, NoteType}; use crate::types::{ClassificationResult, NoteType};
use bread_onnx::{build_session, Provider};
use ort::session::builder::GraphOptimizationLevel;
use std::path::PathBuf; use std::path::PathBuf;
/// Minimum Tier 1 confidence needed to skip Tier 2 entirely. /// Minimum Tier 1 confidence needed to skip Tier 2 entirely.
@ -18,7 +16,7 @@ pub enum ExecutionProvider {
impl ExecutionProvider { impl ExecutionProvider {
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
match self { match self {
ExecutionProvider::Gpu => "MIGraphX (iGPU)", ExecutionProvider::Gpu => "ROCm (iGPU)",
ExecutionProvider::Cpu => "CPU", ExecutionProvider::Cpu => "CPU",
} }
} }
@ -109,7 +107,9 @@ impl Classifier {
// ── Tier 2 ─────────────────────────────────────────────────────────── // ── Tier 2 ───────────────────────────────────────────────────────────
// ONNX model classifies the type only; Tier 1's time/rrule/body are kept. // ONNX model classifies the type only; Tier 1's time/rrule/body are kept.
let tier2 = if let (Some(session), Some(tokenizer)) = (&mut self.session, &self.tokenizer) { let tier2 = if let (Some(session), Some(tokenizer)) =
(&mut self.session, &self.tokenizer)
{
match run_onnx(session, tokenizer, text) { match run_onnx(session, tokenizer, text) {
Ok(r) => { Ok(r) => {
tracing::debug!("Tier 2: {:?} conf={:.2}", r.note_type, r.confidence); tracing::debug!("Tier 2: {:?} conf={:.2}", r.note_type, r.confidence);
@ -163,18 +163,9 @@ impl Classifier {
// entailment score across all five passes. // entailment score across all five passes.
const HYPOTHESES: [(&str, &str); 5] = [ const HYPOTHESES: [(&str, &str); 5] = [
("This note is a task or action item to complete.", "todo"), ("This note is a task or action item to complete.", "todo"),
( ("This note is a reminder with a specific time or deadline.", "reminder"),
"This note is a reminder with a specific time or deadline.", ("This note is an idea, suggestion, or creative thought.", "idea"),
"reminder", ("This note is a general observation or piece of information.", "note"),
),
(
"This note is an idea, suggestion, or creative thought.",
"idea",
),
(
"This note is a general observation or piece of information.",
"note",
),
("This note is a question that needs an answer.", "question"), ("This note is a question that needs an answer.", "question"),
]; ];
@ -193,17 +184,15 @@ fn run_onnx(
.map_err(|e| anyhow::anyhow!("tokenize: {}", e))?; .map_err(|e| anyhow::anyhow!("tokenize: {}", e))?;
let ids: Vec<i64> = encoding.get_ids().iter().map(|&x| x as i64).collect(); let ids: Vec<i64> = encoding.get_ids().iter().map(|&x| x as i64).collect();
let mask: Vec<i64> = encoding let mask: Vec<i64> = encoding.get_attention_mask().iter().map(|&x| x as i64).collect();
.get_attention_mask()
.iter()
.map(|&x| x as i64)
.collect();
let len = ids.len(); let len = ids.len();
let ids_tensor = ort::value::Tensor::<i64>::from_array((vec![1i64, len as i64], ids)) let ids_tensor = ort::value::Tensor::<i64>::from_array(
.map_err(|e| anyhow::anyhow!("ids tensor: {}", e))?; (vec![1i64, len as i64], ids)
let mask_tensor = ort::value::Tensor::<i64>::from_array((vec![1i64, len as i64], mask)) ).map_err(|e| anyhow::anyhow!("ids tensor: {}", e))?;
.map_err(|e| anyhow::anyhow!("mask tensor: {}", e))?; let mask_tensor = ort::value::Tensor::<i64>::from_array(
(vec![1i64, len as i64], mask)
).map_err(|e| anyhow::anyhow!("mask tensor: {}", e))?;
let inputs = ort::inputs![ let inputs = ort::inputs![
"input_ids" => ids_tensor, "input_ids" => ids_tensor,
@ -218,7 +207,10 @@ fn run_onnx(
.map_err(|e| anyhow::anyhow!("extract logits: {}", e))?; .map_err(|e| anyhow::anyhow!("extract logits: {}", e))?;
let (_, logits_slice) = logits; let (_, logits_slice) = logits;
entailment_scores[i] = logits_slice.get(ENTAILMENT_IDX).copied().unwrap_or(0.0); entailment_scores[i] = logits_slice
.get(ENTAILMENT_IDX)
.copied()
.unwrap_or(0.0);
} }
let best_idx = entailment_scores let best_idx = entailment_scores
@ -251,21 +243,24 @@ fn softmax_single(logits: &[f32], idx: usize) -> f32 {
exps[idx] / sum exps[idx] / sum
} }
fn try_load_session(path: &std::path::Path) -> (Option<ort::session::Session>, ExecutionProvider) { fn try_load_session(
// WHY: distro onnxruntime-rocm is MIGraphX, not classic ROCm; bread-onnx path: &std::path::Path,
// appends CPU so a missing GPU EP does not disable Tier 2. ) -> (Option<ort::session::Session>, ExecutionProvider) {
match build_session( // Try ROCm (iGPU) first, fall back to CPU.
path, let rocm_available = {
GraphOptimizationLevel::Level3, use ort::execution_providers::ExecutionProvider as _;
&[Provider::MiGraphX { device_id: 0 }], ort::ep::ROCm::default().is_available().unwrap_or(false)
) { };
if rocm_available {
match build_onnx_session(path, ort::ep::ROCm::default().build()) {
Ok(s) => { Ok(s) => {
tracing::info!("ONNX session loaded (MIGraphX, CPU fallback)"); tracing::info!("ONNX session loaded (ROCm iGPU)");
(Some(s), ExecutionProvider::Gpu) return (Some(s), ExecutionProvider::Gpu);
} }
Err(e) => { Err(e) => tracing::debug!("ROCm EP unavailable: {}; trying CPU", e),
tracing::debug!("MIGraphX session failed: {}; trying CPU", e); }
match build_session(path, GraphOptimizationLevel::Level3, &[Provider::Cpu]) { }
match build_onnx_session(path, ort::ep::CPU::default().build()) {
Ok(s) => { Ok(s) => {
tracing::info!("ONNX session loaded (CPU)"); tracing::info!("ONNX session loaded (CPU)");
(Some(s), ExecutionProvider::Cpu) (Some(s), ExecutionProvider::Cpu)
@ -275,6 +270,15 @@ fn try_load_session(path: &std::path::Path) -> (Option<ort::session::Session>, E
(None, ExecutionProvider::Cpu) (None, ExecutionProvider::Cpu)
} }
} }
} }
}
fn build_onnx_session(
path: &std::path::Path,
ep: ort::ep::ExecutionProviderDispatch,
) -> anyhow::Result<ort::session::Session> {
let mut builder = ort::session::Session::builder()
.map_err(|e| anyhow::anyhow!("builder: {}", e))?
.with_execution_providers([ep])
.map_err(|e| anyhow::anyhow!("ep: {}", e))?;
builder.commit_from_file(path).map_err(|e| anyhow::anyhow!("load: {}", e))
} }

View file

@ -13,11 +13,6 @@ pub fn apply_live() {
}); });
} }
/// Bind a window to the palette of the monitor it is rendered on.
pub fn bind_window(window: &impl gtk4::prelude::IsA<gtk4::Native>) {
bread_theme::gtk::bind_window_auto(window);
}
/// Generate the full breadpad/breadman CSS string. The base — `@define-color` /// Generate the full breadpad/breadman CSS string. The base — `@define-color`
/// palette, fonts, and generic widget styling — comes from the shared /// palette, fonts, and generic widget styling — comes from the shared
/// `bread_theme::stylesheet`, so breadpad and breadman look identical to the /// `bread_theme::stylesheet`, so breadpad and breadman look identical to the

View file

@ -1,24 +1,17 @@
use breadpad_shared::classifier::{Classifier, ExecutionProvider}; use breadpad_shared::classifier::{Classifier, ExecutionProvider};
use breadpad_shared::types::NoteType; use breadpad_shared::types::NoteType;
use chrono::Timelike; use chrono::Timelike;
use std::path::PathBuf;
/// Rule-based path only — a present `~/.local/share/breadpad/model` must not
/// change these assertions.
fn cl() -> Classifier { fn cl() -> Classifier {
Classifier::load_with_paths( Classifier::load("08:00")
"08:00",
PathBuf::from("/nonexistent/classifier.onnx"),
PathBuf::from("/nonexistent/tokenizer.json"),
)
} }
#[test] #[test]
fn active_provider_is_valid() { fn active_provider_is_valid() {
// The active provider depends on the host: a machine with the ONNX model present and // The active provider depends on the host: a machine with the ONNX model present and
// a working MIGraphX iGPU loads `Gpu`, otherwise `Cpu`. Either is valid — but when no // a working ROCm iGPU loads `Gpu`, otherwise `Cpu`. Either is valid — but when no
// model is available we must be on CPU (no session => no GPU EP in use). // model is available we must be on CPU (no session => no GPU EP in use).
let c = Classifier::load("08:00"); let c = cl();
assert!(matches!( assert!(matches!(
c.active_provider, c.active_provider,
ExecutionProvider::Cpu | ExecutionProvider::Gpu ExecutionProvider::Cpu | ExecutionProvider::Gpu
@ -56,28 +49,19 @@ fn classify_reminder_via_fallback() {
#[test] #[test]
fn classify_idea_via_fallback() { fn classify_idea_via_fallback() {
let mut c = cl(); let mut c = cl();
assert_eq!( assert_eq!(c.classify("what if we added a calendar view").note_type, NoteType::Idea);
c.classify("what if we added a calendar view").note_type,
NoteType::Idea
);
} }
#[test] #[test]
fn classify_question_via_fallback() { fn classify_question_via_fallback() {
let mut c = cl(); let mut c = cl();
assert_eq!( assert_eq!(c.classify("why does this fail?").note_type, NoteType::Question);
c.classify("why does this fail?").note_type,
NoteType::Question
);
} }
#[test] #[test]
fn classify_note_via_fallback() { fn classify_note_via_fallback() {
let mut c = cl(); let mut c = cl();
assert_eq!( assert_eq!(c.classify("meeting went well today").note_type, NoteType::Note);
c.classify("meeting went well today").note_type,
NoteType::Note
);
} }
#[test] #[test]
@ -90,11 +74,7 @@ fn classify_recurrence_via_fallback() {
#[test] #[test]
fn classify_custom_morning_time() { fn classify_custom_morning_time() {
let mut c = Classifier::load_with_paths( let mut c = Classifier::load("07:15");
"07:15",
PathBuf::from("/nonexistent/classifier.onnx"),
PathBuf::from("/nonexistent/tokenizer.json"),
);
let r = c.classify("sync tomorrow morning"); let r = c.classify("sync tomorrow morning");
let t = r.time.expect("should have a time for tomorrow morning"); let t = r.time.expect("should have a time for tomorrow morning");
let local: chrono::DateTime<chrono::Local> = t.into(); let local: chrono::DateTime<chrono::Local> = t.into();
@ -134,16 +114,12 @@ fn classify_returns_cleaned_body() {
let mut c = cl(); let mut c = cl();
let r = c.classify("call mum at 6pm"); let r = c.classify("call mum at 6pm");
assert!(r.body.contains("call mum"), "body: {}", r.body); assert!(r.body.contains("call mum"), "body: {}", r.body);
assert!( assert!(!r.body.contains("6pm"), "time phrase should be stripped from body: {}", r.body);
!r.body.contains("6pm"),
"time phrase should be stripped from body: {}",
r.body
);
} }
#[test] #[test]
fn model_path_points_to_expected_location() { fn model_path_points_to_expected_location() {
let c = Classifier::load("08:00"); let c = cl();
assert!( assert!(
c.model_path.to_str().unwrap().contains("breadpad"), c.model_path.to_str().unwrap().contains("breadpad"),
"model path: {:?}", "model path: {:?}",

View file

@ -9,18 +9,12 @@ use breadpad_shared::classifier::Classifier;
use breadpad_shared::store::Store; use breadpad_shared::store::Store;
use breadpad_shared::types::{Note, NoteType}; use breadpad_shared::types::{Note, NoteType};
use chrono::Timelike; use chrono::Timelike;
use std::path::PathBuf;
use tempfile::TempDir; use tempfile::TempDir;
// Mirrors commit_note() in breadpad/src/main.rs. // Mirrors commit_note() in breadpad/src/main.rs.
// `user_type` is the type the user selected in the chip row (default = NoteType::Note). // `user_type` is the type the user selected in the chip row (default = NoteType::Note).
fn capture(store: &Store, text: &str, user_type: NoteType) -> Note { fn capture(store: &Store, text: &str, user_type: NoteType) -> Note {
// WHY: pipeline tests cover classify→save→reload, not a host ONNX model. let mut classifier = Classifier::load("08:00");
let mut classifier = Classifier::load_with_paths(
"08:00",
PathBuf::from("/nonexistent/classifier.onnx"),
PathBuf::from("/nonexistent/tokenizer.json"),
);
let result = classifier.classify(text); let result = classifier.classify(text);
let mut note = Note::new(text.into(), user_type.clone(), None); let mut note = Note::new(text.into(), user_type.clone(), None);
@ -67,11 +61,7 @@ fn todo_note_appears_in_store() {
#[test] #[test]
fn idea_note_appears_in_store() { fn idea_note_appears_in_store() {
let (dir, store) = setup(); let (dir, store) = setup();
capture( capture(&store, "what if we added dark mode", NoteType::from_str("note"));
&store,
"what if we added dark mode",
NoteType::from_str("note"),
);
let notes = breadman_store(&dir).load_all().unwrap(); let notes = breadman_store(&dir).load_all().unwrap();
assert_eq!(notes.len(), 1); assert_eq!(notes.len(), 1);
@ -81,11 +71,7 @@ fn idea_note_appears_in_store() {
#[test] #[test]
fn question_note_appears_in_store() { fn question_note_appears_in_store() {
let (dir, store) = setup(); let (dir, store) = setup();
capture( capture(&store, "why does the cache miss on cold start?", NoteType::from_str("note"));
&store,
"why does the cache miss on cold start?",
NoteType::from_str("note"),
);
let notes = breadman_store(&dir).load_all().unwrap(); let notes = breadman_store(&dir).load_all().unwrap();
assert_eq!(notes.len(), 1); assert_eq!(notes.len(), 1);
@ -111,10 +97,7 @@ fn reminder_has_time_set() {
let notes = breadman_store(&dir).load_all().unwrap(); let notes = breadman_store(&dir).load_all().unwrap();
assert_eq!(notes[0].note_type, NoteType::Reminder); assert_eq!(notes[0].note_type, NoteType::Reminder);
assert!( assert!(notes[0].time.is_some(), "reminder should have a scheduled time");
notes[0].time.is_some(),
"reminder should have a scheduled time"
);
let local: chrono::DateTime<chrono::Local> = notes[0].time.unwrap().into(); let local: chrono::DateTime<chrono::Local> = notes[0].time.unwrap().into();
assert_eq!(local.hour(), 18); assert_eq!(local.hour(), 18);
} }
@ -125,21 +108,14 @@ fn reminder_body_has_time_stripped() {
capture(&store, "call mum at 6pm", NoteType::from_str("note")); capture(&store, "call mum at 6pm", NoteType::from_str("note"));
let notes = breadman_store(&dir).load_all().unwrap(); let notes = breadman_store(&dir).load_all().unwrap();
assert!( assert!(!notes[0].body.contains("6pm"), "time phrase should be removed from body");
!notes[0].body.contains("6pm"),
"time phrase should be removed from body"
);
assert!(notes[0].body.contains("call mum")); assert!(notes[0].body.contains("call mum"));
} }
#[test] #[test]
fn in_duration_reminder_has_time() { fn in_duration_reminder_has_time() {
let (dir, store) = setup(); let (dir, store) = setup();
capture( capture(&store, "check on the build in 30 minutes", NoteType::from_str("note"));
&store,
"check on the build in 30 minutes",
NoteType::from_str("note"),
);
let notes = breadman_store(&dir).load_all().unwrap(); let notes = breadman_store(&dir).load_all().unwrap();
assert_eq!(notes[0].note_type, NoteType::Reminder); assert_eq!(notes[0].note_type, NoteType::Reminder);
@ -151,11 +127,7 @@ fn in_duration_reminder_has_time() {
#[test] #[test]
fn recurring_reminder_has_rrule() { fn recurring_reminder_has_rrule() {
let (dir, store) = setup(); let (dir, store) = setup();
capture( capture(&store, "standup every monday at 9am", NoteType::from_str("note"));
&store,
"standup every monday at 9am",
NoteType::from_str("note"),
);
let notes = breadman_store(&dir).load_all().unwrap(); let notes = breadman_store(&dir).load_all().unwrap();
assert_eq!(notes[0].note_type, NoteType::Reminder); assert_eq!(notes[0].note_type, NoteType::Reminder);
@ -167,20 +139,11 @@ fn recurring_reminder_has_rrule() {
#[test] #[test]
fn daily_reminder_has_rrule() { fn daily_reminder_has_rrule() {
let (dir, store) = setup(); let (dir, store) = setup();
capture( capture(&store, "drink water every day at 8am", NoteType::from_str("note"));
&store,
"drink water every day at 8am",
NoteType::from_str("note"),
);
let notes = breadman_store(&dir).load_all().unwrap(); let notes = breadman_store(&dir).load_all().unwrap();
assert_eq!(notes[0].note_type, NoteType::Reminder); assert_eq!(notes[0].note_type, NoteType::Reminder);
assert!(notes[0] assert!(notes[0].rrule.as_ref().unwrap().as_str().contains("FREQ=DAILY"));
.rrule
.as_ref()
.unwrap()
.as_str()
.contains("FREQ=DAILY"));
} }
// ---- user-forced type is respected ---- // ---- user-forced type is respected ----
@ -192,11 +155,7 @@ fn user_selected_type_overrides_classifier() {
capture(&store, "fix the login bug", NoteType::Idea); capture(&store, "fix the login bug", NoteType::Idea);
let notes = breadman_store(&dir).load_all().unwrap(); let notes = breadman_store(&dir).load_all().unwrap();
assert_eq!( assert_eq!(notes[0].note_type, NoteType::Idea, "user chip selection should win over classifier");
notes[0].note_type,
NoteType::Idea,
"user chip selection should win over classifier"
);
} }
#[test] #[test]
@ -214,11 +173,7 @@ fn user_selected_reminder_overrides_classifier() {
fn three_notes_all_visible_to_breadman() { fn three_notes_all_visible_to_breadman() {
let (dir, store) = setup(); let (dir, store) = setup();
capture(&store, "buy milk", NoteType::from_str("note")); capture(&store, "buy milk", NoteType::from_str("note"));
capture( capture(&store, "what if we rewrote in Zig", NoteType::from_str("note"));
&store,
"what if we rewrote in Zig",
NoteType::from_str("note"),
);
capture(&store, "team standup went well", NoteType::from_str("note")); capture(&store, "team standup went well", NoteType::from_str("note"));
let notes = breadman_store(&dir).load_all().unwrap(); let notes = breadman_store(&dir).load_all().unwrap();

View file

@ -451,7 +451,6 @@ fn build_reminder_window(
window.set_layer(Layer::Overlay); window.set_layer(Layer::Overlay);
window.set_keyboard_mode(KeyboardMode::Exclusive); window.set_keyboard_mode(KeyboardMode::Exclusive);
window.auto_exclusive_zone_enable(); window.auto_exclusive_zone_enable();
breadpad_shared::theme::bind_window(&window);
apply_css(&cfg); apply_css(&cfg);
@ -754,7 +753,6 @@ fn build_window(
window.set_anchor(Edge::Bottom, false); window.set_anchor(Edge::Bottom, false);
window.set_anchor(Edge::Left, false); window.set_anchor(Edge::Left, false);
window.set_anchor(Edge::Right, false); window.set_anchor(Edge::Right, false);
breadpad_shared::theme::bind_window(&window);
apply_css(&cfg); apply_css(&cfg);