Emit bread.box.launched after a successful app launch
All checks were successful
check / check (push) Successful in 16s
dev release / build (push) Successful in 33s

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).
This commit is contained in:
Breadway 2026-08-15 22:14:42 +08:00
parent e5ce91a9c9
commit 1b02cec0a6
6 changed files with 93 additions and 7 deletions

View file

@ -7,7 +7,7 @@ Follow [`CONTRIBUTING.md`](CONTRIBUTING.md). Single-trunk: `main` plus short-liv
- `github` — mirror. Day-to-day push `origin` only. - `github` — mirror. Day-to-day push `origin` only.
## Product ## 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 ## Distribution
Bakery (`bakery.toml`). Forgejo `.forgejo/workflows/` is canonical; do not re-add a GitHub Actions release workflow. Bakery (`bakery.toml`). Forgejo `.forgejo/workflows/` is canonical; do not re-add a GitHub Actions release workflow.

12
Cargo.lock generated
View file

@ -92,6 +92,17 @@ dependencies = [
"tracing", "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]] [[package]]
name = "bread-theme" name = "bread-theme"
version = "0.3.1" version = "0.3.1"
@ -108,6 +119,7 @@ name = "bread-utils"
version = "0.3.1" version = "0.3.1"
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1#db2fa3c4b4c1e6933bc5cf62a236d05972fdc886" source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1#db2fa3c4b4c1e6933bc5cf62a236d05972fdc886"
dependencies = [ dependencies = [
"bread-shared",
"dirs", "dirs",
"serde", "serde",
"serde_json", "serde_json",

41
EVENTS.md Normal file
View file

@ -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": "<desktop id or exec>", "name": "<display 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.

View file

@ -54,6 +54,9 @@ pub fn app_dirs() -> Vec<PathBuf> {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DesktopEntry { 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 name: String,
pub exec: String, pub exec: String,
pub icon_name: String, pub icon_name: String,
@ -155,7 +158,14 @@ pub fn parse_desktop(path: &Path) -> Option<DesktopEntry> {
.map(|s| s.to_string()) .map(|s| s.to_string())
.collect(); .collect();
let id = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.filter(|s| !s.is_empty())
.unwrap_or_default();
Some(DesktopEntry { Some(DesktopEntry {
id,
name, name,
exec, exec,
icon_name, icon_name,

View file

@ -10,7 +10,7 @@ path = "src/main.rs"
[dependencies] [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.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. # 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 # Not on tag v0.7.1 (crate landed after that tag); rev-pin so this is not
# `branch = "main"`. # `branch = "main"`.

View file

@ -1,4 +1,5 @@
use bread_theme::{hex_to_rgba, ink_on, load_palette, Palette}; use bread_theme::{hex_to_rgba, ink_on, load_palette, Palette};
use bread_utils::bread_client::BreadClient;
use std::{ use std::{
cell::RefCell, cell::RefCell,
collections::HashMap, collections::HashMap,
@ -11,6 +12,10 @@ use std::{
rc::Rc, 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::{ use breadbox_shared::{
config_dir, load_all_desktop_entries, Config, DesktopEntry, IconCache, LaunchHistory, config_dir, load_all_desktop_entries, Config, DesktopEntry, IconCache, LaunchHistory,
}; };
@ -206,24 +211,42 @@ fn pick_terminal() -> String {
fn do_launch(entry: &DesktopEntry) { fn do_launch(entry: &DesktopEntry) {
let cmd = entry.exec.trim(); let cmd = entry.exec.trim();
if entry.terminal { let spawned = if entry.terminal {
let term = pick_terminal(); let term = pick_terminal();
let _ = Command::new(&term) Command::new(&term)
.args(["-e", "bash", "-c", cmd]) .args(["-e", "bash", "-c", cmd])
.stdin(Stdio::null()) .stdin(Stdio::null())
.stdout(Stdio::null()) .stdout(Stdio::null())
.stderr(Stdio::null()) .stderr(Stdio::null())
.spawn(); .spawn()
} else { } else {
let _ = Command::new("bash") Command::new("bash")
.args(["-c", cmd]) .args(["-c", cmd])
.stdin(Stdio::null()) .stdin(Stdio::null())
.stdout(Stdio::null()) .stdout(Stdio::null())
.stderr(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 --------------------------------------------------------- // ---- Fuzzy matching ---------------------------------------------------------
fn fuzzy_matches(pattern: &str, text: &str) -> bool { fn fuzzy_matches(pattern: &str, text: &str) -> bool {