use crate::{
AwwwBackend, Colors, CommandExt, Config, Desktop, Dimension, FileInfo, HyprlandBackend,
Monitor,
Orientation::{Horizontal, Vertical},
ProceduralEffect, SwaybgBackend, U8Extension, WallSwitchError, WallSwitchResult,
detect_monitors, is_installed,
};
use image::{RgbImage, imageops::FilterType};
use rayon::prelude::*; use std::{io::Error, process::Command};
pub trait WallpaperBackend {
fn build_commands(_images: &[FileInfo], _config: &Config) -> WallSwitchResult<Vec<Command>> {
Ok(vec![])
}
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();
cmd.run_with_config(config, &format!("Executing {program_name}"))?;
}
Ok(())
}
}
pub fn set_wallpaper(images: &[FileInfo], config: &Config) -> WallSwitchResult<()> {
let needs_compilation = config.desktop == Desktop::Gnome
|| config.effect != ProceduralEffect::None
|| config.monitors.iter().any(|m| m.pictures_per_monitor > 1);
let compiled_images = if needs_compilation {
compile_wallpapers_for_monitors(images, config)?
} else {
images.to_vec()
};
match config.desktop {
Desktop::Gnome => {
if config.dry_run {
println!(
"[DRY-RUN] Would stitch compiled monitor canvases together to generate final spanned wallpaper."
);
} else {
let final_wallpaper = assemble_final_wallpaper(&compiled_images, config)?;
final_wallpaper
.save(&config.wallpaper)
.map_err(|e| WallSwitchError::Io(Error::other(e)))?;
if config.verbose {
println!("Stitched wallpaper saved to Gnome: {:?}", config.wallpaper);
}
}
GnomeBackend::apply(&compiled_images, config)?;
}
Desktop::Xfce => XfceBackend::apply(&compiled_images, config)?,
Desktop::Hyprland => {
if is_installed("hyprpaper") {
HyprlandBackend::apply(&compiled_images, config)?;
} else if is_installed("awww") {
AwwwBackend::apply(&compiled_images, config)?;
} else if is_installed("swaybg") {
SwaybgBackend::apply(&compiled_images, config)?;
} else {
return Err(WallSwitchError::MissingWaylandTools);
}
}
Desktop::Niri | Desktop::Labwc | Desktop::Mango | Desktop::Wayland => {
if is_installed("awww") {
AwwwBackend::apply(&compiled_images, config)?;
} else if is_installed("swaybg") {
SwaybgBackend::apply(&compiled_images, config)?;
} else {
return Err(WallSwitchError::MissingWaylandTools);
}
}
Desktop::Openbox => OpenboxBackend::apply(&compiled_images, config)?,
}
Ok(())
}
pub struct GnomeBackend;
impl WallpaperBackend for GnomeBackend {
fn build_commands(_images: &[FileInfo], config: &Config) -> WallSwitchResult<Vec<Command>> {
let mut commands = Vec::new();
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])
}
}
struct LayoutTarget {
base_w: u64,
base_h: u64,
rem_w: usize,
rem_h: usize,
}
impl LayoutTarget {
fn calculate(monitor: &Monitor) -> Result<Self, std::num::TryFromIntError> {
let mut width = monitor.resolution.width;
let mut height = monitor.resolution.height;
let pics_per_monitor = monitor.pictures_per_monitor.to_u64();
let rem_w = (width % pics_per_monitor).try_into()?;
let rem_h = (height % pics_per_monitor).try_into()?;
match monitor.picture_orientation {
Horizontal => height /= pics_per_monitor,
Vertical => width /= pics_per_monitor,
}
Ok(Self {
base_w: width,
base_h: height,
rem_w,
rem_h,
})
}
}
fn apply_selected_effect(
canvas: &mut RgbImage,
monitor: &Monitor,
config: &Config,
index: usize,
) -> WallSwitchResult<()> {
if config.effect == ProceduralEffect::None {
return Ok(());
}
let resolved = config.effect.resolve();
if let Some(renderer) = resolved.get_renderer(monitor, config)? {
if config.verbose {
let idx = index.to_string().bold().cyan();
let name = resolved.get_name().bold().blue();
println!("Applying to Monitor {idx} {name} {}", renderer.info());
}
renderer.apply(canvas);
}
Ok(())
}
fn compile_single_monitor_background(
partition: &[FileInfo],
monitor: &Monitor,
config: &Config,
index: usize,
) -> WallSwitchResult<FileInfo> {
let output_path = std::env::temp_dir().join(format!("wallswitch_monitor_{index}.jpg"));
if config.dry_run {
if config.verbose {
println!(
"[DRY-RUN] Would compile backgrounds for Monitor {index} at resolution {}x{}",
monitor.resolution.width, monitor.resolution.height
);
}
} else {
let mut monitor_canvas = assemble_monitor_canvas(partition, monitor)?;
if config.effect != ProceduralEffect::None {
apply_selected_effect(&mut monitor_canvas, monitor, config, index)?;
}
monitor_canvas
.save(&output_path)
.map_err(|e| WallSwitchError::Io(Error::other(e)))?;
if config.verbose {
println!("Monitor {index} background assembled: {:?}", output_path);
}
}
Ok(FileInfo {
path: output_path,
size: 0,
mtime: 0,
hash: String::new(),
dimension: Some(Dimension {
width: monitor.resolution.width,
height: monitor.resolution.height,
}),
is_valid: Some(true),
number: index + 1,
total: config.monitors.len(),
})
}
pub fn compile_wallpapers_for_monitors(
images: &[FileInfo],
config: &Config,
) -> WallSwitchResult<Vec<FileInfo>> {
if config.verbose {
if config.dry_run {
println!("[DRY-RUN] Would assemble multi-monitor wallpaper in pure Rust ...");
} else {
println!("Assembling multi-monitor wallpaper in pure Rust ...");
}
}
let partitions: Vec<&[FileInfo]> = get_partitions_iter(images, config).collect();
let compiled_files = partitions
.into_par_iter()
.zip(&config.monitors)
.enumerate()
.map(|(index, (partition, monitor))| {
compile_single_monitor_background(partition, monitor, config, index)
})
.collect::<WallSwitchResult<Vec<_>>>()?;
Ok(compiled_files)
}
fn assemble_monitor_canvas(
partition: &[FileInfo],
monitor: &Monitor,
) -> WallSwitchResult<RgbImage> {
let mut monitor_canvas = RgbImage::new(
monitor.resolution.width as u32,
monitor.resolution.height as u32,
);
let target = LayoutTarget::calculate(monitor)?;
let mut current_x = 0;
let mut current_y = 0;
for (p_idx, image_info) in partition.iter().enumerate() {
let mut w = target.base_w;
let mut h = target.base_h;
match monitor.picture_orientation {
Horizontal => {
if p_idx < target.rem_h {
h += 1;
}
}
Vertical => {
if p_idx < target.rem_w {
w += 1;
}
}
}
let resized = {
let img =
image::open(&image_info.path).map_err(|err| WallSwitchError::CorruptImage {
path: image_info.path.clone(),
source: err,
})?;
img.resize_to_fill(w as u32, h as u32, FilterType::Triangle)
.to_rgb8()
};
image::imageops::overlay(
&mut monitor_canvas,
&resized,
current_x as i64,
current_y as i64,
);
match monitor.picture_orientation {
Horizontal => {
current_y += h;
}
Vertical => {
current_x += w;
}
}
}
Ok(monitor_canvas)
}
fn assemble_final_wallpaper(
compiled_images: &[FileInfo],
config: &Config,
) -> WallSwitchResult<RgbImage> {
let mut total_w = 0;
let mut total_h = 0;
for monitor in &config.monitors {
match config.monitor_orientation {
Horizontal => {
total_w += monitor.resolution.width;
total_h = total_h.max(monitor.resolution.height);
}
Vertical => {
total_w = total_w.max(monitor.resolution.width);
total_h += monitor.resolution.height;
}
}
}
let mut final_canvas = RgbImage::new(total_w as u32, total_h as u32);
let mut current_x = 0;
let mut current_y = 0;
for (idx, img_info) in compiled_images.iter().enumerate() {
let img = image::open(&img_info.path)
.map_err(|e| {
WallSwitchError::UnableToFind(format!(
"Failed to load compiled monitor canvas: {e}"
))
})?
.to_rgb8();
image::imageops::overlay(&mut final_canvas, &img, current_x as i64, current_y as i64);
match config.monitor_orientation {
Horizontal => {
current_x += config.monitors[idx].resolution.width;
}
Vertical => {
current_y += config.monitors[idx].resolution.height;
}
}
}
Ok(final_canvas)
}
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
})
}