diff --git a/Cargo.lock b/Cargo.lock index d34275d..a110a12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,7 +76,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -87,7 +87,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -305,7 +305,7 @@ dependencies = [ [[package]] name = "bread-screenshots" version = "0.3.1" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#1a3475bd2358202f60e29c9bd27d06b1428b1a27" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#4e76bc707720fbfc52e19a8cc78f3fb1e6e0a713" dependencies = [ "anyhow", "bread-utils", @@ -323,10 +323,22 @@ dependencies = [ "serde_json", ] +[[package]] +name = "bread-theme" +version = "0.3.1" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#4e76bc707720fbfc52e19a8cc78f3fb1e6e0a713" +dependencies = [ + "dirs 5.0.1", + "gtk4", + "libadwaita", + "serde", + "serde_json", +] + [[package]] name = "bread-utils" version = "0.3.1" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#1a3475bd2358202f60e29c9bd27d06b1428b1a27" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#4e76bc707720fbfc52e19a8cc78f3fb1e6e0a713" dependencies = [ "dirs 5.0.1", "serde", @@ -339,11 +351,13 @@ version = "0.3.4" dependencies = [ "anyhow", "bread-screenshots", + "bread-theme 0.3.1", "breadpad-shared", "chrono", "dirs 5.0.1", "futures-channel", "gtk4", + "libadwaita", "serde", "serde_json", "tokio", @@ -376,7 +390,7 @@ name = "breadpad-shared" version = "0.3.4" dependencies = [ "anyhow", - "bread-theme", + "bread-theme 0.2.3", "chrono", "dirs 5.0.1", "ical", @@ -873,7 +887,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -948,7 +962,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1259,7 +1273,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1879,6 +1893,36 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "libadwaita" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b9900e67182a4b5b1f157b448d94f0715c8b9770cce21cf000801917f53bfa" +dependencies = [ + "gdk4", + "gio", + "glib", + "gtk4", + "libadwaita-sys", + "pango", +] + +[[package]] +name = "libadwaita-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3c27642b389852aa99341bd4a4c19ec6f8a2b63ebdd7f5ba1952198079ccd" +dependencies = [ + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk4-sys", + "libc", + "pango-sys", + "system-deps", +] + [[package]] name = "libc" version = "0.2.186" @@ -2101,7 +2145,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2723,7 +2767,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2937,7 +2981,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3047,7 +3091,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3415,7 +3459,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] diff --git a/breadman/Cargo.toml b/breadman/Cargo.toml index 3808281..9e4bd5f 100644 --- a/breadman/Cargo.toml +++ b/breadman/Cargo.toml @@ -13,6 +13,11 @@ path = "src/main.rs" breadpad-shared = { path = "../breadpad-shared" } # Capture primitives for `--screenshot` mode — see src/screenshot.rs. bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", branch = "dev" } +# libadwaita-based settings-screen components (see src/views/settings.rs) — +# on "dev", not breadpad-shared's tag-pinned copy, since the `adw` feature +# doesn't exist on a tagged release yet. +bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", branch = "dev", features = ["adw"] } +libadwaita = { version = "0.9", features = ["v1_7"] } anyhow.workspace = true tracing.workspace = true tracing-subscriber.workspace = true diff --git a/breadman/src/main.rs b/breadman/src/main.rs index a32583c..baf0611 100644 --- a/breadman/src/main.rs +++ b/breadman/src/main.rs @@ -342,6 +342,10 @@ fn build_app_window( screenshot_req: Option, ) -> Result<()> { apply_css(&cfg); + // Needed once before constructing any adw:: widget (see views::settings) — + // also forces dark mode, since bread-theme's palette is a fixed dark base + // regardless of the system GTK preference. + bread_theme::adw::init(); let store = Arc::new(Store::new()?); let notes = store.load_all()?; diff --git a/breadman/src/views/settings.rs b/breadman/src/views/settings.rs index 8cf49c8..8e224ae 100644 --- a/breadman/src/views/settings.rs +++ b/breadman/src/views/settings.rs @@ -1,7 +1,9 @@ use breadpad_shared::config::{ CalendarConfig, Config, ModelConfig, OllamaConfig, RemindersConfig, Settings, }; +use bread_theme::adw; use gtk4::prelude::*; +use libadwaita::prelude::*; pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::ScrolledWindow { let scroll = gtk4::ScrolledWindow::builder() @@ -10,139 +12,148 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled .vexpand(true) .build(); - let outer = gtk4::Box::builder() - .orientation(gtk4::Orientation::Vertical) - .spacing(16) - .margin_top(16) - .margin_bottom(16) - .margin_start(16) - .margin_end(16) - .build(); + let page = libadwaita::PreferencesPage::new(); // ── General ────────────────────────────────────────────────── - let (general_frame, general_grid) = make_section("General"); + let general_group = adw::preferences_group("General", None); let type_options = ["note", "todo", "reminder", "idea", "question"]; - let default_type_combo = gtk4::DropDown::from_strings(&type_options); + let default_type_row = libadwaita::ComboRow::builder() + .title("Default type") + .model(>k4::StringList::new(&type_options)) + .build(); let dt_idx = type_options .iter() .position(|&s| s == cfg.settings.default_type.as_str()) .unwrap_or(0) as u32; - default_type_combo.set_selected(dt_idx); - attach_row(&general_grid, 0, "Default type", &default_type_combo); + default_type_row.set_selected(dt_idx); + general_group.add(&default_type_row); - let ws_tag_switch = gtk4::Switch::builder() - .active(cfg.settings.workspace_tag) + let ws_tag_row = adw::toggle_row( + "Workspace tag", + Some("Tag new notes with the Hyprland workspace they were created on"), + cfg.settings.workspace_tag, + ); + general_group.add(&ws_tag_row); + + let archive_adj = gtk4::Adjustment::new(cfg.settings.archive_after_days as f64, 1.0, 365.0, 1.0, 7.0, 0.0); + let archive_row = adw::spin_row("Archive after (days)", None, &archive_adj); + general_group.add(&archive_row); + + let snooze_row = adw::action_row("Snooze options", Some("Comma-separated (e.g. 15m, 1h, tomorrow_morning)")); + let snooze_entry = gtk4::Entry::builder() + .text(cfg.settings.snooze_options.join(", ")) .valign(gtk4::Align::Center) .build(); - attach_row(&general_grid, 1, "Workspace tag", &ws_tag_switch); + snooze_row.add_suffix(&snooze_entry); + general_group.add(&snooze_row); - let archive_spin = gtk4::SpinButton::with_range(1.0, 365.0, 1.0); - archive_spin.set_value(cfg.settings.archive_after_days as f64); - attach_row(&general_grid, 2, "Archive after (days)", &archive_spin); - - let snooze_entry = gtk4::Entry::builder() - .text(&cfg.settings.snooze_options.join(", ")) - .hexpand(true) - .build(); - attach_row(&general_grid, 3, "Snooze options", &snooze_entry); - - outer.append(&general_frame); + page.add(&general_group); // ── Reminders ──────────────────────────────────────────────── - let (rem_frame, rem_grid) = make_section("Reminders"); + let rem_group = adw::preferences_group("Reminders", None); + let morning_row = adw::action_row("Default morning", Some("Used for \"tomorrow_morning\" snoozes and recurring reminders")); let morning_entry = gtk4::Entry::builder() .text(&cfg.reminders.default_morning) .placeholder_text("HH:MM") - .build(); - attach_row(&rem_grid, 0, "Default morning", &morning_entry); - - let grace_spin = gtk4::SpinButton::with_range(0.0, 1440.0, 5.0); - grace_spin.set_value(cfg.reminders.missed_grace_minutes as f64); - attach_row(&rem_grid, 1, "Missed grace (minutes)", &grace_spin); - - outer.append(&rem_frame); - - // ── Model ───────────────────────────────────────────────────── - let (model_frame, model_grid) = make_section("Model (Tier 2 ONNX)"); - - let model_path_entry = gtk4::Entry::builder() - .text(&cfg.model.path) - .hexpand(true) - .build(); - attach_row(&model_grid, 0, "ONNX path", &model_path_entry); - - let tokenizer_entry = gtk4::Entry::builder() - .text(&cfg.model.tokenizer) - .hexpand(true) - .build(); - attach_row(&model_grid, 1, "Tokenizer path", &tokenizer_entry); - - let ort_dylib_entry = gtk4::Entry::builder() - .text(&cfg.model.ort_dylib_path) - .hexpand(true) - .build(); - attach_row(&model_grid, 2, "ORT dylib path", &ort_dylib_entry); - - outer.append(&model_frame); - - // ── Ollama (Tier 3) ─────────────────────────────────────────── - let (ollama_frame, ollama_grid) = make_section("Ollama (Tier 3)"); - - let ollama_enabled = gtk4::Switch::builder() - .active(cfg.model.ollama.enabled) .valign(gtk4::Align::Center) .build(); - attach_row(&ollama_grid, 0, "Enabled", &ollama_enabled); + morning_row.add_suffix(&morning_entry); + rem_group.add(&morning_row); - let ollama_endpoint = gtk4::Entry::builder() - .text(&cfg.model.ollama.endpoint) - .hexpand(true) - .build(); - attach_row(&ollama_grid, 1, "Endpoint", &ollama_endpoint); + let grace_adj = gtk4::Adjustment::new(cfg.reminders.missed_grace_minutes as f64, 0.0, 1440.0, 5.0, 30.0, 0.0); + let grace_row = adw::spin_row("Missed grace (minutes)", Some("How late a reminder can fire before it's considered missed"), &grace_adj); + rem_group.add(&grace_row); - let ollama_model = gtk4::Entry::builder() - .text(&cfg.model.ollama.model) - .build(); - attach_row(&ollama_grid, 2, "Model", &ollama_model); + page.add(&rem_group); - let ollama_thresh = gtk4::SpinButton::with_range(0.0, 1.0, 0.05); - ollama_thresh.set_value(cfg.model.ollama.confidence_threshold as f64); - ollama_thresh.set_digits(2); - attach_row(&ollama_grid, 3, "Confidence threshold", &ollama_thresh); + // ── Local classifier ─────────────────────────────────────────── + let model_group = adw::preferences_group( + "Local Classifier", + Some("Optional local ONNX model for classifying note type/time without a network round-trip."), + ); - outer.append(&ollama_frame); + let model_path_row = adw::action_row("Model path", None); + let model_path_entry = gtk4::Entry::builder().text(&cfg.model.path).hexpand(true).width_chars(36).valign(gtk4::Align::Center).build(); + model_path_row.add_suffix(&model_path_entry); + model_group.add(&model_path_row); - // ── Calendar ───────────────────────────────────────────────── - let (cal_frame, cal_grid) = make_section("Nextcloud Calendar (CalDAV)"); + let tokenizer_row = adw::action_row("Tokenizer path", None); + let tokenizer_entry = gtk4::Entry::builder().text(&cfg.model.tokenizer).hexpand(true).width_chars(36).valign(gtk4::Align::Center).build(); + tokenizer_row.add_suffix(&tokenizer_entry); + model_group.add(&tokenizer_row); - let cal_enabled = gtk4::Switch::builder() - .active(cfg.calendar.enabled) - .valign(gtk4::Align::Center) - .build(); - attach_row(&cal_grid, 0, "Enabled", &cal_enabled); + let ort_dylib_row = adw::action_row("Runtime library path", None); + let ort_dylib_entry = gtk4::Entry::builder().text(&cfg.model.ort_dylib_path).hexpand(true).width_chars(36).valign(gtk4::Align::Center).build(); + ort_dylib_row.add_suffix(&ort_dylib_entry); + model_group.add(&ort_dylib_row); + page.add(&model_group); + + // ── AI classification (Ollama) ────────────────────────────────── + let ollama_group = adw::preferences_group( + "AI Classification", + Some("Uses a local Ollama model as a fallback classifier when the ONNX model is unavailable or unsure."), + ); + + let ollama_enabled_row = adw::toggle_row("Enabled", None, cfg.model.ollama.enabled); + ollama_group.add(&ollama_enabled_row); + + let ollama_endpoint_row = adw::action_row("Endpoint", None); + let ollama_endpoint_entry = gtk4::Entry::builder().text(&cfg.model.ollama.endpoint).hexpand(true).width_chars(36).valign(gtk4::Align::Center).build(); + ollama_endpoint_row.add_suffix(&ollama_endpoint_entry); + ollama_group.add(&ollama_endpoint_row); + + let ollama_model_row = adw::action_row("Model", None); + let ollama_model_entry = gtk4::Entry::builder().text(&cfg.model.ollama.model).valign(gtk4::Align::Center).build(); + ollama_model_row.add_suffix(&ollama_model_entry); + ollama_group.add(&ollama_model_row); + + let ollama_thresh_adj = gtk4::Adjustment::new(cfg.model.ollama.confidence_threshold as f64, 0.0, 1.0, 0.05, 0.1, 0.0); + let ollama_thresh_row = adw::spin_row("Confidence threshold", None, &ollama_thresh_adj); + if let Some(spin) = ollama_thresh_row.first_child().and_downcast::() { + spin.set_digits(2); + } + ollama_group.add(&ollama_thresh_row); + + page.add(&ollama_group); + + // ── Calendar sync ──────────────────────────────────────────── + let cal_group = adw::preferences_group( + "Calendar Sync", + Some("Sync reminders to a Nextcloud calendar via CalDAV."), + ); + + let cal_enabled_row = adw::toggle_row("Enabled", None, cfg.calendar.enabled); + cal_group.add(&cal_enabled_row); + + let cal_url_row = adw::action_row("Calendar URL", None); let cal_url = gtk4::Entry::builder() .text(&cfg.calendar.url) .placeholder_text("https://nextcloud.example.com/remote.php/dav/calendars/you/personal/") .hexpand(true) + .width_chars(36) + .valign(gtk4::Align::Center) .build(); - attach_row(&cal_grid, 1, "Calendar URL", &cal_url); + cal_url_row.add_suffix(&cal_url); + cal_group.add(&cal_url_row); - let cal_user = gtk4::Entry::builder() - .text(&cfg.calendar.username) - .build(); - attach_row(&cal_grid, 2, "Username", &cal_user); + let cal_user_row = adw::action_row("Username", None); + let cal_user = gtk4::Entry::builder().text(&cfg.calendar.username).valign(gtk4::Align::Center).build(); + cal_user_row.add_suffix(&cal_user); + cal_group.add(&cal_user_row); - let cal_pass = gtk4::Entry::builder() + let cal_pass_row = adw::action_row("App password", None); + let cal_pass = gtk4::PasswordEntry::builder() .text(&cfg.calendar.password) - .input_purpose(gtk4::InputPurpose::Password) - .visibility(false) + .show_peek_icon(true) + .valign(gtk4::Align::Center) .build(); - attach_row(&cal_grid, 3, "App password", &cal_pass); + cal_pass_row.add_suffix(&cal_pass); + cal_group.add(&cal_pass_row); - outer.append(&cal_frame); + page.add(&cal_group); // ── Save ────────────────────────────────────────────────────── let status_label = gtk4::Label::builder() @@ -157,20 +168,20 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled .build(); { - let dtc = default_type_combo.clone(); - let wts = ws_tag_switch.clone(); - let ars = archive_spin.clone(); + let dtc = default_type_row.clone(); + let wts = ws_tag_row.clone(); + let ars = archive_adj.clone(); let sne = snooze_entry.clone(); let moe = morning_entry.clone(); - let grs = grace_spin.clone(); + let grs = grace_adj.clone(); let mpe = model_path_entry.clone(); let tke = tokenizer_entry.clone(); let ode = ort_dylib_entry.clone(); - let oec = ollama_enabled.clone(); - let oee = ollama_endpoint.clone(); - let ome = ollama_model.clone(); - let ots = ollama_thresh.clone(); - let cec = cal_enabled.clone(); + let oec = ollama_enabled_row.clone(); + let oee = ollama_endpoint_entry.clone(); + let ome = ollama_model_entry.clone(); + let ots = ollama_thresh_adj.clone(); + let cec = cal_enabled_row.clone(); let cuc = cal_url.clone(); let csc = cal_user.clone(); let cpc = cal_pass.clone(); @@ -228,37 +239,19 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled let btn_row = gtk4::Box::builder() .orientation(gtk4::Orientation::Horizontal) .spacing(8) + .margin_top(16) + .margin_start(16) + .margin_end(16) + .margin_bottom(16) .build(); btn_row.append(&status_label); btn_row.append(>k4::Box::builder().hexpand(true).build()); btn_row.append(&save_btn); + + let outer = gtk4::Box::builder().orientation(gtk4::Orientation::Vertical).build(); + outer.append(&page); outer.append(&btn_row); scroll.set_child(Some(&outer)); scroll } - -fn make_section(title: &str) -> (gtk4::Frame, gtk4::Grid) { - let frame = gtk4::Frame::builder().label(title).build(); - let grid = gtk4::Grid::builder() - .row_spacing(8) - .column_spacing(16) - .margin_top(8) - .margin_bottom(8) - .margin_start(8) - .margin_end(8) - .build(); - frame.set_child(Some(&grid)); - (frame, grid) -} - -fn attach_row(grid: >k4::Grid, row: i32, label: &str, widget: &impl gtk4::prelude::IsA) { - let lbl = gtk4::Label::builder() - .label(label) - .xalign(0.0) - .hexpand(false) - .width_chars(24) - .build(); - grid.attach(&lbl, 0, row, 1, 1); - grid.attach(widget, 1, row, 1, 1); -}