use crate::*;
use std::process;
use std::{
env,
io::{self, Write}, sync::atomic::{AtomicUsize, Ordering}, thread,
};
pub fn run() -> WallSwitchResult<()> {
let args = Arguments::build()?;
let mut state = State::load();
let config = Config::new(&args)?;
if let Some(criteria) = args.list {
match criteria {
SortCriteria::Processed | SortCriteria::Unprocessed | SortCriteria::Cache => {
list_json_cache(&state, criteria)?;
}
_ => {
let mut images = gather_files(&config, &mut state)?;
images = update_images(&images, &config, &mut state);
list_all_images(images, criteria)?;
}
}
process::exit(0);
}
show_initial_msgs(&config)?;
kill_other_instances(&config)?;
if config.once {
try_run_cycle(&config, &mut state)
} else {
loop {
try_run_cycle(&config, &mut state)?;
}
}
}
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 show_initial_msgs(config: &Config) -> WallSwitchResult<()> {
let env = Environment::new()?;
let pkg_name = env.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(())
}
fn try_run_cycle(config: &Config, state: &mut State) -> WallSwitchResult<()> {
let candidates = get_images(config, state)?;
let needed = config.get_number_of_images();
if config.verbose {
display_files(&candidates, config);
}
let batch_size = candidates.get_optimal_cores();
let mut valid_pool: Vec<FileInfo> = Vec::new();
let mut candidate_iter = candidates.into_iter();
while valid_pool.len() < needed {
let mut batch = Vec::new();
for _ in 0..batch_size {
if let Some(img) = candidate_iter.next() {
batch.push(img);
}
}
if batch.is_empty() {
break;
}
let probed_batch = update_images(&batch, config, state);
valid_pool.extend(
probed_batch
.into_iter()
.filter(|f| f.is_valid == Some(true)),
);
}
if valid_pool.len() >= needed {
let cycle_images: Vec<FileInfo> = valid_pool.drain(0..needed).collect();
if config.verbose {
println!(
"Quorum satisfied: {} valid images found using {} parallel threads.",
cycle_images.len(),
batch_size
);
}
print!("{}", SliceDisplay(&cycle_images));
println!();
set_wallpaper(&cycle_images, config)?;
for fig in &cycle_images {
state.history.push(fig.path.clone());
}
state.save()?;
if config.once {
return Ok(());
}
std::thread::sleep(std::time::Duration::from_secs(config.interval));
return try_run_cycle(config, state);
}
if !state.history.is_empty() {
if config.verbose {
println!(
"\nQuorum failed: Needed {}, but found only {}. Resetting history for a full disk search...",
needed,
valid_pool.len()
);
}
state.history.clear();
state.save()?;
return try_run_cycle(config, state);
}
Err(WallSwitchError::InsufficientNumber)
}
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 needed_images = config.get_number_of_images();
if pool.len() < needed_images {
if config.verbose {
println!(
"Image pool exhausted (less than {needed_images} unseen images). Resetting history cycle."
);
}
state.history.clear();
pool = images.clone();
}
pool.update_number();
if !config.sort {
pool.shuffle();
}
Ok(pool)
}
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() || file.is_valid.is_none())
.collect();
if !needs_update.is_empty() {
let total_to_probe = needs_update
.iter()
.filter(|f| f.dimension.is_none())
.count();
let counter = AtomicUsize::new(0);
if config.verbose && total_to_probe > 0 {
println!("Probing dimensions for {} new files...", total_to_probe);
}
let chunk_size = needs_update.get_chunk_size(needs_update.len());
thread::scope(|scope| {
for chunk in needs_update.chunks_mut(chunk_size) {
scope.spawn(|| {
chunk.iter_mut().for_each(|file| {
if file.dimension.is_none() && 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();
}
let dim_valid = file
.dimension
.as_ref()
.map(|d| d.is_valid(config))
.unwrap_or(false);
let size_valid = file.size_is_valid(config);
let name_valid = file.name_is_valid(config);
file.is_valid = Some(dim_valid && size_valid && name_valid);
});
});
}
});
if config.verbose && total_to_probe > 0 {
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(())
}
}