cueward-adapter-macos 0.5.0

macOS adapter for Cueward with Safari, Notes, Messages, Reminders, Calendar, Screenshot, Clipboard, and OCR integrations.
use std::process::Command;

use crate::MacosError;

use super::geometry::{StickyFrame, StickySize};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StickyDisplayBounds {
    pub frame: StickyFrame,
    pub visible_frame: StickyFrame,
}

impl StickyDisplayBounds {
    pub fn contains(self, frame: StickyFrame) -> bool {
        frame.x >= self.frame.x
            && frame.x < self.frame.x + self.frame.width
            && frame.y >= self.frame.y
            && frame.y < self.frame.y + self.frame.height
    }
}

pub fn resolve_frame_for_display(
    display: StickyDisplayBounds,
    size: StickySize,
    x: i32,
    y: i32,
) -> StickyFrame {
    StickyFrame {
        x: display.visible_frame.x + x,
        y: display.visible_frame.y + display.visible_frame.height - size.height - y,
        width: size.width,
        height: size.height,
    }
}

pub fn cascade_frame_for_display(
    display: StickyDisplayBounds,
    size: StickySize,
    existing: &[StickyFrame],
) -> StickyFrame {
    existing
        .iter()
        .copied()
        .max_by_key(|frame| (frame.x, frame.y))
        .map(|frame| frame.offset(24, -24).with_size(size))
        .unwrap_or_else(|| resolve_frame_for_display(display, size, 40, 40))
}

pub fn load_display_bounds() -> Result<Vec<StickyDisplayBounds>, MacosError> {
    let script = r#"import AppKit
for screen in NSScreen.screens {
    let f = screen.frame
    let v = screen.visibleFrame
    print("\(Int(f.origin.x))\t\(Int(f.origin.y))\t\(Int(f.size.width))\t\(Int(f.size.height))\t\(Int(v.origin.x))\t\(Int(v.origin.y))\t\(Int(v.size.width))\t\(Int(v.size.height))")
}"#;
    let output = Command::new("swift")
        .arg("-e")
        .arg(script)
        .output()
        .map_err(|err| MacosError::Other(format!("swift: {err}")))?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(MacosError::Other(format!(
            "failed to load display bounds: {stderr}"
        )));
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    stdout
        .lines()
        .map(|line| {
            let parts: Vec<_> = line.split('\t').collect();
            if parts.len() != 8 {
                return Err(MacosError::Other(format!(
                    "invalid display bounds output: {line}"
                )));
            }
            let ints: Result<Vec<i32>, _> = parts.iter().map(|part| part.parse::<i32>()).collect();
            let ints = ints.map_err(|_| {
                MacosError::Other(format!("invalid display bounds output: {line}"))
            })?;

            Ok(StickyDisplayBounds {
                frame: StickyFrame {
                    x: ints[0],
                    y: ints[1],
                    width: ints[2],
                    height: ints[3],
                },
                visible_frame: StickyFrame {
                    x: ints[4],
                    y: ints[5],
                    width: ints[6],
                    height: ints[7],
                },
            })
        })
        .collect()
}

pub fn find_display_bounds(
    displays: &[StickyDisplayBounds],
    index: u32,
) -> Result<StickyDisplayBounds, MacosError> {
    let zero_based = index
        .checked_sub(1)
        .ok_or_else(|| MacosError::Other(format!("invalid display number: {index}")))?;
    displays
        .get(zero_based as usize)
        .copied()
        .ok_or_else(|| MacosError::Other(format!("display not found: {index}")))
}