use core_graphics::event::{
CGEvent, CGEventFlags, CGEventTapLocation, CGEventType, CGKeyCode, CGMouseButton,
};
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use core_graphics::geometry::CGPoint;
use std::thread;
use std::time::Duration;
#[derive(Debug, Clone, Copy, Default)]
pub enum MouseButton {
#[default]
Left,
Right,
Center,
}
pub fn check_accessibility_permission() -> bool {
use core_foundation::base::TCFType;
use core_foundation::dictionary::CFDictionary;
use core_foundation::string::CFString;
#[link(name = "ApplicationServices", kind = "framework")]
extern "C" {
fn AXIsProcessTrustedWithOptions(options: core_foundation::base::CFTypeRef) -> bool;
}
let key = CFString::new("AXTrustedCheckOptionPrompt");
let value = core_foundation::boolean::CFBoolean::false_value();
let options = CFDictionary::from_CFType_pairs(&[(key.as_CFType(), value.as_CFType())]);
unsafe { AXIsProcessTrustedWithOptions(options.as_concrete_TypeRef() as _) }
}
pub fn move_mouse(x: f64, y: f64) -> Result<(), String> {
let source = CGEventSource::new(CGEventSourceStateID::HIDSystemState)
.map_err(|_| "Failed to create event source")?;
let point = CGPoint::new(x, y);
let event = CGEvent::new_mouse_event(
source,
CGEventType::MouseMoved,
point,
CGMouseButton::Left, )
.map_err(|_| "Failed to create mouse move event")?;
event.post(CGEventTapLocation::HID);
Ok(())
}
pub fn click(x: f64, y: f64, button: MouseButton, click_count: u32) -> Result<(), String> {
let source = CGEventSource::new(CGEventSourceStateID::HIDSystemState)
.map_err(|_| "Failed to create event source")?;
let point = CGPoint::new(x, y);
let (down_type, up_type, cg_button) = match button {
MouseButton::Left => (
CGEventType::LeftMouseDown,
CGEventType::LeftMouseUp,
CGMouseButton::Left,
),
MouseButton::Right => (
CGEventType::RightMouseDown,
CGEventType::RightMouseUp,
CGMouseButton::Right,
),
MouseButton::Center => (
CGEventType::OtherMouseDown,
CGEventType::OtherMouseUp,
CGMouseButton::Center,
),
};
for i in 0..click_count {
let down_event = CGEvent::new_mouse_event(source.clone(), down_type, point, cg_button)
.map_err(|_| "Failed to create mouse down event")?;
let up_event = CGEvent::new_mouse_event(source.clone(), up_type, point, cg_button)
.map_err(|_| "Failed to create mouse up event")?;
down_event.set_integer_value_field(
core_graphics::event::EventField::MOUSE_EVENT_CLICK_STATE,
(i + 1) as i64,
);
up_event.set_integer_value_field(
core_graphics::event::EventField::MOUSE_EVENT_CLICK_STATE,
(i + 1) as i64,
);
down_event.post(CGEventTapLocation::HID);
thread::sleep(Duration::from_millis(10));
up_event.post(CGEventTapLocation::HID);
if i < click_count - 1 {
thread::sleep(Duration::from_millis(50));
}
}
Ok(())
}
pub fn drag(
start_x: f64,
start_y: f64,
end_x: f64,
end_y: f64,
button: MouseButton,
) -> Result<(), String> {
let source = CGEventSource::new(CGEventSourceStateID::HIDSystemState)
.map_err(|_| "Failed to create event source")?;
let start_point = CGPoint::new(start_x, start_y);
let end_point = CGPoint::new(end_x, end_y);
let (down_type, drag_type, up_type, cg_button) = match button {
MouseButton::Left => (
CGEventType::LeftMouseDown,
CGEventType::LeftMouseDragged,
CGEventType::LeftMouseUp,
CGMouseButton::Left,
),
MouseButton::Right => (
CGEventType::RightMouseDown,
CGEventType::RightMouseDragged,
CGEventType::RightMouseUp,
CGMouseButton::Right,
),
MouseButton::Center => (
CGEventType::OtherMouseDown,
CGEventType::OtherMouseDragged,
CGEventType::OtherMouseUp,
CGMouseButton::Center,
),
};
let down_event = CGEvent::new_mouse_event(source.clone(), down_type, start_point, cg_button)
.map_err(|_| "Failed to create mouse down event")?;
down_event.post(CGEventTapLocation::HID);
thread::sleep(Duration::from_millis(10));
let steps = 10;
for i in 1..=steps {
let t = i as f64 / steps as f64;
let current_x = start_x + (end_x - start_x) * t;
let current_y = start_y + (end_y - start_y) * t;
let current_point = CGPoint::new(current_x, current_y);
let drag_event =
CGEvent::new_mouse_event(source.clone(), drag_type, current_point, cg_button)
.map_err(|_| "Failed to create drag event")?;
drag_event.post(CGEventTapLocation::HID);
thread::sleep(Duration::from_millis(10));
}
let up_event = CGEvent::new_mouse_event(source.clone(), up_type, end_point, cg_button)
.map_err(|_| "Failed to create mouse up event")?;
up_event.post(CGEventTapLocation::HID);
Ok(())
}
pub fn scroll(x: f64, y: f64, delta_x: i32, delta_y: i32) -> Result<(), String> {
move_mouse(x, y)?;
thread::sleep(Duration::from_millis(10));
#[link(name = "CoreGraphics", kind = "framework")]
extern "C" {
fn CGEventCreateScrollWheelEvent(
source: *const std::ffi::c_void,
units: u32,
wheel_count: u32,
wheel1: i32,
wheel2: i32,
) -> *mut std::ffi::c_void;
fn CGEventPost(tap: u32, event: *mut std::ffi::c_void);
fn CFRelease(cf: *mut std::ffi::c_void);
}
unsafe {
let event = CGEventCreateScrollWheelEvent(
std::ptr::null(),
0, 2, delta_y,
delta_x,
);
if event.is_null() {
return Err("Failed to create scroll event".to_string());
}
CGEventPost(0, event); CFRelease(event);
}
Ok(())
}
fn key_name_to_code(key: &str) -> Option<CGKeyCode> {
Some(match key.to_lowercase().as_str() {
"a" => 0x00,
"s" => 0x01,
"d" => 0x02,
"f" => 0x03,
"h" => 0x04,
"g" => 0x05,
"z" => 0x06,
"x" => 0x07,
"c" => 0x08,
"v" => 0x09,
"b" => 0x0B,
"q" => 0x0C,
"w" => 0x0D,
"e" => 0x0E,
"r" => 0x0F,
"y" => 0x10,
"t" => 0x11,
"1" | "!" => 0x12,
"2" | "@" => 0x13,
"3" | "#" => 0x14,
"4" | "$" => 0x15,
"6" | "^" => 0x16,
"5" | "%" => 0x17,
"=" | "+" => 0x18,
"9" | "(" => 0x19,
"7" | "&" => 0x1A,
"-" | "_" => 0x1B,
"8" | "*" => 0x1C,
"0" | ")" => 0x1D,
"]" | "}" => 0x1E,
"o" => 0x1F,
"u" => 0x20,
"[" | "{" => 0x21,
"i" => 0x22,
"p" => 0x23,
"l" => 0x25,
"j" => 0x26,
"'" | "\"" => 0x27,
"k" => 0x28,
";" | ":" => 0x29,
"\\" | "|" => 0x2A,
"," | "<" => 0x2B,
"/" | "?" => 0x2C,
"n" => 0x2D,
"m" => 0x2E,
"." | ">" => 0x2F,
"`" | "~" => 0x32,
"return" | "enter" => 0x24,
"tab" => 0x30,
"space" | " " => 0x31,
"delete" | "backspace" => 0x33,
"escape" | "esc" => 0x35,
"command" | "cmd" => 0x37,
"shift" => 0x38,
"capslock" => 0x39,
"option" | "alt" => 0x3A,
"control" | "ctrl" => 0x3B,
"rightshift" => 0x3C,
"rightoption" | "rightalt" => 0x3D,
"rightcontrol" | "rightctrl" => 0x3E,
"fn" | "function" => 0x3F,
"f1" => 0x7A,
"f2" => 0x78,
"f3" => 0x63,
"f4" => 0x76,
"f5" => 0x60,
"f6" => 0x61,
"f7" => 0x62,
"f8" => 0x64,
"f9" => 0x65,
"f10" => 0x6D,
"f11" => 0x67,
"f12" => 0x6F,
"f13" => 0x69,
"f14" => 0x6B,
"f15" => 0x71,
"f16" => 0x6A,
"f17" => 0x40,
"f18" => 0x4F,
"f19" => 0x50,
"f20" => 0x5A,
"home" => 0x73,
"end" => 0x77,
"pageup" => 0x74,
"pagedown" => 0x79,
"left" | "leftarrow" => 0x7B,
"right" | "rightarrow" => 0x7C,
"down" | "downarrow" => 0x7D,
"up" | "uparrow" => 0x7E,
"forwarddelete" => 0x75,
"help" => 0x72,
_ => return None,
})
}
pub fn press_key(key: &str, modifiers: &[String]) -> Result<(), String> {
let source = CGEventSource::new(CGEventSourceStateID::HIDSystemState)
.map_err(|_| "Failed to create event source")?;
let keycode = key_name_to_code(key).ok_or_else(|| format!("Unknown key: {}", key))?;
let mut flags = CGEventFlags::empty();
for modifier in modifiers {
match modifier.to_lowercase().as_str() {
"shift" => flags |= CGEventFlags::CGEventFlagShift,
"control" | "ctrl" => flags |= CGEventFlags::CGEventFlagControl,
"option" | "alt" => flags |= CGEventFlags::CGEventFlagAlternate,
"command" | "cmd" => flags |= CGEventFlags::CGEventFlagCommand,
_ => return Err(format!("Unknown modifier: {}", modifier)),
}
}
let down_event = CGEvent::new_keyboard_event(source.clone(), keycode, true)
.map_err(|_| "Failed to create key down event")?;
down_event.set_flags(flags);
down_event.post(CGEventTapLocation::HID);
thread::sleep(Duration::from_millis(10));
let up_event = CGEvent::new_keyboard_event(source, keycode, false)
.map_err(|_| "Failed to create key up event")?;
up_event.set_flags(flags);
up_event.post(CGEventTapLocation::HID);
Ok(())
}
pub fn type_text(text: &str) -> Result<(), String> {
let source = CGEventSource::new(CGEventSourceStateID::HIDSystemState)
.map_err(|_| "Failed to create event source")?;
for c in text.chars() {
let needs_shift = c.is_uppercase()
|| matches!(
c,
'!' | '@'
| '#'
| '$'
| '%'
| '^'
| '&'
| '*'
| '('
| ')'
| '_'
| '+'
| '{'
| '}'
| '|'
| ':'
| '"'
| '<'
| '>'
| '?'
| '~'
);
let key_char = c.to_lowercase().next().unwrap_or(c);
let key_str = key_char.to_string();
if let Some(keycode) = key_name_to_code(&key_str) {
let mut flags = CGEventFlags::empty();
if needs_shift {
flags |= CGEventFlags::CGEventFlagShift;
}
let down_event = CGEvent::new_keyboard_event(source.clone(), keycode, true)
.map_err(|_| "Failed to create key down event")?;
down_event.set_flags(flags);
down_event.post(CGEventTapLocation::HID);
thread::sleep(Duration::from_millis(5));
let up_event = CGEvent::new_keyboard_event(source.clone(), keycode, false)
.map_err(|_| "Failed to create key up event")?;
up_event.set_flags(flags);
up_event.post(CGEventTapLocation::HID);
thread::sleep(Duration::from_millis(5));
} else {
let down_event = CGEvent::new_keyboard_event(source.clone(), 0, true)
.map_err(|_| "Failed to create key event")?;
down_event.set_string(&c.to_string());
down_event.post(CGEventTapLocation::HID);
thread::sleep(Duration::from_millis(5));
let up_event = CGEvent::new_keyboard_event(source.clone(), 0, false)
.map_err(|_| "Failed to create key up event")?;
up_event.post(CGEventTapLocation::HID);
thread::sleep(Duration::from_millis(5));
}
}
Ok(())
}
pub fn get_cursor_position() -> Result<(f64, f64), String> {
let source = CGEventSource::new(CGEventSourceStateID::HIDSystemState)
.map_err(|_| "Failed to create event source")?;
let event = CGEvent::new(source).map_err(|_| "Failed to create CGEvent")?;
let point = event.location();
Ok((point.x, point.y))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_mapping() {
assert!(key_name_to_code("a").is_some());
assert!(key_name_to_code("return").is_some());
assert!(key_name_to_code("f1").is_some());
assert!(key_name_to_code("nonexistent").is_none());
}
#[test]
fn test_get_cursor_position_returns_coordinates() {
let (_x, _y) = get_cursor_position().expect("should get cursor position");
}
}