Emit bread.box.launched after a successful app launch
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:
parent
e5ce91a9c9
commit
1b02cec0a6
6 changed files with 93 additions and 7 deletions
|
|
@ -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
12
Cargo.lock
generated
|
|
@ -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
41
EVENTS.md
Normal 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.
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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"`.
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue