Compare commits

...

4 commits

Author SHA1 Message Date
Breadway
710cb768ea Switch to tag-pinned bread-ecosystem deps; bump version to v0.5.0
All checks were successful
Mirror to GitHub / mirror (push) Successful in 2s
release / build (push) Successful in 2m51s
Build and publish package / package (push) Successful in 3m30s
2026-07-19 03:52:59 +08:00
Breadway
6a06872f09 Fix silent ROCm-to-CPU fallback: switch to MIGraphX; fix XDG tilde bugs
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.
2026-07-17 13:53:00 +08:00
Breadway
369935515b breadpad-shared: fix stale test expectation from the bread-theme v0.2.10 bump
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).
2026-07-17 08:32:17 +08:00
Breadway
6e5448e853 breadpad: save note before window can close, add network timeouts, lock the JSONL store, fix monthly recurrence, restrict config permissions
- 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.
2026-07-17 08:28:19 +08:00
14 changed files with 600 additions and 442 deletions

741
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,7 @@ members = [
resolver = "2"
[workspace.package]
version = "0.4.1"
version = "0.5.0"
edition = "2021"
license = "MIT"
authors = ["Breadway"]
@ -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"

View file

@ -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/

View file

@ -26,6 +26,8 @@ regex.workspace = true
ureq.workspace = true
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
ical = "0.11"
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"

View file

@ -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))?;

View file

@ -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);

View file

@ -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<ort::session::Session>, 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<ort::session::Session> {
let mut builder = ort::session::Session::builder()
.map_err(|e| anyhow::anyhow!("builder: {}", e))?
.with_execution_providers([ep])
.map_err(|e| anyhow::anyhow!("ep: {}", e))?;
builder.commit_from_file(path).map_err(|e| anyhow::anyhow!("load: {}", e))
}

View file

@ -193,20 +193,32 @@ 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(())
}
}
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")
}

View file

@ -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<Local> = 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<Local> = 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<Local> = t.unwrap().into();
assert_eq!(local.day(), 31);
}
#[test]

View file

@ -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<fs::File> {
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<Vec<Note>> {
let _lock = self.acquire_lock()?;
self.load_from(&self.notes_path)
}
pub fn load_archive(&self) -> Result<Vec<Note>> {
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<Note>, Vec<Note>) = 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<Note>, Vec<Note>) =
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(&note), 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<F>(&self, mut f: F) -> Result<()>
where
F: FnMut(Note) -> Note,
{
let notes: Vec<Note> = self.load_all()?.into_iter().map(|n| f(n)).collect();
let _lock = self.acquire_lock()?;
let notes: Vec<Note> = self.load_from(&self.notes_path)?.into_iter().map(|n| f(n)).collect();
self.write_all(&self.notes_path, &notes)
}
@ -141,8 +187,9 @@ impl Store {
}
pub fn rotate_archive(&self, archive_after_days: i64) -> Result<usize> {
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<Note>, Vec<Note>) = notes
.into_iter()
.partition(|n| n.done && n.completed.map_or(false, |c| c < cutoff));

View file

@ -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]

View file

@ -23,3 +23,4 @@ gtk4-layer-shell.workspace = true
hyprland.workspace = true
dirs.workspace = true
tokio.workspace = true
bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.3.0" }

View file

@ -536,13 +536,14 @@ fn run_popup(preset_type: Option<String>, no_classify: bool, cfg: Config) -> Res
}
fn get_active_workspace() -> Option<String> {
// 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(
@ -694,6 +695,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 +703,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 +714,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);
});
}
};

View file

@ -1,11 +1,11 @@
# Maintainer: Breadway <rileyhorsham@gmail.com>
# Maintainer: Breadway <plasticbread849@gmail.com>
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,