Honor bread.command.paper.set via breadpaper listen
All checks were successful
dev release / build (push) Successful in 21s

Pin bread-utils to v0.7.2 (command/subscribe). Add a long-running
listen subcommand that subscribes to bread.command.paper.**, calls
set() on set, and emits bread.paper.set.done / .failed. Fail-silent
if breadd is down. No slideshow verbs. See EVENTS.md.
This commit is contained in:
Breadway 2026-08-15 22:47:12 +08:00
parent 54210ddc93
commit 0283ac4e81
6 changed files with 133 additions and 30 deletions

View file

@ -3,9 +3,11 @@ mod theme;
mod wallpaper;
use std::path::{Path, PathBuf};
use std::thread;
use anyhow::{bail, Context, Result};
use bread_utils::bread_client::BreadClient;
use bread_utils::bread_client::{BreadClient, BreadEvent};
use serde_json::{json, Value};
/// App id in bread's sibling-app registry (`KNOWN_APPS`). Events publish as
/// `bread.paper.*`. See `EVENTS.md`.
@ -22,15 +24,65 @@ pub fn set(path: &Path) -> Result<()> {
Ok(())
}
/// Honor `bread.command.paper.*` until killed. Subscribe reconnects with
/// backoff if breadd is down or restarts — this never errors the caller.
pub fn listen() -> Result<()> {
let client = BreadClient::connect(APP_ID);
let _subscription = client.subscribe("bread.command.paper.**", handle_command);
loop {
thread::park();
}
}
/// Fire-and-forget `bread.paper.changed`. Silent no-op if breadd is down
/// (`BreadClient::emit` never blocks or errors the caller).
fn emit_changed(path: &Path) {
BreadClient::connect(APP_ID).emit(
"bread.paper.changed",
serde_json::json!({ "path": path.to_string_lossy() }),
json!({ "path": path.to_string_lossy() }),
);
}
fn handle_command(event: BreadEvent) {
let Some(verb) = event.event.strip_prefix("bread.command.paper.") else {
return;
};
match verb {
"set" => handle_set(&event.data),
other => {
eprintln!("breadpaper: ignoring unrecognized command verb '{other}'");
}
}
}
fn handle_set(data: &Value) {
let client = BreadClient::connect(APP_ID);
let Some(path_str) = data.get("path").and_then(Value::as_str) else {
client.emit(
"bread.paper.set.failed",
json!({ "error": "missing string \"path\"" }),
);
return;
};
let path = Path::new(path_str);
match set(path) {
Ok(()) => {
let applied = path
.canonicalize()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|_| path_str.to_string());
client.emit("bread.paper.set.done", json!({ "path": applied }));
}
Err(e) => {
eprintln!("breadpaper: bread.command.paper.set failed: {e:#}");
client.emit(
"bread.paper.set.failed",
json!({ "error": format!("{e:#}"), "path": path_str }),
);
}
}
}
pub fn get() -> Result<PathBuf> {
let home = std::env::var("HOME").context("HOME not set")?;
let wal_file = PathBuf::from(home).join(".cache/wal/wal");
@ -85,4 +137,40 @@ mod tests {
// socket is missing — this is the fail-silent contract.
emit_changed(Path::new("/tmp/wallpaper.png"));
}
#[test]
fn subscribe_is_silent_without_breadd() {
let client = BreadClient::connect(APP_ID);
let sub = client.subscribe("bread.command.paper.**", |_| {});
drop(sub);
}
#[test]
fn handle_command_ignores_unrecognized_verb() {
handle_command(BreadEvent {
event: "bread.command.paper.next".into(),
timestamp: 0,
data: json!({}),
});
}
#[test]
fn handle_command_ignores_events_outside_its_own_command_namespace() {
handle_command(BreadEvent {
event: "bread.command.clip.clear".into(),
timestamp: 0,
data: json!({}),
});
handle_command(BreadEvent {
event: "bread.paper.changed".into(),
timestamp: 0,
data: json!({ "path": "/tmp/wallpaper.png" }),
});
}
#[test]
fn handle_set_missing_path_is_silent_without_breadd() {
handle_set(&json!({}));
handle_set(&json!({ "path": 1 }));
}
}

View file

@ -21,6 +21,8 @@ enum Command {
},
/// Print the current wallpaper path
Get,
/// Honor bread.command.paper.set until killed
Listen,
}
fn main() {
@ -28,6 +30,7 @@ fn main() {
let result = match (cli.command, cli.path) {
(Some(Command::Set { path }), _) | (None, Some(path)) => breadpaper::set(&path),
(Some(Command::Listen), _) => breadpaper::listen(),
(Some(Command::Get), _) | (None, None) => {
breadpaper::get().map(|p| println!("{}", p.display()))
}