1use crate::{
2 AwwwBackend, Config, Desktop, FileInfo,
3 Orientation::{Horizontal, Vertical},
4 U8Extension, WallSwitchError, WallSwitchResult, detect_monitors, is_installed,
5};
6use std::process::{Command, Output, Stdio};
7
8pub trait WallpaperBackend {
11 fn build_commands(images: &[FileInfo], config: &Config) -> WallSwitchResult<Vec<Command>>;
14
15 fn apply(images: &[FileInfo], config: &Config) -> WallSwitchResult<()> {
20 let mut commands = Self::build_commands(images, config)?;
21 for cmd in commands.iter_mut() {
22 let program_name = cmd.get_program().to_string_lossy().to_string();
23 exec_cmd(cmd, config.verbose, &format!("Executing {program_name}"))?;
24 }
25 Ok(())
26 }
27}
28
29pub fn set_wallpaper(images: &[FileInfo], config: &Config) -> WallSwitchResult<()> {
31 match config.desktop {
32 Desktop::Gnome => GnomeBackend::apply(images, config)?,
33 Desktop::Xfce => XfceBackend::apply(images, config)?,
34 Desktop::Hyprland => HyprlandBackend::apply(images, config)?,
35
36 Desktop::Niri | Desktop::Labwc | Desktop::Mango | Desktop::Wayland => {
38 if is_installed("awww") {
39 AwwwBackend::apply(images, config)?;
40 } else if is_installed("swaybg") {
41 SwaybgBackend::apply(images, config)?;
42 } else if is_installed("hyprpaper") {
43 HyprlandBackend::apply(images, config)?;
44 } else {
45 return Err(WallSwitchError::MissingWaylandTools);
46 }
47 }
48
49 Desktop::Openbox => OpenboxBackend::apply(images, config)?,
50 }
51
52 println!();
53 Ok(())
54}
55
56pub struct GnomeBackend;
61
62impl WallpaperBackend for GnomeBackend {
63 fn build_commands(images: &[FileInfo], config: &Config) -> WallSwitchResult<Vec<Command>> {
64 let mut commands = Vec::new();
65
66 commands.push(build_magick_command(images, config)?);
68
69 for picture in ["picture-uri", "picture-uri-dark"] {
71 let mut cmd = Command::new("gsettings");
72 cmd.args(["set", "org.gnome.desktop.background", picture])
73 .arg(&config.wallpaper);
74 commands.push(cmd);
75 }
76
77 let mut cmd = Command::new("gsettings");
79 cmd.args([
80 "set",
81 "org.gnome.desktop.background",
82 "picture-options",
83 "spanned",
84 ]);
85 commands.push(cmd);
86
87 Ok(commands)
88 }
89}
90
91pub struct XfceBackend;
92
93impl WallpaperBackend for XfceBackend {
94 fn build_commands(images: &[FileInfo], config: &Config) -> WallSwitchResult<Vec<Command>> {
95 let mut commands = Vec::new();
96 let monitors = detect_monitors(config)?;
97
98 if config.verbose {
99 println!("monitors:\n{monitors:#?}");
100 }
101
102 for (image, monitor) in images.iter().cycle().zip(monitors) {
105 let mut cmd = Command::new("xfconf-query");
106 cmd.args([
107 "--channel",
108 "xfce4-desktop",
109 "--property",
110 &monitor,
111 "--create",
112 "--type",
113 "string",
114 "--set",
115 ])
116 .arg(&image.path);
117
118 commands.push(cmd);
119 }
120
121 Ok(commands)
122 }
123}
124
125pub struct OpenboxBackend;
126
127impl WallpaperBackend for OpenboxBackend {
128 fn build_commands(images: &[FileInfo], config: &Config) -> WallSwitchResult<Vec<Command>> {
129 let mut feh_cmd = Command::new(&config.path_feh);
130
131 for image in images {
132 feh_cmd.arg("--bg-fill").arg(&image.path);
133 }
134
135 Ok(vec![feh_cmd])
136 }
137}
138
139pub struct SwaybgBackend;
140
141impl WallpaperBackend for SwaybgBackend {
142 fn build_commands(_images: &[FileInfo], _config: &Config) -> WallSwitchResult<Vec<Command>> {
143 Ok(vec![])
146 }
147
148 fn apply(images: &[FileInfo], config: &Config) -> WallSwitchResult<()> {
149 let monitors = detect_monitors(config)?;
150
151 if config.verbose {
152 println!("monitors:\n{monitors:#?}");
153 }
154
155 let _ = Command::new("pkill").arg("swaybg").output();
157
158 let mut cmd = Command::new("swaybg");
159 for (image, monitor) in images.iter().cycle().zip(&monitors) {
161 let path_str = image.path.to_str().unwrap_or_default();
162 cmd.arg("-o")
163 .arg(monitor)
164 .arg("-i")
165 .arg(path_str)
166 .arg("-m")
167 .arg("fill");
168 }
169
170 if config.verbose {
171 let program = cmd.get_program();
172 let arguments: Vec<_> = cmd.get_args().collect::<Vec<_>>();
173 println!("\nprogram: {program:?}");
174 println!("arguments: {arguments:#?}");
175 }
176
177 cmd.stdout(Stdio::null())
179 .stderr(Stdio::null())
180 .spawn()
181 .map_err(WallSwitchError::Io)?;
182
183 Ok(())
184 }
185}
186
187pub struct HyprlandBackend;
188
189impl WallpaperBackend for HyprlandBackend {
190 fn build_commands(_images: &[FileInfo], _config: &Config) -> WallSwitchResult<Vec<Command>> {
191 Ok(vec![])
194 }
195
196 fn apply(images: &[FileInfo], config: &Config) -> WallSwitchResult<()> {
197 let monitors = detect_monitors(config)?;
198
199 if config.verbose {
200 println!("monitors:\n{monitors:#?}");
201 }
202
203 let mut check_cmd = Command::new("hyprctl");
205 check_cmd.args(["hyprpaper", "listloaded"]);
206
207 let loaded_str = match check_cmd.output() {
208 Ok(out) => String::from_utf8_lossy(&out.stdout).to_string(),
209 Err(_) => {
210 return Err(WallSwitchError::UnableToFind(
211 "hyprpaper daemon not running".into(),
212 ));
213 }
214 };
215
216 for (image, monitor) in images.iter().cycle().zip(&monitors) {
219 let path_str = image.path.to_str().unwrap_or_default();
220
221 if !loaded_str.contains(path_str) {
222 let mut preload_cmd = Command::new("hyprctl");
223 preload_cmd.args(["hyprpaper", "preload", path_str]);
224
225 if config.verbose {
226 println!("\nprogram: {:?}", preload_cmd.get_program());
227 println!(
228 "arguments: {:#?}",
229 preload_cmd.get_args().collect::<Vec<_>>()
230 );
231 }
232 let _ = preload_cmd.output();
233 }
234
235 let mut wall_cmd = Command::new("hyprctl");
236 let wall_arg = format!("{monitor},{path_str}");
237 wall_cmd.args(["hyprpaper", "wallpaper", &wall_arg]);
238
239 exec_cmd(
240 &mut wall_cmd,
241 config.verbose,
242 &format!("Apply wallpaper on {monitor}"),
243 )?;
244 }
245
246 let mut unload_cmd = Command::new("hyprctl");
248 unload_cmd.args(["hyprpaper", "unload", "unused"]);
249 let _ = unload_cmd.output();
250
251 Ok(())
252 }
253}
254
255fn build_magick_command(images: &[FileInfo], config: &Config) -> WallSwitchResult<Command> {
261 let mut magick_cmd = Command::new(&config.path_magick);
262
263 get_partitions_iter(images, config)
264 .zip(&config.monitors)
265 .try_for_each(|(images, monitor)| -> WallSwitchResult<()> {
266 let mut width: u64 = monitor.resolution.width;
267 let mut height: u64 = monitor.resolution.height;
268
269 let pictures_per_monitor = monitor.pictures_per_monitor.to_u64();
270
271 let remainder_w: usize = (width % pictures_per_monitor).try_into()?;
272 let remainder_h: usize = (height % pictures_per_monitor).try_into()?;
273
274 match monitor.picture_orientation {
275 Horizontal => height /= pictures_per_monitor,
276 Vertical => width /= pictures_per_monitor,
277 }
278
279 magick_cmd.args(["(", "-gravity", "Center"]);
280
281 images.iter().enumerate().for_each(|(index, image)| {
282 let mut w = width;
283 let mut h = height;
284
285 match monitor.picture_orientation {
286 Horizontal => {
287 if index < remainder_h {
288 h += 1;
289 }
290 }
291 Vertical => {
292 if index < remainder_w {
293 w += 1;
294 }
295 }
296 }
297
298 let resize = format!("{w}x{h}^");
299 let extent = format!("{w}x{h}");
300
301 magick_cmd
302 .arg("(")
303 .arg(&image.path)
304 .args(["-resize", &resize])
305 .args(["-extent", &extent])
306 .arg(")");
307 });
308
309 match monitor.picture_orientation {
310 Horizontal => {
311 magick_cmd.args(["-gravity", "South", "-append", ")"]);
312 }
313 Vertical => {
314 magick_cmd.args(["-gravity", "South", "+append", ")"]);
315 }
316 }
317
318 Ok(())
319 })?;
320
321 match config.monitor_orientation {
322 Horizontal => {
323 magick_cmd.arg("+append").arg(&config.wallpaper);
324 }
325 Vertical => {
326 magick_cmd.arg("-append").arg(&config.wallpaper);
327 }
328 }
329
330 Ok(magick_cmd)
331}
332
333fn get_partitions_iter<'a>(
334 mut images: &'a [FileInfo],
335 config: &'a Config,
336) -> impl Iterator<Item = &'a [FileInfo]> {
337 config.monitors.iter().map(move |monitor| {
338 let (head, tail) = images.split_at(monitor.pictures_per_monitor.into());
339 images = tail;
340 head
341 })
342}
343
344pub fn exec_cmd(cmd: &mut Command, verbose: bool, msg: &str) -> WallSwitchResult<Output> {
345 let output: Output = cmd.output().map_err(|e| {
346 eprintln!("Failed to execute command: {:?}", cmd.get_program());
347 WallSwitchError::Io(e)
348 })?;
349
350 let program = cmd.get_program();
351 let arguments: Vec<_> = cmd.get_args().collect();
352
353 if !output.status.success() || verbose {
354 println!("program: {program:?}");
355 println!("arguments: {arguments:#?}\n");
356
357 let stdout = String::from_utf8_lossy(&output.stdout);
358
359 if !stdout.trim().is_empty() {
360 println!("stdout:'{}'\n", stdout.trim());
361 }
362 }
363
364 if !output.status.success() {
365 let stderr = String::from_utf8_lossy(&output.stderr);
366 let status = output.status;
367
368 eprintln!("{msg} status: {status}");
369 eprintln!("{msg} stderr: {stderr}");
370
371 return Err(WallSwitchError::CommandFailed {
372 program: format!("{:?}", cmd.get_program()),
373 status: output.status.to_string(),
374 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
375 });
376 }
377
378 Ok(output)
379}