diff --git a/.changeset/0350b4af.md b/.changeset/0350b4af.md new file mode 100644 index 0000000..4c4300e --- /dev/null +++ b/.changeset/0350b4af.md @@ -0,0 +1,5 @@ +--- +bump: patch +--- + +More reliable cmd-cmd chord detection via a session event tap. diff --git a/Sources/cmdcmd/CmdChord.swift b/Sources/cmdcmd/CmdChord.swift index ae5dafc..80f09ad 100644 --- a/Sources/cmdcmd/CmdChord.swift +++ b/Sources/cmdcmd/CmdChord.swift @@ -5,6 +5,8 @@ import Carbon.HIToolbox /// with no other key pressed during the chord. final class CmdChord { private var monitors: [Any] = [] + private var eventTap: CFMachPort? + private var eventTapRunLoopSource: CFRunLoopSource? private var leftDown = false private var rightDown = false private var contaminated = false @@ -29,10 +31,50 @@ final class CmdChord { return e } monitors = [global, local, globalKey, localKey].compactMap { $0 } + installEventTap() } deinit { for m in monitors { NSEvent.removeMonitor(m) } + if let eventTap { CGEvent.tapEnable(tap: eventTap, enable: false) } + if let eventTapRunLoopSource { + CFRunLoopRemoveSource(CFRunLoopGetMain(), eventTapRunLoopSource, .commonModes) + } + } + + private func installEventTap() { + let mask = (1 << CGEventType.flagsChanged.rawValue) | (1 << CGEventType.keyDown.rawValue) + let callback: CGEventTapCallBack = { _, type, event, userInfo in + guard let userInfo else { return Unmanaged.passUnretained(event) } + let chord = Unmanaged.fromOpaque(userInfo).takeUnretainedValue() + let keyCode = Int(event.getIntegerValueField(.keyboardEventKeycode)) + let flags = event.flags + DispatchQueue.main.async { + if type == .keyDown { + chord.markContaminated() + } else if type == .flagsChanged { + chord.handleFlags(keyCode: keyCode, flags: flags) + } + } + return Unmanaged.passUnretained(event) + } + let ref = Unmanaged.passUnretained(self).toOpaque() + guard let tap = CGEvent.tapCreate( + tap: .cgSessionEventTap, + place: .headInsertEventTap, + options: .listenOnly, + eventsOfInterest: CGEventMask(mask), + callback: callback, + userInfo: ref + ) else { + Log.write("cmd-cmd event tap unavailable") + return + } + eventTap = tap + let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0) + eventTapRunLoopSource = source + CFRunLoopAddSource(CFRunLoopGetMain(), source, .commonModes) + CGEvent.tapEnable(tap: tap, enable: true) } private func markContaminated() { @@ -40,12 +82,17 @@ final class CmdChord { } private func handleFlags(_ event: NSEvent) { - let cmd = event.modifierFlags.contains(.command) - switch Int(event.keyCode) { + let raw = event.cgEvent?.flags ?? CGEventFlags(rawValue: UInt64(event.modifierFlags.rawValue)) + handleFlags(keyCode: Int(event.keyCode), flags: raw) + } + + private func handleFlags(keyCode: Int, flags: CGEventFlags) { + let raw = flags.rawValue + switch keyCode { case kVK_Command: - leftDown = cmd && event.modifierFlags.rawValue & 0x8 != 0 + leftDown = raw & CGEventFlags.maskCommand.rawValue != 0 && raw & 0x8 != 0 case kVK_RightCommand: - rightDown = cmd && event.modifierFlags.rawValue & 0x10 != 0 + rightDown = raw & CGEventFlags.maskCommand.rawValue != 0 && raw & 0x10 != 0 default: return }