use std::cell::RefCell;
use std::collections::HashMap;
use core_foundation::base::{CFTypeRef, TCFType as _};
use core_foundation::number::CFNumber;
use core_foundation::string::{CFString, CFStringRef};
use core_graphics::event::CGEvent;
use foreign_types_shared::ForeignType as _;
use crate::EventDevice;
type IOHIDEventRef = *mut std::ffi::c_void;
#[link(name = "CoreGraphics", kind = "framework")]
unsafe extern "C" {
fn CGEventCopyIOHIDEvent(event: *const std::ffi::c_void) -> IOHIDEventRef;
}
#[link(name = "IOKit", kind = "framework")]
unsafe extern "C" {
fn IOHIDEventGetSenderID(event: IOHIDEventRef) -> u64;
}
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
fn CFRelease(cf: *const std::ffi::c_void);
}
pub(super) fn event_sender_id(event: &CGEvent) -> Option<u64> {
let hid = unsafe { CGEventCopyIOHIDEvent(event.as_ptr().cast()) };
if hid.is_null() {
return None;
}
let sender = unsafe { IOHIDEventGetSenderID(hid) };
unsafe { CFRelease(hid) };
Some(sender)
}
type IoObjectT = u32;
#[link(name = "IOKit", kind = "framework")]
unsafe extern "C" {
fn IORegistryEntryIDMatching(entry_id: u64) -> *mut std::ffi::c_void;
fn IOServiceGetMatchingService(main_port: u32, matching: *const std::ffi::c_void) -> IoObjectT;
fn IORegistryEntrySearchCFProperty(
entry: IoObjectT,
plane: *const std::ffi::c_char,
key: CFStringRef,
allocator: CFTypeRef,
options: u32,
) -> CFTypeRef;
fn IOObjectRelease(object: IoObjectT) -> i32;
}
const IO_REGISTRY_ITERATE_RECURSIVELY: u32 = 1;
const IO_REGISTRY_ITERATE_PARENTS: u32 = 2;
fn open_service(sender_id: u64) -> Option<IoObjectT> {
let matching = unsafe { IORegistryEntryIDMatching(sender_id) };
if matching.is_null() {
return None;
}
let service = unsafe { IOServiceGetMatchingService(0, matching) };
(service != 0).then_some(service)
}
fn service_property(service: IoObjectT, key: &str) -> Option<CFTypeRef> {
let cf_key = CFString::new(key);
let plane = c"IOService";
let prop = unsafe {
IORegistryEntrySearchCFProperty(
service,
plane.as_ptr(),
cf_key.as_concrete_TypeRef(),
std::ptr::null(),
IO_REGISTRY_ITERATE_RECURSIVELY | IO_REGISTRY_ITERATE_PARENTS,
)
};
(!prop.is_null()).then_some(prop)
}
#[derive(Clone, Default)]
pub(super) struct SenderDeviceInfo {
pub(super) event_device: EventDevice,
pub(super) is_trackpad: bool,
}
pub(super) fn sender_device_info(sender_id: u64) -> SenderDeviceInfo {
thread_local! {
static CACHE: RefCell<HashMap<u64, SenderDeviceInfo>> = RefCell::new(HashMap::new());
}
CACHE.with_borrow_mut(|cache| {
cache
.entry(sender_id)
.or_insert_with(|| {
let Some(service) = open_service(sender_id) else {
return SenderDeviceInfo::default();
};
let string_prop = |k| {
service_property(service, k)
.map(|p| unsafe { CFString::wrap_under_create_rule(p.cast()) }.to_string())
};
let num_prop = |k| {
service_property(service, k)
.and_then(|p| {
unsafe { CFNumber::wrap_under_create_rule(p.cast()) }.to_i64()
})
.and_then(|n| u32::try_from(n).ok())
};
let product_name = string_prop("Product");
let info = SenderDeviceInfo {
is_trackpad: product_name
.as_deref()
.is_some_and(|p| p.to_lowercase().contains("trackpad")),
event_device: EventDevice {
vendor_id: num_prop("VendorID").or_else(|| num_prop("idVendor")),
product_id: num_prop("ProductID").or_else(|| num_prop("idProduct")),
product_name,
},
};
unsafe { IOObjectRelease(service) };
info
})
.clone()
})
}