From 1b02cec0a613fbf6f61608ab81f5d7a571ef0f6e Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 15 Aug 2026 22:14:42 +0800 Subject: [PATCH] Emit bread.box.launched after a successful app launch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Link bread-utils bread-client (tag v0.7.1) from the breadbox crate. Fire-and-forget via BreadClient; silent if breadd is down. No command verbs — breadbox is not a daemon. EVENTS.md is the contract (app id box). --- AGENTS.md | 2 +- Cargo.lock | 12 +++++++++++ EVENTS.md | 41 ++++++++++++++++++++++++++++++++++++++ breadbox-shared/src/lib.rs | 10 ++++++++++ breadbox/Cargo.toml | 2 +- breadbox/src/main.rs | 33 +++++++++++++++++++++++++----- 6 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 EVENTS.md diff --git a/AGENTS.md b/AGENTS.md index 578d722..a48505f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ Follow [`CONTRIBUTING.md`](CONTRIBUTING.md). Single-trunk: `main` plus short-liv - `github` — mirror. Day-to-day push `origin` only. ## Product -GTK4 app launcher + `breadbox-sync` icon cache. Theme via `bread-theme` (pin by tag on `git.breadway.dev`). Toggle uses `bread-utils::singleton`, not a homegrown PID file. +GTK4 app launcher + `breadbox-sync` icon cache. Theme via `bread-theme` (pin by tag on `git.breadway.dev`). Toggle uses `bread-utils::singleton`, not a homegrown PID file. `EVENTS.md` is the bread-event contract (app id `box`); emit `bread.box.launched` after a successful launch. No command verbs. ## Distribution Bakery (`bakery.toml`). Forgejo `.forgejo/workflows/` is canonical; do not re-add a GitHub Actions release workflow. diff --git a/Cargo.lock b/Cargo.lock index d05d5ff..142e3f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,6 +92,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "bread-shared" +version = "0.7.0" +source = "git+https://git.breadway.dev/Breadway/bread?tag=v0.7.0#22e34e2cf2202305d7960759dfccb54dc79f948b" +dependencies = [ + "dirs", + "serde", + "serde_json", + "toml 0.8.23", +] + [[package]] name = "bread-theme" version = "0.3.1" @@ -108,6 +119,7 @@ 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", "serde", "serde_json", diff --git a/EVENTS.md b/EVENTS.md new file mode 100644 index 0000000..6e76638 --- /dev/null +++ b/EVENTS.md @@ -0,0 +1,41 @@ +# breadbox — bread event integration + +breadbox is a standalone app launcher: it works exactly the same with or +without `breadd` running. When breadd *is* present, the GTK launcher +publishes a single event into the shared bread automation fabric after a +successful launch. 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: **`box`**. Transport: `bread-utils`'s `bread_client` module +(feature `bread-client`) — `breadbox` links it directly. breadbox is a +short-lived process (it exits when the launcher closes), so each `emit` +is its own short-lived connection. There is no long-running daemon and +therefore no command subscription. + +## Events published (`bread.box.*`) + +| Event | Data | When | +|-------|------|------| +| `bread.box.launched` | `{ "id": "", "name": "" }` | The user launched an app (Enter / keypad Enter on the selected row, or activating a row) **and** the spawn succeeded. Not emitted if `Command::spawn` fails (missing terminal, `exec` that cannot start). `id` is the desktop-file id (the `.desktop` filename, e.g. `firefox.desktop`), falling back to the stripped `Exec=` line when that id is empty. `name` is the desktop-entry display name. | + +Launch history is local to breadbox (`~/.cache/breadbox/history.json`); +the event bus is a notification that a launch happened, not a channel +for the exec line's arguments or the resulting process. + +## Commands honored (`bread.command.box.*`) + +None. breadbox is not a daemon — it is not running (and not subscribed) +except while the launcher overlay is open. There is no existing +"launch this desktop id from the bus" product surface, and inventing +`bread.command.box.launch` (or similar) without that surface would be a +stub. If/when breadbox grows a long-running piece that can honor a +verb, the corresponding `bread.command.box.*` command should be added +at the same time, not stubbed out 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) — launching, + history, theming, and the singleton toggle are entirely unaffected. +- Closing the launcher without launching anything emits nothing. diff --git a/breadbox-shared/src/lib.rs b/breadbox-shared/src/lib.rs index 5286729..92c573b 100644 --- a/breadbox-shared/src/lib.rs +++ b/breadbox-shared/src/lib.rs @@ -54,6 +54,9 @@ pub fn app_dirs() -> Vec { #[derive(Debug, Clone)] pub struct DesktopEntry { + /// Desktop file id (the `.desktop` filename, e.g. `firefox.desktop`). + /// Empty only if the path had no file name; callers fall back to `exec`. + pub id: String, pub name: String, pub exec: String, pub icon_name: String, @@ -155,7 +158,14 @@ pub fn parse_desktop(path: &Path) -> Option { .map(|s| s.to_string()) .collect(); + let id = path + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .filter(|s| !s.is_empty()) + .unwrap_or_default(); + Some(DesktopEntry { + id, name, exec, icon_name, diff --git a/breadbox/Cargo.toml b/breadbox/Cargo.toml index acc4146..8846db8 100644 --- a/breadbox/Cargo.toml +++ b/breadbox/Cargo.toml @@ -10,7 +10,7 @@ path = "src/main.rs" [dependencies] bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1", features = ["gtk"] } -bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1" } +bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1", features = ["bread-client"] } # Capture primitives for `--screenshot` mode — see src/screenshot.rs. # Not on tag v0.7.1 (crate landed after that tag); rev-pin so this is not # `branch = "main"`. diff --git a/breadbox/src/main.rs b/breadbox/src/main.rs index 2de4682..449b1ed 100644 --- a/breadbox/src/main.rs +++ b/breadbox/src/main.rs @@ -1,4 +1,5 @@ use bread_theme::{hex_to_rgba, ink_on, load_palette, Palette}; +use bread_utils::bread_client::BreadClient; use std::{ cell::RefCell, collections::HashMap, @@ -11,6 +12,10 @@ use std::{ rc::Rc, }; +/// This app's id in bread's sibling-app namespace registry +/// (`bread_shared::apps::KNOWN_APPS`) — events publish as `bread.box.*`. +const APP_ID: &str = "box"; + use breadbox_shared::{ config_dir, load_all_desktop_entries, Config, DesktopEntry, IconCache, LaunchHistory, }; @@ -206,24 +211,42 @@ fn pick_terminal() -> String { fn do_launch(entry: &DesktopEntry) { let cmd = entry.exec.trim(); - if entry.terminal { + let spawned = if entry.terminal { let term = pick_terminal(); - let _ = Command::new(&term) + Command::new(&term) .args(["-e", "bash", "-c", cmd]) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) - .spawn(); + .spawn() } else { - let _ = Command::new("bash") + Command::new("bash") .args(["-c", cmd]) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) - .spawn(); + .spawn() + }; + if spawned.is_ok() { + emit_launched(entry); } } +/// Publishes `bread.box.launched` after a successful spawn. Fire-and-forget +/// and non-fatal (`BreadClient::emit` never blocks or errors this caller) — +/// breadd being absent must never affect launching itself. +fn emit_launched(entry: &DesktopEntry) { + let id = if entry.id.is_empty() { + entry.exec.as_str() + } else { + entry.id.as_str() + }; + BreadClient::connect(APP_ID).emit( + "bread.box.launched", + serde_json::json!({ "id": id, "name": entry.name }), + ); +} + // ---- Fuzzy matching --------------------------------------------------------- fn fuzzy_matches(pattern: &str, text: &str) -> bool {