mod args;
mod awww;
mod backends;
mod colors;
mod config;
mod dependencies;
mod desktop;
mod detector;
mod dimension;
mod environment;
mod error;
mod fileinfo;
mod list;
mod monitors;
mod orientation;
mod pids;
mod random;
mod state;
mod traits;
mod walkdir;
pub use self::{
args::*, awww::*, backends::*, colors::*, config::*, dependencies::*, desktop::*, detector::*,
dimension::*, environment::*, error::*, fileinfo::*, list::*, monitors::*, orientation::*,
pids::*, random::*, state::*, traits::*, walkdir::*,
};
use std::{
env,
io::{self, Write}, sync::atomic::{AtomicUsize, Ordering}, thread,
};
pub fn show_initial_msgs(config: &Config) -> WallSwitchResult<()> {
let pkg_name = ENVIRON.get_pkg_name();
let pkg_desc = env!("CARGO_PKG_DESCRIPTION");
let pkg_version = env!("CARGO_PKG_VERSION");
let interval = config.interval;
let info = format!("Interval between each wallpaper: {interval} seconds.");
let author = "Claudio Fernandes de Souza Rodrigues (claudiofsrodrigues@gmail.com)";
println!("{pkg_name} {pkg_desc}\n{info}\n{author}");
println!("version: {pkg_version}\n");
let depend1 = "imagemagick (image viewing/manipulation program)";
let depend2 = "feh (fast and light image viewer for X11/Openbox)";
let depend3 = "awww (animated Wayland wallpaper daemon)";
let depend4 = "swaybg (wallpaper utility for Wayland compositors)";
let depend5 = "hyprpaper (wallpaper utility for Hyprland)";
let dependencies = [depend1, depend2, depend3, depend4, depend5];
println!("Dependencies:");
dependencies.print_with_spaces(" ");
println!();
config.print()?;
Ok(())
}
pub fn get_images(config: &Config, state: &mut State) -> WallSwitchResult<Vec<FileInfo>> {
let images: Vec<FileInfo> = gather_files(config, state)?;
if images.is_empty() {
let directories = config.directories.clone();
return Err(WallSwitchError::NoImages { paths: directories });
}
let mut pool: Vec<FileInfo> = images
.iter()
.filter(|img| !state.history.contains(&img.path))
.cloned()
.collect();
let nimages: usize = pool.len();
if nimages < config.monitors.len() {
if config.verbose {
println!("Image pool exhausted. Resetting history cycle.");
}
state.history.clear();
pool = images.clone();
}
pool.update_number();
if !config.sort {
pool.shuffle();
}
Ok(pool)
}
pub fn gather_files(config: &Config, state: &mut State) -> WallSwitchResult<Vec<FileInfo>> {
state.garbage_collect();
let mut raw_files = Vec::new();
for dir in &config.directories {
raw_files.extend(get_files_from_directory(dir, config)?);
}
let mut needs_hash = Vec::new();
let mut cached_files = Vec::new();
for mut file in raw_files {
if let Some(cache) = state.hashes.get(&file.path)
&& cache.size == file.size
&& cache.mtime == file.mtime
{
file.hash = cache.hash.clone();
file.dimension = cache.dimension.clone();
cached_files.push(file);
continue;
}
needs_hash.push(file);
}
if !needs_hash.is_empty() {
if config.verbose {
println!(
"Calculating deep BLAKE3 hashes for {} new/modified files...",
needs_hash.len()
);
}
needs_hash.update_hash()?;
}
for file in &needs_hash {
state.hashes.insert(
file.path.clone(),
CacheEntry {
size: file.size,
mtime: file.mtime,
hash: file.hash.clone(),
dimension: file.dimension.clone(),
},
);
}
let all_files = cached_files.into_iter().chain(needs_hash);
let mut files = Vec::new();
let mut seen_hashes = std::collections::HashSet::new();
for file in all_files {
if seen_hashes.insert(file.hash.clone()) {
files.push(file);
} else if config.verbose {
println!("Visual duplicate ignored: {:?}", file.path);
}
}
Ok(files)
}
pub fn display_files(files: &[FileInfo], config: &Config) {
let nfiles = files.len();
if nfiles == 0 {
return;
}
let ndigits = nfiles.to_string().len();
if config.sort {
println!(
"\n{} images were found (sorted):",
nfiles.to_string().green().bold()
);
} else {
println!(
"\n{} images were found (shuffled):",
nfiles.to_string().green().bold()
);
}
for file in files {
println!(
"images[{n:0ndigits$}/{t}]: {p:?}",
n = file.number,
p = file.path,
t = file.total,
);
}
println!();
}
pub fn update_images(files: &[FileInfo], config: &Config, state: &mut State) -> Vec<FileInfo> {
let mut owned_files: Vec<FileInfo> = files.to_vec();
let mut needs_update: Vec<&mut FileInfo> = owned_files
.iter_mut()
.filter(|file| file.dimension.is_none())
.collect();
if !needs_update.is_empty() {
let total_to_probe = needs_update.len();
let counter = AtomicUsize::new(0);
if config.verbose {
println!("Probing dimensions for {} new files...", total_to_probe);
}
let chunk_size = needs_update.get_chunk_size(total_to_probe);
thread::scope(|scope| {
for chunk in needs_update.chunks_mut(chunk_size) {
scope.spawn(|| {
for file in chunk {
if file.update_info(config).is_ok() {
let current = counter.fetch_add(1, Ordering::SeqCst) + 1;
let file_name =
file.path.file_name().unwrap_or_default().to_string_lossy();
let msg = format!(
"Probing image [{current: >4}/{total_to_probe}] : {file_name}"
)
.to_line_start();
print!("{msg}");
let _ = io::stdout().flush();
}
}
});
}
});
println!("\nProbing completed.\n");
let mut state_changed = false;
for file in &owned_files {
if let Some(dim) = &file.dimension
&& let Some(entry) = state.hashes.get_mut(&file.path)
&& entry.dimension.is_none()
{
entry.dimension = Some(dim.clone());
state_changed = true;
}
}
if state_changed {
let _ = state.save();
}
}
owned_files
}
#[cfg(test)]
mod test_lib {
use crate::*;
#[test]
fn vec_shuffle() {
let mut vec: Vec<u32> = (1..=100).collect();
vec.shuffle();
println!("vec: {vec:?}");
assert_eq!(vec.len(), 100);
}
#[test]
fn random_integers_v1() {
let value: u64 = get_random_integer(1, 20);
println!("integer: {value:?}");
let integers: Vec<u64> = (0..100).map(|_| get_random_integer(1, 20)).collect();
println!("integers: {integers:?}");
let condition_a = integers.iter().min() >= Some(&1);
let condition_b = integers.iter().max() <= Some(&20);
assert!(condition_a);
assert!(condition_b);
assert_eq!(integers.len(), 100);
}
#[test]
fn random_integers_v2() -> WallSwitchResult<()> {
let value: u64 = get_random_integer_safe(1, 20)?;
println!("integer: {value:?}");
let integers: Vec<u64> = (0..100)
.map(|_| get_random_integer_safe(1, 20))
.collect::<Result<Vec<u64>, _>>()?;
println!("integers: {integers:?}");
let condition_a = integers.iter().min() >= Some(&1);
let condition_b = integers.iter().max() <= Some(&20);
assert!(condition_a);
assert!(condition_b);
assert_eq!(integers.len(), 100);
Ok(())
}
#[test]
fn random_integers_v3() -> WallSwitchResult<()> {
let result = get_random_integer_safe(21, 20).map_err(|err| {
eprintln!("{err}");
err
});
assert!(result.is_err());
let error = result.unwrap_err();
eprintln!("error: {error:?}");
Ok(())
}
}