From df42aba1d32ff85b5d20dbb7f14c22905aacf85b Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 19 Jun 2026 08:38:40 +0800 Subject: [PATCH 01/53] CI: use /tmp for ecosystem clone, avoid permissions conflict --- .github/workflows/release.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8df4368..e492838 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ permissions: env: DL_DIR: /srv/breadway-dl - ECOSYSTEM_DIR: /home/breadway/Projects/bread-ecosystem + ECOSYSTEM_DIR: /tmp/bread-ecosystem-ci jobs: build: @@ -40,12 +40,8 @@ jobs: - name: ensure bread-ecosystem run: | - if [[ -d "${ECOSYSTEM_DIR}/.git" ]]; then - git -C "${ECOSYSTEM_DIR}" pull --ff-only - else - mkdir -p "$(dirname "${ECOSYSTEM_DIR}")" - git clone https://github.com/Breadway/bread-ecosystem.git "${ECOSYSTEM_DIR}" - fi + rm -rf "${ECOSYSTEM_DIR}" + git clone https://github.com/Breadway/bread-ecosystem.git "${ECOSYSTEM_DIR}" - name: regenerate index.json run: bash "${ECOSYSTEM_DIR}/scripts/gen-index.sh" From b828f8ec79879871fd87463c5476cea80b67bdb3 Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 3 Jul 2026 14:10:07 +0800 Subject: [PATCH 02/53] CI: migrate release workflow from GitHub Actions to Forgejo Actions GitHub Actions self-hosted runners need per-repo registration on a personal account; Forgejo Actions' runner already serves every repo with zero setup. Moves release publishing there (dl.breadway.dev stays the primary bakery target; GitHub release upload is kept as the fallback via an explicit token, since Forgejo Actions has no ambient GITHUB_TOKEN) and adds a mirror workflow to keep GitHub in sync automatically. --- .forgejo/workflows/mirror.yml | 2 -- .forgejo/workflows/release.yml | 58 +++++++++++++++++++++++++++++++ .github/workflows/release.yml | 62 ---------------------------------- README.md | 9 +++-- 4 files changed, 64 insertions(+), 67 deletions(-) create mode 100644 .forgejo/workflows/release.yml delete mode 100644 .github/workflows/release.yml diff --git a/.forgejo/workflows/mirror.yml b/.forgejo/workflows/mirror.yml index e91c97e..01b9bef 100644 --- a/.forgejo/workflows/mirror.yml +++ b/.forgejo/workflows/mirror.yml @@ -14,8 +14,6 @@ jobs: set -euo pipefail git clone --mirror "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" repo.git cd repo.git - # Mirror only branches and tags (not refs/pull/*, which GitHub rejects); - # --prune deletes GitHub refs that no longer exist on Forgejo. git push --prune \ "https://x-access-token:${{ secrets.MIRROR_TOKEN }}@github.com/Breadway/breadpad.git" \ '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 0000000..c878cf4 --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,58 @@ +name: release + +on: + push: + tags: ["v*"] + +jobs: + build: + runs-on: [self-hosted, hestia] + steps: + - name: checkout + run: | + set -euo pipefail + rm -rf src && mkdir src + git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src + + - name: build + run: cd src && cargo build --release --locked + + - name: prepare artifacts + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + PKG_DIR="/srv/breadway-dl/breadpad/${VERSION}" + mkdir -p "${PKG_DIR}" + for bin in breadpad breadman; do + cp "src/target/release/${bin}" "${PKG_DIR}/${bin}-x86_64" + strip "${PKG_DIR}/${bin}-x86_64" + sha256sum "${PKG_DIR}/${bin}-x86_64" | awk '{print $1}' \ + > "${PKG_DIR}/${bin}-x86_64.sha256" + done + cp src/breadpad.example.toml "${PKG_DIR}/" + cp src/bakery.toml "${PKG_DIR}/bakery.toml" + ln -sfn "${VERSION}" "/srv/breadway-dl/breadpad/latest" + + - name: regenerate index.json + run: | + set -euo pipefail + rm -rf /tmp/bread-ecosystem-ci + git clone https://git.breadway.dev/Breadway/bread-ecosystem.git /tmp/bread-ecosystem-ci + bash /tmp/bread-ecosystem-ci/scripts/gen-index.sh + + - name: upload to GitHub Release + env: + GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }} + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + PKG_DIR="/srv/breadway-dl/breadpad/${VERSION}" + gh release create "${GITHUB_REF_NAME}" --repo Breadway/breadpad \ + --title "breadpad v${VERSION}" --generate-notes 2>/dev/null || true + gh release upload "${GITHUB_REF_NAME}" --repo Breadway/breadpad \ + "${PKG_DIR}/breadpad-x86_64" \ + "${PKG_DIR}/breadman-x86_64" \ + "${PKG_DIR}/breadpad-x86_64.sha256" \ + "${PKG_DIR}/breadman-x86_64.sha256" \ + --clobber diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index e492838..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: release - -on: - push: - tags: ["v*"] - -permissions: - contents: write - -env: - DL_DIR: /srv/breadway-dl - ECOSYSTEM_DIR: /tmp/bread-ecosystem-ci - -jobs: - build: - runs-on: [self-hosted, hestia] - steps: - - uses: actions/checkout@v4 - - - name: install build deps - run: sudo apt-get install -y libgtk-4-dev libdbus-1-dev pkg-config 2>/dev/null || true - - - name: build - run: cargo build --release --locked - - - name: prepare artifacts - run: | - VERSION="${GITHUB_REF_NAME#v}" - PKG_DIR="${DL_DIR}/breadpad/${VERSION}" - mkdir -p "${PKG_DIR}" - for bin in breadpad breadman; do - cp "target/release/${bin}" "${PKG_DIR}/${bin}-x86_64" - strip "${PKG_DIR}/${bin}-x86_64" - sha256sum "${PKG_DIR}/${bin}-x86_64" | awk '{print $1}' \ - > "${PKG_DIR}/${bin}-x86_64.sha256" - done - cp breadpad.example.toml "${PKG_DIR}/" - cp bakery.toml "${PKG_DIR}/bakery.toml" - ln -sfn "${VERSION}" "${DL_DIR}/breadpad/latest" - - - name: ensure bread-ecosystem - run: | - rm -rf "${ECOSYSTEM_DIR}" - git clone https://github.com/Breadway/bread-ecosystem.git "${ECOSYSTEM_DIR}" - - - name: regenerate index.json - run: bash "${ECOSYSTEM_DIR}/scripts/gen-index.sh" - - - name: upload to GitHub Release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - VERSION="${GITHUB_REF_NAME#v}" - PKG_DIR="${DL_DIR}/breadpad/${VERSION}" - gh release create "${GITHUB_REF_NAME}" \ - --title "breadpad v${VERSION}" --generate-notes 2>/dev/null || true - gh release upload "${GITHUB_REF_NAME}" \ - "${PKG_DIR}/breadpad-x86_64" \ - "${PKG_DIR}/breadman-x86_64" \ - "${PKG_DIR}/breadpad-x86_64.sha256" \ - "${PKG_DIR}/breadman-x86_64.sha256" \ - --clobber diff --git a/README.md b/README.md index b39ea3d..6d41ee8 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ User-defined tags can be added freely on top of the built-in types. - **One-off reminders** — natural language time ("at 7pm", "in 30 minutes", "tomorrow morning") parsed at classification time; scheduled via a systemd user timer - **Recurring reminders** — "every Sunday at 9pm", "every weekday morning" — stored as an iCal-compatible RRULE and re-scheduled on each trigger -- **Snooze** — notification popup includes snooze actions: 15 min / 1 hour / tomorrow morning / custom; snoozing reschedules the timer without touching the original note +- **Snooze** — notification popup includes snooze actions drawn from `snooze_options` (default: 15 min / 1 hour / tomorrow morning); snoozing reschedules the timer without touching the original note - **Missed reminders** — if the system was off or suspended at the scheduled time, the reminder fires on next login ### Viewer (`breadman`) @@ -139,7 +139,7 @@ breadpad model-info # shows active EP and model path - 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 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. +- **Tier 3 only (optional):** [Ollama](https://ollama.com) running locally with your chosen model pulled (e.g. `ollama pull fastflowlm`). Tier 3 is silently skipped if Ollama is not running. --- @@ -183,7 +183,7 @@ ort_dylib_path = "" # optional: explicit path to libonnxruntime.so; [model.ollama] endpoint = "http://localhost:11434" -model = "llama3.2:3b" # any model you have pulled in Ollama +model = "fastflowlm" # any model you have pulled in Ollama confidence_threshold = 0.6 # Tier 2 scores below this trigger Tier 3 enabled = true # set false to never call Ollama @@ -243,6 +243,9 @@ breadpad --no-classify # Show model and storage status breadpad --status + +# Print expected model paths (does not download automatically) +breadpad download-model ``` Hyprland keybind: From 6fea1af54492e86264b562604465bb3a5f6abb20 Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 4 Jul 2026 10:53:52 +0800 Subject: [PATCH 03/53] Release 0.4.0: GUI design pass + td:/rem:/note:/q: prefix grammar Unifies the "selected" visual language across breadman's sidebar and breadpad's type chips (ghost default, solid accent when active), gives note cards a real surface instead of just a left border, caps the note list width so type chips and action buttons sit next to the title they belong to, and swaps the sidebar's full-color emoji for theme-safe monochrome glyphs plus per-type colored dots. breadman also gets live per-row sidebar counts, a note-count footer, relative timestamps with absolute-time tooltips, and a de-emphasized Errors row. breadpad's popup gets an explicit short-prefix grammar (td:, rem:, idea:, note:, q:) that live-highlights the matching chip as you type, a keyboard-shortcut hint row, and a confirm button that's visually separated from the chip row instead of reading as a sixth pill. --- Cargo.lock | 8 +- Cargo.toml | 2 +- breadman/src/main.rs | 202 +++++++++++++++++++++++++++++----- breadpad-shared/src/parser.rs | 113 ++++++++++++++++++- breadpad-shared/src/theme.rs | 95 +++++++++++++--- breadpad/src/main.rs | 35 +++++- 6 files changed, 404 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 756f0c3..4936958 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -315,7 +315,7 @@ dependencies = [ [[package]] name = "breadman" -version = "0.3.4" +version = "0.4.0" dependencies = [ "anyhow", "breadpad-shared", @@ -332,7 +332,7 @@ dependencies = [ [[package]] name = "breadpad" -version = "0.3.4" +version = "0.4.0" dependencies = [ "anyhow", "breadpad-shared", @@ -351,7 +351,7 @@ dependencies = [ [[package]] name = "breadpad-shared" -version = "0.3.4" +version = "0.4.0" dependencies = [ "anyhow", "bread-theme", @@ -377,7 +377,7 @@ dependencies = [ [[package]] name = "breadpad-test" -version = "0.3.4" +version = "0.4.0" dependencies = [ "anyhow", "breadpad-shared", diff --git a/Cargo.toml b/Cargo.toml index 4092bed..1de5996 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.3.4" +version = "0.4.0" edition = "2021" license = "MIT" authors = ["Breadway"] diff --git a/breadman/src/main.rs b/breadman/src/main.rs index d46557a..e132a80 100644 --- a/breadman/src/main.rs +++ b/breadman/src/main.rs @@ -6,7 +6,7 @@ use breadpad_shared::{ store::Store, types::{Note, NoteType, RecurrenceRule}, }; -use chrono::Local; +use chrono::{DateTime, Local, Utc}; use gtk4::{glib, prelude::*}; use std::cell::RefCell; use std::rc::Rc; @@ -69,10 +69,21 @@ struct AppState { errors: Rc, String)>>>, active_view: Rc>, stack: gtk4::Stack, + /// Sidebar row id ("all", "upcoming", "archive", or a note type name) -> + /// its count `Label`, so counts can be refreshed in place after every + /// `rebuild_stack` without rebuilding the sidebar itself. + sidebar_counts: Rc>>, + status_label: gtk4::Label, } impl AppState { - fn new(store: Arc, notes: Vec, cfg: Config, stack: gtk4::Stack) -> Self { + fn new( + store: Arc, + notes: Vec, + cfg: Config, + stack: gtk4::Stack, + status_label: gtk4::Label, + ) -> Self { AppState { store, notes: Rc::new(RefCell::new(notes)), @@ -80,6 +91,8 @@ impl AppState { errors: Rc::new(RefCell::new(Vec::new())), active_view: Rc::new(RefCell::new("all".to_string())), stack, + sidebar_counts: Rc::new(RefCell::new(Vec::new())), + status_label, } } @@ -192,6 +205,40 @@ fn rebuild_stack(state: &AppState) { // Errors let errors_view = views::errors::build(&errors); state.stack.add_named(&errors_view, Some("errors")); + + update_counts_and_status(state); +} + +/// Refreshes the sidebar's per-row counts and the content pane's footer +/// note count from the current `state.notes`. Cheap enough to call on every +/// rebuild — five type counts plus all/upcoming/archive over a note list +/// that in practice stays small. +fn update_counts_and_status(state: &AppState) { + let notes = state.notes.borrow(); + let total = notes.iter().filter(|n| !n.done).count(); + state + .status_label + .set_label(&format!("{total} note{}", if total == 1 { "" } else { "s" })); + + for (key, label) in state.sidebar_counts.borrow().iter() { + let n = match key.as_str() { + "all" => total, + "upcoming" => notes + .iter() + .filter(|n| { + !n.done + && matches!(n.note_type, NoteType::Reminder | NoteType::Todo) + && n.effective_time().is_some() + }) + .count(), + "archive" => notes.iter().filter(|n| n.done).count(), + other => { + let nt = NoteType::from_str(other); + notes.iter().filter(|n| !n.done && n.note_type == nt).count() + } + }; + label.set_label(&n.to_string()); + } } // ── main ───────────────────────────────────────────────────────────────────── @@ -331,7 +378,12 @@ fn build_app_window( )); row }; - let make_item = |id: &str, icon: &str, label: &str| { + // Returns the row plus its count `Label` when `counted` is set — callers + // collect these so counts can be kept live from `AppState.sidebar_counts` + // (monochrome geometric icons + a colored dot per type instead of the + // old full-color emoji, which always render in fixed colors no matter + // what the pywal palette says). + let make_item = |id: &str, icon: &str, icon_class: Option<&str>, label: &str, counted: bool| { let row = gtk4::ListBoxRow::builder() .css_classes(["sidebar-row"]) .build(); @@ -340,13 +392,15 @@ fn build_app_window( .orientation(gtk4::Orientation::Horizontal) .spacing(10) .build(); - hbox.append( - >k4::Label::builder() - .label(icon) - .width_chars(2) - .xalign(0.5) - .build(), - ); + let icon_label = gtk4::Label::builder() + .label(icon) + .width_chars(2) + .xalign(0.5) + .build(); + if let Some(class) = icon_class { + icon_label.add_css_class(class); + } + hbox.append(&icon_label); hbox.append( >k4::Label::builder() .label(label) @@ -354,23 +408,62 @@ fn build_app_window( .hexpand(true) .build(), ); + let count_label = if counted { + let l = gtk4::Label::builder() + .label("") + .css_classes(["sidebar-count"]) + .build(); + hbox.append(&l); + Some(l) + } else { + None + }; row.set_child(Some(&hbox)); - row + (row, count_label) }; + let mut sidebar_counts: Vec<(String, gtk4::Label)> = Vec::new(); + sidebar_list.append(&make_section("VIEWS")); - sidebar_list.append(&make_item("all", "📋", "All")); - sidebar_list.append(&make_item("upcoming", "📅", "Upcoming")); + { + let (row, count) = make_item("all", "▦", None, "All", true); + sidebar_counts.push(("all".into(), count.unwrap())); + sidebar_list.append(&row); + } + { + let (row, count) = make_item("upcoming", "◷", None, "Upcoming", true); + sidebar_counts.push(("upcoming".into(), count.unwrap())); + sidebar_list.append(&row); + } sidebar_list.append(&make_section("TYPES")); - sidebar_list.append(&make_item("todo", "✅", "Todo")); - sidebar_list.append(&make_item("reminder", "🔔", "Reminder")); - sidebar_list.append(&make_item("idea", "💡", "Idea")); - sidebar_list.append(&make_item("note", "📝", "Note")); - sidebar_list.append(&make_item("question", "❓", "Question")); + for (id, icon_class, label) in [ + ("todo", "icon-todo", "Todo"), + ("reminder", "icon-reminder", "Reminder"), + ("idea", "icon-idea", "Idea"), + ("note", "icon-note", "Note"), + ("question", "icon-question", "Question"), + ] { + let (row, count) = make_item(id, "●", Some(icon_class), label, true); + sidebar_counts.push((id.to_string(), count.unwrap())); + sidebar_list.append(&row); + } sidebar_list.append(&make_section("MORE")); - sidebar_list.append(&make_item("archive", "📦", "Archive")); - sidebar_list.append(&make_item("settings", "⚙", "Settings")); - sidebar_list.append(&make_item("errors", "⚠", "Errors")); + { + let (row, count) = make_item("archive", "▢", None, "Archive", true); + sidebar_counts.push(("archive".into(), count.unwrap())); + sidebar_list.append(&row); + } + { + let (row, _) = make_item("settings", "⚙", None, "Settings", false); + sidebar_list.append(&row); + } + { + // De-emphasized: session-only debug info, not part of the daily + // triage flow the rest of the sidebar serves. + let (row, _) = make_item("errors", "⚠", None, "Errors", false); + row.add_css_class("sidebar-row-minor"); + sidebar_list.append(&row); + } sidebar_vbox.append(&sidebar_list); // ── Content area ────────────────────────────────────────────── @@ -390,8 +483,19 @@ fn build_app_window( let stack = gtk4::Stack::builder().hexpand(true).vexpand(true).build(); + let status_label = gtk4::Label::builder() + .label("0 notes") + .css_classes(["dim-label"]) + .xalign(0.0) + .margin_start(12) + .margin_end(12) + .margin_top(6) + .margin_bottom(8) + .build(); + content_vbox.append(&search_entry); content_vbox.append(&stack); + content_vbox.append(&status_label); hbox.append(&sidebar_vbox); hbox.append(>k4::Separator::builder() @@ -401,7 +505,8 @@ fn build_app_window( window.set_child(Some(&hbox)); // ── AppState ────────────────────────────────────────────────── - let state = AppState::new(store, notes, cfg, stack.clone()); + let state = AppState::new(store, notes, cfg, stack.clone(), status_label.clone()); + state.sidebar_counts.replace(sidebar_counts); // Initial build rebuild_stack(&state); @@ -478,6 +583,9 @@ fn build_note_list(notes: &[Note], state: AppState) -> gtk4::ScrolledWindow { .vexpand(true) .build(); + // Capped and centered so cards don't stretch edge-to-edge on a wide + // window — full-width rows left the type chip and action buttons + // hundreds of pixels from the title they belong to. let list = gtk4::Box::builder() .orientation(gtk4::Orientation::Vertical) .spacing(8) @@ -485,18 +593,33 @@ fn build_note_list(notes: &[Note], state: AppState) -> gtk4::ScrolledWindow { .margin_bottom(12) .margin_start(12) .margin_end(12) + .width_request(700) + .halign(gtk4::Align::Center) .build(); let mut sorted: Vec = notes.iter().filter(|n| !n.done).cloned().collect(); sorted.sort_by(|a, b| b.created.cmp(&a.created)); if sorted.is_empty() { - list.append( + let empty = gtk4::Box::builder() + .orientation(gtk4::Orientation::Vertical) + .spacing(6) + .halign(gtk4::Align::Center) + .margin_top(64) + .build(); + empty.append( >k4::Label::builder() - .label("No notes here yet.") - .margin_top(32) + .label("No notes here yet") + .css_classes(["note-title"]) .build(), ); + empty.append( + >k4::Label::builder() + .label("Capture something with breadpad and it'll show up here.") + .css_classes(["dim-label"]) + .build(), + ); + list.append(&empty); } else { for note in &sorted { list.append(&build_note_card(note, state.clone())); @@ -507,6 +630,26 @@ fn build_note_list(notes: &[Note], state: AppState) -> gtk4::ScrolledWindow { scroll } +/// Short relative form for a recency-ordered list ("2h ago"); the exact +/// absolute timestamp is still available via tooltip. Falls back to a plain +/// date once a note is more than a week old, where "Nd ago" stops being +/// useful at a glance. +fn humanize_relative(dt: DateTime) -> String { + let secs = Utc::now().signed_duration_since(dt).num_seconds().max(0); + if secs < 60 { + "just now".to_string() + } else if secs < 3600 { + format!("{}m ago", secs / 60) + } else if secs < 86_400 { + format!("{}h ago", secs / 3600) + } else if secs < 86_400 * 7 { + format!("{}d ago", secs / 86_400) + } else { + let local: DateTime = dt.into(); + local.format("%b %d").to_string() + } +} + fn build_note_card(note: &Note, state: AppState) -> gtk4::Box { let card = gtk4::Box::builder() .orientation(gtk4::Orientation::Vertical) @@ -530,6 +673,7 @@ fn build_note_card(note: &Note, state: AppState) -> gtk4::Box { .hexpand(true) .xalign(0.0) .wrap(true) + .css_classes(["note-title"]) .build(); let type_chip = gtk4::Label::builder() @@ -546,14 +690,15 @@ fn build_note_card(note: &Note, state: AppState) -> gtk4::Box { .spacing(8) .build(); - let created_str = { + let created_abs = { let local: chrono::DateTime = note.created.into(); local.format("%b %d %H:%M").to_string() }; let meta_label = gtk4::Label::builder() - .label(&created_str) + .label(&humanize_relative(note.created)) .css_classes(["dim-label"]) .xalign(0.0) + .tooltip_text(&created_abs) .build(); // Date first, then chips @@ -561,8 +706,9 @@ fn build_note_card(note: &Note, state: AppState) -> gtk4::Box { if let Some(ws) = ¬e.workspace { bottom_row.append( >k4::Label::builder() - .label(&format!("ws:{}", ws)) + .label(&format!("ws {}", ws)) .css_classes(["type-chip"]) + .tooltip_text(&format!("Workspace {}", ws)) .build(), ); } diff --git a/breadpad-shared/src/parser.rs b/breadpad-shared/src/parser.rs index cc03b5e..e398e73 100644 --- a/breadpad-shared/src/parser.rs +++ b/breadpad-shared/src/parser.rs @@ -85,6 +85,43 @@ fn rrule_weekday(wd: Weekday) -> &'static str { } } +/// Explicit type prefixes, checked before any lexical heuristics. Short forms +/// exist so the capture popup can be driven without reaching for the mouse — +/// see `breadpad_shared::parser::detect_prefix_type`, which the popup's entry +/// uses to live-highlight the matching chip as the user types. +const TYPE_PREFIXES: &[(&str, NoteType)] = &[ + ("td:", NoteType::Todo), + ("rem:", NoteType::Reminder), + ("idea:", NoteType::Idea), + ("note:", NoteType::Note), + ("q:", NoteType::Question), +]; + +/// If `text` starts with one of [`TYPE_PREFIXES`] (case-insensitive), returns +/// the type it forces. Used both to classify at save time and to live-drive +/// the popup's chip highlighting as the user types. +pub fn detect_prefix_type(text: &str) -> Option { + let lower = text.trim_start().to_lowercase(); + TYPE_PREFIXES + .iter() + .find(|(prefix, _)| lower.starts_with(prefix)) + .map(|(_, nt)| nt.clone()) +} + +/// Strips a leading explicit type prefix (if any), returning the forced type +/// and the remaining text with the prefix and any following whitespace removed. +fn strip_explicit_prefix(text: &str) -> (Option, String) { + let trimmed = text.trim_start(); + let lower = trimmed.to_lowercase(); + for (prefix, nt) in TYPE_PREFIXES { + if lower.starts_with(prefix) { + let rest = trimmed[prefix.len()..].trim_start().to_string(); + return (Some(nt.clone()), rest); + } + } + (None, text.to_string()) +} + fn next_occurrence_of_weekday(wd: Weekday, time: NaiveTime) -> DateTime { let local = Local::now(); let days_ahead = (wd.num_days_from_monday() as i64 @@ -105,6 +142,8 @@ fn next_occurrence_of_weekday(wd: Weekday, time: NaiveTime) -> DateTime { } pub fn parse_rule_based(text: &str, default_morning: &str) -> ClassificationResult { + let (forced_type, text_owned) = strip_explicit_prefix(text); + let text: &str = &text_owned; let p = patterns(); let morning_time: NaiveTime = default_morning .split(':') @@ -290,8 +329,9 @@ pub fn parse_rule_based(text: &str, default_morning: &str) -> ClassificationResu .to_string(); } - // Infer note type - let note_type = infer_type(text, extracted_time.is_some(), rrule.is_some()); + // Infer note type — an explicit prefix (`td:`, `rem:`, …) always wins. + let note_type = + forced_type.clone().unwrap_or_else(|| infer_type(text, extracted_time.is_some(), rrule.is_some())); // Trim artifacts cleaned = cleaned @@ -303,7 +343,9 @@ pub fn parse_rule_based(text: &str, default_morning: &str) -> ClassificationResu // Calibrated confidence: high when structural signals drove the decision, // low when we fell back to "note" with no positive evidence. - let confidence = if rrule.is_some() || extracted_time.is_some() { + let confidence = if forced_type.is_some() { + 0.99 // explicit prefix — unambiguous + } else if rrule.is_some() || extracted_time.is_some() { 0.95 // time/recurrence extraction is deterministic } else { match ¬e_type { @@ -410,6 +452,70 @@ mod tests { assert_eq!(p("idea: reactive state module in Lua").note_type, NoteType::Idea); } + #[test] + fn idea_prefix_stripped_from_body() { + let r = p("idea: reactive state module in Lua"); + assert_eq!(r.body, "reactive state module in Lua"); + } + + // ---- Explicit short prefixes (td:, rem:, note:, q:) ---- + + #[test] + fn td_prefix_is_todo() { + let r = p("td: buy milk"); + assert_eq!(r.note_type, NoteType::Todo); + assert_eq!(r.body, "buy milk"); + } + + #[test] + fn rem_prefix_is_reminder() { + let r = p("rem: water the plants"); + assert_eq!(r.note_type, NoteType::Reminder); + assert_eq!(r.body, "water the plants"); + } + + #[test] + fn note_prefix_is_note() { + // Without the prefix this would classify as Todo ("check ..."). + let r = p("note: check engine light has been on for a week"); + assert_eq!(r.note_type, NoteType::Note); + assert_eq!(r.body, "check engine light has been on for a week"); + } + + #[test] + fn q_prefix_is_question() { + // Without the prefix this has no strong signal and would fall to Note. + let r = p("q: ONNX rocm vs cpu perf"); + assert_eq!(r.note_type, NoteType::Question); + assert_eq!(r.body, "ONNX rocm vs cpu perf"); + } + + #[test] + fn explicit_prefix_confidence_is_high() { + assert_eq!(p("td: buy milk").confidence, 0.99); + } + + #[test] + fn explicit_prefix_is_case_insensitive() { + assert_eq!(p("TD: buy milk").note_type, NoteType::Todo); + assert_eq!(p("Rem: standup").note_type, NoteType::Reminder); + } + + #[test] + fn explicit_prefix_overrides_time_extraction_type() { + // "at 7pm" alone would infer Reminder; an explicit td: prefix wins. + let r = p("td: pack bag at 7pm"); + assert_eq!(r.note_type, NoteType::Todo); + assert!(r.time.is_some(), "time should still be extracted"); + } + + #[test] + fn detect_prefix_type_matches_parse() { + assert_eq!(detect_prefix_type("td: buy milk"), Some(NoteType::Todo)); + assert_eq!(detect_prefix_type("rem: call mum"), Some(NoteType::Reminder)); + assert_eq!(detect_prefix_type("no prefix here"), None); + } + #[test] fn idea_maybe() { assert_eq!(p("maybe we could cache the ONNX model").note_type, NoteType::Idea); @@ -884,7 +990,6 @@ fn infer_type(text: &str, has_time: bool, has_rrule: bool) -> NoteType { return NoteType::Todo; } if lower.starts_with("what if ") - || lower.starts_with("idea:") || lower.contains("could ") || lower.contains("maybe ") || lower.starts_with("should we ") diff --git a/breadpad-shared/src/theme.rs b/breadpad-shared/src/theme.rs index cb1139a..8cc2512 100644 --- a/breadpad-shared/src/theme.rs +++ b/breadpad-shared/src/theme.rs @@ -43,9 +43,14 @@ window { border-radius: 8px; } border-color: @teal; } +/* Shared "selected/active" language: a ghost/ outline default state that + stays quiet, and a solid, high-contrast accent fill for whatever is + currently selected — used identically by .type-chip.active and + .sidebar-row:selected so the two windows read as one system. */ .type-chip { - background: @overlay; - color: @on-overlay; + background: transparent; + color: alpha(@fg, 0.6); + border: 1px solid alpha(@fg, 0.18); border-radius: 999px; padding: 4px 12px; font-size: 12px; @@ -55,27 +60,69 @@ window { border-radius: 8px; } .type-chip.active { background: @blue; color: @on-accent; + border-color: @blue; + font-weight: 600; } +/* Per-type tint so info badges (note cards, workspace/recur tags) stay + scannable at a glance without the old full-color emoji. */ +.note-card-todo .type-chip { color: @green; border-color: alpha(@green, 0.4); } +.note-card-reminder .type-chip { color: @yellow; border-color: alpha(@yellow, 0.4); } +.note-card-idea .type-chip { color: @pink; border-color: alpha(@pink, 0.4); } +.note-card-question .type-chip { color: @teal; border-color: alpha(@teal, 0.4); } +.note-card-note .type-chip { color: @blue; border-color: alpha(@blue, 0.4); } + .confirm-button { background: @blue; color: @on-accent; border: none; border-radius: 8px; - padding: 8px 16px; + padding: 10px 22px; + min-height: 20px; + font-size: 15px; font-weight: bold; } +.confirm-button:hover { background: shade(@blue, 1.1); } + +/* Separates the primary action from the chip row it sits beside so it + doesn't read as just another pill. */ +.confirm-wrap { + border-left: 1px solid alpha(@fg, 0.12); + padding-left: 12px; + margin-left: 4px; +} + +.prefix-hint { + color: alpha(@fg, 0.4); + font-size: 10px; + letter-spacing: 0.3px; +} + .note-card { - background: shade(@bg, 1.1); - border-radius: 8px; + background: shade(@bg, 1.12); + border: 1px solid alpha(@fg, 0.07); + border-radius: 10px; padding: 12px; - margin: 8px; + margin: 6px 0; border-left: 3px solid @blue; } .note-card:hover { - background: shade(@bg, 1.2); + background: shade(@bg, 1.22); + border-color: alpha(@fg, 0.12); +} + +.note-title { + font-weight: 600; +} + +.note-card .action-btn { + opacity: 0.45; +} + +.note-card:hover .action-btn { + opacity: 1; } .search-entry { @@ -83,7 +130,8 @@ window { border-radius: 8px; } color: @fg; border: 1px solid @overlay; border-radius: 6px; - padding: 8px 12px; + padding: 5px 10px; + font-size: 13px; } .search-entry:focus { @@ -104,7 +152,27 @@ window { border-radius: 8px; } .sidebar-row:selected { background: @blue; color: @on-accent; - font-weight: 500; + font-weight: 600; +} + +.sidebar-row-minor { + opacity: 0.5; + font-size: 12px; +} + +.sidebar-count { + color: alpha(@fg, 0.45); + font-size: 11px; +} + +.icon-todo { color: @green; } +.icon-reminder { color: @yellow; } +.icon-idea { color: @pink; } +.icon-note { color: @blue; } +.icon-question { color: @teal; } + +.sidebar-row:selected .sidebar-count { + color: alpha(@on-accent, 0.75); } .sidebar-section-label { @@ -119,14 +187,15 @@ window { border-radius: 8px; } background: transparent; border: none; border-radius: 6px; - padding: 2px 7px; - min-width: 28px; - min-height: 28px; - font-size: 14px; + padding: 3px 8px; + min-width: 32px; + min-height: 32px; + font-size: 16px; } .action-btn:hover { background: shade(@bg, 1.3); + opacity: 1; } .done-btn { color: @green; } diff --git a/breadpad/src/main.rs b/breadpad/src/main.rs index aa924d0..033a114 100644 --- a/breadpad/src/main.rs +++ b/breadpad/src/main.rs @@ -636,11 +636,43 @@ fn build_window( } } + // Live prefix grammar: typing "td: ", "rem: ", "idea: ", "note: ", or + // "q: " at the start of the entry drives the chip selection without + // ever touching the mouse — the chips become feedback for what you + // typed rather than the only way to pick a type. + { + let selected_type_clone = selected_type.clone(); + let chips_clone: Vec<(gtk4::Button, NoteType)> = chips.clone(); + entry.connect_changed(move |e| { + let Some(nt) = breadpad_shared::parser::detect_prefix_type(&e.text()) else { + return; + }; + *selected_type_clone.borrow_mut() = nt.clone(); + for (btn, chip_nt) in &chips_clone { + if *chip_nt == nt { + btn.add_css_class("active"); + } else { + btn.remove_css_class("active"); + } + } + }); + } + + let hint = gtk4::Label::builder() + .label("td: · rem: · idea: · note: · q:") + .css_classes(["prefix-hint"]) + .xalign(0.0) + .build(); + // Confirm button let confirm_btn = gtk4::Button::builder() .label("✓") .css_classes(["confirm-button"]) .build(); + let confirm_wrap = gtk4::Box::builder() + .css_classes(["confirm-wrap"]) + .build(); + confirm_wrap.append(&confirm_btn); let bottom_row = gtk4::Box::builder() .orientation(gtk4::Orientation::Horizontal) @@ -650,9 +682,10 @@ fn build_window( let spacer = gtk4::Box::builder().hexpand(true).build(); bottom_row.append(&spacer); - bottom_row.append(&confirm_btn); + bottom_row.append(&confirm_wrap); vbox.append(&entry); + vbox.append(&hint); vbox.append(&bottom_row); window.set_child(Some(&vbox)); From 7394e65da59cb65211630800f12883c5907ef4bf Mon Sep 17 00:00:00 2001 From: Breadway Date: Thu, 16 Jul 2026 17:52:05 +0800 Subject: [PATCH 04/53] breadpad: bump bread-theme to v0.2.10 (fixes pywal-colored window background) --- Cargo.lock | 22 +++++++++++----------- breadpad-shared/Cargo.toml | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4936958..de5654b 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-theme" version = "0.2.3" -source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.2.8#77417d552130281ff787e07d52541eb25e9d533b" +source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.2.10#17d1bb85801b9a8c195b64c02d288cd662c9c780" dependencies = [ "dirs 5.0.1", "gtk4", @@ -851,7 +851,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -926,7 +926,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]] @@ -1237,7 +1237,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2079,7 +2079,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]] @@ -2701,7 +2701,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2915,7 +2915,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]] @@ -3025,7 +3025,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3393,7 +3393,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] diff --git a/breadpad-shared/Cargo.toml b/breadpad-shared/Cargo.toml index 01470f9..7af8279 100644 --- a/breadpad-shared/Cargo.toml +++ b/breadpad-shared/Cargo.toml @@ -7,7 +7,7 @@ authors.workspace = true [dependencies] -bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.8", features = ["gtk"] } +bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.10", features = ["gtk"] } anyhow.workspace = true tracing.workspace = true serde.workspace = true From b7aed8a37cb943e604d1b0831997bb627eefcf3e Mon Sep 17 00:00:00 2001 From: Breadway Date: Thu, 16 Jul 2026 18:04:39 +0800 Subject: [PATCH 05/53] breadpad: bump version to 0.4.1 --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de5654b..3e8ac3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -315,7 +315,7 @@ dependencies = [ [[package]] name = "breadman" -version = "0.4.0" +version = "0.4.1" dependencies = [ "anyhow", "breadpad-shared", @@ -332,7 +332,7 @@ dependencies = [ [[package]] name = "breadpad" -version = "0.4.0" +version = "0.4.1" dependencies = [ "anyhow", "breadpad-shared", @@ -351,7 +351,7 @@ dependencies = [ [[package]] name = "breadpad-shared" -version = "0.4.0" +version = "0.4.1" dependencies = [ "anyhow", "bread-theme", @@ -377,7 +377,7 @@ dependencies = [ [[package]] name = "breadpad-test" -version = "0.4.0" +version = "0.4.1" dependencies = [ "anyhow", "breadpad-shared", diff --git a/Cargo.toml b/Cargo.toml index 1de5996..b287686 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.4.0" +version = "0.4.1" edition = "2021" license = "MIT" authors = ["Breadway"] From 6e5448e853d68aca27b0e80c299d05ae46c28697 Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 17 Jul 2026 08:28:19 +0800 Subject: [PATCH 06/53] breadpad: save note before window can close, add network timeouts, lock the JSONL store, fix monthly recurrence, restrict config permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - main.rs: hold the gtk4::Application (RAII guard from app.hold()) across the idle_add_local_once save callback, so closing the capture window can no longer let the process quit before the note is actually written to disk - breadpad-shared/src/ai.rs: 10s timeout on the Ollama HTTP call (ureq had none, so a hung local endpoint could stall indefinitely) - breadpad-shared/src/calendar.rs: 15s timeout on the CalDAV reqwest client (applies to every CalDAV request made through it) - breadpad-shared/src/store.rs: exclusive flock (std::fs::File::lock, no new dependency needed at this Rust version) on a sidecar lock file around every read-modify-write span, guarding against breadpad/breadman/ reminder-fire processes racing each other on notes.jsonl - breadpad-shared/src/scheduler.rs: parse_next_from_rrule now handles FREQ=MONTHLY (previously fell into the catch-all None arm, so a monthly reminder fired once and never rescheduled); reachable today via breadman's free-text RRULE editor field - breadpad-shared/src/config.rs: Config::save() chmods breadpad.toml to 0600 after writing, since it can hold the CalDAV password in plaintext Pre-existing, unrelated test failure noted: theme::tests::css_defines_bg_color fails identically on the original commit (depends on this machine's live pywal cache) — confirmed via git stash, not touched. --- breadpad-shared/src/ai.rs | 6 +++ breadpad-shared/src/calendar.rs | 6 +++ breadpad-shared/src/config.rs | 14 +++++++ breadpad-shared/src/scheduler.rs | 71 +++++++++++++++++++++++++++++++- breadpad-shared/src/store.rs | 71 ++++++++++++++++++++++++++------ breadpad/src/main.rs | 14 ++++++- 6 files changed, 168 insertions(+), 14 deletions(-) diff --git a/breadpad-shared/src/ai.rs b/breadpad-shared/src/ai.rs index 7bb69ad..b6afa17 100644 --- a/breadpad-shared/src/ai.rs +++ b/breadpad-shared/src/ai.rs @@ -61,8 +61,14 @@ impl OllamaClient { "stream": false }); + // ureq 2's default agent has no overall request timeout, so a hung + // local Ollama endpoint would otherwise stall this call forever — + // and since classification now runs from an idle callback after the + // capture window has already closed (see `main.rs`), a hang here is + // invisible to the user, not just slow. let response = ureq::post(&url) .set("Content-Type", "application/json") + .timeout(std::time::Duration::from_secs(10)) .send_json(payload) .map_err(|e| anyhow::anyhow!("Ollama HTTP error: {}", e))?; diff --git a/breadpad-shared/src/calendar.rs b/breadpad-shared/src/calendar.rs index 479ad05..99df780 100644 --- a/breadpad-shared/src/calendar.rs +++ b/breadpad-shared/src/calendar.rs @@ -18,7 +18,13 @@ impl CalDavClient { pub fn new(config: CalendarConfig) -> Self { // `reqwest::Client::builder().build()` can only fail if the TLS backend can't be // initialised; fall back to `Client::new()` semantics rather than panicking. + // + // A request-wide timeout is set here (rather than on `Client::new()`'s + // untimed defaults) so a hung/unreachable CalDAV server can't hang + // whatever's making the request indefinitely — `reqwest::Client::new()` + // has no timeout of its own. let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(15)) .build() .unwrap_or_else(|e| { tracing::warn!("falling back to default HTTP client: {}", e); diff --git a/breadpad-shared/src/config.rs b/breadpad-shared/src/config.rs index 9d81e5e..2eeb3d5 100644 --- a/breadpad-shared/src/config.rs +++ b/breadpad-shared/src/config.rs @@ -193,6 +193,20 @@ impl Config { } let text = toml::to_string_pretty(self)?; fs::write(&path, text)?; + + // This file can hold the CalDAV password in plaintext (see + // `CalendarConfig`'s own doc comment) — `fs::write` creates it with + // the process's default umask, which on most setups means + // world-readable. Lock it down to owner-only rather than just + // telling the user to do it themselves. + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if let Err(e) = fs::set_permissions(&path, fs::Permissions::from_mode(0o600)) { + tracing::warn!("failed to restrict permissions on {}: {}", path.display(), e); + } + } + Ok(()) } } diff --git a/breadpad-shared/src/scheduler.rs b/breadpad-shared/src/scheduler.rs index ff5ff26..98d9f2b 100644 --- a/breadpad-shared/src/scheduler.rs +++ b/breadpad-shared/src/scheduler.rs @@ -206,6 +206,42 @@ pub(crate) fn parse_next_from_rrule(rrule_str: &str, default_morning: &str) -> O (now.date_naive() + chrono::Duration::days(days_ahead)).and_time(fire_time); return Some(local_naive_to_utc(target_date)); } + "MONTHLY" => { + use chrono::Datelike; + // BYMONTHDAY isn't guaranteed to be present — breadman's note + // editor lets a user type an arbitrary RRULE by hand, and + // "FREQ=MONTHLY" alone is a perfectly valid (if under-specified) + // one. Fall back to today's day-of-month, mirroring how WEEKLY + // above defaults BYDAY to "MO" when absent. + let day: u32 = parts + .get("BYMONTHDAY") + .and_then(|v| v.parse().ok()) + .filter(|d: &u32| (1..=31).contains(d)) + .unwrap_or_else(|| now.day()); + + let mut year = now.year(); + let mut month = now.month(); + + // Walk forward month by month for the next calendar date that + // (a) actually has this day-of-month (a 31st skips e.g. April) + // and (b) is still in the future. Bounded to 24 months as a + // defensive cap — every valid day (1-31) recurs well within a + // year, so this should never come close to firing. + for _ in 0..24 { + if let Some(date) = chrono::NaiveDate::from_ymd_opt(year, month, day) { + let candidate = date.and_time(fire_time); + if now.naive_local() < candidate { + return Some(local_naive_to_utc(candidate)); + } + } + month += 1; + if month > 12 { + month = 1; + year += 1; + } + } + None + } _ => None, } } @@ -404,7 +440,40 @@ mod tests { #[test] fn unknown_freq_returns_none() { - assert!(parse_next_from_rrule("RRULE:FREQ=MONTHLY;BYHOUR=9;BYMINUTE=0", "08:00").is_none()); + assert!(parse_next_from_rrule("RRULE:FREQ=YEARLY;BYHOUR=9;BYMINUTE=0", "08:00").is_none()); + } + + #[test] + fn monthly_reschedules_instead_of_returning_none() { + // This used to be the exact bug: MONTHLY fell into the `_ => None` + // arm, so a monthly reminder fired once and never rescheduled. + let t = parse_next_from_rrule("RRULE:FREQ=MONTHLY;BYMONTHDAY=15;BYHOUR=9;BYMINUTE=0", "08:00"); + assert!(t.is_some(), "MONTHLY must produce a next occurrence, not None"); + let local: chrono::DateTime = t.unwrap().into(); + assert_eq!(local.day(), 15); + assert_eq!(local.hour(), 9); + assert_eq!(local.minute(), 0); + assert!(local > Local::now()); + } + + #[test] + fn monthly_without_bymonthday_uses_todays_day_of_month() { + let t = parse_next_from_rrule("RRULE:FREQ=MONTHLY;BYHOUR=23;BYMINUTE=59", "08:00").unwrap(); + let local: chrono::DateTime = t.into(); + assert_eq!(local.day(), Local::now().day()); + } + + #[test] + fn monthly_on_the_31st_skips_shorter_months() { + // Every candidate month/day combination this walks must actually + // exist (from_ymd_opt returning None for e.g. April 31 is skipped + // internally) — this mostly guards against a panic/infinite loop + // regression rather than a specific date, since "next Feb 31" et al + // must fall through to a month that really has a 31st. + let t = parse_next_from_rrule("RRULE:FREQ=MONTHLY;BYMONTHDAY=31;BYHOUR=9;BYMINUTE=0", "08:00"); + assert!(t.is_some()); + let local: chrono::DateTime = t.unwrap().into(); + assert_eq!(local.day(), 31); } #[test] diff --git a/breadpad-shared/src/store.rs b/breadpad-shared/src/store.rs index bc8b86a..653eff8 100644 --- a/breadpad-shared/src/store.rs +++ b/breadpad-shared/src/store.rs @@ -44,11 +44,43 @@ impl Store { } } + /// Path to the sidecar lock file guarding the whole note store — a + /// single lock covers both `notes.jsonl` and `archive.jsonl` since + /// `rotate_archive` moves notes between them in one logical operation. + fn lock_path(&self) -> PathBuf { + self.notes_path.with_extension("lock") + } + + /// Blocks until an exclusive advisory lock (`flock`) is held on the + /// sidecar lock file, and holds it for as long as the returned `File` + /// stays alive (the lock is released automatically when it's dropped, + /// same as it would be on process exit/crash). + /// + /// breadpad (capture/fire/snooze), breadman (edit), and multiple + /// concurrent reminder-fire processes all read and rewrite the same + /// JSONL file with no coordination otherwise: two concurrent + /// load-modify-rewrite cycles racing `write_all`'s rename can silently + /// lose one side's change. This is intentionally one lock for the + /// entire store rather than per-note or per-file locking — contention + /// is expected to be rare (a handful of short-lived CLI-ish processes, + /// not a server), so simplicity wins over fine-grained locking here. + fn acquire_lock(&self) -> Result { + let file = OpenOptions::new() + .create(true) + .write(true) + .open(self.lock_path()) + .context("failed to open notes store lock file")?; + file.lock().context("failed to acquire notes store lock")?; + Ok(file) + } + pub fn load_all(&self) -> Result> { + let _lock = self.acquire_lock()?; self.load_from(&self.notes_path) } pub fn load_archive(&self) -> Result> { + let _lock = self.acquire_lock()?; self.load_from(&self.archive_path) } @@ -74,12 +106,15 @@ impl Store { } pub fn save_note(&self, note: &Note) -> Result<()> { - let mut file = OpenOptions::new() - .create(true) - .append(true) - .open(&self.notes_path)?; - let line = serde_json::to_string(note)?; - writeln!(file, "{}", line)?; + { + let _lock = self.acquire_lock()?; + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(&self.notes_path)?; + let line = serde_json::to_string(note)?; + writeln!(file, "{}", line)?; + } if let Some(cal_cfg) = &self.calendar { if cal_cfg.enabled && (note.time.is_some() || note.rrule.is_some()) { @@ -103,13 +138,18 @@ impl Store { } pub fn delete_note(&self, id: &str) -> Result<()> { - let all = self.load_all()?; - let (to_delete, keep): (Vec, Vec) = all.into_iter().partition(|n| n.id == id); - self.write_all(&self.notes_path, &keep)?; + let to_delete_note = { + let _lock = self.acquire_lock()?; + let all = self.load_from(&self.notes_path)?; + let (to_delete, keep): (Vec, Vec) = + all.into_iter().partition(|n| n.id == id); + self.write_all(&self.notes_path, &keep)?; + to_delete.into_iter().next() + }; if let Some(cal_cfg) = &self.calendar { if cal_cfg.enabled { - if let Some(note) = to_delete.into_iter().next() { + if let Some(note) = to_delete_note { spawn_caldav_delete(caldav_uid(¬e), cal_cfg.clone()); } } @@ -118,11 +158,17 @@ impl Store { Ok(()) } + /// Holds the store lock across the whole load-modify-write span (not + /// just the write) — `load_from` is used directly here rather than the + /// public, self-locking `load_all`, since re-acquiring the same + /// process-wide advisory lock while already holding it would block + /// forever (`flock` isn't re-entrant across separate file descriptors). fn rewrite_notes(&self, mut f: F) -> Result<()> where F: FnMut(Note) -> Note, { - let notes: Vec = self.load_all()?.into_iter().map(|n| f(n)).collect(); + let _lock = self.acquire_lock()?; + let notes: Vec = self.load_from(&self.notes_path)?.into_iter().map(|n| f(n)).collect(); self.write_all(&self.notes_path, ¬es) } @@ -141,8 +187,9 @@ impl Store { } pub fn rotate_archive(&self, archive_after_days: i64) -> Result { + let _lock = self.acquire_lock()?; let cutoff = Utc::now() - Duration::days(archive_after_days); - let notes = self.load_all()?; + let notes = self.load_from(&self.notes_path)?; let (to_archive, keep): (Vec, Vec) = notes .into_iter() .partition(|n| n.done && n.completed.map_or(false, |c| c < cutoff)); diff --git a/breadpad/src/main.rs b/breadpad/src/main.rs index 033a114..e33087c 100644 --- a/breadpad/src/main.rs +++ b/breadpad/src/main.rs @@ -694,6 +694,7 @@ fn build_window( let selected_type_clone = selected_type.clone(); let cfg_clone = cfg.clone(); let workspace_clone = workspace.clone(); + let app_clone = app.clone(); let save_and_close = { let win = win_clone.clone(); @@ -701,6 +702,7 @@ fn build_window( let selected_type = selected_type_clone.clone(); let cfg = cfg_clone.clone(); let workspace = workspace_clone.clone(); + let app = app_clone.clone(); move || { let text = entry.text().to_string(); @@ -711,10 +713,20 @@ fn build_window( let note_type = selected_type.borrow().clone(); let cfg_c = cfg.clone(); let ws_c = workspace.clone(); - // Close first so the popup disappears immediately, then save. + // Close first so the popup disappears immediately, then save — + // but `hold()` the application across the gap first. Without + // this, closing the only open window can let the whole process + // (and with it, the `idle_add_local_once` callback below that + // actually writes the note) exit before that callback ever + // runs, silently losing the note the user just typed. `hold()` + // returns an RAII guard that keeps the app alive with zero + // windows open until it's dropped, right after the save + // finishes below. + let hold_guard = app.hold(); win.close(); glib::idle_add_local_once(move || { save_note_classified(&text, note_type, no_classify, cfg_c, ws_c); + drop(hold_guard); }); } }; From 369935515b166d89d8157f8bd93d4c59ebf55e3f Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 17 Jul 2026 08:32:17 +0800 Subject: [PATCH 07/53] breadpad-shared: fix stale test expectation from the bread-theme v0.2.10 bump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit css_defines_bg_color still expected the pre-v0.2.10 Catppuccin Mocha default (#1e1e2e). bread-theme v0.2.10 deliberately fixed background to #0c0c0c (BOS's own dark theme, no longer pywal-derived — see FIXED_BACKGROUND's doc comment in bread-theme's palette.rs) so a light or muddy wallpaper can't wash out every bread app's background. The product code was already correct; only this assertion was never updated for the bump, so it started failing the moment breadpad's Cargo.toml pin moved to v0.2.10 (confirmed via git stash: it fails identically on the original commit). --- breadpad-shared/src/theme.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/breadpad-shared/src/theme.rs b/breadpad-shared/src/theme.rs index 8cc2512..1b6cb7f 100644 --- a/breadpad-shared/src/theme.rs +++ b/breadpad-shared/src/theme.rs @@ -284,8 +284,14 @@ mod tests { #[test] fn css_defines_bg_color() { + // #1e1e2e (Catppuccin Mocha) was the default before bread-theme + // v0.2.10, which deliberately fixed background/surface/overlay to + // BOS's own dark theme (#0c0c0c etc.) instead of tracking pywal — + // see bread-theme's `palette.rs` `FIXED_BACKGROUND` doc comment. + // This assertion was never updated for that bump, so it started + // failing the moment breadpad's Cargo.toml pin moved to v0.2.10. 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] From 6a06872f09b7376d3387d415e5cd3af2bc1ac44f Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 17 Jul 2026 13:53:00 +0800 Subject: [PATCH 08/53] Fix silent ROCm-to-CPU fallback: switch to MIGraphX; fix XDG tilde bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the actual bug the night's ecosystem-utils audit was looking for. classifier.rs::try_load_session requested ort::ep::ROCm (the classic ROCMExecutionProvider) first. Per this machine's own breadsearch-gpu- backends operator notes, that EP silently no-ops on this class of system: distro ROCm onnxruntime builds (Arch's onnxruntime-rocm) are commonly compiled with --use_migraphx, not --use_rocm, so ROCMExecutionProvider never actually registers — active_provider could report "ROCm (iGPU)" while every real inference secretly ran on CPU, with nothing surfacing that fact anywhere. Switched to ort::ep::MIGraphX via bread_onnx::build_session (path dependency for now, see the TODO in Cargo.toml), matching breadsearch's own already-correct embed.rs. Also fixed the same literal-tilde XDG fallback bug found across this pass (breadclip-core, breadmon, breadarr-shared) in three more places: classifier.rs::model_dir, config.rs::config_path, config.rs::style_css_path. Bumped the workspace's tokenizers pin 0.21 -> 0.23 to unify with bread-onnx's own requirement (breadarr already pins 0.23); verified via a full workspace build + test pass, no API changes needed at any call site. Validation, and an important finding: breadpad-shared's full test suite (unit: 181/181, config: 26/26, classifier integration: 15/15) passes clean. The pipeline.rs integration suite (16 tests, each building a real classifier session) surfaced ONE genuine, 100%-reproducible failure: plain_note_appears_in_store expects "retro went well today" to classify as Note, but MIGraphX execution classifies it as Question (CPU execution returns Note). This is not a regression from this change — it's proof the fix works: the GPU path was never actually running before, so this CPU-vs-GPU floating-point divergence on a borderline NLI classification was always latent and simply never observable. Left the test as-is (its failure now accurately reflects reality) rather than "fixing" it by reverting to the broken EP or silently forcing a test-only CPU path — that's a product/test-fixture decision for the owner, not something a duplication-extraction pass should decide unilaterally. See bread-onnx's companion fix (ORT_MIGRAPHX_MODEL_CACHE_PATH default) which cut this suite's wall time from 905s to 112s by letting compiled kernels persist across runs. --- Cargo.lock | 135 ++++++++++++++++++++++++++---- Cargo.toml | 6 +- breadpad-shared/Cargo.toml | 3 + breadpad-shared/src/classifier.rs | 65 ++++++++------ breadpad-shared/src/config.rs | 14 ++-- breadpad/Cargo.toml | 2 + breadpad/src/main.rs | 15 ++-- 7 files changed, 183 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3e8ac3d..01a39f8 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.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -87,7 +87,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -302,6 +302,20 @@ dependencies = [ "piper", ] +[[package]] +name = "bread-onnx" +version = "0.2.3" +dependencies = [ + "anyhow", + "bread-utils", + "hex", + "ort", + "sha2", + "tokenizers", + "tracing", + "ureq", +] + [[package]] name = "bread-theme" version = "0.2.3" @@ -313,6 +327,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "bread-utils" +version = "0.2.3" +dependencies = [ + "dirs 5.0.1", + "serde", + "serde_json", +] + [[package]] name = "breadman" version = "0.4.1" @@ -335,6 +358,7 @@ name = "breadpad" version = "0.4.1" dependencies = [ "anyhow", + "bread-utils", "breadpad-shared", "chrono", "dirs 5.0.1", @@ -354,7 +378,9 @@ name = "breadpad-shared" version = "0.4.1" dependencies = [ "anyhow", + "bread-onnx", "bread-theme", + "bread-utils", "chrono", "dirs 5.0.1", "ical", @@ -613,6 +639,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "console" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width", + "windows-sys 0.61.2", +] + [[package]] name = "convert_case" version = "0.10.0" @@ -704,6 +742,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "daachorse" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f55d7153ba3b507595872a3874803f07a8a81d1e888abed8e5db7da0597d6e2" + [[package]] name = "darling" version = "0.20.11" @@ -851,7 +895,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -926,7 +970,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -934,6 +978,9 @@ name = "esaxx-rs" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" +dependencies = [ + "cc", +] [[package]] name = "event-listener" @@ -958,9 +1005,9 @@ dependencies = [ [[package]] name = "fancy-regex" -version = "0.14.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" +checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8" dependencies = [ "bit-set", "regex-automata", @@ -1237,7 +1284,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1489,7 +1536,7 @@ checksum = "629d8f3bbeda9d148036d6b0de0a3ab947abd08ce90626327fc3547a49d59d97" dependencies = [ "dirs 6.0.0", "http", - "indicatif", + "indicatif 0.17.11", "libc", "log", "rand 0.9.4", @@ -1793,13 +1840,26 @@ version = "0.17.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" dependencies = [ - "console", + "console 0.15.11", "number_prefix", "portable-atomic", "unicode-width", "web-time", ] +[[package]] +name = "indicatif" +version = "0.18.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" +dependencies = [ + "console 0.16.4", + "portable-atomic", + "unicode-width", + "unit-prefix", + "web-time", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -2079,7 +2139,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2127,6 +2187,28 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "onig" +version = "6.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" +dependencies = [ + "bitflags", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2701,7 +2783,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2859,6 +2941,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -2915,7 +3008,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3025,7 +3118,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3104,23 +3197,25 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenizers" -version = "0.21.4" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a620b996116a59e184c2fa2dfd8251ea34a36d0a514758c6f966386bd2e03476" +checksum = "44e5bea67576e04b6ff8564c5d9e09c2ef0cf476502245f2f120e497769d3112" dependencies = [ "ahash", - "aho-corasick", "compact_str", + "daachorse", "dary_heap", "derive_builder", "esaxx-rs", "fancy-regex", "getrandom 0.3.4", "hf-hub", + "indicatif 0.18.6", "itertools", "log", "macro_rules_attribute", "monostate", + "onig", "paste", "rand 0.9.4", "rayon", @@ -3393,7 +3488,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3435,6 +3530,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index b287686..53daebd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,11 @@ tokio = { version = "1", features = ["full"] } zbus = { version = "4", default-features = false, features = ["tokio"] } ort = { version = "2.0.0-rc.12", default-features = false, features = ["std", "ndarray", "tracing", "api-24", "rocm", "load-dynamic"] } ndarray = "0.16" -tokenizers = { version = "0.21", default-features = false, features = ["http", "fancy-regex"] } +# Bumped 0.21 -> 0.23 to unify with bread-onnx's own tokenizers requirement +# (breadarr already pins 0.23) — the APIs this crate actually calls +# (Tokenizer::from_file, encode, get_ids/get_attention_mask) are unchanged +# between the two; verified via a full workspace build + test pass. +tokenizers = { version = "0.23", default-features = false, features = ["http", "fancy-regex"] } gtk4 = { version = "0.11", features = ["v4_12"] } gtk4-layer-shell = "0.8" hyprland = "0.4.0-beta.3" diff --git a/breadpad-shared/Cargo.toml b/breadpad-shared/Cargo.toml index 7af8279..cc2e2f5 100644 --- a/breadpad-shared/Cargo.toml +++ b/breadpad-shared/Cargo.toml @@ -26,6 +26,9 @@ regex.workspace = true ureq.workspace = true reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } ical = "0.11" +# TODO(owner): switch to tag-pinned git dependency once bread-onnx/bread-utils are merged and tagged, matching the bread-theme pattern +bread-onnx = { path = "../../bread-ecosystem-fix-worktree/bread-onnx" } +bread-utils = { path = "../../bread-ecosystem-fix-worktree/bread-utils" } [dev-dependencies] tempfile = "3" diff --git a/breadpad-shared/src/classifier.rs b/breadpad-shared/src/classifier.rs index e752a31..18efb83 100644 --- a/breadpad-shared/src/classifier.rs +++ b/breadpad-shared/src/classifier.rs @@ -16,7 +16,7 @@ pub enum ExecutionProvider { impl ExecutionProvider { pub fn as_str(&self) -> &str { match self { - ExecutionProvider::Gpu => "ROCm (iGPU)", + ExecutionProvider::Gpu => "MIGraphX (iGPU)", ExecutionProvider::Cpu => "CPU", } } @@ -32,10 +32,13 @@ pub struct Classifier { } fn model_dir() -> PathBuf { - dirs::data_local_dir() - .unwrap_or_else(|| PathBuf::from("~/.local/share")) - .join("breadpad") - .join("model") + // Was `dirs::data_local_dir().unwrap_or_else(|| PathBuf::from("~/.local/share"))` + // — the same literal-tilde-fallback bug flagged (but not fixed) in + // breadclip-core tonight: PathBuf/std::fs never expand `~`, so on the + // rare box where `dirs` can't resolve a home directory this silently + // resolved to a directory literally named `~` under the current working + // directory instead of the user's actual home. + bread_utils::xdg::data_dir("breadpad").join("model") } impl Classifier { @@ -246,21 +249,46 @@ fn softmax_single(logits: &[f32], idx: usize) -> f32 { fn try_load_session( path: &std::path::Path, ) -> (Option, ExecutionProvider) { - // Try ROCm (iGPU) first, fall back to CPU. - let rocm_available = { + // AMD iGPU via MIGraphX, falling back to CPU. This used to request the + // classic `ort::ep::ROCm` (ROCMExecutionProvider) first — per this + // machine's own breadsearch-gpu-backends operator notes, that EP + // silently no-ops back to CPU on this class of system (distro ROCm + // onnxruntime builds, e.g. Arch's onnxruntime-rocm, are commonly + // compiled with `--use_migraphx`, not `--use_rocm`), so "ROCm (iGPU)" + // could report as active in this struct's own `active_provider` while + // every inference actually ran on CPU. See bread_onnx::provider's doc + // comment for the full history — breadsearch's own embed.rs already + // got this right. + // + // The `is_available()` gate (kept from the original implementation) + // means `active_provider` only ever claims Gpu when we actually + // attempted the GPU build — bread_onnx::build_session's loud EP- + // selection logging (visible once tracing_subscriber is initialized, + // which this crate's own main.rs already does) is what catches the + // *silent per-node fallback* class of bug this rewrite exists to fix, + // rather than papering over it by unconditionally reporting Gpu. + let migraphx_available = { use ort::execution_providers::ExecutionProvider as _; - ort::ep::ROCm::default().is_available().unwrap_or(false) + ort::ep::MIGraphX::default().is_available().unwrap_or(false) }; - if rocm_available { - match build_onnx_session(path, ort::ep::ROCm::default().build()) { + if migraphx_available { + match bread_onnx::build_session( + path, + ort::session::builder::GraphOptimizationLevel::Level3, + &[bread_onnx::Provider::MiGraphX { device_id: 0 }], + ) { Ok(s) => { - tracing::info!("ONNX session loaded (ROCm iGPU)"); + tracing::info!("ONNX session loaded (MIGraphX iGPU)"); return (Some(s), ExecutionProvider::Gpu); } - Err(e) => tracing::debug!("ROCm EP unavailable: {}; trying CPU", e), + Err(e) => tracing::debug!("MIGraphX EP unavailable: {}; trying CPU", e), } } - match build_onnx_session(path, ort::ep::CPU::default().build()) { + match bread_onnx::build_session( + path, + ort::session::builder::GraphOptimizationLevel::Level3, + &[bread_onnx::Provider::Cpu], + ) { Ok(s) => { tracing::info!("ONNX session loaded (CPU)"); (Some(s), ExecutionProvider::Cpu) @@ -271,14 +299,3 @@ fn try_load_session( } } } - -fn build_onnx_session( - path: &std::path::Path, - ep: ort::ep::ExecutionProviderDispatch, -) -> anyhow::Result { - 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)) -} diff --git a/breadpad-shared/src/config.rs b/breadpad-shared/src/config.rs index 2eeb3d5..12702ec 100644 --- a/breadpad-shared/src/config.rs +++ b/breadpad-shared/src/config.rs @@ -212,15 +212,13 @@ impl Config { } pub fn config_path() -> PathBuf { - dirs::config_dir() - .unwrap_or_else(|| PathBuf::from("~/.config")) - .join("breadpad") - .join("breadpad.toml") + // Was `dirs::config_dir().unwrap_or_else(|| PathBuf::from("~/.config"))` + // — same literal-tilde-fallback bug as `classifier.rs::model_dir` (see + // its doc comment) and breadclip-core's `data_dir`; PathBuf/std::fs + // never expand `~`. + bread_utils::xdg::config_dir("breadpad").join("breadpad.toml") } pub fn style_css_path() -> PathBuf { - dirs::config_dir() - .unwrap_or_else(|| PathBuf::from("~/.config")) - .join("breadpad") - .join("style.css") + bread_utils::xdg::config_dir("breadpad").join("style.css") } diff --git a/breadpad/Cargo.toml b/breadpad/Cargo.toml index c835bb9..4bf9b0b 100644 --- a/breadpad/Cargo.toml +++ b/breadpad/Cargo.toml @@ -23,3 +23,5 @@ gtk4-layer-shell.workspace = true hyprland.workspace = true dirs.workspace = true tokio.workspace = true +# TODO(owner): switch to tag-pinned git dependency once bread-utils is merged and tagged, matching the bread-theme pattern +bread-utils = { path = "../../bread-ecosystem-fix-worktree/bread-utils" } diff --git a/breadpad/src/main.rs b/breadpad/src/main.rs index e33087c..3d946f2 100644 --- a/breadpad/src/main.rs +++ b/breadpad/src/main.rs @@ -536,13 +536,14 @@ fn run_popup(preset_type: Option, no_classify: bool, cfg: Config) -> Res } fn get_active_workspace() -> Option { - // Use hyprctl via CLI since the async API would require a runtime here - let out = std::process::Command::new("hyprctl") - .args(["activeworkspace", "-j"]) - .output() - .ok()?; - let val: serde_json::Value = serde_json::from_slice(&out.stdout).ok()?; - val.get("id").and_then(|v| v.as_i64()).map(|id| id.to_string()) + // Was a `Command::new("hyprctl").output()` call with no timeout (the + // `hyprland` crate's async API would require a runtime here, which this + // call site doesn't have) — bread_utils::hypr's socket1 client is + // synchronous and needs neither a subprocess nor a runtime. + bread_utils::hypr::request_json("j/activeworkspace")? + .get("id") + .and_then(|v| v.as_i64()) + .map(|id| id.to_string()) } fn build_window( From 710cb768ea921202f9c06479aa6cf4598dbf738b Mon Sep 17 00:00:00 2001 From: Breadway Date: Sun, 19 Jul 2026 03:52:59 +0800 Subject: [PATCH 09/53] Switch to tag-pinned bread-ecosystem deps; bump version to v0.5.0 --- Cargo.lock | 612 +++++++++++++++---------------------- Cargo.toml | 2 +- README.md | 2 +- breadpad-shared/Cargo.toml | 5 +- breadpad/Cargo.toml | 3 +- packaging/arch/PKGBUILD | 4 +- 6 files changed, 250 insertions(+), 378 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 01a39f8..147ec8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,9 +92,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "async-broadcast" @@ -276,9 +276,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "block-buffer" @@ -304,7 +304,8 @@ dependencies = [ [[package]] name = "bread-onnx" -version = "0.2.3" +version = "0.3.0" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.3.0#8e82d2d833e992ce939a5b836f910ee109f2e939" dependencies = [ "anyhow", "bread-utils", @@ -329,7 +330,8 @@ dependencies = [ [[package]] name = "bread-utils" -version = "0.2.3" +version = "0.3.0" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.3.0#8e82d2d833e992ce939a5b836f910ee109f2e939" dependencies = [ "dirs 5.0.1", "serde", @@ -338,7 +340,7 @@ dependencies = [ [[package]] name = "breadman" -version = "0.4.1" +version = "0.5.0" dependencies = [ "anyhow", "breadpad-shared", @@ -355,7 +357,7 @@ dependencies = [ [[package]] name = "breadpad" -version = "0.4.1" +version = "0.5.0" dependencies = [ "anyhow", "bread-utils", @@ -375,7 +377,7 @@ dependencies = [ [[package]] name = "breadpad-shared" -version = "0.4.1" +version = "0.5.0" dependencies = [ "anyhow", "bread-onnx", @@ -403,7 +405,7 @@ dependencies = [ [[package]] name = "breadpad-test" -version = "0.4.1" +version = "0.5.0" dependencies = [ "anyhow", "breadpad-shared", @@ -431,9 +433,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cairo-rs" @@ -469,9 +471,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.63" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" dependencies = [ "find-msvc-tools", "shlex", @@ -495,9 +497,20 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] [[package]] name = "chrono" @@ -537,9 +550,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.1" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011" dependencies = [ "clap_builder", "clap_derive", @@ -547,9 +560,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" dependencies = [ "anstream", "anstyle", @@ -675,6 +688,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -686,9 +708,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -696,18 +718,18 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crossterm" @@ -1052,12 +1074,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1069,24 +1085,24 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -1095,9 +1111,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-lite" @@ -1114,9 +1130,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", @@ -1125,21 +1141,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-core", "futures-macro", @@ -1176,9 +1192,9 @@ dependencies = [ [[package]] name = "gdk4" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd42fdbbf48612c6e8f47c65fb92d2e8f39c25aecd6af047e83897c1a22d2a4e" +checksum = "d81e2a6c6ecba2aab60633a98df1868b03fa0bfdce8105edc27c1bccf71f0e39" dependencies = [ "cairo-rs", "gdk-pixbuf", @@ -1192,9 +1208,9 @@ dependencies = [ [[package]] name = "gdk4-sys" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d974ac4f15e67472c3a9728daf612590b4a5762a4b33f0edd298df0b80d043c" +checksum = "3d8f608d8d7d229975c4d0d026f5d3071598c4ddab3c5262b0a31840fec78d13" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -1237,31 +1253,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", - "wasip2", - "wasip3", + "rand_core 0.10.1", + "wasm-bindgen", ] [[package]] name = "gio" -version = "0.22.6" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3848bcba3a35cc0a71df8ba8ecfd799d6bfb862342a53a4a915fb62213aa4e6" +checksum = "8b3e1f669909c326b9413bde5a742097b8c90a7d78f45326db13668984769ded" dependencies = [ "futures-channel", "futures-core", @@ -1276,9 +1291,9 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.22.0" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64729ba2772c080448f9f966dba8f4456beeb100d8c28a865ef8a0f2ef4987e1" +checksum = "353fdc7da7cd16da916104b1e0e4e7de380ec9c8aaa20d4d742d66310ab4b0d5" dependencies = [ "glib-sys", "gobject-sys", @@ -1309,9 +1324,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.22.7" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c207e04e51605dcf7b2924c41591b3a10e1438eaac5bcf448fb91f325381104a" +checksum = "ddbcf514bd1881fc1b960e4e52b4e82873f4da3bceddbd58d42827b508888100" dependencies = [ "bitflags", "futures-channel", @@ -1342,9 +1357,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.22.6" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7fbac234ed5bc2a28359b7bde8e1b9cdf1441cc2d7f068e4824672d7db9445" +checksum = "030967459f9f676851872c6304adea7825c6d462ec9b72554c733cf0c5952233" dependencies = [ "libc", "system-deps", @@ -1363,32 +1378,30 @@ dependencies = [ [[package]] name = "graphene-rs" -version = "0.22.0" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d1b7881f96869f49808b6adfe906a93a57a34204952253444d68c3208d71f1" +checksum = "eb856b9c558971c3f13ab692358926da710b046932a4e087aedcc35b040d7dff" dependencies = [ "glib", "graphene-sys", - "libc", ] [[package]] name = "graphene-sys" -version = "0.22.0" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "517f062f3fd6b7fd3e57a3f038a74b3c23ca32f51199ff028aa704609943f79c" +checksum = "5c7ffdfde88f3570d3705e0d8a2433e036d387a1f2930bbf47eafcb5f569fd04" dependencies = [ "glib-sys", "libc", - "pkg-config", "system-deps", ] [[package]] name = "gsk4" -version = "0.11.1" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c912dfcbd28acace5fc99c40bb9f25e1dcb73efb1f2608327f66a99acdcb62" +checksum = "b867be1c5f14dcb8f552c0eff6e9a9b1da5f8b43943e8efc3a63c889d84952ff" dependencies = [ "cairo-rs", "gdk4", @@ -1401,9 +1414,9 @@ dependencies = [ [[package]] name = "gsk4-sys" -version = "0.11.1" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d54bbc7a9d8b6ffe4f0c95eede15ccfb365c8bf521275abe6bcfb57b18fb8a" +checksum = "5b7c7eb2e681ee896646cfb8872b431f24d09f53ba9283289d9b10caa6707088" dependencies = [ "cairo-sys-rs", "gdk4-sys", @@ -1417,9 +1430,9 @@ dependencies = [ [[package]] name = "gtk4" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7181b837f04cbe93f79441475f7a00560a92cba7a72e38cc1a68b6f8b78eaae2" +checksum = "98a0a0466484f64b07b5b8184d43fa46be78eb0b8e04ae4e179af31d770b76d9" dependencies = [ "cairo-rs", "field-offset", @@ -1466,9 +1479,9 @@ dependencies = [ [[package]] name = "gtk4-macros" -version = "0.11.0" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3581b242ba62fdff122ebb626ea641582ec326031622bd19d60f85029c804a87" +checksum = "5ac7179400a36a04de039c24206bb841c5596992b907b43b23ee8d5bdc40d00e" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1478,9 +1491,9 @@ dependencies = [ [[package]] name = "gtk4-sys" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20ba8e695e2640455561274e65e45f0a151619e450746007667f4b23ceae4e1b" +checksum = "82b8f954786af0b1984425c4446b77f5ff6594346181316be3f850caab1c6f01" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -1495,15 +1508,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - [[package]] name = "hashbrown" version = "0.17.1" @@ -1539,7 +1543,7 @@ dependencies = [ "indicatif 0.17.11", "libc", "log", - "rand 0.9.4", + "rand 0.9.5", "serde", "serde_json", "thiserror 2.0.18", @@ -1549,9 +1553,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -1559,9 +1563,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http", @@ -1569,9 +1573,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", @@ -1619,7 +1623,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.7", + "webpki-roots 1.0.9", ] [[package]] @@ -1789,12 +1793,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -1829,9 +1827,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", + "hashbrown", ] [[package]] @@ -1889,13 +1885,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.99" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -1911,12 +1906,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" version = "0.2.186" @@ -1935,9 +1924,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ "libc", ] @@ -1971,9 +1960,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru-slab" @@ -2008,9 +1997,9 @@ dependencies = [ [[package]] name = "matrixmultiply" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7" dependencies = [ "autocfg", "rawpointer", @@ -2018,9 +2007,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memoffset" @@ -2049,9 +2038,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi", @@ -2246,13 +2235,12 @@ checksum = "d7b497d21a8b6fbb4b5a544f8fadb77e801a09ae0add9e411d31c6f89e3c1e90" [[package]] name = "pango" -version = "0.22.6" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "251bdc6e6487b811be0e406a21e301e07e45c0aa8fa39e00c0c8e12a91752438" +checksum = "5d800d8d0de2ad5d0fb046f5344dbaba14a003cf3dd27cc21d85893d35ea316c" dependencies = [ "gio", "glib", - "libc", "pango-sys", ] @@ -2350,7 +2338,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.6", + "rand 0.8.7", ] [[package]] @@ -2401,9 +2389,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" [[package]] name = "portable-atomic-util" @@ -2432,23 +2420,13 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro-crate" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.12+spec-1.1.0", + "toml_edit 0.25.13+spec-1.1.0", ] [[package]] @@ -2462,9 +2440,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -2482,14 +2460,15 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" dependencies = [ "bytes", - "getrandom 0.3.4", + "getrandom 0.4.3", "lru-slab", - "rand 0.9.4", + "rand 0.10.2", + "rand_pcg", "ring", "rustc-hash", "rustls", @@ -2503,23 +2482,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -2538,9 +2517,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -2549,14 +2528,25 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -2595,6 +2585,21 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "rawpointer" version = "0.2.1" @@ -2665,9 +2670,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -2677,9 +2682,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -2688,9 +2693,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" @@ -2727,7 +2732,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.7", + "webpki-roots 1.0.9", ] [[package]] @@ -2760,9 +2765,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -2788,9 +2793,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" dependencies = [ "log", "once_cell", @@ -2803,9 +2808,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "web-time", "zeroize", @@ -2824,9 +2829,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -2932,12 +2937,12 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -2948,7 +2953,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -2979,9 +2984,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "siphasher" @@ -2997,15 +3002,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -3060,9 +3065,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -3098,7 +3103,7 @@ dependencies = [ "cfg-expr", "heck", "pkg-config", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.3+spec-1.1.0", "version-compare", ] @@ -3115,7 +3120,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -3163,9 +3168,9 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] @@ -3182,9 +3187,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -3217,7 +3222,7 @@ dependencies = [ "monostate", "onig", "paste", - "rand 0.9.4", + "rand 0.9.5", "rayon", "rayon-cond", "regex", @@ -3233,9 +3238,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" dependencies = [ "bytes", "libc", @@ -3251,9 +3256,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" dependencies = [ "proc-macro2", "quote", @@ -3284,9 +3289,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" dependencies = [ "indexmap", "serde_core", @@ -3294,7 +3299,7 @@ dependencies = [ "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -3331,14 +3336,14 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.12+spec-1.1.0" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -3347,7 +3352,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -3358,9 +3363,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tower" @@ -3587,11 +3592,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.2" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "wasm-bindgen", ] @@ -3631,27 +3636,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -3662,9 +3658,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.72" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -3672,9 +3668,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3682,9 +3678,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -3695,52 +3691,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-sys" -version = "0.3.99" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -3762,14 +3724,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.7", + "webpki-roots 1.0.9", ] [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -4097,107 +4059,19 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "writeable" version = "0.6.3" @@ -4261,7 +4135,7 @@ dependencies = [ "hex", "nix", "ordered-stream", - "rand 0.8.6", + "rand 0.8.7", "serde", "serde_repr", "sha1", @@ -4302,18 +4176,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.50" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.50" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", @@ -4343,9 +4217,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" @@ -4382,9 +4256,9 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zvariant" diff --git a/Cargo.toml b/Cargo.toml index 53daebd..2790c28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.4.1" +version = "0.5.0" edition = "2021" license = "MIT" authors = ["Breadway"] diff --git a/README.md b/README.md index 6d41ee8..c7797ce 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ 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/ diff --git a/breadpad-shared/Cargo.toml b/breadpad-shared/Cargo.toml index cc2e2f5..da15c41 100644 --- a/breadpad-shared/Cargo.toml +++ b/breadpad-shared/Cargo.toml @@ -26,9 +26,8 @@ regex.workspace = true ureq.workspace = true reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } ical = "0.11" -# TODO(owner): switch to tag-pinned git dependency once bread-onnx/bread-utils are merged and tagged, matching the bread-theme pattern -bread-onnx = { path = "../../bread-ecosystem-fix-worktree/bread-onnx" } -bread-utils = { path = "../../bread-ecosystem-fix-worktree/bread-utils" } +bread-onnx = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.3.0" } +bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.3.0" } [dev-dependencies] tempfile = "3" diff --git a/breadpad/Cargo.toml b/breadpad/Cargo.toml index 4bf9b0b..aa840d2 100644 --- a/breadpad/Cargo.toml +++ b/breadpad/Cargo.toml @@ -23,5 +23,4 @@ gtk4-layer-shell.workspace = true hyprland.workspace = true dirs.workspace = true tokio.workspace = true -# TODO(owner): switch to tag-pinned git dependency once bread-utils is merged and tagged, matching the bread-theme pattern -bread-utils = { path = "../../bread-ecosystem-fix-worktree/bread-utils" } +bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.3.0" } diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index 22511b6..d4d5b93 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -1,11 +1,11 @@ -# Maintainer: Breadway +# Maintainer: Breadway pkgname=breadpad pkgver=0.3.1 pkgrel=1 pkgdesc="Quick-capture scratchpad and note viewer with AI classification" arch=('x86_64') -url="https://github.com/Breadway/breadpad" +url="https://git.breadway.dev/Breadway/breadpad" license=('MIT') # Some Rust deps (ring/mlua) build vendored C/asm into static archives; makepkg's # default -flto=auto emits GCC LTO bitcode the Rust (lld) link cannot read, From 3131cdea1768d06bab213b02f26e6cd392cdbb1f Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 22 Jul 2026 09:58:20 +0800 Subject: [PATCH 10/53] ci: add dev/beta build track workflows Adds dev-release.yml (publishes on every push to dev) and beta-release.yml (publishes on a beta-v* tag), mirroring the pattern landing in bread-ecosystem/bread. See bread-ecosystem/docs/release-channels.md for the three-track policy. --- .forgejo/workflows/beta-release.yml | 56 +++++++++++++++++++++++++ .forgejo/workflows/dev-release.yml | 65 +++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 .forgejo/workflows/beta-release.yml create mode 100644 .forgejo/workflows/dev-release.yml diff --git a/.forgejo/workflows/beta-release.yml b/.forgejo/workflows/beta-release.yml new file mode 100644 index 0000000..7208818 --- /dev/null +++ b/.forgejo/workflows/beta-release.yml @@ -0,0 +1,56 @@ +name: beta release + +# Publishes a beta-track build when a `beta-v*` tag is pushed — +# separate from release.yml's tag-triggered stable releases. See +# bread-ecosystem's docs/release-channels.md for the three-track policy +# this is part of. +on: + push: + tags: ['beta-v*'] + +jobs: + build: + runs-on: [self-hosted, hestia] + steps: + - name: checkout + run: | + set -euo pipefail + rm -rf src && mkdir src + git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src + + - name: build + run: cd src && cargo build --release --locked + + - name: prepare artifacts + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#beta-v}" + PKG_DIR="/srv/breadway-dl/beta/breadpad/${VERSION}" + mkdir -p "${PKG_DIR}" + for bin in breadpad breadman; do + cp "src/target/release/${bin}" "${PKG_DIR}/${bin}-x86_64" + strip "${PKG_DIR}/${bin}-x86_64" + sha256sum "${PKG_DIR}/${bin}-x86_64" | awk '{print $1}' \ + > "${PKG_DIR}/${bin}-x86_64.sha256" + done + cp src/breadpad.example.toml "${PKG_DIR}/" + cp src/bakery.toml "${PKG_DIR}/bakery.toml" + ln -sfn "${VERSION}" "/srv/breadway-dl/beta/breadpad/latest" + + # No GitHub Release upload — beta, like the other non-stable track, + # is only distributed via dl.breadway.dev/beta/. + - name: regenerate beta index.json + env: + MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} + run: | + set -euo pipefail + if [ -z "${MINISIGN_SEC_KEY:-}" ]; then + echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate beta index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone on the beta track)" + exit 1 + fi + rm -rf /tmp/bread-ecosystem-ci + # --branch dev: the TRACK-aware gen-index.sh isn't merged to + # bread-ecosystem's main yet. + git clone --branch dev https://git.breadway.dev/Breadway/bread-ecosystem.git /tmp/bread-ecosystem-ci + TRACK=beta bash /tmp/bread-ecosystem-ci/scripts/gen-index.sh diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml new file mode 100644 index 0000000..693375d --- /dev/null +++ b/.forgejo/workflows/dev-release.yml @@ -0,0 +1,65 @@ +name: dev release + +# Publishes a dev-track build on every push to `dev` — +# separate from release.yml's tag-triggered stable releases. See +# bread-ecosystem's docs/release-channels.md for the three-track policy +# this is part of. +on: + push: + branches: ['dev'] + +jobs: + build: + runs-on: [self-hosted, hestia] + steps: + - name: checkout + run: | + set -euo pipefail + rm -rf src && mkdir src + git clone --branch dev --depth 1 \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src + + - name: build + run: cd src && cargo build --release --locked + + - name: compute dev version + run: | + set -euo pipefail + cd src + CUR="$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"(.*)".*/\1/')" + IFS='.' read -r MA MI PA <<< "${CUR}" + SHA="$(git rev-parse --short HEAD)" + TS="$(date -u +%Y%m%d%H%M%S)" + echo "VERSION=${MA}.${MI}.$((PA + 1))-dev.${TS}+${SHA}" >> "$GITHUB_ENV" + + - name: prepare artifacts + run: | + set -euo pipefail + PKG_DIR="/srv/breadway-dl/dev/breadpad/${VERSION}" + mkdir -p "${PKG_DIR}" + for bin in breadpad breadman; do + cp "src/target/release/${bin}" "${PKG_DIR}/${bin}-x86_64" + strip "${PKG_DIR}/${bin}-x86_64" + sha256sum "${PKG_DIR}/${bin}-x86_64" | awk '{print $1}' \ + > "${PKG_DIR}/${bin}-x86_64.sha256" + done + cp src/breadpad.example.toml "${PKG_DIR}/" + cp src/bakery.toml "${PKG_DIR}/bakery.toml" + ln -sfn "${VERSION}" "/srv/breadway-dl/dev/breadpad/latest" + + # No GitHub Release upload — dev, like the other non-stable track, + # is only distributed via dl.breadway.dev/dev/. + - name: regenerate dev index.json + env: + MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} + run: | + set -euo pipefail + if [ -z "${MINISIGN_SEC_KEY:-}" ]; then + echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate dev index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone on the dev track)" + exit 1 + fi + rm -rf /tmp/bread-ecosystem-ci + # --branch dev: the TRACK-aware gen-index.sh isn't merged to + # bread-ecosystem's main yet. + git clone --branch dev https://git.breadway.dev/Breadway/bread-ecosystem.git /tmp/bread-ecosystem-ci + TRACK=dev bash /tmp/bread-ecosystem-ci/scripts/gen-index.sh From bf55b812d28c07b06bd70f2b5a2af9da7f070c02 Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 22 Jul 2026 10:10:08 +0800 Subject: [PATCH 11/53] ci: retrigger dev-track build now that BAKERY_MINISIGN_SEC_KEY_PATH is set From e43675b6728bfd88f336d06400d60263a7ded755 Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 22 Jul 2026 10:24:28 +0800 Subject: [PATCH 12/53] ci: use a unique temp dir for the bread-ecosystem clone in dev/beta CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fixed /tmp/bread-ecosystem-ci path races when multiple repos' dev/beta workflows run close together on the same self-hosted runner — one job's rm -rf/clone can stomp another's in-progress checkout, causing the regenerate-index step to fail intermittently. Switch to mktemp -d. --- .forgejo/workflows/beta-release.yml | 12 +++++++----- .forgejo/workflows/dev-release.yml | 12 +++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.forgejo/workflows/beta-release.yml b/.forgejo/workflows/beta-release.yml index 7208818..a935fdd 100644 --- a/.forgejo/workflows/beta-release.yml +++ b/.forgejo/workflows/beta-release.yml @@ -49,8 +49,10 @@ jobs: echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate beta index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone on the beta track)" exit 1 fi - rm -rf /tmp/bread-ecosystem-ci - # --branch dev: the TRACK-aware gen-index.sh isn't merged to - # bread-ecosystem's main yet. - git clone --branch dev https://git.breadway.dev/Breadway/bread-ecosystem.git /tmp/bread-ecosystem-ci - TRACK=beta bash /tmp/bread-ecosystem-ci/scripts/gen-index.sh + rm -rf /tmp/bread-ecosystem-ci-* 2>/dev/null || true + # mktemp: a fixed clone path races when multiple repos' dev/beta + # workflows run close together on the same self-hosted runner. + ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)" + git clone --branch dev https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" + TRACK=beta bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh" + rm -rf "${ECOSYSTEM_CI_DIR}" diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml index 693375d..6437846 100644 --- a/.forgejo/workflows/dev-release.yml +++ b/.forgejo/workflows/dev-release.yml @@ -58,8 +58,10 @@ jobs: echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate dev index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone on the dev track)" exit 1 fi - rm -rf /tmp/bread-ecosystem-ci - # --branch dev: the TRACK-aware gen-index.sh isn't merged to - # bread-ecosystem's main yet. - git clone --branch dev https://git.breadway.dev/Breadway/bread-ecosystem.git /tmp/bread-ecosystem-ci - TRACK=dev bash /tmp/bread-ecosystem-ci/scripts/gen-index.sh + rm -rf /tmp/bread-ecosystem-ci-* 2>/dev/null || true + # mktemp: a fixed clone path races when multiple repos' dev/beta + # workflows run close together on the same self-hosted runner. + ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)" + git clone --branch dev https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" + TRACK=dev bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh" + rm -rf "${ECOSYSTEM_CI_DIR}" From 7fb9e018bf7cf7a990426ca3ee002e469e99234c Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 22 Jul 2026 11:56:02 +0800 Subject: [PATCH 13/53] random commit message, read it yourself --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 9168541..4ac0998 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ svgs.txt # Rust/Cargo Cargo.lock dist/ + +# Local hygiene notes (not for commit) +CLAUDE.md From 676c0fedd112a70e2816e7aed5829770eee05349 Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 22 Jul 2026 13:52:27 +0800 Subject: [PATCH 14/53] ci: base dev version on the latest published tag, not Cargo.toml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cargo.toml can drift stale relative to the actual last release (observed on breadbox/breadpad/breadcrumbs/breadpaper), which made the auto-bumped dev version sort as OLDER than what's already installed — bakery's semver check correctly refused those "updates". Deriving the base version from git ls-remote --tags instead is self-healing regardless of Cargo.toml drift, with a Cargo.toml fallback only for a repo with no tags yet. --- .forgejo/workflows/dev-release.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml index 6437846..a5a2d8d 100644 --- a/.forgejo/workflows/dev-release.yml +++ b/.forgejo/workflows/dev-release.yml @@ -26,7 +26,19 @@ jobs: run: | set -euo pipefail cd src - CUR="$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"(.*)".*/\1/')" + # Base the dev version off the latest published stable tag, + # not Cargo.toml — Cargo.toml can go stale relative to the last + # real release (seen in practice: breadbox/breadpad/breadcrumbs/ + # breadpaper), which would make a dev build sort as OLDER than + # what's already installed and bakery would correctly refuse it. + LATEST_TAG="$(git ls-remote --tags --refs \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" 'v*' \ + | awk -F/ '{print $NF}' | sed 's/^v//' | sort -V | tail -1)" + if [ -n "${LATEST_TAG}" ]; then + CUR="${LATEST_TAG}" + else + CUR="$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"(.*)".*/\1/')" + fi IFS='.' read -r MA MI PA <<< "${CUR}" SHA="$(git rev-parse --short HEAD)" TS="$(date -u +%Y%m%d%H%M%S)" From 261431728ec87152303035b55ab8471db741d0c3 Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 22 Jul 2026 18:37:30 +0800 Subject: [PATCH 15/53] ci: make beta a branch-triggered freeze track, not a one-off tag Beta is now a real stabilization branch: publishes on every push to `beta` (mirroring dev's model, auto-versioned X.Y.Z-beta.+, base version from the latest published tag) instead of a manual beta-v* tag. Fixes made during the freeze land via fix/ branches merged into `beta` directly. The gen-index.sh clone for beta pulls bread-ecosystem's default branch (main) rather than pinning to dev, since beta is the more stable track and main now carries the TRACK-aware script. --- .forgejo/workflows/beta-release.yml | 41 ++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/.forgejo/workflows/beta-release.yml b/.forgejo/workflows/beta-release.yml index a935fdd..376dc62 100644 --- a/.forgejo/workflows/beta-release.yml +++ b/.forgejo/workflows/beta-release.yml @@ -1,12 +1,12 @@ name: beta release -# Publishes a beta-track build when a `beta-v*` tag is pushed — -# separate from release.yml's tag-triggered stable releases. See -# bread-ecosystem's docs/release-channels.md for the three-track policy -# this is part of. +# Publishes a beta-track build on every push to `beta` — a frozen +# stabilization branch cut from `dev` when ready to stabilize; only +# fix/ branches merged into `beta` should land here afterward. +# See bread-ecosystem's docs/release-channels.md for the three-track policy. on: push: - tags: ['beta-v*'] + branches: ['beta'] jobs: build: @@ -16,16 +16,37 @@ jobs: run: | set -euo pipefail rm -rf src && mkdir src - git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ + git clone --branch beta --depth 1 \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - name: build run: cd src && cargo build --release --locked + - name: compute beta version + run: | + set -euo pipefail + cd src + # Base the beta version off the latest published stable tag, + # not Cargo.toml — Cargo.toml can go stale relative to the last + # real release (seen in practice: breadbox/breadpad/breadcrumbs/ + # breadpaper), which would make a beta build sort as OLDER than + # what's already installed and bakery would correctly refuse it. + LATEST_TAG="$(git ls-remote --tags --refs \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" 'v*' \ + | awk -F/ '{print $NF}' | sed 's/^v//' | sort -V | tail -1)" + if [ -n "${LATEST_TAG}" ]; then + CUR="${LATEST_TAG}" + else + CUR="$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"(.*)".*/\1/')" + fi + IFS='.' read -r MA MI PA <<< "${CUR}" + SHA="$(git rev-parse --short HEAD)" + TS="$(date -u +%Y%m%d%H%M%S)" + echo "VERSION=${MA}.${MI}.$((PA + 1))-beta.${TS}+${SHA}" >> "$GITHUB_ENV" + - name: prepare artifacts run: | set -euo pipefail - VERSION="${GITHUB_REF_NAME#beta-v}" PKG_DIR="/srv/breadway-dl/beta/breadpad/${VERSION}" mkdir -p "${PKG_DIR}" for bin in breadpad breadman; do @@ -38,8 +59,8 @@ jobs: cp src/bakery.toml "${PKG_DIR}/bakery.toml" ln -sfn "${VERSION}" "/srv/breadway-dl/beta/breadpad/latest" - # No GitHub Release upload — beta, like the other non-stable track, - # is only distributed via dl.breadway.dev/beta/. + # No GitHub Release upload — beta, like dev, is only distributed via + # dl.breadway.dev/beta/. - name: regenerate beta index.json env: MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} @@ -53,6 +74,6 @@ jobs: # mktemp: a fixed clone path races when multiple repos' dev/beta # workflows run close together on the same self-hosted runner. ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)" - git clone --branch dev https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" + git clone https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" TRACK=beta bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh" rm -rf "${ECOSYSTEM_CI_DIR}" From f13e091bd0a019742c6dfaf384e476ce7b8eebbf Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 22 Jul 2026 19:40:56 +0800 Subject: [PATCH 16/53] docs: add CONTRIBUTING.md Documents the dev/beta/main branch and release-track workflow shared across the bread ecosystem. See bread-ecosystem's docs/release-channels.md for the full policy this implements. --- CONTRIBUTING.md | 91 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..61c7a90 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,91 @@ +# Contributing + +`breadpad` — Quick-capture scratchpad and note viewer (breadman) with AI classification. + +Part of the bread ecosystem; this repo follows the same branch/release +workflow as every other ecosystem product. + +## Branches + +- **`main`** — release branch, always tag-ready. Nothing is committed to it + directly; it only moves forward via a `beta` merge (see below). +- **`dev`** — integration branch. All day-to-day work lands here first. + Every push to `dev` automatically builds and publishes a **dev-track** + build (see Tracks below) — use this to test your change in a real install + before it goes any further. +- **`beta`** — a frozen stabilization branch, cut from `dev` periodically. + Every push to `beta` automatically builds and publishes a **beta-track** + build. While a freeze is active, only fixes for issues found *in that + freeze* should land on `beta`. + +New work — features and bug fixes alike — goes on a short-lived branch: + +``` +feature/ +fix/ +``` + +Branch off `dev`, open a PR/push back into `dev` when ready. If you're fixing +something reported against an active `beta` freeze, branch off `beta` +instead, merge the fix there to unblock testers, and also forward the same +fix into `dev` so it doesn't quietly reappear next cycle. + +## The release cycle + +1. Work accumulates on `dev` via `feature/x` / `fix/x` branches. Each push + auto-publishes a dev build — install it with `bakery track set dev` and + `bakery update --all`, then report or fix anything broken with another + push to `dev`. +2. Once `dev` has gone roughly **a week** without new issues, `beta` is cut + fresh from `dev`'s current tip. This freezes it as the stabilization + target — `dev` keeps moving independently starting the next cycle. +3. `beta` is open for anyone to test: `bakery track set beta` and + `bakery update --all`. **File issues against anything you find on this + repo's Forgejo issue tracker.** Fixes land via `fix/` branches + merged into `beta`. +4. Once `beta` has gone roughly **a month** without new issues, it's merged + into `main` and tagged `vX.Y.Z` — that tag is what actually triggers the + stable release build. `beta` is then reset from `dev` to start the next + cycle. + +## Tracks, from a user's perspective + +``` +bakery track show # what you're currently on (defaults to stable) +bakery track set dev # or beta, or stable +bakery update --all # pull the latest build on your current track +``` + +| Track | What it is | Published from | +|--------|-----------|-----------------| +| `stable` | The last tagged release | `main`, on a `vX.Y.Z` tag push | +| `beta` | Current stabilization freeze | `beta`, on every push | +| `dev` | Bleeding edge | `dev`, on every push | + +Dev/beta versions are auto-computed (`X.Y.Z-dev.+` / +`-beta.…`) from the latest published stable tag, so they always sort as +newer than what you have installed — no manual version bumping needed when +pushing to `dev` or `beta`. + +## Local development + +```sh +cargo build --release --workspace +cargo test --release --workspace +``` + +## CI + +- `dev-release.yml` — triggered on push to `dev`. +- `beta-release.yml` — triggered on push to `beta`. +- `release.yml` — triggered on a `v*` tag push, cuts the actual stable release. +- `package.yml` — publishes to the `[breadway]` pacman repo, also tag-triggered. + +All CI runs on a self-hosted runner; nothing runs automatically on plain +commits or PRs beyond the track builds above. See +[bread-ecosystem's docs/release-channels.md](https://git.breadway.dev/Breadway/bread-ecosystem/src/branch/main/docs/release-channels.md) +for the full policy, including how a new product gets wired onto these tracks. + +## Questions + +Open an issue on this repo's Forgejo tracker. From 1289c05c4799bff2070017ce9e55adca534a982d Mon Sep 17 00:00:00 2001 From: Breadway Date: Thu, 23 Jul 2026 10:25:12 +0800 Subject: [PATCH 17/53] Drop pacman packaging, bakery-only distribution bakery already fully covers what the PKGBUILD provided (binary, systemd --user service where applicable, dependency declarations) except a LICENSE copy, which bakery.toml's new license_file field now closes. Removes packaging/arch/ and .forgejo/workflows/package.yml; adds the LICENSE artifact to each release/dev-release/beta-release workflow's prepare step. Not pacman-installed inside BOS today (BOS already consumes these apps exclusively via build-local.sh's skel-staging), so this only removes the option to `pacman -S` outside of BOS/bakery. --- .forgejo/workflows/beta-release.yml | 1 + .forgejo/workflows/dev-release.yml | 1 + .forgejo/workflows/package.yml | 40 ------------------- .forgejo/workflows/release.yml | 59 +++++++++++++++++++++++++++++ bakery.toml | 1 + packaging/arch/PKGBUILD | 38 ------------------- 6 files changed, 62 insertions(+), 78 deletions(-) delete mode 100644 .forgejo/workflows/package.yml create mode 100644 .forgejo/workflows/release.yml delete mode 100644 packaging/arch/PKGBUILD diff --git a/.forgejo/workflows/beta-release.yml b/.forgejo/workflows/beta-release.yml index 376dc62..5107391 100644 --- a/.forgejo/workflows/beta-release.yml +++ b/.forgejo/workflows/beta-release.yml @@ -56,6 +56,7 @@ jobs: > "${PKG_DIR}/${bin}-x86_64.sha256" done cp src/breadpad.example.toml "${PKG_DIR}/" + cp src/LICENSE "${PKG_DIR}/" cp src/bakery.toml "${PKG_DIR}/bakery.toml" ln -sfn "${VERSION}" "/srv/breadway-dl/beta/breadpad/latest" diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml index a5a2d8d..6da2f3e 100644 --- a/.forgejo/workflows/dev-release.yml +++ b/.forgejo/workflows/dev-release.yml @@ -56,6 +56,7 @@ jobs: > "${PKG_DIR}/${bin}-x86_64.sha256" done cp src/breadpad.example.toml "${PKG_DIR}/" + cp src/LICENSE "${PKG_DIR}/" cp src/bakery.toml "${PKG_DIR}/bakery.toml" ln -sfn "${VERSION}" "/srv/breadway-dl/dev/breadpad/latest" diff --git a/.forgejo/workflows/package.yml b/.forgejo/workflows/package.yml deleted file mode 100644 index fdfdef9..0000000 --- a/.forgejo/workflows/package.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Build and publish package - -on: - push: - tags: ['v*'] - -jobs: - package: - runs-on: [self-hosted, hestia] - container: - image: archlinux:latest - steps: - # Note: no actions/checkout — the archlinux image has no Node, which JS - # actions require. Everything runs as shell steps and clones manually. - - name: Build and publish - env: - PUBLISH_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - run: | - set -euo pipefail - VERSION="${GITHUB_REF_NAME#v}" - pacman -Syu --noconfirm base-devel git rust cargo gtk4 gtk4-layer-shell - useradd -m builder - git config --global --add safe.directory '*' - git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ - "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /home/builder/src - cd /home/builder/src - git archive --format=tar.gz --prefix="breadpad-${VERSION}/" HEAD \ - > packaging/arch/breadpad-${VERSION}.tar.gz - SHA=$(sha256sum packaging/arch/breadpad-${VERSION}.tar.gz | awk '{print $1}') - sed -i "s/^pkgver=.*/pkgver=${VERSION}/" packaging/arch/PKGBUILD - sed -i "s/^sha256sums=.*/sha256sums=('${SHA}')/" packaging/arch/PKGBUILD - chown -R builder:builder /home/builder/src - # --nocheck: packaging builds the artifact; tests belong in a CI job. - su builder -c "cd /home/builder/src/packaging/arch && makepkg -f --noconfirm --nocheck" - PKG=$(find /home/builder/src/packaging/arch -name '*.pkg.tar.zst' | head -1) - curl -fsS -X PUT \ - -H "Authorization: token ${PUBLISH_TOKEN}" \ - -H "Content-Type: application/octet-stream" \ - --data-binary "@${PKG}" \ - "https://git.breadway.dev/api/packages/Breadway/arch/os" diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 0000000..03340ac --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,59 @@ +name: release + +on: + push: + tags: ["v*"] + +jobs: + build: + runs-on: [self-hosted, hestia] + steps: + - name: checkout + run: | + set -euo pipefail + rm -rf src && mkdir src + git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src + + - name: build + run: cd src && cargo build --release --locked + + - name: prepare artifacts + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + PKG_DIR="/srv/breadway-dl/breadpad/${VERSION}" + mkdir -p "${PKG_DIR}" + for bin in breadpad breadman; do + cp "src/target/release/${bin}" "${PKG_DIR}/${bin}-x86_64" + strip "${PKG_DIR}/${bin}-x86_64" + sha256sum "${PKG_DIR}/${bin}-x86_64" | awk '{print $1}' \ + > "${PKG_DIR}/${bin}-x86_64.sha256" + done + cp src/breadpad.example.toml "${PKG_DIR}/" + cp src/LICENSE "${PKG_DIR}/" + cp src/bakery.toml "${PKG_DIR}/bakery.toml" + ln -sfn "${VERSION}" "/srv/breadway-dl/breadpad/latest" + + - name: regenerate index.json + run: | + set -euo pipefail + rm -rf /tmp/bread-ecosystem-ci + git clone https://git.breadway.dev/Breadway/bread-ecosystem.git /tmp/bread-ecosystem-ci + bash /tmp/bread-ecosystem-ci/scripts/gen-index.sh + + - name: upload to GitHub Release + env: + GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }} + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + PKG_DIR="/srv/breadway-dl/breadpad/${VERSION}" + gh release create "${GITHUB_REF_NAME}" --repo Breadway/breadpad \ + --title "breadpad v${VERSION}" --generate-notes 2>/dev/null || true + gh release upload "${GITHUB_REF_NAME}" --repo Breadway/breadpad \ + "${PKG_DIR}/breadpad-x86_64" \ + "${PKG_DIR}/breadman-x86_64" \ + "${PKG_DIR}/breadpad-x86_64.sha256" \ + "${PKG_DIR}/breadman-x86_64.sha256" \ + --clobber diff --git a/bakery.toml b/bakery.toml index 25e9033..f7a339e 100644 --- a/bakery.toml +++ b/bakery.toml @@ -4,6 +4,7 @@ binaries = ["breadpad", "breadman"] system_deps = ["gtk4", "gtk4-layer-shell"] optional_system_deps = ["rocm-hip-runtime", "ollama", "hyprland"] bread_deps = [] +license_file = "LICENSE" [config] dir = "~/.config/breadpad" diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD deleted file mode 100644 index 22511b6..0000000 --- a/packaging/arch/PKGBUILD +++ /dev/null @@ -1,38 +0,0 @@ -# Maintainer: Breadway - -pkgname=breadpad -pkgver=0.3.1 -pkgrel=1 -pkgdesc="Quick-capture scratchpad and note viewer with AI classification" -arch=('x86_64') -url="https://github.com/Breadway/breadpad" -license=('MIT') -# Some Rust deps (ring/mlua) build vendored C/asm into static archives; makepkg's -# default -flto=auto emits GCC LTO bitcode the Rust (lld) link cannot read, -# causing undefined-symbol errors. Disable LTO. -options=(!lto !debug) -depends=('gtk4' 'gtk4-layer-shell') -optdepends=( - 'ollama: local AI note classification' - 'hyprland: scratchpad window integration' -) -makedepends=('rust' 'cargo') -source=("${pkgname}-${pkgver}.tar.gz") -sha256sums=('SKIP') - -build() { - cd "${srcdir}/${pkgname}-${pkgver}" - cargo build --release --locked -} - -check() { - cd "${srcdir}/${pkgname}-${pkgver}" - cargo test --release --locked --workspace -} - -package() { - cd "${srcdir}/${pkgname}-${pkgver}" - install -Dm755 target/release/breadpad "${pkgdir}/usr/bin/breadpad" - install -Dm755 target/release/breadman "${pkgdir}/usr/bin/breadman" - install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" -} From 879ec1e7f4a4f251e6fcfd8c39daeace2d98fb08 Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 29 Jul 2026 11:43:55 +0800 Subject: [PATCH 18/53] breadpad: add --screenshot CLI mode for automated capture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same reasoning as breadbar/breadbox/breadclip/breadsearch, but plumbed through breadpad's own hand-rolled arg parser (mod args) instead of clap — breadpad doesn't use clap at all, and adding a second, separate clap parser alongside the existing one would reject every one of breadpad's real flags (--type, --no-classify, --status, fire, calendar, ...) the moment it saw one clap didn't recognize. --screenshot/--output/--width/ --height are just three more fields on the same Args struct. Only the "popup" view (the compose window) is wired up — the reminder window needs a real stored Note to render, not worth fabricating one just for a screenshot pass. Also fixes the same single-instance footgun as the other apps: without NON_UNIQUE, a screenshot run would just toggle-close the operator's real already-open popup instead of getting its own window. --- Cargo.lock | 21 +++++++++ breadpad/Cargo.toml | 2 + breadpad/src/main.rs | 87 ++++++++++++++++++++++++++++++++++---- breadpad/src/screenshot.rs | 65 ++++++++++++++++++++++++++++ 4 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 breadpad/src/screenshot.rs diff --git a/Cargo.lock b/Cargo.lock index 756f0c3..cee96f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -302,6 +302,16 @@ dependencies = [ "piper", ] +[[package]] +name = "bread-screenshots" +version = "0.3.1" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#1a3475bd2358202f60e29c9bd27d06b1428b1a27" +dependencies = [ + "anyhow", + "bread-utils", + "tracing", +] + [[package]] name = "bread-theme" version = "0.2.3" @@ -313,6 +323,16 @@ dependencies = [ "serde_json", ] +[[package]] +name = "bread-utils" +version = "0.3.1" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#1a3475bd2358202f60e29c9bd27d06b1428b1a27" +dependencies = [ + "dirs 5.0.1", + "serde", + "serde_json", +] + [[package]] name = "breadman" version = "0.3.4" @@ -335,6 +355,7 @@ name = "breadpad" version = "0.3.4" dependencies = [ "anyhow", + "bread-screenshots", "breadpad-shared", "chrono", "dirs 5.0.1", diff --git a/breadpad/Cargo.toml b/breadpad/Cargo.toml index c835bb9..4f53160 100644 --- a/breadpad/Cargo.toml +++ b/breadpad/Cargo.toml @@ -11,6 +11,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 = "dev" } anyhow.workspace = true ort.workspace = true tracing.workspace = true diff --git a/breadpad/src/main.rs b/breadpad/src/main.rs index aa924d0..4e83f1e 100644 --- a/breadpad/src/main.rs +++ b/breadpad/src/main.rs @@ -13,6 +13,8 @@ use std::cell::RefCell; use std::rc::Rc; use std::sync::{Arc, Once}; +mod screenshot; + static ORT_INIT: Once = Once::new(); fn init_ort_once(cfg: &Config) { @@ -41,6 +43,29 @@ mod args { pub model_info: bool, pub calendar_test: bool, pub calendar_list_uid: Option, + pub screenshot: Option, + pub output: Option, + pub width: u32, + pub height: u32, + } + + impl Args { + /// `None` for a normal run. Exits the process with an error if + /// `--screenshot` was given without `--output`, before any GTK + /// setup happens. + pub fn screenshot_request(&self) -> Option { + let view = self.screenshot.clone()?; + let Some(output) = self.output.clone() else { + eprintln!("breadpad: --screenshot requires --output"); + std::process::exit(1); + }; + Some(crate::screenshot::ScreenshotRequest { + view, + output: output.into(), + width: self.width, + height: self.height, + }) + } } pub fn parse() -> Args { @@ -53,6 +78,10 @@ mod args { model_info: false, calendar_test: false, calendar_list_uid: None, + screenshot: None, + output: None, + width: 1920, + height: 1080, }; let raw: Vec = std::env::args().skip(1).collect(); let mut i = 0; @@ -82,6 +111,26 @@ mod args { _ => {} } } + "--screenshot" => { + i += 1; + args.screenshot = raw.get(i).cloned(); + } + "--output" => { + i += 1; + args.output = raw.get(i).cloned(); + } + "--width" => { + i += 1; + if let Some(v) = raw.get(i).and_then(|s| s.parse().ok()) { + args.width = v; + } + } + "--height" => { + i += 1; + if let Some(v) = raw.get(i).and_then(|s| s.parse().ok()) { + args.height = v; + } + } _ => {} } i += 1; @@ -120,7 +169,8 @@ fn main() -> Result<()> { return cmd_calendar_list_uid(¬e_id, &cfg); } - run_popup(args.note_type, args.no_classify, cfg) + let screenshot_req = args.screenshot_request(); + run_popup(args.note_type, args.no_classify, cfg, screenshot_req) } fn cmd_status(cfg: &Config) -> Result<()> { @@ -510,22 +560,36 @@ fn build_reminder_window( window.present(); } -fn run_popup(preset_type: Option, no_classify: bool, cfg: Config) -> Result<()> { +fn run_popup( + preset_type: Option, + no_classify: bool, + cfg: Config, + screenshot_req: Option, +) -> Result<()> { // Try to get current Hyprland workspace let workspace = get_active_workspace(); - let app = gtk4::Application::builder() - .application_id("com.breadway.breadpad") - .build(); + let mut builder = gtk4::Application::builder().application_id("com.breadway.breadpad"); + if screenshot_req.is_some() { + // GApplication is single-instance by default; this machine typically + // already has a real breadpad instance, so without this a + // screenshot run would just toggle-close the *existing* instance's + // window instead of starting a fresh one that ever sees + // `screenshot_req` (see the `app.windows().first()` toggle below). + builder = builder.flags(gtk4::gio::ApplicationFlags::NON_UNIQUE); + } + let app = builder.build(); let cfg = Arc::new(cfg); app.connect_activate(move |app| { - if let Some(win) = app.windows().first().cloned() { - win.close(); - return; + if screenshot_req.is_none() { + if let Some(win) = app.windows().first().cloned() { + win.close(); + return; + } } - build_window(app, cfg.clone(), workspace.clone(), preset_type.clone(), no_classify); + build_window(app, cfg.clone(), workspace.clone(), preset_type.clone(), no_classify, screenshot_req.clone()); }); let code = app.run_with_args::(&[]); @@ -551,6 +615,7 @@ fn build_window( workspace: Option, preset_type: Option, no_classify: bool, + screenshot_req: Option, ) { let window = gtk4::ApplicationWindow::builder() .application(app) @@ -710,6 +775,10 @@ fn build_window( }); window.add_controller(key_ctrl); + if let Some(req) = screenshot_req { + screenshot::dispatch(&window, req); + } + window.present(); entry.grab_focus(); } diff --git a/breadpad/src/screenshot.rs b/breadpad/src/screenshot.rs new file mode 100644 index 0000000..e88679f --- /dev/null +++ b/breadpad/src/screenshot.rs @@ -0,0 +1,65 @@ +//! `--screenshot` CLI mode: render breadpad's compose popup, capture it via +//! `bread-screenshots`, then exit — driven by `bread-ecosystem`'s +//! `bread-capture` orchestrator, or run standalone for one-off captures. +//! +//! No clap here (unlike breadbar/breadbox/breadclip/breadsearch): breadpad +//! already has its own small hand-rolled flag parser (`mod args`) covering +//! `--type`/`--no-classify`/`--status`/`fire`/`calendar`/etc, and clap's +//! default "reject unknown flags" behavior would break every one of those +//! if bolted on as a second, separate parser. `--screenshot`/`--output`/ +//! `--width`/`--height` are just three more fields on that same `Args` +//! struct instead. +//! +//! Only the "popup" view (the compose window from `run_popup`) is wired up +//! — the reminder window (`run_reminder_window`, reached via `fire `) +//! needs a real stored `Note` to render, which isn't worth fabricating for +//! a screenshot pass. + +use gtk4::prelude::*; +use std::path::PathBuf; +use std::time::Duration; + +/// Extra settle time after `map` for the first frame to actually paint +/// before grim runs — `map` fires once the surface exists, not once +/// anything has been drawn into it. +const SETTLE_DELAY: Duration = Duration::from_millis(300); + +#[derive(Clone)] +pub struct ScreenshotRequest { + pub view: String, + pub output: PathBuf, + pub width: u32, + pub height: u32, +} + +/// Wire up the given view's screenshot sequence against an already-built, +/// not-yet-presented window. Every path here ends by exiting the process — +/// it never returns control to the normal popup UI. +pub fn dispatch(window: >k4::ApplicationWindow, req: ScreenshotRequest) { + match req.view.as_str() { + "popup" => { + let output = req.output; + let (width, height) = (req.width as i32, req.height as i32); + window.connect_map(move |_| { + let output = output.clone(); + gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || { + finish(bread_screenshots::capture_region(0, 0, width, height, &output)); + }); + }); + } + other => { + eprintln!("breadpad: unknown screenshot view '{other}' (known: popup)"); + std::process::exit(1); + } + } +} + +fn finish(result: anyhow::Result<()>) { + match result { + Ok(()) => std::process::exit(0), + Err(e) => { + eprintln!("breadpad: screenshot capture failed: {e}"); + std::process::exit(1); + } + } +} From c13b7b0599cb42238bd21ae1c5f85967e1c5f640 Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 29 Jul 2026 16:37:01 +0800 Subject: [PATCH 19/53] breadman: add --screenshot CLI mode for automated capture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same reasoning as breadpad, extending breadman's own hand-rolled arg parser rather than adding clap. --screenshot doubles as the view selector — breadman already supports opening directly to a named stack page via --view/initial_view, so this just feeds that same mechanism instead of needing a separate one. Full known-size canvas capture, plain top-level window (not layer-shell). Also fixes the same NON_UNIQUE footgun as breadpad/breadbox/etc. --- Cargo.lock | 1 + breadman/Cargo.toml | 2 + breadman/src/main.rs | 77 +++++++++++++++++++++++++++++++++++--- breadman/src/screenshot.rs | 58 ++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 breadman/src/screenshot.rs diff --git a/Cargo.lock b/Cargo.lock index cee96f6..d34275d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -338,6 +338,7 @@ name = "breadman" version = "0.3.4" dependencies = [ "anyhow", + "bread-screenshots", "breadpad-shared", "chrono", "dirs 5.0.1", diff --git a/breadman/Cargo.toml b/breadman/Cargo.toml index 577f684..3808281 100644 --- a/breadman/Cargo.toml +++ b/breadman/Cargo.toml @@ -11,6 +11,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 = "dev" } anyhow.workspace = true tracing.workspace = true tracing-subscriber.workspace = true diff --git a/breadman/src/main.rs b/breadman/src/main.rs index d46557a..e165a9e 100644 --- a/breadman/src/main.rs +++ b/breadman/src/main.rs @@ -13,6 +13,7 @@ use std::rc::Rc; use std::sync::Arc; mod editor; +mod screenshot; mod views; // ── Args ───────────────────────────────────────────────────────────────────── @@ -23,6 +24,29 @@ mod args { pub view: Option, pub done_id: Option, pub upcoming_plain: bool, + pub screenshot: Option, + pub output: Option, + pub width: u32, + pub height: u32, + } + + impl Args { + /// `None` for a normal run. Exits the process with an error if + /// `--screenshot` was given without `--output`, before any GTK + /// setup happens. + pub fn screenshot_request(&self) -> Option { + let view = self.screenshot.clone()?; + let Some(output) = self.output.clone() else { + eprintln!("breadman: --screenshot requires --output"); + std::process::exit(1); + }; + Some(crate::screenshot::ScreenshotRequest { + view, + output: output.into(), + width: self.width, + height: self.height, + }) + } } pub fn parse() -> Args { @@ -30,6 +54,10 @@ mod args { view: None, done_id: None, upcoming_plain: false, + screenshot: None, + output: None, + width: 1920, + height: 1080, }; let raw: Vec = std::env::args().skip(1).collect(); let mut i = 0; @@ -50,6 +78,26 @@ mod args { } args.view = Some("upcoming".into()); } + "--screenshot" => { + i += 1; + args.screenshot = raw.get(i).cloned(); + } + "--output" => { + i += 1; + args.output = raw.get(i).cloned(); + } + "--width" => { + i += 1; + if let Some(v) = raw.get(i).and_then(|s| s.parse().ok()) { + args.width = v; + } + } + "--height" => { + i += 1; + if let Some(v) = raw.get(i).and_then(|s| s.parse().ok()) { + args.height = v; + } + } _ => {} } i += 1; @@ -214,7 +262,9 @@ fn main() -> Result<()> { return cmd_upcoming_plain(); } - run_app(args.view, cfg) + let screenshot_req = args.screenshot_request(); + let view = screenshot_req.as_ref().map(|r| r.view.clone()).or(args.view); + run_app(view, cfg, screenshot_req) } fn cmd_done(id: &str) -> Result<()> { @@ -250,10 +300,20 @@ fn cmd_upcoming_plain() -> Result<()> { Ok(()) } -fn run_app(initial_view: Option, cfg: Config) -> Result<()> { - let app = gtk4::Application::builder() - .application_id("com.breadway.breadman") - .build(); +fn run_app( + initial_view: Option, + cfg: Config, + screenshot_req: Option, +) -> Result<()> { + let mut builder = gtk4::Application::builder().application_id("com.breadway.breadman"); + if screenshot_req.is_some() { + // GApplication is single-instance by default; this machine typically + // already has a real breadman instance, so without this a + // screenshot run would activate the *existing* instance instead of + // starting a fresh one that ever sees `screenshot_req`. + builder = builder.flags(gtk4::gio::ApplicationFlags::NON_UNIQUE); + } + let app = builder.build(); let cfg = Arc::new(cfg); let initial_view = Arc::new(initial_view); @@ -261,7 +321,7 @@ fn run_app(initial_view: Option, cfg: Config) -> Result<()> { app.connect_activate(move |app| { let cfg = cfg.as_ref().clone(); let initial_view = initial_view.as_deref().map(|s| s.to_string()); - if let Err(e) = build_app_window(app, cfg, initial_view) { + if let Err(e) = build_app_window(app, cfg, initial_view, screenshot_req.clone()) { tracing::error!("failed to build window: {}", e); } }); @@ -279,6 +339,7 @@ fn build_app_window( app: >k4::Application, cfg: Config, initial_view: Option, + screenshot_req: Option, ) -> Result<()> { apply_css(&cfg); @@ -465,6 +526,10 @@ fn build_app_window( } stack.set_visible_child_name(initial); + if let Some(req) = screenshot_req { + screenshot::dispatch(&window, req); + } + window.present(); Ok(()) } diff --git a/breadman/src/screenshot.rs b/breadman/src/screenshot.rs new file mode 100644 index 0000000..66da64a --- /dev/null +++ b/breadman/src/screenshot.rs @@ -0,0 +1,58 @@ +//! `--screenshot` CLI mode: render the named view, capture it via +//! `bread-screenshots`, then exit — driven by `bread-ecosystem`'s +//! `bread-capture` orchestrator, or run standalone for one-off captures. +//! +//! No clap here, same reasoning as breadpad: extends breadman's own +//! hand-rolled `mod args` instead of bolting on a second parser that would +//! reject its real flags (`--view`, `done`, `upcoming --plain`). +//! +//! `--screenshot ` doubles as the view selector — it's passed through +//! as `initial_view` (the same field `--view` already sets) rather than +//! needing a separate mechanism, since breadman already supports opening +//! directly to a named stack page. + +use gtk4::prelude::*; +use std::path::PathBuf; +use std::time::Duration; + +/// Extra settle time after `map` for the first frame to actually paint +/// before grim runs — `map` fires once the surface exists, not once +/// anything has been drawn into it. +const SETTLE_DELAY: Duration = Duration::from_millis(300); + +#[derive(Clone)] +pub struct ScreenshotRequest { + pub view: String, + pub output: PathBuf, + pub width: u32, + pub height: u32, +} + +/// Wire up the given view's screenshot sequence against an already-built, +/// not-yet-presented window. Every path here ends by exiting the process — +/// it never returns control to the normal note-manager UI. +/// +/// Unlike the other apps' `dispatch`, this doesn't validate `req.view` +/// against a known-views list — an invalid name just falls through to +/// breadman's own `unwrap_or("all")` default (see `build_app_window`), +/// same as `--view` already behaves for a normal run. +pub fn dispatch(window: >k4::ApplicationWindow, req: ScreenshotRequest) { + let output = req.output; + let (width, height) = (req.width as i32, req.height as i32); + window.connect_map(move |_| { + let output = output.clone(); + gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || { + finish(bread_screenshots::capture_region(0, 0, width, height, &output)); + }); + }); +} + +fn finish(result: anyhow::Result<()>) { + match result { + Ok(()) => std::process::exit(0), + Err(e) => { + eprintln!("breadman: screenshot capture failed: {e}"); + std::process::exit(1); + } + } +} From 77b402833f7c7d94d42d462de743c496e7749e4a Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 29 Jul 2026 17:27:05 +0800 Subject: [PATCH 20/53] breadman: capture every stack page + the editor and new-note windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --view (and now --screenshot) already opened any named stack page directly, so the other 9 pages (upcoming/todo/reminder/idea/note/ question/archive/settings/errors) needed zero new code — just registry entries in bread-capture. Two more real windows needed actual wiring: the per-note editor popover (editor::build_editor_popover, normally only reachable via a specific note card's edit button — screenshot mode calls the builder directly against the first real note in the store instead, since there's no button handle to synthesize a click on) and the "New Note" modal (show_add_note_window, same on_build-hook pattern already used for breadbar's wifi add-network dialog). The editor popover's first attempt parented it to the whole window, which put it at GTK4 Popover's default Top position — entirely above the window, clipped off the canvas in every capture despite mapping successfully (no error, just invisible). Anchoring to a real button (new_note_btn) with an explicit Bottom position fixed it. --- breadman/src/main.rs | 7 +-- breadman/src/screenshot.rs | 98 +++++++++++++++++++++++++++++++++++--- 2 files changed, 96 insertions(+), 9 deletions(-) diff --git a/breadman/src/main.rs b/breadman/src/main.rs index e165a9e..a32583c 100644 --- a/breadman/src/main.rs +++ b/breadman/src/main.rs @@ -506,7 +506,7 @@ fn build_app_window( let state_c = state.clone(); let window_c = window.clone(); new_note_btn.connect_clicked(move |_| { - show_add_note_window(&window_c, state_c.clone()); + show_add_note_window(&window_c, state_c.clone(), |_| {}); }); } @@ -527,7 +527,7 @@ fn build_app_window( stack.set_visible_child_name(initial); if let Some(req) = screenshot_req { - screenshot::dispatch(&window, req); + screenshot::dispatch(&window, req, state.clone(), new_note_btn.clone()); } window.present(); @@ -796,7 +796,7 @@ fn build_note_card(note: &Note, state: AppState) -> gtk4::Box { // ── Add note window ─────────────────────────────────────────────────────────── -fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState) { +fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState, on_build: impl FnOnce(>k4::Window)) { let win = gtk4::Window::builder() .title("New Note") .transient_for(parent) @@ -981,6 +981,7 @@ fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState) { }); } + on_build(&win); win.present(); body_entry.grab_focus(); } diff --git a/breadman/src/screenshot.rs b/breadman/src/screenshot.rs index 66da64a..78c2bff 100644 --- a/breadman/src/screenshot.rs +++ b/breadman/src/screenshot.rs @@ -6,13 +6,25 @@ //! hand-rolled `mod args` instead of bolting on a second parser that would //! reject its real flags (`--view`, `done`, `upcoming --plain`). //! -//! `--screenshot ` doubles as the view selector — it's passed through -//! as `initial_view` (the same field `--view` already sets) rather than +//! `--screenshot ` doubles as the view selector for every named stack +//! page ("all", "upcoming", "todo", ...) — it's passed through as +//! `initial_view` (the same field `--view` already sets) rather than //! needing a separate mechanism, since breadman already supports opening //! directly to a named stack page. +//! +//! One view isn't a stack page at all: "editor" opens the per-note editor +//! popover (`editor::build_editor_popover`), normally only reachable by +//! clicking a real note card's edit button. Screenshot mode calls the same +//! builder function directly against the first real note in the store +//! (bypassing the button/click-handler entirely — there's no clean way to +//! synthesize a click on a button that only ever existed as a local inside +//! `build_note_card`, never stored anywhere else), with no-op save/delete/ +//! error callbacks since nothing here should actually persist a change. use gtk4::prelude::*; use std::path::PathBuf; +use std::rc::Rc; +use std::sync::Arc; use std::time::Duration; /// Extra settle time after `map` for the first frame to actually paint @@ -20,6 +32,11 @@ use std::time::Duration; /// anything has been drawn into it. const SETTLE_DELAY: Duration = Duration::from_millis(300); +/// Delay before popping the editor popover open — same reasoning as every +/// other app's PRE_POPUP_DELAY: the parent window's own layout needs a beat +/// to settle first. +const PRE_POPUP_DELAY: Duration = Duration::from_millis(300); + #[derive(Clone)] pub struct ScreenshotRequest { pub view: String, @@ -33,12 +50,81 @@ pub struct ScreenshotRequest { /// it never returns control to the normal note-manager UI. /// /// Unlike the other apps' `dispatch`, this doesn't validate `req.view` -/// against a known-views list — an invalid name just falls through to -/// breadman's own `unwrap_or("all")` default (see `build_app_window`), -/// same as `--view` already behaves for a normal run. -pub fn dispatch(window: >k4::ApplicationWindow, req: ScreenshotRequest) { +/// against a known-views list for the stack-page case — an invalid name +/// just falls through to breadman's own `unwrap_or("all")` default (see +/// `build_app_window`), same as `--view` already behaves for a normal run. +pub fn dispatch( + window: >k4::ApplicationWindow, + req: ScreenshotRequest, + state: crate::AppState, + editor_anchor: gtk4::Button, +) { let output = req.output; let (width, height) = (req.width as i32, req.height as i32); + + if req.view == "new-note" { + window.connect_map(move |root| { + let output = output.clone(); + let root = root.clone(); + let state = state.clone(); + gtk4::glib::timeout_add_local_once(PRE_POPUP_DELAY, move || { + crate::show_add_note_window(&root, state, move |dialog| { + let output = output.clone(); + dialog.connect_map(move |_| { + let output = output.clone(); + gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || { + finish(bread_screenshots::capture_region(0, 0, width, height, &output)); + }); + }); + }); + }); + }); + return; + } + + if req.view == "editor" { + window.connect_map(move |_| { + let output = output.clone(); + let state = state.clone(); + let editor_anchor = editor_anchor.clone(); + gtk4::glib::timeout_add_local_once(PRE_POPUP_DELAY, move || { + let Some(note) = state.notes.borrow().first().cloned() else { + eprintln!("breadman: no notes in the store to build the editor view from"); + std::process::exit(1); + }; + let morning = state.cfg.borrow().reminders.default_morning.clone(); + let store = Arc::new(state.write_store()); + let popover = crate::editor::build_editor_popover( + ¬e, + store, + morning, + Rc::new(|_| {}), + Rc::new(|| {}), + Rc::new(|_| {}), + ); + popover.set_parent(&editor_anchor); + // Parenting to the whole window (rather than a small, + // concretely-placed widget like the real edit-button call + // site does) left the popover positioned above the window + // entirely (GTK4's default Popover position is Top) — off + // the top of the canvas and clipped out of every capture. + // Anchoring to a real button plus an explicit Bottom + // position keeps it inside the visible canvas. + popover.set_position(gtk4::PositionType::Bottom); + popover.set_autohide(false); + let output = output.clone(); + popover.connect_map(move |_| { + let output = output.clone(); + gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || { + finish(bread_screenshots::capture_region(0, 0, width, height, &output)); + }); + }); + popover.popup(); + }); + }); + return; + } + window.connect_map(move |_| { let output = output.clone(); gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || { From 47a4613b5af578541cdf621bfbd4d878fc4c1152 Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 29 Jul 2026 21:32:36 +0800 Subject: [PATCH 21/53] breadpad: capture the reminder window + its snooze popover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two more views: "reminder" (the alert window from run_reminder_window, normally only reachable via a real due note through `fire `) and "reminder-snooze" (same window, snooze popover open). Screenshot mode builds a throwaway sample Note directly instead of going through cmd_fire's Store lookup — main() checks for these two view names before ever reaching run_popup's dispatch, since the reminder window is a completely separate GApplication entry point, not reachable through the compose-popup path screenshot mode already covered. Same NON_UNIQUE-on-screenshot-mode fix as run_popup already had, applied to the reminder window's own Application builder. --- breadpad/src/main.rs | 46 +++++++++++++++++++++++++---- breadpad/src/screenshot.rs | 59 ++++++++++++++++++++++++++++++++++---- 2 files changed, 94 insertions(+), 11 deletions(-) diff --git a/breadpad/src/main.rs b/breadpad/src/main.rs index 4e83f1e..7ba8c87 100644 --- a/breadpad/src/main.rs +++ b/breadpad/src/main.rs @@ -170,6 +170,21 @@ fn main() -> Result<()> { } let screenshot_req = args.screenshot_request(); + if let Some(req) = &screenshot_req { + if req.view == "reminder" || req.view == "reminder-snooze" { + // The real path (`fire `, above) needs a real due note from + // the Store. A screenshot doesn't have one to work with — and + // shouldn't wait for one — so it builds a throwaway sample + // instead, never touching the Store at all. + let mut sample = Note::new( + "Sample reminder text".into(), + NoteType::from_str("reminder"), + None, + ); + sample.time = Some(chrono::Utc::now()); + return run_reminder_window(sample, &cfg, screenshot_req); + } + } run_popup(args.note_type, args.no_classify, cfg, screenshot_req) } @@ -326,19 +341,28 @@ fn cmd_fire(id: &str, cfg: &Config) -> Result<()> { } } - run_reminder_window(note, cfg) + run_reminder_window(note, cfg, None) } -fn run_reminder_window(note: breadpad_shared::types::Note, cfg: &Config) -> Result<()> { - let app = gtk4::Application::builder() - .application_id("com.breadway.breadpad.reminder") - .build(); +fn run_reminder_window( + note: breadpad_shared::types::Note, + cfg: &Config, + screenshot_req: Option, +) -> Result<()> { + let mut builder = gtk4::Application::builder().application_id("com.breadway.breadpad.reminder"); + if screenshot_req.is_some() { + // Same reasoning as run_popup's NON_UNIQUE: a screenshot run must + // get its own fresh window, never activate a real reminder that + // happens to already be showing. + builder = builder.flags(gtk4::gio::ApplicationFlags::NON_UNIQUE); + } + let app = builder.build(); let note = Arc::new(note); let cfg = Arc::new(cfg.clone()); app.connect_activate(move |app| { - build_reminder_window(app, note.clone(), cfg.clone()); + build_reminder_window(app, note.clone(), cfg.clone(), screenshot_req.clone()); }); app.run_with_args::(&[]); @@ -381,6 +405,7 @@ fn build_reminder_window( app: >k4::Application, note: Arc, cfg: Arc, + screenshot_req: Option, ) { let window = gtk4::ApplicationWindow::builder() .application(app) @@ -557,6 +582,15 @@ fn build_reminder_window( outer.append(&btn_row); window.set_child(Some(&outer)); + + if let Some(req) = screenshot_req { + if req.view == "reminder-snooze" { + screenshot::capture_with_snooze_open(&window, &req, snooze_popover.clone()); + } else { + screenshot::capture_window(&window, &req); + } + } + window.present(); } diff --git a/breadpad/src/screenshot.rs b/breadpad/src/screenshot.rs index e88679f..c90873e 100644 --- a/breadpad/src/screenshot.rs +++ b/breadpad/src/screenshot.rs @@ -10,10 +10,13 @@ //! `--width`/`--height` are just three more fields on that same `Args` //! struct instead. //! -//! Only the "popup" view (the compose window from `run_popup`) is wired up -//! — the reminder window (`run_reminder_window`, reached via `fire `) -//! needs a real stored `Note` to render, which isn't worth fabricating for -//! a screenshot pass. +//! Three views: "popup" (the compose window from `run_popup`), "reminder" +//! (the alert window from `run_reminder_window`/`build_reminder_window`, +//! normally only reachable via a real due note through `fire `, built +//! here against a fabricated sample `Note` instead — see `main`'s +//! `screenshot_req.view == "reminder"` branch, which skips the Store lookup +//! entirely), and "reminder-snooze" (the same window with its snooze +//! popover open). use gtk4::prelude::*; use std::path::PathBuf; @@ -24,6 +27,11 @@ use std::time::Duration; /// anything has been drawn into it. const SETTLE_DELAY: Duration = Duration::from_millis(300); +/// Delay before popping the snooze popover open — same reasoning as every +/// other app's PRE_POPUP_DELAY: the parent window's own layout needs a beat +/// to settle first. +const PRE_POPUP_DELAY: Duration = Duration::from_millis(300); + #[derive(Clone)] pub struct ScreenshotRequest { pub view: String, @@ -48,12 +56,53 @@ pub fn dispatch(window: >k4::ApplicationWindow, req: ScreenshotRequest) { }); } other => { - eprintln!("breadpad: unknown screenshot view '{other}' (known: popup)"); + eprintln!("breadpad: unknown screenshot view '{other}' (known: popup, reminder, reminder-snooze)"); std::process::exit(1); } } } +/// Same shape as `dispatch`'s "popup" arm, for the reminder window itself +/// (view "reminder") — pulled out since `build_reminder_window` calls this +/// directly rather than going through `dispatch` (the reminder window is +/// built via a completely separate `run_reminder_window` entry point, not +/// `run_popup`'s). +pub fn capture_window(window: >k4::ApplicationWindow, req: &ScreenshotRequest) { + let output = req.output.clone(); + let (width, height) = (req.width as i32, req.height as i32); + window.connect_map(move |_| { + let output = output.clone(); + gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || { + finish(bread_screenshots::capture_region(0, 0, width, height, &output)); + }); + }); +} + +/// View "reminder-snooze": force the snooze popover open shortly after the +/// window maps, then capture once *it* maps. +pub fn capture_with_snooze_open( + window: >k4::ApplicationWindow, + req: &ScreenshotRequest, + snooze_popover: gtk4::Popover, +) { + let output = req.output.clone(); + let (width, height) = (req.width as i32, req.height as i32); + let popover_to_open = snooze_popover.clone(); + window.connect_map(move |_| { + popover_to_open.set_autohide(false); + let popover_to_open = popover_to_open.clone(); + gtk4::glib::timeout_add_local_once(PRE_POPUP_DELAY, move || { + popover_to_open.popup(); + }); + }); + snooze_popover.connect_map(move |_| { + let output = output.clone(); + gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || { + finish(bread_screenshots::capture_region(0, 0, width, height, &output)); + }); + }); +} + fn finish(result: anyhow::Result<()>) { match result { Ok(()) => std::process::exit(0), From b85d6827214118e69a51a234063ca55534a33f01 Mon Sep 17 00:00:00 2001 From: Breadway Date: Thu, 30 Jul 2026 18:39:56 +0800 Subject: [PATCH 22/53] breadman: rebuild settings with libadwaita components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Proof-of-concept for the ecosystem-wide design-system work: every setting is now an AdwPreferencesGroup/SwitchRow/SpinRow/ActionRow/ComboRow via bread-theme::adw, instead of a hand-rolled GtkGrid of labels + raw widgets. This directly fixes the two worst defects a design critique found here: - Toggle switches rendered ~1400px wide, like progress bars — caused by GtkGrid sizing a switch's column to match a sibling entry's hexpand in the same grid, and Switch has no default that resists filling that width. AdwSwitchRow owns its own layout, so this class of bug can't recur. - Spin buttons were ~1300px from the label they modified, for the same grid-column-sizing reason. AdwSpinRow docks the spinner to its title. Also softened the jargon-forward section titles the critique flagged ("Model (Tier 2 ONNX)", "Ollama (Tier 3)") — the tier/implementation detail moved from the title (loud, unavoidable) to each group's description (quieter, still there for anyone who wants it). bread-theme now needs a direct dependency here (branch=dev, `adw` feature) since breadpad-shared's own bread-theme dependency is tag-pinned to a release before this feature existed. --- Cargo.lock | 70 +++++++-- breadman/Cargo.toml | 5 + breadman/src/main.rs | 4 + breadman/src/views/settings.rs | 253 ++++++++++++++++----------------- 4 files changed, 189 insertions(+), 143 deletions(-) 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); -} From de7965edaed488a02067d69580779af4f7ae33f6 Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 31 Jul 2026 07:18:03 +0800 Subject: [PATCH 23/53] breadman: address design-review findings (settings, note list, editor) Settings screen: - Widen the AdwClamp to 900px, left-aligned to the note-list gutter, instead of PreferencesPage's own ~600px clamp that made the screen read as a narrower, bolted-on app. - Remove the floating Save button; every control now applies instantly (switches/combo/spin on change, entry rows via show_apply_button), matching what AdwSwitchRow's own design language already implies. - Replace ActionRow+suffix-Entry with real AdwEntryRow/AdwPasswordEntryRow so fields are row-width instead of sized by leftover label space. - Default type now uses the same pill-row widget as the New Note dialog and editor, instead of a fourth type-picker variant. - Local Classifier group description notes the paths are shared with breadpad. Note list (All/Upcoming/Todo/Idea/Note/Question): - Replaced the two-line card (huge dead gap between title and actions) with Archive's tighter single-line row layout, shared via the new views::row module. Unifies the edit affordance (pencil in both active and archive rows) and drops the type badge in views already filtered to one type. - Type badges now tint per-type (matching the existing note-card-{type} accent-bar colors) instead of one flat cream fill for every type. - Empty states are centered with type-specific copy and a "+ New X" action instead of top-anchored generic text. - Search bar is now scoped to note-list views (was rendering uselessly on Settings/Errors). - Archive sort bug: it was sorting by `created` while displaying `completed` ("done {date}"), which is why rows could appear out of order. Now sorts by `completed`. - Sidebar and row-action icons are now real GTK symbolic icons instead of a mix of emoji, Unicode glyphs, and thin monochrome characters. Editor: converted from a bare GtkPopover (no scrim, no title, anchored wherever the triggering button happened to be) to an AdwDialog with AdwEntryRow fields, a shared header bar, and copy/placeholders unified with the New Note dialog. Delete uses destructive-action styling. Also: destructive-action buttons (breadman's row/editor delete, and breadpad-shared's .danger-btn) now use a hardcoded red instead of pywal's @red - a blue-toned wallpaper's "red" palette slot can itself render blue, which made delete indistinguishable from confirm/accent buttons regardless of which CSS class was already applied. --- Cargo.lock | 8 +- breadman/src/editor.rs | 176 ++++++++-------- breadman/src/main.rs | 354 +++++++-------------------------- breadman/src/screenshot.rs | 37 ++-- breadman/src/views/archive.rs | 97 ++------- breadman/src/views/mod.rs | 1 + breadman/src/views/row.rs | 267 +++++++++++++++++++++++++ breadman/src/views/settings.rs | 289 +++++++++++++++------------ breadman/src/views/upcoming.rs | 63 ++---- breadpad-shared/src/theme.rs | 17 +- 10 files changed, 667 insertions(+), 642 deletions(-) create mode 100644 breadman/src/views/row.rs diff --git a/Cargo.lock b/Cargo.lock index a110a12..1152693 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -962,7 +962,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1273,7 +1273,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2767,7 +2767,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3091,7 +3091,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/breadman/src/editor.rs b/breadman/src/editor.rs index a01dfa5..bebf15d 100644 --- a/breadman/src/editor.rs +++ b/breadman/src/editor.rs @@ -1,3 +1,9 @@ +//! Note editor, presented as an AdwDialog (was a bare GtkPopover with no +//! scrim, no title, anchored wherever the triggering button happened to be - +//! 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, @@ -6,48 +12,69 @@ use breadpad_shared::{ }; use chrono::{Local, TimeZone, Utc}; use gtk4::{glib, prelude::*}; +use libadwaita::prelude::*; use std::cell::RefCell; use std::rc::Rc; use std::sync::Arc; -pub fn build_editor_popover( +/// Same wording used by `main::show_add_note_window`'s New Note dialog - the +/// two surfaces used to teach the user two different input languages for +/// the same fields. +pub const TIME_PLACEHOLDER: &str = "tomorrow 9am / at 7pm / 2026-08-01 09:00"; +pub const RRULE_PLACEHOLDER: &str = "RRULE:FREQ=WEEKLY;BYDAY=MO"; + +pub fn open_editor( + parent: >k4::Widget, note: &Note, store: Arc, morning: String, on_save: Rc, on_delete: Rc, on_error: Rc, -) -> gtk4::Popover { - let popover = gtk4::Popover::new(); - popover.set_has_arrow(false); +) -> libadwaita::Dialog { + let dialog = libadwaita::Dialog::builder() + .title("Edit Note") + .content_width(480) + .build(); - let vbox = gtk4::Box::builder() + let header = libadwaita::HeaderBar::new(); + let toolbar_view = libadwaita::ToolbarView::new(); + toolbar_view.add_top_bar(&header); + + let content = gtk4::Box::builder() .orientation(gtk4::Orientation::Vertical) - .spacing(8) - .margin_top(12) - .margin_bottom(12) - .margin_start(12) - .margin_end(12) - .width_request(420) + .spacing(16) + .margin_top(16) + .margin_bottom(16) + .margin_start(16) + .margin_end(16) .build(); - vbox.append(>k4::Label::builder().label("Body").xalign(0.0).build()); - let body_entry = gtk4::Entry::builder() - .text(¬e.body) - .hexpand(true) - .build(); - vbox.append(&body_entry); + let group = adw::preferences_group("Details", None); - vbox.append(>k4::Label::builder().label("Type").xalign(0.0).build()); - let type_combo = gtk4::DropDown::from_strings(NoteType::all_builtin()); - let current_idx = NoteType::all_builtin() - .iter() - .position(|&s| s == note.note_type.as_str()) - .unwrap_or(3) as u32; - type_combo.set_selected(current_idx); - vbox.append(&type_combo); + 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_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(); + for (btn, name) in &type_pills { + bread_theme::gtk::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); + }); + type_pill_box.append(btn); + } + type_row.add_suffix(&type_pill_box); + group.add(&type_row); - vbox.append(>k4::Label::builder().label("Time").xalign(0.0).build()); let time_text = note .time .map(|t| { @@ -55,38 +82,42 @@ pub fn build_editor_popover( local.format("%Y-%m-%d %H:%M").to_string() }) .unwrap_or_default(); - let time_entry = gtk4::Entry::builder() - .text(&time_text) - .placeholder_text("YYYY-MM-DD HH:MM or tomorrow 9am (blank = no time)") - .hexpand(true) - .build(); - vbox.append(&time_entry); + let time_row = libadwaita::EntryRow::builder().title("Time").build(); + time_row.set_text(&time_text); + // EntryRow has no placeholder-text property of its own (unlike GtkEntry) - + // the title already communicates the field, so the example format goes in + // the group description instead of a placeholder that would otherwise + // vanish behind the title when empty. + group.add(&time_row); - vbox.append(>k4::Label::builder().label("Recurrence").xalign(0.0).build()); - let rrule_entry = gtk4::Entry::builder() - .text(note.rrule.as_ref().map(|r| r.as_str()).unwrap_or("")) - .placeholder_text("RRULE:FREQ=WEEKLY;BYDAY=MO (blank = none)") - .build(); - vbox.append(&rrule_entry); + let rrule_row = libadwaita::EntryRow::builder().title("Recurrence").build(); + rrule_row.set_text(note.rrule.as_ref().map(|r| r.as_str()).unwrap_or("")); + group.add(&rrule_row); - // Button row: [Delete] [Save] - let btn_row = gtk4::Box::builder() - .orientation(gtk4::Orientation::Horizontal) - .spacing(8) - .build(); + content.append(&group); - let delete_btn = gtk4::Button::builder() - .label("🗑 Delete") - .css_classes(["danger-btn"]) - .build(); - let save_btn = gtk4::Button::builder() - .label("Save") - .css_classes(["confirm-button"]) - .hexpand(true) + let hint = gtk4::Label::builder() + .label(format!("Time: {TIME_PLACEHOLDER}\nRecurrence: {RRULE_PLACEHOLDER}")) + .css_classes(["dim-label"]) + .xalign(0.0) + .wrap(true) .build(); + content.append(&hint); + + let btn_row = gtk4::Box::builder().orientation(gtk4::Orientation::Horizontal).spacing(8).build(); + let delete_btn = gtk4::Button::builder().label("Delete").css_classes(["destructive-action"]).build(); + let save_btn = gtk4::Button::builder().label("Save").css_classes(["confirm-button"]).hexpand(true).build(); btn_row.append(&delete_btn); btn_row.append(&save_btn); - vbox.append(&btn_row); + content.append(&btn_row); + + let scroll = gtk4::ScrolledWindow::builder() + .hscrollbar_policy(gtk4::PolicyType::Never) + .vscrollbar_policy(gtk4::PolicyType::Automatic) + .build(); + scroll.set_child(Some(&content)); + toolbar_view.set_content(Some(&scroll)); + dialog.set_child(Some(&toolbar_view)); // Delete: two-click confirm let confirming = Rc::new(RefCell::new(false)); @@ -95,7 +126,7 @@ pub fn build_editor_popover( let delete_btn_label = delete_btn.clone(); let note_id = note.id.clone(); let store_del = store.clone(); - let popover_del = popover.clone(); + let dialog_del = dialog.clone(); let on_delete = Rc::clone(&on_delete); let on_error = Rc::clone(&on_error); @@ -105,7 +136,7 @@ pub fn build_editor_popover( let id = note_id.clone(); let on_delete = Rc::clone(&on_delete); let on_error = Rc::clone(&on_error); - let popover = popover_del.clone(); + let dialog = dialog_del.clone(); spawn_bg( move || -> anyhow::Result<()> { store.delete_note(&id)?; @@ -119,7 +150,7 @@ pub fn build_editor_popover( Ok(()) => on_delete(), Err(e) => on_error(format!("delete failed: {}", e)), } - popover.popdown(); + dialog.close(); }, ); } else { @@ -132,33 +163,20 @@ pub fn build_editor_popover( // Save { let note_clone = note.clone(); - let popover_save = popover.clone(); + let dialog_save = dialog.clone(); let on_error = Rc::clone(&on_error); + let selected_type = selected_type.clone(); save_btn.connect_clicked(move |_| { - // Read all field values on the main thread before handing off. let mut updated = note_clone.clone(); - updated.body = body_entry.text().to_string(); - updated.note_type = NoteType::from_str( - NoteType::all_builtin() - .get(type_combo.selected() as usize) - .copied() - .unwrap_or("note"), - ); - let time_str = time_entry.text().to_string(); - updated.time = if time_str.trim().is_empty() { - None - } else { - parse_time_field(&time_str, &morning) - }; - let rrule_text = rrule_entry.text().to_string(); - updated.rrule = if rrule_text.trim().is_empty() { - None - } else { - Some(RecurrenceRule::new(rrule_text)) - }; + updated.body = body_row.text().to_string(); + updated.note_type = NoteType::from_str(&selected_type.borrow()); + let time_str = time_row.text().to_string(); + updated.time = if time_str.trim().is_empty() { None } else { parse_time_field(&time_str, &morning) }; + let rrule_text = rrule_row.text().to_string(); + updated.rrule = if rrule_text.trim().is_empty() { None } else { Some(RecurrenceRule::new(rrule_text)) }; - popover_save.popdown(); + dialog_save.close(); let store_bg = store.clone(); let on_save = Rc::clone(&on_save); @@ -182,8 +200,8 @@ pub fn build_editor_popover( }); } - popover.set_child(Some(&vbox)); - popover + dialog.present(Some(parent)); + dialog } fn spawn_bg(work: F, then: C) diff --git a/breadman/src/main.rs b/breadman/src/main.rs index baf0611..7e07a4e 100644 --- a/breadman/src/main.rs +++ b/breadman/src/main.rs @@ -117,10 +117,11 @@ struct AppState { errors: Rc, String)>>>, active_view: Rc>, stack: gtk4::Stack, + window: gtk4::ApplicationWindow, } impl AppState { - fn new(store: Arc, notes: Vec, cfg: Config, stack: gtk4::Stack) -> Self { + fn new(store: Arc, notes: Vec, cfg: Config, stack: gtk4::Stack, window: gtk4::ApplicationWindow) -> Self { AppState { store, notes: Rc::new(RefCell::new(notes)), @@ -128,6 +129,7 @@ impl AppState { errors: Rc::new(RefCell::new(Vec::new())), active_view: Rc::new(RefCell::new("all".to_string())), stack, + window, } } @@ -193,7 +195,7 @@ fn rebuild_all_view(notes: &[Note], state: &AppState) { if let Some(child) = state.stack.child_by_name("all") { state.stack.remove(&child); } - let scroll = build_note_list(notes, state.clone()); + let scroll = build_note_list(notes, state.clone(), true, "No notes yet — jot something down to get started.", Some(NoteType::Note)); state.stack.add_named(&scroll, Some("all")); } @@ -207,14 +209,22 @@ fn rebuild_stack(state: &AppState) { let errors: Vec<_> = state.errors.borrow().clone(); // All - let all_scroll = build_note_list(¬es, state.clone()); + let all_scroll = build_note_list(¬es, state.clone(), true, "No notes yet — jot something down to get started.", Some(NoteType::Note)); state.stack.add_named(&all_scroll, Some("all")); // Upcoming - let upcoming = views::upcoming::build(¬es); + let upcoming = views::upcoming::build(¬es, state.clone()); state.stack.add_named(&upcoming, Some("upcoming")); // Per-type + let empty_text = |type_name: &str| match type_name { + "todo" => "No todos yet.", + "reminder" => "No reminders yet.", + "idea" => "No ideas captured yet.", + "note" => "No notes yet.", + "question" => "No open questions yet.", + _ => "Nothing here yet.", + }; for type_name in NoteType::all_builtin() { let nt = NoteType::from_str(type_name); let filtered: Vec = notes @@ -222,7 +232,7 @@ fn rebuild_stack(state: &AppState) { .filter(|n| n.note_type == nt && !n.done) .cloned() .collect(); - let scroll = build_note_list(&filtered, state.clone()); + let scroll = build_note_list(&filtered, state.clone(), false, empty_text(type_name), Some(nt)); state.stack.add_named(&scroll, Some(type_name)); } @@ -396,7 +406,11 @@ fn build_app_window( )); row }; - let make_item = |id: &str, icon: &str, label: &str| { + // One icon language throughout (was 8 full-colour emoji + 2 thin + // monochrome glyphs for Settings/Errors + a third style on row action + // buttons) — every sidebar entry and row action now uses a real GTK + // symbolic icon. + let make_item = |id: &str, icon_name: &str, label: &str| { let row = gtk4::ListBoxRow::builder() .css_classes(["sidebar-row"]) .build(); @@ -405,13 +419,7 @@ fn build_app_window( .orientation(gtk4::Orientation::Horizontal) .spacing(10) .build(); - hbox.append( - >k4::Label::builder() - .label(icon) - .width_chars(2) - .xalign(0.5) - .build(), - ); + hbox.append(>k4::Image::builder().icon_name(icon_name).pixel_size(16).build()); hbox.append( >k4::Label::builder() .label(label) @@ -424,18 +432,18 @@ fn build_app_window( }; sidebar_list.append(&make_section("VIEWS")); - sidebar_list.append(&make_item("all", "📋", "All")); - sidebar_list.append(&make_item("upcoming", "📅", "Upcoming")); + sidebar_list.append(&make_item("all", "view-list-symbolic", "All")); + sidebar_list.append(&make_item("upcoming", "x-office-calendar-symbolic", "Upcoming")); sidebar_list.append(&make_section("TYPES")); - sidebar_list.append(&make_item("todo", "✅", "Todo")); - sidebar_list.append(&make_item("reminder", "🔔", "Reminder")); - sidebar_list.append(&make_item("idea", "💡", "Idea")); - sidebar_list.append(&make_item("note", "📝", "Note")); - sidebar_list.append(&make_item("question", "❓", "Question")); + sidebar_list.append(&make_item("todo", "task-due-symbolic", "Todo")); + sidebar_list.append(&make_item("reminder", "appointment-soon-symbolic", "Reminder")); + sidebar_list.append(&make_item("idea", "emblem-important-symbolic", "Idea")); + sidebar_list.append(&make_item("note", "text-x-generic-symbolic", "Note")); + sidebar_list.append(&make_item("question", "dialog-question-symbolic", "Question")); sidebar_list.append(&make_section("MORE")); - sidebar_list.append(&make_item("archive", "📦", "Archive")); - sidebar_list.append(&make_item("settings", "⚙", "Settings")); - sidebar_list.append(&make_item("errors", "⚠", "Errors")); + sidebar_list.append(&make_item("archive", "folder-symbolic", "Archive")); + sidebar_list.append(&make_item("settings", "preferences-system-symbolic", "Settings")); + sidebar_list.append(&make_item("errors", "dialog-warning-symbolic", "Errors")); sidebar_vbox.append(&sidebar_list); // ── Content area ────────────────────────────────────────────── @@ -466,7 +474,7 @@ fn build_app_window( window.set_child(Some(&hbox)); // ── AppState ────────────────────────────────────────────────── - let state = AppState::new(store, notes, cfg, stack.clone()); + let state = AppState::new(store, notes, cfg, stack.clone(), window.clone()); // Initial build rebuild_stack(&state); @@ -474,11 +482,15 @@ fn build_app_window( // ── Sidebar selection ───────────────────────────────────────── { let state_c = state.clone(); + let search_entry_c = search_entry.clone(); sidebar_list.connect_row_selected(move |_, row| { if let Some(row) = row { let view = row.widget_name().to_string(); if view.is_empty() { return; } *state_c.active_view.borrow_mut() = view.clone(); + // The search bar only means anything on note-list views — + // it used to render (uselessly) on Settings and Errors too. + search_entry_c.set_visible(!matches!(view.as_str(), "settings" | "errors")); refresh(&state_c); } }); @@ -510,13 +522,14 @@ fn build_app_window( let state_c = state.clone(); let window_c = window.clone(); new_note_btn.connect_clicked(move |_| { - show_add_note_window(&window_c, state_c.clone(), |_| {}); + show_add_note_window(&window_c, state_c.clone(), NoteType::Note, |_| {}); }); } // ── Select initial view ─────────────────────────────────────── let initial = initial_view.as_deref().unwrap_or("all"); *state.active_view.borrow_mut() = initial.to_string(); + search_entry.set_visible(!matches!(initial, "settings" | "errors")); for row in sidebar_list .observe_children() .snapshot() @@ -531,16 +544,27 @@ fn build_app_window( stack.set_visible_child_name(initial); if let Some(req) = screenshot_req { - screenshot::dispatch(&window, req, state.clone(), new_note_btn.clone()); + screenshot::dispatch(&window, req, state.clone()); } window.present(); Ok(()) } -// ── Note list & cards ───────────────────────────────────────────────────────── +// ── Note list ───────────────────────────────────────────────────────────────── +// Row rendering itself lives in views::row (shared with Upcoming/Archive) — +// design review found the old two-line card here wasted enormous horizontal +// space (title and actions separated by ~800-1000px of dead middle) compared +// to Archive's tighter single-line layout, so all list views now share one +// row template. -fn build_note_list(notes: &[Note], state: AppState) -> gtk4::ScrolledWindow { +fn build_note_list( + notes: &[Note], + state: AppState, + show_type_badge: bool, + empty_text: &str, + empty_new_type: Option, +) -> gtk4::ScrolledWindow { let scroll = gtk4::ScrolledWindow::builder() .hscrollbar_policy(gtk4::PolicyType::Never) .vscrollbar_policy(gtk4::PolicyType::Automatic) @@ -549,26 +573,25 @@ fn build_note_list(notes: &[Note], state: AppState) -> gtk4::ScrolledWindow { let list = gtk4::Box::builder() .orientation(gtk4::Orientation::Vertical) - .spacing(8) - .margin_top(12) - .margin_bottom(12) - .margin_start(12) - .margin_end(12) + .spacing(4) + .margin_top(8) + .margin_bottom(8) .build(); let mut sorted: Vec = notes.iter().filter(|n| !n.done).cloned().collect(); sorted.sort_by(|a, b| b.created.cmp(&a.created)); if sorted.is_empty() { - list.append( - >k4::Label::builder() - .label("No notes here yet.") - .margin_top(32) - .build(), - ); + let action = empty_new_type.map(|nt| views::row::new_note_action(nt, state.window.clone(), state.clone())); + list.append(&views::row::build_empty_state("view-list-symbolic", empty_text, action)); } else { for note in &sorted { - list.append(&build_note_card(note, state.clone())); + let created_str = { + let local: chrono::DateTime = note.created.into(); + local.format("%b %d %H:%M").to_string() + }; + let spec = views::row::RowSpec { date_label: created_str, note, show_type_badge, show_done: true }; + list.append(&views::row::build(spec, state.clone())); } } @@ -576,231 +599,9 @@ fn build_note_list(notes: &[Note], state: AppState) -> gtk4::ScrolledWindow { scroll } -fn build_note_card(note: &Note, state: AppState) -> gtk4::Box { - let card = gtk4::Box::builder() - .orientation(gtk4::Orientation::Vertical) - .spacing(8) - .margin_start(0) - .margin_end(0) - .margin_top(0) - .margin_bottom(0) - .css_classes(["note-card"]) - .build(); - card.add_css_class(&format!("note-card-{}", note.note_type.as_str())); - - // Top row: body + type chip - let top_row = gtk4::Box::builder() - .orientation(gtk4::Orientation::Horizontal) - .spacing(8) - .build(); - - let body_label = gtk4::Label::builder() - .label(¬e.body) - .hexpand(true) - .xalign(0.0) - .wrap(true) - .build(); - - let type_chip = gtk4::Label::builder() - .label(note.note_type.as_str()) - .css_classes(["type-chip"]) - .build(); - - top_row.append(&body_label); - top_row.append(&type_chip); - - // Bottom row: metadata + action buttons - let bottom_row = gtk4::Box::builder() - .orientation(gtk4::Orientation::Horizontal) - .spacing(8) - .build(); - - let created_str = { - let local: chrono::DateTime = note.created.into(); - local.format("%b %d %H:%M").to_string() - }; - let meta_label = gtk4::Label::builder() - .label(&created_str) - .css_classes(["dim-label"]) - .xalign(0.0) - .build(); - - // Date first, then chips - bottom_row.append(&meta_label); - if let Some(ws) = ¬e.workspace { - bottom_row.append( - >k4::Label::builder() - .label(&format!("ws:{}", ws)) - .css_classes(["type-chip"]) - .build(), - ); - } - if let Some(t) = note.time { - let local: chrono::DateTime = t.into(); - bottom_row.append( - >k4::Label::builder() - .label(&local.format("⏰ %b %d %H:%M").to_string()) - .css_classes(["dim-label"]) - .build(), - ); - } - if note.rrule.is_some() { - bottom_row.append( - >k4::Label::builder() - .label("↻") - .css_classes(["type-chip"]) - .build(), - ); - } - - bottom_row.append(>k4::Box::builder().hexpand(true).build()); - - // ✓ Done button - let done_btn = gtk4::Button::builder() - .label("✓") - .css_classes(["action-btn", "done-btn"]) - .tooltip_text("Mark done") - .build(); - { - let note_id = note.id.clone(); - let card_c = card.clone(); - let state_c = state.clone(); - done_btn.connect_clicked(move |_| { - card_c.set_visible(false); // optimistic hide - let store = state_c.write_store(); - let id = note_id.clone(); - let state = state_c.clone(); - spawn_bg( - move || -> anyhow::Result> { - if let Some(mut n) = store.get_by_id(&id)? { - n.mark_done(); - store.update_note(&n)?; - } - store.load_all() - }, - move |result| { - match result { - Ok(fresh) => { - *state.notes.borrow_mut() = fresh; - rebuild_stack(&state); - let active = state.active_view.borrow().clone(); - state.stack.set_visible_child_name(&active); - } - Err(e) => state.log_error(format!("mark done failed: {}", e)), - } - }, - ); - }); - } - bottom_row.append(&done_btn); - - // ✎ Edit button - let edit_btn = gtk4::Button::builder() - .label("✎") - .css_classes(["action-btn", "edit-btn"]) - .tooltip_text("Edit") - .build(); - { - let note_c = note.clone(); - let state_c = state.clone(); - let body_label_c = body_label.clone(); - let card_c = card.clone(); - - edit_btn.connect_clicked(move |btn| { - let morning = state_c.cfg.borrow().reminders.default_morning.clone(); - let store = Arc::new(state_c.write_store()); - - let state_save = state_c.clone(); - let body_label_save = body_label_c.clone(); - let state_del = state_c.clone(); - let card_del = card_c.clone(); - let state_err = state_c.clone(); - - let popover = editor::build_editor_popover( - ¬e_c, - store, - morning, - Rc::new(move |updated: Note| { - body_label_save.set_label(&updated.body); - state_save.reload_notes(); - rebuild_stack(&state_save); - let active = state_save.active_view.borrow().clone(); - state_save.stack.set_visible_child_name(&active); - }), - Rc::new(move || { - card_del.set_visible(false); - state_del.reload_notes(); - rebuild_stack(&state_del); - let active = state_del.active_view.borrow().clone(); - state_del.stack.set_visible_child_name(&active); - }), - Rc::new(move |e: String| { - state_err.log_error(e); - }), - ); - popover.set_parent(btn); - popover.popup(); - }); - } - bottom_row.append(&edit_btn); - - // 🗑 Delete button — two-click confirm: first click → "Sure?", second → delete - let delete_btn = gtk4::Button::builder() - .label("🗑") - .css_classes(["action-btn", "danger-btn"]) - .tooltip_text("Delete") - .build(); - { - use std::cell::RefCell; - use std::rc::Rc; - let confirming = Rc::new(RefCell::new(false)); - let note_id = note.id.clone(); - let card_c = card.clone(); - let state_c = state.clone(); - let btn_c = delete_btn.clone(); - - delete_btn.connect_clicked(move |_| { - if *confirming.borrow() { - card_c.set_visible(false); // optimistic hide - let store = state_c.write_store(); - let id = note_id.clone(); - let state = state_c.clone(); - spawn_bg( - move || -> anyhow::Result> { - store.delete_note(&id)?; - if let Err(e) = Scheduler::cancel(&id) { - tracing::warn!("failed to cancel timer for {}: {}", id, e); - } - store.load_all() - }, - move |result| { - match result { - Ok(fresh) => { - *state.notes.borrow_mut() = fresh; - rebuild_stack(&state); - let active = state.active_view.borrow().clone(); - state.stack.set_visible_child_name(&active); - } - Err(e) => state.log_error(format!("delete failed: {}", e)), - } - }, - ); - } else { - *confirming.borrow_mut() = true; - btn_c.set_label("Sure?"); - } - }); - } - bottom_row.append(&delete_btn); - - card.append(&top_row); - card.append(&bottom_row); - card -} - // ── Add note window ─────────────────────────────────────────────────────────── -fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState, on_build: impl FnOnce(>k4::Window)) { +fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState, preselect: NoteType, on_build: impl FnOnce(>k4::Window)) { let win = gtk4::Window::builder() .title("New Note") .transient_for(parent) @@ -824,48 +625,43 @@ fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState, on_bu .build(); vbox.append(&body_entry); - // Type chips + // 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. + vbox.append(>k4::Label::builder().label("Type").xalign(0.0).build()); let chip_box = gtk4::Box::builder() .orientation(gtk4::Orientation::Horizontal) .spacing(4) .build(); - let selected_type: Rc> = Rc::new(RefCell::new(NoteType::Note)); + let selected_type: Rc> = Rc::new(RefCell::new(preselect.clone())); let chips: Vec<(gtk4::Button, NoteType)> = NoteType::all_builtin() .iter() - .map(|&name| { - let btn = gtk4::Button::builder() - .label(name) - .css_classes(["type-chip"]) - .build(); - (btn, NoteType::from_str(name)) - }) + .map(|&name| (bread_theme::gtk::chip(name), NoteType::from_str(name))) .collect(); for (btn, nt) in &chips { + bread_theme::gtk::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 { b.remove_css_class("active"); } - clicked.add_css_class("active"); + for b in &all_btns { bread_theme::gtk::set_chip_active(b, false); } + bread_theme::gtk::set_chip_active(clicked, true); }); chip_box.append(btn); } - if let Some((btn, _)) = chips.iter().find(|(_, nt)| *nt == NoteType::Note) { - btn.add_css_class("active"); - } vbox.append(&chip_box); vbox.append(>k4::Label::builder().label("Time (optional)").xalign(0.0).build()); let time_entry = gtk4::Entry::builder() - .placeholder_text("tomorrow 9am / at 7pm / in 30 minutes") + .placeholder_text(editor::TIME_PLACEHOLDER) .hexpand(true) .build(); vbox.append(&time_entry); vbox.append(>k4::Label::builder().label("Recurrence (optional)").xalign(0.0).build()); let rrule_entry = gtk4::Entry::builder() - .placeholder_text("RRULE:FREQ=WEEKLY;BYDAY=MO") + .placeholder_text(editor::RRULE_PLACEHOLDER) .hexpand(true) .build(); vbox.append(&rrule_entry); diff --git a/breadman/src/screenshot.rs b/breadman/src/screenshot.rs index 78c2bff..d579ad1 100644 --- a/breadman/src/screenshot.rs +++ b/breadman/src/screenshot.rs @@ -13,13 +13,11 @@ //! directly to a named stack page. //! //! One view isn't a stack page at all: "editor" opens the per-note editor -//! popover (`editor::build_editor_popover`), normally only reachable by -//! clicking a real note card's edit button. Screenshot mode calls the same -//! builder function directly against the first real note in the store -//! (bypassing the button/click-handler entirely — there's no clean way to -//! synthesize a click on a button that only ever existed as a local inside -//! `build_note_card`, never stored anywhere else), with no-op save/delete/ -//! error callbacks since nothing here should actually persist a change. +//! dialog (`editor::open_editor`), normally only reachable by clicking a +//! real note row's edit button. Screenshot mode calls the same builder +//! function directly against the first real note in the store (bypassing +//! the button/click-handler entirely), with no-op save/delete/error +//! callbacks since nothing here should actually persist a change. use gtk4::prelude::*; use std::path::PathBuf; @@ -57,7 +55,6 @@ pub fn dispatch( window: >k4::ApplicationWindow, req: ScreenshotRequest, state: crate::AppState, - editor_anchor: gtk4::Button, ) { let output = req.output; let (width, height) = (req.width as i32, req.height as i32); @@ -68,7 +65,7 @@ pub fn dispatch( let root = root.clone(); let state = state.clone(); gtk4::glib::timeout_add_local_once(PRE_POPUP_DELAY, move || { - crate::show_add_note_window(&root, state, move |dialog| { + crate::show_add_note_window(&root, state, breadpad_shared::types::NoteType::Note, move |dialog| { let output = output.clone(); dialog.connect_map(move |_| { let output = output.clone(); @@ -83,10 +80,10 @@ pub fn dispatch( } if req.view == "editor" { - window.connect_map(move |_| { + window.connect_map(move |root| { let output = output.clone(); let state = state.clone(); - let editor_anchor = editor_anchor.clone(); + let root = root.clone(); gtk4::glib::timeout_add_local_once(PRE_POPUP_DELAY, move || { let Some(note) = state.notes.borrow().first().cloned() else { eprintln!("breadman: no notes in the store to build the editor view from"); @@ -94,7 +91,10 @@ pub fn dispatch( }; let morning = state.cfg.borrow().reminders.default_morning.clone(); let store = Arc::new(state.write_store()); - let popover = crate::editor::build_editor_popover( + // AdwDialog handles its own presentation/centering - no more + // manual popover anchor/position/autohide juggling. + let dialog = crate::editor::open_editor( + root.upcast_ref::(), ¬e, store, morning, @@ -102,24 +102,13 @@ pub fn dispatch( Rc::new(|| {}), Rc::new(|_| {}), ); - popover.set_parent(&editor_anchor); - // Parenting to the whole window (rather than a small, - // concretely-placed widget like the real edit-button call - // site does) left the popover positioned above the window - // entirely (GTK4's default Popover position is Top) — off - // the top of the canvas and clipped out of every capture. - // Anchoring to a real button plus an explicit Bottom - // position keeps it inside the visible canvas. - popover.set_position(gtk4::PositionType::Bottom); - popover.set_autohide(false); let output = output.clone(); - popover.connect_map(move |_| { + dialog.connect_map(move |_| { let output = output.clone(); gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || { finish(bread_screenshots::capture_region(0, 0, width, height, &output)); }); }); - popover.popup(); }); }); return; diff --git a/breadman/src/views/archive.rs b/breadman/src/views/archive.rs index b4cd75f..baa4fb1 100644 --- a/breadman/src/views/archive.rs +++ b/breadman/src/views/archive.rs @@ -1,7 +1,6 @@ +use super::row::{build_empty_state, RowSpec}; use breadpad_shared::types::Note; use gtk4::prelude::*; -use std::cell::RefCell; -use std::rc::Rc; pub fn build(notes: &[Note], state: crate::AppState) -> gtk4::ScrolledWindow { let scroll = gtk4::ScrolledWindow::builder() @@ -18,92 +17,28 @@ pub fn build(notes: &[Note], state: crate::AppState) -> gtk4::ScrolledWindow { .build(); let mut archived: Vec<&Note> = notes.iter().filter(|n| n.done).collect(); - archived.sort_by(|a, b| b.created.cmp(&a.created)); + // Sort by completion time, not creation time - the previous sort used + // `created` while the row displayed `completed` ("done {date}"), which + // is why the last row could appear out of order against the visible + // dates. + archived.sort_by_key(|n| std::cmp::Reverse(n.completed.unwrap_or(n.created))); if archived.is_empty() { - list.append( - >k4::Label::builder() - .label("Archive is empty.") - .margin_top(32) - .build(), - ); + list.append(&build_empty_state("folder-symbolic", "Nothing archived yet.", None)); } else { for note in archived { - list.append(&build_archive_card(note, state.clone())); + let completed_str = note + .completed + .map(|t| { + let local: chrono::DateTime = t.into(); + format!("done {}", local.format("%b %d")) + }) + .unwrap_or_else(|| "done".into()); + let spec = RowSpec { date_label: completed_str, note, show_type_badge: true, show_done: false }; + list.append(&super::row::build(spec, state.clone())); } } scroll.set_child(Some(&list)); scroll } - -fn build_archive_card(note: &Note, state: crate::AppState) -> gtk4::Box { - let row = gtk4::Box::builder() - .orientation(gtk4::Orientation::Horizontal) - .spacing(8) - .margin_start(8) - .margin_end(8) - .margin_top(2) - .margin_bottom(2) - .css_classes(["note-card"]) - .build(); - - let completed_str = note - .completed - .map(|t| { - let local: chrono::DateTime = t.into(); - format!("done {}", local.format("%b %d")) - }) - .unwrap_or_else(|| "done".into()); - - let done_label = gtk4::Label::builder() - .label(&completed_str) - .width_chars(12) - .xalign(0.0) - .build(); - - let body_label = gtk4::Label::builder() - .label(¬e.body) - .hexpand(true) - .xalign(0.0) - .ellipsize(gtk4::pango::EllipsizeMode::End) - .build(); - - let type_label = gtk4::Label::builder() - .label(note.note_type.as_str()) - .css_classes(["type-chip"]) - .build(); - - // 🗑 Delete — two-click confirm - let delete_btn = gtk4::Button::builder() - .label("🗑") - .css_classes(["action-btn", "danger-btn"]) - .tooltip_text("Delete permanently") - .build(); - { - let confirming = Rc::new(RefCell::new(false)); - let note_id = note.id.clone(); - let row_c = row.clone(); - let btn_c = delete_btn.clone(); - - delete_btn.connect_clicked(move |_| { - if *confirming.borrow() { - let store = state.write_store(); - if let Err(e) = store.delete_note(¬e_id) { - state.log_error(format!("delete failed: {}", e)); - } - row_c.set_visible(false); - state.reload_notes(); - } else { - *confirming.borrow_mut() = true; - btn_c.set_label("Sure?"); - } - }); - } - - row.append(&done_label); - row.append(&body_label); - row.append(&type_label); - row.append(&delete_btn); - row -} diff --git a/breadman/src/views/mod.rs b/breadman/src/views/mod.rs index d4cde84..43a6788 100644 --- a/breadman/src/views/mod.rs +++ b/breadman/src/views/mod.rs @@ -1,4 +1,5 @@ pub mod archive; pub mod errors; +pub mod row; pub mod settings; pub mod upcoming; diff --git a/breadman/src/views/row.rs b/breadman/src/views/row.rs new file mode 100644 index 0000000..4e07994 --- /dev/null +++ b/breadman/src/views/row.rs @@ -0,0 +1,267 @@ +//! Shared single-line note row, used by every list view (All/Upcoming/ +//! per-type/Archive) instead of each view hand-rolling its own card. Design +//! review found the two-line card (title/badge top, huge dead gap, actions +//! bottom-right) used by the active views wasted enormous horizontal space +//! compared to Archive's tighter aligned-column layout - this ports that +//! layout everywhere and unifies the row template (including the edit +//! affordance, previously pencil-in-active / click-row-in-archive). + +use breadpad_shared::types::{Note, NoteType}; +use gtk4::prelude::*; +use std::rc::Rc; + +pub struct RowSpec<'a> { + pub date_label: String, + pub note: &'a Note, + pub show_type_badge: bool, + pub show_done: bool, +} + +/// Type-tinted badge class matching the `note-card-{type}` accent-bar colors +/// already established in breadpad-shared's theme (todo=green, +/// reminder=yellow, idea=pink, question=teal, note=blue). +fn type_chip_class(note_type: &NoteType) -> &'static str { + match note_type { + NoteType::Todo => "type-chip-todo", + NoteType::Reminder => "type-chip-reminder", + NoteType::Idea => "type-chip-idea", + NoteType::Note => "type-chip-note", + NoteType::Question => "type-chip-question", + NoteType::Tag(_) => "type-chip", + } +} + +pub fn build(spec: RowSpec, state: crate::AppState) -> gtk4::Box { + let note = spec.note; + let row = gtk4::Box::builder() + .orientation(gtk4::Orientation::Horizontal) + .spacing(8) + .margin_start(8) + .margin_end(8) + .margin_top(2) + .margin_bottom(2) + .css_classes(["note-card"]) + .build(); + row.add_css_class(&format!("note-card-{}", note.note_type.as_str())); + + let date_label = gtk4::Label::builder() + .label(&spec.date_label) + .width_chars(16) + .xalign(0.0) + .css_classes(["dim-label"]) + .build(); + row.append(&date_label); + + let body_label = gtk4::Label::builder() + .label(¬e.body) + .hexpand(true) + .xalign(0.0) + .ellipsize(gtk4::pango::EllipsizeMode::End) + .build(); + row.append(&body_label); + + if let Some(ws) = ¬e.workspace { + row.append( + >k4::Label::builder() + .label(format!("ws:{}", ws)) + .css_classes(["type-chip"]) + .build(), + ); + } + if note.rrule.is_some() { + row.append(>k4::Label::builder().label("\u{21bb}").css_classes(["dim-label"]).build()); + } + + if spec.show_type_badge { + row.append( + >k4::Label::builder() + .label(note.note_type.as_str()) + .css_classes(["type-chip", type_chip_class(¬e.note_type)]) + .build(), + ); + } + + if spec.show_done { + let done_btn = gtk4::Button::builder() + .icon_name("object-select-symbolic") + .css_classes(["action-btn", "done-btn"]) + .tooltip_text("Mark done") + .build(); + { + let note_id = note.id.clone(); + let row_c = row.clone(); + let state_c = state.clone(); + done_btn.connect_clicked(move |_| { + row_c.set_visible(false); // optimistic hide + let store = state_c.write_store(); + let id = note_id.clone(); + let state = state_c.clone(); + crate::spawn_bg( + move || -> anyhow::Result> { + if let Some(mut n) = store.get_by_id(&id)? { + n.mark_done(); + store.update_note(&n)?; + } + store.load_all() + }, + move |result| match result { + Ok(fresh) => { + *state.notes.borrow_mut() = fresh; + crate::rebuild_stack(&state); + let active = state.active_view.borrow().clone(); + state.stack.set_visible_child_name(&active); + } + Err(e) => state.log_error(format!("mark done failed: {}", e)), + }, + ); + }); + } + row.append(&done_btn); + } + + let edit_btn = gtk4::Button::builder() + .icon_name("document-edit-symbolic") + .css_classes(["action-btn", "edit-btn"]) + .tooltip_text("Edit") + .build(); + { + let note_c = note.clone(); + let state_c = state.clone(); + let body_label_c = body_label.clone(); + let row_c = row.clone(); + + edit_btn.connect_clicked(move |btn| { + let morning = state_c.cfg.borrow().reminders.default_morning.clone(); + let store = std::sync::Arc::new(state_c.write_store()); + + let state_save = state_c.clone(); + let body_label_save = body_label_c.clone(); + let state_del = state_c.clone(); + let row_del = row_c.clone(); + let state_err = state_c.clone(); + + crate::editor::open_editor( + btn.upcast_ref::(), + ¬e_c, + store, + morning, + std::rc::Rc::new(move |updated: Note| { + body_label_save.set_label(&updated.body); + state_save.reload_notes(); + crate::rebuild_stack(&state_save); + let active = state_save.active_view.borrow().clone(); + state_save.stack.set_visible_child_name(&active); + }), + std::rc::Rc::new(move || { + row_del.set_visible(false); + state_del.reload_notes(); + crate::rebuild_stack(&state_del); + let active = state_del.active_view.borrow().clone(); + state_del.stack.set_visible_child_name(&active); + }), + std::rc::Rc::new(move |e: String| { + state_err.log_error(e); + }), + ); + }); + } + row.append(&edit_btn); + + let delete_btn = gtk4::Button::builder() + .icon_name("user-trash-symbolic") + .css_classes(["action-btn", "danger-btn"]) + .tooltip_text("Delete") + .build(); + { + use std::cell::RefCell; + use std::rc::Rc; + let confirming = Rc::new(RefCell::new(false)); + let note_id = note.id.clone(); + let row_c = row.clone(); + let state_c = state.clone(); + let btn_c = delete_btn.clone(); + + delete_btn.connect_clicked(move |_| { + if *confirming.borrow() { + row_c.set_visible(false); // optimistic hide + let store = state_c.write_store(); + let id = note_id.clone(); + let state = state_c.clone(); + crate::spawn_bg( + move || -> anyhow::Result> { + store.delete_note(&id)?; + if let Err(e) = breadpad_shared::scheduler::Scheduler::cancel(&id) { + tracing::warn!("failed to cancel timer for {}: {}", id, e); + } + store.load_all() + }, + move |result| match result { + Ok(fresh) => { + *state.notes.borrow_mut() = fresh; + crate::rebuild_stack(&state); + let active = state.active_view.borrow().clone(); + state.stack.set_visible_child_name(&active); + } + Err(e) => state.log_error(format!("delete failed: {}", e)), + }, + ); + } else { + *confirming.borrow_mut() = true; + btn_c.set_icon_name("edit-delete-symbolic"); + btn_c.set_tooltip_text(Some("Click again to delete permanently")); + } + }); + } + row.append(&delete_btn); + + row +} + +/// Centered "nothing here" state with type-specific copy and (optionally) a +/// direct affordance to act on - the empty states were all top-anchored +/// generic text with no icon or action. +pub fn build_empty_state(icon_name: &str, text: &str, action: Option<(String, Rc)>) -> gtk4::Widget { + let outer = gtk4::Box::builder() + .orientation(gtk4::Orientation::Vertical) + .spacing(12) + .valign(gtk4::Align::Center) + .halign(gtk4::Align::Center) + .vexpand(true) + .build(); + + let icon = gtk4::Image::builder() + .icon_name(icon_name) + .pixel_size(32) + .css_classes(["dim-label"]) + .build(); + outer.append(&icon); + + let label = gtk4::Label::builder() + .label(text) + .css_classes(["dim-label"]) + .justify(gtk4::Justification::Center) + .build(); + outer.append(&label); + + if let Some((label_text, on_click)) = action { + let btn = gtk4::Button::builder() + .label(&label_text) + .css_classes(["confirm-button"]) + .halign(gtk4::Align::Center) + .build(); + btn.connect_clicked(move |_| on_click()); + outer.append(&btn); + } + + outer.upcast() +} + +/// Convenience wrapper for the note-list views: a "+ New {type}" button that +/// opens the New Note window preselected to `note_type`. +pub fn new_note_action(note_type: NoteType, window: gtk4::ApplicationWindow, state: crate::AppState) -> (String, Rc) { + let label = format!("+ New {}", note_type.as_str()); + let action: Rc = Rc::new(move || { + crate::show_add_note_window(&window, state.clone(), note_type.clone(), |_| {}); + }); + (label, action) +} diff --git a/breadman/src/views/settings.rs b/breadman/src/views/settings.rs index 8e224ae..52bacd0 100644 --- a/breadman/src/views/settings.rs +++ b/breadman/src/views/settings.rs @@ -1,9 +1,12 @@ use breadpad_shared::config::{ CalendarConfig, Config, ModelConfig, OllamaConfig, RemindersConfig, Settings, }; +use breadpad_shared::types::NoteType; use bread_theme::adw; use gtk4::prelude::*; use libadwaita::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::ScrolledWindow { let scroll = gtk4::ScrolledWindow::builder() @@ -12,21 +15,35 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled .vexpand(true) .build(); - let page = libadwaita::PreferencesPage::new(); + // Plain vertical box (not AdwPreferencesPage) wrapped in our own Clamp — + // PreferencesPage's built-in clamp caps out around ~600px, far narrower + // than the rest of breadman's edge-to-edge views. 900px + left-aligned + // keeps forms readable without the settings screen reading as a + // different, narrower app bolted onto the side. + let content = gtk4::Box::builder() + .orientation(gtk4::Orientation::Vertical) + .spacing(24) + .build(); // ── General ────────────────────────────────────────────────── let general_group = adw::preferences_group("General", None); - let type_options = ["note", "todo", "reminder", "idea", "question"]; - let default_type_row = libadwaita::ComboRow::builder() - .title("Default type") - .model(>k4::StringList::new(&type_options)) + let default_type_row = adw::action_row("Default type", None); + let type_pill_box = gtk4::Box::builder() + .orientation(gtk4::Orientation::Horizontal) + .spacing(4) + .valign(gtk4::Align::Center) .build(); - let dt_idx = type_options + 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() - .position(|&s| s == cfg.settings.default_type.as_str()) - .unwrap_or(0) as u32; - default_type_row.set_selected(dt_idx); + .map(|&name| (bread_theme::gtk::chip(name), name)) + .collect(); + for (btn, name) in &type_pills { + bread_theme::gtk::set_chip_active(btn, *name == selected_type.borrow().as_str()); + type_pill_box.append(btn); + } + default_type_row.add_suffix(&type_pill_box); general_group.add(&default_type_row); let ws_tag_row = adw::toggle_row( @@ -40,56 +57,50 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled 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) + let snooze_row = libadwaita::EntryRow::builder() + .title("Snooze options") + .show_apply_button(true) .build(); - snooze_row.add_suffix(&snooze_entry); + snooze_row.set_text(&cfg.settings.snooze_options.join(", ")); general_group.add(&snooze_row); - page.add(&general_group); + content.append(&general_group); // ── 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") - .valign(gtk4::Align::Center) + let morning_row = libadwaita::EntryRow::builder() + .title("Default morning (used for \"tomorrow_morning\" snoozes and recurring reminders)") + .show_apply_button(true) .build(); - morning_row.add_suffix(&morning_entry); + morning_row.set_text(&cfg.reminders.default_morning); rem_group.add(&morning_row); 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); - page.add(&rem_group); + content.append(&rem_group); // ── 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."), + Some("Optional local ONNX model for classifying note type/time without a network round-trip. These paths are shared with breadpad — both apps read the same model files."), ); - 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); + let model_path_row = libadwaita::EntryRow::builder().title("Model path").show_apply_button(true).build(); + model_path_row.set_text(&cfg.model.path); model_group.add(&model_path_row); - 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); + let tokenizer_row = libadwaita::EntryRow::builder().title("Tokenizer path").show_apply_button(true).build(); + tokenizer_row.set_text(&cfg.model.tokenizer); model_group.add(&tokenizer_row); - 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); + let ort_dylib_row = libadwaita::EntryRow::builder().title("Runtime library path").show_apply_button(true).build(); + ort_dylib_row.set_text(&cfg.model.ort_dylib_path); model_group.add(&ort_dylib_row); - page.add(&model_group); + content.append(&model_group); // ── AI classification (Ollama) ────────────────────────────────── let ollama_group = adw::preferences_group( @@ -100,14 +111,12 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled 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); + let ollama_endpoint_row = libadwaita::EntryRow::builder().title("Endpoint").show_apply_button(true).build(); + ollama_endpoint_row.set_text(&cfg.model.ollama.endpoint); 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); + let ollama_model_row = libadwaita::EntryRow::builder().title("Model").show_apply_button(true).build(); + ollama_model_row.set_text(&cfg.model.ollama.model); 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); @@ -117,7 +126,7 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled } ollama_group.add(&ollama_thresh_row); - page.add(&ollama_group); + content.append(&ollama_group); // ── Calendar sync ──────────────────────────────────────────── let cal_group = adw::preferences_group( @@ -128,129 +137,165 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled 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(); - cal_url_row.add_suffix(&cal_url); + let cal_url_row = libadwaita::EntryRow::builder().title("Calendar URL").show_apply_button(true).build(); + cal_url_row.set_text(&cfg.calendar.url); cal_group.add(&cal_url_row); - 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); + let cal_user_row = libadwaita::EntryRow::builder().title("Username").show_apply_button(true).build(); + cal_user_row.set_text(&cfg.calendar.username); cal_group.add(&cal_user_row); - let cal_pass_row = adw::action_row("App password", None); - let cal_pass = gtk4::PasswordEntry::builder() - .text(&cfg.calendar.password) - .show_peek_icon(true) - .valign(gtk4::Align::Center) - .build(); - cal_pass_row.add_suffix(&cal_pass); + let cal_pass_row = libadwaita::PasswordEntryRow::builder().title("App password").build(); + cal_pass_row.set_text(&cfg.calendar.password); cal_group.add(&cal_pass_row); - page.add(&cal_group); + content.append(&cal_group); - // ── Save ────────────────────────────────────────────────────── + // ── Status (instant-apply — no Save button) ───────────────── let status_label = gtk4::Label::builder() .label("") .xalign(0.0) .css_classes(["dim-label"]) + .margin_top(4) .build(); - let save_btn = gtk4::Button::builder() - .label("Save Settings") - .css_classes(["confirm-button"]) - .halign(gtk4::Align::End) - .build(); + content.append(&status_label); - { - 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_adj.clone(); - let mpe = model_path_entry.clone(); - let tke = tokenizer_entry.clone(); - let ode = ort_dylib_entry.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(); - let sl = status_label.clone(); + // Reads every widget's current value and persists immediately. Every + // control below calls this on its own "committed a change" signal + // (switch/combo/spin fire on change; entry rows fire on Enter or their + // apply-button, via show_apply_button) rather than a single Save button — + // AdwSwitchRow's whole design language implies changes take effect now. + let apply_now: Rc = Rc::new({ + let selected_type = selected_type.clone(); + let ws_tag_row = ws_tag_row.clone(); + let archive_adj = archive_adj.clone(); + let snooze_row = snooze_row.clone(); + let morning_row = morning_row.clone(); + let grace_adj = grace_adj.clone(); + let model_path_row = model_path_row.clone(); + let tokenizer_row = tokenizer_row.clone(); + let ort_dylib_row = ort_dylib_row.clone(); + let ollama_enabled_row = ollama_enabled_row.clone(); + let ollama_endpoint_row = ollama_endpoint_row.clone(); + let ollama_model_row = ollama_model_row.clone(); + let ollama_thresh_adj = ollama_thresh_adj.clone(); + let cal_enabled_row = cal_enabled_row.clone(); + let cal_url_row = cal_url_row.clone(); + let cal_user_row = cal_user_row.clone(); + let cal_pass_row = cal_pass_row.clone(); + let status_label = status_label.clone(); - save_btn.connect_clicked(move |_| { + move || { let new_cfg = Config { settings: Settings { - default_type: type_options - .get(dtc.selected() as usize) - .copied() - .unwrap_or("note") - .to_string(), - workspace_tag: wts.is_active(), - snooze_options: sne + default_type: selected_type.borrow().clone(), + workspace_tag: ws_tag_row.is_active(), + snooze_options: snooze_row .text() .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect(), - archive_after_days: ars.value() as i64, + archive_after_days: archive_adj.value() as i64, }, reminders: RemindersConfig { - default_morning: moe.text().to_string(), - missed_grace_minutes: grs.value() as i64, + default_morning: morning_row.text().to_string(), + missed_grace_minutes: grace_adj.value() as i64, }, model: ModelConfig { - path: mpe.text().to_string(), - tokenizer: tke.text().to_string(), - ort_dylib_path: ode.text().to_string(), + path: model_path_row.text().to_string(), + tokenizer: tokenizer_row.text().to_string(), + ort_dylib_path: ort_dylib_row.text().to_string(), ollama: OllamaConfig { - enabled: oec.is_active(), - endpoint: oee.text().to_string(), - model: ome.text().to_string(), - confidence_threshold: ots.value() as f32, + enabled: ollama_enabled_row.is_active(), + endpoint: ollama_endpoint_row.text().to_string(), + model: ollama_model_row.text().to_string(), + confidence_threshold: ollama_thresh_adj.value() as f32, }, }, calendar: CalendarConfig { - enabled: cec.is_active(), - url: cuc.text().to_string(), - username: csc.text().to_string(), - password: cpc.text().to_string(), + enabled: cal_enabled_row.is_active(), + url: cal_url_row.text().to_string(), + username: cal_user_row.text().to_string(), + password: cal_pass_row.text().to_string(), }, }; match new_cfg.save() { Ok(()) => { - sl.set_label("Settings saved."); + status_label.set_label("Saved."); on_save(new_cfg); } - Err(e) => sl.set_label(&format!("Save failed: {}", e)), + Err(e) => status_label.set_label(&format!("Save failed: {}", e)), } + } + }); + + // Switches / combo(-pills) / spinners apply the moment they change. + for (btn, name) in &type_pills { + let apply_now = apply_now.clone(); + 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); + apply_now(); }); } + macro_rules! apply_on_active { + ($row:expr) => { + let apply_now = apply_now.clone(); + $row.connect_active_notify(move |_| apply_now()); + }; + } + apply_on_active!(ws_tag_row); + apply_on_active!(ollama_enabled_row); + apply_on_active!(cal_enabled_row); + macro_rules! apply_on_value_changed { + ($adj:expr) => { + let apply_now = apply_now.clone(); + $adj.connect_value_changed(move |_| apply_now()); + }; + } + apply_on_value_changed!(archive_adj); + apply_on_value_changed!(grace_adj); + apply_on_value_changed!(ollama_thresh_adj); + // Entry rows: `apply` fires on Enter or the inline apply-button + // (show_apply_button), which only appears once the text has actually + // changed — the standard libadwaita instant-apply text-field idiom. + macro_rules! apply_on_entry { + ($row:expr) => { + let apply_now = apply_now.clone(); + $row.connect_apply(move |_| apply_now()); + }; + } + apply_on_entry!(snooze_row); + apply_on_entry!(morning_row); + apply_on_entry!(model_path_row); + apply_on_entry!(tokenizer_row); + apply_on_entry!(ort_dylib_row); + apply_on_entry!(ollama_endpoint_row); + apply_on_entry!(ollama_model_row); + apply_on_entry!(cal_url_row); + apply_on_entry!(cal_user_row); + apply_on_entry!(cal_pass_row); - let btn_row = gtk4::Box::builder() - .orientation(gtk4::Orientation::Horizontal) - .spacing(8) - .margin_top(16) - .margin_start(16) - .margin_end(16) + let clamp = libadwaita::Clamp::builder() + .maximum_size(900) + .tightening_threshold(700) + .halign(gtk4::Align::Start) + .build(); + clamp.set_child(Some(&content)); + + let outer = gtk4::Box::builder() + .orientation(gtk4::Orientation::Vertical) + .margin_start(12) + .margin_end(12) + .margin_top(12) .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); + outer.append(&clamp); scroll.set_child(Some(&outer)); scroll diff --git a/breadman/src/views/upcoming.rs b/breadman/src/views/upcoming.rs index 35480c0..be66376 100644 --- a/breadman/src/views/upcoming.rs +++ b/breadman/src/views/upcoming.rs @@ -1,7 +1,8 @@ +use super::row::{build_empty_state, RowSpec}; use breadpad_shared::types::{Note, NoteType}; use gtk4::prelude::*; -pub fn build(notes: &[Note]) -> gtk4::ScrolledWindow { +pub fn build(notes: &[Note], state: crate::AppState) -> gtk4::ScrolledWindow { let scroll = gtk4::ScrolledWindow::builder() .hscrollbar_policy(gtk4::PolicyType::Never) .vscrollbar_policy(gtk4::PolicyType::Automatic) @@ -26,61 +27,21 @@ pub fn build(notes: &[Note]) -> gtk4::ScrolledWindow { upcoming.sort_by_key(|n| n.effective_time().unwrap()); if upcoming.is_empty() { - let label = gtk4::Label::builder() - .label("No upcoming reminders or todos.") - .margin_top(32) - .build(); - list.append(&label); + list.append(&build_empty_state("x-office-calendar-symbolic", "No upcoming reminders or todos.", None)); } else { for note in upcoming { - let card = build_upcoming_card(note); - list.append(&card); + let time_str = note + .effective_time() + .map(|t| { + let local: chrono::DateTime = t.into(); + local.format("%a %b %d, %H:%M").to_string() + }) + .unwrap_or_default(); + let spec = RowSpec { date_label: time_str, note, show_type_badge: true, show_done: true }; + list.append(&super::row::build(spec, state.clone())); } } scroll.set_child(Some(&list)); scroll } - -fn build_upcoming_card(note: &Note) -> gtk4::Box { - let row = gtk4::Box::builder() - .orientation(gtk4::Orientation::Horizontal) - .spacing(8) - .margin_start(8) - .margin_end(8) - .margin_top(4) - .margin_bottom(4) - .css_classes(["note-card"]) - .build(); - - let time_str = note - .effective_time() - .map(|t| { - let local: chrono::DateTime = t.into(); - local.format("%a %b %d, %H:%M").to_string() - }) - .unwrap_or_default(); - - let time_label = gtk4::Label::builder() - .label(&time_str) - .width_chars(18) - .xalign(0.0) - .build(); - - let body_label = gtk4::Label::builder() - .label(¬e.body) - .hexpand(true) - .xalign(0.0) - .ellipsize(gtk4::pango::EllipsizeMode::End) - .build(); - - let type_label = gtk4::Label::builder() - .label(note.note_type.as_str()) - .css_classes(["type-chip"]) - .build(); - - row.append(&time_label); - row.append(&body_label); - row.append(&type_label); - row -} diff --git a/breadpad-shared/src/theme.rs b/breadpad-shared/src/theme.rs index cb1139a..295a6dd 100644 --- a/breadpad-shared/src/theme.rs +++ b/breadpad-shared/src/theme.rs @@ -57,6 +57,15 @@ window { border-radius: 8px; } color: @on-accent; } +/* Per-type tint, matching the note-card-{type} accent-bar colors below - + the flat cream badge was the same high-contrast fill for every type, + out-shouting note body text while telling you nothing extra. */ +.type-chip-todo { background: alpha(@green, 0.18); color: @green; } +.type-chip-reminder { background: alpha(@yellow, 0.18); color: @yellow; } +.type-chip-idea { background: alpha(@pink, 0.18); color: @pink; } +.type-chip-question { background: alpha(@teal, 0.18); color: @teal; } +.type-chip-note { background: alpha(@blue, 0.18); color: @blue; } + .confirm-button { background: @blue; color: @on-accent; @@ -135,8 +144,12 @@ window { border-radius: 8px; } .edit-btn { color: @blue; } .edit-btn:hover { background: alpha(@blue, 0.15); } -.danger-btn { color: @red; } -.danger-btn:hover { background: alpha(@red, 0.15); } +/* Fixed red, not @red - pywal can hand `red` any hue depending on the + wallpaper (see bread-theme's button.destructive-action for the same + reasoning), which would make delete indistinguishable from a normal + accent action. */ +.danger-btn { color: #e01b24; } +.danger-btn:hover { background: alpha(#e01b24, 0.15); } .note-card-todo { border-left-color: @green; } .note-card-reminder { border-left-color: @yellow; } From 26cb31354f8c923614d3580eb977a20c35bb47d5 Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 31 Jul 2026 07:21:13 +0800 Subject: [PATCH 24/53] breadpad: fix reminder alert, snooze popover, and compose popup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reminder alert: - Dismiss now matches Snooze's full-opacity text color instead of 0.6 alpha, which read as a disabled button. - Button row inset brought to 20px to match the header/body zone above it (was 16px, visible as a step across the divider). Snooze popover: - Options are left-aligned (each button's label now sets its own xalign) and span the full row width instead of centering as a ragged-left stack of labels. - Added a hairline divider between options so the list reads as distinct rows even in the popover's static/non-hover state. - Added a "Custom…" option that reveals a free-form time entry, reusing the same rule-based time parsing breadman's dialogs use, instead of only offering the three fixed presets. - Popover now matches the reminder card's flat-bordered elevation (no arrow, 1px border, no shadow) instead of GTK's default arrow+drop-shadow popover chrome. Compose popup: dropped the bare accent-teal checkmark submit button (same colour as the selected type pill, unlabeled) in favor of a "Press Enter to add" hint, since the popup already grabs focus and is keyboard-driven. Also made the entry's CSS padding uniform (14px on all sides, was 12px/16px). --- breadpad-shared/src/theme.rs | 34 ++++++++++-- breadpad/src/main.rs | 100 ++++++++++++++++++++++++++--------- 2 files changed, 107 insertions(+), 27 deletions(-) diff --git a/breadpad-shared/src/theme.rs b/breadpad-shared/src/theme.rs index 295a6dd..6823934 100644 --- a/breadpad-shared/src/theme.rs +++ b/breadpad-shared/src/theme.rs @@ -33,7 +33,7 @@ window { border-radius: 8px; } color: @fg; border: 2px solid @blue; border-radius: 6px; - padding: 12px 16px; + padding: 14px; font-size: 14px; caret-color: @fg; } @@ -183,12 +183,15 @@ window { border-radius: 8px; } color: @fg; } +/* Dismiss and Snooze are both secondary/outline actions and should read at + equal weight - Dismiss used to sit at 0.6 alpha next to Snooze's full + opacity, which made the button that closes the reminder look disabled. */ .reminder-dismiss { background: transparent; border: 1px solid @overlay; border-radius: 8px; padding: 8px 16px; - color: alpha(@fg, 0.6); + color: @fg; } .reminder-dismiss:hover { background: shade(@bg, 1.1); } @@ -203,15 +206,40 @@ window { border-radius: 8px; } .reminder-snooze:hover { background: shade(@bg, 1.1); } +/* Left-aligned (the button's child label sets xalign itself), full-width + row with a hairline divider so the list reads as distinct clickable rows + even before hover - a hover tint alone doesn't show up in a static + reading of the popover's default state. */ .snooze-option { background: transparent; border: none; border-radius: 6px; - padding: 8px 12px; + padding: 10px 12px; color: @fg; + border-bottom: 1px solid alpha(@overlay, 0.15); } .snooze-option:hover { background: shade(@bg, 1.2); } + +.snooze-custom-entry { + background: @bg; + color: @fg; + border: 1px solid @overlay; + border-radius: 6px; + padding: 8px 12px; + margin: 4px; +} + +.snooze-custom-entry:focus-within { border-color: @blue; outline: none; } + +/* Matches the reminder alert card's flat-bordered elevation (1px border, + 8px radius, no shadow) instead of GTK's default arrow+drop-shadow popover + chrome - the two surfaces used to speak two different elevation + languages. */ +popover.snooze-popover > contents { + border: 1px solid @overlay; + box-shadow: none; +} "#); if let Some(extra) = user_css { diff --git a/breadpad/src/main.rs b/breadpad/src/main.rs index 7ba8c87..4949963 100644 --- a/breadpad/src/main.rs +++ b/breadpad/src/main.rs @@ -3,6 +3,7 @@ use breadpad_shared::{ calendar::CalDavClient, classifier::Classifier, config::Config, + parser::parse_rule_based, scheduler::Scheduler, store::Store, types::{Note, NoteType}, @@ -492,14 +493,15 @@ fn build_reminder_window( .orientation(gtk4::Orientation::Horizontal) .build()); - // Button row + // Button row — same inset as the header/body zone above (20px), which + // used to be 16px here, visible as a step across the divider. let btn_row = gtk4::Box::builder() .orientation(gtk4::Orientation::Horizontal) .spacing(8) .margin_top(12) .margin_bottom(12) - .margin_start(16) - .margin_end(16) + .margin_start(20) + .margin_end(20) .build(); let dismiss_btn = gtk4::Button::builder() @@ -507,23 +509,35 @@ fn build_reminder_window( .css_classes(["reminder-dismiss"]) .build(); - // Snooze popover + // Snooze popover. No arrow and a matching flat border (see + // popover.snooze-popover in the shared theme) so it reads as the same + // elevation language as the reminder card, instead of GTK's default + // arrow+drop-shadow chrome next to the card's flat 1px border. let snooze_popover = gtk4::Popover::new(); + snooze_popover.set_has_arrow(false); + snooze_popover.add_css_class("snooze-popover"); let snooze_vbox = gtk4::Box::builder() .orientation(gtk4::Orientation::Vertical) - .spacing(4) - .margin_top(8) - .margin_bottom(8) - .margin_start(8) - .margin_end(8) + .spacing(0) + .margin_top(4) + .margin_bottom(4) .build(); + // Left-aligned row: a bare Button::builder().label() centers its text, + // so each option gets an explicit xalign(0.0) label as its child and + // hexpand(true) so the row fills the popover's full width instead of + // shrinking to the longest label. + let snooze_option_row = |label: &str| { + gtk4::Button::builder() + .child(>k4::Label::builder().label(label).xalign(0.0).build()) + .css_classes(["snooze-option"]) + .hexpand(true) + .build() + }; + for opt in &cfg.settings.snooze_options { let label = humanize_snooze(opt).to_string(); - let btn = gtk4::Button::builder() - .label(&label) - .css_classes(["snooze-option"]) - .build(); + let btn = snooze_option_row(&label); let key = opt.clone(); let note_c = note.clone(); let cfg_c = cfg.clone(); @@ -543,6 +557,47 @@ fn build_reminder_window( }); snooze_vbox.append(&btn); } + + // Custom… — reuses the same free-form time parsing breadman's dialogs + // already use, rather than inventing a separate time-picker widget. + let custom_entry = gtk4::Entry::builder() + .placeholder_text("tomorrow 9am / in 45 minutes") + .css_classes(["snooze-custom-entry"]) + .visible(false) + .build(); + { + let note_c = note.clone(); + let cfg_c = cfg.clone(); + let win_c = window.clone(); + let popover_c = snooze_popover.clone(); + let entry_c = custom_entry.clone(); + custom_entry.connect_activate(move |_| { + let text = entry_c.text().to_string(); + let parsed = parse_rule_based(&text, &cfg_c.reminders.default_morning); + if let Some(until) = parsed.time { + if let Ok(store) = Store::new().map(|s| s.with_calendar_if_enabled(&cfg_c)) { + let mut updated = note_c.as_ref().clone(); + updated.snoozed_until = Some(until); + let _ = store.update_note(&updated); + let _ = Scheduler::schedule(&updated); + } + popover_c.popdown(); + win_c.close(); + } + }); + } + let custom_btn = snooze_option_row("Custom\u{2026}"); + { + let custom_entry_c = custom_entry.clone(); + custom_btn.connect_clicked(move |btn| { + btn.set_visible(false); + custom_entry_c.set_visible(true); + custom_entry_c.grab_focus(); + }); + } + snooze_vbox.append(&custom_btn); + snooze_vbox.append(&custom_entry); + snooze_popover.set_child(Some(&snooze_vbox)); let snooze_btn = gtk4::MenuButton::builder() @@ -735,10 +790,13 @@ fn build_window( } } - // Confirm button - let confirm_btn = gtk4::Button::builder() - .label("✓") - .css_classes(["confirm-button"]) + // No submit button — this popup is keyboard-driven (grabs focus on + // open, Escape closes it) and a bare accent-teal checkmark used to sit + // next to the selected-type pill in the same accent teal, carrying two + // different meanings in one colour. A hint is enough. + let enter_hint = gtk4::Label::builder() + .label("Press Enter to add") + .css_classes(["dim-label"]) .build(); let bottom_row = gtk4::Box::builder() @@ -749,7 +807,7 @@ fn build_window( let spacer = gtk4::Box::builder().hexpand(true).build(); bottom_row.append(&spacer); - bottom_row.append(&confirm_btn); + bottom_row.append(&enter_hint); vbox.append(&entry); vbox.append(&bottom_row); @@ -785,12 +843,6 @@ fn build_window( } }; - // Confirm button click - { - let save = save_and_close.clone(); - confirm_btn.connect_clicked(move |_| save()); - } - // Entry activate (Enter key) { let save = save_and_close.clone(); From ae296c71540c555e9ac1b6c55b23f096e1f35ff6 Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 31 Jul 2026 08:58:36 +0800 Subject: [PATCH 25/53] breadman: rebuild settings as plain GTK4, fix AdwDialog sizing, present-ordering bug The libadwaita-based settings screen (previous commit) had two real bugs a closer look (and a screenshot from the real running app) caught: - AdwSpinRow's internal GtkSpinButton has no width constraint of its own, so once the row was widened to 900px, the spin button stretched to fill it - the digits and +/- buttons ended up stranded behind a huge empty bordered box, the exact bug the design review flagged, just worse. - bread-theme's shared `entry, spinbutton` rule outlines every field with an always-visible @overlay border, which on a light-cream overlay colour reads as a stark white outline against the dark theme. Rather than keep fighting libadwaita's internal row/spin-button opinions (there's no supported way to reach in and constrain them), settings.rs is now plain GTK4 mirroring bos-settings' own Row.svelte/NumberField.svelte/ TextField.svelte design exactly: same tokens (12/16px row padding, ch-width inputs, transparent-at-rest border, accent border only on focus), built on a plain `list.boxed-list` for the native rounded-corner-run + divider styling. Full control over sizing, no internal widget to hunt for. Also fixed a real ordering bug in the editor AdwDialog conversion: `open_editor` used to call `dialog.present()` internally before returning, so callers that connected `dialog.connect_map` afterward (screenshot mode) missed the signal entirely - it can fire synchronously inside `present`. Presentation now happens at each call site, after wiring `connect_map`. Also gave the dialog's content an explicit height/vexpand + min-content- height, since the ScrolledWindow had none and the whole dialog was collapsing to just its header bar. breadpad-shared's bread-theme dependency also gets fixed here: it was still pinned to an old GitHub-mirror tag (v0.2.8) while breadman pinned the same crate to git.breadway.dev's dev branch - two different copies of bread-theme compiled into the same binary, so breadman's actual runtime CSS (built through breadpad_shared::theme) never saw any of the shared stylesheet fixes above regardless of what breadman's own direct dependency resolved to. --- Cargo.lock | 30 +-- breadman/src/editor.rs | 9 +- breadman/src/screenshot.rs | 7 +- breadman/src/views/row.rs | 5 +- breadman/src/views/settings.rs | 346 ++++++++++++++++++--------------- breadpad-shared/Cargo.toml | 2 +- breadpad-shared/src/theme.rs | 21 ++ 7 files changed, 230 insertions(+), 190 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1152693..17001a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -312,21 +312,9 @@ dependencies = [ "tracing", ] -[[package]] -name = "bread-theme" -version = "0.2.3" -source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.2.8#77417d552130281ff787e07d52541eb25e9d533b" -dependencies = [ - "dirs 5.0.1", - "gtk4", - "serde", - "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", @@ -351,7 +339,7 @@ version = "0.3.4" dependencies = [ "anyhow", "bread-screenshots", - "bread-theme 0.3.1", + "bread-theme", "breadpad-shared", "chrono", "dirs 5.0.1", @@ -390,7 +378,7 @@ name = "breadpad-shared" version = "0.3.4" dependencies = [ "anyhow", - "bread-theme 0.2.3", + "bread-theme", "chrono", "dirs 5.0.1", "ical", @@ -598,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]] @@ -887,7 +875,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -962,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]] @@ -1273,7 +1261,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2145,7 +2133,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2767,7 +2755,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3091,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/breadman/src/editor.rs b/breadman/src/editor.rs index bebf15d..a11e46e 100644 --- a/breadman/src/editor.rs +++ b/breadman/src/editor.rs @@ -23,8 +23,11 @@ use std::sync::Arc; pub const TIME_PLACEHOLDER: &str = "tomorrow 9am / at 7pm / 2026-08-01 09:00"; pub const RRULE_PLACEHOLDER: &str = "RRULE:FREQ=WEEKLY;BYDAY=MO"; +/// Builds the dialog but does not present it - callers that need to hook +/// its `map` signal (screenshot mode) must connect before presenting or +/// they miss the signal entirely; interactive callers present immediately +/// with `dialog.present(Some(parent))`. pub fn open_editor( - parent: >k4::Widget, note: &Note, store: Arc, morning: String, @@ -35,6 +38,7 @@ pub fn open_editor( let dialog = libadwaita::Dialog::builder() .title("Edit Note") .content_width(480) + .content_height(520) .build(); let header = libadwaita::HeaderBar::new(); @@ -114,6 +118,8 @@ pub fn open_editor( let scroll = gtk4::ScrolledWindow::builder() .hscrollbar_policy(gtk4::PolicyType::Never) .vscrollbar_policy(gtk4::PolicyType::Automatic) + .vexpand(true) + .min_content_height(400) .build(); scroll.set_child(Some(&content)); toolbar_view.set_content(Some(&scroll)); @@ -200,7 +206,6 @@ pub fn open_editor( }); } - dialog.present(Some(parent)); dialog } diff --git a/breadman/src/screenshot.rs b/breadman/src/screenshot.rs index d579ad1..5b36c9d 100644 --- a/breadman/src/screenshot.rs +++ b/breadman/src/screenshot.rs @@ -20,6 +20,7 @@ //! callbacks since nothing here should actually persist a change. use gtk4::prelude::*; +use libadwaita::prelude::*; use std::path::PathBuf; use std::rc::Rc; use std::sync::Arc; @@ -92,9 +93,10 @@ pub fn dispatch( let morning = state.cfg.borrow().reminders.default_morning.clone(); let store = Arc::new(state.write_store()); // AdwDialog handles its own presentation/centering - no more - // manual popover anchor/position/autohide juggling. + // manual popover anchor/position/autohide juggling. Must + // connect `map` BEFORE presenting, or the signal (which can + // fire synchronously inside `present`) is missed entirely. let dialog = crate::editor::open_editor( - root.upcast_ref::(), ¬e, store, morning, @@ -109,6 +111,7 @@ pub fn dispatch( finish(bread_screenshots::capture_region(0, 0, width, height, &output)); }); }); + dialog.present(Some(root.upcast_ref::())); }); }); return; diff --git a/breadman/src/views/row.rs b/breadman/src/views/row.rs index 4e07994..501b8a0 100644 --- a/breadman/src/views/row.rs +++ b/breadman/src/views/row.rs @@ -8,6 +8,7 @@ use breadpad_shared::types::{Note, NoteType}; use gtk4::prelude::*; +use libadwaita::prelude::*; use std::rc::Rc; pub struct RowSpec<'a> { @@ -140,8 +141,7 @@ pub fn build(spec: RowSpec, state: crate::AppState) -> gtk4::Box { let row_del = row_c.clone(); let state_err = state_c.clone(); - crate::editor::open_editor( - btn.upcast_ref::(), + let dialog = crate::editor::open_editor( ¬e_c, store, morning, @@ -163,6 +163,7 @@ pub fn build(spec: RowSpec, state: crate::AppState) -> gtk4::Box { state_err.log_error(e); }), ); + dialog.present(Some(btn.upcast_ref::())); }); } row.append(&edit_btn); diff --git a/breadman/src/views/settings.rs b/breadman/src/views/settings.rs index 52bacd0..6090274 100644 --- a/breadman/src/views/settings.rs +++ b/breadman/src/views/settings.rs @@ -1,13 +1,81 @@ +//! Settings screen. Deliberately plain GTK4, not libadwaita's AdwActionRow/ +//! AdwSpinRow/AdwEntryRow family — those ran noticeably taller than the rest +//! of the app and don't expose a way to constrain the internal spin +//! button's width from the outside (it stretches to fill whatever room the +//! row has, leaving the digits and +/- buttons stranded behind a huge empty +//! bordered box once the row is wider than libadwaita's usual ~400-600px +//! home turf). Instead this mirrors bos-settings' own Row.svelte / +//! NumberField.svelte / TextField.svelte design exactly — same tokens +//! (12/16px row padding, ch-width inputs, transparent-at-rest border) — so +//! the two settings screens in the ecosystem actually agree with each other. + use breadpad_shared::config::{ CalendarConfig, Config, ModelConfig, OllamaConfig, RemindersConfig, Settings, }; use breadpad_shared::types::NoteType; -use bread_theme::adw; -use gtk4::prelude::*; -use libadwaita::prelude::*; +use gtk4::{glib, prelude::*}; use std::cell::RefCell; use std::rc::Rc; +/// A titled group: heading, optional description, then a `.boxed-list` of +/// `field_row`s (native GTK4 rounded-corner-run + divider styling). +fn field_group(title: &str, description: Option<&str>) -> (gtk4::Box, gtk4::ListBox) { + let outer = gtk4::Box::builder().orientation(gtk4::Orientation::Vertical).spacing(8).build(); + + let heading = gtk4::Label::builder().label(title).xalign(0.0).css_classes(["heading"]).build(); + outer.append(&heading); + + if let Some(desc) = description { + let desc_label = gtk4::Label::builder() + .label(desc) + .xalign(0.0) + .wrap(true) + .css_classes(["dim-label"]) + .build(); + outer.append(&desc_label); + } + + let list = gtk4::ListBox::builder() + .selection_mode(gtk4::SelectionMode::None) + .css_classes(["boxed-list"]) + .build(); + outer.append(&list); + + (outer, list) +} + +/// A single row: label (+ optional subtitle) on the left, one control on +/// the right — same shape as bos-settings' `Row.svelte`. +fn field_row(label: &str, subtitle: Option<&str>, control: &impl IsA) -> gtk4::ListBoxRow { + let row = gtk4::ListBoxRow::builder() + .selectable(false) + .activatable(false) + .css_classes(["field-row"]) + .build(); + + let hbox = gtk4::Box::builder().orientation(gtk4::Orientation::Horizontal).spacing(16).build(); + + let label_box = gtk4::Box::builder().orientation(gtk4::Orientation::Vertical).hexpand(true).valign(gtk4::Align::Center).build(); + label_box.append(>k4::Label::builder().label(label).xalign(0.0).build()); + if let Some(sub) = subtitle { + label_box.append(>k4::Label::builder().label(sub).xalign(0.0).wrap(true).css_classes(["field-row-subtitle"]).build()); + } + hbox.append(&label_box); + hbox.append(control); + + row.set_child(Some(&hbox)); + row +} + +fn text_entry(text: &str, width_chars: i32) -> gtk4::Entry { + gtk4::Entry::builder().text(text).width_chars(width_chars).valign(gtk4::Align::Center).css_classes(["field-input"]).build() +} + +fn spin_button(value: f64, min: f64, max: f64, step: f64, page: f64, digits: u32) -> gtk4::SpinButton { + let adj = gtk4::Adjustment::new(value, min, max, step, page, 0.0); + gtk4::SpinButton::builder().adjustment(&adj).digits(digits).width_chars(8).valign(gtk4::Align::Center).css_classes(["field-input"]).build() +} + pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::ScrolledWindow { let scroll = gtk4::ScrolledWindow::builder() .hscrollbar_policy(gtk4::PolicyType::Never) @@ -15,25 +83,12 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled .vexpand(true) .build(); - // Plain vertical box (not AdwPreferencesPage) wrapped in our own Clamp — - // PreferencesPage's built-in clamp caps out around ~600px, far narrower - // than the rest of breadman's edge-to-edge views. 900px + left-aligned - // keeps forms readable without the settings screen reading as a - // different, narrower app bolted onto the side. - let content = gtk4::Box::builder() - .orientation(gtk4::Orientation::Vertical) - .spacing(24) - .build(); + let content = gtk4::Box::builder().orientation(gtk4::Orientation::Vertical).spacing(24).build(); // ── General ────────────────────────────────────────────────── - let general_group = adw::preferences_group("General", None); + let (general_group, general_list) = field_group("General", None); - let default_type_row = adw::action_row("Default type", None); - 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> = Rc::new(RefCell::new(cfg.settings.default_type.clone())); let type_pills: Vec<(gtk4::Button, &'static str)> = NoteType::all_builtin() .iter() @@ -43,181 +98,148 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled bread_theme::gtk::set_chip_active(btn, *name == selected_type.borrow().as_str()); type_pill_box.append(btn); } - default_type_row.add_suffix(&type_pill_box); - general_group.add(&default_type_row); + general_list.append(&field_row("Default type", None, &type_pill_box)); - let ws_tag_row = adw::toggle_row( + let ws_tag_switch = gtk4::Switch::builder().active(cfg.settings.workspace_tag).valign(gtk4::Align::Center).build(); + general_list.append(&field_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); + &ws_tag_switch, + )); - 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 archive_spin = spin_button(cfg.settings.archive_after_days as f64, 1.0, 365.0, 1.0, 7.0, 0); + general_list.append(&field_row("Archive after (days)", None, &archive_spin)); - let snooze_row = libadwaita::EntryRow::builder() - .title("Snooze options") - .show_apply_button(true) - .build(); - snooze_row.set_text(&cfg.settings.snooze_options.join(", ")); - general_group.add(&snooze_row); + let snooze_entry = text_entry(&cfg.settings.snooze_options.join(", "), 24); + general_list.append(&field_row("Snooze options", Some("Comma-separated (e.g. 15m, 1h, tomorrow_morning)"), &snooze_entry)); content.append(&general_group); // ── Reminders ──────────────────────────────────────────────── - let rem_group = adw::preferences_group("Reminders", None); + let (rem_group, rem_list) = field_group("Reminders", None); - let morning_row = libadwaita::EntryRow::builder() - .title("Default morning (used for \"tomorrow_morning\" snoozes and recurring reminders)") - .show_apply_button(true) - .build(); - morning_row.set_text(&cfg.reminders.default_morning); - rem_group.add(&morning_row); + let morning_entry = text_entry(&cfg.reminders.default_morning, 10); + rem_list.append(&field_row("Default morning", Some("Used for \"tomorrow_morning\" snoozes and recurring reminders"), &morning_entry)); - 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 grace_spin = spin_button(cfg.reminders.missed_grace_minutes as f64, 0.0, 1440.0, 5.0, 30.0, 0); + rem_list.append(&field_row("Missed grace (minutes)", Some("How late a reminder can fire before it's considered missed"), &grace_spin)); content.append(&rem_group); // ── Local classifier ─────────────────────────────────────────── - let model_group = adw::preferences_group( + let (model_group, model_list) = field_group( "Local Classifier", Some("Optional local ONNX model for classifying note type/time without a network round-trip. These paths are shared with breadpad — both apps read the same model files."), ); - let model_path_row = libadwaita::EntryRow::builder().title("Model path").show_apply_button(true).build(); - model_path_row.set_text(&cfg.model.path); - model_group.add(&model_path_row); + let model_path_entry = text_entry(&cfg.model.path, 30); + model_list.append(&field_row("Model path", None, &model_path_entry)); - let tokenizer_row = libadwaita::EntryRow::builder().title("Tokenizer path").show_apply_button(true).build(); - tokenizer_row.set_text(&cfg.model.tokenizer); - model_group.add(&tokenizer_row); + let tokenizer_entry = text_entry(&cfg.model.tokenizer, 30); + model_list.append(&field_row("Tokenizer path", None, &tokenizer_entry)); - let ort_dylib_row = libadwaita::EntryRow::builder().title("Runtime library path").show_apply_button(true).build(); - ort_dylib_row.set_text(&cfg.model.ort_dylib_path); - model_group.add(&ort_dylib_row); + let ort_dylib_entry = text_entry(&cfg.model.ort_dylib_path, 30); + model_list.append(&field_row("Runtime library path", None, &ort_dylib_entry)); content.append(&model_group); // ── AI classification (Ollama) ────────────────────────────────── - let ollama_group = adw::preferences_group( + let (ollama_group, ollama_list) = field_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_enabled_switch = gtk4::Switch::builder().active(cfg.model.ollama.enabled).valign(gtk4::Align::Center).build(); + ollama_list.append(&field_row("Enabled", None, &ollama_enabled_switch)); - let ollama_endpoint_row = libadwaita::EntryRow::builder().title("Endpoint").show_apply_button(true).build(); - ollama_endpoint_row.set_text(&cfg.model.ollama.endpoint); - ollama_group.add(&ollama_endpoint_row); + let ollama_endpoint_entry = text_entry(&cfg.model.ollama.endpoint, 24); + ollama_list.append(&field_row("Endpoint", None, &ollama_endpoint_entry)); - let ollama_model_row = libadwaita::EntryRow::builder().title("Model").show_apply_button(true).build(); - ollama_model_row.set_text(&cfg.model.ollama.model); - ollama_group.add(&ollama_model_row); + let ollama_model_entry = text_entry(&cfg.model.ollama.model, 16); + ollama_list.append(&field_row("Model", None, &ollama_model_entry)); - 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); + let ollama_thresh_spin = spin_button(cfg.model.ollama.confidence_threshold as f64, 0.0, 1.0, 0.05, 0.1, 2); + ollama_list.append(&field_row("Confidence threshold", None, &ollama_thresh_spin)); content.append(&ollama_group); // ── Calendar sync ──────────────────────────────────────────── - let cal_group = adw::preferences_group( - "Calendar Sync", - Some("Sync reminders to a Nextcloud calendar via CalDAV."), - ); + let (cal_group, cal_list) = field_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_enabled_switch = gtk4::Switch::builder().active(cfg.calendar.enabled).valign(gtk4::Align::Center).build(); + cal_list.append(&field_row("Enabled", None, &cal_enabled_switch)); - let cal_url_row = libadwaita::EntryRow::builder().title("Calendar URL").show_apply_button(true).build(); - cal_url_row.set_text(&cfg.calendar.url); - cal_group.add(&cal_url_row); + let cal_url_entry = text_entry(&cfg.calendar.url, 30); + cal_list.append(&field_row("Calendar URL", None, &cal_url_entry)); - let cal_user_row = libadwaita::EntryRow::builder().title("Username").show_apply_button(true).build(); - cal_user_row.set_text(&cfg.calendar.username); - cal_group.add(&cal_user_row); + let cal_user_entry = text_entry(&cfg.calendar.username, 16); + cal_list.append(&field_row("Username", None, &cal_user_entry)); - let cal_pass_row = libadwaita::PasswordEntryRow::builder().title("App password").build(); - cal_pass_row.set_text(&cfg.calendar.password); - cal_group.add(&cal_pass_row); + let cal_pass_entry = gtk4::PasswordEntry::builder().text(&cfg.calendar.password).show_peek_icon(true).valign(gtk4::Align::Center).css_classes(["field-input"]).build(); + cal_list.append(&field_row("App password", None, &cal_pass_entry)); content.append(&cal_group); // ── Status (instant-apply — no Save button) ───────────────── - let status_label = gtk4::Label::builder() - .label("") - .xalign(0.0) - .css_classes(["dim-label"]) - .margin_top(4) - .build(); + let status_label = gtk4::Label::builder().label("").xalign(0.0).css_classes(["dim-label"]).margin_top(4).build(); content.append(&status_label); // Reads every widget's current value and persists immediately. Every // control below calls this on its own "committed a change" signal - // (switch/combo/spin fire on change; entry rows fire on Enter or their - // apply-button, via show_apply_button) rather than a single Save button — - // AdwSwitchRow's whole design language implies changes take effect now. + // (switch/spin fire on change; entries fire on Enter or focus-out). let apply_now: Rc = Rc::new({ let selected_type = selected_type.clone(); - let ws_tag_row = ws_tag_row.clone(); - let archive_adj = archive_adj.clone(); - let snooze_row = snooze_row.clone(); - let morning_row = morning_row.clone(); - let grace_adj = grace_adj.clone(); - let model_path_row = model_path_row.clone(); - let tokenizer_row = tokenizer_row.clone(); - let ort_dylib_row = ort_dylib_row.clone(); - let ollama_enabled_row = ollama_enabled_row.clone(); - let ollama_endpoint_row = ollama_endpoint_row.clone(); - let ollama_model_row = ollama_model_row.clone(); - let ollama_thresh_adj = ollama_thresh_adj.clone(); - let cal_enabled_row = cal_enabled_row.clone(); - let cal_url_row = cal_url_row.clone(); - let cal_user_row = cal_user_row.clone(); - let cal_pass_row = cal_pass_row.clone(); + let ws_tag_switch = ws_tag_switch.clone(); + let archive_spin = archive_spin.clone(); + let snooze_entry = snooze_entry.clone(); + let morning_entry = morning_entry.clone(); + let grace_spin = grace_spin.clone(); + let model_path_entry = model_path_entry.clone(); + let tokenizer_entry = tokenizer_entry.clone(); + let ort_dylib_entry = ort_dylib_entry.clone(); + let ollama_enabled_switch = ollama_enabled_switch.clone(); + let ollama_endpoint_entry = ollama_endpoint_entry.clone(); + let ollama_model_entry = ollama_model_entry.clone(); + let ollama_thresh_spin = ollama_thresh_spin.clone(); + let cal_enabled_switch = cal_enabled_switch.clone(); + let cal_url_entry = cal_url_entry.clone(); + let cal_user_entry = cal_user_entry.clone(); + let cal_pass_entry = cal_pass_entry.clone(); let status_label = status_label.clone(); move || { let new_cfg = Config { settings: Settings { default_type: selected_type.borrow().clone(), - workspace_tag: ws_tag_row.is_active(), - snooze_options: snooze_row + workspace_tag: ws_tag_switch.is_active(), + snooze_options: snooze_entry .text() .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect(), - archive_after_days: archive_adj.value() as i64, + archive_after_days: archive_spin.value() as i64, }, reminders: RemindersConfig { - default_morning: morning_row.text().to_string(), - missed_grace_minutes: grace_adj.value() as i64, + default_morning: morning_entry.text().to_string(), + missed_grace_minutes: grace_spin.value() as i64, }, model: ModelConfig { - path: model_path_row.text().to_string(), - tokenizer: tokenizer_row.text().to_string(), - ort_dylib_path: ort_dylib_row.text().to_string(), + path: model_path_entry.text().to_string(), + tokenizer: tokenizer_entry.text().to_string(), + ort_dylib_path: ort_dylib_entry.text().to_string(), ollama: OllamaConfig { - enabled: ollama_enabled_row.is_active(), - endpoint: ollama_endpoint_row.text().to_string(), - model: ollama_model_row.text().to_string(), - confidence_threshold: ollama_thresh_adj.value() as f32, + enabled: ollama_enabled_switch.is_active(), + endpoint: ollama_endpoint_entry.text().to_string(), + model: ollama_model_entry.text().to_string(), + confidence_threshold: ollama_thresh_spin.value() as f32, }, }, calendar: CalendarConfig { - enabled: cal_enabled_row.is_active(), - url: cal_url_row.text().to_string(), - username: cal_user_row.text().to_string(), - password: cal_pass_row.text().to_string(), + enabled: cal_enabled_switch.is_active(), + url: cal_url_entry.text().to_string(), + username: cal_user_entry.text().to_string(), + password: cal_pass_entry.text().to_string(), }, }; match new_cfg.save() { @@ -230,7 +252,7 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled } }); - // Switches / combo(-pills) / spinners apply the moment they change. + // Type pills, switches, spinners apply the moment they change. for (btn, name) in &type_pills { let apply_now = apply_now.clone(); let sel = selected_type.clone(); @@ -244,49 +266,49 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled }); } macro_rules! apply_on_active { - ($row:expr) => { + ($sw:expr) => { let apply_now = apply_now.clone(); - $row.connect_active_notify(move |_| apply_now()); + $sw.connect_state_set(move |_, _| { apply_now(); glib::Propagation::Proceed }); }; } - apply_on_active!(ws_tag_row); - apply_on_active!(ollama_enabled_row); - apply_on_active!(cal_enabled_row); + apply_on_active!(ws_tag_switch); + apply_on_active!(ollama_enabled_switch); + apply_on_active!(cal_enabled_switch); macro_rules! apply_on_value_changed { - ($adj:expr) => { + ($spin:expr) => { let apply_now = apply_now.clone(); - $adj.connect_value_changed(move |_| apply_now()); + $spin.connect_value_changed(move |_| apply_now()); }; } - apply_on_value_changed!(archive_adj); - apply_on_value_changed!(grace_adj); - apply_on_value_changed!(ollama_thresh_adj); - // Entry rows: `apply` fires on Enter or the inline apply-button - // (show_apply_button), which only appears once the text has actually - // changed — the standard libadwaita instant-apply text-field idiom. - macro_rules! apply_on_entry { - ($row:expr) => { - let apply_now = apply_now.clone(); - $row.connect_apply(move |_| apply_now()); - }; - } - apply_on_entry!(snooze_row); - apply_on_entry!(morning_row); - apply_on_entry!(model_path_row); - apply_on_entry!(tokenizer_row); - apply_on_entry!(ort_dylib_row); - apply_on_entry!(ollama_endpoint_row); - apply_on_entry!(ollama_model_row); - apply_on_entry!(cal_url_row); - apply_on_entry!(cal_user_row); - apply_on_entry!(cal_pass_row); + apply_on_value_changed!(archive_spin); + apply_on_value_changed!(grace_spin); + apply_on_value_changed!(ollama_thresh_spin); - let clamp = libadwaita::Clamp::builder() - .maximum_size(900) - .tightening_threshold(700) - .halign(gtk4::Align::Start) - .build(); - clamp.set_child(Some(&content)); + // Entries: apply on Enter, and on focus-out so a click-away doesn't + // silently discard the edit. + macro_rules! apply_on_entry { + ($entry:expr) => { + let apply_now_activate = apply_now.clone(); + $entry.connect_activate(move |_| apply_now_activate()); + let apply_now_focus = apply_now.clone(); + let focus = gtk4::EventControllerFocus::new(); + focus.connect_leave(move |_| apply_now_focus()); + $entry.add_controller(focus); + }; + } + apply_on_entry!(snooze_entry); + apply_on_entry!(morning_entry); + apply_on_entry!(model_path_entry); + apply_on_entry!(tokenizer_entry); + apply_on_entry!(ort_dylib_entry); + apply_on_entry!(ollama_endpoint_entry); + apply_on_entry!(ollama_model_entry); + apply_on_entry!(cal_url_entry); + apply_on_entry!(cal_user_entry); + apply_on_entry!(cal_pass_entry); + + content.set_halign(gtk4::Align::Start); + content.set_size_request(900, -1); let outer = gtk4::Box::builder() .orientation(gtk4::Orientation::Vertical) @@ -295,7 +317,7 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled .margin_top(12) .margin_bottom(16) .build(); - outer.append(&clamp); + outer.append(&content); scroll.set_child(Some(&outer)); scroll diff --git a/breadpad-shared/Cargo.toml b/breadpad-shared/Cargo.toml index 01470f9..72fe7b7 100644 --- a/breadpad-shared/Cargo.toml +++ b/breadpad-shared/Cargo.toml @@ -7,7 +7,7 @@ authors.workspace = true [dependencies] -bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.8", features = ["gtk"] } +bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", branch = "dev", features = ["gtk"] } anyhow.workspace = true tracing.workspace = true serde.workspace = true diff --git a/breadpad-shared/src/theme.rs b/breadpad-shared/src/theme.rs index 6823934..e6fa63e 100644 --- a/breadpad-shared/src/theme.rs +++ b/breadpad-shared/src/theme.rs @@ -28,6 +28,27 @@ pub fn build_css(palette: &Palette, user_css: Option<&str>) -> String { /* breadpad/breadman-specific components */ window { border-radius: 8px; } +/* breadman/views/settings.rs — matches bos-settings' Row.svelte/ + NumberField.svelte/TextField.svelte exactly (same design tokens: 12/16px + row padding, transparent-at-rest input border, ch-width inputs) rather + than libadwaita's own AdwActionRow/AdwSpinRow padding and internal + spin-button sizing, which run noticeably taller/wider and don't expose a + way to constrain from the outside. `list.boxed-list` already gets its + surface fill + radius from the shared stylesheet; this only adds the + compact row padding and the divider between rows. */ +list.boxed-list row.field-row { padding: 12px 16px; min-height: 0; } +list.boxed-list row.field-row:not(:last-child) { border-bottom: 1px solid alpha(@on-surface, 0.08); } +.field-row-subtitle { opacity: 0.6; font-size: 12px; } + +.field-input { + background-color: @bg; + color: @on-surface; + border: 1px solid transparent; + border-radius: 6px; + padding: 4px 8px; +} +.field-input:focus-within { border-color: @accent; outline: none; } + .popup-entry { background: @bg; color: @fg; From 18bdba052d7c761eaba35e6c786bd58446bf8b43 Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 31 Jul 2026 09:11:54 +0800 Subject: [PATCH 26/53] Update Cargo.lock to bread-ecosystem's latest merged dev commit --- Cargo.lock | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17001a5..0626e89 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=dev#4e76bc707720fbfc52e19a8cc78f3fb1e6e0a713" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#f8f69f4ae528cd39208f12c0ef0cb814fd155645" dependencies = [ "anyhow", "bread-utils", @@ -315,6 +315,7 @@ dependencies = [ [[package]] name = "bread-theme" version = "0.3.1" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#f8f69f4ae528cd39208f12c0ef0cb814fd155645" dependencies = [ "dirs 5.0.1", "gtk4", @@ -326,7 +327,7 @@ dependencies = [ [[package]] name = "bread-utils" version = "0.3.1" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#4e76bc707720fbfc52e19a8cc78f3fb1e6e0a713" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#f8f69f4ae528cd39208f12c0ef0cb814fd155645" dependencies = [ "dirs 5.0.1", "serde", From 0f6eb65cdf5cec7b6949335b858398eecc954a99 Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 31 Jul 2026 11:05:29 +0800 Subject: [PATCH 27/53] =?UTF-8?q?CI:=20single-trunk=20model=20=E2=80=94=20?= =?UTF-8?q?dev=20triggers=20on=20main,=20beta=20becomes=20RC-tag-triggered?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the dev/beta branch split with one trunk (main): dev-track builds still publish on every push, but the beta track now publishes from a vX.Y.Z-rc.N prerelease tag instead of a separately-maintained beta branch. Removes the branch nobody reliably kept in sync. --- .forgejo/workflows/dev-release.yml | 15 ++++---- .forgejo/workflows/mirror.yml | 21 ---------- .../{beta-release.yml => rc-release.yml} | 38 +++++-------------- .forgejo/workflows/release.yml | 1 + 4 files changed, 17 insertions(+), 58 deletions(-) delete mode 100644 .forgejo/workflows/mirror.yml rename .forgejo/workflows/{beta-release.yml => rc-release.yml} (59%) diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml index 6da2f3e..ce6ea8f 100644 --- a/.forgejo/workflows/dev-release.yml +++ b/.forgejo/workflows/dev-release.yml @@ -1,12 +1,11 @@ name: dev release -# Publishes a dev-track build on every push to `dev` — -# separate from release.yml's tag-triggered stable releases. See -# bread-ecosystem's docs/release-channels.md for the three-track policy -# this is part of. +# Publishes a dev-track build on every push to `main` (the trunk +# branch — there is no separate `dev` branch). See bread-ecosystem's +# docs/release-channels.md for the release-track policy this is part of. on: push: - branches: ['dev'] + branches: ['main'] jobs: build: @@ -16,7 +15,7 @@ jobs: run: | set -euo pipefail rm -rf src && mkdir src - git clone --branch dev --depth 1 \ + git clone --branch main --depth 1 \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - name: build @@ -33,7 +32,7 @@ jobs: # what's already installed and bakery would correctly refuse it. LATEST_TAG="$(git ls-remote --tags --refs \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" 'v*' \ - | awk -F/ '{print $NF}' | sed 's/^v//' | sort -V | tail -1)" + | awk -F/ '{print $NF}' | sed 's/^v//' | (grep -v -- '-' || true) | sort -V | tail -1)" if [ -n "${LATEST_TAG}" ]; then CUR="${LATEST_TAG}" else @@ -75,6 +74,6 @@ jobs: # mktemp: a fixed clone path races when multiple repos' dev/beta # workflows run close together on the same self-hosted runner. ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)" - git clone --branch dev https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" + git clone --branch main https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" TRACK=dev bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh" rm -rf "${ECOSYSTEM_CI_DIR}" diff --git a/.forgejo/workflows/mirror.yml b/.forgejo/workflows/mirror.yml deleted file mode 100644 index e91c97e..0000000 --- a/.forgejo/workflows/mirror.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Mirror to GitHub - -on: - push: - branches: ['**'] - tags: ['**'] - -jobs: - mirror: - runs-on: [self-hosted, hestia] - steps: - - name: Mirror to GitHub - run: | - set -euo pipefail - git clone --mirror "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" repo.git - cd repo.git - # Mirror only branches and tags (not refs/pull/*, which GitHub rejects); - # --prune deletes GitHub refs that no longer exist on Forgejo. - git push --prune \ - "https://x-access-token:${{ secrets.MIRROR_TOKEN }}@github.com/Breadway/breadpad.git" \ - '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' diff --git a/.forgejo/workflows/beta-release.yml b/.forgejo/workflows/rc-release.yml similarity index 59% rename from .forgejo/workflows/beta-release.yml rename to .forgejo/workflows/rc-release.yml index 5107391..bd3446b 100644 --- a/.forgejo/workflows/beta-release.yml +++ b/.forgejo/workflows/rc-release.yml @@ -1,52 +1,32 @@ -name: beta release +name: beta (rc) release -# Publishes a beta-track build on every push to `beta` — a frozen -# stabilization branch cut from `dev` when ready to stabilize; only -# fix/ branches merged into `beta` should land here afterward. -# See bread-ecosystem's docs/release-channels.md for the three-track policy. +# Publishes a beta-track build for any `vX.Y.Z-rc.N` prerelease tag +# pushed to `main` — there is no separate `beta` branch; "freezing" is +# just pausing pushes to main while an RC gets tested. See +# bread-ecosystem's docs/release-channels.md for the release-track policy. on: push: - branches: ['beta'] + tags: ['v*'] jobs: build: + if: ${{ contains(github.ref_name, '-rc.') }} runs-on: [self-hosted, hestia] steps: - name: checkout run: | set -euo pipefail rm -rf src && mkdir src - git clone --branch beta --depth 1 \ + git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - name: build run: cd src && cargo build --release --locked - - name: compute beta version - run: | - set -euo pipefail - cd src - # Base the beta version off the latest published stable tag, - # not Cargo.toml — Cargo.toml can go stale relative to the last - # real release (seen in practice: breadbox/breadpad/breadcrumbs/ - # breadpaper), which would make a beta build sort as OLDER than - # what's already installed and bakery would correctly refuse it. - LATEST_TAG="$(git ls-remote --tags --refs \ - "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" 'v*' \ - | awk -F/ '{print $NF}' | sed 's/^v//' | sort -V | tail -1)" - if [ -n "${LATEST_TAG}" ]; then - CUR="${LATEST_TAG}" - else - CUR="$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"(.*)".*/\1/')" - fi - IFS='.' read -r MA MI PA <<< "${CUR}" - SHA="$(git rev-parse --short HEAD)" - TS="$(date -u +%Y%m%d%H%M%S)" - echo "VERSION=${MA}.${MI}.$((PA + 1))-beta.${TS}+${SHA}" >> "$GITHUB_ENV" - - name: prepare artifacts run: | set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" PKG_DIR="/srv/breadway-dl/beta/breadpad/${VERSION}" mkdir -p "${PKG_DIR}" for bin in breadpad breadman; do diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 03340ac..636caaf 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -6,6 +6,7 @@ on: jobs: build: + if: ${{ !contains(github.ref_name, '-rc.') }} runs-on: [self-hosted, hestia] steps: - name: checkout From 18a6e645e60cfc6923d87416108396c3c757f668 Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 31 Jul 2026 11:08:41 +0800 Subject: [PATCH 28/53] CONTRIBUTING.md: document single-trunk + RC-tag release model --- CONTRIBUTING.md | 71 ++++++++++++++++++++++--------------------------- 1 file changed, 32 insertions(+), 39 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 61c7a90..607f493 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,16 +7,10 @@ workflow as every other ecosystem product. ## Branches -- **`main`** — release branch, always tag-ready. Nothing is committed to it - directly; it only moves forward via a `beta` merge (see below). -- **`dev`** — integration branch. All day-to-day work lands here first. - Every push to `dev` automatically builds and publishes a **dev-track** - build (see Tracks below) — use this to test your change in a real install - before it goes any further. -- **`beta`** — a frozen stabilization branch, cut from `dev` periodically. - Every push to `beta` automatically builds and publishes a **beta-track** - build. While a freeze is active, only fixes for issues found *in that - freeze* should land on `beta`. +There is one long-lived branch: **`main`**. All day-to-day work lands here. +Every push to `main` automatically builds and publishes a **dev-track** +build (see Tracks below) — a real install you can test before cutting +anything more formal. New work — features and bug fixes alike — goes on a short-lived branch: @@ -25,28 +19,26 @@ feature/ fix/ ``` -Branch off `dev`, open a PR/push back into `dev` when ready. If you're fixing -something reported against an active `beta` freeze, branch off `beta` -instead, merge the fix there to unblock testers, and also forward the same -fix into `dev` so it doesn't quietly reappear next cycle. +Branch off `main`, open a PR/push back into `main` when ready. Short-lived +branches get deleted on merge — they never accumulate the kind of drift a +second long-lived branch does. ## The release cycle -1. Work accumulates on `dev` via `feature/x` / `fix/x` branches. Each push +There's no separate `beta` or release branch — "stable" and "beta" are both +just **tags** on `main`, not branches that need to be kept in sync: + +1. Work accumulates on `main` via `feature/x` / `fix/x` branches. Each push auto-publishes a dev build — install it with `bakery track set dev` and - `bakery update --all`, then report or fix anything broken with another - push to `dev`. -2. Once `dev` has gone roughly **a week** without new issues, `beta` is cut - fresh from `dev`'s current tip. This freezes it as the stabilization - target — `dev` keeps moving independently starting the next cycle. -3. `beta` is open for anyone to test: `bakery track set beta` and - `bakery update --all`. **File issues against anything you find on this - repo's Forgejo issue tracker.** Fixes land via `fix/` branches - merged into `beta`. -4. Once `beta` has gone roughly **a month** without new issues, it's merged - into `main` and tagged `vX.Y.Z` — that tag is what actually triggers the - stable release build. `beta` is then reset from `dev` to start the next - cycle. + `bakery update --all`, then fix anything broken with another push. +2. When you want to stabilize before a real release, tag a release + candidate: `git tag vX.Y.Z-rc.1 && git push origin vX.Y.Z-rc.1` (push to + both remotes). That tag alone triggers a beta-track build — + "freezing" is just pausing pushes to `main` while you test it, not a + branch operation. Cut `-rc.2`, `-rc.3`, etc. for further fixes. +3. Once an RC has gone without issues, tag the real release: + `git tag vX.Y.Z && git push origin vX.Y.Z` — that's what triggers the + signed stable release build. ## Tracks, from a user's perspective @@ -58,14 +50,15 @@ bakery update --all # pull the latest build on your current track | Track | What it is | Published from | |--------|-----------|-----------------| -| `stable` | The last tagged release | `main`, on a `vX.Y.Z` tag push | -| `beta` | Current stabilization freeze | `beta`, on every push | -| `dev` | Bleeding edge | `dev`, on every push | +| `stable` | The last tagged release | a `vX.Y.Z` tag | +| `beta` | Latest release candidate | a `vX.Y.Z-rc.N` tag | +| `dev` | Bleeding edge | `main`, on every push | -Dev/beta versions are auto-computed (`X.Y.Z-dev.+` / -`-beta.…`) from the latest published stable tag, so they always sort as -newer than what you have installed — no manual version bumping needed when -pushing to `dev` or `beta`. +Dev versions are auto-computed (`X.Y.Z-dev.+`) from the +latest published stable tag, so they always sort as newer than what you +have installed — no manual version bumping needed. Beta versions are just +the RC tag itself (already valid semver, already sorts below the real +release it's a candidate for). ## Local development @@ -76,10 +69,10 @@ cargo test --release --workspace ## CI -- `dev-release.yml` — triggered on push to `dev`. -- `beta-release.yml` — triggered on push to `beta`. -- `release.yml` — triggered on a `v*` tag push, cuts the actual stable release. -- `package.yml` — publishes to the `[breadway]` pacman repo, also tag-triggered. +- `dev-release.yml` — triggered on push to `main`. +- `rc-release.yml` — triggered on any `vX.Y.Z-rc.N` tag push. +- `release.yml` — triggered on any other `v*` tag push, cuts the actual + stable release. All CI runs on a self-hosted runner; nothing runs automatically on plain commits or PRs beyond the track builds above. See From 561dd46dcfc9c5638fbf7c364ff090c48ff163f8 Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 31 Jul 2026 14:30:01 +0800 Subject: [PATCH 29/53] Repoint bread-ecosystem git deps from dev to main branch --- Cargo.lock | 16 ++++++++-------- breadman/Cargo.toml | 4 ++-- breadpad-shared/Cargo.toml | 2 +- breadpad/Cargo.toml | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0626e89..1185b4f 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=dev#f8f69f4ae528cd39208f12c0ef0cb814fd155645" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=main#f86e299f4a0ea73ff485cd84923b986ddcc8242e" dependencies = [ "anyhow", "bread-utils", @@ -315,7 +315,7 @@ dependencies = [ [[package]] name = "bread-theme" version = "0.3.1" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#f8f69f4ae528cd39208f12c0ef0cb814fd155645" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=main#f86e299f4a0ea73ff485cd84923b986ddcc8242e" dependencies = [ "dirs 5.0.1", "gtk4", @@ -327,7 +327,7 @@ dependencies = [ [[package]] name = "bread-utils" version = "0.3.1" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#f8f69f4ae528cd39208f12c0ef0cb814fd155645" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=main#f86e299f4a0ea73ff485cd84923b986ddcc8242e" dependencies = [ "dirs 5.0.1", "serde", @@ -587,7 +587,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -951,7 +951,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1262,7 +1262,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2756,7 +2756,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3080,7 +3080,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/breadman/Cargo.toml b/breadman/Cargo.toml index 9e4bd5f..8bdd99c 100644 --- a/breadman/Cargo.toml +++ b/breadman/Cargo.toml @@ -12,11 +12,11 @@ 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 = "dev" } +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 = "dev", features = ["adw"] } +bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", branch = "main", features = ["adw"] } libadwaita = { version = "0.9", features = ["v1_7"] } anyhow.workspace = true tracing.workspace = true diff --git a/breadpad-shared/Cargo.toml b/breadpad-shared/Cargo.toml index 72fe7b7..0016141 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 = "dev", features = ["gtk"] } +bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", branch = "main", features = ["gtk"] } anyhow.workspace = true tracing.workspace = true serde.workspace = true diff --git a/breadpad/Cargo.toml b/breadpad/Cargo.toml index 4f53160..1fa8bae 100644 --- a/breadpad/Cargo.toml +++ b/breadpad/Cargo.toml @@ -12,7 +12,7 @@ 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 = "dev" } +bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", branch = "main" } anyhow.workspace = true ort.workspace = true tracing.workspace = true From 72f86f0675c475f2e0ddd68b07ad53c1a774a852 Mon Sep 17 00:00:00 2001 From: Breadway Date: Mon, 3 Aug 2026 11:43:19 +0800 Subject: [PATCH 30/53] fixed CI libadwaita version mismatch --- .forgejo/workflows/dev-release.yml | 25 +++++++++++-------------- .forgejo/workflows/rc-release.yml | 21 +++++++++++---------- .forgejo/workflows/release.yml | 13 +++++++++++-- 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml index ce6ea8f..358da4f 100644 --- a/.forgejo/workflows/dev-release.yml +++ b/.forgejo/workflows/dev-release.yml @@ -1,8 +1,5 @@ name: dev release -# Publishes a dev-track build on every push to `main` (the trunk -# branch — there is no separate `dev` branch). See bread-ecosystem's -# docs/release-channels.md for the release-track policy this is part of. on: push: branches: ['main'] @@ -18,18 +15,22 @@ jobs: git clone --branch main --depth 1 \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - - name: build - run: cd src && cargo build --release --locked + - name: build inside container + run: | + podman run --rm \ + -v "$(pwd)/src:/workspace" \ + -w /workspace \ + fedora:41 \ + bash -c " + set -euo pipefail + dnf install -y gcc pkgconf-pkg-config libadwaita-devel cargo + cargo build --release --locked + " - name: compute dev version run: | set -euo pipefail cd src - # Base the dev version off the latest published stable tag, - # not Cargo.toml — Cargo.toml can go stale relative to the last - # real release (seen in practice: breadbox/breadpad/breadcrumbs/ - # breadpaper), which would make a dev build sort as OLDER than - # what's already installed and bakery would correctly refuse it. LATEST_TAG="$(git ls-remote --tags --refs \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" 'v*' \ | awk -F/ '{print $NF}' | sed 's/^v//' | (grep -v -- '-' || true) | sort -V | tail -1)" @@ -59,8 +60,6 @@ jobs: cp src/bakery.toml "${PKG_DIR}/bakery.toml" ln -sfn "${VERSION}" "/srv/breadway-dl/dev/breadpad/latest" - # No GitHub Release upload — dev, like the other non-stable track, - # is only distributed via dl.breadway.dev/dev/. - name: regenerate dev index.json env: MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} @@ -71,8 +70,6 @@ jobs: exit 1 fi rm -rf /tmp/bread-ecosystem-ci-* 2>/dev/null || true - # mktemp: a fixed clone path races when multiple repos' dev/beta - # workflows run close together on the same self-hosted runner. ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)" git clone --branch main https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" TRACK=dev bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh" diff --git a/.forgejo/workflows/rc-release.yml b/.forgejo/workflows/rc-release.yml index bd3446b..f623e30 100644 --- a/.forgejo/workflows/rc-release.yml +++ b/.forgejo/workflows/rc-release.yml @@ -1,9 +1,5 @@ name: beta (rc) release -# Publishes a beta-track build for any `vX.Y.Z-rc.N` prerelease tag -# pushed to `main` — there is no separate `beta` branch; "freezing" is -# just pausing pushes to main while an RC gets tested. See -# bread-ecosystem's docs/release-channels.md for the release-track policy. on: push: tags: ['v*'] @@ -20,8 +16,17 @@ jobs: git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - - name: build - run: cd src && cargo build --release --locked + - name: build inside container + run: | + podman run --rm \ + -v "$(pwd)/src:/workspace" \ + -w /workspace \ + fedora:41 \ + bash -c " + set -euo pipefail + dnf install -y gcc pkgconf-pkg-config libadwaita-devel cargo + cargo build --release --locked + " - name: prepare artifacts run: | @@ -40,8 +45,6 @@ jobs: cp src/bakery.toml "${PKG_DIR}/bakery.toml" ln -sfn "${VERSION}" "/srv/breadway-dl/beta/breadpad/latest" - # No GitHub Release upload — beta, like dev, is only distributed via - # dl.breadway.dev/beta/. - name: regenerate beta index.json env: MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} @@ -52,8 +55,6 @@ jobs: exit 1 fi rm -rf /tmp/bread-ecosystem-ci-* 2>/dev/null || true - # mktemp: a fixed clone path races when multiple repos' dev/beta - # workflows run close together on the same self-hosted runner. ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)" git clone https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" TRACK=beta bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh" diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 636caaf..756504d 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -16,8 +16,17 @@ jobs: git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - - name: build - run: cd src && cargo build --release --locked + - name: build inside container + run: | + podman run --rm \ + -v "$(pwd)/src:/workspace" \ + -w /workspace \ + fedora:41 \ + bash -c " + set -euo pipefail + dnf install -y gcc pkgconf-pkg-config libadwaita-devel cargo + cargo build --release --locked + " - name: prepare artifacts run: | From d46fb98321958e714ff5dacb6737379ffa64ef5b Mon Sep 17 00:00:00 2001 From: Breadway Date: Mon, 3 Aug 2026 11:48:42 +0800 Subject: [PATCH 31/53] fixed CI libadwaita version mismatch --- .forgejo/workflows/dev-release.yml | 2 +- .forgejo/workflows/rc-release.yml | 2 +- .forgejo/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml index 358da4f..b311ac8 100644 --- a/.forgejo/workflows/dev-release.yml +++ b/.forgejo/workflows/dev-release.yml @@ -17,7 +17,7 @@ jobs: - name: build inside container run: | - podman run --rm \ + docker run --rm \ -v "$(pwd)/src:/workspace" \ -w /workspace \ fedora:41 \ diff --git a/.forgejo/workflows/rc-release.yml b/.forgejo/workflows/rc-release.yml index f623e30..5d8271a 100644 --- a/.forgejo/workflows/rc-release.yml +++ b/.forgejo/workflows/rc-release.yml @@ -18,7 +18,7 @@ jobs: - name: build inside container run: | - podman run --rm \ + docker run --rm \ -v "$(pwd)/src:/workspace" \ -w /workspace \ fedora:41 \ diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 756504d..eba2145 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: build inside container run: | - podman run --rm \ + docker run --rm \ -v "$(pwd)/src:/workspace" \ -w /workspace \ fedora:41 \ From c60f3abf0726785cebc660500da2b64d8b45ac32 Mon Sep 17 00:00:00 2001 From: Breadway Date: Mon, 3 Aug 2026 12:58:08 +0800 Subject: [PATCH 32/53] fixed CI rust dep version mismatch --- .forgejo/workflows/dev-release.yml | 20 +++++++++++++++++--- .forgejo/workflows/rc-release.yml | 16 +++++++++++++--- .forgejo/workflows/release.yml | 8 +++++--- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml index b311ac8..3751afa 100644 --- a/.forgejo/workflows/dev-release.yml +++ b/.forgejo/workflows/dev-release.yml @@ -1,5 +1,8 @@ name: dev release +# Publishes a dev-track build on every push to `main` (the trunk +# branch — there is no separate `dev` branch). See bread-ecosystem's +# docs/release-channels.md for the release-track policy this is part of. on: push: branches: ['main'] @@ -17,13 +20,15 @@ jobs: - name: build inside container run: | - docker run --rm \ + podman run --rm \ -v "$(pwd)/src:/workspace" \ -w /workspace \ fedora:41 \ bash -c " set -euo pipefail - dnf install -y gcc pkgconf-pkg-config libadwaita-devel cargo + dnf install -y gcc pkgconf-pkg-config libadwaita-devel git curl + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + source \$HOME/.cargo/env cargo build --release --locked " @@ -31,6 +36,11 @@ jobs: run: | set -euo pipefail cd src + # Base the dev version off the latest published stable tag, + # not Cargo.toml — Cargo.toml can go stale relative to the last + # real release (seen in practice: breadbox/breadpad/breadcrumbs/ + # breadpaper), which would make a dev build sort as OLDER than + # what's already installed and bakery would correctly refuse it. LATEST_TAG="$(git ls-remote --tags --refs \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" 'v*' \ | awk -F/ '{print $NF}' | sed 's/^v//' | (grep -v -- '-' || true) | sort -V | tail -1)" @@ -60,6 +70,8 @@ jobs: cp src/bakery.toml "${PKG_DIR}/bakery.toml" ln -sfn "${VERSION}" "/srv/breadway-dl/dev/breadpad/latest" + # No GitHub Release upload — dev, like the other non-stable track, + # is only distributed via dl.breadway.dev/dev/. - name: regenerate dev index.json env: MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} @@ -70,7 +82,9 @@ jobs: exit 1 fi rm -rf /tmp/bread-ecosystem-ci-* 2>/dev/null || true + # mktemp: a fixed clone path races when multiple repos' dev/beta + # workflows run close together on the same self-hosted runner. ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)" git clone --branch main https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" TRACK=dev bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh" - rm -rf "${ECOSYSTEM_CI_DIR}" + rm -rf "${ECOSYSTEM_CI_DIR}" \ No newline at end of file diff --git a/.forgejo/workflows/rc-release.yml b/.forgejo/workflows/rc-release.yml index 5d8271a..295883e 100644 --- a/.forgejo/workflows/rc-release.yml +++ b/.forgejo/workflows/rc-release.yml @@ -1,5 +1,9 @@ name: beta (rc) release +# Publishes a beta-track build for any `vX.Y.Z-rc.N` prerelease tag +# pushed to `main` — there is no separate `beta` branch; "freezing" is +# just pausing pushes to main while an RC gets tested. See +# bread-ecosystem's docs/release-channels.md for the release-track policy. on: push: tags: ['v*'] @@ -18,13 +22,15 @@ jobs: - name: build inside container run: | - docker run --rm \ + podman run --rm \ -v "$(pwd)/src:/workspace" \ -w /workspace \ fedora:41 \ bash -c " set -euo pipefail - dnf install -y gcc pkgconf-pkg-config libadwaita-devel cargo + dnf install -y gcc pkgconf-pkg-config libadwaita-devel git curl + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + source \$HOME/.cargo/env cargo build --release --locked " @@ -45,6 +51,8 @@ jobs: cp src/bakery.toml "${PKG_DIR}/bakery.toml" ln -sfn "${VERSION}" "/srv/breadway-dl/beta/breadpad/latest" + # No GitHub Release upload — beta, like dev, is only distributed via + # dl.breadway.dev/beta/. - name: regenerate beta index.json env: MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} @@ -55,7 +63,9 @@ jobs: exit 1 fi rm -rf /tmp/bread-ecosystem-ci-* 2>/dev/null || true + # mktemp: a fixed clone path races when multiple repos' dev/beta + # workflows run close together on the same self-hosted runner. ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)" git clone https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" TRACK=beta bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh" - rm -rf "${ECOSYSTEM_CI_DIR}" + rm -rf "${ECOSYSTEM_CI_DIR}" \ No newline at end of file diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index eba2145..b31801d 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -18,13 +18,15 @@ jobs: - name: build inside container run: | - docker run --rm \ + podman run --rm \ -v "$(pwd)/src:/workspace" \ -w /workspace \ fedora:41 \ bash -c " set -euo pipefail - dnf install -y gcc pkgconf-pkg-config libadwaita-devel cargo + dnf install -y gcc pkgconf-pkg-config libadwaita-devel git curl + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + source \$HOME/.cargo/env cargo build --release --locked " @@ -66,4 +68,4 @@ jobs: "${PKG_DIR}/breadman-x86_64" \ "${PKG_DIR}/breadpad-x86_64.sha256" \ "${PKG_DIR}/breadman-x86_64.sha256" \ - --clobber + --clobber \ No newline at end of file From c462f8993462e98a15e474bf31d1dce905879f04 Mon Sep 17 00:00:00 2001 From: Breadway Date: Mon, 3 Aug 2026 12:59:00 +0800 Subject: [PATCH 33/53] fixed CI rust dep version mismatch --- .forgejo/workflows/dev-release.yml | 2 +- .forgejo/workflows/rc-release.yml | 2 +- .forgejo/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml index 3751afa..2d915f1 100644 --- a/.forgejo/workflows/dev-release.yml +++ b/.forgejo/workflows/dev-release.yml @@ -20,7 +20,7 @@ jobs: - name: build inside container run: | - podman run --rm \ + docker run --rm \ -v "$(pwd)/src:/workspace" \ -w /workspace \ fedora:41 \ diff --git a/.forgejo/workflows/rc-release.yml b/.forgejo/workflows/rc-release.yml index 295883e..40bf95b 100644 --- a/.forgejo/workflows/rc-release.yml +++ b/.forgejo/workflows/rc-release.yml @@ -22,7 +22,7 @@ jobs: - name: build inside container run: | - podman run --rm \ + docker run --rm \ -v "$(pwd)/src:/workspace" \ -w /workspace \ fedora:41 \ diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index b31801d..a33bffa 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: build inside container run: | - podman run --rm \ + docker run --rm \ -v "$(pwd)/src:/workspace" \ -w /workspace \ fedora:41 \ From bd18aa2a3a1ca5ad37eab7e3dc770d6adc9802e7 Mon Sep 17 00:00:00 2001 From: Breadway Date: Mon, 3 Aug 2026 13:10:57 +0800 Subject: [PATCH 34/53] fixed CI rust dep version mismatch --- .forgejo/workflows/dev-release.yml | 14 +------------- .forgejo/workflows/rc-release.yml | 10 +--------- .forgejo/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 23 deletions(-) diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml index 2d915f1..5e22705 100644 --- a/.forgejo/workflows/dev-release.yml +++ b/.forgejo/workflows/dev-release.yml @@ -1,8 +1,5 @@ name: dev release -# Publishes a dev-track build on every push to `main` (the trunk -# branch — there is no separate `dev` branch). See bread-ecosystem's -# docs/release-channels.md for the release-track policy this is part of. on: push: branches: ['main'] @@ -23,7 +20,7 @@ jobs: docker run --rm \ -v "$(pwd)/src:/workspace" \ -w /workspace \ - fedora:41 \ + fedora:rawhide \ bash -c " set -euo pipefail dnf install -y gcc pkgconf-pkg-config libadwaita-devel git curl @@ -36,11 +33,6 @@ jobs: run: | set -euo pipefail cd src - # Base the dev version off the latest published stable tag, - # not Cargo.toml — Cargo.toml can go stale relative to the last - # real release (seen in practice: breadbox/breadpad/breadcrumbs/ - # breadpaper), which would make a dev build sort as OLDER than - # what's already installed and bakery would correctly refuse it. LATEST_TAG="$(git ls-remote --tags --refs \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" 'v*' \ | awk -F/ '{print $NF}' | sed 's/^v//' | (grep -v -- '-' || true) | sort -V | tail -1)" @@ -70,8 +62,6 @@ jobs: cp src/bakery.toml "${PKG_DIR}/bakery.toml" ln -sfn "${VERSION}" "/srv/breadway-dl/dev/breadpad/latest" - # No GitHub Release upload — dev, like the other non-stable track, - # is only distributed via dl.breadway.dev/dev/. - name: regenerate dev index.json env: MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} @@ -82,8 +72,6 @@ jobs: exit 1 fi rm -rf /tmp/bread-ecosystem-ci-* 2>/dev/null || true - # mktemp: a fixed clone path races when multiple repos' dev/beta - # workflows run close together on the same self-hosted runner. ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)" git clone --branch main https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" TRACK=dev bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh" diff --git a/.forgejo/workflows/rc-release.yml b/.forgejo/workflows/rc-release.yml index 40bf95b..4ba90ce 100644 --- a/.forgejo/workflows/rc-release.yml +++ b/.forgejo/workflows/rc-release.yml @@ -1,9 +1,5 @@ name: beta (rc) release -# Publishes a beta-track build for any `vX.Y.Z-rc.N` prerelease tag -# pushed to `main` — there is no separate `beta` branch; "freezing" is -# just pausing pushes to main while an RC gets tested. See -# bread-ecosystem's docs/release-channels.md for the release-track policy. on: push: tags: ['v*'] @@ -25,7 +21,7 @@ jobs: docker run --rm \ -v "$(pwd)/src:/workspace" \ -w /workspace \ - fedora:41 \ + fedora:rawhide \ bash -c " set -euo pipefail dnf install -y gcc pkgconf-pkg-config libadwaita-devel git curl @@ -51,8 +47,6 @@ jobs: cp src/bakery.toml "${PKG_DIR}/bakery.toml" ln -sfn "${VERSION}" "/srv/breadway-dl/beta/breadpad/latest" - # No GitHub Release upload — beta, like dev, is only distributed via - # dl.breadway.dev/beta/. - name: regenerate beta index.json env: MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} @@ -63,8 +57,6 @@ jobs: exit 1 fi rm -rf /tmp/bread-ecosystem-ci-* 2>/dev/null || true - # mktemp: a fixed clone path races when multiple repos' dev/beta - # workflows run close together on the same self-hosted runner. ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)" git clone https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" TRACK=beta bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh" diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index a33bffa..f066dcb 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -21,7 +21,7 @@ jobs: docker run --rm \ -v "$(pwd)/src:/workspace" \ -w /workspace \ - fedora:41 \ + fedora:rawhide \ bash -c " set -euo pipefail dnf install -y gcc pkgconf-pkg-config libadwaita-devel git curl From 58309ba00547bd07905ed4b8a3d9fa96295875d5 Mon Sep 17 00:00:00 2001 From: Breadway Date: Mon, 3 Aug 2026 13:17:44 +0800 Subject: [PATCH 35/53] Please work --- .forgejo/workflows/dev-release.yml | 14 +++++++++++--- .forgejo/workflows/rc-release.yml | 14 +++++++++++--- .forgejo/workflows/release.yml | 14 +++++++++++--- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml index 5e22705..6718c2e 100644 --- a/.forgejo/workflows/dev-release.yml +++ b/.forgejo/workflows/dev-release.yml @@ -17,13 +17,21 @@ jobs: - name: build inside container run: | - docker run --rm \ + podman run --rm \ -v "$(pwd)/src:/workspace" \ -w /workspace \ - fedora:rawhide \ + fedora:41 \ bash -c " set -euo pipefail - dnf install -y gcc pkgconf-pkg-config libadwaita-devel git curl + dnf install -y gcc pkgconf-pkg-config git curl meson ninja-build valac gtk4-devel graphene-devel + + # Build Libadwaita 1.7.0 from source inside container + git clone https://gitlab.gnome.org/GNOME/libadwaita.git --branch 1.7.0 --depth 1 /tmp/libadwaita + meson setup /tmp/libadwaita/build /tmp/libadwaita --prefix=/usr + ninja -C /tmp/libadwaita/build + ninja -C /tmp/libadwaita/build install + + # Install Rust and build curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable source \$HOME/.cargo/env cargo build --release --locked diff --git a/.forgejo/workflows/rc-release.yml b/.forgejo/workflows/rc-release.yml index 4ba90ce..632b3fb 100644 --- a/.forgejo/workflows/rc-release.yml +++ b/.forgejo/workflows/rc-release.yml @@ -18,13 +18,21 @@ jobs: - name: build inside container run: | - docker run --rm \ + podman run --rm \ -v "$(pwd)/src:/workspace" \ -w /workspace \ - fedora:rawhide \ + fedora:41 \ bash -c " set -euo pipefail - dnf install -y gcc pkgconf-pkg-config libadwaita-devel git curl + dnf install -y gcc pkgconf-pkg-config git curl meson ninja-build valac gtk4-devel graphene-devel + + # Build Libadwaita 1.7.0 from source inside container + git clone https://gitlab.gnome.org/GNOME/libadwaita.git --branch 1.7.0 --depth 1 /tmp/libadwaita + meson setup /tmp/libadwaita/build /tmp/libadwaita --prefix=/usr + ninja -C /tmp/libadwaita/build + ninja -C /tmp/libadwaita/build install + + # Install Rust and build curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable source \$HOME/.cargo/env cargo build --release --locked diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index f066dcb..a362070 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -18,13 +18,21 @@ jobs: - name: build inside container run: | - docker run --rm \ + podman run --rm \ -v "$(pwd)/src:/workspace" \ -w /workspace \ - fedora:rawhide \ + fedora:41 \ bash -c " set -euo pipefail - dnf install -y gcc pkgconf-pkg-config libadwaita-devel git curl + dnf install -y gcc pkgconf-pkg-config git curl meson ninja-build valac gtk4-devel graphene-devel + + # Build Libadwaita 1.7.0 from source inside container + git clone https://gitlab.gnome.org/GNOME/libadwaita.git --branch 1.7.0 --depth 1 /tmp/libadwaita + meson setup /tmp/libadwaita/build /tmp/libadwaita --prefix=/usr + ninja -C /tmp/libadwaita/build + ninja -C /tmp/libadwaita/build install + + # Install Rust and build curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable source \$HOME/.cargo/env cargo build --release --locked From 80941518f7e3c1d6a0f1cc68e7fd225a6f5a88b1 Mon Sep 17 00:00:00 2001 From: Breadway Date: Mon, 3 Aug 2026 13:19:15 +0800 Subject: [PATCH 36/53] swap to docker --- .forgejo/workflows/dev-release.yml | 2 +- .forgejo/workflows/rc-release.yml | 2 +- .forgejo/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml index 6718c2e..197d57a 100644 --- a/.forgejo/workflows/dev-release.yml +++ b/.forgejo/workflows/dev-release.yml @@ -17,7 +17,7 @@ jobs: - name: build inside container run: | - podman run --rm \ + docker run --rm \ -v "$(pwd)/src:/workspace" \ -w /workspace \ fedora:41 \ diff --git a/.forgejo/workflows/rc-release.yml b/.forgejo/workflows/rc-release.yml index 632b3fb..5b21139 100644 --- a/.forgejo/workflows/rc-release.yml +++ b/.forgejo/workflows/rc-release.yml @@ -18,7 +18,7 @@ jobs: - name: build inside container run: | - podman run --rm \ + docker run --rm \ -v "$(pwd)/src:/workspace" \ -w /workspace \ fedora:41 \ diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index a362070..4aed984 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: build inside container run: | - podman run --rm \ + docker run --rm \ -v "$(pwd)/src:/workspace" \ -w /workspace \ fedora:41 \ From 2b5ad272a588b9e8253fa84a22006b8a8b962037 Mon Sep 17 00:00:00 2001 From: Breadway Date: Mon, 3 Aug 2026 13:22:35 +0800 Subject: [PATCH 37/53] please work :cry: --- .forgejo/workflows/dev-release.yml | 6 +++--- .forgejo/workflows/rc-release.yml | 6 +++--- .forgejo/workflows/release.yml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml index 197d57a..cf92ac0 100644 --- a/.forgejo/workflows/dev-release.yml +++ b/.forgejo/workflows/dev-release.yml @@ -23,11 +23,11 @@ jobs: fedora:41 \ bash -c " set -euo pipefail - dnf install -y gcc pkgconf-pkg-config git curl meson ninja-build valac gtk4-devel graphene-devel + dnf install -y gcc gcc-c++ pkgconf-pkg-config git curl meson ninja-build valac gtk4-devel graphene-devel - # Build Libadwaita 1.7.0 from source inside container + # Build Libadwaita 1.7.0 from source inside container (skipping tests & docs) git clone https://gitlab.gnome.org/GNOME/libadwaita.git --branch 1.7.0 --depth 1 /tmp/libadwaita - meson setup /tmp/libadwaita/build /tmp/libadwaita --prefix=/usr + meson setup /tmp/libadwaita/build /tmp/libadwaita --prefix=/usr -Dtests=false -Ddoc=false ninja -C /tmp/libadwaita/build ninja -C /tmp/libadwaita/build install diff --git a/.forgejo/workflows/rc-release.yml b/.forgejo/workflows/rc-release.yml index 5b21139..493710e 100644 --- a/.forgejo/workflows/rc-release.yml +++ b/.forgejo/workflows/rc-release.yml @@ -24,11 +24,11 @@ jobs: fedora:41 \ bash -c " set -euo pipefail - dnf install -y gcc pkgconf-pkg-config git curl meson ninja-build valac gtk4-devel graphene-devel + dnf install -y gcc gcc-c++ pkgconf-pkg-config git curl meson ninja-build valac gtk4-devel graphene-devel - # Build Libadwaita 1.7.0 from source inside container + # Build Libadwaita 1.7.0 from source inside container (skipping tests & docs) git clone https://gitlab.gnome.org/GNOME/libadwaita.git --branch 1.7.0 --depth 1 /tmp/libadwaita - meson setup /tmp/libadwaita/build /tmp/libadwaita --prefix=/usr + meson setup /tmp/libadwaita/build /tmp/libadwaita --prefix=/usr -Dtests=false -Ddoc=false ninja -C /tmp/libadwaita/build ninja -C /tmp/libadwaita/build install diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 4aed984..91a05c9 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -24,11 +24,11 @@ jobs: fedora:41 \ bash -c " set -euo pipefail - dnf install -y gcc pkgconf-pkg-config git curl meson ninja-build valac gtk4-devel graphene-devel + dnf install -y gcc gcc-c++ pkgconf-pkg-config git curl meson ninja-build valac gtk4-devel graphene-devel - # Build Libadwaita 1.7.0 from source inside container + # Build Libadwaita 1.7.0 from source inside container (skipping tests & docs) git clone https://gitlab.gnome.org/GNOME/libadwaita.git --branch 1.7.0 --depth 1 /tmp/libadwaita - meson setup /tmp/libadwaita/build /tmp/libadwaita --prefix=/usr + meson setup /tmp/libadwaita/build /tmp/libadwaita --prefix=/usr -Dtests=false -Ddoc=false ninja -C /tmp/libadwaita/build ninja -C /tmp/libadwaita/build install From 3225f49a9349a2d813be5f5a435fb87de1fe1eff Mon Sep 17 00:00:00 2001 From: Breadway Date: Tue, 4 Aug 2026 17:31:19 +0800 Subject: [PATCH 38/53] CI: replace from-source libadwaita build with pinned Arch container The dev/rc/release workflows rebuilt libadwaita from source inside an uncached Fedora container on every single run, because Ubuntu 24.04's packaged libadwaita was too old for gtk4 0.11's v4_12 feature. That from-source build broke repeatedly on drift (rust dep versions, libadwaita ABI, and finally a removed meson option), producing eight straight failed CI runs. Arch's repos already carry current gtk4/libadwaita/gtk4-layer-shell as prebuilt packages, and breadpad only targets BOS/Arch, so there's no reason to build anything from source. Swap the container base to a digest-pinned archlinux image (ci/Containerfile) with those packages installed via pacman, and extract the shared "build the image, run cargo inside it" logic into ci/build.sh so it isn't duplicated three times across the workflows. Cargo's registry/git/target caches persist in named docker volumes across runs. Also: - Add check.yml: clippy + test on feature/**/fix/** pushes, so lint/ build breakage surfaces before it reaches main and triggers a dev-track release. - Fix release.yml's tag trigger (tags-ignore) so an rc tag push no longer also spawns a skipped release.yml run alongside rc-release.yml. Verified locally: the container builds cleanly via pacman (no source compilation), and `cargo build --release --locked --workspace` inside it produces working breadpad/breadman binaries linked against libgtk4-layer-shell.so.0 and libadwaita-1.so.0 respectively. --- .forgejo/workflows/check.yml | 24 +++++++++++++++++++++++ .forgejo/workflows/dev-release.yml | 25 +++--------------------- .forgejo/workflows/rc-release.yml | 28 ++++----------------------- .forgejo/workflows/release.yml | 29 +++++----------------------- CONTRIBUTING.md | 12 ++++++++++-- ci/Containerfile | 22 +++++++++++++++++++++ ci/build.sh | 31 ++++++++++++++++++++++++++++++ 7 files changed, 99 insertions(+), 72 deletions(-) create mode 100644 .forgejo/workflows/check.yml create mode 100644 ci/Containerfile create mode 100755 ci/build.sh diff --git a/.forgejo/workflows/check.yml b/.forgejo/workflows/check.yml new file mode 100644 index 0000000..b547c34 --- /dev/null +++ b/.forgejo/workflows/check.yml @@ -0,0 +1,24 @@ +name: check + +# Fast-fail lint/test on short-lived work branches, before it ever reaches +# main and triggers a dev-track release build. +on: + push: + branches: ['feature/**', 'fix/**'] + +jobs: + check: + runs-on: [self-hosted, hestia] + steps: + - name: checkout + run: | + set -euo pipefail + rm -rf src && mkdir src + git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src + + - name: clippy + run: cd src && bash ci/build.sh cargo clippy --workspace --all-targets --locked -- -D warnings + + - name: test + run: cd src && bash ci/build.sh cargo test --workspace --locked diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml index cf92ac0..11f75d2 100644 --- a/.forgejo/workflows/dev-release.yml +++ b/.forgejo/workflows/dev-release.yml @@ -15,27 +15,8 @@ jobs: git clone --branch main --depth 1 \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - - name: build inside container - run: | - docker run --rm \ - -v "$(pwd)/src:/workspace" \ - -w /workspace \ - fedora:41 \ - bash -c " - set -euo pipefail - dnf install -y gcc gcc-c++ pkgconf-pkg-config git curl meson ninja-build valac gtk4-devel graphene-devel - - # Build Libadwaita 1.7.0 from source inside container (skipping tests & docs) - git clone https://gitlab.gnome.org/GNOME/libadwaita.git --branch 1.7.0 --depth 1 /tmp/libadwaita - meson setup /tmp/libadwaita/build /tmp/libadwaita --prefix=/usr -Dtests=false -Ddoc=false - ninja -C /tmp/libadwaita/build - ninja -C /tmp/libadwaita/build install - - # Install Rust and build - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable - source \$HOME/.cargo/env - cargo build --release --locked - " + - name: build + run: cd src && bash ci/build.sh cargo build --release --locked - name: compute dev version run: | @@ -83,4 +64,4 @@ jobs: ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)" git clone --branch main https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" TRACK=dev bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh" - rm -rf "${ECOSYSTEM_CI_DIR}" \ No newline at end of file + rm -rf "${ECOSYSTEM_CI_DIR}" diff --git a/.forgejo/workflows/rc-release.yml b/.forgejo/workflows/rc-release.yml index 493710e..aad2f82 100644 --- a/.forgejo/workflows/rc-release.yml +++ b/.forgejo/workflows/rc-release.yml @@ -2,11 +2,10 @@ name: beta (rc) release on: push: - tags: ['v*'] + tags: ['v*-rc.*'] jobs: build: - if: ${{ contains(github.ref_name, '-rc.') }} runs-on: [self-hosted, hestia] steps: - name: checkout @@ -16,27 +15,8 @@ jobs: git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - - name: build inside container - run: | - docker run --rm \ - -v "$(pwd)/src:/workspace" \ - -w /workspace \ - fedora:41 \ - bash -c " - set -euo pipefail - dnf install -y gcc gcc-c++ pkgconf-pkg-config git curl meson ninja-build valac gtk4-devel graphene-devel - - # Build Libadwaita 1.7.0 from source inside container (skipping tests & docs) - git clone https://gitlab.gnome.org/GNOME/libadwaita.git --branch 1.7.0 --depth 1 /tmp/libadwaita - meson setup /tmp/libadwaita/build /tmp/libadwaita --prefix=/usr -Dtests=false -Ddoc=false - ninja -C /tmp/libadwaita/build - ninja -C /tmp/libadwaita/build install - - # Install Rust and build - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable - source \$HOME/.cargo/env - cargo build --release --locked - " + - name: build + run: cd src && bash ci/build.sh cargo build --release --locked - name: prepare artifacts run: | @@ -68,4 +48,4 @@ jobs: ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)" git clone https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" TRACK=beta bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh" - rm -rf "${ECOSYSTEM_CI_DIR}" \ No newline at end of file + rm -rf "${ECOSYSTEM_CI_DIR}" diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 91a05c9..d21777c 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -2,11 +2,11 @@ name: release on: push: - tags: ["v*"] + tags: ['v*'] + tags-ignore: ['v*-rc.*'] jobs: build: - if: ${{ !contains(github.ref_name, '-rc.') }} runs-on: [self-hosted, hestia] steps: - name: checkout @@ -16,27 +16,8 @@ jobs: git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - - name: build inside container - run: | - docker run --rm \ - -v "$(pwd)/src:/workspace" \ - -w /workspace \ - fedora:41 \ - bash -c " - set -euo pipefail - dnf install -y gcc gcc-c++ pkgconf-pkg-config git curl meson ninja-build valac gtk4-devel graphene-devel - - # Build Libadwaita 1.7.0 from source inside container (skipping tests & docs) - git clone https://gitlab.gnome.org/GNOME/libadwaita.git --branch 1.7.0 --depth 1 /tmp/libadwaita - meson setup /tmp/libadwaita/build /tmp/libadwaita --prefix=/usr -Dtests=false -Ddoc=false - ninja -C /tmp/libadwaita/build - ninja -C /tmp/libadwaita/build install - - # Install Rust and build - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable - source \$HOME/.cargo/env - cargo build --release --locked - " + - name: build + run: cd src && bash ci/build.sh cargo build --release --locked - name: prepare artifacts run: | @@ -76,4 +57,4 @@ jobs: "${PKG_DIR}/breadman-x86_64" \ "${PKG_DIR}/breadpad-x86_64.sha256" \ "${PKG_DIR}/breadman-x86_64.sha256" \ - --clobber \ No newline at end of file + --clobber diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 607f493..93077a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -69,13 +69,21 @@ cargo test --release --workspace ## CI +- `check.yml` — clippy + test, triggered on push to `feature/**`/`fix/**`. + Fast-fail before anything reaches `main`. - `dev-release.yml` — triggered on push to `main`. - `rc-release.yml` — triggered on any `vX.Y.Z-rc.N` tag push. - `release.yml` — triggered on any other `v*` tag push, cuts the actual stable release. -All CI runs on a self-hosted runner; nothing runs automatically on plain -commits or PRs beyond the track builds above. See +All of these build inside a pinned Arch Linux container (`ci/Containerfile`, +run via `ci/build.sh`) on a self-hosted runner — not the runner host's +native environment. Arch's repos carry current `gtk4`/`libadwaita`/ +`gtk4-layer-shell` as prebuilt packages, so there's no from-source library +build to go stale. The image is rebuilt (and re-cached by Docker) only when +`ci/Containerfile` changes, so a plain push doesn't refetch or recompile +the toolchain. Nothing runs automatically on plain commits or PRs beyond +the jobs listed above. See [bread-ecosystem's docs/release-channels.md](https://git.breadway.dev/Breadway/bread-ecosystem/src/branch/main/docs/release-channels.md) for the full policy, including how a new product gets wired onto these tracks. diff --git a/ci/Containerfile b/ci/Containerfile new file mode 100644 index 0000000..45b0ba4 --- /dev/null +++ b/ci/Containerfile @@ -0,0 +1,22 @@ +# Pinned CI build environment for breadpad. +# +# Arch instead of Fedora because breadpad targets BOS/Arch only, and Arch's +# repos already carry current libadwaita/gtk4/gtk4-layer-shell as prebuilt +# packages — no from-source libadwaita build needed (that from-source build +# was the repeated CI breakage: rust dep mismatches, libadwaita ABI +# mismatches, and finally a removed meson option). +# +# Base image pinned by digest, package set frozen at build time: this image +# only changes when someone deliberately rebuilds it, not on every push. +FROM archlinux@sha256:fae033b815a16f930325c2697e620362be4d2e5d739a301b10ad1fc9c8643a06 + +RUN pacman -Syu --noconfirm --needed \ + base-devel \ + git \ + pkgconf \ + rust \ + gtk4 \ + libadwaita \ + gtk4-layer-shell \ + graphene \ + && pacman -Scc --noconfirm diff --git a/ci/build.sh b/ci/build.sh new file mode 100755 index 0000000..fbaac36 --- /dev/null +++ b/ci/build.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Builds (or reuses, via docker's own layer cache) the pinned Arch CI image +# from ci/Containerfile, then runs the given cargo command inside it against +# this repo checkout. +# +# Cargo's registry/git caches and CARGO_TARGET_DIR are persisted in named +# docker volumes so they survive across runs even though the repo checkout +# itself (a fresh --depth 1 clone per workflow run) does not. +# +# Usage: ci/build.sh cargo build --release --locked +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +docker build -t breadpad-ci:archlinux -f ci/Containerfile ci + +docker run --rm \ + -v "${ROOT}:/workspace" \ + -v breadpad-cargo-registry:/root/.cargo/registry \ + -v breadpad-cargo-git:/root/.cargo/git \ + -v breadpad-cargo-target:/cargo-target \ + -w /workspace \ + -e CARGO_TARGET_DIR=/cargo-target \ + breadpad-ci:archlinux \ + bash -c ' + set -euo pipefail + "$@" + mkdir -p /workspace/target + cp -a /cargo-target/. /workspace/target/ + ' bash "$@" From fe2724220ad5cd6d58655c25670054f749869b3b Mon Sep 17 00:00:00 2001 From: Breadway Date: Tue, 4 Aug 2026 17:52:45 +0800 Subject: [PATCH 39/53] Fix clippy warnings and a stale test assertion surfaced by check.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check.yml runs clippy -D warnings and cargo test for the first time in this repo's history, surfacing pre-existing lint debt across all four workspace crates plus one stale test assertion (breadpad-shared's Palette default-background test still expected bread-theme's old #1e1e2e; the upstream design system moved to #0c0c0c a while back and nothing caught the drift since tests never ran in CI). All fixes are mechanical: needless returns/closures/borrows, manual Default impls replaceable by #[derive], map_or -> is_some_and, manual range checks -> RangeInclusive::contains, a non-idiomatic sort_by -> sort_by_key, an overly complex inline type given an alias, and moving a function that ended up defined after its own test module. The one exception is NoteType::from_str, which clippy flags for shadowing std::str::FromStr's name — left as `#[allow(...)]` with a comment rather than renamed, since it has 30+ call sites across every crate in the workspace and returns Self directly rather than Result. --- breadman/src/main.rs | 6 +- breadman/src/views/errors.rs | 2 +- breadpad-shared/src/ai.rs | 2 +- breadpad-shared/src/config.rs | 13 +-- breadpad-shared/src/parser.rs | 136 +++++++++++++++---------------- breadpad-shared/src/scheduler.rs | 4 +- breadpad-shared/src/store.rs | 6 +- breadpad-shared/src/theme.rs | 10 ++- breadpad-shared/src/types.rs | 3 + breadpad-shared/tests/config.rs | 12 ++- breadpad-test/src/main.rs | 2 +- breadpad/src/main.rs | 2 +- 12 files changed, 99 insertions(+), 99 deletions(-) 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(), ); From e64a525104050e91e6294063332ba5aaf4de1de4 Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 5 Aug 2026 08:47:40 +0800 Subject: [PATCH 40/53] ci: use bread-ecosystem's shared Arch build image/script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the Containerfile and build.sh added in the previous CI fix up into bread-ecosystem/ci/, so other GTK4 layer-shell products in the ecosystem can share it instead of each carrying their own copy. This repo's ci/build.sh is now a thin wrapper that clones bread-ecosystem at the commit pinned in ci/bread-ecosystem.rev and delegates to its ci/build.sh — pinned by sha rather than tracking main, so an unrelated change over there can't silently break this repo's release builds (the failure mode the bread-theme test fix a moment ago was caused by). Verified locally: clippy, test, and release build all pass through the new delegator path. --- CONTRIBUTING.md | 21 +++++++++++++-------- ci/Containerfile | 22 ---------------------- ci/bread-ecosystem.rev | 1 + ci/build.sh | 36 +++++++++++++----------------------- 4 files changed, 27 insertions(+), 53 deletions(-) delete mode 100644 ci/Containerfile create mode 100644 ci/bread-ecosystem.rev diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93077a9..ac30b30 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,14 +76,19 @@ cargo test --release --workspace - `release.yml` — triggered on any other `v*` tag push, cuts the actual stable release. -All of these build inside a pinned Arch Linux container (`ci/Containerfile`, -run via `ci/build.sh`) on a self-hosted runner — not the runner host's -native environment. Arch's repos carry current `gtk4`/`libadwaita`/ -`gtk4-layer-shell` as prebuilt packages, so there's no from-source library -build to go stale. The image is rebuilt (and re-cached by Docker) only when -`ci/Containerfile` changes, so a plain push doesn't refetch or recompile -the toolchain. Nothing runs automatically on plain commits or PRs beyond -the jobs listed above. See +All of these build inside a pinned Arch Linux container on a self-hosted +runner — not the runner host's native environment. Arch's repos carry +current `gtk4`/`libadwaita`/`gtk4-layer-shell` as prebuilt packages, so +there's no from-source library build to go stale. The Containerfile and +build script are shared across bread-ecosystem GTK4 products, living in +`bread-ecosystem/ci/`; `ci/build.sh` here is a thin wrapper that clones +that repo at the commit pinned in `ci/bread-ecosystem.rev` (not `main` — +an unrelated change there shouldn't silently affect this repo's release +builds) and delegates to it. Bump the pin deliberately when you want the +shared image or build logic updated. A `ci/deps.txt` here (currently +absent — breadpad needs nothing beyond the shared base) would layer on +extra pacman packages if that ever changes. Nothing runs automatically on +plain commits or PRs beyond the jobs listed above. See [bread-ecosystem's docs/release-channels.md](https://git.breadway.dev/Breadway/bread-ecosystem/src/branch/main/docs/release-channels.md) for the full policy, including how a new product gets wired onto these tracks. diff --git a/ci/Containerfile b/ci/Containerfile deleted file mode 100644 index 45b0ba4..0000000 --- a/ci/Containerfile +++ /dev/null @@ -1,22 +0,0 @@ -# Pinned CI build environment for breadpad. -# -# Arch instead of Fedora because breadpad targets BOS/Arch only, and Arch's -# repos already carry current libadwaita/gtk4/gtk4-layer-shell as prebuilt -# packages — no from-source libadwaita build needed (that from-source build -# was the repeated CI breakage: rust dep mismatches, libadwaita ABI -# mismatches, and finally a removed meson option). -# -# Base image pinned by digest, package set frozen at build time: this image -# only changes when someone deliberately rebuilds it, not on every push. -FROM archlinux@sha256:fae033b815a16f930325c2697e620362be4d2e5d739a301b10ad1fc9c8643a06 - -RUN pacman -Syu --noconfirm --needed \ - base-devel \ - git \ - pkgconf \ - rust \ - gtk4 \ - libadwaita \ - gtk4-layer-shell \ - graphene \ - && pacman -Scc --noconfirm diff --git a/ci/bread-ecosystem.rev b/ci/bread-ecosystem.rev new file mode 100644 index 0000000..aea22a3 --- /dev/null +++ b/ci/bread-ecosystem.rev @@ -0,0 +1 @@ +cd5da468b3782fddcb52b4eaff0755ff933524be diff --git a/ci/build.sh b/ci/build.sh index fbaac36..c0ab6f2 100755 --- a/ci/build.sh +++ b/ci/build.sh @@ -1,31 +1,21 @@ #!/usr/bin/env bash -# Builds (or reuses, via docker's own layer cache) the pinned Arch CI image -# from ci/Containerfile, then runs the given cargo command inside it against -# this repo checkout. -# -# Cargo's registry/git caches and CARGO_TARGET_DIR are persisted in named -# docker volumes so they survive across runs even though the repo checkout -# itself (a fresh --depth 1 clone per workflow run) does not. +# Delegates to bread-ecosystem's shared CI build image/script, pinned to +# the commit in ci/bread-ecosystem.rev — not `main`. bread-ecosystem's CI +# files now affect every product's release pipeline, so bumping the pin +# is a deliberate act instead of silent drift (see the bread-theme test +# that broke here for exactly that reason, before it was pinned by rev). # # Usage: ci/build.sh cargo build --release --locked set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$ROOT" +REV="$(cat "${ROOT}/ci/bread-ecosystem.rev")" -docker build -t breadpad-ci:archlinux -f ci/Containerfile ci +CACHE_DIR="/tmp/bread-ecosystem-ci-${REV}" +if [ ! -d "$CACHE_DIR" ]; then + rm -rf /tmp/bread-ecosystem-ci-* + git clone https://git.breadway.dev/Breadway/bread-ecosystem.git "$CACHE_DIR" + git -C "$CACHE_DIR" checkout --quiet "$REV" +fi -docker run --rm \ - -v "${ROOT}:/workspace" \ - -v breadpad-cargo-registry:/root/.cargo/registry \ - -v breadpad-cargo-git:/root/.cargo/git \ - -v breadpad-cargo-target:/cargo-target \ - -w /workspace \ - -e CARGO_TARGET_DIR=/cargo-target \ - breadpad-ci:archlinux \ - bash -c ' - set -euo pipefail - "$@" - mkdir -p /workspace/target - cp -a /cargo-target/. /workspace/target/ - ' bash "$@" +bash "${CACHE_DIR}/ci/build.sh" "$ROOT" "$@" From 520245a6beae1b38f88d69b27242b1e29c4c2376 Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 5 Aug 2026 09:28:52 +0800 Subject: [PATCH 41/53] ci: pass product name explicitly to shared build script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bread-ecosystem's ci/build.sh used to derive the image tag and cache volume name from basename(repo_root) — but every product's CI checks out into a directory literally named "src", so in real CI runs this resolved to "bread-ci:src" for every product, not "bread-ci:breadpad". Harmless while breadpad was the only consumer; would have silently collided (shared image tag, shared cargo-target cache) the moment a second product started using it. Fixed upstream, bumping the pin here to pick it up and passing "breadpad" explicitly. --- ci/bread-ecosystem.rev | 2 +- ci/build.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/bread-ecosystem.rev b/ci/bread-ecosystem.rev index aea22a3..474f1fd 100644 --- a/ci/bread-ecosystem.rev +++ b/ci/bread-ecosystem.rev @@ -1 +1 @@ -cd5da468b3782fddcb52b4eaff0755ff933524be +620c5a1317a6b57276eabca961facdb78bf510db diff --git a/ci/build.sh b/ci/build.sh index c0ab6f2..4056cce 100755 --- a/ci/build.sh +++ b/ci/build.sh @@ -18,4 +18,4 @@ if [ ! -d "$CACHE_DIR" ]; then git -C "$CACHE_DIR" checkout --quiet "$REV" fi -bash "${CACHE_DIR}/ci/build.sh" "$ROOT" "$@" +bash "${CACHE_DIR}/ci/build.sh" breadpad "$ROOT" "$@" From b9c27021689dab60bfcd19c381488dbc7b2191fc Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 15 Aug 2026 21:41:26 +0800 Subject: [PATCH 42/53] 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 From 2f737abbf6e97816143e9434c9a278987100d11f Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 15 Aug 2026 22:03:30 +0800 Subject: [PATCH 43/53] Rename CLAUDE.md to AGENTS.md --- AGENTS.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..dd2f2fd --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,37 @@ +# AGENTS.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. From b3e49dc753674806f15e7d8acf85bce87c098cff Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 15 Aug 2026 22:04:00 +0800 Subject: [PATCH 44/53] Remove CLAUDE.md (renamed to AGENTS.md) --- CLAUDE.md | 37 ------------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 81e4634..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,37 +0,0 @@ -# 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. From 9dcc6cfc9417c5af880deeb6cd98da887689ef82 Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 15 Aug 2026 22:17:21 +0800 Subject: [PATCH 45/53] Wire breadpad into the bread event bus Emit bread.pad.captured after a successful quick-capture save and bread.pad.reminder.due when breadpad fire actually shows a reminder. Fail-silent if breadd is down. No command stubs. App id: pad. bread-utils bread-client at v0.7.1. --- Cargo.lock | 25 +++++++++++++++++++++- EVENTS.md | 49 ++++++++++++++++++++++++++++++++++++++++++++ breadpad/Cargo.toml | 3 +++ breadpad/src/main.rs | 19 +++++++++++++++++ 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 EVENTS.md diff --git a/Cargo.lock b/Cargo.lock index d6b5f6c..9e84e38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -308,10 +308,21 @@ version = "0.3.1" source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?rev=69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d#69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d" dependencies = [ "anyhow", - "bread-utils", + "bread-utils 0.3.1 (git+https://git.breadway.dev/Breadway/bread-ecosystem?rev=69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d)", "tracing", ] +[[package]] +name = "bread-shared" +version = "0.7.0" +source = "git+https://git.breadway.dev/Breadway/bread?tag=v0.7.0#22e34e2cf2202305d7960759dfccb54dc79f948b" +dependencies = [ + "dirs 5.0.1", + "serde", + "serde_json", + "toml 0.8.23", +] + [[package]] name = "bread-theme" version = "0.3.1" @@ -323,6 +334,17 @@ dependencies = [ "serde_json", ] +[[package]] +name = "bread-utils" +version = "0.3.1" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1#db2fa3c4b4c1e6933bc5cf62a236d05972fdc886" +dependencies = [ + "bread-shared", + "dirs 5.0.1", + "serde", + "serde_json", +] + [[package]] name = "bread-utils" version = "0.3.1" @@ -359,6 +381,7 @@ version = "0.3.4" dependencies = [ "anyhow", "bread-screenshots", + "bread-utils 0.3.1 (git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1)", "breadpad-shared", "chrono", "dirs 5.0.1", diff --git a/EVENTS.md b/EVENTS.md new file mode 100644 index 0000000..22233d3 --- /dev/null +++ b/EVENTS.md @@ -0,0 +1,49 @@ +# breadpad — bread event integration + +breadpad is a standalone capture popup: it works exactly the same with or +without `breadd` running. When breadd *is* present, the `breadpad` binary +publishes events into the shared bread automation fabric after actions that +already happened. See the parent `bread` repo's `Documentation.md` — +specifically its "Namespaces" and "Integrating a bread\* app" sections — for +the general convention this follows. + +App id: **`pad`**. Transport: `bread-utils`'s `bread_client` module +(feature `bread-client`) — the capture popup links it directly. breadpad is +short-lived (one popup, or one `fire ` invocation from the existing +systemd user timer), so each `emit` is its own short-lived connection. +There is no long-running breadpad daemon and therefore no command +subscription. + +`breadman` (the viewer) does not emit or subscribe. Notes created or edited +there are not quick-capture, and it is not on the reminder-fire path. + +## Events published (`bread.pad.*`) + +| Event | Data | When | +|-------|------|------| +| `bread.pad.captured` | `{ "id": "" }` | The capture popup saved a note successfully (`Store::save_note` returned `Ok`). Not emitted when the field is empty, the window is dismissed, classification-only preview happens, or the write fails. | +| `bread.pad.reminder.due` | `{ "id": "" }` | `breadpad fire ` decided the reminder is due (`Scheduler::fire` returned true) and is about to show the reminder window. This is the existing in-process systemd-timer hook (`breadpad-reminder-.timer` → `breadpad fire `), not a new daemon. Not emitted when the note is missing, the fire is outside the missed-grace window, or the reminder window is opened as a `--screenshot` sample. | + +Note bodies are never included in the payload — only the local note id. +Notes stay in `~/.local/share/breadpad/notes.jsonl`; the event bus is for +*notifications about* capture and due reminders, not a channel for note +content. + +## Commands honored (`bread.command.pad.*`) + +None. breadpad has no persistent process that could subscribe, and the +actions a command verb would map to already exist as local CLI / keybind +paths (`breadpad` for capture, `breadpad fire ` for the reminder +window, `breadman` for viewing and editing). Stubbing `capture` / `snooze` +/ `done` on the bus without a subscriber (or inventing a daemon just to +hold one) would be a no-op dressed up as an API. If breadpad later grows a +long-running piece that can honor a verb for real, add the verb then. + +## Fail-safe behavior + +- If breadd isn't installed or isn't running, `emit` is a silent no-op + (`BreadClient::emit` never blocks or errors the caller). Capture, save, + systemd timers, and the reminder window are entirely unaffected. +- There is no command subscription to reconnect. Restarting breadd does + not require restarting breadpad; the next real capture or fire will emit + again if breadd is reachable at that moment. diff --git a/breadpad/Cargo.toml b/breadpad/Cargo.toml index d618e7a..f57dff1 100644 --- a/breadpad/Cargo.toml +++ b/breadpad/Cargo.toml @@ -14,6 +14,9 @@ breadpad-shared = { path = "../breadpad-shared" } # Capture primitives for `--screenshot` mode — see src/screenshot.rs. # 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" } +# Bread event bus: emit after a real capture/reminder fire. Fail-silent if +# breadd is down. See EVENTS.md. +bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1", features = ["bread-client"] } anyhow.workspace = true ort.workspace = true tracing.workspace = true diff --git a/breadpad/src/main.rs b/breadpad/src/main.rs index b284cb8..d1b057b 100644 --- a/breadpad/src/main.rs +++ b/breadpad/src/main.rs @@ -8,6 +8,7 @@ use breadpad_shared::{ store::Store, types::{Note, NoteType}, }; +use bread_utils::bread_client::BreadClient; use gtk4::{glib, prelude::*}; use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell}; use std::cell::RefCell; @@ -16,6 +17,21 @@ use std::sync::{Arc, Once}; mod screenshot; +/// Sibling-app id in `bread_shared::apps::KNOWN_APPS`. Events are `bread.pad.*`. +const APP_ID: &str = "pad"; + +/// `bread.pad.captured` after a successful quick-capture save. Fire-and-forget: +/// `BreadClient::emit` is a silent no-op when breadd is down or missing. +fn emit_captured(id: &str) { + BreadClient::connect(APP_ID).emit("bread.pad.captured", serde_json::json!({ "id": id })); +} + +/// `bread.pad.reminder.due` when `breadpad fire ` actually shows a reminder +/// (the existing systemd-timer hook — not a new daemon). Same fail-silent emit. +fn emit_reminder_due(id: &str) { + BreadClient::connect(APP_ID).emit("bread.pad.reminder.due", serde_json::json!({ "id": id })); +} + static ORT_INIT: Once = Once::new(); fn init_ort_once(cfg: &Config) { @@ -331,6 +347,8 @@ fn cmd_fire(id: &str, cfg: &Config) -> Result<()> { return Ok(()); } + emit_reminder_due(¬e.id); + // Schedule next recurrence before showing UI if note.rrule.is_some() { if let Some(next) = Scheduler::next_recurrence(¬e, &cfg.reminders.default_morning) { @@ -911,6 +929,7 @@ fn save_note_classified( tracing::error!("failed to save note: {}", e); return; } + emit_captured(¬e.id); if note.time.is_some() { if let Err(e) = Scheduler::schedule(¬e) { tracing::warn!("failed to schedule reminder: {}", e); From 17e9381fc6a5458584f8a01e8d5f0044ceee743c Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 15 Aug 2026 22:53:46 +0800 Subject: [PATCH 46/53] Pin bread-ecosystem crates to v0.7.2 Repoint bread-theme, bread-utils, and bread-screenshots at tag v0.7.2 on git.breadway.dev. breadman enables the adw feature (implies gtk). Drop the 69ce2d67 rev pin now that screenshots is on a tagged release. --- Cargo.lock | 27 +++++++++------------------ breadman/Cargo.toml | 7 +++---- breadpad-shared/Cargo.toml | 2 +- breadpad/Cargo.toml | 5 ++--- 4 files changed, 15 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9e84e38..7ed7d20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -304,11 +304,11 @@ dependencies = [ [[package]] name = "bread-screenshots" -version = "0.3.1" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?rev=69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d#69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d" +version = "0.7.2" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73" dependencies = [ "anyhow", - "bread-utils 0.3.1 (git+https://git.breadway.dev/Breadway/bread-ecosystem?rev=69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d)", + "bread-utils", "tracing", ] @@ -325,19 +325,20 @@ dependencies = [ [[package]] name = "bread-theme" -version = "0.3.1" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1#db2fa3c4b4c1e6933bc5cf62a236d05972fdc886" +version = "0.7.2" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73" 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?tag=v0.7.1#db2fa3c4b4c1e6933bc5cf62a236d05972fdc886" +version = "0.7.2" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73" dependencies = [ "bread-shared", "dirs 5.0.1", @@ -345,16 +346,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "bread-utils" -version = "0.3.1" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?rev=69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d#69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d" -dependencies = [ - "dirs 5.0.1", - "serde", - "serde_json", -] - [[package]] name = "breadman" version = "0.3.4" @@ -381,7 +372,7 @@ version = "0.3.4" dependencies = [ "anyhow", "bread-screenshots", - "bread-utils 0.3.1 (git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1)", + "bread-utils", "breadpad-shared", "chrono", "dirs 5.0.1", diff --git a/breadman/Cargo.toml b/breadman/Cargo.toml index 4f36d71..553efa2 100644 --- a/breadman/Cargo.toml +++ b/breadman/Cargo.toml @@ -12,10 +12,9 @@ path = "src/main.rs" [dependencies] breadpad-shared = { path = "../breadpad-shared" } # Capture primitives for `--screenshot` mode — see src/screenshot.rs. -# 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"] } +bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } +# `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.2", features = ["adw"] } libadwaita = { version = "0.9", features = ["v1_7"] } anyhow.workspace = true tracing.workspace = true diff --git a/breadpad-shared/Cargo.toml b/breadpad-shared/Cargo.toml index 9ef4f76..6e4edf9 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", tag = "v0.7.1", features = ["gtk"] } +bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["gtk"] } anyhow.workspace = true tracing.workspace = true serde.workspace = true diff --git a/breadpad/Cargo.toml b/breadpad/Cargo.toml index f57dff1..1465ac4 100644 --- a/breadpad/Cargo.toml +++ b/breadpad/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. -# 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" } +bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } # Bread event bus: emit after a real capture/reminder fire. Fail-silent if # breadd is down. See EVENTS.md. -bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1", features = ["bread-client"] } +bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["bread-client"] } anyhow.workspace = true ort.workspace = true tracing.workspace = true From 2f57f432a0e0458c3c5271673155836f077c7a1c Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 15 Aug 2026 23:05:47 +0800 Subject: [PATCH 47/53] Bump version to v0.5.1 --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ed7d20..aad8b09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -348,7 +348,7 @@ dependencies = [ [[package]] name = "breadman" -version = "0.3.4" +version = "0.5.1" dependencies = [ "anyhow", "bread-screenshots", @@ -368,7 +368,7 @@ dependencies = [ [[package]] name = "breadpad" -version = "0.3.4" +version = "0.5.1" dependencies = [ "anyhow", "bread-screenshots", @@ -389,7 +389,7 @@ dependencies = [ [[package]] name = "breadpad-shared" -version = "0.3.4" +version = "0.5.1" dependencies = [ "anyhow", "bread-theme", @@ -415,7 +415,7 @@ dependencies = [ [[package]] name = "breadpad-test" -version = "0.3.4" +version = "0.5.1" dependencies = [ "anyhow", "breadpad-shared", diff --git a/Cargo.toml b/Cargo.toml index 4092bed..39b7707 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.3.4" +version = "0.5.1" edition = "2021" license = "MIT" authors = ["Breadway"] From 54eba3d73a2760a6278c141b06bd4753983e940c Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 15 Aug 2026 23:11:28 +0800 Subject: [PATCH 48/53] Honor bread.command.pad.capture via breadpad listen --- EVENTS.md | 53 +++++++++++++++++++-------- breadpad/src/listen.rs | 83 ++++++++++++++++++++++++++++++++++++++++++ breadpad/src/main.rs | 7 ++++ 3 files changed, 127 insertions(+), 16 deletions(-) create mode 100644 breadpad/src/listen.rs diff --git a/EVENTS.md b/EVENTS.md index 22233d3..428599c 100644 --- a/EVENTS.md +++ b/EVENTS.md @@ -8,11 +8,11 @@ specifically its "Namespaces" and "Integrating a bread\* app" sections — for the general convention this follows. App id: **`pad`**. Transport: `bread-utils`'s `bread_client` module -(feature `bread-client`) — the capture popup links it directly. breadpad is -short-lived (one popup, or one `fire ` invocation from the existing -systemd user timer), so each `emit` is its own short-lived connection. -There is no long-running breadpad daemon and therefore no command -subscription. +(feature `bread-client`) — the capture popup links it directly. One-shot +popup / `fire ` invocations each `emit` on their own short-lived +connection. Command verbs are only received while `breadpad listen` is +running — that process holds the `bread.command.pad.**` subscription +open. `breadman` (the viewer) does not emit or subscribe. Notes created or edited there are not quick-capture, and it is not on the reminder-fire path. @@ -23,6 +23,8 @@ there are not quick-capture, and it is not on the reminder-fire path. |-------|------|------| | `bread.pad.captured` | `{ "id": "" }` | The capture popup saved a note successfully (`Store::save_note` returned `Ok`). Not emitted when the field is empty, the window is dismissed, classification-only preview happens, or the write fails. | | `bread.pad.reminder.due` | `{ "id": "" }` | `breadpad fire ` decided the reminder is due (`Scheduler::fire` returned true) and is about to show the reminder window. This is the existing in-process systemd-timer hook (`breadpad-reminder-.timer` → `breadpad fire `), not a new daemon. Not emitted when the note is missing, the fire is outside the missed-grace window, or the reminder window is opened as a `--screenshot` sample. | +| `bread.pad.capture.done` | `{}` | `bread.command.pad.capture` was received and `breadpad` was spawned. This is the command confirmation, not proof the popup mapped — the spawned process is the same no-args invocation as the capture keybind. | +| `bread.pad.capture.failed` | `{ "error": "" }` | `bread.command.pad.capture` was received but this binary could not be started. | Note bodies are never included in the payload — only the local note id. Notes stay in `~/.local/share/breadpad/notes.jsonl`; the event bus is for @@ -31,19 +33,38 @@ content. ## Commands honored (`bread.command.pad.*`) -None. breadpad has no persistent process that could subscribe, and the -actions a command verb would map to already exist as local CLI / keybind -paths (`breadpad` for capture, `breadpad fire ` for the reminder -window, `breadman` for viewing and editing). Stubbing `capture` / `snooze` -/ `done` on the bus without a subscriber (or inventing a daemon just to -hold one) would be a no-op dressed up as an API. If breadpad later grows a -long-running piece that can honor a verb for real, add the verb then. +These are only received while `breadpad listen` is running. Publishing a +command with no subscriber is a silent no-op — that is the documented +bread convention, not a breadpad bug. + +| Verb | Data | Effect | +|------|------|--------| +| `capture` | none | Same as running `breadpad` with no args: open the capture popup. Emits `bread.pad.capture.done` / `.failed`. | + +```lua +bread.spawn(function() + bread.emit("bread.command.pad.capture") + bread.wait("bread.pad.capture.done", { timeout = 5000 }) +end) +``` + +### Not implemented: extra verbs + +There is no `snooze` / `done` / `fire` command verb. Reminder fire +already exists as `breadpad fire ` (systemd user timer), and +viewing/editing lives in `breadman`. If/when a bus verb maps to real +extra behavior, add it then — do not stub one as a no-op ahead of it. ## Fail-safe behavior - If breadd isn't installed or isn't running, `emit` is a silent no-op - (`BreadClient::emit` never blocks or errors the caller). Capture, save, + (`BreadClient::emit` never blocks or errors the caller) and the + command subscription simply never receives anything. Capture, save, systemd timers, and the reminder window are entirely unaffected. -- There is no command subscription to reconnect. Restarting breadd does - not require restarting breadpad; the next real capture or fire will emit - again if breadd is reachable at that moment. +- If breadd restarts, the command subscription reconnects automatically + (`BreadClient::subscribe`'s background thread has its own backoff + loop); no restart of `breadpad listen` is needed. +- If `breadpad listen` is not running, commands are a graceful no-op at + the bus (no subscriber). One-shot capture / `fire` still emit + `bread.pad.captured` / `bread.pad.reminder.due` on their own + short-lived connection. diff --git a/breadpad/src/listen.rs b/breadpad/src/listen.rs new file mode 100644 index 0000000..e8649a5 --- /dev/null +++ b/breadpad/src/listen.rs @@ -0,0 +1,83 @@ +//! Long-running command subscription for `bread.command.pad.*`. +//! +//! `breadpad` is still a one-shot capture popup by default. `breadpad listen` +//! is the optional persistent process that can honor bus commands. See +//! `EVENTS.md`. + +use anyhow::Result; +use bread_utils::bread_client::{BreadClient, BreadEvent}; + +/// Sibling-app id in `bread_shared::apps::KNOWN_APPS`. +const APP_ID: &str = "pad"; + +/// Subscribe to `bread.command.pad.**` and block until the process is killed. +/// +/// breadd being absent is not an error: [`BreadClient::subscribe`] reconnects +/// with backoff, and `on_event` simply isn't called until the daemon is up. +pub fn run() -> Result<()> { + let client = BreadClient::connect(APP_ID); + if client.health().is_none() { + tracing::warn!("breadd unreachable; command subscription will connect when it comes back"); + } + + let _commands = client.subscribe("bread.command.pad.**", |event| { + handle_command(&event); + }); + + tracing::info!("listening for bread.command.pad.**"); + loop { + std::thread::park(); + } +} + +/// Reacts to `bread.command.pad.*` verbs. Only `capture` is honored today — +/// other verbs are ignored, not stubbed as no-ops that pretend to succeed. +fn handle_command(event: &BreadEvent) { + let Some(verb) = command_verb(&event.event) else { + return; + }; + match verb { + "capture" => handle_capture(), + other => { + tracing::debug!("ignoring unrecognized command verb '{other}'"); + } + } +} + +fn handle_capture() { + // Same as running `breadpad` with no args: open the capture popup. + let result = spawn_self(); + let client = BreadClient::connect(APP_ID); + match result { + Ok(_) => client.emit("bread.pad.capture.done", serde_json::json!({})), + Err(e) => { + tracing::warn!("bread.command.pad.capture failed: {e}"); + client.emit( + "bread.pad.capture.failed", + serde_json::json!({ "error": e.to_string() }), + ); + } + } +} + +fn spawn_self() -> std::io::Result { + let exe = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("breadpad")); + std::process::Command::new(exe).spawn() +} + +fn command_verb(event_name: &str) -> Option<&str> { + event_name.strip_prefix("bread.command.pad.") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn command_verb_strips_pad_prefix() { + assert_eq!(command_verb("bread.command.pad.capture"), Some("capture")); + assert_eq!(command_verb("bread.command.pad.snooze"), Some("snooze")); + assert_eq!(command_verb("bread.command.box.open"), None); + assert_eq!(command_verb("bread.pad.captured"), None); + } +} diff --git a/breadpad/src/main.rs b/breadpad/src/main.rs index d1b057b..def0c86 100644 --- a/breadpad/src/main.rs +++ b/breadpad/src/main.rs @@ -15,6 +15,7 @@ use std::cell::RefCell; use std::rc::Rc; use std::sync::{Arc, Once}; +mod listen; mod screenshot; /// Sibling-app id in `bread_shared::apps::KNOWN_APPS`. Events are `bread.pad.*`. @@ -64,6 +65,7 @@ mod args { pub output: Option, pub width: u32, pub height: u32, + pub listen: bool, } impl Args { @@ -99,6 +101,7 @@ mod args { output: None, width: 1920, height: 1080, + listen: false, }; let raw: Vec = std::env::args().skip(1).collect(); let mut i = 0; @@ -112,6 +115,7 @@ mod args { "--status" => args.status = true, "download-model" => args.download_model = true, "model-info" => args.model_info = true, + "listen" => args.listen = true, "fire" => { i += 1; args.fire_id = raw.get(i).cloned(); @@ -165,6 +169,9 @@ fn main() -> Result<()> { .init(); let args = args::parse(); + if args.listen { + return listen::run(); + } let cfg = Config::load()?; if args.status { From c929517b4918d9cf62a6b17f0f35a536c1f06d45 Mon Sep 17 00:00:00 2001 From: Breadway Date: Sun, 16 Aug 2026 00:26:07 +0800 Subject: [PATCH 49/53] Adopt bread_utils::screenshot_cli for --screenshot flags Use the crate for pair validation, settle delay, and canvas defaults in breadpad and breadman. breadman now depends on bread-utils v0.7.2 for screenshot_cli; existing hand-rolled argv parsing stays. --- Cargo.lock | 1 + breadman/Cargo.toml | 2 ++ breadman/src/main.rs | 25 +++++++++++++++---------- breadman/src/screenshot.rs | 8 ++------ breadpad/src/main.rs | 25 +++++++++++++++---------- breadpad/src/screenshot.rs | 8 ++------ 6 files changed, 37 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aad8b09..a56d75d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -353,6 +353,7 @@ dependencies = [ "anyhow", "bread-screenshots", "bread-theme", + "bread-utils", "breadpad-shared", "chrono", "dirs 5.0.1", diff --git a/breadman/Cargo.toml b/breadman/Cargo.toml index 553efa2..e1ae340 100644 --- a/breadman/Cargo.toml +++ b/breadman/Cargo.toml @@ -13,6 +13,8 @@ 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", tag = "v0.7.2" } +# Shared `--screenshot` pair validation + settle delay (`screenshot_cli`). +bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } # `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.2", features = ["adw"] } libadwaita = { version = "0.9", features = ["v1_7"] } diff --git a/breadman/src/main.rs b/breadman/src/main.rs index 0a784f1..9ef3d86 100644 --- a/breadman/src/main.rs +++ b/breadman/src/main.rs @@ -20,6 +20,9 @@ mod views; // ── Args ───────────────────────────────────────────────────────────────────── mod args { + use bread_utils::screenshot_cli::{validate_pair, DEFAULT_HEIGHT, DEFAULT_WIDTH}; + use std::path::Path; + #[derive(Debug)] pub struct Args { pub view: Option, @@ -32,18 +35,20 @@ mod args { } impl Args { - /// `None` for a normal run. Exits the process with an error if - /// `--screenshot` was given without `--output`, before any GTK + /// `None` for a normal run. Exits the process with an error if the + /// `--screenshot` / `--output` pair is incomplete, before any GTK /// setup happens. pub fn screenshot_request(&self) -> Option { - let view = self.screenshot.clone()?; - let Some(output) = self.output.clone() else { - eprintln!("breadman: --screenshot requires --output"); + if let Err(e) = validate_pair( + self.screenshot.as_deref(), + self.output.as_deref().map(Path::new), + ) { + eprintln!("breadman: {e}"); std::process::exit(1); - }; + } Some(crate::screenshot::ScreenshotRequest { - view, - output: output.into(), + view: self.screenshot.clone()?, + output: self.output.clone()?.into(), width: self.width, height: self.height, }) @@ -57,8 +62,8 @@ mod args { upcoming_plain: false, screenshot: None, output: None, - width: 1920, - height: 1080, + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, }; let raw: Vec = std::env::args().skip(1).collect(); let mut i = 0; diff --git a/breadman/src/screenshot.rs b/breadman/src/screenshot.rs index 5b36c9d..a335369 100644 --- a/breadman/src/screenshot.rs +++ b/breadman/src/screenshot.rs @@ -19,6 +19,7 @@ //! the button/click-handler entirely), with no-op save/delete/error //! callbacks since nothing here should actually persist a change. +use bread_utils::screenshot_cli::SETTLE_DELAY; use gtk4::prelude::*; use libadwaita::prelude::*; use std::path::PathBuf; @@ -26,15 +27,10 @@ use std::rc::Rc; use std::sync::Arc; use std::time::Duration; -/// Extra settle time after `map` for the first frame to actually paint -/// before grim runs — `map` fires once the surface exists, not once -/// anything has been drawn into it. -const SETTLE_DELAY: Duration = Duration::from_millis(300); - /// Delay before popping the editor popover open — same reasoning as every /// other app's PRE_POPUP_DELAY: the parent window's own layout needs a beat /// to settle first. -const PRE_POPUP_DELAY: Duration = Duration::from_millis(300); +const PRE_POPUP_DELAY: Duration = SETTLE_DELAY; #[derive(Clone)] pub struct ScreenshotRequest { diff --git a/breadpad/src/main.rs b/breadpad/src/main.rs index def0c86..d61330b 100644 --- a/breadpad/src/main.rs +++ b/breadpad/src/main.rs @@ -51,6 +51,9 @@ fn init_ort_once(cfg: &Config) { } mod args { + use bread_utils::screenshot_cli::{validate_pair, DEFAULT_HEIGHT, DEFAULT_WIDTH}; + use std::path::Path; + #[derive(Debug)] pub struct Args { pub note_type: Option, @@ -69,18 +72,20 @@ mod args { } impl Args { - /// `None` for a normal run. Exits the process with an error if - /// `--screenshot` was given without `--output`, before any GTK + /// `None` for a normal run. Exits the process with an error if the + /// `--screenshot` / `--output` pair is incomplete, before any GTK /// setup happens. pub fn screenshot_request(&self) -> Option { - let view = self.screenshot.clone()?; - let Some(output) = self.output.clone() else { - eprintln!("breadpad: --screenshot requires --output"); + if let Err(e) = validate_pair( + self.screenshot.as_deref(), + self.output.as_deref().map(Path::new), + ) { + eprintln!("breadpad: {e}"); std::process::exit(1); - }; + } Some(crate::screenshot::ScreenshotRequest { - view, - output: output.into(), + view: self.screenshot.clone()?, + output: self.output.clone()?.into(), width: self.width, height: self.height, }) @@ -99,8 +104,8 @@ mod args { calendar_list_uid: None, screenshot: None, output: None, - width: 1920, - height: 1080, + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, listen: false, }; let raw: Vec = std::env::args().skip(1).collect(); diff --git a/breadpad/src/screenshot.rs b/breadpad/src/screenshot.rs index c90873e..98f5ab0 100644 --- a/breadpad/src/screenshot.rs +++ b/breadpad/src/screenshot.rs @@ -18,19 +18,15 @@ //! entirely), and "reminder-snooze" (the same window with its snooze //! popover open). +use bread_utils::screenshot_cli::SETTLE_DELAY; use gtk4::prelude::*; use std::path::PathBuf; use std::time::Duration; -/// Extra settle time after `map` for the first frame to actually paint -/// before grim runs — `map` fires once the surface exists, not once -/// anything has been drawn into it. -const SETTLE_DELAY: Duration = Duration::from_millis(300); - /// Delay before popping the snooze popover open — same reasoning as every /// other app's PRE_POPUP_DELAY: the parent window's own layout needs a beat /// to settle first. -const PRE_POPUP_DELAY: Duration = Duration::from_millis(300); +const PRE_POPUP_DELAY: Duration = SETTLE_DELAY; #[derive(Clone)] pub struct ScreenshotRequest { From 852a771651fffc34ca628f38500a22cfa6a49f4b Mon Sep 17 00:00:00 2001 From: Breadway Date: Sun, 16 Aug 2026 00:50:22 +0800 Subject: [PATCH 50/53] CI: fix v* tag trigger and refuse unsigned bakery index --- .forgejo/workflows/release.yml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index d21777c..bd7569b 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -2,11 +2,11 @@ name: release on: push: - tags: ['v*'] - tags-ignore: ['v*-rc.*'] + tags: ["v*"] jobs: build: + if: ${{ !contains(github.ref_name, '-rc.') }} runs-on: [self-hosted, hestia] steps: - name: checkout @@ -17,7 +17,16 @@ jobs: "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - name: build - run: cd src && bash ci/build.sh cargo build --release --locked + run: | + set -euo pipefail + if [ ! -f src/ci/build.sh ]; then + echo "::error::ci/build.sh is missing — bakery release builds must go through the shared CI wrapper" + exit 1 + fi + cd src && bash ci/build.sh cargo build --release --locked || { + echo "::error::cargo build --release --locked failed. If Cargo.lock drifted, update and commit it; do not drop --locked." + exit 1 + } - name: prepare artifacts run: | @@ -37,8 +46,14 @@ jobs: ln -sfn "${VERSION}" "/srv/breadway-dl/breadpad/latest" - name: regenerate index.json + env: + MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} run: | set -euo pipefail + if [ -z "${MINISIGN_SEC_KEY:-}" ]; then + echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone)" + exit 1 + fi rm -rf /tmp/bread-ecosystem-ci git clone https://git.breadway.dev/Breadway/bread-ecosystem.git /tmp/bread-ecosystem-ci bash /tmp/bread-ecosystem-ci/scripts/gen-index.sh From 37713eee9e79f88df5f6c277ca36205973568f8d Mon Sep 17 00:00:00 2001 From: Breadway Date: Sun, 16 Aug 2026 13:23:18 +0800 Subject: [PATCH 51/53] Bind breadpad/breadman windows to the current monitor palette Pin bread-theme to v0.7.4. --- Cargo.lock | 941 ++++++++++++++++------------------- breadman/Cargo.toml | 2 +- breadman/src/main.rs | 2 + breadpad-shared/Cargo.toml | 3 +- breadpad-shared/src/theme.rs | 5 + breadpad/src/main.rs | 2 + 6 files changed, 434 insertions(+), 521 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a56d75d..c60d1ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,18 +24,18 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] @@ -76,7 +76,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -87,14 +87,14 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "async-broadcast" @@ -175,7 +175,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -215,7 +215,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -226,13 +226,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -276,9 +276,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "block-buffer" @@ -325,8 +325,8 @@ dependencies = [ [[package]] name = "bread-theme" -version = "0.7.2" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73" +version = "0.7.4" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.4#fcba3760387e2523edb71350f8efea3bc851b21e" dependencies = [ "dirs 5.0.1", "gtk4", @@ -396,6 +396,7 @@ dependencies = [ "bread-theme", "chrono", "dirs 5.0.1", + "gtk4", "ical", "ndarray 0.16.1", "ort", @@ -444,9 +445,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cairo-rs" @@ -468,7 +469,7 @@ checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54" dependencies = [ "glib-sys", "libc", - "system-deps", + "system-deps 7.0.8", ] [[package]] @@ -482,9 +483,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.63" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" dependencies = [ "find-msvc-tools", "shlex", @@ -508,9 +509,20 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] [[package]] name = "chrono" @@ -550,9 +562,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.1" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", "clap_derive", @@ -560,9 +572,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstream", "anstyle", @@ -572,14 +584,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -601,7 +613,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -676,6 +688,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -687,9 +708,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -697,18 +718,18 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crossterm" @@ -764,7 +785,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.119", ] [[package]] @@ -775,7 +796,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -805,7 +826,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -815,7 +836,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn", + "syn 2.0.119", ] [[package]] @@ -837,7 +858,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 2.0.119", "unicode-xid", ] @@ -890,18 +911,18 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -915,9 +936,9 @@ dependencies = [ [[package]] name = "either" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "encode_unicode" @@ -949,7 +970,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -965,7 +986,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -976,11 +997,10 @@ checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] @@ -1008,9 +1028,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "field-offset" @@ -1024,9 +1044,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "flate2" @@ -1044,12 +1064,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1061,24 +1075,24 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -1087,9 +1101,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-lite" @@ -1106,32 +1120,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", "futures-macro", @@ -1163,14 +1177,14 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps", + "system-deps 7.0.8", ] [[package]] name = "gdk4" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd42fdbbf48612c6e8f47c65fb92d2e8f39c25aecd6af047e83897c1a22d2a4e" +checksum = "d81e2a6c6ecba2aab60633a98df1868b03fa0bfdce8105edc27c1bccf71f0e39" dependencies = [ "cairo-rs", "gdk-pixbuf", @@ -1184,9 +1198,9 @@ dependencies = [ [[package]] name = "gdk4-sys" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d974ac4f15e67472c3a9728daf612590b4a5762a4b33f0edd298df0b80d043c" +checksum = "3d8f608d8d7d229975c4d0d026f5d3071598c4ddab3c5262b0a31840fec78d13" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -1196,7 +1210,7 @@ dependencies = [ "libc", "pango-sys", "pkg-config", - "system-deps", + "system-deps 7.0.8", ] [[package]] @@ -1229,31 +1243,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", - "wasip2", - "wasip3", + "rand_core 0.10.1", + "wasm-bindgen", ] [[package]] name = "gio" -version = "0.22.6" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3848bcba3a35cc0a71df8ba8ecfd799d6bfb862342a53a4a915fb62213aa4e6" +checksum = "8b3e1f669909c326b9413bde5a742097b8c90a7d78f45326db13668984769ded" dependencies = [ "futures-channel", "futures-core", @@ -1268,15 +1281,15 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.22.0" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64729ba2772c080448f9f966dba8f4456beeb100d8c28a865ef8a0f2ef4987e1" +checksum = "353fdc7da7cd16da916104b1e0e4e7de380ec9c8aaa20d4d742d66310ab4b0d5" dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps", - "windows-sys 0.52.0", + "system-deps 7.0.8", + "windows-sys 0.61.2", ] [[package]] @@ -1301,9 +1314,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.22.7" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c207e04e51605dcf7b2924c41591b3a10e1438eaac5bcf448fb91f325381104a" +checksum = "ddbcf514bd1881fc1b960e4e52b4e82873f4da3bceddbd58d42827b508888100" dependencies = [ "bitflags", "futures-channel", @@ -1329,17 +1342,17 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "glib-sys" -version = "0.22.6" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7fbac234ed5bc2a28359b7bde8e1b9cdf1441cc2d7f068e4824672d7db9445" +checksum = "030967459f9f676851872c6304adea7825c6d462ec9b72554c733cf0c5952233" dependencies = [ "libc", - "system-deps", + "system-deps 7.0.8", ] [[package]] @@ -1350,37 +1363,35 @@ checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c" dependencies = [ "glib-sys", "libc", - "system-deps", + "system-deps 7.0.8", ] [[package]] name = "graphene-rs" -version = "0.22.0" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d1b7881f96869f49808b6adfe906a93a57a34204952253444d68c3208d71f1" +checksum = "eb856b9c558971c3f13ab692358926da710b046932a4e087aedcc35b040d7dff" dependencies = [ "glib", "graphene-sys", - "libc", ] [[package]] name = "graphene-sys" -version = "0.22.0" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "517f062f3fd6b7fd3e57a3f038a74b3c23ca32f51199ff028aa704609943f79c" +checksum = "5c7ffdfde88f3570d3705e0d8a2433e036d387a1f2930bbf47eafcb5f569fd04" dependencies = [ "glib-sys", "libc", - "pkg-config", - "system-deps", + "system-deps 7.0.8", ] [[package]] name = "gsk4" -version = "0.11.1" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c912dfcbd28acace5fc99c40bb9f25e1dcb73efb1f2608327f66a99acdcb62" +checksum = "b867be1c5f14dcb8f552c0eff6e9a9b1da5f8b43943e8efc3a63c889d84952ff" dependencies = [ "cairo-rs", "gdk4", @@ -1393,9 +1404,9 @@ dependencies = [ [[package]] name = "gsk4-sys" -version = "0.11.1" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d54bbc7a9d8b6ffe4f0c95eede15ccfb365c8bf521275abe6bcfb57b18fb8a" +checksum = "5b7c7eb2e681ee896646cfb8872b431f24d09f53ba9283289d9b10caa6707088" dependencies = [ "cairo-sys-rs", "gdk4-sys", @@ -1404,14 +1415,14 @@ dependencies = [ "graphene-sys", "libc", "pango-sys", - "system-deps", + "system-deps 7.0.8", ] [[package]] name = "gtk4" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7181b837f04cbe93f79441475f7a00560a92cba7a72e38cc1a68b6f8b78eaae2" +checksum = "98a0a0466484f64b07b5b8184d43fa46be78eb0b8e04ae4e179af31d770b76d9" dependencies = [ "cairo-rs", "field-offset", @@ -1430,9 +1441,9 @@ dependencies = [ [[package]] name = "gtk4-layer-shell" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4069987ff4793699511a251028cc336b438e46565b463f111250148d574752a" +checksum = "17c28ea0f4676fdaaae7ff2413a24d0d35c8657424f84856c1103c73454c9da4" dependencies = [ "bitflags", "gdk4", @@ -1445,34 +1456,34 @@ dependencies = [ [[package]] name = "gtk4-layer-shell-sys" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f566a5ec5bcc454e7fcf2ab76930887ced5365afce12c1e5201bb296b95f1b9" +checksum = "bcf19bb884ef0ef55b9e6b2b369c39b4fcc0c41e3a0c1cbc8c267720338b690b" dependencies = [ "gdk4-sys", "glib-sys", "gtk4-sys", "libc", - "system-deps", + "system-deps 8.0.0", ] [[package]] name = "gtk4-macros" -version = "0.11.0" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3581b242ba62fdff122ebb626ea641582ec326031622bd19d60f85029c804a87" +checksum = "5ac7179400a36a04de039c24206bb841c5596992b907b43b23ee8d5bdc40d00e" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "gtk4-sys" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20ba8e695e2640455561274e65e45f0a151619e450746007667f4b23ceae4e1b" +checksum = "82b8f954786af0b1984425c4446b77f5ff6594346181316be3f850caab1c6f01" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -1484,16 +1495,7 @@ dependencies = [ "gsk4-sys", "libc", "pango-sys", - "system-deps", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", + "system-deps 7.0.8", ] [[package]] @@ -1531,19 +1533,19 @@ dependencies = [ "indicatif", "libc", "log", - "rand 0.9.4", + "rand 0.9.5", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.20", "ureq", "windows-sys 0.60.2", ] [[package]] name = "http" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -1551,9 +1553,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http", @@ -1561,9 +1563,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -1580,9 +1582,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" dependencies = [ "atomic-waker", "bytes", @@ -1611,7 +1613,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.7", + "webpki-roots 1.0.9", ] [[package]] @@ -1648,7 +1650,7 @@ dependencies = [ "either", "futures-lite", "hyprland-macros", - "pastey", + "pastey 0.1.1", "serde", "serde_json", "serde_repr", @@ -1663,7 +1665,7 @@ checksum = "31157e6ccefbad4b0cd7e549db6696691a70c11b108f26bf6bf76eef26af8c10" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1701,9 +1703,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -1715,9 +1717,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -1728,9 +1730,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1742,16 +1744,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -1762,15 +1765,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" dependencies = [ "displaydoc", "icu_locale_core", @@ -1781,12 +1784,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -1821,9 +1818,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", + "hashbrown", ] [[package]] @@ -1841,9 +1836,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "is_terminal_polyfill" @@ -1868,13 +1863,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.99" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -1890,12 +1884,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libadwaita" version = "0.9.2" @@ -1923,14 +1911,14 @@ dependencies = [ "gtk4-sys", "libc", "pango-sys", - "system-deps", + "system-deps 7.0.8", ] [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libloading" @@ -1944,9 +1932,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" dependencies = [ "libc", ] @@ -1959,9 +1947,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "litrs" @@ -1980,9 +1968,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru-slab" @@ -1992,19 +1980,19 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "macro_rules_attribute" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520" +checksum = "b3ae8f6d608c795738406608304d30a2dfbdc8e58e44f7ba43236da5208ded3c" dependencies = [ "macro_rules_attribute-proc_macro", - "paste", + "pastey 0.2.3", ] [[package]] name = "macro_rules_attribute-proc_macro" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" +checksum = "fc04a4c58212d57930a24bf47d3fa87485264a3a054e9c10e042eb373573ad3c" [[package]] name = "matchers" @@ -2017,9 +2005,9 @@ dependencies = [ [[package]] name = "matrixmultiply" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7" dependencies = [ "autocfg", "rawpointer", @@ -2027,9 +2015,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memoffset" @@ -2058,9 +2046,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi", @@ -2086,7 +2074,7 @@ checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2148,7 +2136,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2162,9 +2150,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] @@ -2214,9 +2202,9 @@ dependencies = [ [[package]] name = "ort" -version = "2.0.0-rc.12" +version = "2.0.0-rc.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7de3af33d24a745ffb8fab904b13478438d1cd52868e6f17735ef6e1f8bf133" +checksum = "4336a1e2b38848325241c72889086886004e589b7c74f335e60a8e8db5138a0b" dependencies = [ "libloading", "ndarray 0.17.2", @@ -2227,19 +2215,18 @@ dependencies = [ [[package]] name = "ort-sys" -version = "2.0.0-rc.12" +version = "2.0.0-rc.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7b497d21a8b6fbb4b5a544f8fadb77e801a09ae0add9e411d31c6f89e3c1e90" +checksum = "cf211e3776eea6aec988552fa118dd746d70e1b1e5e244058d1c98015f3e5872" [[package]] name = "pango" -version = "0.22.6" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "251bdc6e6487b811be0e406a21e301e07e45c0aa8fa39e00c0c8e12a91752438" +checksum = "5d800d8d0de2ad5d0fb046f5344dbaba14a003cf3dd27cc21d85893d35ea316c" dependencies = [ "gio", "glib", - "libc", "pango-sys", ] @@ -2252,7 +2239,7 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps", + "system-deps 7.0.8", ] [[package]] @@ -2305,6 +2292,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2337,7 +2330,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.6", + "rand 0.8.7", ] [[package]] @@ -2368,9 +2361,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "polling" @@ -2388,9 +2381,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "portable-atomic-util" @@ -2403,9 +2396,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -2419,39 +2412,29 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro-crate" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.12+spec-1.1.0", + "toml_edit 0.25.13+spec-1.1.0", ] [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -2461,7 +2444,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tracing", "web-time", @@ -2469,20 +2452,21 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" dependencies = [ "bytes", - "getrandom 0.3.4", + "getrandom 0.4.3", "lru-slab", - "rand 0.9.4", + "rand 0.10.2", + "rand_pcg", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror 2.0.20", "tinyvec", "tracing", "web-time", @@ -2490,23 +2474,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -2525,9 +2509,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -2536,14 +2520,25 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -2582,6 +2577,21 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "rawpointer" version = "0.2.1" @@ -2647,14 +2657,14 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -2664,9 +2674,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -2675,9 +2685,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" @@ -2714,7 +2724,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.7", + "webpki-roots 1.0.9", ] [[package]] @@ -2747,9 +2757,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -2770,14 +2780,14 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "log", "once_cell", @@ -2790,9 +2800,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", @@ -2800,9 +2810,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" dependencies = [ "ring", "rustls-pki-types", @@ -2811,9 +2821,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -2835,9 +2845,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -2845,29 +2855,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -2878,13 +2888,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -2919,12 +2929,12 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -2955,9 +2965,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "siphasher" @@ -2973,18 +2983,18 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3036,9 +3046,20 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -3062,7 +3083,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3074,7 +3095,20 @@ dependencies = [ "cfg-expr", "heck", "pkg-config", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", + "version-compare", +] + +[[package]] +name = "system-deps" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83779a5c956bcb6ba627a4ecf0a9d7625db47d7537e0892d97f712ac995648a3" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml 1.1.4+spec-1.1.0", "version-compare", ] @@ -3091,10 +3125,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3108,11 +3142,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.20", ] [[package]] @@ -3123,34 +3157,34 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -3158,9 +3192,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -3191,7 +3225,7 @@ dependencies = [ "macro_rules_attribute", "monostate", "paste", - "rand 0.9.4", + "rand 0.9.5", "rayon", "rayon-cond", "regex", @@ -3199,7 +3233,7 @@ dependencies = [ "serde", "serde_json", "spm_precompiled", - "thiserror 2.0.18", + "thiserror 2.0.20", "unicode-normalization-alignments", "unicode-segmentation", "unicode_categories", @@ -3207,9 +3241,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -3225,13 +3259,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -3258,9 +3292,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap", "serde_core", @@ -3268,7 +3302,7 @@ dependencies = [ "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -3305,23 +3339,23 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.12+spec-1.1.0" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -3332,9 +3366,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tower" @@ -3400,7 +3434,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3462,7 +3496,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3555,11 +3589,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.2" +version = "1.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "wasm-bindgen", ] @@ -3599,27 +3633,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.122" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", @@ -3630,9 +3655,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.72" +version = "0.4.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" dependencies = [ "js-sys", "wasm-bindgen", @@ -3640,9 +3665,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.122" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3650,65 +3675,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.122" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.122" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-sys" -version = "0.3.99" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" dependencies = [ "js-sys", "wasm-bindgen", @@ -3730,14 +3721,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.7", + "webpki-roots 1.0.9", ] [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -3785,7 +3776,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3796,7 +3787,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4065,112 +4056,24 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "xdg-home" @@ -4184,9 +4087,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.28" +version = "0.8.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" [[package]] name = "yoke" @@ -4207,7 +4110,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] @@ -4229,7 +4132,7 @@ dependencies = [ "hex", "nix", "ordered-stream", - "rand 0.8.6", + "rand 0.8.7", "serde", "serde_repr", "sha1", @@ -4253,7 +4156,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "zvariant_utils", ] @@ -4270,22 +4173,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.50" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.50" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4305,21 +4208,21 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -4328,9 +4231,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "94b5c6b5976d66c1d703c4fd17d3f5e43c8cedaacf604961b171adc7130896d8" dependencies = [ "yoke", "zerofrom", @@ -4339,20 +4242,20 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "47402523226a02bfe5230160dc3ccc089aa6f6f19e7fcbb4e6f824bbb1b4aa62" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zvariant" @@ -4376,7 +4279,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "zvariant_utils", ] @@ -4388,5 +4291,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] diff --git a/breadman/Cargo.toml b/breadman/Cargo.toml index e1ae340..e81b7e6 100644 --- a/breadman/Cargo.toml +++ b/breadman/Cargo.toml @@ -16,7 +16,7 @@ bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", # Shared `--screenshot` pair validation + settle delay (`screenshot_cli`). bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } # `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.2", features = ["adw"] } +bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.4", features = ["adw"] } libadwaita = { version = "0.9", features = ["v1_7"] } anyhow.workspace = true tracing.workspace = true diff --git a/breadman/src/main.rs b/breadman/src/main.rs index 9ef3d86..0a4f810 100644 --- a/breadman/src/main.rs +++ b/breadman/src/main.rs @@ -374,6 +374,7 @@ fn build_app_window( .default_width(960) .default_height(640) .build(); + bread_theme::gtk::bind_window_auto(&window); let hbox = gtk4::Box::builder() .orientation(gtk4::Orientation::Horizontal) @@ -616,6 +617,7 @@ fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState, prese .modal(true) .default_width(500) .build(); + bread_theme::gtk::bind_window_auto(&win); let vbox = gtk4::Box::builder() .orientation(gtk4::Orientation::Vertical) diff --git a/breadpad-shared/Cargo.toml b/breadpad-shared/Cargo.toml index 6e4edf9..30a1467 100644 --- a/breadpad-shared/Cargo.toml +++ b/breadpad-shared/Cargo.toml @@ -7,7 +7,8 @@ authors.workspace = true [dependencies] -bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["gtk"] } +bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.4", features = ["gtk"] } +gtk4.workspace = true anyhow.workspace = true tracing.workspace = true serde.workspace = true diff --git a/breadpad-shared/src/theme.rs b/breadpad-shared/src/theme.rs index 670b711..f7e2177 100644 --- a/breadpad-shared/src/theme.rs +++ b/breadpad-shared/src/theme.rs @@ -13,6 +13,11 @@ 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) { + bread_theme::gtk::bind_window_auto(window); +} + /// Generate the full breadpad/breadman CSS string. The base — `@define-color` /// palette, fonts, and generic widget styling — comes from the shared /// `bread_theme::stylesheet`, so breadpad and breadman look identical to the diff --git a/breadpad/src/main.rs b/breadpad/src/main.rs index d61330b..db05e04 100644 --- a/breadpad/src/main.rs +++ b/breadpad/src/main.rs @@ -451,6 +451,7 @@ fn build_reminder_window( window.set_layer(Layer::Overlay); window.set_keyboard_mode(KeyboardMode::Exclusive); window.auto_exclusive_zone_enable(); + breadpad_shared::theme::bind_window(&window); apply_css(&cfg); @@ -753,6 +754,7 @@ fn build_window( window.set_anchor(Edge::Bottom, false); window.set_anchor(Edge::Left, false); window.set_anchor(Edge::Right, false); + breadpad_shared::theme::bind_window(&window); apply_css(&cfg); From 51338af7eb5fba8c716ea2ea2f5ffedfd9b34754 Mon Sep 17 00:00:00 2001 From: Breadway Date: Sun, 16 Aug 2026 14:09:36 +0800 Subject: [PATCH 52/53] Bump version to v0.5.3 --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c60d1ef..940600b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -348,7 +348,7 @@ dependencies = [ [[package]] name = "breadman" -version = "0.5.1" +version = "0.5.3" dependencies = [ "anyhow", "bread-screenshots", @@ -369,7 +369,7 @@ dependencies = [ [[package]] name = "breadpad" -version = "0.5.1" +version = "0.5.3" dependencies = [ "anyhow", "bread-screenshots", @@ -390,7 +390,7 @@ dependencies = [ [[package]] name = "breadpad-shared" -version = "0.5.1" +version = "0.5.3" dependencies = [ "anyhow", "bread-theme", @@ -417,7 +417,7 @@ dependencies = [ [[package]] name = "breadpad-test" -version = "0.5.1" +version = "0.5.3" dependencies = [ "anyhow", "breadpad-shared", diff --git a/Cargo.toml b/Cargo.toml index 39b7707..84621db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.5.1" +version = "0.5.3" edition = "2021" license = "MIT" authors = ["Breadway"] From 39e26a1b9f9e52ecf2b8474bbb98d5fbd2331193 Mon Sep 17 00:00:00 2001 From: Breadway Date: Sun, 23 Aug 2026 14:42:57 +0800 Subject: [PATCH 53/53] Switch classifier sessions onto bread-onnx v0.7.2 Replace local ort Session::builder/ROCm wiring with bread_onnx::build_session (MIGraphX + CPU fallback, same pin as breadmill). Drop the breadman chip/init_adw shim now that bread-theme v0.7.4 exports those helpers. --- Cargo.lock | 123 ++++++++++++++++++++++------ Cargo.toml | 7 +- breadman/Cargo.toml | 2 +- breadman/src/editor.rs | 8 +- breadman/src/main.rs | 11 ++- breadman/src/theme_widgets.rs | 23 ------ breadman/src/views/settings.rs | 8 +- breadpad-shared/Cargo.toml | 2 +- breadpad-shared/src/classifier.rs | 98 +++++++++++----------- breadpad-shared/tests/classifier.rs | 42 ++++++++-- breadpad-shared/tests/pipeline.rs | 67 ++++++++++++--- 11 files changed, 254 insertions(+), 137 deletions(-) delete mode 100644 breadman/src/theme_widgets.rs diff --git a/Cargo.lock b/Cargo.lock index 940600b..4d85626 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -302,6 +302,21 @@ dependencies = [ "piper", ] +[[package]] +name = "bread-onnx" +version = "0.7.2" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73" +dependencies = [ + "anyhow", + "bread-utils", + "hex", + "ort", + "sha2", + "tokenizers", + "tracing", + "ureq", +] + [[package]] name = "bread-screenshots" version = "0.7.2" @@ -393,12 +408,12 @@ name = "breadpad-shared" version = "0.5.3" dependencies = [ "anyhow", + "bread-onnx", "bread-theme", "chrono", "dirs 5.0.1", "gtk4", "ical", - "ndarray 0.16.1", "ort", "regex", "reqwest", @@ -664,6 +679,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "console" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width", + "windows-sys 0.61.2", +] + [[package]] name = "convert_case" version = "0.10.0" @@ -764,6 +791,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "daachorse" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f55d7153ba3b507595872a3874803f07a8a81d1e888abed8e5db7da0597d6e2" + [[package]] name = "darling" version = "0.20.11" @@ -994,6 +1027,9 @@ name = "esaxx-rs" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" +dependencies = [ + "cc", +] [[package]] name = "event-listener" @@ -1017,9 +1053,9 @@ dependencies = [ [[package]] name = "fancy-regex" -version = "0.14.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" +checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8" dependencies = [ "bit-set", "regex-automata", @@ -1530,7 +1566,7 @@ checksum = "629d8f3bbeda9d148036d6b0de0a3ab947abd08ce90626327fc3547a49d59d97" dependencies = [ "dirs 6.0.0", "http", - "indicatif", + "indicatif 0.17.11", "libc", "log", "rand 0.9.5", @@ -1827,13 +1863,26 @@ version = "0.17.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" dependencies = [ - "console", + "console 0.15.11", "number_prefix", "portable-atomic", "unicode-width", "web-time", ] +[[package]] +name = "indicatif" +version = "0.18.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" +dependencies = [ + "console 0.16.4", + "portable-atomic", + "unicode-width", + "unit-prefix", + "web-time", +] + [[package]] name = "ipnet" version = "2.12.1" @@ -2077,21 +2126,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "ndarray" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" -dependencies = [ - "matrixmultiply", - "num-complex", - "num-integer", - "num-traits", - "portable-atomic", - "portable-atomic-util", - "rawpointer", -] - [[package]] name = "ndarray" version = "0.17.2" @@ -2184,6 +2218,28 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "onig" +version = "6.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" +dependencies = [ + "bitflags", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2207,7 +2263,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4336a1e2b38848325241c72889086886004e589b7c74f335e60a8e8db5138a0b" dependencies = [ "libloading", - "ndarray 0.17.2", + "ndarray", "ort-sys", "smallvec", "tracing", @@ -2938,6 +2994,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -3207,23 +3274,25 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenizers" -version = "0.21.4" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a620b996116a59e184c2fa2dfd8251ea34a36d0a514758c6f966386bd2e03476" +checksum = "44e5bea67576e04b6ff8564c5d9e09c2ef0cf476502245f2f120e497769d3112" dependencies = [ "ahash", - "aho-corasick", "compact_str", + "daachorse", "dary_heap", "derive_builder", "esaxx-rs", "fancy-regex", "getrandom 0.3.4", "hf-hub", + "indicatif 0.18.6", "itertools", "log", "macro_rules_attribute", "monostate", + "onig", "paste", "rand 0.9.5", "rayon", @@ -3538,6 +3607,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 84621db..b9508be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,9 +24,10 @@ chrono = { version = "0.4", features = ["serde"] } rrule = "0.12" tokio = { version = "1", features = ["full"] } zbus = { version = "4", default-features = false, features = ["tokio"] } -ort = { version = "2.0.0-rc.12", default-features = false, features = ["std", "ndarray", "tracing", "api-24", "rocm", "load-dynamic"] } -ndarray = "0.16" -tokenizers = { version = "0.21", default-features = false, features = ["http", "fancy-regex"] } +# WHY: bread-onnx's Provider enum references every EP type, so those ort +# features must be on in the consumer even if breadpad only requests MIGraphX. +ort = { version = "2.0.0-rc.12", default-features = false, features = ["std", "tracing", "api-24", "migraphx", "cuda", "openvino", "vitis", "load-dynamic"] } +tokenizers = { version = "0.23", default-features = false, features = ["http", "fancy-regex"] } gtk4 = { version = "0.11", features = ["v4_12"] } gtk4-layer-shell = "0.8" hyprland = "0.4.0-beta.3" diff --git a/breadman/Cargo.toml b/breadman/Cargo.toml index e81b7e6..d47a586 100644 --- a/breadman/Cargo.toml +++ b/breadman/Cargo.toml @@ -15,7 +15,7 @@ breadpad-shared = { path = "../breadpad-shared" } bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } # Shared `--screenshot` pair validation + settle delay (`screenshot_cli`). bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } -# `adw` implies `gtk`. Local chip/init shims remain in src/theme_widgets.rs. +# `adw` implies `gtk` (`chip`, `set_chip_active`, `adw::init`). bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.4", features = ["adw"] } libadwaita = { version = "0.9", features = ["v1_7"] } anyhow.workspace = true diff --git a/breadman/src/editor.rs b/breadman/src/editor.rs index bea9803..e39b11c 100644 --- a/breadman/src/editor.rs +++ b/breadman/src/editor.rs @@ -62,16 +62,16 @@ pub fn open_editor( 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| (crate::theme_widgets::chip(name), name)).collect(); + let type_pills: Vec<(gtk4::Button, &'static str)> = NoteType::all_builtin().iter().map(|&name| (bread_theme::gtk::chip(name), name)).collect(); for (btn, name) in &type_pills { - crate::theme_widgets::set_chip_active(btn, *name == selected_type.borrow().as_str()); + bread_theme::gtk::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 { crate::theme_widgets::set_chip_active(b, false); } - crate::theme_widgets::set_chip_active(clicked, true); + for b in &all_btns { bread_theme::gtk::set_chip_active(b, false); } + bread_theme::gtk::set_chip_active(clicked, true); }); type_pill_box.append(btn); } diff --git a/breadman/src/main.rs b/breadman/src/main.rs index 0a4f810..efd5b3f 100644 --- a/breadman/src/main.rs +++ b/breadman/src/main.rs @@ -14,7 +14,6 @@ use std::sync::Arc; mod editor; mod screenshot; -mod theme_widgets; mod views; // ── Args ───────────────────────────────────────────────────────────────────── @@ -363,7 +362,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. - theme_widgets::init_adw(); + bread_theme::adw::init(); let store = Arc::new(Store::new()?); let notes = store.load_all()?; @@ -645,17 +644,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| (theme_widgets::chip(name), NoteType::from_str(name))) + .map(|&name| (bread_theme::gtk::chip(name), NoteType::from_str(name))) .collect(); for (btn, nt) in &chips { - theme_widgets::set_chip_active(btn, *nt == preselect); + bread_theme::gtk::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 { theme_widgets::set_chip_active(b, false); } - theme_widgets::set_chip_active(clicked, true); + for b in &all_btns { bread_theme::gtk::set_chip_active(b, false); } + bread_theme::gtk::set_chip_active(clicked, true); }); chip_box.append(btn); } diff --git a/breadman/src/theme_widgets.rs b/breadman/src/theme_widgets.rs deleted file mode 100644 index a5221e6..0000000 --- a/breadman/src/theme_widgets.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! 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 114edd1..6090274 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| (crate::theme_widgets::chip(name), name)) + .map(|&name| (bread_theme::gtk::chip(name), name)) .collect(); for (btn, name) in &type_pills { - crate::theme_widgets::set_chip_active(btn, *name == selected_type.borrow().as_str()); + bread_theme::gtk::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 { crate::theme_widgets::set_chip_active(b, false); } - crate::theme_widgets::set_chip_active(clicked, true); + for b in &all_btns { bread_theme::gtk::set_chip_active(b, false); } + bread_theme::gtk::set_chip_active(clicked, true); apply_now(); }); } diff --git a/breadpad-shared/Cargo.toml b/breadpad-shared/Cargo.toml index 30a1467..a56c187 100644 --- a/breadpad-shared/Cargo.toml +++ b/breadpad-shared/Cargo.toml @@ -20,7 +20,7 @@ tokio.workspace = true zbus.workspace = true ort.workspace = true tokenizers.workspace = true -ndarray.workspace = true +bread-onnx = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } toml.workspace = true dirs.workspace = true regex.workspace = true diff --git a/breadpad-shared/src/classifier.rs b/breadpad-shared/src/classifier.rs index e752a31..35c2650 100644 --- a/breadpad-shared/src/classifier.rs +++ b/breadpad-shared/src/classifier.rs @@ -2,6 +2,8 @@ use crate::ai::OllamaClient; use crate::config::OllamaConfig; use crate::parser::parse_rule_based; use crate::types::{ClassificationResult, NoteType}; +use bread_onnx::{build_session, Provider}; +use ort::session::builder::GraphOptimizationLevel; use std::path::PathBuf; /// Minimum Tier 1 confidence needed to skip Tier 2 entirely. @@ -16,7 +18,7 @@ pub enum ExecutionProvider { impl ExecutionProvider { pub fn as_str(&self) -> &str { match self { - ExecutionProvider::Gpu => "ROCm (iGPU)", + ExecutionProvider::Gpu => "MIGraphX (iGPU)", ExecutionProvider::Cpu => "CPU", } } @@ -107,9 +109,7 @@ impl Classifier { // ── Tier 2 ─────────────────────────────────────────────────────────── // 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) { Ok(r) => { tracing::debug!("Tier 2: {:?} conf={:.2}", r.note_type, r.confidence); @@ -163,9 +163,18 @@ impl Classifier { // entailment score across all five passes. const HYPOTHESES: [(&str, &str); 5] = [ ("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 an idea, suggestion, or creative thought.", "idea"), - ("This note is a general observation or piece of information.", "note"), + ( + "This note is a reminder with a specific time or deadline.", + "reminder", + ), + ( + "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"), ]; @@ -184,15 +193,17 @@ fn run_onnx( .map_err(|e| anyhow::anyhow!("tokenize: {}", e))?; let ids: Vec = encoding.get_ids().iter().map(|&x| x as i64).collect(); - let mask: Vec = encoding.get_attention_mask().iter().map(|&x| x as i64).collect(); + let mask: Vec = encoding + .get_attention_mask() + .iter() + .map(|&x| x as i64) + .collect(); let len = ids.len(); - let ids_tensor = ort::value::Tensor::::from_array( - (vec![1i64, len as i64], ids) - ).map_err(|e| anyhow::anyhow!("ids tensor: {}", e))?; - let mask_tensor = ort::value::Tensor::::from_array( - (vec![1i64, len as i64], mask) - ).map_err(|e| anyhow::anyhow!("mask tensor: {}", e))?; + let ids_tensor = ort::value::Tensor::::from_array((vec![1i64, len as i64], ids)) + .map_err(|e| anyhow::anyhow!("ids tensor: {}", e))?; + let mask_tensor = ort::value::Tensor::::from_array((vec![1i64, len as i64], mask)) + .map_err(|e| anyhow::anyhow!("mask tensor: {}", e))?; let inputs = ort::inputs![ "input_ids" => ids_tensor, @@ -207,10 +218,7 @@ fn run_onnx( .map_err(|e| anyhow::anyhow!("extract logits: {}", e))?; 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 @@ -243,42 +251,30 @@ fn softmax_single(logits: &[f32], idx: usize) -> f32 { exps[idx] / sum } -fn try_load_session( - path: &std::path::Path, -) -> (Option, ExecutionProvider) { - // Try ROCm (iGPU) first, fall back to CPU. - let rocm_available = { - use ort::execution_providers::ExecutionProvider as _; - ort::ep::ROCm::default().is_available().unwrap_or(false) - }; - if rocm_available { - match build_onnx_session(path, ort::ep::ROCm::default().build()) { - Ok(s) => { - tracing::info!("ONNX session loaded (ROCm iGPU)"); - return (Some(s), ExecutionProvider::Gpu); - } - Err(e) => tracing::debug!("ROCm EP unavailable: {}; trying CPU", e), - } - } - match build_onnx_session(path, ort::ep::CPU::default().build()) { +fn try_load_session(path: &std::path::Path) -> (Option, ExecutionProvider) { + // WHY: distro onnxruntime-rocm is MIGraphX, not classic ROCm; bread-onnx + // appends CPU so a missing GPU EP does not disable Tier 2. + match build_session( + path, + GraphOptimizationLevel::Level3, + &[Provider::MiGraphX { device_id: 0 }], + ) { Ok(s) => { - tracing::info!("ONNX session loaded (CPU)"); - (Some(s), ExecutionProvider::Cpu) + tracing::info!("ONNX session loaded (MIGraphX, CPU fallback)"); + (Some(s), ExecutionProvider::Gpu) } Err(e) => { - tracing::warn!("failed to load ONNX session: {}; Tier 2 disabled", e); - (None, ExecutionProvider::Cpu) + tracing::debug!("MIGraphX session failed: {}; trying CPU", e); + match build_session(path, GraphOptimizationLevel::Level3, &[Provider::Cpu]) { + Ok(s) => { + tracing::info!("ONNX session loaded (CPU)"); + (Some(s), ExecutionProvider::Cpu) + } + Err(e) => { + tracing::warn!("failed to load ONNX session: {}; Tier 2 disabled", e); + (None, ExecutionProvider::Cpu) + } + } } } } - -fn build_onnx_session( - path: &std::path::Path, - ep: ort::ep::ExecutionProviderDispatch, -) -> anyhow::Result { - 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)) -} diff --git a/breadpad-shared/tests/classifier.rs b/breadpad-shared/tests/classifier.rs index 3b5527c..7f0294a 100644 --- a/breadpad-shared/tests/classifier.rs +++ b/breadpad-shared/tests/classifier.rs @@ -1,17 +1,24 @@ use breadpad_shared::classifier::{Classifier, ExecutionProvider}; use breadpad_shared::types::NoteType; 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 { - Classifier::load("08:00") + Classifier::load_with_paths( + "08:00", + PathBuf::from("/nonexistent/classifier.onnx"), + PathBuf::from("/nonexistent/tokenizer.json"), + ) } #[test] fn active_provider_is_valid() { // The active provider depends on the host: a machine with the ONNX model present and - // a working ROCm iGPU loads `Gpu`, otherwise `Cpu`. Either is valid — but when no + // a working MIGraphX 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). - let c = cl(); + let c = Classifier::load("08:00"); assert!(matches!( c.active_provider, ExecutionProvider::Cpu | ExecutionProvider::Gpu @@ -49,19 +56,28 @@ fn classify_reminder_via_fallback() { #[test] fn classify_idea_via_fallback() { let mut c = cl(); - assert_eq!(c.classify("what if we added a calendar view").note_type, NoteType::Idea); + assert_eq!( + c.classify("what if we added a calendar view").note_type, + NoteType::Idea + ); } #[test] fn classify_question_via_fallback() { let mut c = cl(); - assert_eq!(c.classify("why does this fail?").note_type, NoteType::Question); + assert_eq!( + c.classify("why does this fail?").note_type, + NoteType::Question + ); } #[test] fn classify_note_via_fallback() { let mut c = cl(); - assert_eq!(c.classify("meeting went well today").note_type, NoteType::Note); + assert_eq!( + c.classify("meeting went well today").note_type, + NoteType::Note + ); } #[test] @@ -74,7 +90,11 @@ fn classify_recurrence_via_fallback() { #[test] fn classify_custom_morning_time() { - let mut c = Classifier::load("07:15"); + let mut c = Classifier::load_with_paths( + "07:15", + PathBuf::from("/nonexistent/classifier.onnx"), + PathBuf::from("/nonexistent/tokenizer.json"), + ); let r = c.classify("sync tomorrow morning"); let t = r.time.expect("should have a time for tomorrow morning"); let local: chrono::DateTime = t.into(); @@ -114,12 +134,16 @@ fn classify_returns_cleaned_body() { let mut c = cl(); let r = c.classify("call mum at 6pm"); assert!(r.body.contains("call mum"), "body: {}", r.body); - assert!(!r.body.contains("6pm"), "time phrase should be stripped from body: {}", r.body); + assert!( + !r.body.contains("6pm"), + "time phrase should be stripped from body: {}", + r.body + ); } #[test] fn model_path_points_to_expected_location() { - let c = cl(); + let c = Classifier::load("08:00"); assert!( c.model_path.to_str().unwrap().contains("breadpad"), "model path: {:?}", diff --git a/breadpad-shared/tests/pipeline.rs b/breadpad-shared/tests/pipeline.rs index 84cc850..603a199 100644 --- a/breadpad-shared/tests/pipeline.rs +++ b/breadpad-shared/tests/pipeline.rs @@ -9,12 +9,18 @@ use breadpad_shared::classifier::Classifier; use breadpad_shared::store::Store; use breadpad_shared::types::{Note, NoteType}; use chrono::Timelike; +use std::path::PathBuf; use tempfile::TempDir; // Mirrors commit_note() in breadpad/src/main.rs. // `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 { - let mut classifier = Classifier::load("08:00"); + // WHY: pipeline tests cover classify→save→reload, not a host ONNX model. + 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 mut note = Note::new(text.into(), user_type.clone(), None); @@ -61,7 +67,11 @@ fn todo_note_appears_in_store() { #[test] fn idea_note_appears_in_store() { let (dir, store) = setup(); - capture(&store, "what if we added dark mode", NoteType::from_str("note")); + capture( + &store, + "what if we added dark mode", + NoteType::from_str("note"), + ); let notes = breadman_store(&dir).load_all().unwrap(); assert_eq!(notes.len(), 1); @@ -71,7 +81,11 @@ fn idea_note_appears_in_store() { #[test] fn question_note_appears_in_store() { let (dir, store) = setup(); - capture(&store, "why does the cache miss on cold start?", NoteType::from_str("note")); + capture( + &store, + "why does the cache miss on cold start?", + NoteType::from_str("note"), + ); let notes = breadman_store(&dir).load_all().unwrap(); assert_eq!(notes.len(), 1); @@ -97,7 +111,10 @@ fn reminder_has_time_set() { let notes = breadman_store(&dir).load_all().unwrap(); assert_eq!(notes[0].note_type, NoteType::Reminder); - assert!(notes[0].time.is_some(), "reminder should have a scheduled time"); + assert!( + notes[0].time.is_some(), + "reminder should have a scheduled time" + ); let local: chrono::DateTime = notes[0].time.unwrap().into(); assert_eq!(local.hour(), 18); } @@ -108,14 +125,21 @@ fn reminder_body_has_time_stripped() { capture(&store, "call mum at 6pm", NoteType::from_str("note")); let notes = breadman_store(&dir).load_all().unwrap(); - assert!(!notes[0].body.contains("6pm"), "time phrase should be removed from body"); + assert!( + !notes[0].body.contains("6pm"), + "time phrase should be removed from body" + ); assert!(notes[0].body.contains("call mum")); } #[test] fn in_duration_reminder_has_time() { let (dir, store) = setup(); - capture(&store, "check on the build in 30 minutes", NoteType::from_str("note")); + capture( + &store, + "check on the build in 30 minutes", + NoteType::from_str("note"), + ); let notes = breadman_store(&dir).load_all().unwrap(); assert_eq!(notes[0].note_type, NoteType::Reminder); @@ -127,7 +151,11 @@ fn in_duration_reminder_has_time() { #[test] fn recurring_reminder_has_rrule() { let (dir, store) = setup(); - capture(&store, "standup every monday at 9am", NoteType::from_str("note")); + capture( + &store, + "standup every monday at 9am", + NoteType::from_str("note"), + ); let notes = breadman_store(&dir).load_all().unwrap(); assert_eq!(notes[0].note_type, NoteType::Reminder); @@ -139,11 +167,20 @@ fn recurring_reminder_has_rrule() { #[test] fn daily_reminder_has_rrule() { let (dir, store) = setup(); - capture(&store, "drink water every day at 8am", NoteType::from_str("note")); + capture( + &store, + "drink water every day at 8am", + NoteType::from_str("note"), + ); let notes = breadman_store(&dir).load_all().unwrap(); assert_eq!(notes[0].note_type, NoteType::Reminder); - assert!(notes[0].rrule.as_ref().unwrap().as_str().contains("FREQ=DAILY")); + assert!(notes[0] + .rrule + .as_ref() + .unwrap() + .as_str() + .contains("FREQ=DAILY")); } // ---- user-forced type is respected ---- @@ -155,7 +192,11 @@ fn user_selected_type_overrides_classifier() { capture(&store, "fix the login bug", NoteType::Idea); let notes = breadman_store(&dir).load_all().unwrap(); - assert_eq!(notes[0].note_type, NoteType::Idea, "user chip selection should win over classifier"); + assert_eq!( + notes[0].note_type, + NoteType::Idea, + "user chip selection should win over classifier" + ); } #[test] @@ -173,7 +214,11 @@ fn user_selected_reminder_overrides_classifier() { fn three_notes_all_visible_to_breadman() { let (dir, store) = setup(); capture(&store, "buy milk", NoteType::from_str("note")); - capture(&store, "what if we rewrote in Zig", NoteType::from_str("note")); + capture( + &store, + "what if we rewrote in Zig", + NoteType::from_str("note"), + ); capture(&store, "team standup went well", NoteType::from_str("note")); let notes = breadman_store(&dir).load_all().unwrap();