wallswitch 0.57.4

randomly selects wallpapers for multiple monitors
Documentation
use crate::{WallSwitchError, WallSwitchResult};
use std::path::PathBuf;

/// Helper to check if a command exists in the system PATH.
///
/// Uses the pure-Rust `which` crate for extreme performance and safety,
/// avoiding the overhead of spawning shell processes.
pub fn is_installed(binary: &str) -> bool {
    which::which(binary).is_ok()
}

/// Locate the exact absolute path for a system binary.
///
/// Returns a `WallSwitchResult<PathBuf>` which gracefully fails
/// if the command is not found in the user's `$PATH`.
pub fn where_is(cmd: &str) -> WallSwitchResult<PathBuf> {
    which::which(cmd).map_err(|_| WallSwitchError::UnableToFind(cmd.to_string()))
}

/// Get the 'magick' (or legacy 'convert') binary path.
pub fn get_magick_path(verbose: bool) -> WallSwitchResult<PathBuf> {
    let path = where_is("magick").or_else(|_| where_is("convert"));

    match path {
        Ok(magick_path) => {
            if verbose {
                println!("ImageMagick found at: {}", magick_path.display());
            }
            Ok(magick_path)
        }
        Err(_) => Err(WallSwitchError::UnableToFind(
            "magick (or convert)".to_string(),
        )),
    }
}

/// Get the 'identify' binary path (part of ImageMagick), used by metadata.rs.
pub fn get_identify_path(verbose: bool) -> WallSwitchResult<PathBuf> {
    match where_is("identify") {
        Ok(identify_path) => {
            if verbose {
                println!(
                    "ImageMagick 'identify' found at: {}",
                    identify_path.display()
                );
            }
            Ok(identify_path)
        }
        Err(_) => Err(WallSwitchError::UnableToFind("identify".to_string())),
    }
}

/// Get the 'feh' binary path, used by the Openbox/X11 backend.
pub fn get_feh_path(verbose: bool) -> WallSwitchResult<PathBuf> {
    match where_is("feh") {
        Ok(feh_path) => {
            if verbose {
                println!("feh found at: {}", feh_path.display());
            }
            Ok(feh_path)
        }
        Err(_) => Err(WallSwitchError::UnableToFind("feh".to_string())),
    }
}

/// Get the 'awww' binary path, used by the generic Wayland backend.
///
/// If 'awww' is not found, it returns a highly descriptive error message
/// containing installation instructions across multiple Linux distributions
/// and source compilation.
pub fn get_awww_path(verbose: bool) -> WallSwitchResult<PathBuf> {
    match where_is("awww") {
        Ok(awww_path) => {
            if verbose {
                println!("awww daemon found at: {}", awww_path.display());
            }
            Ok(awww_path)
        }
        Err(_) => {
            let install_instructions = "\
                The 'awww' wallpaper daemon was not found on your system.\n\n\
                Please install it to enable modern Wayland transitions:\n\n\
                - Arch Linux / Manjaro (AUR):\n    \
                    paru -S awww  (or yay -S awww)\n\n\
                - Debian / Ubuntu:\n    \
                    Download the latest .deb from the GitHub releases page.\n\n\
                - Fedora / RPM-based:\n    \
                    Download the latest .rpm from the GitHub releases page.\n\n\
                - Compile from Source (Any distro):\n    \
                    git clone https://codeberg.org/LGFae/awww.git\n    \
                    cd awww\n    \
                    cargo build --release\n    \
                    cargo install --path .\n"
                .to_string();

            // Pass the detailed multi-line instruction into the existing UnableToFind error.
            Err(WallSwitchError::UnableToFind(install_instructions))
        }
    }
}

// ==============================================================================
// TESTS
// ==============================================================================

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    /// Tests the `is_installed` function using standard Unix binaries
    /// that are guaranteed to exist on almost any Linux/macOS system.
    fn test_is_installed() {
        // 'ls' or 'sh' should practically always exist
        assert!(
            is_installed("sh") || is_installed("ls"),
            "Standard shell utilities should be detected"
        );

        // A highly improbable binary name to verify the negative case
        assert!(
            !is_installed("some_non_existent_binary_xyz_123"),
            "Non-existent binaries should return false"
        );
    }

    #[test]
    /// Tests the path retrieval logic.
    /// Since we can't guarantee 'feh' or 'magick' is installed on the CI machine,
    /// we just check that the function returns a known domain error if it fails,
    /// rather than panicking.
    fn test_get_paths_error_handling() {
        let feh_result = get_feh_path(false);

        if let Err(err) = feh_result {
            // If it fails, it MUST be our specific domain error
            assert!(
                matches!(err, WallSwitchError::UnableToFind(_)),
                "Expected UnableToFind error, got {:?}",
                err
            );
        }
    }
}