From b9c27021689dab60bfcd19c381488dbc7b2191fc Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 15 Aug 2026 21:41:26 +0800 Subject: [PATCH] Pin bread-theme to v0.7.1; document Super+U and unshipped ONNX model bread-screenshots is not on the v0.7.1 tag, so it is rev-pinned instead of floating on branch=main. bread-theme v0.7.1 has no adw/chip helpers; breadman keeps local shims. CLAUDE.md records the single-trunk workflow. --- .gitignore | 4 ++-- CLAUDE.md | 37 ++++++++++++++++++++++++++++++++++ Cargo.lock | 17 ++++++++-------- README.md | 32 ++++++++++++++++------------- breadman/Cargo.toml | 9 ++++----- breadman/src/editor.rs | 13 ++++++------ breadman/src/main.rs | 16 +++++++-------- breadman/src/theme_widgets.rs | 23 +++++++++++++++++++++ breadman/src/views/settings.rs | 8 ++++---- breadpad-shared/Cargo.toml | 2 +- breadpad/Cargo.toml | 3 ++- 11 files changed, 113 insertions(+), 51 deletions(-) create mode 100644 CLAUDE.md create mode 100644 breadman/src/theme_widgets.rs diff --git a/.gitignore b/.gitignore index 4ac0998..3b0b041 100644 --- a/.gitignore +++ b/.gitignore @@ -28,5 +28,5 @@ svgs.txt Cargo.lock dist/ -# Local hygiene notes (not for commit) -CLAUDE.md +# Local knowledge-graph cache +graphify-out/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..81e4634 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,37 @@ +# CLAUDE.md — Repo hygiene + +Scope: this file covers *repo hygiene* — branching, remotes, CI, cleanup. It is not project documentation. + +This repo follows the branch/release workflow documented in `CONTRIBUTING.md` +— read and follow it for any git, branch, or release work here (the +single-trunk model, `feature/x`/`fix/x` branch naming, how RC tags work, +etc). Don't improvise a different workflow. The short version: there is one +long-lived branch, `main` — no `dev` or `beta` branch exists. `main` +auto-publishes a dev-track build on every push. "Beta" and "stable" are both +just tags, not branches: push a `vX.Y.Z-rc.N` tag to publish a beta-track +build, push a plain `vX.Y.Z` tag to cut the signed stable release. +"Freezing" for stabilization means pausing pushes to `main`, not moving a +branch. This replaced an earlier three-branch (`dev`/`beta`/`main`) model +after `main` was found to have silently rotted out of sync with `dev`/`beta` +across most repos in this ecosystem. + +## Remotes +- `origin` — Forgejo (`git.breadway.dev` via Hestia, SSH) — authoritative. +- `github` — GitHub mirror. Push both when publishing. + +## CI +- `check.yml` — clippy + test, triggers on push to `feature/**`/`fix/**`. +- `dev-release.yml` — triggers on push to `main`. +- `rc-release.yml` — triggers on `vX.Y.Z-rc.N` tag push. +- `release.yml` — triggers on any other `v*` tag push. + +All four run on a self-hosted runner (`hestia`) inside a pinned Arch +container — not the host's native environment. The Containerfile/build +script are shared across bread-ecosystem products and live in +`bread-ecosystem/ci/`; this repo's `ci/build.sh` clones that repo at the +sha in `ci/bread-ecosystem.rev` (deliberately pinned, not `main`) and +delegates to it. Nothing runs automatically on plain commits or PRs +beyond what's listed. + +## Don't +- Don't embed credentials in remote URLs — SSH or a credential helper only. diff --git a/Cargo.lock b/Cargo.lock index 1185b4f..d6b5f6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -305,7 +305,7 @@ dependencies = [ [[package]] name = "bread-screenshots" version = "0.3.1" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=main#f86e299f4a0ea73ff485cd84923b986ddcc8242e" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?rev=69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d#69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d" dependencies = [ "anyhow", "bread-utils", @@ -315,11 +315,10 @@ dependencies = [ [[package]] name = "bread-theme" version = "0.3.1" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=main#f86e299f4a0ea73ff485cd84923b986ddcc8242e" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1#db2fa3c4b4c1e6933bc5cf62a236d05972fdc886" dependencies = [ "dirs 5.0.1", "gtk4", - "libadwaita", "serde", "serde_json", ] @@ -327,7 +326,7 @@ dependencies = [ [[package]] name = "bread-utils" version = "0.3.1" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=main#f86e299f4a0ea73ff485cd84923b986ddcc8242e" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?rev=69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d#69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d" dependencies = [ "dirs 5.0.1", "serde", @@ -587,7 +586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -951,7 +950,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1262,7 +1261,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2756,7 +2755,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3080,7 +3079,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/README.md b/README.md index b39ea3d..e9f4498 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,11 @@ breadman GTK4 note viewer / manager ### Classification -Every note passes through a three-tier pipeline at capture time: +Every note passes through a three-tier pipeline at capture time. **Tier 1 is the only tier that ships with breadpad** — the ONNX classifier model is not bundled, and Ollama is optional. Capture works without either. -1. **Rule-based parser** — always runs first; handles time extraction ("at 7pm", "in 30 minutes", "tomorrow morning", "next Friday"), recurrence ("every Sunday at 9pm", "every weekday morning"), and strong type signals ("?" → question, "idea:" prefix → idea, action verbs → todo). High-confidence results skip the remaining tiers entirely. -2. **Small local ONNX model** — runs when Tier 1 can't confidently assign a type. Responsible for type classification only; Tier 1's extracted time, recurrence rule, and cleaned body are always preserved. -3. **Large local model via Ollama** — runs only when Tier 2 confidence falls below a configurable threshold. Communicates with a locally running Ollama instance over HTTP. If Ollama is unreachable, the Tier 2 result is used. No cloud APIs are involved. +1. **Rule-based parser** — always runs first; handles time extraction ("at 7pm", "in 30 minutes", "tomorrow morning", "next Friday"), recurrence ("every Sunday at 9pm", "every weekday morning"), and strong type signals ("?" → question, "idea:" prefix → idea, action verbs → todo). High-confidence results skip the remaining tiers entirely. This is the default path; no model files required. +2. **Small local ONNX model (optional, not shipped)** — runs when Tier 1 can't confidently assign a type *and* you have dropped in your own classifier. Responsible for type classification only; Tier 1's extracted time, recurrence rule, and cleaned body are always preserved. +3. **Large local model via Ollama (optional)** — runs only when Tier 2 confidence falls below a configurable threshold. Communicates with a locally running Ollama instance over HTTP. If Ollama is unreachable, the previous tier's result is used. No cloud APIs are involved. Manual override always available — the AI-assigned type is shown as a chip you can tap to change before saving. @@ -104,11 +104,11 @@ Always runs. Handles: Returns a calibrated confidence. If ≥ 0.82, Tiers 2 and 3 are skipped. -#### Tier 2 — Small local ONNX model +#### Tier 2 — Small local ONNX model (optional, not shipped) -Runs when Tier 1 confidence is below threshold. Responsible for **type classification only** — Tier 1's extracted time, recurrence rule, and cleaned body are always preserved. +Runs when Tier 1 confidence is below threshold **and** a compatible `classifier.onnx` + `tokenizer.json` are present. Responsible for **type classification only** — Tier 1's extracted time, recurrence rule, and cleaned body are always preserved. -Invoked via `ort` (ONNX Runtime Rust bindings, `load-dynamic`) on the CPU. Requires an external `libonnxruntime.so`; set `model.ort_dylib_path` in `breadpad.toml` or let breadpad auto-discover it via `ORT_DYLIB_PATH`. +Invoked via `ort` (ONNX Runtime Rust bindings, `load-dynamic`) on the CPU. Requires an external `libonnxruntime.so`; set `model.ort_dylib_path` in `breadpad.toml` or let breadpad auto-discover it via `ORT_DYLIB_PATH`. Without a model file or runtime library, Tier 2 is skipped and Tier 1 (plus optional Tier 3) still works. #### Tier 3 — Large local model via Ollama @@ -123,7 +123,7 @@ If Ollama is unreachable or returns an invalid response, breadpad logs a warning ~/.local/share/breadpad/model/tokenizer.json ``` -breadpad ships without a bundled model. Drop a compatible ONNX classifier and `tokenizer.json` at those paths, then configure `model.ort_dylib_path` to point at your ONNX Runtime library. +**breadpad does not ship a classifier model.** Tier 1 rules work with no extra files. If you want Tier 2, drop a compatible ONNX classifier and `tokenizer.json` at those paths and point `model.ort_dylib_path` at your ONNX Runtime library. ```bash breadpad model-info # shows active EP and model path @@ -138,7 +138,7 @@ breadpad model-info # shows active EP and model path - D-Bus session bus (for notifications) - systemd user session (for timer-backed reminders) - Rust 1.80+ -- **Tier 2 (ONNX classifier):** An external `libonnxruntime.so`. Set `model.ort_dylib_path` in `breadpad.toml`, or set `ORT_DYLIB_PATH` in your environment. Without a library, Tier 2 is disabled; Tier 1 + 3 still work. +- **Tier 2 (ONNX classifier, optional):** A model you supply yourself (`classifier.onnx` + `tokenizer.json`) and an external `libonnxruntime.so`. Set `model.ort_dylib_path` in `breadpad.toml`, or set `ORT_DYLIB_PATH` in your environment. Neither the model nor the runtime is shipped. Without them, Tier 2 is disabled; Tier 1 (and Tier 3, if Ollama is running) still work. - **Tier 3 only (optional):** [Ollama](https://ollama.com) running locally with your chosen model pulled (`ollama pull llama3.2:3b`). Tier 3 is silently skipped if Ollama is not running. --- @@ -146,15 +146,19 @@ breadpad model-info # shows active EP and model path ## Installation ```bash -git clone https://github.com/breadway/breadpad +git clone https://git.breadway.dev/Breadway/breadpad cd breadpad cargo build --release cp target/release/breadpad ~/.local/bin/ cp target/release/breadman ~/.local/bin/ +``` -# Place your ONNX classifier and tokenizer in the model directory +That's enough for capture + viewing. Tier 1 classification works out of the box. The ONNX classifier is **not** included — only add it if you want Tier 2: + +```bash mkdir -p ~/.local/share/breadpad/model -# Then set model.ort_dylib_path in breadpad.toml to your libonnxruntime.so +# Drop your own classifier.onnx + tokenizer.json in that directory, then +# set model.ort_dylib_path in breadpad.toml to your libonnxruntime.so ``` On Arch Linux, install GTK4 dependencies first: @@ -245,10 +249,10 @@ breadpad --no-classify breadpad --status ``` -Hyprland keybind: +Hyprland keybind (BOS default is Super+U; bind whatever you want): ``` -bind = $mainMod, N, exec, breadpad +bind = $mainMod, U, exec, breadpad ``` ### breadman (viewer) diff --git a/breadman/Cargo.toml b/breadman/Cargo.toml index 8bdd99c..4f36d71 100644 --- a/breadman/Cargo.toml +++ b/breadman/Cargo.toml @@ -12,11 +12,10 @@ path = "src/main.rs" [dependencies] 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 = "main" } -# 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 = "main", features = ["adw"] } +# v0.7.1 predates this crate; rev-pin so this is not `branch = "main"`. +bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", rev = "69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d" } +# `adw` / `chip` landed after v0.7.1 — breadman keeps local shims (see src/theme_widgets.rs). +bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1", features = ["gtk"] } libadwaita = { version = "0.9", features = ["v1_7"] } anyhow.workspace = true tracing.workspace = true diff --git a/breadman/src/editor.rs b/breadman/src/editor.rs index a11e46e..bea9803 100644 --- a/breadman/src/editor.rs +++ b/breadman/src/editor.rs @@ -3,7 +3,6 @@ //! flagged in design review as the weakest surface in the app). AdwDialog //! gives us the scrim, the title, and correct modal anchoring for free. -use bread_theme::adw; use breadpad_shared::{ parser::parse_rule_based, scheduler::Scheduler, @@ -54,25 +53,25 @@ pub fn open_editor( .margin_end(16) .build(); - let group = adw::preferences_group("Details", None); + let group = libadwaita::PreferencesGroup::builder().title("Details").build(); let body_row = libadwaita::EntryRow::builder().title("Body").build(); body_row.set_text(¬e.body); group.add(&body_row); - let type_row = adw::action_row("Type", None); + 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 selected_type: Rc> = 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 { - 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 name = *name; let all_btns: Vec = type_pills.iter().map(|(b, _)| b.clone()).collect(); btn.connect_clicked(move |clicked| { *sel.borrow_mut() = name.to_string(); - for b in &all_btns { bread_theme::gtk::set_chip_active(b, false); } - bread_theme::gtk::set_chip_active(clicked, true); + for b in &all_btns { crate::theme_widgets::set_chip_active(b, false); } + crate::theme_widgets::set_chip_active(clicked, true); }); type_pill_box.append(btn); } diff --git a/breadman/src/main.rs b/breadman/src/main.rs index 48430d7..0a784f1 100644 --- a/breadman/src/main.rs +++ b/breadman/src/main.rs @@ -14,6 +14,7 @@ use std::sync::Arc; mod editor; mod screenshot; +mod theme_widgets; mod views; // ── Args ───────────────────────────────────────────────────────────────────── @@ -357,7 +358,7 @@ fn build_app_window( // 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(); + theme_widgets::init_adw(); let store = Arc::new(Store::new()?); let notes = store.load_all()?; @@ -627,9 +628,8 @@ fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState, prese .build(); vbox.append(&body_entry); - // Type pills — same bread_theme::gtk::chip widget the editor dialog and - // settings screen use, instead of three different type-picker widgets - // across the app. + // Type pills — same chip widget the editor dialog and settings screen + // use, instead of three different type-picker widgets across the app. vbox.append(>k4::Label::builder().label("Type").xalign(0.0).build()); let chip_box = gtk4::Box::builder() .orientation(gtk4::Orientation::Horizontal) @@ -638,17 +638,17 @@ fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState, prese let selected_type: Rc> = Rc::new(RefCell::new(preselect.clone())); let chips: Vec<(gtk4::Button, NoteType)> = NoteType::all_builtin() .iter() - .map(|&name| (bread_theme::gtk::chip(name), NoteType::from_str(name))) + .map(|&name| (theme_widgets::chip(name), NoteType::from_str(name))) .collect(); 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 nt_c = nt.clone(); let all_btns: Vec = chips.iter().map(|(b, _)| b.clone()).collect(); btn.connect_clicked(move |clicked| { *sel.borrow_mut() = nt_c.clone(); - for b in &all_btns { bread_theme::gtk::set_chip_active(b, false); } - bread_theme::gtk::set_chip_active(clicked, true); + for b in &all_btns { theme_widgets::set_chip_active(b, false); } + theme_widgets::set_chip_active(clicked, true); }); chip_box.append(btn); } diff --git a/breadman/src/theme_widgets.rs b/breadman/src/theme_widgets.rs new file mode 100644 index 0000000..a5221e6 --- /dev/null +++ b/breadman/src/theme_widgets.rs @@ -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, 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); +} diff --git a/breadman/src/views/settings.rs b/breadman/src/views/settings.rs index 6090274..114edd1 100644 --- a/breadman/src/views/settings.rs +++ b/breadman/src/views/settings.rs @@ -92,10 +92,10 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled let selected_type: Rc> = Rc::new(RefCell::new(cfg.settings.default_type.clone())); let type_pills: Vec<(gtk4::Button, &'static str)> = NoteType::all_builtin() .iter() - .map(|&name| (bread_theme::gtk::chip(name), name)) + .map(|&name| (crate::theme_widgets::chip(name), name)) .collect(); 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); } 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 = type_pills.iter().map(|(b, _)| b.clone()).collect(); btn.connect_clicked(move |clicked| { *sel.borrow_mut() = name.to_string(); - for b in &all_btns { bread_theme::gtk::set_chip_active(b, false); } - bread_theme::gtk::set_chip_active(clicked, true); + for b in &all_btns { crate::theme_widgets::set_chip_active(b, false); } + crate::theme_widgets::set_chip_active(clicked, true); apply_now(); }); } diff --git a/breadpad-shared/Cargo.toml b/breadpad-shared/Cargo.toml index 0016141..9ef4f76 100644 --- a/breadpad-shared/Cargo.toml +++ b/breadpad-shared/Cargo.toml @@ -7,7 +7,7 @@ authors.workspace = true [dependencies] -bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", branch = "main", features = ["gtk"] } +bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1", features = ["gtk"] } anyhow.workspace = true tracing.workspace = true serde.workspace = true diff --git a/breadpad/Cargo.toml b/breadpad/Cargo.toml index 1fa8bae..d618e7a 100644 --- a/breadpad/Cargo.toml +++ b/breadpad/Cargo.toml @@ -12,7 +12,8 @@ path = "src/main.rs" [dependencies] 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 = "main" } +# v0.7.1 predates this crate; rev-pin so this is not `branch = "main"`. +bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", rev = "69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d" } anyhow.workspace = true ort.workspace = true tracing.workspace = true