wallswitch 0.56.1

randomly selects wallpapers for multiple monitors
Documentation
use crate::{
    ConcurrencyExt, Config, Countable, Dimension, DimensionError, WallSwitchError,
    WallSwitchResult, exec_cmd,
};
use blake3::Hasher;
use std::{
    fmt,
    fs::File,
    io::{BufReader, Read},
    path::PathBuf,
    process::Command,
    thread,
};

const BUFFER_SIZE: usize = 64 * 1024;

/// Image information
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub struct FileInfo {
    /// File number (index + 1)
    pub number: usize,
    /// Total File number
    pub total: usize,
    /// dimension: width x length of an image.
    pub dimension: Option<Dimension>,
    /// The size of the file, in bytes
    pub size: u64,
    /// Timestamp
    pub mtime: u64,
    /// AHash from Path
    pub hash: String,
    /// The image file path
    pub path: PathBuf,
}

impl FileInfo {
    /**
    Returns true if the given pattern matches a sub-slice of this path.

    Returns false if it does not.
    */
    pub fn path_contains(&self, string: &str) -> bool {
        match self.path.to_str() {
            Some(p) => p.contains(string),
            None => false,
        }
    }

    /// Update dimension field and valid_dimension field
    pub fn update_info(&mut self, config: &Config) -> WallSwitchResult<()> {
        // identify -format %wx%h image_file_path
        let mut cmd = Command::new("identify");
        let identify_cmd = cmd
            .arg("-format")
            .arg("%wx%h") // x separator
            .arg(&self.path);

        let identify_out = exec_cmd(identify_cmd, config.verbose, "identify")?;

        let sdt_output = String::from_utf8(identify_out.stdout)?;

        self.dimension = Some(Dimension::new(&sdt_output)?);

        Ok(())
    }

    /// Check if the dimension is valid.
    ///
    /// Returns false if the dimension hasn't been probed yet.
    pub fn dimension_is_valid(&self, config: &Config) -> bool {
        match &self.dimension {
            Some(dim) => {
                let is_valid = dim.is_valid(config);
                if !is_valid {
                    let dim_error = DimensionError::DimensionFormatError {
                        dimension: dim.clone(),
                        log_min: dim.get_log_min(config),
                        log_max: dim.get_log_max(config),
                        path: self.path.clone(),
                    };
                    eprintln!("{}", WallSwitchError::InvalidDimension(dim_error));
                }
                is_valid
            }
            None => false, // Invalid state if it reaches here without being probed
        }
    }

    /// Check if the size is valid.
    pub fn size_is_valid(&self, config: &Config) -> bool {
        self.size >= config.min_size && self.size <= config.max_size
    }

    pub fn name_is_valid(&self, config: &Config) -> bool {
        let is_valid = self.path.file_name() != config.wallpaper.file_name();

        if !is_valid && let Some(path) = self.path.file_name() {
            eprintln!("{}\n", WallSwitchError::InvalidFilename(path.into()));
        }

        is_valid
    }
}

/// FileInfo Extension
pub trait FileInfoExt {
    fn get_width_min(&self) -> Option<u64>;
    fn get_max_size(&self) -> Option<u64>;
    fn get_max_number(&self) -> Option<usize>;
    fn get_max_dimension(&self) -> Option<u64>;
    fn sizes_are_valid(&self, config: &Config) -> bool;
    fn update_number(&mut self);
    fn update_hash(&mut self) -> WallSwitchResult<()>;
}

impl FileInfoExt for [FileInfo] {
    fn get_width_min(&self) -> Option<u64> {
        self.iter()
            .filter_map(|f| f.dimension.as_ref().map(|d| d.width))
            .min()
    }

    fn get_max_size(&self) -> Option<u64> {
        self.iter().map(|file_info| file_info.size).max()
    }

    fn get_max_number(&self) -> Option<usize> {
        self.iter().map(|file_info| file_info.number).max()
    }

    fn get_max_dimension(&self) -> Option<u64> {
        self.iter()
            .filter_map(|f| f.dimension.as_ref().map(|d| d.maximum()))
            .max()
    }

    fn sizes_are_valid(&self, config: &Config) -> bool {
        self.iter().all(|file_info| {
            let is_valid = file_info.size_is_valid(config);

            if !is_valid {
                let size = file_info.size;

                let min_size = config.min_size;
                let max_size = config.max_size;

                let path = file_info.path.clone();

                // Print Indented file information
                print!("{}", SliceDisplay(self));

                eprintln!(
                    "{}",
                    WallSwitchError::InvalidSize {
                        min_size,
                        size,
                        max_size,
                    }
                );
                eprintln!("{}\n", WallSwitchError::DisregardPath(path));
            }

            is_valid
        })
    }

    /// Update FileInfo number field
    fn update_number(&mut self) {
        let total = self.len();
        self.iter_mut().enumerate().for_each(|(index, file)| {
            file.number = index + 1;
            file.total = total;
        });
    }

    /// Update FileInfo hash field utilizing Hardware-Aware Concurrency via ConcurrencyExt.
    fn update_hash(&mut self) -> WallSwitchResult<()> {
        // Use the trait method to determine how to split the work
        let chunk_size = self.get_chunk_size(self.len());

        // Parallelize the computation using std::thread::scope
        thread::scope(|scope| {
            for chunk in self.chunks_mut(chunk_size) {
                scope.spawn(move || {
                    // Sequential processing within this core's chunk
                    // prevents "Too many open files" (Error 24).
                    for file_info in chunk {
                        // let id = thread::current().id();
                        // println!("identifier thread: {id:?}");

                        if let Ok(file) = File::open(&file_info.path) {
                            let reader = BufReader::with_capacity(BUFFER_SIZE, file);
                            if let Ok(hash) = get_hash(reader) {
                                // println!("path: '{}' ; hash: '{}'", file_info.path.display(), hash);
                                file_info.hash = hash;
                            }
                        }
                    }
                });
            }
        });

        Ok(())
    }
}

/// Calculates the BLAKE3 hash from Path.
///
/// Extremely fast, hardware-accelerated cryptographic hash.
pub fn get_hash(mut reader: impl Read) -> WallSwitchResult<String> {
    let mut hasher = Hasher::new();
    let mut buffer = [0_u8; BUFFER_SIZE];

    loop {
        let count = reader.read(&mut buffer)?;
        if count == 0 {
            break;
        }
        hasher.update(&buffer[..count]);
    }

    // Retorna o hash hexadecimal em string (como no seu Python script)
    Ok(hasher.finalize().to_hex().to_string())
}

/// Implement fmt::Display for Slice `[T]`
///
/// <https://stackoverflow.com/questions/30633177/implement-fmtdisplay-for-vect>
///
/// <https://stackoverflow.com/questions/33759072/why-doesnt-vect-implement-the-display-trait>
///
/// <https://gist.github.com/hyone/d6018ee1ac8f9496fed839f481eb59d6>
pub struct SliceDisplay<'a>(pub &'a [FileInfo]);

impl fmt::Display for SliceDisplay<'_> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let digits_n: Option<usize> = self.0.get_max_number().map(|n| n.count_chars());
        let digits_s: Option<usize> = self.0.get_max_size().map(|s| s.count_chars());
        let digits_d: Option<usize> = self.0.get_max_dimension().map(|d| d.count_chars());

        match (digits_n, digits_s, digits_d) {
            (Some(num_digits_number), Some(num_digits_size), num_digits_dimension) => {
                let d_padding = num_digits_dimension.unwrap_or(4); // default padding if None

                for file in self.0 {
                    let dim_str = match &file.dimension {
                        Some(dim) => format!(
                            "Dimension {{ width: {width:>d$}, height: {height:>d$} }}",
                            width = dim.width,
                            height = dim.height,
                            d = d_padding,
                        ),
                        // Cleaner "Unprobed" or "Error" state display
                        None => format!(
                            "Dimension {{ {:>width$} }}",
                            "Pending probe",
                            width = d_padding * 2 + 13
                        ),
                    };

                    writeln!(
                        f,
                        "images[{number:0n$}/{t}]: {dim_str}, size: {size:>s$}, path: {p:?}",
                        number = file.number,
                        n = num_digits_number,
                        t = file.total,
                        size = file.size,
                        s = num_digits_size,
                        p = file.path,
                    )?;
                }
            }
            _ => return Err(std::fmt::Error),
        }

        Ok(())
    }
}

#[cfg(test)]
mod test_info {
    #[test]
    /// `cargo test -- --show-output get_min_value_of_vec`
    fn get_min_value_of_vec_v1() {
        let values: Vec<i32> = vec![5, 6, 8, 4, 2, 7];

        let min_value: Option<i32> = values.iter().min().copied();

        println!("values: {values:?}");
        println!("min_value: {min_value:?}");

        assert_eq!(min_value, Some(2));
    }

    #[test]
    /// `cargo test -- --show-output get_min_value_of_vec`
    ///
    /// <https://stackoverflow.com/questions/58669865/how-to-get-the-minimum-value-within-a-vector-in-rust>
    fn get_min_value_of_vec_v2() {
        let values: Vec<i32> = vec![5, 6, 8, 4, 2, 7];

        // The empty vector must be filtered beforehand!
        // let values: Vec<i32> = vec![]; // Not work!!!

        // Get the minimum value without being wrapped by Option<T>
        let min_value: i32 = values
            .iter()
            //.into_iter()
            //.fold(i32::MAX, i32::min);
            .fold(i32::MAX, |arg0: i32, other: &i32| i32::min(arg0, *other));

        println!("values: {values:?}");
        println!("min_value: {min_value}");

        assert_eq!(min_value, 2);
    }
}