Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/0350b4af.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
bump: patch
---

More reliable cmd-cmd chord detection via a session event tap.
55 changes: 51 additions & 4 deletions Sources/cmdcmd/CmdChord.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,23 +31,68 @@ 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<CmdChord>.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() {
if leftDown || rightDown { contaminated = true }
}

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
}
Expand Down
Loading