Skip to content

Commit

Permalink
refactor: commit experimentations folder for posterity
Browse files Browse the repository at this point in the history
  • Loading branch information
lwouis committed Nov 27, 2024
1 parent c8eb091 commit f56c65d
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 0 deletions.
108 changes: 108 additions & 0 deletions src/experimentations/IOKit.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import Foundation
import IOKit.hid
import Carbon.HIToolbox
import CoreGraphics

class IOKitPrototype {
static let manager = IOHIDManagerCreate(kCFAllocatorDefault, IOOptionBits(kIOHIDOptionsTypeNone))

static func initialize() {
IOHIDManagerSetDeviceMatching(manager, [
kIOHIDDeviceUsagePageKey: kHIDPage_GenericDesktop,
kIOHIDDeviceUsageKey: kHIDUsage_GD_Keyboard
] as CFDictionary)

IOHIDManagerRegisterInputValueCallback(manager, keyboardEventHandler, nil)
IOHIDManagerScheduleWithRunLoop(manager, BackgroundWork.keyboardEventsThread.runLoop!, CFRunLoopMode.commonModes.rawValue)
IOHIDManagerOpen(manager, IOOptionBits(kIOHIDOptionsTypeNone))
}
}

func keyCodeToString(keyCode: UInt32) -> String? {
guard let keyboardLayoutPtr = TISCopyCurrentKeyboardLayoutInputSource()?.takeRetainedValue() else {
print("Could not retrieve keyboard layout data")
return nil
}
guard let layoutDataPtr = TISGetInputSourceProperty(keyboardLayoutPtr, kTISPropertyUnicodeKeyLayoutData) else {
print("Keyboard layout data is nil")
return nil
}

let layoutBytes = CFDataGetBytePtr(unsafeBitCast(layoutDataPtr, to: CFData.self))
let layoutPtr = unsafeBitCast(layoutBytes, to: UnsafePointer<UCKeyboardLayout>.self)

var chars: [UniChar] = Array(repeating: 0, count: 4)
var actualLength: Int = 0
let modifierFlags = UInt32(0) // You can populate this based on event flags if available
var deadKeyState: UInt32 = 0

let osStatus = UCKeyTranslate(
layoutPtr,
UInt16(keyCode), // Adjust usage to match standard key mapping
UInt16(kUCKeyActionDown), // Pressed key
modifierFlags,
UInt32(LMGetKbdType()), // Keyboard Type
OptionBits(kUCKeyTranslateNoDeadKeysBit),
&deadKeyState,
chars.count,
&actualLength,
&chars
)

if osStatus == noErr, actualLength > 0 {
return String(utf16CodeUnits: chars, count: actualLength)
}
return nil
}

func keyCodeToStringUsingCG(keyCode: UInt32) -> String? {
// Create a CGEventSource to simulate the event
// let eventSource = CGEventSource(stateID: .hidSystemState)
// Create a keydown event for the specified keycode
let keyDownEvent = CGEvent(keyboardEventSource: nil, virtualKey: CGKeyCode(keyCode), keyDown: true)
// Create a buffer to store the Unicode string result
var unicodeString = [UniChar](repeating: 0, count: 4)
var actualLength: Int = 0
// Get the Unicode string corresponding to the key press
keyDownEvent?.keyboardGetUnicodeString(maxStringLength: unicodeString.count, actualStringLength: &actualLength, unicodeString: &unicodeString)
// Check if the call was successful and return the corresponding string
if actualLength > 0 {
return String(utf16CodeUnits: unicodeString, count: actualLength)
}
// If something went wrong, return nil
print("Failed to get Unicode string for keycode \(keyCode)")
return nil
}

func keyboardEventHandler(_: UnsafeMutableRawPointer?, _: IOReturn, _: UnsafeMutableRawPointer?, event: IOHIDValue) {
let element = IOHIDValueGetElement(event)
if IOHIDElementGetUsagePage(element) != kHIDPage_KeyboardOrKeypad {
return
}
let scancode = IOHIDElementGetUsage(element)
if scancode < 4 || scancode > 231 {
return
}
let pressed = IOHIDValueGetIntegerValue(event) == 1

// TIS calls have to happen on the main thread
// Apple docs: TextInputSources API is not thread safe. If you are a UI application, you must call TextInputSources API on the main thread
DispatchQueue.main.sync {
let inputSource = TISCopyCurrentKeyboardLayoutInputSource()?.takeRetainedValue()
// print(inputSource)
let inputSourceID = TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID)!
let sourceId = unsafeBitCast(inputSourceID, to: CFString?.self)
// print("Input Source ID: \(sourceId)")
// print(scancode, keyCodeToString(keyCode: scancode))
// print(scancode, keyCodeToStringUsingCG(keyCode: scancode))

// let keyCode = Int(scancode - 4) // Subtract 4 to align with key codes in `UCKeyTranslate`
// if let keyString = keyCodeToString(keyCode: keyCode) {
// let state = pressed ? "Pressed" : "Released"
// print("Key '\(keyString)' \(state)")
// } else {
// let state = pressed ? "Pressed" : "Released"
// print("Key Code \(keyCode) \(state)")
// }
}
}
13 changes: 13 additions & 0 deletions src/experimentations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
| API | supports modifiers-only shortcuts | event propagation | works through Secure Input | can work from background thread | Requires Input Monitoring permission |
|---|---|---|---|---|---|
| `CGEvent.tapCreate` | **Yes** | **Can propagate or not** | No (modifiers work) | **Yes** | **No** |
| `RegisterEventHotKey`/`InstallEventHandler` | No | Can't propagate; but not needed | **Yes** | No | **No** |
| `NSEvent.addGlobalMonitorForEvents ` | **Yes** | Can't propagate | No (modifiers work) | No | **No** |
| `CGSSetHotModifierWithExclusion` + `CGSSetHotKeyWithExclusion` | **Yes** | Can't propagate | **Yes** | No | **No** |
| `IOHIDManagerRegisterInputValueCallback` | **Yes** | ? | No (modifiers work) | **Yes** | Yes |

Also to consider:

* macOS may send keyboard events disordered
* macOS may not send some keyboard events (e.g. when under heavy load)
* when macOS sends an event, other APIs may disagree (e.g. receiving a keyDown event for `shift`. Calling `NSEvent.modifierFlags` can return that `shift` is down, or that it's not down. It's data-racy.

0 comments on commit f56c65d

Please sign in to comment.