ui-automation 0.1.1

Control the User Interface, cross-platform.
Documentation
// Copyright (C) 2024 Tristan Gerritsen <tristan@thewoosh.org>
// All Rights Reserved.

use std::{ffi::c_void, ptr::null};

use crate::{backend::macos::core_foundation::CFType, error::UIErrorKind, ApplicationId, MouseButton, Position, Size, UIResult};

use super::core_foundation::{CFArray, CFArrayIterator, CFArrayRef, CFDictionary, CFNumber, CFNumberType, CFString};

type CGFloat = f64;

pub struct CGEvent {
    inner: CFType,
}

#[allow(unused)]
impl CGEvent {
    pub fn from_state(state_source: CGEventSource) -> Option<Self> {
        let ptr = unsafe { CGEventCreate(state_source.inner.ptr()) };
        let inner = CFType::from_create(ptr)?;
        Some(Self { inner })
    }

    pub fn mouse_move(point: CGPoint) -> Option<Self> {
        let source = null();
        let ty = CGEventType::MouseMoved;
        let mouse = CGMouseType::Left;

        let ptr = unsafe { CGEventCreateMouseEvent(source, ty, point, mouse) };
        let inner = CFType::from_create(ptr)?;

        Some(Self { inner })
    }

    pub fn mouse_click(button: CGMouseType, down: bool, point: CGPoint) -> Option<Self> {
        let source = null();
        let ty = match (button, down) {
            (CGMouseType::Left, false) => CGEventType::LeftMouseUp,
            (CGMouseType::Left, true) => CGEventType::LeftMouseDown,
            (CGMouseType::Center, false) => CGEventType::OtherMouseUp,
            (CGMouseType::Center, true) => CGEventType::OtherMouseDown,
            (CGMouseType::Right, false) => CGEventType::RightMouseUp,
            (CGMouseType::Right, true) => CGEventType::RightMouseDown,
        };
        let mouse = CGMouseType::Left;

        let ptr = unsafe { CGEventCreateMouseEvent(source, ty, point, mouse) };
        let inner = CFType::from_create(ptr)?;

        Some(Self { inner })
    }

    pub fn set_type(&mut self, ty: CGEventType) {
        unsafe {
            CGEventSetType(self.inner.ptr(), ty);
        }
    }

    #[must_use]
    pub fn with_type(mut self, ty: CGEventType) -> Self {
        self.set_type(ty);
        self
    }

    pub fn mouse_location(&self) -> CGPoint {
        unsafe { CGEventGetLocation(self.inner.ptr()) }
    }

    pub fn post(self) {
        unsafe {
            CGEventPost(CGEventTapLocation::HIDEventTap, self.inner.ptr());
        }
    }
}

pub struct CGEventSource {
    inner: CFType,
}

impl CGEventSource {
    pub fn new(source: CGEventSourceStateID) -> Option<Self> {
        let ptr = unsafe { CGEventSourceCreate(source) };
        let inner = CFType::from_create(ptr)?;
        Some(Self { inner })
    }

    pub fn combined_state() -> Option<Self> {
        Self::new(CGEventSourceStateID::CombinedSessionState)
    }
}

#[derive(Debug, Default, Clone, Copy, PartialEq)]
#[repr(C)]
pub struct CGPoint {
    pub x: CGFloat,
    pub y: CGFloat,
}

impl From<Position> for CGPoint {
    fn from(value: Position) -> Self {
        Self {
            x: value.x(),
            y: value.y(),
        }
    }
}

impl From<CGPoint> for Position {
    fn from(value: CGPoint) -> Self {
        Self::new(value.x, value.y)
    }
}

#[derive(Debug, Default, Clone, Copy, PartialEq)]
#[repr(C)]
pub struct CGSize {
    pub width: CGFloat,
    pub height: CGFloat,
}

impl From<Size> for CGSize {
    fn from(value: Size) -> Self {
        Self {
            width: value.width(),
            height: value.height(),
        }
    }
}

impl From<CGSize> for Size {
    fn from(value: CGSize) -> Self {
        Self::new(value.width, value.height)
    }
}

#[derive(Debug, Default, Clone, Copy, PartialEq)]
#[repr(C)]
pub struct CGRect {
    pub origin: CGPoint,
    pub size: CGSize,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
#[allow(unused)]
pub enum CGEventType {
    Null = 0,
    LeftMouseDown = 1,
    LeftMouseUp = 2,
    RightMouseDown = 3,
    RightMouseUp = 4,
    MouseMoved = 5,
    LeftMouseDragged = 6,
    RightMouseDragged = 7,

    OtherMouseDown = 25,
    OtherMouseUp = 26,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
#[allow(unused)]
pub enum CGMouseType {
    Left = 0,
    Right = 1,
    Center = 2,
}

impl From<MouseButton> for CGMouseType {
    fn from(value: MouseButton) -> Self {
        match value {
            MouseButton::Left => Self::Left,
            MouseButton::Middle => Self::Center,
            MouseButton::Right => Self::Right,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
#[allow(unused)]
pub enum CGEventTapLocation {
    HIDEventTap = 0,
    SessionEventTap = 1,
    AnnotatedSessionEventTap = 2,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
#[allow(unused)]
pub enum CGEventSourceStateID {
    Private = -1,
    CombinedSessionState = 0,
    HIDSystemState = 1,
}

pub type CGEventSourceRef = *const c_void;
pub type CGEventRef = *const c_void;

pub type CGWindowListOption = u32;
pub type CGWindowId = u32;

pub(super) struct CGWindowList {
    array: CFArrayIterator<CFDictionary<CFString>>,
}

impl CGWindowList {
    pub fn create() -> UIResult<Self> {
        Self::new(None)
    }

    pub fn new(relative_to: Option<ApplicationId>) -> UIResult<Self> {
        let option = CGWindowListOptionType::ALL;

        let relative_to = relative_to.unwrap_or(ApplicationId(0)).0 as u32;
        let array = unsafe { CGWindowListCopyWindowInfo(option, relative_to) };
        let Some(inner) = CFType::from_create(array) else {
            return Err(UIErrorKind::AllocationFailed { resource: "CGWindowListCopyWindowInfo" }.into());
        };

        let array = CFArray::from_ptr(inner);

        Ok(Self {
            array: array.iter(),
        })
    }
}

impl Iterator for CGWindowList {
    type Item = CGWindowInformation;

    fn next(&mut self) -> Option<Self::Item> {
        Some(CGWindowInformation {
            dict: self.array.next()?,
        })
    }
}

pub struct CGWindowInformation {
    pub(crate) dict: CFDictionary<CFString>,
}

#[allow(unused)]
impl CGWindowInformation {
    pub fn create_description(id: CGWindowId) -> UIResult<Self> {
        let Some(ids) = CFArray::from_slice(&[id]) else {
            return Err(UIErrorKind::AllocationFailed { resource: "CGArray" }.into());
        };

        debug_assert_eq!(ids.len(), 1);

        let Some(ptr) = CFType::from_create(unsafe { CGWindowListCreateDescriptionFromArray(ids.inner.ptr()) }) else {
            return Err(UIErrorKind::AllocationFailed { resource: "CGWindowListCreateDescriptionFromArray" }.into());
        };

        let dict_array = CFArray::from_ptr(ptr);

        let Some(dict_ref) = dict_array.get_ptr(0) else {
            return Err(UIErrorKind::AllocationFailed { resource: "CGWindowListDescription" }.into());
        };

        let dict = CFDictionary::from_get(dict_ref)?;
        Ok(Self { dict })
    }

    pub fn owner_pid(&self) -> Option<u32> {
        let key = CFString::from("kCGWindowOwnerPID");
        let value: CFNumber = self.dict.get(&key)?;
        let value = value.get(CFNumberType::Int)?;
        Some(value as _)
    }

    pub fn number(&self) -> Option<u32> {
        let key = CFString::new("kCGWindowNumber")?;
        self.dict.get::<u32>(&key)
    }

    pub fn owner_name(&self) -> UIResult<String> {
        let Some(key) = CFString::new("kCGWindowOwnerName") else {
            return Err(UIErrorKind::AllocationFailed { resource: "CFString of kCGWindowOwnerName" }.into());
        };

        let Some(value) = self.dict.get::<CFString>(&key) else {
            return Ok(String::new());
        };

        value.to_string()
    }
}

struct CGWindowListOptionType;
#[allow(unused)]
impl CGWindowListOptionType {
    const ALL: u32 = 0;
    const ON_SCREEN_ONLY: u32 = 1;
    const ON_SCREEN_ABOVE_WINDOW: u32 = 2;
    const ON_SCREEN_BELOW_WINDOW: u32 = 4;
    const INCLUDING_WINDOW: u32 = 8;
    const EXCLUDE_DESKTOP_ELEMENTS: u32 = 16;
}

#[allow(unused)]
extern "C-unwind" {
fn CGEventCreateMouseEvent(source: CGEventSourceRef, ty: CGEventType, point: CGPoint, mouse: CGMouseType) -> CGEventRef;
fn CGEventSetType(event: CGEventRef, ty: CGEventType);
fn CGEventPost(tap: CGEventTapLocation, event: CGEventRef);
fn CGEventCreate(source: CGEventSourceRef) -> CGEventRef;
fn CGEventGetLocation(event: CGEventRef) -> CGPoint;

fn CGEventSourceCreate(state_id: CGEventSourceStateID) -> CGEventSourceRef;
fn CGWindowListCreate(option: CGWindowListOption, relative_to_window: CGWindowId) -> CFArrayRef;
fn CGWindowListCreateDescriptionFromArray(window_array: CFArrayRef) -> CFArrayRef;
fn CGWindowListCopyWindowInfo(option: CGWindowListOption, relative_to: CGWindowId) -> CFArrayRef;
}

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

    #[test]
    fn test_cg_window_list() {
        let _: Vec<_> = CGWindowList::create().unwrap().collect();
    }
}