Compare commits
4 commits
b7aed8a37c
...
710cb768ea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
710cb768ea | ||
|
|
6a06872f09 | ||
|
|
369935515b | ||
|
|
6e5448e853 |
14 changed files with 600 additions and 442 deletions
741
Cargo.lock
generated
741
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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))?;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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(¬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<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, ¬es)
|
||||
}
|
||||
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue