use anyhow::{anyhow, Result};
use std::collections::HashMap;
use std::process::Command;
use std::thread;
use std::time::Duration;
use super::{NativeControl, PlatformInfo, WindowInfo};
mod cg {
use std::ffi::c_void;
pub const kCGEventLeftMouseDown: u32 = 1;
pub const kCGEventLeftMouseUp: u32 = 2;
pub const kCGEventRightMouseDown: u32 = 3;
pub const kCGEventRightMouseUp: u32 = 4;
pub const kCGEventMouseMoved: u32 = 5;
pub const kCGEventLeftMouseDragged: u32 = 6;
pub const kCGEventRightMouseDragged: u32 = 7;
pub const kCGEventOtherMouseDown: u32 = 25;
pub const kCGEventOtherMouseUp: u32 = 26;
pub const kCGEventOtherMouseDragged: u32 = 27;
pub const kCGMouseButtonLeft: u32 = 0;
pub const kCGMouseButtonRight: u32 = 1;
pub const kCGMouseButtonCenter: u32 = 2;
pub const kCGHIDEventTap: u32 = 0;
pub const kCGScrollEventUnitLine: u32 = 1;
pub type CGEventRef = *mut c_void;
#[repr(C)]
#[derive(Copy, Clone, Debug)]
pub struct CGPoint {
pub x: f64,
pub y: f64,
}
#[link(name = "CoreGraphics", kind = "framework")]
extern "C" {
pub fn CGEventCreateMouseEvent(
source: *const c_void,
mouseType: u32,
mouseCursorPosition: CGPoint,
mouseButton: u32,
) -> CGEventRef;
pub fn CGEventCreateKeyboardEvent(
source: *const c_void,
virtualKey: u16,
keyDown: bool,
) -> CGEventRef;
pub fn CGEventCreateScrollWheelEvent(
source: *const c_void,
units: u32,
wheelCount: u32,
wheel1: i32,
) -> CGEventRef;
pub fn CGEventPost(tap: u32, event: CGEventRef);
pub fn CGDisplayPixelsWide(display: u32) -> usize;
pub fn CGDisplayPixelsHigh(display: u32) -> usize;
pub fn CGMainDisplayID() -> u32;
}
#[link(name = "CoreFoundation", kind = "framework")]
extern "C" {
pub fn CFRelease(cf: *mut c_void);
}
}
fn get_key_code(key: &str) -> Option<u16> {
let code = 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, "5" => 0x17, "6" => 0x16,
"7" => 0x1a, "8" => 0x1c, "9" => 0x19, "0" => 0x1d,
"-" => 0x1b, "=" => 0x18, "[" => 0x21, "]" => 0x1e, "\\" => 0x2a,
";" => 0x29, "'" => 0x27, "`" => 0x32, "," => 0x2b, "." => 0x2f, "/" => 0x2c,
"o" => 0x1f, "u" => 0x20, "i" => 0x22, "p" => 0x23, "l" => 0x25, "j" => 0x26,
"k" => 0x28, "n" => 0x2d, "m" => 0x2e,
" " | "space" => 0x31,
"return" | "enter" | "\n" | "\r" => 0x24,
"tab" | "\t" => 0x30,
"backspace" | "\x08" => 0x33,
"escape" | "esc" => 0x35,
"command" | "cmd" => 0x37,
"shift" | "shiftleft" => 0x38,
"shiftright" => 0x3c,
"capslock" => 0x39,
"option" | "alt" | "optionleft" | "altleft" => 0x3a,
"optionright" | "altright" => 0x3d,
"control" | "ctrl" | "ctrlleft" => 0x3b,
"ctrlright" => 0x3e,
"fn" => 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,
"home" => 0x73, "end" => 0x77, "pageup" => 0x74, "pagedown" => 0x79,
"delete" | "del" => 0x75,
"left" => 0x7b, "right" => 0x7c, "down" => 0x7d, "up" => 0x7e,
_ => return None,
};
Some(code)
}
const SHIFT_CHARS: &str = "~!@#$%^&*()_+{}|:\"<>?ABCDEFGHIJKLMNOPQRSTUVWXYZ";
pub struct MacOSControl;
impl MacOSControl {
pub fn new() -> Self {
Self
}
fn send_mouse_event(&self, event_type: u32, x: i32, y: i32, button: u32) {
unsafe {
let point = cg::CGPoint {
x: x as f64,
y: y as f64,
};
let event = cg::CGEventCreateMouseEvent(
std::ptr::null(),
event_type,
point,
button,
);
if !event.is_null() {
cg::CGEventPost(cg::kCGHIDEventTap, event);
cg::CFRelease(event as *mut std::ffi::c_void);
}
}
}
fn send_key_event(&self, key_code: u16, down: bool) {
unsafe {
let event = cg::CGEventCreateKeyboardEvent(std::ptr::null(), key_code, down);
if !event.is_null() {
cg::CGEventPost(cg::kCGHIDEventTap, event);
cg::CFRelease(event as *mut std::ffi::c_void);
}
}
}
}
impl NativeControl for MacOSControl {
fn platform_info(&self) -> PlatformInfo {
let mut backends = HashMap::new();
backends.insert("quartz".to_string(), true);
backends.insert("xdotool".to_string(), false);
backends.insert("scrot".to_string(), false);
backends.insert("win32".to_string(), false);
PlatformInfo {
platform: "darwin".to_string(),
native_available: true,
backends,
}
}
fn get_pixel(&self, x: i32, y: i32) -> Result<(u8, u8, u8)> {
let tmp_path = format!("/tmp/hanzo_pixel_{}.png", std::process::id());
Command::new("screencapture")
.arg("-x")
.arg("-t").arg("png")
.arg("-R").arg(format!("{},{},1,1", x, y))
.arg(&tmp_path)
.output()?;
let data = std::fs::read(&tmp_path)?;
let _ = std::fs::remove_file(&tmp_path);
let output = Command::new("sips")
.arg("-g").arg("pixelWidth")
.arg("-g").arg("pixelHeight")
.arg(&tmp_path)
.output();
if data.len() > 50 {
let script = format!(
r#"
use framework "AppKit"
set img to current application's NSImage's alloc()'s initWithContentsOfFile:"{}"
if img is missing value then return "0,0,0"
set bitmapRep to current application's NSBitmapImageRep's imageRepWithData:(img's TIFFRepresentation())
set pixelColor to bitmapRep's colorAtX:0 y:0
if pixelColor is missing value then return "0,0,0"
set r to (pixelColor's redComponent()) * 255 as integer
set g to (pixelColor's greenComponent()) * 255 as integer
set b to (pixelColor's blueComponent()) * 255 as integer
return (r as text) & "," & (g as text) & "," & (b as text)
"#,
tmp_path
);
Command::new("screencapture")
.arg("-x")
.arg("-t").arg("png")
.arg("-R").arg(format!("{},{},1,1", x, y))
.arg(&tmp_path)
.output()?;
let output = Command::new("osascript")
.arg("-e")
.arg(&script)
.output()?;
let _ = std::fs::remove_file(&tmp_path);
let stdout = String::from_utf8_lossy(&output.stdout);
let parts: Vec<&str> = stdout.trim().split(',').collect();
if parts.len() == 3 {
let r: u8 = parts[0].parse().unwrap_or(0);
let g: u8 = parts[1].parse().unwrap_or(0);
let b: u8 = parts[2].parse().unwrap_or(0);
return Ok((r, g, b));
}
}
Ok((0, 0, 0))
}
fn minimize_window(&self, title: &str) -> Result<bool> {
let script = format!(
r#"
tell application "System Events"
set targetApp to first application process whose name contains "{}"
tell targetApp
set frontmost to true
try
click button 2 of window 1
return true
on error
try
set miniaturized of window 1 to true
return true
end try
end try
end tell
end tell
return false
"#,
title
);
let output = Command::new("osascript")
.arg("-e")
.arg(&script)
.output()?;
Ok(output.status.success())
}
fn maximize_window(&self, title: &str) -> Result<bool> {
let script = format!(
r#"
tell application "System Events"
set targetApp to first application process whose name contains "{}"
tell targetApp
set frontmost to true
try
click button 1 of window 1
return true
on error
try
set value of attribute "AXFullScreen" of window 1 to true
return true
end try
end try
end tell
end tell
return false
"#,
title
);
let output = Command::new("osascript")
.arg("-e")
.arg(&script)
.output()?;
Ok(output.status.success())
}
fn resize_window(&self, title: &str, width: i32, height: i32) -> Result<bool> {
let script = format!(
r#"
tell application "System Events"
set targetApp to first application process whose name contains "{}"
tell targetApp
try
set size of window 1 to {{{}, {}}}
return true
end try
end tell
end tell
return false
"#,
title, width, height
);
let output = Command::new("osascript")
.arg("-e")
.arg(&script)
.output()?;
Ok(output.status.success())
}
fn move_window(&self, title: &str, x: i32, y: i32) -> Result<bool> {
let script = format!(
r#"
tell application "System Events"
set targetApp to first application process whose name contains "{}"
tell targetApp
try
set position of window 1 to {{{}, {}}}
return true
end try
end tell
end tell
return false
"#,
title, x, y
);
let output = Command::new("osascript")
.arg("-e")
.arg(&script)
.output()?;
Ok(output.status.success())
}
fn close_window(&self, title: &str) -> Result<bool> {
let script = format!(
r#"
tell application "System Events"
set targetApp to first application process whose name contains "{}"
tell targetApp
try
click button 1 of window 1
return true
on error
try
keystroke "w" using command down
return true
end try
end try
end tell
end tell
return false
"#,
title
);
let output = Command::new("osascript")
.arg("-e")
.arg(&script)
.output()?;
Ok(output.status.success())
}
fn mouse_position(&self) -> Result<(i32, i32)> {
let output = Command::new("osascript")
.arg("-e")
.arg(r#"
use framework "AppKit"
set mousePos to current application's NSEvent's mouseLocation()
set screenHeight to (current application's NSScreen's mainScreen()'s frame()'s |size|()'s height) as integer
set x to (mousePos's x) as integer
set y to screenHeight - ((mousePos's y) as integer)
return (x as text) & "," & (y as text)
"#)
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
let parts: Vec<&str> = stdout.trim().split(',').collect();
if parts.len() == 2 {
let x: i32 = parts[0].parse().unwrap_or(0);
let y: i32 = parts[1].parse().unwrap_or(0);
return Ok((x, y));
}
Ok((0, 0))
}
fn screen_size(&self) -> Result<(i32, i32)> {
unsafe {
let display = cg::CGMainDisplayID();
let w = cg::CGDisplayPixelsWide(display) as i32;
let h = cg::CGDisplayPixelsHigh(display) as i32;
Ok((w, h))
}
}
fn click(&self, x: i32, y: i32, button: &str) -> Result<()> {
let (down_type, up_type, btn) = match button {
"right" => (
cg::kCGEventRightMouseDown,
cg::kCGEventRightMouseUp,
cg::kCGMouseButtonRight,
),
"middle" => (
cg::kCGEventOtherMouseDown,
cg::kCGEventOtherMouseUp,
cg::kCGMouseButtonCenter,
),
_ => (
cg::kCGEventLeftMouseDown,
cg::kCGEventLeftMouseUp,
cg::kCGMouseButtonLeft,
),
};
self.send_mouse_event(down_type, x, y, btn);
self.send_mouse_event(up_type, x, y, btn);
Ok(())
}
fn double_click(&self, x: i32, y: i32) -> Result<()> {
self.click(x, y, "left")?;
thread::sleep(Duration::from_millis(10));
self.click(x, y, "left")?;
Ok(())
}
fn move_to(&self, x: i32, y: i32) -> Result<()> {
self.send_mouse_event(cg::kCGEventMouseMoved, x, y, 0);
Ok(())
}
fn drag(&self, start_x: i32, start_y: i32, end_x: i32, end_y: i32, button: &str) -> Result<()> {
let (down_type, drag_type, up_type, btn) = match button {
"right" => (
cg::kCGEventRightMouseDown,
cg::kCGEventRightMouseDragged,
cg::kCGEventRightMouseUp,
cg::kCGMouseButtonRight,
),
"middle" => (
cg::kCGEventOtherMouseDown,
cg::kCGEventOtherMouseDragged,
cg::kCGEventOtherMouseUp,
cg::kCGMouseButtonCenter,
),
_ => (
cg::kCGEventLeftMouseDown,
cg::kCGEventLeftMouseDragged,
cg::kCGEventLeftMouseUp,
cg::kCGMouseButtonLeft,
),
};
self.send_mouse_event(down_type, start_x, start_y, btn);
let steps = ((end_x - start_x).abs().max((end_y - start_y).abs()) / 10).max(1);
for i in 1..=steps {
let cx = start_x + (end_x - start_x) * i / steps;
let cy = start_y + (end_y - start_y) * i / steps;
self.send_mouse_event(drag_type, cx, cy, btn);
thread::sleep(Duration::from_millis(1));
}
self.send_mouse_event(up_type, end_x, end_y, btn);
Ok(())
}
fn scroll(&self, amount: i32, x: Option<i32>, y: Option<i32>) -> Result<()> {
if let (Some(x), Some(y)) = (x, y) {
self.move_to(x, y)?;
}
unsafe {
let event = cg::CGEventCreateScrollWheelEvent(
std::ptr::null(),
cg::kCGScrollEventUnitLine,
1,
amount,
);
if !event.is_null() {
cg::CGEventPost(cg::kCGHIDEventTap, event);
cg::CFRelease(event as *mut std::ffi::c_void);
}
}
Ok(())
}
fn key_down(&self, key: &str) -> Result<()> {
if let Some(code) = get_key_code(key) {
self.send_key_event(code, true);
Ok(())
} else {
Err(anyhow!("Unknown key: {}", key))
}
}
fn key_up(&self, key: &str) -> Result<()> {
if let Some(code) = get_key_code(key) {
self.send_key_event(code, false);
Ok(())
} else {
Err(anyhow!("Unknown key: {}", key))
}
}
fn press(&self, key: &str) -> Result<()> {
self.key_down(key)?;
self.key_up(key)?;
Ok(())
}
fn hotkey(&self, keys: &[String]) -> Result<()> {
for key in keys {
self.key_down(key)?;
}
for key in keys.iter().rev() {
self.key_up(key)?;
}
Ok(())
}
fn type_char(&self, c: char) -> Result<()> {
let s = c.to_string();
let key = s.as_str();
if SHIFT_CHARS.contains(c) {
self.key_down("shift")?;
let lower = c.to_lowercase().to_string();
if get_key_code(&lower).is_some() {
self.press(&lower)?;
}
self.key_up("shift")?;
} else if get_key_code(key).is_some() {
self.press(key)?;
}
Ok(())
}
fn type_text(&self, text: &str, interval: f64) -> Result<()> {
for c in text.chars() {
self.type_char(c)?;
if interval > 0.0 {
thread::sleep(Duration::from_secs_f64(interval));
}
}
Ok(())
}
fn screenshot(&self, region: Option<&[i32]>) -> Result<Vec<u8>> {
let tmp_path = format!("/tmp/hanzo_screenshot_{}.png", std::process::id());
let mut cmd = Command::new("screencapture");
cmd.arg("-x").arg("-t").arg("png");
if let Some(r) = region {
if r.len() == 4 {
cmd.arg("-R").arg(format!("{},{},{},{}", r[0], r[1], r[2], r[3]));
}
}
cmd.arg(&tmp_path);
cmd.output()?;
let data = std::fs::read(&tmp_path)?;
let _ = std::fs::remove_file(&tmp_path);
Ok(data)
}
fn get_active_window(&self) -> Result<WindowInfo> {
let script = r#"
tell application "System Events"
set frontApp to first application process whose frontmost is true
set appName to name of frontApp
try
set frontWindow to front window of frontApp
set winName to name of frontWindow
set winPos to position of frontWindow
set winSize to size of frontWindow
return appName & "|" & winName & "|" & (item 1 of winPos) & "|" & (item 2 of winPos) & "|" & (item 1 of winSize) & "|" & (item 2 of winSize)
on error
return appName & "|" & "" & "|0|0|0|0"
end try
end tell
"#;
let output = Command::new("osascript")
.arg("-e")
.arg(script)
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
let parts: Vec<&str> = stdout.trim().split('|').collect();
if parts.len() >= 6 {
Ok(WindowInfo {
app: Some(parts[0].to_string()),
title: parts[1].to_string(),
x: parts[2].parse().unwrap_or(0),
y: parts[3].parse().unwrap_or(0),
width: parts[4].parse().unwrap_or(0),
height: parts[5].parse().unwrap_or(0),
})
} else {
Err(anyhow!("Could not get active window"))
}
}
fn list_windows(&self) -> Result<Vec<WindowInfo>> {
let script = r#"
set windowList to ""
set delim to ASCII character 31
tell application "System Events"
set allProcesses to application processes whose visible is true
repeat with proc in allProcesses
set procName to name of proc
try
set procWindows to windows of proc
repeat with win in procWindows
set winName to name of win
set winPos to position of win
set winSize to size of win
set windowList to windowList & procName & delim & winName & delim & (item 1 of winPos) & delim & (item 2 of winPos) & delim & (item 1 of winSize) & delim & (item 2 of winSize) & "\n"
end repeat
end try
end repeat
end tell
return windowList
"#;
let output = Command::new("osascript")
.arg("-e")
.arg(script)
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
let mut windows = Vec::new();
for line in stdout.lines() {
if line.contains('\x1f') {
let parts: Vec<&str> = line.split('\x1f').collect();
if parts.len() >= 6 {
windows.push(WindowInfo {
app: Some(parts[0].to_string()),
title: parts[1].to_string(),
x: parts[2].parse().unwrap_or(0),
y: parts[3].parse().unwrap_or(0),
width: parts[4].parse().unwrap_or(0),
height: parts[5].parse().unwrap_or(0),
});
}
}
}
Ok(windows)
}
fn focus_window(&self, title: &str) -> Result<bool> {
let script = format!(r#"tell application "{}" to activate"#, title);
let result = Command::new("osascript")
.arg("-e")
.arg(&script)
.output();
if result.map(|r| r.status.success()).unwrap_or(false) {
return Ok(true);
}
let search_script = format!(
r#"
tell application "System Events"
set matchingApps to (application processes whose name contains "{}")
if (count of matchingApps) > 0 then
set frontApp to item 1 of matchingApps
set frontmost of frontApp to true
return name of frontApp
end if
end tell
return ""
"#,
title
);
let output = Command::new("osascript")
.arg("-e")
.arg(&search_script)
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(!stdout.trim().is_empty())
}
}