use crate::{
AwwwBackend, Config, Desktop, FileInfo,
Orientation::{Horizontal, Vertical},
U8Extension, WallSwitchError, WallSwitchResult, detect_monitors, is_installed,
};
use std::process::{Command, Output, Stdio};
pub trait WallpaperBackend {
fn build_commands(images: &[FileInfo], config: &Config) -> WallSwitchResult<Vec<Command>>;
fn apply(images: &[FileInfo], config: &Config) -> WallSwitchResult<()> {
let mut commands = Self::build_commands(images, config)?;
for cmd in commands.iter_mut() {
let program_name = cmd.get_program().to_string_lossy().to_string();
exec_cmd(cmd, config.verbose, &format!("Executing {program_name}"))?;
}
Ok(())
}
}
pub fn set_wallpaper(images: &[FileInfo], config: &Config) -> WallSwitchResult<()> {
match config.desktop {
Desktop::Gnome => GnomeBackend::apply(images, config)?,
Desktop::Xfce => XfceBackend::apply(images, config)?,
Desktop::Hyprland => HyprlandBackend::apply(images, config)?,
Desktop::Niri | Desktop::Labwc | Desktop::Mango | Desktop::Wayland => {
if is_installed("awww") {
AwwwBackend::apply(images, config)?;
} else if is_installed("swaybg") {
SwaybgBackend::apply(images, config)?;
} else if is_installed("hyprpaper") {
HyprlandBackend::apply(images, config)?;
} else {
return Err(WallSwitchError::MissingWaylandTools);
}
}
Desktop::Openbox => OpenboxBackend::apply(images, config)?,
}
println!();
Ok(())
}
pub struct GnomeBackend;
impl WallpaperBackend for GnomeBackend {
fn build_commands(images: &[FileInfo], config: &Config) -> WallSwitchResult<Vec<Command>> {
let mut commands = Vec::new();
commands.push(build_magick_command(images, config)?);
for picture in ["picture-uri", "picture-uri-dark"] {
let mut cmd = Command::new("gsettings");
cmd.args(["set", "org.gnome.desktop.background", picture])
.arg(&config.wallpaper);
commands.push(cmd);
}
let mut cmd = Command::new("gsettings");
cmd.args([
"set",
"org.gnome.desktop.background",
"picture-options",
"spanned",
]);
commands.push(cmd);
Ok(commands)
}
}
pub struct XfceBackend;
impl WallpaperBackend for XfceBackend {
fn build_commands(images: &[FileInfo], config: &Config) -> WallSwitchResult<Vec<Command>> {
let mut commands = Vec::new();
let monitors = detect_monitors(config)?;
if config.verbose {
println!("monitors:\n{monitors:#?}");
}
for (image, monitor) in images.iter().cycle().zip(monitors) {
let mut cmd = Command::new("xfconf-query");
cmd.args([
"--channel",
"xfce4-desktop",
"--property",
&monitor,
"--create",
"--type",
"string",
"--set",
])
.arg(&image.path);
commands.push(cmd);
}
Ok(commands)
}
}
pub struct OpenboxBackend;
impl WallpaperBackend for OpenboxBackend {
fn build_commands(images: &[FileInfo], config: &Config) -> WallSwitchResult<Vec<Command>> {
let mut feh_cmd = Command::new(&config.path_feh);
for image in images {
feh_cmd.arg("--bg-fill").arg(&image.path);
}
Ok(vec![feh_cmd])
}
}
pub struct SwaybgBackend;
impl WallpaperBackend for SwaybgBackend {
fn build_commands(_images: &[FileInfo], _config: &Config) -> WallSwitchResult<Vec<Command>> {
Ok(vec![])
}
fn apply(images: &[FileInfo], config: &Config) -> WallSwitchResult<()> {
let monitors = detect_monitors(config)?;
if config.verbose {
println!("monitors:\n{monitors:#?}");
}
let _ = Command::new("pkill").arg("swaybg").output();
let mut cmd = Command::new("swaybg");
for (image, monitor) in images.iter().cycle().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(())
}
}
pub struct HyprlandBackend;
impl WallpaperBackend for HyprlandBackend {
fn build_commands(_images: &[FileInfo], _config: &Config) -> WallSwitchResult<Vec<Command>> {
Ok(vec![])
}
fn apply(images: &[FileInfo], config: &Config) -> WallSwitchResult<()> {
let monitors = detect_monitors(config)?;
if config.verbose {
println!("monitors:\n{monitors:#?}");
}
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().cycle().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 build_magick_command(images: &[FileInfo], config: &Config) -> WallSwitchResult<Command> {
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);
}
}
Ok(magick_cmd)
}
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().map_err(|e| {
eprintln!("Failed to execute command: {:?}", cmd.get_program());
WallSwitchError::Io(e)
})?;
let program = cmd.get_program();
let arguments: Vec<_> = cmd.get_args().collect();
if !output.status.success() || verbose {
println!("program: {program:?}");
println!("arguments: {arguments:#?}\n");
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}");
return Err(WallSwitchError::CommandFailed {
program: format!("{:?}", cmd.get_program()),
status: output.status.to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
});
}
Ok(output)
}