From 5afe12d70f644845b311875995bae7c4d98fd5f7 Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 22 Jul 2026 19:52:56 +0800 Subject: [PATCH] Will change this commit message to mean something later --- bread-utils/src/bread_client.rs | 52 +++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/bread-utils/src/bread_client.rs b/bread-utils/src/bread_client.rs index 3f4adcf..9976fc6 100644 --- a/bread-utils/src/bread_client.rs +++ b/bread-utils/src/bread_client.rs @@ -115,6 +115,50 @@ impl BreadClient { let _ = writeln!(stream, "{line}"); } + /// Send a one-shot IPC request and return its `result`, or `None` on any + /// failure — breadd unreachable, a malformed response, or an `error` + /// field in the response. Mirrors `emit`'s graceful-degradation stance: + /// a caller checks for `None` the same way it'd handle "daemon not + /// installed," not via a `Result` that forces error-path plumbing for + /// what is, for most callers (a refresh-on-connect read), an expected + /// possibility rather than an exceptional one. + /// + /// Unlike `emit`, this is not restricted to the client's own namespace — + /// `method`/`params` map directly onto breadd's IPC method table (see + /// `Documentation.md`'s "Dictionary: IPC protocol"), most of which + /// (`state.get`, `widgets.list`, ...) are cross-namespace reads by + /// design. Only first-party compiled code links `bread-utils`, so this + /// carries the same trust level as `emit`'s own request construction. + pub fn request(&self, method: &str, params: Value) -> Option { + let request = json!({ + "id": "0", + "method": method, + "params": params, + }); + let line = serde_json::to_string(&request).ok()?; + + let mut stream = UnixStream::connect(bread_shared::resolve_socket_path()).ok()?; + stream + .set_write_timeout(Some(Duration::from_millis(200))) + .ok()?; + stream + .set_read_timeout(Some(Duration::from_millis(500))) + .ok()?; + writeln!(stream, "{line}").ok()?; + + let mut response_line = String::new(); + BufReader::new(stream).read_line(&mut response_line).ok()?; + if response_line.trim().is_empty() { + return None; + } + + let value: Value = serde_json::from_str(&response_line).ok()?; + if value.get("error").is_some() { + return None; + } + value.get("result").cloned() + } + /// Subscribe to events matching `pattern` (glob: `*`/`**`/`?`), invoking /// `on_event` for each one on a dedicated background thread. Typically /// called with `"bread.command..**"` to receive commands @@ -290,6 +334,14 @@ mod tests { // integration tests for the IPC-side of namespace validation. } + #[test] + fn request_returns_none_when_daemon_is_unreachable() { + // No daemon present in the test environment; must return None + // promptly rather than blocking or panicking. + let client = BreadClient::connect("clip"); + assert!(client.request("widgets.list", json!(null)).is_none()); + } + #[test] fn subscription_stop_joins_the_background_thread() { let client = BreadClient::connect("clip");