wallswitch 0.55.0

randomly selects wallpapers for multiple monitors
Documentation
use crate::{
    Config, FileInfo, WallSwitchError, WallSwitchResult, dependencies::is_installed, exec_cmd,
};
use std::{
    env, fs,
    io::{self, Write},
    path::PathBuf,
    process::{Command, Stdio},
    thread::sleep,
    time::Duration,
};

/// Orchestrates wallpaper application using the `awww` Wayland daemon.
/// Handles daemon initialization, cleanup, and transitions.
pub fn set_awww_wallpaper(images: &[FileInfo], config: &Config) -> WallSwitchResult<()> {
    // 1. Detect active Wayland monitors
    let monitors = get_wayland_monitors(config)?;

    // 2. Self-healing check: Ensure awww-daemon is running correctly
    ensure_daemon_running(config)?;

    // 3. Apply Wallpapers
    for (image, monitor) in images.iter().zip(monitors.iter()) {
        let effect = get_transition_effect(config);

        let mut cmd = Command::new("awww");
        cmd.args(["img", "-o", monitor])
            .arg(&image.path)
            .args(["--transition-type", &effect])
            .args([
                "--transition-duration",
                &config.transition_duration.to_string(),
            ])
            .args(["--transition-fps", &config.transition_fps.to_string()])
            .args(["--transition-angle", &config.transition_angle.to_string()])
            .args(["--transition-pos", &config.transition_pos]);

        if config.dry_run {
            println!("[DRY-RUN] Would execute: {:?}", cmd);
        } else {
            let msg = format!("Apply awww wallpaper on {}", monitor);
            exec_cmd(&mut cmd, config.verbose, &msg)?;
        }
    }

    Ok(())
}

/// Chooses a transition effect. Picks a random one if configured to "random".
fn get_transition_effect(config: &Config) -> String {
    if config.transition_type.to_lowercase() == "random" {
        let effects = ["wipe", "fade", "center", "outer", "wave", "left", "right"];
        let idx = (crate::rand() as usize) % effects.len();
        effects[idx].to_string()
    } else {
        config.transition_type.clone()
    }
}

/// Guarantees that `awww-daemon` is fully functional.
/// It automatically cleans lingering socket files that block the daemon from restarting.
fn ensure_daemon_running(config: &Config) -> WallSwitchResult<()> {
    if is_daemon_alive() {
        return Ok(());
    }

    if config.verbose {
        println!("awww-daemon is down. Performing clean start...");
    }

    // Attempt to kill unresponsive daemon before cleaning up
    let _ = Command::new("killall").arg("awww-daemon").output();

    // The most common error in `awww` is stale unix sockets (.sock).
    // We must manually clean them up in the XDG runtime directory.
    clean_stale_sockets();

    // Start daemon silently in the background
    Command::new("awww-daemon")
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
        .map_err(|e| WallSwitchError::AwwwDaemonError(e.to_string()))?;

    // Polling loop: Wait until the daemon responds to queries
    let mut elapsed = 0.0;
    let step = 0.2;
    let max_wait = 5.0;

    while elapsed < max_wait {
        if is_daemon_alive() {
            if config.verbose {
                println!("\nawww-daemon successfully initialized.");
            }
            return Ok(());
        }

        if config.verbose {
            // Usamos \r para sobrescrever a mesma linha no terminal (efeito de progresso)
            print!(
                "\rWait to initialize awww-daemon. Time: {:0.1}/{:0.1}",
                elapsed, max_wait
            );

            // Em Rust, o stdout é buffereado.
            // Precisamos de flush manual para exibir o print! imediatamente.
            io::stdout().flush().ok();
        }

        sleep(Duration::from_secs_f32(step));
        elapsed += step;
    }

    if config.verbose {
        println!();
    }

    Err(WallSwitchError::AwwwDaemonError(
        "Daemon failed to initialize within the timeout period.".into(),
    ))
}

/// Checks if the daemon is responding using its native query command.
fn is_daemon_alive() -> bool {
    Command::new("awww")
        .arg("query")
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
}

/// Deletes lingering Wayland socket files created by `awww-daemon` crashes.
fn clean_stale_sockets() {
    let runtime_dir = env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string());

    if let Ok(entries) = fs::read_dir(&runtime_dir) {
        for entry in entries.flatten() {
            let name = entry.file_name().to_string_lossy().to_string();
            // Awww socket names follow this pattern
            if name.contains("awww") && name.ends_with(".sock") {
                let _ = fs::remove_file(entry.path());
            }
        }
    }
}

/// Detects active outputs (monitors) universally on Linux/Wayland.
pub fn get_wayland_monitors(config: &Config) -> WallSwitchResult<Vec<String>> {
    // 1. Primary Route: Try `wlr-randr` for wlroots-based compositors
    if is_installed("wlr-randr")
        && let Ok(output) = Command::new("wlr-randr").output()
    {
        let stdout = String::from_utf8_lossy(&output.stdout);
        let monitors: Vec<String> = stdout
            .lines()
            .filter(|line| !line.starts_with(' ') && !line.is_empty())
            .map(|line| line.split_whitespace().next().unwrap_or("").to_string())
            .filter(|s| !s.is_empty())
            .collect();

        if !monitors.is_empty() {
            return Ok(monitors);
        }
    }

    // 2. Fallback Route: Query the Linux Kernel directly via DRM Sysfs
    // Highly resilient if the user doesn't have wlr-randr installed
    let drm_path = PathBuf::from("/sys/class/drm");
    let mut monitors = Vec::new();

    if drm_path.exists()
        && let Ok(entries) = fs::read_dir(drm_path)
    {
        for entry in entries.flatten() {
            let name = entry.file_name().to_string_lossy().to_string();
            // Find directories like "card1-DP-1"
            if name.starts_with("card") && name.contains('-') {
                let status_path = entry.path().join("status");
                if let Ok(status) = fs::read_to_string(status_path)
                    && status.trim() == "connected"
                    && let Some(idx) = name.find('-')
                {
                    monitors.push(name[idx + 1..].to_string());
                }
            }
        }
    }

    if !monitors.is_empty() {
        return Ok(monitors);
    }

    // 3. Absolute Fallback: Return safe defaults
    if config.verbose {
        println!("All Wayland monitor detection methods failed. Using default outputs.");
    }
    Ok(vec!["DP-1".to_string(), "DP-2".to_string()])
}