diff --git a/Cargo.lock b/Cargo.lock index cf5aeb6..5052a56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,8 +83,8 @@ dependencies = [ [[package]] name = "bread-theme" -version = "0.7.4" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.4#fcba3760387e2523edb71350f8efea3bc851b21e" +version = "0.7.2" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73" dependencies = [ "dirs", "gtk4", @@ -105,7 +105,7 @@ dependencies = [ [[package]] name = "breadpaper" -version = "0.1.14" +version = "0.1.12" dependencies = [ "anyhow", "bread-theme", diff --git a/Cargo.toml b/Cargo.toml index b2bea93..f89a4c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "breadpaper" -version = "0.1.14" +version = "0.1.12" edition = "2024" description = "Wallpaper manager for the bread desktop" license = "MIT" @@ -12,5 +12,5 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" toml = "0.8" gtk4 = { version = "0.11", features = ["v4_12"] } -bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.4", features = ["gtk"] } +bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["gtk"] } bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["bread-client"] } diff --git a/EVENTS.md b/EVENTS.md index 37260ba..6922dd6 100644 --- a/EVENTS.md +++ b/EVENTS.md @@ -15,10 +15,9 @@ long-running `listen` subcommand holds a `subscribe` open. `breadpaper listen` is fail-silent if breadd is down: `subscribe` reconnects with backoff and simply delivers nothing until the daemon -comes back. It also honors `bread.monitor.connected` by re-applying -`~/.config/breadpaper/current.json`. The one-shot `set`/`get`/`library` -/`apply` path does not require `listen`. Modules that want to change -the wallpaper without a listener can still shell out: +comes back. The one-shot `set`/`get`/`library` 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") @@ -36,9 +35,9 @@ bread.wait("bread.paper.set.done", { timeout = 10000 }) | Event | Data | When | |-------|------|------| -| `bread.paper.changed` | `{ "path": "", "output"?: "" }` | After a successful `set` / `set --output` (awww + palette + theme write), including when `listen` honors `bread.command.paper.set` or restores `current.json` after `bread.monitor.connected`. `path` is the canonical absolute path that was applied. `output` is the compositor output name when only one monitor was targeted; omit (or `null`) means every output. Not emitted on `get`, and not emitted if the set fails. | -| `bread.paper.set.done` | `{ "path": "", "output"?: "" }` | `bread.command.paper.set` was received and `set()` / `set_on()` succeeded. `path` is the canonical absolute path that was applied. `output` is present when the command targeted one monitor. Not emitted by the one-shot CLI `set` — that path only publishes `changed`. | -| `bread.paper.set.failed` | `{ "error": "", "path"?: "", "output"?: "" }` | `bread.command.paper.set` was received but `set()` / `set_on()` failed, or `data.path` was missing/not a string. `path` is the requested (not canonical) path when one was supplied. `output` is echoed when the command included one. | +| `bread.paper.changed` | `{ "path": "" }` | 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": "" }` | `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": "", "path"?: "" }` | `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. | | `bread.paper.library.done` | `{}` | `bread.command.paper.library` was received and a `breadpaper library` process was started. Not emitted by the one-shot CLI `library` / `browse`. | | `bread.paper.library.failed` | `{ "error": "" }` | `bread.command.paper.library` was received but the library process could not be spawned. | @@ -50,8 +49,8 @@ Honored only while `breadpaper listen` is running. A | Verb | Data | Effect | |------|------|--------| -| `set` | `{ "path": "...", "output"?: "..." }` | Missing `output` calls `set()` (all live outputs, global `wal -i`, `bread-theme reload`). A string `output` calls `set_on()` (that monitor only; no global wal unless it is the focused Hyprland monitor). Emits `bread.paper.set.done` / `.failed`. A successful set also emits `bread.paper.changed`. | -| `library` | `{}` | Spawns `breadpaper library` (GTK picker). Emits `bread.paper.library.done` once the process is started, or `bread.paper.library.failed` if the spawn fails. Clicking a thumbnail in that window applies to the picker's output and publishes `bread.paper.changed`. | +| `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`. | +| `library` | `{}` | Spawns `breadpaper library` (GTK picker). Emits `bread.paper.library.done` once the process is started, or `bread.paper.library.failed` if the spawn fails. Clicking a thumbnail in that window is a normal `set` and publishes `bread.paper.changed`. | ### Not implemented: slideshow / random / next diff --git a/README.md b/README.md index f49b156..7064d1d 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,11 @@ Wallpaper setter for the bread desktop. One command sets the wallpaper via [awww](https://github.com/heywoodlh/awww), generates a palette with [pywal](https://github.com/dylanaraps/pywal) (`wal`), and runs -`bread-theme reload`. Two monitors can keep different wallpapers (and -per-output bread-theme files); the last path per output is stored in -`~/.config/breadpaper/current.json`. +`bread-theme reload`. -`set` / `get` / `apply` stay one-shot CLI. `breadpaper library` (alias -`browse`) opens a GTK picker over the wallpaper directories. It is not -a slideshow daemon. +`set` / `get` stay one-shot CLI. `breadpaper library` (alias `browse`) +opens a GTK picker over the wallpaper directories. It is not a slideshow +daemon. ## Dependencies @@ -20,9 +18,7 @@ Must be on `$PATH` for `set`: - `bread-theme` — theme reload (bakery package, not `breadd`) `library` also needs GTK4 (the window loads `bread-theme`'s shared -stylesheet and follows the monitor it sits on). `get` without -`--output` still reads the path pywal stored at `~/.cache/wal/wal`. -`get --output NAME` reads `~/.config/breadpaper/current.json`. +stylesheet). `get` only reads the path pywal stored at `~/.cache/wal/wal`. ## Install @@ -40,16 +36,12 @@ install -Dm755 target/release/breadpaper ~/.local/bin/breadpaper ## Usage ``` -breadpaper # shorthand for `set` (all outputs) -breadpaper --output NAME # one output -breadpaper set # awww + wal + bread-theme reload (all) -breadpaper set --output NAME -breadpaper get # print ~/.cache/wal/wal -breadpaper get --output NAME # path from current.json -breadpaper apply # restore current.json -breadpaper library # GTK picker (alias: browse) -breadpaper library --dir PATH # also scan PATH (repeatable) -breadpaper listen # honor bread.command.paper.set / .library +breadpaper # shorthand for `set` +breadpaper set # awww + wal + bread-theme reload +breadpaper get # print the current wallpaper path +breadpaper library # GTK picker (alias: browse) +breadpaper library --dir PATH # also scan PATH (repeatable) +breadpaper listen # honor bread.command.paper.set / .library ``` Supported formats: `png`, `jpg`, `jpeg`, `webp`, `gif`, `bmp`. @@ -71,9 +63,8 @@ library_dirs = [ ``` `BREADPAPER_LIBRARY_DIRS` (colon-separated) overrides the file. `--dir` -appends extra roots for that invocation. Clicking a thumbnail applies -to the monitor the picker is on (`set --output`); if the output cannot -be resolved it falls back to all outputs. +appends extra roots for that invocation. Clicking a thumbnail runs the +same `set` path as the CLI. ## Bread events diff --git a/src/current.rs b/src/current.rs deleted file mode 100644 index f39d680..0000000 --- a/src/current.rs +++ /dev/null @@ -1,126 +0,0 @@ -use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; - -use anyhow::{Context, Result}; -use serde::{Deserialize, Serialize}; - -/// Persisted wallpaper path per output (`~/.config/breadpaper/current.json`). -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct Current { - #[serde(default)] - outputs: BTreeMap, -} - -impl Current { - pub fn path() -> PathBuf { - bread_utils::xdg::config_dir("breadpaper").join("current.json") - } - - pub fn load() -> Self { - Self::load_from(&Self::path()) - } - - /// Missing or unreadable file => empty map. - pub fn load_from(path: &Path) -> Self { - let Ok(text) = std::fs::read_to_string(path) else { - return Self::default(); - }; - serde_json::from_str(&text).unwrap_or_default() - } - - pub fn save(&self) -> Result<()> { - self.save_to(&Self::path()) - } - - pub fn save_to(&self, path: &Path) -> Result<()> { - let text = serde_json::to_string_pretty(self).context("serialize current.json")?; - let text = format!("{text}\n"); - bread_utils::atomic::write_atomic(path, &text, None) - .with_context(|| format!("write {}", path.display())) - } - - pub fn set_output(&mut self, output: impl Into, path: impl Into) { - self.outputs.insert(output.into(), path.into()); - } - - pub fn get_output(&self, output: &str) -> Option<&Path> { - self.outputs.get(output).map(PathBuf::as_path) - } - - pub fn all(&self) -> &BTreeMap { - &self.outputs - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn tmp_dir(name: &str) -> PathBuf { - let dir = std::env::temp_dir().join(format!( - "breadpaper-current-{name}-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos()) - .unwrap_or(0) - )); - std::fs::create_dir_all(&dir).unwrap(); - dir - } - - #[test] - fn missing_file_is_empty_map() { - let dir = tmp_dir("missing"); - let path = dir.join("current.json"); - let cur = Current::load_from(&path); - assert!(cur.all().is_empty()); - let _ = std::fs::remove_dir_all(&dir); - } - - #[test] - fn roundtrip_pretty_json() { - let dir = tmp_dir("roundtrip"); - let path = dir.join("nested").join("current.json"); - let mut cur = Current::default(); - cur.set_output("eDP-1", "/abs/path/a.png"); - cur.set_output("HDMI-A-1", "/abs/path/b.png"); - cur.save_to(&path).unwrap(); - - let text = std::fs::read_to_string(&path).unwrap(); - assert!(text.contains("\n \"outputs\"")); - assert!(text.contains("\n \"HDMI-A-1\"")); - assert!(text.contains("\n \"eDP-1\"")); - - let loaded = Current::load_from(&path); - assert_eq!( - loaded.get_output("eDP-1"), - Some(Path::new("/abs/path/a.png")) - ); - assert_eq!( - loaded.get_output("HDMI-A-1"), - Some(Path::new("/abs/path/b.png")) - ); - assert_eq!(loaded.all().len(), 2); - let _ = std::fs::remove_dir_all(&dir); - } - - #[test] - fn set_one_output_does_not_drop_others() { - let dir = tmp_dir("keep"); - let path = dir.join("current.json"); - let mut cur = Current::default(); - cur.set_output("eDP-1", "/abs/a.png"); - cur.set_output("HDMI-A-1", "/abs/b.png"); - cur.save_to(&path).unwrap(); - - let mut cur = Current::load_from(&path); - cur.set_output("eDP-1", "/abs/c.png"); - cur.save_to(&path).unwrap(); - - let loaded = Current::load_from(&path); - assert_eq!(loaded.get_output("eDP-1"), Some(Path::new("/abs/c.png"))); - assert_eq!(loaded.get_output("HDMI-A-1"), Some(Path::new("/abs/b.png"))); - let _ = std::fs::remove_dir_all(&dir); - } -} diff --git a/src/lib.rs b/src/lib.rs index 60d1c9b..26be507 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ mod config; -mod current; mod library; mod pywal; mod theme; @@ -31,101 +30,12 @@ pub fn library(extra_dirs: impl IntoIterator) -> Result<()> { ui::run(cfg.library_dirs) } -/// Set wallpaper + global pywal palette on every live output. pub fn set(path: &Path) -> Result<()> { let path = validate(path)?; apply_wallpaper(&path)?; generate_palette(&path)?; reload_theme()?; - - let mut cur = current::Current::load(); - if cur.all().is_empty() { - let live = live_outputs(); - if live.is_empty() { - cur.set_output("*", path.clone()); - } else { - for output in live { - cur.set_output(output, path.clone()); - } - } - } else { - let keys: Vec = cur.all().keys().cloned().collect(); - for output in keys { - cur.set_output(output, path.clone()); - } - } - cur.save()?; - - for output in cur.all().keys() { - if output != "*" { - theme::generate_for_output(output, &path)?; - } - } - - emit_changed(&path, None); - Ok(()) -} - -/// Set wallpaper + per-output theme on a single compositor output. -/// -/// Does not run global `wal -i`. If `output` is the focused Hyprland -/// monitor, the shared stylesheet is updated from that output's palette -/// so unbound apps match the focused screen. -pub fn set_on(path: &Path, output: &str) -> Result<()> { - if output.is_empty() { - bail!("output name is empty"); - } - let path = validate(path)?; - wallpaper::apply_on(&path, output)?; - let palette = theme::generate_for_output(output, &path)?; - - let mut cur = current::Current::load(); - cur.set_output(output, path.clone()); - cur.save()?; - - if is_focused_output(output) { - theme::write_shared_from(&palette)?; - } - - emit_changed(&path, Some(output)); - Ok(()) -} - -/// Re-apply every wallpaper + per-output theme stored in current.json. -pub fn apply_saved() -> Result<()> { - let cur = current::Current::load(); - let mut first_err = None; - for (output, path) in cur.all() { - let result = restore_one(output, path); - match result { - Ok(()) => { - let name = (output != "*").then_some(output.as_str()); - emit_changed(path, name); - } - Err(e) => { - eprintln!("breadpaper: apply {output}: {e:#}"); - if first_err.is_none() { - first_err = Some(e); - } - } - } - } - match first_err { - Some(e) => Err(e), - None => Ok(()), - } -} - -fn restore_one(output: &str, path: &Path) -> Result<()> { - if output == "*" { - wallpaper::apply(path)?; - return Ok(()); - } - wallpaper::apply_on(path, output)?; - let palette = theme::generate_for_output(output, path)?; - if is_focused_output(output) { - theme::write_shared_from(&palette)?; - } + emit_changed(&path); Ok(()) } @@ -134,11 +44,6 @@ fn restore_one(output: &str, path: &Path) -> Result<()> { pub fn listen() -> Result<()> { let client = BreadClient::connect(APP_ID); let _subscription = client.subscribe("bread.command.paper.**", handle_command); - let _monitors = client.subscribe("bread.monitor.connected", |_| { - if let Err(e) = apply_saved() { - eprintln!("breadpaper: apply_saved on monitor connect failed: {e:#}"); - } - }); loop { thread::park(); } @@ -146,14 +51,11 @@ pub fn listen() -> Result<()> { /// Fire-and-forget `bread.paper.changed`. Silent no-op if breadd is down /// (`BreadClient::emit` never blocks or errors the caller). -/// -/// `output` is `None` when the wallpaper was applied to every output. -fn emit_changed(path: &Path, output: Option<&str>) { - let mut data = json!({ "path": path.to_string_lossy() }); - if let Some(name) = output { - data["output"] = json!(name); - } - BreadClient::connect(APP_ID).emit("bread.paper.changed", data); +fn emit_changed(path: &Path) { + BreadClient::connect(APP_ID).emit( + "bread.paper.changed", + json!({ "path": path.to_string_lossy() }), + ); } fn handle_command(event: BreadEvent) { @@ -178,31 +80,21 @@ fn handle_set(data: &Value) { ); return; }; - let output = data.get("output").and_then(Value::as_str); let path = Path::new(path_str); - let result = match output { - Some(name) => set_on(path, name), - None => set(path), - }; - match result { + match set(path) { Ok(()) => { let applied = path .canonicalize() .map(|p| p.to_string_lossy().into_owned()) .unwrap_or_else(|_| path_str.to_string()); - let mut payload = json!({ "path": applied }); - if let Some(name) = output { - payload["output"] = json!(name); - } - client.emit("bread.paper.set.done", payload); + client.emit("bread.paper.set.done", json!({ "path": applied })); } Err(e) => { eprintln!("breadpaper: bread.command.paper.set failed: {e:#}"); - let mut payload = json!({ "error": format!("{e:#}"), "path": path_str }); - if let Some(name) = output { - payload["output"] = json!(name); - } - client.emit("bread.paper.set.failed", payload); + client.emit( + "bread.paper.set.failed", + json!({ "error": format!("{e:#}"), "path": path_str }), + ); } } } @@ -257,14 +149,6 @@ pub fn get() -> Result { Ok(PathBuf::from(contents.trim())) } -/// Wallpaper path last persisted for `output` in current.json. -pub fn get_on(output: &str) -> Result { - current::Current::load() - .get_output(output) - .map(Path::to_path_buf) - .with_context(|| format!("no wallpaper saved for output {output}")) -} - pub fn apply_wallpaper(path: &Path) -> Result<()> { wallpaper::apply(path) } @@ -299,52 +183,15 @@ fn validate(path: &Path) -> Result { Ok(canonical) } -fn live_outputs() -> Vec { - if let Some(names) = hypr_output_names() { - return names; - } - wallpaper::query_outputs() -} - -fn hypr_output_names() -> Option> { - let v = bread_utils::hypr::request_json("j/monitors")?; - let names: Vec = v - .as_array()? - .iter() - .filter_map(|m| m.get("name").and_then(|n| n.as_str()).map(str::to_string)) - .collect(); - if names.is_empty() { None } else { Some(names) } -} - -fn is_focused_output(output: &str) -> bool { - bread_utils::hypr::focused_monitor() - .map(|m| m.name == output) - .unwrap_or(false) -} - #[cfg(test)] mod tests { use super::*; - fn tmp_dir(name: &str) -> PathBuf { - let dir = std::env::temp_dir().join(format!( - "breadpaper-lib-{name}-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos()) - .unwrap_or(0) - )); - std::fs::create_dir_all(&dir).unwrap(); - dir - } - #[test] fn emit_changed_is_silent_without_breadd() { // BreadClient::emit must never panic or error just because the // socket is missing — this is the fail-silent contract. - emit_changed(Path::new("/tmp/wallpaper.png"), None); - emit_changed(Path::new("/tmp/wallpaper.png"), Some("eDP-1")); + emit_changed(Path::new("/tmp/wallpaper.png")); } #[test] @@ -383,16 +230,6 @@ mod tests { handle_set(&json!({ "path": 1 })); } - #[test] - fn handle_set_with_output_vs_without_is_silent_without_breadd() { - // Missing files fail in validate — never reaches awww/wal. - handle_set(&json!({ "path": "/no/such/breadpaper-wallpaper.png" })); - handle_set(&json!({ - "path": "/no/such/breadpaper-wallpaper.png", - "output": "eDP-1" - })); - } - #[test] fn handle_command_library_is_silent_without_breadd() { handle_command(BreadEvent { @@ -401,29 +238,4 @@ mod tests { data: json!({}), }); } - - #[test] - fn validate_rejects_bad_extensions() { - let dir = tmp_dir("validate"); - let txt = dir.join("notes.txt"); - std::fs::write(&txt, b"x").unwrap(); - assert!(validate(&txt).is_err()); - - let png = dir.join("ok.png"); - std::fs::write(&png, b"x").unwrap(); - assert_eq!(validate(&png).unwrap(), png.canonicalize().unwrap()); - let _ = std::fs::remove_dir_all(&dir); - } - - #[test] - fn handle_set_bad_extension_with_output_is_silent_without_breadd() { - let dir = tmp_dir("bad-ext"); - let txt = dir.join("notes.txt"); - std::fs::write(&txt, b"x").unwrap(); - handle_set(&json!({ - "path": txt.to_string_lossy(), - "output": "HDMI-A-1" - })); - let _ = std::fs::remove_dir_all(&dir); - } } diff --git a/src/main.rs b/src/main.rs index 899386f..8069a89 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,10 +13,6 @@ struct Cli { /// Image file to set as wallpaper (shorthand for `set`) path: Option, - /// Restrict set/get to one compositor output - #[arg(long, value_name = "NAME", global = true)] - output: Option, - #[command(subcommand)] command: Option, } @@ -27,8 +23,6 @@ enum Command { Set { path: PathBuf }, /// Print the current wallpaper path Get, - /// Re-apply wallpapers and per-output themes from current.json - Apply, /// Honor bread.command.paper.set / .library until killed Listen, /// Open the wallpaper library (alias: browse) @@ -44,17 +38,12 @@ fn main() { let cli = Cli::parse(); let result = match (cli.command, cli.path) { - (Some(Command::Set { path }), _) | (None, Some(path)) => match cli.output.as_deref() { - Some(output) => breadpaper::set_on(&path, output), - None => breadpaper::set(&path), - }, + (Some(Command::Set { path }), _) | (None, Some(path)) => breadpaper::set(&path), (Some(Command::Listen), _) => breadpaper::listen(), (Some(Command::Library { dirs }), _) => breadpaper::library(dirs), - (Some(Command::Apply), _) => breadpaper::apply_saved(), - (Some(Command::Get), _) | (None, None) => match cli.output.as_deref() { - Some(output) => breadpaper::get_on(output).map(|p| println!("{}", p.display())), - None => breadpaper::get().map(|p| println!("{}", p.display())), - }, + (Some(Command::Get), _) | (None, None) => { + breadpaper::get().map(|p| println!("{}", p.display())) + } }; if let Err(e) = result { diff --git a/src/theme.rs b/src/theme.rs index a104e17..5d06076 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,8 +1,6 @@ -use std::path::Path; use std::process::Command; use anyhow::{Context, Result, bail}; -use bread_theme::Palette; pub fn reload() -> Result<()> { let status = Command::new("bread-theme") @@ -15,16 +13,3 @@ pub fn reload() -> Result<()> { } Ok(()) } - -/// Per-output palette + bread-theme files. Does not run `wal -i`. -pub fn generate_for_output(output: &str, path: &Path) -> Result { - bread_theme::generate_output(output, path) - .with_context(|| format!("bread-theme generate_output({output}, {})", path.display()))?; - Ok(bread_theme::load_palette_for(output)) -} - -pub fn write_shared_from(palette: &Palette) -> Result<()> { - bread_theme::write_shared_css_from(palette) - .context("bread-theme write_shared_css_from") - .map(|_| ()) -} diff --git a/src/ui.rs b/src/ui.rs index 01e5365..5193541 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -118,7 +118,6 @@ fn present(app: &Application, dirs: Vec) { root.append(&stack); window.set_child(Some(&root)); - bread_theme::gtk::bind_window_auto(&window); let dirs = Rc::new(dirs); let reload = { @@ -128,7 +127,6 @@ fn present(app: &Application, dirs: Vec) { let status = status.clone(); let stack = stack.clone(); let empty = empty.clone(); - let window = window.clone(); Rc::new(move || { let papers = library::scan(&dirs); summary.set_text(&dirs_summary(&dirs, papers.len())); @@ -138,44 +136,30 @@ fn present(app: &Application, dirs: Vec) { } else { stack.set_visible_child_name("grid"); } - fill_grid(&flow, &papers, &status, &window); + fill_grid(&flow, &papers, &status); }) }; - window.present(); reload(); { let reload = reload.clone(); refresh.connect_clicked(move |_| reload()); } + + window.present(); } -fn fill_grid(flow: &FlowBox, papers: &[Wallpaper], status: &Label, host: &impl IsA) { +fn fill_grid(flow: &FlowBox, papers: &[Wallpaper], status: &Label) { while let Some(child) = flow.first_child() { flow.remove(&child); } - let current = current_path_for(host); + let current = crate::get().ok(); for paper in papers { let is_current = current.as_deref() == Some(paper.path.as_path()); flow.insert(&tile(paper, is_current, flow, status), -1); } } -fn current_path_for(widget: &impl IsA) -> Option { - target_output(widget) - .and_then(|output| { - crate::current::Current::load() - .get_output(&output) - .map(Path::to_path_buf) - }) - .or_else(|| crate::get().ok()) -} - -fn target_output(widget: &impl IsA) -> Option { - bread_theme::gtk::output_for_widget(widget) - .or_else(|| bread_utils::hypr::focused_monitor().map(|m| m.name)) -} - fn tile(paper: &Wallpaper, is_current: bool, flow: &FlowBox, status: &Label) -> Button { let btn = Button::new(); btn.add_css_class("wallpaper-tile"); @@ -205,11 +189,7 @@ fn tile(paper: &Wallpaper, is_current: bool, flow: &FlowBox, status: &Label) -> return; } clicked.set_sensitive(false); - let output = target_output(clicked); - status.set_text(&match output.as_deref() { - Some(name) => format!("Applying {pretty} on {name}…"), - None => format!("Applying {pretty}…"), - }); + status.set_text(&format!("Applying {pretty}…")); let path = path.clone(); let pretty = pretty.clone(); let status = status.clone(); @@ -217,19 +197,11 @@ fn tile(paper: &Wallpaper, is_current: bool, flow: &FlowBox, status: &Label) -> let clicked = clicked.clone(); gtk4::glib::spawn_future_local(async move { let path_thread = path.clone(); - let output_thread = output.clone(); - let result = gtk4::gio::spawn_blocking(move || match output_thread.as_deref() { - Some(name) => crate::set_on(&path_thread, name), - None => crate::set(&path_thread), - }) - .await; + let result = gtk4::gio::spawn_blocking(move || crate::set(&path_thread)).await; clicked.set_sensitive(true); match result { Ok(Ok(())) => { - status.set_text(&match output.as_deref() { - Some(name) => format!("Applied {pretty} on {name}"), - None => format!("Applied {pretty}"), - }); + status.set_text(&format!("Applied {pretty}")); mark_current(&flow, &path); } Ok(Err(e)) => status.set_text(&format!("{e:#}")), diff --git a/src/wallpaper.rs b/src/wallpaper.rs index fcf5d4b..f59fae1 100644 --- a/src/wallpaper.rs +++ b/src/wallpaper.rs @@ -4,73 +4,14 @@ use std::process::Command; use anyhow::{Context, Result, bail}; pub fn apply(path: &Path) -> Result<()> { - run_awww(Command::new("awww").arg("img").arg(path)) -} - -pub fn apply_on(path: &Path, output: &str) -> Result<()> { - run_awww( - Command::new("awww") - .arg("img") - .arg(path) - .arg("--outputs") - .arg(output), - ) -} - -/// Output names from `awww query`. Empty if the daemon isn't running. -pub fn query_outputs() -> Vec { - let Ok(out) = Command::new("awww").arg("query").output() else { - return Vec::new(); - }; - if !out.status.success() { - return Vec::new(); - } - parse_awww_query(&String::from_utf8_lossy(&out.stdout)) -} - -fn run_awww(cmd: &mut Command) -> Result<()> { - let status = cmd + let status = Command::new("awww") + .arg("img") + .arg(path) .status() .context("failed to run awww — is awww-daemon running?")?; + if !status.success() { bail!("awww img exited with {}", status); } Ok(()) } - -fn parse_awww_query(stdout: &str) -> Vec { - stdout.lines().filter_map(parse_awww_query_line).collect() -} - -/// `awww query` lines look like `: eDP-1: 1920x1080, scale: 1, ...`. -fn parse_awww_query_line(line: &str) -> Option { - let rest = line.trim().strip_prefix(':').unwrap_or(line).trim(); - let name = rest.split(':').next()?.trim(); - if name.is_empty() { - None - } else { - Some(name.to_string()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_awww_query_names() { - let sample = "\ -: eDP-1: 1920x1080, scale: 1, currently displaying: image: /a.png -: HDMI-A-1: 2560x1440, scale: 1, currently displaying: image: /b.png -"; - assert_eq!( - parse_awww_query(sample), - vec!["eDP-1".to_string(), "HDMI-A-1".to_string()] - ); - } - - #[test] - fn parse_awww_query_skips_blank() { - assert!(parse_awww_query("\n \n").is_empty()); - } -}