diff --git a/Cargo.lock b/Cargo.lock index 7c5bc1d..3c878cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,42 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + [[package]] name = "bitflags" version = "2.6.0" @@ -20,6 +50,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + [[package]] name = "cairo-rs" version = "0.18.5" @@ -360,6 +396,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "gio" version = "0.18.4" @@ -680,6 +722,7 @@ dependencies = [ "rand", "rayon", "shellexpand", + "tokio", ] [[package]] @@ -739,6 +782,27 @@ dependencies = [ "autocfg", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -749,6 +813,15 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.20.2" @@ -990,6 +1063,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1049,6 +1128,15 @@ dependencies = [ "dirs", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.9" @@ -1064,6 +1152,16 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "syn" version = "1.0.109" @@ -1137,6 +1235,35 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "toml" version = "0.8.19" diff --git a/Cargo.toml b/Cargo.toml index 53fba79..9f188d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ rayon = "1.7" num_cpus = "1.15" rand = "0.8" crossbeam-channel = "0.5" +tokio = { version = "1.28", features = ["full"] } [profile.release] lto = "fat" diff --git a/readme.md b/readme.md index f7e43d3..0db7fda 100644 --- a/readme.md +++ b/readme.md @@ -15,6 +15,7 @@ An unofficial GUI for setting wallpapers with multiple backends, built with GTK4 - **Performance** - Hyprwall is designed to be performant, it uses a thread pool to load images in parallel and caches images. - **High capacity** - Hyprwall can handle a large number of wallpapers (over 1000 at one time!) without any issues. - **Multiple monitors** - Hyprwall supports setting wallpapers on **Multiple** monitors at once. +- **True async** - Hyprwall is built to be asynchronous, it uses tokio to run commands in this manner massively improving performance. - **Supports swaybg, swww, wallutils, feh, and hyprpaper** - Hyprwall supports a variety of wallpaper backends, so you can use it with your preferred wallpaper tool. ## Requirements diff --git a/src/gui.rs b/src/gui.rs index 22ea3d2..cba3475 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -342,8 +342,9 @@ fn load_images( .unwrap_or("Unknown"); button.set_tooltip_text(Some(file_name)); + let path_clone2 = path_clone.clone(); button.connect_clicked(move |_| { - crate::set_wallpaper(path_clone.clone()); + crate::set_wallpaper(path_clone2.clone()); }); flowbox.insert(&button, -1); diff --git a/src/main.rs b/src/main.rs index 56a02a2..675e4b2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,15 +3,16 @@ mod gui; use gtk::{prelude::*, Application}; use lazy_static::lazy_static; use parking_lot::Mutex; -use std::{process::Command, process::Stdio, sync::Once}; +use std::process::Stdio; +use tokio::process::Command as TokioCommand; +use tokio::runtime::Runtime; lazy_static! { static ref MONITORS: Mutex> = Mutex::new(Vec::new()); static ref CURRENT_BACKEND: Mutex = Mutex::new(WallpaperBackend::Hyprpaper); } -static INIT: Once = Once::new(); - +#[derive(Clone, Copy)] pub enum WallpaperBackend { Hyprpaper, Swaybg, @@ -21,6 +22,9 @@ pub enum WallpaperBackend { } fn main() { + let rt = Runtime::new().expect("Failed to create Tokio runtime"); + let _guard = rt.enter(); + let app = Application::builder() .application_id("nnyyxxxx.hyprwall") .build(); @@ -34,7 +38,7 @@ pub fn set_wallpaper(path: String) { match set_wallpaper_internal(&path).await { Ok(_) => println!("Wallpaper set successfully"), Err(e) => { - gui::custom_error_popup("Error setting wallpaper", e.as_str(), true); + gui::custom_error_popup("Error setting wallpaper", &e, true); eprintln!("Error setting wallpaper: {}", e); } } @@ -42,118 +46,78 @@ pub fn set_wallpaper(path: String) { } async fn set_wallpaper_internal(path: &str) -> Result<(), String> { - ensure_backend_running()?; + ensure_backend_running().await?; println!("Attempting to set wallpaper: {}", path); - INIT.call_once(|| match get_monitors() { - Ok(monitors) => *MONITORS.lock() = monitors, - Err(e) => eprintln!("Failed to get monitors: {}", e), - }); - - println!("Found monitors: {:?}", *MONITORS.lock()); - - match *CURRENT_BACKEND.lock() { - WallpaperBackend::Hyprpaper => set_hyprpaper_wallpaper(path), - WallpaperBackend::Swaybg => set_swaybg_wallpaper(path), - WallpaperBackend::Swww => set_swww_wallpaper(path), - WallpaperBackend::Wallutils => set_wallutils_wallpaper(path), - WallpaperBackend::Feh => set_feh_wallpaper(path), + let backend = *CURRENT_BACKEND.lock(); + match backend { + WallpaperBackend::Hyprpaper => set_hyprpaper_wallpaper(path).await, + WallpaperBackend::Swaybg => set_swaybg_wallpaper(path).await, + WallpaperBackend::Swww => set_swww_wallpaper(path).await, + WallpaperBackend::Wallutils => set_wallutils_wallpaper(path).await, + WallpaperBackend::Feh => set_feh_wallpaper(path).await, } } -fn set_hyprpaper_wallpaper(path: &str) -> Result<(), String> { +async fn set_hyprpaper_wallpaper(path: &str) -> Result<(), String> { let preload_command = format!("hyprctl hyprpaper preload \"{}\"", path); - if !execute_command(&preload_command) { - return Err("Failed to preload wallpaper".to_string()); + spawn_background_process(&preload_command).await?; + + let monitors = get_monitors().await?; + + if monitors.is_empty() { + return Err("No monitors detected".to_string()); } - for monitor in MONITORS.lock().iter() { + *MONITORS.lock() = monitors.clone(); + + for monitor in monitors { let set_command = format!("hyprctl hyprpaper wallpaper \"{},{}\"", monitor, path); - if !execute_command(&set_command) { - return Err(format!("Failed to set wallpaper for {}", monitor)); - } + spawn_background_process(&set_command).await?; } Ok(()) } -fn set_swaybg_wallpaper(path: &str) -> Result<(), String> { - Command::new("swaybg") - .arg("-i") - .arg(path) - .arg("-m") - .arg("fill") - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .map_err(|e| format!("Failed to start swaybg: {}", e))?; - - std::thread::sleep(std::time::Duration::from_millis(500)); - if is_process_running("swaybg") { - Ok(()) - } else { - Err("swaybg failed to start or crashed immediately".to_string()) - } +async fn set_swaybg_wallpaper(path: &str) -> Result<(), String> { + let command = format!("swaybg -i \"{}\" -m fill", path); + spawn_background_process(&command).await } -fn set_swww_wallpaper(path: &str) -> Result<(), String> { - let set_command = format!("swww img \"{}\"", path); - if !execute_command(&set_command) { - return Err("Failed to set wallpaper with swww".to_string()); - } - Ok(()) +async fn set_swww_wallpaper(path: &str) -> Result<(), String> { + let command = format!("swww img \"{}\" 2>/dev/null", path); + spawn_background_process(&command).await } -fn set_wallutils_wallpaper(path: &str) -> Result<(), String> { - let set_command = format!("setwallpaper \"{}\"", path); - if !execute_command(&set_command) { - return Err("Failed to set wallpaper with wallutils".to_string()); - } - Ok(()) +async fn set_wallutils_wallpaper(path: &str) -> Result<(), String> { + let command = format!("setwallpaper \"{}\"", path); + spawn_background_process(&command).await } -fn set_feh_wallpaper(path: &str) -> Result<(), String> { - let set_command = format!("feh --bg-fill \"{}\"", path); - if !execute_command(&set_command) { - return Err("Failed to set wallpaper with feh".to_string()); - } - Ok(()) +async fn set_feh_wallpaper(path: &str) -> Result<(), String> { + let command = format!("feh --bg-fill \"{}\"", path); + spawn_background_process(&command).await } -fn execute_command(command: &str) -> bool { - match Command::new("sh") +async fn spawn_background_process(command: &str) -> Result<(), String> { + TokioCommand::new("sh") .arg("-c") .arg(command) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - { - Ok(output) => { - if output.status.success() { - true - } else { - eprintln!( - "Command failed: {}\nStderr: {}\nStdout: {}", - command, - String::from_utf8_lossy(&output.stderr).trim(), - String::from_utf8_lossy(&output.stdout).trim() - ); - false - } - } - Err(e) => { - eprintln!("Failed to execute command: {}. Error: {}", command, e); - false - } - } + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .map_err(|e| format!("Failed to execute command '{}': {}", command, e))?; + + Ok(()) } -fn get_monitors() -> Result, String> { +async fn get_monitors() -> Result, String> { println!("Retrieving monitor information"); - let output = Command::new("hyprctl") + let output = TokioCommand::new("hyprctl") .arg("monitors") .output() + .await .map_err(|e| format!("Failed to execute hyprctl monitors: {}", e))?; let monitors: Vec = String::from_utf8_lossy(&output.stdout) @@ -173,62 +137,61 @@ fn get_monitors() -> Result, String> { Ok(monitors) } -fn ensure_backend_running() -> Result<(), String> { - match *CURRENT_BACKEND.lock() { - WallpaperBackend::Hyprpaper => ensure_hyprpaper_running(), - WallpaperBackend::Swaybg => ensure_swaybg_running(), - WallpaperBackend::Swww => ensure_swww_running(), +async fn ensure_backend_running() -> Result<(), String> { + let backend = *CURRENT_BACKEND.lock(); + match backend { + WallpaperBackend::Hyprpaper => ensure_hyprpaper_running().await, + WallpaperBackend::Swaybg => ensure_swaybg_running().await, + WallpaperBackend::Swww => ensure_swww_running().await, WallpaperBackend::Wallutils => Ok(()), WallpaperBackend::Feh => Ok(()), } } -fn ensure_hyprpaper_running() -> Result<(), String> { - if !is_process_running("hyprpaper") { +async fn ensure_hyprpaper_running() -> Result<(), String> { + if !is_process_running("hyprpaper").await { println!("hyprpaper is not running. Attempting to start it..."); - start_process("hyprpaper")?; + start_process("hyprpaper").await?; } Ok(()) } -fn ensure_swaybg_running() -> Result<(), String> { - if !is_process_running("swaybg") { +async fn ensure_swaybg_running() -> Result<(), String> { + if !is_process_running("swaybg").await { println!("swaybg is not running. Attempting to start it..."); - start_process("swaybg")?; + start_process("swaybg").await?; } Ok(()) } -fn ensure_swww_running() -> Result<(), String> { - if !is_process_running("swww") { +async fn ensure_swww_running() -> Result<(), String> { + if !is_process_running("swww-daemon").await { println!("swww is not running. Attempting to start it..."); - start_process("swww init")?; + start_process("swww-daemon 2>/dev/null").await?; } Ok(()) } -fn is_process_running(process_name: &str) -> bool { - Command::new("pgrep") +async fn is_process_running(process_name: &str) -> bool { + TokioCommand::new("pgrep") .arg("-x") .arg(process_name) - .stdout(Stdio::null()) .status() + .await .map(|status| status.success()) .unwrap_or(false) } -fn start_process(command: &str) -> Result<(), String> { - Command::new("sh") +async fn start_process(command: &str) -> Result<(), String> { + TokioCommand::new("sh") .arg("-c") .arg(command) - .stdout(Stdio::null()) - .stderr(Stdio::null()) .spawn() .map_err(|e| format!("Failed to start {}: {}", command, e))?; - std::thread::sleep(std::time::Duration::from_secs(1)); + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - if is_process_running(command.split_whitespace().next().unwrap_or(command)) { + if is_process_running(command.split_whitespace().next().unwrap_or(command)).await { Ok(()) } else { Err(format!("Failed to start {}", command)) @@ -236,5 +199,44 @@ fn start_process(command: &str) -> Result<(), String> { } pub fn set_wallpaper_backend(backend: WallpaperBackend) { - *CURRENT_BACKEND.lock() = backend; + let previous_backend = { + let mut current = CURRENT_BACKEND.lock(); + let prev = *current; + *current = backend; + prev + }; + tokio::spawn(async move { + drop_all_wallpapers(previous_backend).await; + kill_previous_backend(previous_backend).await; + }); +} + +async fn kill_previous_backend(backend: WallpaperBackend) { + let process_name = match backend { + WallpaperBackend::Hyprpaper => "hyprpaper", + WallpaperBackend::Swaybg => "swaybg", + WallpaperBackend::Swww => "swww-daemon", + WallpaperBackend::Wallutils => return, + WallpaperBackend::Feh => return, + }; + + let _ = TokioCommand::new("killall") + .arg(process_name) + .status() + .await; +} + +async fn drop_all_wallpapers(backend: WallpaperBackend) { + match backend { + WallpaperBackend::Hyprpaper => { + let _ = TokioCommand::new("hyprctl") + .args(["hyprpaper", "unload", "all"]) + .status() + .await; + } + WallpaperBackend::Swww => { + let _ = TokioCommand::new("swww").args(["clear"]).status().await; + } + _ => {} + } }