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