breadbox/EVENTS.md
Breadway 09530e236d
All checks were successful
check / check (push) Successful in 1m29s
Adopt bread-launcher: shared matching/ranking/rows + manifest theming
breadbox's launcher is now a thin host over the `bread-launcher` crate
(bread-ecosystem v0.7.5) — the same core breadbar's embedded capsule
uses — instead of a private implementation:

- `breadbox-shared` becomes a re-export shim over `bread_launcher`
  (`DesktopEntry`, `LaunchHistory`, `config_dir`, `LAUNCHER_APP`); the
  local fuzzy-match / ranking / desktop-file parsing is deleted.
- `run_ui` builds `bread_launcher::gtk::ResultsList` for the results
  list and calls `bread_launcher::do_launch` / `load_sorted_entries`;
  the hand-rolled row/icon/score code is gone.
- Launcher geometry and style come from the active shell-theme manifest
  (`bread_theme::shell`, via the new `src/theme.rs` accessor mirroring
  breadbar's) — width, radius, panel alpha, top margin, icon size,
  footer wording. The Daylight / liquid-motion look is the manifest's
  job now, not hardcoded CSS.
- Keeps `main`'s recent improvements on top of the branch work: the
  overlay window + outside-click come from `bread_utils::gtk_popup`
  (`new_overlay_window` / `bind_window_auto` / `close_on_outside_click`),
  replacing the manual layer-shell + GestureClick boilerplate.
- Embedded-open redirect: when the active theme embeds the launcher
  (spotlight capsule), breadbox emits `bread.box.open_requested` on the
  bus instead of mapping its own window; falls back to its own overlay
  when breadd is unreachable.
- Pins: bread-theme / bread-utils / bread-screenshots / bread-launcher
  → v0.7.5 (bread-launcher and `bread_theme::shell` first ship there).
  `.cargo/` gitignored — the local sibling-checkout source override
  (`fa85fc1`) is no longer needed now that v0.7.5 is tagged.

Reconciles the long-lived `feature/launcher-core` (13 commits, incl.
`feature/launcher-theming`) with `main`'s independent launcher redesign
(`feature/liquid-motion` + `bread_utils::gtk_popup` adoption). Applied
as one squashed reconciliation rather than a 13-commit rebase because
the two lines rewrote the same ~200-line `run_ui`/`build_css` region
in incompatible directions.

cargo build --locked / test --locked / clippy --all-targets --locked
-D warnings all clean.
2026-08-31 15:59:32 +08:00

7.4 KiB

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. One-shot launcher invocations each emit on their own fire-and-forget connection. Command verbs are only received while breadbox listen is running — that process holds the bread.command.box.** subscription open.

Under an [launcher] mode = "embedded" theme (spotlight), breadbar's own bar-drawer capsule is the launcher UI, and breadbox redirects instead of mapping its own overlay window — see the "Embedded launcher theme" section below. Everything else on this page describes the default (overlay-mode) behavior.

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.
bread.box.open.done {} bread.command.box.open was received (via breadbox listen) and this binary was spawned — non-embedded themes only. Not proof the overlay mapped, just that the process was started (same toggle as a keybind). Never emitted under an embedded theme — see below.
bread.box.open.failed { "error": "<message>" } bread.command.box.open was received but this binary could not be started — non-embedded themes only. Unreachable under an embedded theme, since that branch returns before ever attempting to spawn.
bread.box.open_requested {} Emitted by the plain breadbox binary itself (not breadbox listen) instead of mapping its own overlay window, only when the active theme's [launcher].mode is embedded. breadbar's capsule (launcher_command.rs) subscribes to this and opens itself. Own-namespace event, not a command — see dispatch_embedded_open's doc comment for why bread.command.box.open would be the wrong shape here.
bread.box.open.redirected {} Emitted by breadbox listen's handle_open, only when bread.command.box.open is received while an embedded theme is active. Replaces .done/.failed in that case: this process never spawns anything (breadbar's capsule is the intended handler) and has no way to confirm breadbar actually received or handled the event — pub/sub here is one-way with no ack. This event means "redirect happened, outcome unknown", not "succeeded".

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.*)

These are only received while breadbox listen is running. Publishing a command with no subscriber is a silent no-op — that is the documented bread convention, not a breadbox bug.

Verb Data Effect
open none Under a non-embedded theme: same as running breadbox (toggle the launcher overlay via the existing singleton). Emits bread.box.open.done / .failed. Under an embedded theme: never spawns; emits bread.box.open.redirected instead — see "Embedded launcher theme" below.
-- Non-embedded themes: `.done` is a real completion signal.
bread.spawn(function()
    bread.emit("bread.command.box.open")
    bread.wait("bread.box.open.done", { timeout = 5000 })
end)

Under an embedded theme this .done wait will time out — .done is never emitted there. A workflow that needs to work under every theme should wait on bread.box.open.redirected too (or treat a timeout as "probably fine, embedded themes have no ack" rather than a failure).

Not implemented: extra verbs

There is no launch / close / query command verb. Picking a desktop id from the bus would be a new product surface. If/when that exists, add the corresponding bread.command.box.* verb at the same time, not stubbed as a no-op ahead of it.

Embedded launcher theme (spotlight)

When the active shell theme's [launcher].mode is embedded, running breadbox directly (e.g. from a keybind) does not map this binary's overlay window — that would stack a second launcher on top of breadbar's own capsule. Instead:

  1. breadbox's main checks BreadClient::health() — a real, bounded-timeout round trip to breadd, not just "did the socket exist".
  2. If breadd is reachable, it emits bread.box.open_requested and returns. breadbar's capsule (subscribed only while its own active theme is also embedded) is expected to open itself in response. There is no ack for this — if breadbar isn't running, or is running under a different theme and therefore never subscribed, this is still a silent no-op on the bus with nothing further to fall back to. (BreadClient has no "is anyone subscribed" query.)
  3. If breadd is not reachable, breadbox logs that to stderr and falls back to mapping its own overlay window anyway — a keybind press always opens something, rather than the historical fully silent no-op when the bus was down.

breadbox listen's handle_open (the bread.command.box.open handler) applies the same "never map a second launcher" rule but from the command side: under an embedded theme it never spawns, logs to stderr, and emits bread.box.open.redirected instead of .done — see the events table above for why .done would be a false claim here.

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) and the command subscription simply never receives anything — launching, history, theming, and the singleton toggle are entirely unaffected. Under an embedded theme specifically, a direct breadbox invocation additionally checks reachability up front and falls back to mapping its own overlay window when breadd is down (see above) — so this invariant now holds there too, not just for every other theme.
  • If breadd restarts, the command subscription reconnects automatically (BreadClient::subscribe's background thread has its own backoff loop); no restart of breadbox listen is needed.
  • If breadbox listen is not running, commands are a graceful no-op at the bus (no subscriber). The CLI still works, and one-shot invocations still emit bread.box.launched on their own short-lived connection.
  • Closing the launcher without launching anything emits nothing.
  • What is not covered: under an embedded theme, if breadd is reachable but breadbar itself isn't running (or isn't subscribed), neither the direct-keybind path nor the command path can detect that or fall back further — both are fire-and-forget with no ack. This is a known limitation of the current transport, not an oversight.