Honor bread.command.search.open via breadsearch listen
All checks were successful
dev release / build (push) Successful in 1m45s
All checks were successful
dev release / build (push) Successful in 1m45s
This commit is contained in:
parent
e20c64fa1a
commit
cdee9da609
3 changed files with 125 additions and 9 deletions
44
EVENTS.md
44
EVENTS.md
|
|
@ -10,8 +10,10 @@ convention this follows.
|
||||||
App id: **`search`**. Transport: `bread-utils`'s `bread_client` module
|
App id: **`search`**. Transport: `bread-utils`'s `bread_client` module
|
||||||
(feature `bread-client`) — the overlay links it directly. Each `emit` is
|
(feature `bread-client`) — the overlay links it directly. Each `emit` is
|
||||||
its own short-lived connection (`BreadClient::emit` is fire-and-forget,
|
its own short-lived connection (`BreadClient::emit` is fire-and-forget,
|
||||||
the same stance as `bread-emit`). breadmill, the indexing daemon, does
|
the same stance as `bread-emit`). Command verbs are only received while
|
||||||
not talk to breadd.
|
`breadsearch listen` is running — that process holds the
|
||||||
|
`bread.command.search.**` subscription open. breadmill, the indexing
|
||||||
|
daemon, does not talk to breadd.
|
||||||
|
|
||||||
## Events published (`bread.search.*`)
|
## Events published (`bread.search.*`)
|
||||||
|
|
||||||
|
|
@ -19,18 +21,42 @@ not talk to breadd.
|
||||||
|-------|------|------|
|
|-------|------|------|
|
||||||
| `bread.search.opened` | `{}` | The overlay window maps (the search panel is shown). |
|
| `bread.search.opened` | `{}` | The overlay window maps (the search panel is shown). |
|
||||||
| `bread.search.opened_result` | `{ "path": "<hit path>" }` | The user opens a hit — Enter / click opens the file, Ctrl+Enter reveals its folder. `path` is the hit's document path, not the parent folder. |
|
| `bread.search.opened_result` | `{ "path": "<hit path>" }` | The user opens a hit — Enter / click opens the file, Ctrl+Enter reveals its folder. `path` is the hit's document path, not the parent folder. |
|
||||||
|
| `bread.search.open.done` | `{}` | `bread.command.search.open` was received and `breadsearch` was spawned. This is the command confirmation, not proof the overlay mapped — the spawned process is the same PID-file toggle as a keybind. |
|
||||||
|
| `bread.search.open.failed` | `{ "error": "<message>" }` | `bread.command.search.open` was received but this binary could not be started. |
|
||||||
|
|
||||||
## Commands honored (`bread.command.search.*`)
|
## Commands honored (`bread.command.search.*`)
|
||||||
|
|
||||||
None. The overlay is a short-lived toggle process with no existing command
|
These are only received while `breadsearch listen` is running. Publishing a
|
||||||
surface (no show/hide/query IPC beyond the PID-file toggle and breadmill's
|
command with no subscriber is a silent no-op — that is the documented
|
||||||
own query socket). Adding verbs would mean inventing a control plane that
|
bread convention, not a breadsearch bug.
|
||||||
does not exist; if/when breadsearch grows one, the corresponding
|
|
||||||
`bread.command.search.*` verbs should be added at the same time, not stubbed
|
| Verb | Data | Effect |
|
||||||
out ahead of it.
|
|------|------|--------|
|
||||||
|
| `open` | none | Same as running `breadsearch` (PID-file toggle: show the overlay, or dismiss it if it is already up). Emits `bread.search.open.done` / `.failed`. |
|
||||||
|
|
||||||
|
```lua
|
||||||
|
bread.spawn(function()
|
||||||
|
bread.emit("bread.command.search.open")
|
||||||
|
bread.wait("bread.search.open.done", { timeout = 5000 })
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Not implemented: extra verbs
|
||||||
|
|
||||||
|
There is no `query` / `close` / `reindex` command verb. breadmill already
|
||||||
|
has its own query socket; inventing a bus query plane would be a new
|
||||||
|
product surface. If/when that exists, add the corresponding
|
||||||
|
`bread.command.search.*` verb at the same time, not stubbed as a no-op
|
||||||
|
ahead of it.
|
||||||
|
|
||||||
## Fail-safe behavior
|
## Fail-safe behavior
|
||||||
|
|
||||||
- If breadd isn't installed or isn't running, `emit` is a silent no-op
|
- If breadd isn't installed or isn't running, `emit` is a silent no-op
|
||||||
(`BreadClient::emit` never blocks or errors the caller) — breadsearch's
|
(`BreadClient::emit` never blocks or errors the caller) and the
|
||||||
|
command subscription simply never receives anything — breadsearch's
|
||||||
overlay and breadmill's indexing/query path are entirely unaffected.
|
overlay and breadmill's indexing/query path are entirely unaffected.
|
||||||
|
- If breadd restarts, the command subscription reconnects automatically
|
||||||
|
(`BreadClient::subscribe`'s background thread has its own backoff
|
||||||
|
loop); no restart of `breadsearch listen` is needed.
|
||||||
|
- If `breadsearch listen` is not running, commands are a graceful no-op at
|
||||||
|
the bus (no subscriber). The overlay CLI still works.
|
||||||
|
|
|
||||||
84
breadsearch/src/listen.rs
Normal file
84
breadsearch/src/listen.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
//! Long-running command subscription for `bread.command.search.*`.
|
||||||
|
//!
|
||||||
|
//! `breadsearch` is still a one-shot toggle overlay by default.
|
||||||
|
//! `breadsearch listen` is the optional persistent process that can honor
|
||||||
|
//! bus commands. See `EVENTS.md`.
|
||||||
|
|
||||||
|
use bread_utils::bread_client::{BreadClient, BreadEvent};
|
||||||
|
|
||||||
|
use crate::bread_events::APP_ID;
|
||||||
|
|
||||||
|
/// Subscribe to `bread.command.search.**` and block until the process is killed.
|
||||||
|
///
|
||||||
|
/// breadd being absent is not an error: [`BreadClient::subscribe`] reconnects
|
||||||
|
/// with backoff, and `on_event` simply isn't called until the daemon is up.
|
||||||
|
pub fn run() {
|
||||||
|
let client = BreadClient::connect(APP_ID);
|
||||||
|
if client.health().is_none() {
|
||||||
|
eprintln!(
|
||||||
|
"breadsearch: breadd unreachable; command subscription will connect when it comes back"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _commands = client.subscribe("bread.command.search.**", |event| {
|
||||||
|
handle_command(&event);
|
||||||
|
});
|
||||||
|
|
||||||
|
eprintln!("breadsearch: listening for bread.command.search.**");
|
||||||
|
loop {
|
||||||
|
std::thread::park();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reacts to `bread.command.search.*` verbs. Only `open` is honored today —
|
||||||
|
/// other verbs are ignored, not stubbed as no-ops that pretend to succeed.
|
||||||
|
fn handle_command(event: &BreadEvent) {
|
||||||
|
let Some(verb) = command_verb(&event.event) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match verb {
|
||||||
|
"open" => handle_open(),
|
||||||
|
other => {
|
||||||
|
eprintln!("breadsearch: ignoring unrecognized bread.command.search.{other}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_open() {
|
||||||
|
// Same as running `breadsearch` from a keybind: the PID-file toggle
|
||||||
|
// shows the overlay (or dismisses it if it is already up).
|
||||||
|
let result = spawn_self();
|
||||||
|
let client = BreadClient::connect(APP_ID);
|
||||||
|
match result {
|
||||||
|
Ok(_) => client.emit("bread.search.open.done", serde_json::json!({})),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("breadsearch: bread.command.search.open failed: {e}");
|
||||||
|
client.emit(
|
||||||
|
"bread.search.open.failed",
|
||||||
|
serde_json::json!({ "error": e.to_string() }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_self() -> std::io::Result<std::process::Child> {
|
||||||
|
let exe = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("breadsearch"));
|
||||||
|
std::process::Command::new(exe).spawn()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_verb(event_name: &str) -> Option<&str> {
|
||||||
|
event_name.strip_prefix("bread.command.search.")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_verb_strips_search_prefix() {
|
||||||
|
assert_eq!(command_verb("bread.command.search.open"), Some("open"));
|
||||||
|
assert_eq!(command_verb("bread.command.search.query"), Some("query"));
|
||||||
|
assert_eq!(command_verb("bread.command.box.open"), None);
|
||||||
|
assert_eq!(command_verb("bread.search.opened"), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ use gtk4::{
|
||||||
use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
|
use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
|
||||||
|
|
||||||
mod bread_events;
|
mod bread_events;
|
||||||
|
mod listen;
|
||||||
mod screenshot;
|
mod screenshot;
|
||||||
|
|
||||||
// ---- Theming ----------------------------------------------------------------
|
// ---- Theming ----------------------------------------------------------------
|
||||||
|
|
@ -454,6 +455,11 @@ fn run_ui(screenshot_req: Option<screenshot::ScreenshotRequest>) {
|
||||||
// ---- Main -------------------------------------------------------------------
|
// ---- Main -------------------------------------------------------------------
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
if std::env::args().nth(1).as_deref() == Some("listen") {
|
||||||
|
listen::run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
let cli = screenshot::Cli::parse();
|
let cli = screenshot::Cli::parse();
|
||||||
let screenshot_req = cli.screenshot_request();
|
let screenshot_req = cli.screenshot_request();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue