user-idle3 0.7.0

Get a user's idle time.
Documentation
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::OnceLock;
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

use crate::Error;

static LAST_INPUT_TIME: AtomicU64 = AtomicU64::new(0);
static LISTENER_STARTED: OnceLock<()> = OnceLock::new();
static START_TIME: OnceLock<Instant> = OnceLock::new();

fn current_time_millis() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or(Duration::ZERO)
        .as_millis() as u64
}

fn update_last_input_time() {
    LAST_INPUT_TIME.store(current_time_millis(), Ordering::Relaxed);
}

fn ensure_listener_started() {
    LISTENER_STARTED.get_or_init(|| {
        START_TIME.get_or_init(Instant::now);
        LAST_INPUT_TIME.store(current_time_millis(), Ordering::Relaxed);

        thread::spawn(|| {
            if let Err(e) = run_evdev_listener() {
                eprintln!("evdev listener error: {:?}", e);
            }
        });
    });
}

fn run_evdev_listener() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let devices = evdev::enumerate().collect::<Vec<_>>();

    for (path, device) in devices {
        if device.supported_events().contains(evdev::EventType::KEY)
            || device.supported_events().contains(evdev::EventType::RELATIVE)
            || device.supported_events().contains(evdev::EventType::ABSOLUTE)
        {
            thread::spawn(move || {
                if let Ok(mut dev) = evdev::Device::open(&path) {
                    loop {
                        match dev.fetch_events() {
                            Ok(events) => {
                                for _ in events {
                                    update_last_input_time();
                                }
                            }
                            Err(_) => break,
                        }
                    }
                }
            });
        }
    }

    loop {
        thread::sleep(Duration::from_secs(60));
    }
}

pub fn get_idle_time() -> Result<Duration, Error> {
    ensure_listener_started();

    let last_input = LAST_INPUT_TIME.load(Ordering::Relaxed);
    let now = current_time_millis();

    if last_input == 0 {
        let start = START_TIME
            .get()
            .ok_or_else(|| Error::new("Start time not initialized"))?;
        return Ok(start.elapsed());
    }

    if now >= last_input {
        Ok(Duration::from_millis(now - last_input))
    } else {
        Ok(Duration::ZERO)
    }
}

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

    const TEST_SECS: u64 = 5;

    #[test]
    fn test_evdev_idle_time() {
        ensure_listener_started();
        sleep(Duration::from_millis(500));

        let initial_idle = get_idle_time().unwrap();
        println!("Initial idle after listener start: {:?}", initial_idle);

        println!("Sleeping for {} seconds... (don't touch mouse/keyboard)", TEST_SECS);
        sleep(Duration::from_secs(TEST_SECS));

        let idle = get_idle_time().unwrap();
        let idle_secs = idle.as_secs();
        println!("Idle after sleep: {:?} ({} seconds)", idle, idle_secs);
        println!("LAST_INPUT_TIME: {}", LAST_INPUT_TIME.load(Ordering::Relaxed));
        println!("current_time_millis: {}", current_time_millis());

        assert!(
            idle_secs >= TEST_SECS - 1,
            "Expected idle time >= {} seconds, got {} seconds. If you moved mouse/keyboard, this is expected.",
            TEST_SECS - 1,
            idle_secs
        );
    }
}