use crate::collector::types::{
KeyboardEvent, KeyboardEventType, MouseEvent, SensorEvent, ShortcutEvent, ShortcutType,
};
use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop};
use core_graphics::event::{
CGEvent, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, CGEventType,
CallbackResult,
};
use crossbeam_channel::{bounded, Receiver, Sender};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread::{self, JoinHandle};
#[derive(Debug, Clone)]
pub struct CollectorConfig {
pub capture_keyboard: bool,
pub capture_mouse: bool,
}
impl Default for CollectorConfig {
fn default() -> Self {
Self {
capture_keyboard: true,
capture_mouse: true,
}
}
}
pub struct MacOSCollector {
config: CollectorConfig,
sender: Sender<SensorEvent>,
receiver: Receiver<SensorEvent>,
running: Arc<AtomicBool>,
thread_handle: Option<JoinHandle<()>>,
}
impl MacOSCollector {
pub fn new(config: CollectorConfig) -> Self {
let (sender, receiver) = bounded(10_000);
Self {
config,
sender,
receiver,
running: Arc::new(AtomicBool::new(false)),
thread_handle: None,
}
}
pub fn start(&mut self) -> Result<(), CollectorError> {
if self.running.load(Ordering::SeqCst) {
return Err(CollectorError::AlreadyRunning);
}
self.running.store(true, Ordering::SeqCst);
let sender = self.sender.clone();
let running = self.running.clone();
let config = self.config.clone();
let handle = thread::spawn(move || {
if let Err(e) = run_event_loop(sender, running.clone(), config) {
eprintln!("Event loop error: {e:?}");
}
running.store(false, Ordering::SeqCst);
});
self.thread_handle = Some(handle);
Ok(())
}
pub fn stop(&mut self) {
self.running.store(false, Ordering::SeqCst);
if let Some(handle) = self.thread_handle.take() {
let _ = handle.join();
}
}
pub fn is_running(&self) -> bool {
self.running.load(Ordering::SeqCst)
}
pub fn receiver(&self) -> &Receiver<SensorEvent> {
&self.receiver
}
pub fn try_recv(&self) -> Option<SensorEvent> {
self.receiver.try_recv().ok()
}
}
impl Drop for MacOSCollector {
fn drop(&mut self) {
self.stop();
}
}
#[derive(Debug)]
pub enum CollectorError {
AlreadyRunning,
PermissionDenied,
TapCreationFailed,
RunLoopSourceFailed,
}
impl std::fmt::Display for CollectorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CollectorError::AlreadyRunning => write!(f, "Collector is already running"),
CollectorError::PermissionDenied => {
write!(f, "Input Monitoring permission not granted")
}
CollectorError::TapCreationFailed => write!(f, "Failed to create CGEvent tap"),
CollectorError::RunLoopSourceFailed => {
write!(f, "Failed to create run loop source")
}
}
}
}
impl std::error::Error for CollectorError {}
fn build_event_types(config: &CollectorConfig) -> Vec<CGEventType> {
let mut types = Vec::new();
if config.capture_keyboard {
types.push(CGEventType::KeyDown);
types.push(CGEventType::KeyUp);
types.push(CGEventType::FlagsChanged);
}
if config.capture_mouse {
types.push(CGEventType::MouseMoved);
types.push(CGEventType::LeftMouseDown);
types.push(CGEventType::LeftMouseUp);
types.push(CGEventType::RightMouseDown);
types.push(CGEventType::RightMouseUp);
types.push(CGEventType::LeftMouseDragged);
types.push(CGEventType::RightMouseDragged);
types.push(CGEventType::ScrollWheel);
}
types
}
fn run_event_loop(
sender: Sender<SensorEvent>,
running: Arc<AtomicBool>,
config: CollectorConfig,
) -> Result<(), CollectorError> {
let event_types = build_event_types(&config);
thread_local! {
static EVENT_SENDER: std::cell::RefCell<Option<Sender<SensorEvent>>> = const { std::cell::RefCell::new(None) };
}
EVENT_SENDER.with(|s| {
*s.borrow_mut() = Some(sender);
});
fn event_callback(
_proxy: core_graphics::event::CGEventTapProxy,
event_type: CGEventType,
event: &CGEvent,
) -> CallbackResult {
thread_local! {
static EVENT_SENDER: std::cell::RefCell<Option<Sender<SensorEvent>>> = const { std::cell::RefCell::new(None) };
}
EVENT_SENDER.with(|sender_cell| {
if let Some(ref sender) = *sender_cell.borrow() {
if let Some(sensor_event) = process_cg_event(event_type, event) {
let _ = sender.try_send(sensor_event);
}
}
});
CallbackResult::Keep
}
let tap = CGEventTap::new(
CGEventTapLocation::Session,
CGEventTapPlacement::HeadInsertEventTap,
CGEventTapOptions::ListenOnly,
event_types,
event_callback,
)
.map_err(|_| CollectorError::TapCreationFailed)?;
let source = tap
.mach_port()
.create_runloop_source(0)
.map_err(|_| CollectorError::RunLoopSourceFailed)?;
let run_loop = CFRunLoop::get_current();
unsafe {
run_loop.add_source(&source, kCFRunLoopCommonModes);
}
tap.enable();
while running.load(Ordering::SeqCst) {
CFRunLoop::run_in_mode(
unsafe { kCFRunLoopCommonModes },
std::time::Duration::from_millis(100),
false,
);
}
Ok(())
}
pub(crate) fn classify_keyboard_event(event: &CGEvent) -> KeyboardEventType {
let keycode =
event.get_integer_value_field(core_graphics::event::EventField::KEYBOARD_EVENT_KEYCODE);
classify_keycode(keycode)
}
pub(crate) fn classify_keycode(keycode: i64) -> KeyboardEventType {
const KEY_LEFT_ARROW: i64 = 123;
const KEY_RIGHT_ARROW: i64 = 124;
const KEY_DOWN_ARROW: i64 = 125;
const KEY_UP_ARROW: i64 = 126;
const KEY_PAGE_UP: i64 = 116;
const KEY_PAGE_DOWN: i64 = 121;
const KEY_HOME: i64 = 115;
const KEY_END: i64 = 119;
const KEY_BACKSPACE: i64 = 51;
const KEY_DELETE: i64 = 117;
const KEY_RETURN: i64 = 36;
const KEY_TAB: i64 = 48;
const KEY_ESCAPE: i64 = 53;
const KEY_F1: i64 = 122;
const KEY_F2: i64 = 120;
const KEY_F3: i64 = 99;
const KEY_F4: i64 = 118;
const KEY_F5: i64 = 96;
const KEY_F6: i64 = 97;
const KEY_F7: i64 = 98;
const KEY_F8: i64 = 100;
const KEY_F9: i64 = 101;
const KEY_F10: i64 = 109;
const KEY_F11: i64 = 103;
const KEY_F12: i64 = 111;
match keycode {
KEY_LEFT_ARROW | KEY_RIGHT_ARROW | KEY_DOWN_ARROW | KEY_UP_ARROW | KEY_PAGE_UP
| KEY_PAGE_DOWN | KEY_HOME | KEY_END => KeyboardEventType::NavigationKey,
KEY_BACKSPACE => KeyboardEventType::Backspace,
KEY_DELETE => KeyboardEventType::Delete,
KEY_RETURN => KeyboardEventType::Enter,
KEY_TAB => KeyboardEventType::Tab,
KEY_ESCAPE => KeyboardEventType::Escape,
KEY_F1 | KEY_F2 | KEY_F3 | KEY_F4 | KEY_F5 | KEY_F6 | KEY_F7 | KEY_F8 | KEY_F9
| KEY_F10 | KEY_F11 | KEY_F12 => KeyboardEventType::FunctionKey,
_ => KeyboardEventType::TypingTap,
}
}
fn detect_shortcut(event: &CGEvent) -> Option<ShortcutType> {
use core_graphics::event::CGEventFlags;
let flags = event.get_flags();
let keycode =
event.get_integer_value_field(core_graphics::event::EventField::KEYBOARD_EVENT_KEYCODE);
let has_cmd = flags.contains(CGEventFlags::CGEventFlagCommand);
if !has_cmd {
return None;
}
let has_shift = flags.contains(CGEventFlags::CGEventFlagShift);
const KEY_C: i64 = 8;
const KEY_V: i64 = 9;
const KEY_X: i64 = 7;
const KEY_Z: i64 = 6;
const KEY_A: i64 = 0;
const KEY_S: i64 = 1;
match keycode {
KEY_C => Some(ShortcutType::Copy),
KEY_V => Some(ShortcutType::Paste),
KEY_X => Some(ShortcutType::Cut),
KEY_Z if has_shift => Some(ShortcutType::Redo),
KEY_Z => Some(ShortcutType::Undo),
KEY_A => Some(ShortcutType::SelectAll),
KEY_S => Some(ShortcutType::Save),
_ => None,
}
}
fn process_cg_event(event_type: CGEventType, event: &CGEvent) -> Option<SensorEvent> {
use core_graphics::event::CGEventType::*;
match event_type {
KeyDown => {
if let Some(shortcut_type) = detect_shortcut(event) {
return Some(SensorEvent::Shortcut(ShortcutEvent {
timestamp: chrono::Utc::now(),
shortcut_type,
}));
}
let event_class = classify_keyboard_event(event);
Some(SensorEvent::Keyboard(KeyboardEvent::with_type(
true,
event_class,
)))
}
KeyUp => {
let event_class = classify_keyboard_event(event);
Some(SensorEvent::Keyboard(KeyboardEvent::with_type(
false,
event_class,
)))
}
FlagsChanged => {
Some(SensorEvent::Keyboard(KeyboardEvent::with_type(
true,
KeyboardEventType::ModifierKey,
)))
}
MouseMoved | LeftMouseDragged | RightMouseDragged => {
let delta_x =
event.get_double_value_field(core_graphics::event::EventField::MOUSE_EVENT_DELTA_X);
let delta_y =
event.get_double_value_field(core_graphics::event::EventField::MOUSE_EVENT_DELTA_Y);
Some(SensorEvent::Mouse(MouseEvent::movement(delta_x, delta_y)))
}
LeftMouseDown => Some(SensorEvent::Mouse(MouseEvent::click(true))),
LeftMouseUp => None,
RightMouseDown => Some(SensorEvent::Mouse(MouseEvent::click(false))),
RightMouseUp => None,
ScrollWheel => {
let delta_x = event.get_double_value_field(
core_graphics::event::EventField::SCROLL_WHEEL_EVENT_POINT_DELTA_AXIS_2,
);
let delta_y = event.get_double_value_field(
core_graphics::event::EventField::SCROLL_WHEEL_EVENT_POINT_DELTA_AXIS_1,
);
Some(SensorEvent::Mouse(MouseEvent::scroll(delta_x, delta_y)))
}
_ => None,
}
}
#[allow(unexpected_cfgs)]
pub fn get_frontmost_app_id() -> Option<String> {
use objc::runtime::{Class, Object};
use objc::{msg_send, sel, sel_impl};
use std::ffi::CStr;
unsafe {
let cls = Class::get("NSWorkspace")?;
let workspace: *mut Object = msg_send![cls, sharedWorkspace];
if workspace.is_null() {
return None;
}
let app: *mut Object = msg_send![workspace, frontmostApplication];
if app.is_null() {
return None;
}
let bundle_id: *mut Object = msg_send![app, bundleIdentifier];
if bundle_id.is_null() {
return None;
}
let c_str: *const std::os::raw::c_char = msg_send![bundle_id, UTF8String];
if c_str.is_null() {
return None;
}
Some(CStr::from_ptr(c_str).to_string_lossy().into_owned())
}
}
pub fn check_permission() -> bool {
let result = CGEventTap::new(
CGEventTapLocation::Session,
CGEventTapPlacement::HeadInsertEventTap,
CGEventTapOptions::ListenOnly,
vec![CGEventType::KeyDown],
|_proxy, _type, _event| CallbackResult::Keep,
);
result.is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_collector_config_default() {
let config = CollectorConfig::default();
assert!(config.capture_keyboard);
assert!(config.capture_mouse);
}
#[test]
fn test_collector_creation() {
let collector = MacOSCollector::new(CollectorConfig::default());
assert!(!collector.is_running());
}
#[test]
fn test_classify_keycode_navigation() {
assert_eq!(classify_keycode(123), KeyboardEventType::NavigationKey); assert_eq!(classify_keycode(124), KeyboardEventType::NavigationKey); assert_eq!(classify_keycode(125), KeyboardEventType::NavigationKey); assert_eq!(classify_keycode(126), KeyboardEventType::NavigationKey); assert_eq!(classify_keycode(116), KeyboardEventType::NavigationKey); assert_eq!(classify_keycode(121), KeyboardEventType::NavigationKey); assert_eq!(classify_keycode(115), KeyboardEventType::NavigationKey); assert_eq!(classify_keycode(119), KeyboardEventType::NavigationKey); }
#[test]
fn test_classify_keycode_special_keys() {
assert_eq!(classify_keycode(51), KeyboardEventType::Backspace);
assert_eq!(classify_keycode(117), KeyboardEventType::Delete);
assert_eq!(classify_keycode(36), KeyboardEventType::Enter);
assert_eq!(classify_keycode(48), KeyboardEventType::Tab);
assert_eq!(classify_keycode(53), KeyboardEventType::Escape);
}
#[test]
fn test_classify_keycode_function_keys() {
assert_eq!(classify_keycode(122), KeyboardEventType::FunctionKey); assert_eq!(classify_keycode(120), KeyboardEventType::FunctionKey); assert_eq!(classify_keycode(99), KeyboardEventType::FunctionKey); assert_eq!(classify_keycode(118), KeyboardEventType::FunctionKey); assert_eq!(classify_keycode(96), KeyboardEventType::FunctionKey); assert_eq!(classify_keycode(97), KeyboardEventType::FunctionKey); assert_eq!(classify_keycode(98), KeyboardEventType::FunctionKey); assert_eq!(classify_keycode(100), KeyboardEventType::FunctionKey); assert_eq!(classify_keycode(101), KeyboardEventType::FunctionKey); assert_eq!(classify_keycode(109), KeyboardEventType::FunctionKey); assert_eq!(classify_keycode(103), KeyboardEventType::FunctionKey); assert_eq!(classify_keycode(111), KeyboardEventType::FunctionKey); }
#[test]
fn test_classify_keycode_typing() {
assert_eq!(classify_keycode(0), KeyboardEventType::TypingTap); assert_eq!(classify_keycode(1), KeyboardEventType::TypingTap); assert_eq!(classify_keycode(13), KeyboardEventType::TypingTap); assert_eq!(classify_keycode(49), KeyboardEventType::TypingTap); }
#[test]
fn test_get_frontmost_app_id() {
let app_id = get_frontmost_app_id();
if let Some(ref id) = app_id {
assert!(!id.is_empty());
}
}
}