focus_by_cursor 0.1.0

Automatically focuses the yabai window under the mouse cursor, without clicks. Works exclusively on macOS.
Documentation
use rdev::{Event, EventType, ListenError, listen};
use serde::Deserialize;
use std::{
    process::Command,
    sync::{Arc, Mutex},
    time::{Duration, Instant},
};

/// Represents the frame rectangle of a window
#[derive(Deserialize)]
pub struct Frame {
    pub x: f64,
    pub y: f64,
    pub w: f64,
    pub h: f64,
}

/// Represents a window as returned by 'yabai -m query --windows'
#[derive(Deserialize)]
pub struct YabaiWindow {
    pub id: u32,
    pub frame: Frame,
}

/// Starts the global mouse listener and focuses windows under the cursor.
///
/// # Errors
/// Returns 'ListenError' if the global listener cannot be initialized.
pub fn run_focus_listener() -> Result<(), ListenError> {
    // Debounce so we only refocus every 50 ms
    let last_focus = Arc::new(Mutex::new(Instant::now() - Duration::from_secs(1)));
    let last_clone = Arc::clone(&last_focus);

    // Listen for global mouse-move events
    listen(move |event: Event| {
        if let EventType::MouseMove { x, y } = event.event_type {
            let mut last = last_clone.lock().unwrap();

            if last.elapsed() < Duration::from_millis(50) {
                return;
            }

            *last = Instant::now();

            // Query yabai for all windows
            if let Ok(out) = Command::new("yabai")
                .args(&["-m", "query", "--windows"])
                .output()
            {
                if !out.status.success() {
                    return;
                }

                // Parse JSON into Vec<YabaiWindow>
                if let Ok(wins) = serde_json::from_slice::<Vec<YabaiWindow>>(&out.stdout) {
                    let px = x as f64;
                    let py = y as f64;

                    // Find window whose frame contains the cursor
                    if let Some(w) = wins.iter().find(|w| {
                        px >= w.frame.x
                            && px <= w.frame.x + w.frame.w
                            && py >= w.frame.y
                            && py <= w.frame.y + w.frame.h
                    }) {
                        // Focus the window by its ID
                        let _ = Command::new("yabai")
                            .args(&["-m", "window", "--focus", &w.id.to_string()])
                            .status();
                    }
                }
            }
        }
    })?;

    Ok(())
}