Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 44 additions & 14 deletions src/macos/listen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,25 @@
use crate::macos::common::*;
use crate::rdev::{Event, ListenError};
use core::ptr::NonNull;
use objc2_core_foundation::{CFMachPort, CFRunLoop, kCFRunLoopCommonModes};
use objc2_core_foundation::{CFMachPort, CFRetained, CFRunLoop, kCFRunLoopCommonModes};
use objc2_core_graphics::{
CGEvent, CGEventTapCallBack, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement,
CGEventTapProxy, CGEventType, kCGEventMaskForAllEvents,
};
use objc2_foundation::NSAutoreleasePool;
use std::ffi::c_void;
use std::ptr::null_mut;

static mut GLOBAL_CALLBACK: Option<Box<dyn FnMut(Event)>> = None;
struct EventTapContext {
callback: Box<dyn FnMut(Event)>,
tap: Option<CFRetained<CFMachPort>>,
}

fn is_tap_disabled_event(event_type: CGEventType) -> bool {
matches!(
event_type,
CGEventType::TapDisabledByTimeout | CGEventType::TapDisabledByUserInput
)
}

#[link(name = "Cocoa", kind = "framework")]
unsafe extern "C" {}
Expand All @@ -20,18 +29,22 @@ unsafe extern "C-unwind" fn raw_callback(
_proxy: CGEventTapProxy,
event_type: CGEventType,
cg_event: NonNull<CGEvent>,
_user_info: *mut c_void,
user_info: *mut c_void,
) -> *mut CGEvent {
let context = unsafe { &mut *user_info.cast::<EventTapContext>() };
if is_tap_disabled_event(event_type) {
if let Some(tap) = context.tap.as_deref() {
CGEvent::tap_enable(tap, true);
eprintln!("rdev: macOS event tap was disabled ({event_type:?}); re-enabled");
}
return cg_event.as_ptr();
}

let opt = KEYBOARD_STATE.lock();
if let Ok(mut keyboard) = opt {
unsafe {
if let Some(event) = convert(event_type, cg_event, &mut keyboard) {
// Reborrowing the global callback pointer.
// Totally UB. but not sure there's a great alternative.
let ptr = &raw mut GLOBAL_CALLBACK;
if let Some(callback) = &mut *ptr {
callback(event);
}
(context.callback)(event);
}
}
}
Expand All @@ -43,18 +56,22 @@ where
T: FnMut(Event) + 'static,
{
unsafe {
GLOBAL_CALLBACK = Some(Box::new(callback));
let _pool = NSAutoreleasePool::new();
let callback: CGEventTapCallBack = Some(raw_callback);
let raw_callback: CGEventTapCallBack = Some(raw_callback);
let mut context = Box::new(EventTapContext {
callback: Box::new(callback),
tap: None,
});
let tap = CGEvent::tap_create(
CGEventTapLocation::HIDEventTap, // HID, Session, AnnotatedSession,
CGEventTapPlacement::HeadInsertEventTap,
CGEventTapOptions::ListenOnly,
kCGEventMaskForAllEvents.into(),
callback,
null_mut(),
raw_callback,
(&mut *context as *mut EventTapContext).cast(),
)
.ok_or(ListenError::EventTapError)?;
context.tap = Some(tap.clone());
let loop_ = CFMachPort::new_run_loop_source(None, Some(&tap), 0)
.ok_or(ListenError::LoopSourceError)?;

Expand All @@ -66,3 +83,16 @@ where
}
Ok(())
}

#[cfg(test)]
mod tests {
use super::is_tap_disabled_event;
use objc2_core_graphics::CGEventType;

#[test]
fn identifies_both_event_tap_disable_reasons() {
assert!(is_tap_disabled_event(CGEventType::TapDisabledByTimeout));
assert!(is_tap_disabled_event(CGEventType::TapDisabledByUserInput));
assert!(!is_tap_disabled_event(CGEventType::FlagsChanged));
}
}