zeuxis 0.1.0

Local read-only MCP screenshot server for screen/window/region capture
use device_query::{DeviceQuery, DeviceState};

use crate::{capture::region::Point, mcp::errors::ServerError};

pub trait CursorProvider: Send + Sync {
    fn cursor_position(&self) -> Result<Point, ServerError>;
}

#[derive(Debug, Clone, Default)]
pub struct DeviceQueryCursorProvider;

impl DeviceQueryCursorProvider {
    pub const fn new() -> Self {
        Self
    }
}

impl CursorProvider for DeviceQueryCursorProvider {
    fn cursor_position(&self) -> Result<Point, ServerError> {
        let state = DeviceState::checked_new()
            .ok_or_else(|| ServerError::cursor_unavailable(cursor_unavailable_message()))?;

        let mouse = state.get_mouse();
        Ok(Point::new(mouse.coords.0, mouse.coords.1))
    }
}

fn cursor_unavailable_message() -> &'static str {
    #[cfg(target_os = "linux")]
    {
        "cursor access is unavailable on this Linux session (common on Wayland without portal support); use capture_screen or capture_rect, or run get_runtime_diagnostics for details"
    }

    #[cfg(not(target_os = "linux"))]
    {
        "cursor access is unavailable (accessibility permission may be missing)"
    }
}

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

    #[test]
    fn cursor_provider_new_constructs_provider() {
        let _provider = DeviceQueryCursorProvider::new();
    }

    #[test]
    fn cursor_provider_cursor_position_returns_point_or_cursor_unavailable() {
        let provider = DeviceQueryCursorProvider::new();
        let _ = provider.cursor_position();
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn cursor_message_linux_includes_fallback_guidance() {
        let message = cursor_unavailable_message();
        assert!(message.contains("capture_screen"));
        assert!(message.contains("get_runtime_diagnostics"));
    }

    #[cfg(not(target_os = "linux"))]
    #[test]
    fn cursor_message_non_linux_mentions_accessibility() {
        let message = cursor_unavailable_message();
        assert!(message.contains("accessibility"));
    }
}