Honor bread.command.paper.set via breadpaper listen
All checks were successful
dev release / build (push) Successful in 21s
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:
parent
54210ddc93
commit
0283ac4e81
6 changed files with 133 additions and 30 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -71,8 +71,8 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "bread-utils"
|
||||
version = "0.3.1"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1#db2fa3c4b4c1e6933bc5cf62a236d05972fdc886"
|
||||
version = "0.7.2"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73"
|
||||
dependencies = [
|
||||
"bread-shared",
|
||||
"dirs",
|
||||
|
|
|
|||
|
|
@ -9,4 +9,4 @@ license = "MIT"
|
|||
clap = { version = "4", features = ["derive"] }
|
||||
anyhow = "1"
|
||||
serde_json = "1"
|
||||
bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1", features = ["bread-client"] }
|
||||
bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["bread-client"] }
|
||||
|
|
|
|||
54
EVENTS.md
54
EVENTS.md
|
|
@ -1,57 +1,67 @@
|
|||
# breadpaper — bread event integration
|
||||
|
||||
breadpaper is a one-shot CLI wallpaper setter: it works exactly the same
|
||||
with or without `breadd` running. When breadd *is* present, each successful
|
||||
`breadpaper set` (or the bare-path shorthand) publishes one event into the
|
||||
shared bread automation fabric. See the parent `bread` repo's
|
||||
breadpaper is a wallpaper setter: it works exactly the same with or
|
||||
without `breadd` running. When breadd *is* present, a successful
|
||||
`breadpaper set` (or the bare-path shorthand) publishes `bread.paper.changed`
|
||||
into the shared bread automation fabric, and `breadpaper listen` honors
|
||||
`bread.command.paper.set`. 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: **`paper`**. Transport: `bread-utils`'s `bread_client` module
|
||||
(feature `bread-client`) — the CLI links it directly and uses
|
||||
`BreadClient::connect("paper")` + `emit` only. v0.7.1 has no `command()`
|
||||
helper, and breadpaper has no long-running process that could hold a
|
||||
`subscribe` open.
|
||||
(feature `bread-client`) — the CLI links it directly. One-shot
|
||||
`set`/`get` use `BreadClient::connect("paper")` + `emit` only. The
|
||||
long-running `listen` subcommand holds a `subscribe` open.
|
||||
|
||||
There is no `breadpaper` daemon and no `watch` subcommand. A `bread-emit
|
||||
bread.command.paper.set` (or any other `bread.command.paper.*`) with no
|
||||
subscriber is a silent no-op — that is the documented bread convention,
|
||||
not a breadpaper bug. Modules that want to change the wallpaper should
|
||||
`breadpaper listen` is fail-silent if breadd is down: `subscribe`
|
||||
reconnects with backoff and simply delivers nothing until the daemon
|
||||
comes back. The one-shot `set`/`get` path does not require `listen`.
|
||||
Modules that want to change the wallpaper without a listener can still
|
||||
shell out:
|
||||
|
||||
```lua
|
||||
bread.exec("breadpaper set /path/to/image.png")
|
||||
```
|
||||
|
||||
The one-shot process still emits `bread.paper.changed` on success, so a
|
||||
workflow can `bread.wait("bread.paper.changed", …)` for the real outcome
|
||||
instead of assuming the exec finished the set.
|
||||
A workflow that publishes the command instead should wait for the
|
||||
confirmation, not assume the emit finished the set:
|
||||
|
||||
```lua
|
||||
bread.emit("bread.command.paper.set", { path = "/path/to/image.png" })
|
||||
bread.wait("bread.paper.set.done", { timeout = 10000 })
|
||||
```
|
||||
|
||||
## Events published (`bread.paper.*`)
|
||||
|
||||
| Event | Data | When |
|
||||
|-------|------|------|
|
||||
| `bread.paper.changed` | `{ "path": "<wallpaper>" }` | After a successful `set` (awww + wal + `bread-theme reload`). `path` is the canonical absolute path that was applied. Not emitted on `get`, and not emitted if any of the three steps fail. |
|
||||
| `bread.paper.changed` | `{ "path": "<wallpaper>" }` | After a successful `set` (awww + wal + `bread-theme reload`), including when `listen` honors `bread.command.paper.set`. `path` is the canonical absolute path that was applied. Not emitted on `get`, and not emitted if any of the three steps fail. |
|
||||
| `bread.paper.set.done` | `{ "path": "<wallpaper>" }` | `bread.command.paper.set` was received and `set()` succeeded. `path` is the canonical absolute path that was applied. Not emitted by the one-shot CLI `set` — that path only publishes `changed`. |
|
||||
| `bread.paper.set.failed` | `{ "error": "<message>", "path"?: "<requested>" }` | `bread.command.paper.set` was received but `set()` failed, or `data.path` was missing/not a string. `path` is the requested (not canonical) path when one was supplied. |
|
||||
|
||||
## Commands honored (`bread.command.paper.*`)
|
||||
|
||||
None, because there is nobody listening.
|
||||
Honored only while `breadpaper listen` is running. A
|
||||
`bread-emit bread.command.paper.set` with no listener is a silent no-op
|
||||
— that is the documented bread convention, not a breadpaper bug.
|
||||
|
||||
| Verb | Data | Status |
|
||||
| Verb | Data | Effect |
|
||||
|------|------|--------|
|
||||
| `set` | `{ "path": "..." }` | **Not subscribed.** The same work is `bread.exec("breadpaper set …")`. A future `breadpaper watch` (or a service-mode of this binary) could honor `bread.command.paper.set` and emit `bread.paper.set.done` / `.failed`; that is deliberately not added here — a long-running process whose only job is to re-exec the existing one-shot CLI is not worth the extra surface. |
|
||||
| `set` | `{ "path": "..." }` | Calls the existing `set()` (awww + wal + `bread-theme reload`). Emits `bread.paper.set.done` / `.failed`. A successful set also emits `bread.paper.changed`. |
|
||||
|
||||
### Not implemented: slideshow / library / random / next
|
||||
|
||||
breadpaper is not a wallpaper library, a slideshow daemon, or a picker.
|
||||
Browsing `~/Pictures/Backgrounds` lives in bos-settings. Do not invent
|
||||
`bread.command.paper.next` / `.random` / `.cycle` (or matching events)
|
||||
ahead of a real product feature.
|
||||
ahead of a real product feature. Unrecognized verbs are ignored.
|
||||
|
||||
## 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) — breadpaper
|
||||
still sets the wallpaper, generates the palette, and reloads themes.
|
||||
- There is no command subscription to reconnect, because there is no
|
||||
long-running subscriber.
|
||||
- `breadpaper listen` does not exit if breadd is down. The command
|
||||
subscription reconnects automatically (`BreadClient::subscribe`'s
|
||||
background thread has its own backoff loop); no restart of `listen`
|
||||
is needed once breadd returns.
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ install -Dm755 target/release/breadpaper ~/.local/bin/breadpaper
|
|||
breadpaper <path> # shorthand for `set`
|
||||
breadpaper set <path> # awww + wal + bread-theme reload
|
||||
breadpaper get # print the current wallpaper path
|
||||
breadpaper listen # honor bread.command.paper.set (fail-silent if breadd is down)
|
||||
```
|
||||
|
||||
Supported formats: `png`, `jpg`, `jpeg`, `webp`, `gif`, `bmp`.
|
||||
|
|
@ -45,9 +46,10 @@ Supported formats: `png`, `jpg`, `jpeg`, `webp`, `gif`, `bmp`.
|
|||
## Bread events
|
||||
|
||||
After a successful `set`, breadpaper emits `bread.paper.changed` if
|
||||
`breadd` is running (silent no-op if it isn't). There is no daemon and
|
||||
no command subscription — Lua modules should `bread.exec("breadpaper set …")`.
|
||||
See [EVENTS.md](EVENTS.md).
|
||||
`breadd` is running (silent no-op if it isn't). `breadpaper listen` is
|
||||
the optional long-running subscriber for `bread.command.paper.set`; it
|
||||
does not start by itself. Lua modules can still
|
||||
`bread.exec("breadpaper set …")`. See [EVENTS.md](EVENTS.md).
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
92
src/lib.rs
92
src/lib.rs
|
|
@ -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 }));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue