use std::ffi::c_void;
use std::ptr::{self, NonNull};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
use std::thread::{self, JoinHandle};
use std::time::Instant;
use crossbeam_channel::{Sender, bounded};
use objc2_core_foundation::{CFMachPort, CFRunLoop, kCFRunLoopCommonModes};
use objc2_core_graphics::{
CGEvent, CGEventField, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement,
CGEventTapProxy, CGEventType,
};
use super::keycodes::key_from_code;
use crate::log;
use crate::{Error, Event, EventKind, Key, tap::TapBuilder};
#[link(name = "IOKit", kind = "framework")]
unsafe extern "C" {
fn IOHIDCheckAccess(request: u32) -> u32;
}
const K_IOHID_REQUEST_TYPE_LISTEN: u32 = 1;
const K_IOHID_ACCESS_TYPE_GRANTED: u32 = 0;
const CGEVENT_FLAG_SHIFT_LEFT: u64 = 0x00020002;
const CGEVENT_FLAG_SHIFT_RIGHT: u64 = 0x00020004;
const CGEVENT_FLAG_CONTROL_LEFT: u64 = 0x00040001;
const CGEVENT_FLAG_CONTROL_RIGHT: u64 = 0x00042000;
const CGEVENT_FLAG_ALT_LEFT: u64 = 0x00080020;
const CGEVENT_FLAG_ALT_RIGHT: u64 = 0x00080040;
const CGEVENT_FLAG_META_LEFT: u64 = 0x00100008;
const CGEVENT_FLAG_META_RIGHT: u64 = 0x00100010;
const CGEVENT_FLAG_CAPS_LOCK: u64 = 0x00010000;
struct CallbackContext {
tx: Sender<Event>,
macos_no_repeat_detection: bool,
last_flags: std::sync::atomic::AtomicU64,
}
struct SendableRunLoopPtr(NonNull<CFRunLoop>);
unsafe impl Send for SendableRunLoopPtr {}
unsafe impl Sync for SendableRunLoopPtr {}
struct SendableCtxPtr(*mut CallbackContext);
unsafe impl Send for SendableCtxPtr {}
impl SendableCtxPtr {
fn into_raw(self) -> *mut CallbackContext {
self.0
}
}
pub(crate) struct ShutdownGuard {
run_loop: Option<SendableRunLoopPtr>,
thread: Option<JoinHandle<()>>,
ctx_ptr: AtomicPtr<CallbackContext>,
}
impl std::fmt::Debug for ShutdownGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ShutdownGuard").finish_non_exhaustive()
}
}
impl Drop for ShutdownGuard {
fn drop(&mut self) {
log::debug!("keytap: stopping macOS CGEventTap");
if let Some(rl) = self.run_loop.take() {
unsafe {
let run_loop_ref: &CFRunLoop = rl.0.as_ref();
run_loop_ref.stop();
}
}
if let Some(t) = self.thread.take() {
let _ = t.join();
}
let ptr = self.ctx_ptr.swap(ptr::null_mut(), Ordering::AcqRel);
if !ptr.is_null() {
unsafe { drop(Box::from_raw(ptr)) };
}
}
}
pub(crate) fn start(tx: Sender<Event>, cfg: &TapBuilder) -> Result<ShutdownGuard, Error> {
log::debug!("keytap: starting macOS CGEventTap");
let access = unsafe { IOHIDCheckAccess(K_IOHID_REQUEST_TYPE_LISTEN) };
if access != K_IOHID_ACCESS_TYPE_GRANTED {
log::debug!("keytap: IOHIDCheckAccess denied (access={})", access);
return Err(Error::PermissionDenied);
}
let ctx = Box::new(CallbackContext {
tx,
macos_no_repeat_detection: cfg.macos_no_repeat_detection,
last_flags: std::sync::atomic::AtomicU64::new(0),
});
let ctx_ptr = Box::into_raw(ctx);
let (rl_tx, rl_rx) = bounded::<Result<SendableRunLoopPtr, Error>>(1);
let ready_flag = Arc::new(AtomicBool::new(false));
let ready_flag_worker = ready_flag.clone();
let ctx_send = SendableCtxPtr(ctx_ptr);
let thread = thread::Builder::new()
.name("keytap-macos-tap".into())
.spawn(move || {
run_tap_thread(ctx_send.into_raw(), rl_tx, ready_flag_worker);
})
.map_err(|e| Error::TapFailed(format!("spawn tap thread: {e}")))?;
let run_loop = match rl_rx.recv_timeout(std::time::Duration::from_secs(2)) {
Ok(Ok(rl)) => rl,
Ok(Err(e)) => {
let _ = thread.join();
unsafe { drop(Box::from_raw(ctx_ptr)) };
return Err(e);
}
Err(_) => {
unsafe { drop(Box::from_raw(ctx_ptr)) };
return Err(Error::TapFailed("tap creation handshake timed out".into()));
}
};
while !ready_flag.load(Ordering::Acquire) {
std::thread::yield_now();
}
Ok(ShutdownGuard {
run_loop: Some(run_loop),
thread: Some(thread),
ctx_ptr: AtomicPtr::new(ctx_ptr),
})
}
fn run_tap_thread(
ctx_ptr: *mut CallbackContext,
rl_tx: Sender<Result<SendableRunLoopPtr, Error>>,
ready: Arc<AtomicBool>,
) {
let mask: u64 = (1u64 << CGEventType::KeyDown.0)
| (1u64 << CGEventType::KeyUp.0)
| (1u64 << CGEventType::FlagsChanged.0);
let tap = unsafe {
CGEvent::tap_create(
CGEventTapLocation::HIDEventTap,
CGEventTapPlacement::HeadInsertEventTap,
CGEventTapOptions::ListenOnly,
mask,
Some(raw_callback),
ctx_ptr as *mut c_void,
)
};
let tap = match tap {
Some(t) => t,
None => {
let _ = rl_tx.send(Err(Error::TapFailed(
"CGEventTapCreate returned null — \
most likely Input Monitoring permission was revoked \
between the permission check and tap creation"
.into(),
)));
return;
}
};
let source = match CFMachPort::new_run_loop_source(None, Some(&tap), 0) {
Some(s) => s,
None => {
let _ = rl_tx.send(Err(Error::TapFailed(
"CFMachPortCreateRunLoopSource returned null".into(),
)));
return;
}
};
let current_loop = match CFRunLoop::current() {
Some(rl) => rl,
None => {
let _ = rl_tx.send(Err(Error::TapFailed(
"CFRunLoop::current() returned None".into(),
)));
return;
}
};
current_loop.add_source(Some(&source), unsafe { kCFRunLoopCommonModes });
CGEvent::tap_enable(&tap, true);
let rl_ptr = SendableRunLoopPtr(NonNull::from(&*current_loop));
if rl_tx.send(Ok(rl_ptr)).is_err() {
return;
}
ready.store(true, Ordering::Release);
CFRunLoop::run();
}
unsafe extern "C-unwind" fn raw_callback(
_proxy: CGEventTapProxy,
event_type: CGEventType,
cg_event: NonNull<CGEvent>,
user_info: *mut c_void,
) -> *mut CGEvent {
let ctx: &CallbackContext = unsafe { &*(user_info as *const CallbackContext) };
let cg_event_ref: &CGEvent = unsafe { cg_event.as_ref() };
let keycode =
CGEvent::integer_value_field(Some(cg_event_ref), CGEventField::KeyboardEventKeycode) as u32;
let key = key_from_code(keycode);
let now = Instant::now();
let maybe_event = match event_type {
CGEventType::KeyDown => {
let auto_repeat = CGEvent::integer_value_field(
Some(cg_event_ref),
CGEventField::KeyboardEventAutorepeat,
) != 0;
let kind = if auto_repeat && !ctx.macos_no_repeat_detection {
EventKind::KeyRepeat(key)
} else {
EventKind::KeyDown(key)
};
Some(Event { time: now, kind })
}
CGEventType::KeyUp => Some(Event {
time: now,
kind: EventKind::KeyUp(key),
}),
CGEventType::FlagsChanged => {
let flags = cg_event_flags(cg_event_ref);
let prev = ctx.last_flags.swap(flags, Ordering::Relaxed);
let bit = flag_bit_for_key(key);
if bit == 0 {
None
} else {
let was_down = (prev & bit) != 0;
let is_down = (flags & bit) != 0;
match (was_down, is_down) {
(false, true) => Some(Event {
time: now,
kind: EventKind::KeyDown(key),
}),
(true, false) => Some(Event {
time: now,
kind: EventKind::KeyUp(key),
}),
_ => None,
}
}
}
_ => None,
};
if let Some(event) = maybe_event {
if ctx.tx.try_send(event).is_err() {
log::trace!("keytap: channel full — dropping event");
}
}
cg_event.as_ptr()
}
fn cg_event_flags(event: &CGEvent) -> u64 {
CGEvent::flags(Some(event)).0
}
fn flag_bit_for_key(key: Key) -> u64 {
match key {
Key::ShiftLeft => CGEVENT_FLAG_SHIFT_LEFT,
Key::ShiftRight => CGEVENT_FLAG_SHIFT_RIGHT,
Key::ControlLeft => CGEVENT_FLAG_CONTROL_LEFT,
Key::ControlRight => CGEVENT_FLAG_CONTROL_RIGHT,
Key::AltLeft => CGEVENT_FLAG_ALT_LEFT,
Key::AltRight => CGEVENT_FLAG_ALT_RIGHT,
Key::MetaLeft => CGEVENT_FLAG_META_LEFT,
Key::MetaRight => CGEVENT_FLAG_META_RIGHT,
Key::CapsLock => CGEVENT_FLAG_CAPS_LOCK,
_ => 0,
}
}