use crate::{
Config, Desktop, FileInfo,
Orientation::{Horizontal, Vertical},
U8Extension, WallSwitchError, WallSwitchResult, set_awww_wallpaper,
};
use std::{
path::PathBuf,
process::{Command, Output, Stdio},
};
pub fn set_wallpaper(images: &[FileInfo], config: &Config) -> WallSwitchResult<()> {
match config.desktop {
Desktop::Gnome => set_gnome_wallpaper(images, config)?,
Desktop::Xfce => set_xfce_wallpaper(images, config)?,
Desktop::Hyprland => set_hyprland_wallpaper(images, config)?,
Desktop::Niri => set_niri_wallpaper(images, config)?,
Desktop::Labwc | Desktop::Mango | Desktop::Wayland => {
if is_installed("awww") {
set_awww_wallpaper(images, config)?;
} else if is_installed("swaybg") {
let monitors = crate::awww::get_wayland_monitors(config)?;
apply_swaybg_wallpaper(images, &monitors, config)?;
} else if is_installed("hyprpaper") {
set_hyprland_wallpaper(images, config)?;
} else {
return Err(WallSwitchError::MissingWaylandTools);
}
}
Desktop::Openbox => set_openbox_wallpaper(images, config)?,
}
println!();
Ok(())
}
fn is_installed(binary: &str) -> bool {
let mut cmd = Command::new("which");
cmd.arg(binary);
cmd.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn apply_swaybg_wallpaper(
images: &[FileInfo],
monitors: &[String],
config: &Config,
) -> WallSwitchResult<()> {
let _ = Command::new("pkill").arg("swaybg").output();
let mut cmd = Command::new("swaybg");
for (image, monitor) in images.iter().zip(monitors) {
let path_str = image.path.to_str().unwrap_or_default();
cmd.arg("-o")
.arg(monitor)
.arg("-i")
.arg(path_str)
.arg("-m")
.arg("fill");
}
if config.verbose {
let program = cmd.get_program();
let arguments: Vec<_> = cmd.get_args().collect::<Vec<_>>();
println!("\nprogram: {program:?}");
println!("arguments: {arguments:#?}");
}
cmd.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(WallSwitchError::Io)?;
Ok(())
}
fn set_hyprland_wallpaper(images: &[FileInfo], config: &Config) -> WallSwitchResult<()> {
let monitors = get_hyprland_monitors(config)?;
let mut check_cmd = Command::new("hyprctl");
check_cmd.args(["hyprpaper", "listloaded"]);
let loaded_str = match check_cmd.output() {
Ok(out) => String::from_utf8_lossy(&out.stdout).to_string(),
Err(_) => {
return Err(WallSwitchError::UnableToFind(
"hyprpaper daemon not running".into(),
));
}
};
for (image, monitor) in images.iter().zip(&monitors) {
let path_str = image.path.to_str().unwrap_or_default();
if !loaded_str.contains(path_str) {
let mut preload_cmd = Command::new("hyprctl");
preload_cmd.args(["hyprpaper", "preload", path_str]);
if config.verbose {
println!("\nprogram: {:?}", preload_cmd.get_program());
println!(
"arguments: {:#?}",
preload_cmd.get_args().collect::<Vec<_>>()
);
}
let _ = preload_cmd.output();
}
let mut wall_cmd = Command::new("hyprctl");
let wall_arg = format!("{monitor},{path_str}");
wall_cmd.args(["hyprpaper", "wallpaper", &wall_arg]);
exec_cmd(
&mut wall_cmd,
config.verbose,
&format!("Apply wallpaper on {monitor}"),
)?;
}
let mut unload_cmd = Command::new("hyprctl");
unload_cmd.args(["hyprpaper", "unload", "unused"]);
let _ = unload_cmd.output();
Ok(())
}
fn get_hyprland_monitors(config: &Config) -> WallSwitchResult<Vec<String>> {
let mut cmd = Command::new("hyprctl");
cmd.arg("monitors");
let output = exec_cmd(&mut cmd, config.verbose, "get_hyprland_monitors")?;
let stdout = String::from_utf8_lossy(&output.stdout);
let monitors: Vec<String> = stdout
.lines()
.filter(|line| line.starts_with("Monitor"))
.filter_map(|line| line.split_whitespace().nth(1).map(|s| s.to_string()))
.collect();
if monitors.is_empty() {
return Err(WallSwitchError::NoMonitors("hyprctl".to_string()));
}
Ok(monitors)
}
fn set_niri_wallpaper(images: &[FileInfo], config: &Config) -> WallSwitchResult<()> {
let monitors = get_niri_monitors(config)?;
if is_installed("swaybg") {
apply_swaybg_wallpaper(images, &monitors, config)
} else if is_installed("hyprpaper") {
set_hyprland_wallpaper(images, config)
} else {
Err(WallSwitchError::MissingWaylandTools)
}
}
fn get_niri_monitors(config: &Config) -> WallSwitchResult<Vec<String>> {
let mut cmd = Command::new("niri");
cmd.args(["msg", "outputs"]);
let output = exec_cmd(&mut cmd, config.verbose, "get_niri_monitors")?;
let stdout = String::from_utf8_lossy(&output.stdout);
let monitors: Vec<String> = stdout
.lines()
.filter(|line| line.starts_with("Output"))
.filter_map(|line| {
let start = line.rfind('(')?; let end = line.rfind(')')?; if start < end {
Some(line[start + 1..end].to_string())
} else {
None
}
})
.collect();
if monitors.is_empty() {
return Err(WallSwitchError::NoMonitors("niri msg".to_string()));
}
Ok(monitors)
}
fn set_xfce_wallpaper(images: &[FileInfo], config: &Config) -> WallSwitchResult<()> {
let monitors = get_xfce_monitors(config)?;
if config.verbose {
println!("monitors:\n{monitors:#?}");
}
for (image, monitor) in images.iter().zip(monitors) {
apply_xfconf(&image.path, &monitor, config)?;
}
Ok(())
}
fn get_xfce_monitors(config: &Config) -> WallSwitchResult<Vec<String>> {
let words = ["screen0", "workspace0", "last-image"];
let mut cmd = Command::new("xfconf-query");
let xfconf_cmd = cmd.args([
"--channel",
"xfce4-desktop",
"--property",
"/backdrop",
"--list",
]);
let xfconf_out: Output = exec_cmd(xfconf_cmd, config.verbose, "get_xfce_monitors: xfconf")?;
let std_output: String = String::from_utf8(xfconf_out.stdout)?;
let outputs: Vec<String> = std_output
.trim()
.split(['\n', ' '])
.filter(|&output| words.into_iter().all(|word| output.contains(word)))
.map(ToString::to_string)
.collect();
Ok(outputs)
}
fn apply_xfconf(path: &PathBuf, monitor: &str, config: &Config) -> WallSwitchResult<()> {
let mut cmd = Command::new("xfconf-query");
let xfconf = cmd
.args(["--channel", "xfce4-desktop", "--property", monitor, "--set"])
.arg(path);
let msg = format!("apply_xfconf: xfconf {monitor}");
exec_cmd(xfconf, config.verbose, &msg)?;
Ok(())
}
fn set_openbox_wallpaper(images: &[FileInfo], config: &Config) -> WallSwitchResult<()> {
let mut feh_cmd = Command::new(&config.path_feh);
for image in images {
feh_cmd.arg("--bg-fill").arg(&image.path);
}
exec_cmd(&mut feh_cmd, config.verbose, "feh")?;
Ok(())
}
fn set_gnome_wallpaper(images: &[FileInfo], config: &Config) -> WallSwitchResult<()> {
create_background_image(images, config)?;
for picture in ["picture-uri", "picture-uri-dark"] {
let mut cmd = Command::new("gsettings");
let gsettings = cmd
.args(["set", "org.gnome.desktop.background", picture])
.arg(&config.wallpaper);
let msg = format!("gsettings {picture}");
exec_cmd(gsettings, config.verbose, &msg)?;
}
let mut cmd = Command::new("gsettings");
let spanned = cmd.args([
"set",
"org.gnome.desktop.background",
"picture-options",
"spanned",
]);
exec_cmd(spanned, config.verbose, "spanned")?;
Ok(())
}
fn create_background_image(images: &[FileInfo], config: &Config) -> WallSwitchResult<()> {
let mut magick_cmd = Command::new(&config.path_magick);
get_partitions_iter(images, config)
.zip(&config.monitors)
.try_for_each(|(images, monitor)| -> WallSwitchResult<()> {
let mut width: u64 = monitor.resolution.width;
let mut height: u64 = monitor.resolution.height;
let pictures_per_monitor = monitor.pictures_per_monitor.to_u64();
let remainder_w: usize = (width % pictures_per_monitor).try_into()?;
let remainder_h: usize = (height % pictures_per_monitor).try_into()?;
match monitor.picture_orientation {
Horizontal => height /= pictures_per_monitor,
Vertical => width /= pictures_per_monitor,
}
magick_cmd.args(["(", "-gravity", "Center"]);
images.iter().enumerate().for_each(|(index, image)| {
let mut w = width;
let mut h = height;
match monitor.picture_orientation {
Horizontal => {
if index < remainder_h {
h += 1; }
}
Vertical => {
if index < remainder_w {
w += 1; }
}
}
let resize = format!("{w}x{h}^");
let extent = format!("{w}x{h}");
magick_cmd
.arg("(")
.arg(&image.path)
.args(["-resize", &resize])
.args(["-extent", &extent])
.arg(")");
});
match monitor.picture_orientation {
Horizontal => {
magick_cmd.args(["-gravity", "South", "-append", ")"]);
}
Vertical => {
magick_cmd.args(["-gravity", "South", "+append", ")"]);
}
}
Ok(())
})?;
match config.monitor_orientation {
Horizontal => {
magick_cmd.arg("+append").arg(&config.wallpaper);
}
Vertical => {
magick_cmd.arg("-append").arg(&config.wallpaper);
}
}
exec_cmd(&mut magick_cmd, config.verbose, "magick")?;
Ok(())
}
#[allow(dead_code)]
fn get_partitions_slice<'a>(mut images: &'a [FileInfo], config: &'a Config) -> Vec<&'a [FileInfo]> {
let mut partition = Vec::new();
config.monitors.iter().for_each(|monitor| {
let (head, tail) = images.split_at(monitor.pictures_per_monitor.into());
images = tail;
partition.push(head);
});
partition
}
fn get_partitions_iter<'a>(
mut images: &'a [FileInfo],
config: &'a Config,
) -> impl Iterator<Item = &'a [FileInfo]> {
config.monitors.iter().map(move |monitor| {
let (head, tail) = images.split_at(monitor.pictures_per_monitor.into());
images = tail;
head
})
}
pub fn exec_cmd(cmd: &mut Command, verbose: bool, msg: &str) -> WallSwitchResult<Output> {
let output: Output = cmd.output().inspect_err(|error| {
eprintln!("fn exec_cmd()");
eprintln!("cmd: {cmd:?}");
eprintln!("Error: {error}");
})?;
if !output.status.success() || verbose {
let program = cmd.get_program();
let arguments: Vec<_> = cmd.get_args().collect();
println!("\nprogram: {program:?}");
println!("arguments: {arguments:#?}");
let stdout = String::from_utf8_lossy(&output.stdout);
if !stdout.trim().is_empty() {
println!("stdout:'{}'\n", stdout.trim());
}
}
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let status = output.status;
eprintln!("{msg} status: {status}");
eprintln!("{msg} stderr: {stderr}");
panic!("{stderr:?}");
}
Ok(output)
}