more_wallpapers/
lib.rs

1#![allow(clippy::tabs_in_doc_comments)]
2#![warn(unreachable_pub)]
3#![cfg_attr(all(doc, nightly), feature(doc_auto_cfg))]
4
5//! Yet another wallpaper crate, which can set a wallpapers per screen.
6//!
7//! The main feature over other crates like [wallpaper] or [wall] is the ability to set **different wallpapers** on different screens.
8//! Currently this feature is only implemented for some environments.
9//! Because of this you can enable the `fallback` feature,
10//! to use a [custom version](https://github.com/LuckyTurtleDev/wallpaper.rs) of the [wallpaper] crate as a fallback on unsupported environments.
11//! This means you can use the additonal features of this crate and
12//! still support a large amount of environments.
13//!
14//! Currently the following environments are supported:
15//!
16//! | environment | set wallpaper | set wallpaper per screen | requirements |
17//! --- | :---: | :---:| --- |
18//! |Windows                     | ✅ | ❌ | `features=["fallback"]`¹ |
19//! |MacOS                       | ✅ | ❌ | `features=["fallback"]`¹ |
20//! |X11³                        | ✅ | ✅ | [xwallpaper], [libxrandr]²|
21//! |Budgie(wayland)             | ✅ | ❌ | `features=["fallback"]`¹ |
22//! |Cinnamon⁴                   | ✅ | ✅ | [xwallpaper], [libxrandr]²|
23//! |Deepin(wayland)             | ✅ | ❌ | `features=["fallback"]`¹ |
24//! |GNOME(wayland)              | ✅ | ❌ | `features=["fallback"]`¹ |
25//! |KDE                         | ✅ | ✅ | |
26//! |Mate(wayland)               | ✅ | ❌ | `features=["fallback"]`¹ |
27//! |Sway                        | ✅ | ✅ |                          |
28//! |XFCE                        | ✅ | ✅ |                          |
29//!
30//! ¹ Please check also the requirements of the [wallpaper] crate.<br/>
31//! ² Normally already installed.<br/>
32//! ³ Wallpapers will be reset after restart. <br/>
33//! ⁴ Wallpapers will be reset to provided default after restart.
34//!
35//! The information about the currently supported features are also provided by the [`Environment`] enum.
36//!
37//! ## QuickStart / Examples:
38//! If you would like to set only a different wallpaper for each screen and don't care
39//! which wallpaper is used on which screen,
40//! you can use [`set_wallpapers_from_vec()`] or [`set_random_wallpapers_from_vec()`] (only aviable with the `rand` feature):
41//! ```no_run
42//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
43//! use more_wallpapers::Mode;
44//!
45//! let images = vec!["1.jpg", "/usr/share/wallpapers/2.jpg"];
46//! more_wallpapers::set_wallpapers_from_vec(images, "default.jpg", Mode::Crop)?;
47//! # Ok(())}
48//! ```
49//! The `"default.jpg"` is used as wallpaper for [inactive screens](Screen::active).
50//! If you do not know witch value you shoud use here, you can simple use the first elment of the images vec.
51//!
52//! For advanced wallpaper settings you can use the [`WallpaperBuilder`]:
53#![doc = doc_WallpaperBuilder_example!()]
54//!  [wallpaper]: https://crates.io/crates/wallpaper
55//!  [wall]: https://crates.io/crates/wall
56//!  [xwallpaper]: https://github.com/stoeckmann/xwallpaper
57//!  [libxrandr]: https://gitlab.freedesktop.org/xorg/app/xrandr
58//!  [dbus]: https://gitlab.freedesktop.org/dbus/dbus
59//!  [swaybg]: https://github.com/swaywm/swaybg
60
61macro_rules! doc_WallpaperBuilder_example {
62	() => {
63		r#"``` no_run
64# fn main() -> Result<(), Box<dyn std::error::Error>> {
65use more_wallpapers::{Mode, WallpaperBuilder};
66
67let fallback_images = vec!["/usr/share/wallpapers/1.jpg", "/usr/share/wallpapers/2.jpg"];
68let mut i = 0;
69WallpaperBuilder::new()?.set_wallpapers(|screen| {
70	i += 1;
71	if i == 1 {
72		return ("first.jpg".to_owned(), Mode::default());
73	}
74	if screen.name == "HDMI1" {
75		return ("/usr/share/wallpapers/hdmi.jpg".to_owned(), Mode::Fit);
76	}
77	(
78		fallback_images[i % fallback_images.len()].to_owned(),
79		Mode::Tile,
80	)
81})?;
82# Ok(())}
83```
84"#
85	};
86}
87
88macro_rules! doc_set_wallpapers_from_vec {
89		(fn) => {
90			concat!(doc_set_wallpapers_from_vec!(@private head), "set_wallpapers_from_vec", doc_set_wallpapers_from_vec!(@private body),
91		doc_set_wallpapers_from_vec!(@private tail))
92		};
93		(builder) => {
94			concat!(doc_set_wallpapers_from_vec!(@private head), "WallpaperBuilder", doc_set_wallpapers_from_vec!(@private body), "WallpaperBuilder::new()?.",
95		doc_set_wallpapers_from_vec!(@private tail))
96		};
97		(@private head) => {
98			r#"
99Set the background of all screens to the wallpapers of `wallpapers`.
100The wallpaper of `screen[i]` will be set to `wallpapers[i mod wallpapers.len()]`.
101The `default_wallpaper` param is used if the given `wallpapers` vec is empty and as wallpaper for [inactive screens](Screen::active).
102Return a vec, with dose inlcude the path of the Wallpapers,
103witch was set as background.
104If the same wallpaper was set multiple times to different screens,
105the return value does also include the wallpaper multiple times.
106```no_run
107# fn main() -> Result<(), Box<dyn std::error::Error>> {
108use more_wallpapers::{"#
109		};
110		(@private body) => {
111			r#", Mode};
112
113let images = vec!["1.jpg", "/usr/share/wallpapers/2.jpg"];
114let used_wallpapers = "#
115		};
116		(@private tail) => {
117			r#"set_wallpapers_from_vec(images, "default.png", Mode::Crop)?;
118println!("background was set to the following wallpapers {used_wallpapers:?}");
119# Ok(())}
120```"#
121		};
122	}
123
124mod error;
125
126use camino::{Utf8Path, Utf8PathBuf};
127#[cfg(target_os = "linux")]
128use error::load_env_var;
129#[cfg(target_os = "linux")]
130pub use error::CommandError;
131use error::Context;
132pub use error::WallpaperError;
133use std::io;
134use strum_macros::{Display, EnumString};
135
136#[cfg(feature = "rand")]
137use rand::{prelude::IteratorRandom, seq::SliceRandom};
138
139#[cfg(target_os = "linux")]
140mod linux;
141#[cfg(target_os = "linux")]
142use crate::linux::*;
143
144#[cfg(all(target_os = "windows", not(feature = "fallback")))]
145std::compile_error!("Windows does need the \"wallpaper\" feature");
146#[cfg(target_os = "windows")]
147mod windows;
148#[cfg(target_os = "windows")]
149use crate::windows::*;
150
151#[cfg(all(target_os = "macos", not(feature = "fallback")))]
152std::compile_error!("MacOS does need the \"wallpaper\" feature");
153#[cfg(target_os = "macos")]
154mod macos;
155#[cfg(target_os = "macos")]
156use crate::macos::*;
157
158/// define how the wallpaper will be stretch, zoom, repeated etc
159#[derive(Debug, Clone, Copy, Default, EnumString, Display, PartialEq, Eq)]
160#[strum(serialize_all = "lowercase")]
161pub enum Mode {
162	///center image witout zooming. Image is may not full visible. Empty space is filled with black.
163	Center,
164	///zoom image to match x and y size of display and keep aspect ratio. Some parts of the image is may cut off.
165	#[default]
166	Crop,
167	///zoom image to match match x or y size of the display, the other will be filled with a black bar at each side. All parts of the immages are visible.
168	Fit,
169	///zoom x and y independently from each other to match display size.
170	Stretch,
171	///Repeat the image until the Screen is filled. May not all parts of the image are visible.
172	Tile,
173}
174
175#[cfg(feature = "fallback")]
176impl From<Mode> for fallback::Mode {
177	fn from(mode: Mode) -> Self {
178		match mode {
179			Mode::Center => fallback::Mode::Center,
180			Mode::Crop => fallback::Mode::Crop,
181			Mode::Fit => fallback::Mode::Fit,
182			Mode::Stretch => fallback::Mode::Stretch,
183			Mode::Tile => fallback::Mode::Tile,
184		}
185	}
186}
187
188/// Represent the used operating system or desktop.
189/// Inform about supported features, at the curren environment.
190#[derive(Debug, Clone, Copy, Display, PartialEq, Eq)]
191#[strum(serialize_all = "lowercase")]
192#[non_exhaustive]
193pub enum Environment {
194	#[cfg(target_os = "linux")]
195	Cinnamon,
196	#[cfg(target_os = "linux")]
197	Kde,
198	#[cfg(target_os = "linux")]
199	Sway,
200	#[cfg(all(target_os = "linux", feature = "fallback"))]
201	LinuxFallback,
202	#[cfg(all(target_os = "macos", feature = "fallback"))]
203	MacOS,
204	#[cfg(all(target_os = "windows", feature = "fallback"))]
205	Windows,
206	#[cfg(target_os = "linux")]
207	X11,
208	#[cfg(target_os = "linux")]
209	Xfce,
210}
211impl Environment {
212	///return true, if the current environment does support various wallpaper on each screen
213	pub fn support_various_wallpaper(&self) -> bool {
214		match self {
215			#[cfg(target_os = "linux")]
216			Self::Cinnamon => true,
217			#[cfg(target_os = "linux")]
218			Self::Kde => true,
219			#[cfg(target_os = "linux")]
220			Self::Sway => true,
221			#[cfg(all(target_os = "linux", feature = "fallback"))]
222			Self::LinuxFallback => false,
223			#[cfg(all(target_os = "macos", feature = "fallback"))]
224			Self::MacOS => false,
225			#[cfg(all(target_os = "windows", feature = "fallback"))]
226			Self::Windows => false,
227			#[cfg(target_os = "linux")]
228			Self::X11 => true,
229			#[cfg(target_os = "linux")]
230			Self::Xfce => true,
231		}
232	}
233}
234
235/// include information about a connected screen
236#[derive(Clone, Debug)]
237pub struct Screen {
238	pub name: String,
239	/// current wallpaper of the screen
240	pub wallpaper: Option<Utf8PathBuf>,
241	/// current mode of the screen
242	pub mode: Option<Mode>,
243	/// indicates if screen is active.
244	/// A inactive screen is current disconneted or repesents a default for new connected screens or is a fallback after restart
245	pub active: bool,
246}
247
248///Builder for advance Wallpaper settings and informations.
249///This struct should not be stored for a long time, because it can become outdated if the user connect or disconnect monitors or change the Display settings.
250#[derive(Debug)]
251pub struct WallpaperBuilder {
252	screens: Vec<Screen>,
253	environment: Environment,
254}
255
256impl WallpaperBuilder {
257	pub fn new() -> Result<Self, WallpaperError> {
258		get_builder()
259	}
260
261	///Return the count of active screens. This does not include disable screens.
262	pub fn screen_count(&self) -> usize {
263		self.screens.len()
264	}
265
266	///Return the count of active screens. This does not include disable screens.
267	pub fn active_screen_count(&self) -> usize {
268		self.screens.iter().filter(|screen| screen.active).count()
269	}
270
271	///Return the current desktop environment.
272	pub fn environment(&self) -> Environment {
273		self.environment
274	}
275
276	///Return a vec including all detected Screens
277	pub fn screens(&self) -> &Vec<Screen> {
278		&self.screens
279	}
280
281	///Set background to wallpapers, witch will be selected by the given closure.
282	///The index oft screen and the current screen are passed to the closure.x
283	#[doc = doc_WallpaperBuilder_example!()]
284	pub fn set_wallpapers<F, P>(mut self, mut f: F) -> Result<(), WallpaperError>
285	where
286		P: AsRef<Utf8Path>,
287		F: FnMut(&Screen) -> (P, Mode),
288	{
289		for screen in self.screens.iter_mut() {
290			let tuple = f(screen);
291			let path = tuple.0.as_ref();
292			let path = path.canonicalize_utf8().context(path)?;
293			if !path.exists() {
294				return Err(io::Error::from(io::ErrorKind::NotFound)).context(path);
295			}
296			screen.wallpaper = Some(path);
297			screen.mode = Some(tuple.1)
298		}
299		set_screens_from_builder(self)
300	}
301
302	#[doc = doc_set_wallpapers_from_vec!(builder)]
303	pub fn set_wallpapers_from_vec<P>(
304		self,
305		wallpapers: Vec<P>,
306		default_wallpaper: P,
307		mode: Mode,
308	) -> Result<Vec<Utf8PathBuf>, WallpaperError>
309	where
310		P: AsRef<Utf8Path>,
311	{
312		let mut used_wallpapers = Vec::new();
313		let mut i = 0;
314		self.set_wallpapers(|screen| {
315			if !screen.active {
316				return (default_wallpaper.as_ref(), mode);
317			}
318			let wallpaper = if wallpapers.is_empty() {
319				default_wallpaper.as_ref()
320			} else {
321				wallpapers[i % wallpapers.len()].as_ref()
322			};
323			i += 1;
324			used_wallpapers.push(wallpaper.to_owned());
325			(wallpaper, mode)
326		})?;
327		Ok(used_wallpapers)
328	}
329
330	///Like [`Self::set_wallpapers_from_vec`],
331	///but map the wallpapers randomly to the screens.
332	///Selecting the same wallpaper multiple time will be avoid, if this is possible.
333	#[cfg(feature = "rand")]
334	pub fn set_random_wallpapers_from_vec<P>(
335		self,
336		wallpapers: Vec<P>,
337		default_wallpaper: P,
338		mode: Mode,
339	) -> Result<Vec<Utf8PathBuf>, WallpaperError>
340	where
341		P: AsRef<Utf8Path>,
342		P: Clone,
343	{
344		if wallpapers.is_empty() {
345			// set_wallpapers_from_vec() will deal the empty inupt
346			return self.set_wallpapers_from_vec(wallpapers, default_wallpaper, mode);
347		}
348		let mut rng = rand::thread_rng();
349		let wallpapers = if wallpapers.len() < self.screen_count() {
350			//extend vec to match length of screen_count
351			let mut new_wallpapers = Vec::new();
352			while new_wallpapers.len() < self.active_screen_count() {
353				let count = (self.screen_count() - new_wallpapers.len()).min(wallpapers.len());
354				let mut add = wallpapers.clone().into_iter().choose_multiple(&mut rng, count);
355				new_wallpapers.append(&mut add);
356			}
357			new_wallpapers
358		} else {
359			wallpapers
360		};
361		let mut choose_wallpapers = wallpapers.into_iter().choose_multiple(&mut rng, self.screen_count());
362		choose_wallpapers.shuffle(&mut rng);
363		self.set_wallpapers_from_vec(choose_wallpapers, default_wallpaper, mode)
364	}
365}
366
367#[doc = doc_set_wallpapers_from_vec!(fn)]
368pub fn set_wallpapers_from_vec<P>(
369	wallpapers: Vec<P>,
370	default_wallpaper: P,
371	mode: Mode,
372) -> Result<Vec<Utf8PathBuf>, WallpaperError>
373where
374	P: AsRef<Utf8Path>,
375{
376	let builder = WallpaperBuilder::new()?;
377	builder.set_wallpapers_from_vec(wallpapers, default_wallpaper, mode)
378}
379
380///Like [`set_wallpapers_from_vec`],
381///but map the wallpapers randomly to the screens.
382///Selecting the same wallpaper multiple time will be avoid, if this is possible.
383#[cfg(feature = "rand")]
384pub fn set_random_wallpapers_from_vec<P>(
385	wallpapers: Vec<P>,
386	default_wallpaper: P,
387	mode: Mode,
388) -> Result<Vec<Utf8PathBuf>, WallpaperError>
389where
390	P: AsRef<Utf8Path>,
391	P: Clone,
392{
393	let builder = WallpaperBuilder::new()?;
394	builder.set_random_wallpapers_from_vec(wallpapers, default_wallpaper, mode)
395}