From 70a0d3c87677d058c0f0e1fd19307fee3432d042 Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Sun, 24 Oct 2021 11:41:34 -0700 Subject: [PATCH] Implement Resolving Hotkeys on macOS This adds the implementation for resolving `KeyCode` on macOS based on the user's current keyboard layout. --- crates/livesplit-hotkey/src/macos/carbon.rs | 56 ++++++++++ crates/livesplit-hotkey/src/macos/cf.rs | 3 + crates/livesplit-hotkey/src/macos/mod.rs | 118 +++++++++++++++++++- 3 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 crates/livesplit-hotkey/src/macos/carbon.rs diff --git a/crates/livesplit-hotkey/src/macos/carbon.rs b/crates/livesplit-hotkey/src/macos/carbon.rs new file mode 100644 index 00000000..120b405a --- /dev/null +++ b/crates/livesplit-hotkey/src/macos/carbon.rs @@ -0,0 +1,56 @@ +#![allow(dead_code)] + +use super::cf::StringRef; +use std::os::raw::{c_ulong, c_void}; + +mod opaque { + pub enum TISInputSource {} + pub enum UCKeyboardLayout {} +} + +pub type TISInputSourceRef = *mut opaque::TISInputSource; + +pub type OptionBits = u32; +pub type UniCharCount = c_ulong; +pub type UniChar = u16; +pub type OSStatus = i32; + +bitflags::bitflags! { + #[repr(transparent)] + pub struct UCKeyTranslateBits: OptionBits { + const NO_DEAD_KEYS_BIT = 0; + } +} + +#[repr(u16)] +#[non_exhaustive] +pub enum UCKeyAction { + Down = 0, + Up = 1, + AutoKey = 2, + Display = 3, +} + +#[link(name = "Carbon", kind = "framework")] +extern "C" { + pub static kTISPropertyUnicodeKeyLayoutData: StringRef; + pub fn TISCopyCurrentKeyboardInputSource() -> TISInputSourceRef; + pub fn TISCopyCurrentKeyboardLayoutInputSource() -> TISInputSourceRef; + pub fn TISGetInputSourceProperty( + input_source: TISInputSourceRef, + property_key: StringRef, + ) -> *mut c_void; + pub fn UCKeyTranslate( + key_layout_ptr: *const opaque::UCKeyboardLayout, + virtual_key_code: u16, + key_action: u16, + modifier_key_state: u32, + keyboard_type: u32, + key_translate_options: OptionBits, + dead_key_state: *mut u32, + max_string_length: UniCharCount, + actual_string_length: *mut UniCharCount, + unicode_string: *mut UniChar, + ) -> OSStatus; + pub fn LMGetKbdType() -> u8; +} diff --git a/crates/livesplit-hotkey/src/macos/cf.rs b/crates/livesplit-hotkey/src/macos/cf.rs index bc67f556..e0aaaaa0 100644 --- a/crates/livesplit-hotkey/src/macos/cf.rs +++ b/crates/livesplit-hotkey/src/macos/cf.rs @@ -6,6 +6,7 @@ mod opaque { pub enum RunLoop {} pub enum RunLoopSource {} pub enum String {} + pub enum Data {} } pub type AllocatorRef = *mut opaque::Allocator; @@ -15,6 +16,7 @@ pub type RunLoopSourceRef = *mut opaque::RunLoopSource; pub type StringRef = *const opaque::String; pub type TypeRef = *const c_void; +pub type DataRef = *const opaque::Data; pub type RunLoopMode = StringRef; @@ -62,4 +64,5 @@ extern "C" { pub fn CFRelease(cf: TypeRef); + pub fn CFDataGetBytePtr(the_data: DataRef) -> *const u8; } diff --git a/crates/livesplit-hotkey/src/macos/mod.rs b/crates/livesplit-hotkey/src/macos/mod.rs index f3d8826c..f684cea1 100644 --- a/crates/livesplit-hotkey/src/macos/mod.rs +++ b/crates/livesplit-hotkey/src/macos/mod.rs @@ -1,11 +1,17 @@ +mod carbon; mod cf; mod cg; use self::{ + carbon::{ + kTISPropertyUnicodeKeyLayoutData, LMGetKbdType, TISCopyCurrentKeyboardInputSource, + TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, UCKeyAction, + UCKeyTranslate, UCKeyTranslateBits, + }, cf::{ - kCFAllocatorDefault, kCFRunLoopDefaultMode, CFMachPortCreateRunLoopSource, CFRelease, - CFRunLoopAddSource, CFRunLoopContainsSource, CFRunLoopGetCurrent, CFRunLoopRemoveSource, - CFRunLoopRun, + kCFAllocatorDefault, kCFRunLoopDefaultMode, CFDataGetBytePtr, + CFMachPortCreateRunLoopSource, CFRelease, CFRunLoopAddSource, CFRunLoopContainsSource, + CFRunLoopGetCurrent, CFRunLoopRemoveSource, CFRunLoopRun, }, cg::{ CGEventTapCreate, EventMask, EventRef, EventTapLocation, EventTapOptions, @@ -293,6 +299,108 @@ unsafe extern "C" fn callback( event } -pub(crate) fn try_resolve(_key_code: KeyCode) -> Option { - None +pub(crate) fn try_resolve(key_code: KeyCode) -> Option { + unsafe { + let current_keyboard_raw = TISCopyCurrentKeyboardInputSource(); + if current_keyboard_raw.is_null() { + return None; + } + let mut current_keyboard = Owned(current_keyboard_raw); + + let mut layout_data = + TISGetInputSourceProperty(current_keyboard.0, kTISPropertyUnicodeKeyLayoutData); + + if layout_data.is_null() { + let current_keyboard_raw = TISCopyCurrentKeyboardLayoutInputSource(); + if current_keyboard_raw.is_null() { + return None; + } + current_keyboard = Owned(current_keyboard_raw); + + layout_data = + TISGetInputSourceProperty(current_keyboard.0, kTISPropertyUnicodeKeyLayoutData); + if layout_data.is_null() { + return None; + } + } + + let keyboard_layout = CFDataGetBytePtr(layout_data.cast()); + + let key_code = match key_code { + KeyCode::Backquote => 0x32, + KeyCode::Backslash => 0x2A, + KeyCode::Backspace => 0x33, + KeyCode::BracketLeft => 0x21, + KeyCode::BracketRight => 0x1E, + KeyCode::Comma => 0x2B, + KeyCode::Digit0 => 0x1D, + KeyCode::Digit1 => 0x12, + KeyCode::Digit2 => 0x13, + KeyCode::Digit3 => 0x14, + KeyCode::Digit4 => 0x15, + KeyCode::Digit5 => 0x17, + KeyCode::Digit6 => 0x16, + KeyCode::Digit7 => 0x1A, + KeyCode::Digit8 => 0x1C, + KeyCode::Digit9 => 0x19, + KeyCode::Equal => 0x18, + KeyCode::IntlBackslash => 0x0A, + KeyCode::IntlRo => 0x5E, + KeyCode::IntlYen => 0x5D, + KeyCode::KeyA => 0x00, + KeyCode::KeyB => 0x0B, + KeyCode::KeyC => 0x08, + KeyCode::KeyD => 0x02, + KeyCode::KeyE => 0x0E, + KeyCode::KeyF => 0x03, + KeyCode::KeyG => 0x05, + KeyCode::KeyH => 0x04, + KeyCode::KeyI => 0x22, + KeyCode::KeyJ => 0x26, + KeyCode::KeyK => 0x28, + KeyCode::KeyL => 0x25, + KeyCode::KeyM => 0x2E, + KeyCode::KeyN => 0x2D, + KeyCode::KeyO => 0x1F, + KeyCode::KeyP => 0x23, + KeyCode::KeyQ => 0x0C, + KeyCode::KeyR => 0x0F, + KeyCode::KeyS => 0x01, + KeyCode::KeyT => 0x11, + KeyCode::KeyU => 0x20, + KeyCode::KeyV => 0x09, + KeyCode::KeyW => 0x0D, + KeyCode::KeyX => 0x07, + KeyCode::KeyY => 0x10, + KeyCode::KeyZ => 0x06, + KeyCode::Minus => 0x1B, + KeyCode::Period => 0x2F, + KeyCode::Quote => 0x27, + KeyCode::Semicolon => 0x29, + KeyCode::Slash => 0x2C, + _ => return None, + }; + + let mut chars = [0; 4]; + let mut len = 0; + + UCKeyTranslate( + keyboard_layout.cast(), + key_code, + UCKeyAction::Display as _, + 0, + LMGetKbdType() as _, + UCKeyTranslateBits::NO_DEAD_KEYS_BIT.bits(), + &mut 0, + 4, + &mut len, + chars.as_mut_ptr(), + ); + + if len == 0 { + return None; + } + + String::from_utf16(&chars[..len as usize]).ok() + } }