diff --git a/src/macos/listen.rs b/src/macos/listen.rs index 0c87e0bc..2f6dc8d5 100644 --- a/src/macos/listen.rs +++ b/src/macos/listen.rs @@ -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> = None; +struct EventTapContext { + callback: Box, + tap: Option>, +} + +fn is_tap_disabled_event(event_type: CGEventType) -> bool { + matches!( + event_type, + CGEventType::TapDisabledByTimeout | CGEventType::TapDisabledByUserInput + ) +} #[link(name = "Cocoa", kind = "framework")] unsafe extern "C" {} @@ -20,18 +29,22 @@ unsafe extern "C-unwind" fn raw_callback( _proxy: CGEventTapProxy, event_type: CGEventType, cg_event: NonNull, - _user_info: *mut c_void, + user_info: *mut c_void, ) -> *mut CGEvent { + let context = unsafe { &mut *user_info.cast::() }; + 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); } } } @@ -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)?; @@ -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)); + } +}