waywarp 0.1.5

A high-performance, keyboard-driven mouse control tool for Wayland compositors
#![allow(dead_code)]
use std::io::Read;
use std::process::{Command, Stdio};

#[derive(serde::Deserialize, Debug, Clone)]
pub struct ScannerElement {
    pub id: u32,
    #[serde(rename = "type")]
    pub element_type: String,
    pub text: String,
    pub monitor_index: u32,
    pub center: [i32; 2],
    pub bbox: [i32; 4],
}

#[derive(serde::Deserialize, Debug, Clone)]
pub struct ScannerOutput {
    pub screen_width: i32,
    pub screen_height: i32,
    pub elements: Vec<ScannerElement>,
}

pub fn run_visual_scan() -> anyhow::Result<ScannerOutput> {
    let mut child = Command::new("waywarp-scanner")
        .arg("scan")
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .map_err(|e| {
            if e.kind() == std::io::ErrorKind::NotFound {
                anyhow::anyhow!(
                    "waywarp-scanner not found in PATH.\n\
                     Please install it via: uv tool install waywarp-scanner"
                )
            } else {
                anyhow::anyhow!("Failed to spawn waywarp-scanner: {:?}", e)
            }
        })?;

    let start = std::time::Instant::now();
    let timeout = std::time::Duration::from_secs(10);
    let mut status = None;

    while start.elapsed() < timeout {
        if let Some(s) = child.try_wait()? {
            status = Some(s);
            break;
        }
        std::thread::sleep(std::time::Duration::from_millis(50));
    }

    if status.is_none() {
        let _ = child.kill();
        return Err(anyhow::anyhow!(
            "waywarp-scanner visual scan timed out after 10 seconds."
        ));
    }

    let exit_status = status.unwrap();
    if !exit_status.success() {
        let mut err_msg = String::new();
        if let Some(mut stderr) = child.stderr.take() {
            let _ = stderr.read_to_string(&mut err_msg);
        }
        return Err(anyhow::anyhow!(
            "waywarp-scanner exited with non-zero code {}.\nError: {}",
            exit_status.code().unwrap_or(-1),
            err_msg.trim()
        ));
    }

    let mut json_str = String::new();
    if let Some(mut stdout) = child.stdout.take() {
        stdout.read_to_string(&mut json_str)?;
    }

    let parsed: ScannerOutput = serde_json::from_str(&json_str).map_err(|e| {
        anyhow::anyhow!(
            "Failed to parse scanner output JSON: {:?}. Output was: {}",
            e,
            json_str
        )
    })?;

    Ok(parsed)
}

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

    #[test]
    fn test_scanner_json_deserialization() {
        let raw_json = r#"{
            "screen_width": 1920,
            "screen_height": 1080,
            "elements": [
                {
                    "id": 0,
                    "type": "button",
                    "text": "Login",
                    "monitor_index": 0,
                    "center": [100, 200],
                    "bbox": [80, 180, 40, 40]
                }
            ]
        }"#;
        let parsed: ScannerOutput = serde_json::from_str(raw_json).unwrap();
        assert_eq!(parsed.screen_width, 1920);
        assert_eq!(parsed.elements.len(), 1);
        assert_eq!(parsed.elements[0].text, "Login");
        assert_eq!(parsed.elements[0].center, [100, 200]);
    }
}