use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
pub const KEYCODE_CAPS_LOCK: i64 = 57;
pub const KEYCODE_FN: i64 = 63;
#[allow(non_upper_case_globals)]
mod ffi {
use std::ffi::c_void;
pub type CFMachPortRef = *mut c_void;
pub type CFRunLoopSourceRef = *mut c_void;
pub type CFRunLoopRef = *mut c_void;
pub type CFAllocatorRef = *const c_void;
pub type CFDictionaryRef = *const c_void;
pub type CFStringRef = *const c_void;
pub type CFRunLoopMode = CFStringRef;
pub type CGEventRef = *mut c_void;
pub type CGEventTapProxy = *mut c_void;
pub type CGEventType = u32;
pub const kCGHIDEventTap: u32 = 0;
pub const kCGHeadInsertEventTap: u32 = 0;
pub const kCGEventTapOptionDefault: u32 = 0;
pub const kCGEventKeyDown: u32 = 10;
pub const kCGEventKeyUp: u32 = 11;
pub const kCGEventFlagsChanged: u32 = 12;
pub const kCGKeyboardEventKeycode: u32 = 9;
pub const kCFRunLoopRunFinished: i32 = 1;
pub type CGEventTapCallBack = unsafe extern "C" fn(
proxy: CGEventTapProxy,
event_type: CGEventType,
event: CGEventRef,
user_info: *mut c_void,
) -> CGEventRef;
#[link(name = "CoreGraphics", kind = "framework")]
extern "C" {}
#[link(name = "CoreFoundation", kind = "framework")]
extern "C" {}
#[link(name = "ApplicationServices", kind = "framework")]
extern "C" {}
extern "C" {
pub static kCFAllocatorDefault: CFAllocatorRef;
pub static kCFRunLoopCommonModes: CFRunLoopMode;
pub static kCFRunLoopDefaultMode: CFRunLoopMode;
pub fn CGEventTapCreate(
tap: u32,
place: u32,
options: u32,
events_of_interest: u64,
callback: CGEventTapCallBack,
user_info: *mut c_void,
) -> CFMachPortRef;
pub fn CGEventTapEnable(tap: CFMachPortRef, enable: bool);
pub fn CFMachPortCreateRunLoopSource(
allocator: CFAllocatorRef,
port: CFMachPortRef,
order: i64,
) -> CFRunLoopSourceRef;
pub fn CGEventGetIntegerValueField(event: CGEventRef, field: u32) -> i64;
pub fn CFRunLoopGetCurrent() -> CFRunLoopRef;
pub fn CFRunLoopAddSource(
rl: CFRunLoopRef,
source: CFRunLoopSourceRef,
mode: CFRunLoopMode,
);
pub fn CFRunLoopRemoveSource(
rl: CFRunLoopRef,
source: CFRunLoopSourceRef,
mode: CFRunLoopMode,
);
pub fn CFRunLoopRunInMode(mode: CFRunLoopMode, seconds: f64, return_after: bool) -> i32;
pub fn CFRelease(cf: *const c_void);
pub fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> bool;
}
}
pub fn is_accessibility_trusted() -> bool {
unsafe { ffi::AXIsProcessTrustedWithOptions(std::ptr::null()) }
}
pub fn prompt_accessibility_permission() {
let _ = std::process::Command::new("open")
.arg("x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent")
.spawn();
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HotkeyEvent {
Press,
Release,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HotkeyMonitorStatus {
Starting,
Active,
Failed(String),
Stopped,
}
pub struct HotkeyMonitor {
stop: Arc<AtomicBool>,
_thread: Option<std::thread::JoinHandle<()>>,
}
impl HotkeyMonitor {
pub fn start<F, S>(keycode: i64, callback: F, status_callback: S) -> Result<Self, String>
where
F: Fn(HotkeyEvent) + Send + 'static,
S: Fn(HotkeyMonitorStatus) + Send + 'static,
{
let stop = Arc::new(AtomicBool::new(false));
let stop_clone = Arc::clone(&stop);
let boxed_callback: Box<dyn Fn(HotkeyEvent) + Send> = Box::new(callback);
let boxed_status_callback: Box<dyn Fn(HotkeyMonitorStatus) + Send> =
Box::new(status_callback);
let thread = std::thread::Builder::new()
.name("hotkey-monitor".into())
.spawn(move || {
run_event_tap(keycode, boxed_callback, boxed_status_callback, stop_clone);
})
.map_err(|err| format!("Could not spawn hotkey monitor: {}", err))?;
Ok(HotkeyMonitor {
stop,
_thread: Some(thread),
})
}
pub fn stop(&self) {
self.stop.store(true, Ordering::Relaxed);
}
}
impl Drop for HotkeyMonitor {
fn drop(&mut self) {
self.stop();
}
}
struct TapContext {
target_keycode: i64,
callback: Box<dyn Fn(HotkeyEvent) + Send>,
stop: Arc<AtomicBool>,
key_is_down: AtomicBool,
}
fn run_event_tap(
target_keycode: i64,
callback: Box<dyn Fn(HotkeyEvent) + Send>,
status_callback: Box<dyn Fn(HotkeyMonitorStatus) + Send>,
stop: Arc<AtomicBool>,
) {
let event_mask: u64 =
(1 << ffi::kCGEventKeyDown) | (1 << ffi::kCGEventKeyUp) | (1 << ffi::kCGEventFlagsChanged);
let context = Box::new(TapContext {
target_keycode,
callback,
stop: Arc::clone(&stop),
key_is_down: AtomicBool::new(false),
});
let context_ptr = Box::into_raw(context) as *mut std::ffi::c_void;
status_callback(HotkeyMonitorStatus::Starting);
unsafe {
let tap = ffi::CGEventTapCreate(
ffi::kCGHIDEventTap,
ffi::kCGHeadInsertEventTap,
ffi::kCGEventTapOptionDefault,
event_mask,
event_tap_callback,
context_ptr,
);
if tap.is_null() {
let message =
"Could not start native hotkey. Enable Minutes in System Settings > Privacy & Security > Input Monitoring, then try again.";
tracing::error!("{}", message);
let _ = Box::from_raw(context_ptr as *mut TapContext);
status_callback(HotkeyMonitorStatus::Failed(message.to_string()));
return;
}
tracing::info!(keycode = target_keycode, "native hotkey monitor started");
let source = ffi::CFMachPortCreateRunLoopSource(ffi::kCFAllocatorDefault, tap, 0);
if source.is_null() {
let message = "Could not start native hotkey run loop.";
tracing::error!("{}", message);
ffi::CFRelease(tap as *const std::ffi::c_void);
let _ = Box::from_raw(context_ptr as *mut TapContext);
status_callback(HotkeyMonitorStatus::Failed(message.to_string()));
return;
}
let run_loop = ffi::CFRunLoopGetCurrent();
ffi::CFRunLoopAddSource(run_loop, source, ffi::kCFRunLoopCommonModes);
ffi::CGEventTapEnable(tap, true);
status_callback(HotkeyMonitorStatus::Active);
while !stop.load(Ordering::Relaxed) {
let result = ffi::CFRunLoopRunInMode(ffi::kCFRunLoopDefaultMode, 0.5, false);
if result == ffi::kCFRunLoopRunFinished {
break;
}
}
ffi::CGEventTapEnable(tap, false);
ffi::CFRunLoopRemoveSource(run_loop, source, ffi::kCFRunLoopCommonModes);
ffi::CFRelease(source as *const std::ffi::c_void);
ffi::CFRelease(tap as *const std::ffi::c_void);
let _ = Box::from_raw(context_ptr as *mut TapContext);
}
tracing::info!("native hotkey monitor stopped");
status_callback(HotkeyMonitorStatus::Stopped);
}
unsafe extern "C" fn event_tap_callback(
_proxy: ffi::CGEventTapProxy,
event_type: ffi::CGEventType,
event: ffi::CGEventRef,
user_info: *mut std::ffi::c_void,
) -> ffi::CGEventRef {
let context = &*(user_info as *const TapContext);
if context.stop.load(Ordering::Relaxed) {
return event;
}
let keycode = ffi::CGEventGetIntegerValueField(event, ffi::kCGKeyboardEventKeycode);
if keycode != context.target_keycode {
return event; }
match event_type {
ffi::kCGEventKeyDown => {
if !context.key_is_down.swap(true, Ordering::Relaxed) {
(context.callback)(HotkeyEvent::Press);
}
std::ptr::null_mut() }
ffi::kCGEventKeyUp => {
context.key_is_down.store(false, Ordering::Relaxed);
(context.callback)(HotkeyEvent::Release);
std::ptr::null_mut() }
ffi::kCGEventFlagsChanged => {
let was_down = context.key_is_down.load(Ordering::Relaxed);
if was_down {
context.key_is_down.store(false, Ordering::Relaxed);
(context.callback)(HotkeyEvent::Release);
} else {
context.key_is_down.store(true, Ordering::Relaxed);
(context.callback)(HotkeyEvent::Press);
}
std::ptr::null_mut() }
_ => event, }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accessibility_check_returns_bool() {
let _ = is_accessibility_trusted();
}
#[test]
fn constants_are_correct() {
assert_eq!(KEYCODE_CAPS_LOCK, 57);
assert_eq!(KEYCODE_FN, 63);
}
}