@@ -14,8 +14,13 @@ final class EventTapManager {
1414 case middle = 2
1515 }
1616
17+ enum ResolvedAction {
18+ case quit
19+ case forceQuit
20+ }
21+
1722 // Return true to consume the event (prevent Dock from seeing it)
18- typealias MiddleClickHandler = ( _ locationInScreen: CGPoint ) -> Bool
23+ typealias MiddleClickHandler = ( _ locationInScreen: CGPoint , _ action : ResolvedAction ) -> Bool
1924
2025 private var eventTap : CFMachPort ?
2126 private var runLoopSource : CFRunLoopSource ?
@@ -27,31 +32,33 @@ final class EventTapManager {
2732 // Public read-only: whether this tap can swallow events (depends on creation mode)
2833 private( set) var canSwallow = false
2934
30- // Accessor for the current activation mode (injected so changes are live)
31- private var activationModeProvider : ( ( ) -> Preferences . ActivationMode ) ?
35+ // Provider to resolve action based on current modifiers. Return nil for "no action".
36+ private var actionResolver : ( ( NSEvent . ModifierFlags ) -> ResolvedAction ? ) ?
3237
33- func start( activationMode : @escaping ( ) -> Preferences . ActivationMode ,
38+ func start( resolveAction : @escaping ( NSEvent . ModifierFlags ) -> ResolvedAction ? ,
3439 handler: @escaping MiddleClickHandler ) {
3540 self . handler = handler
36- self . activationModeProvider = activationMode
41+ self . actionResolver = resolveAction
3742
38- // Listen only for middle (other) mouse clicks, down and up.
39- let mask =
40- ( 1 << CGEventType . otherMouseDown. rawValue) |
41- ( 1 << CGEventType . otherMouseUp. rawValue)
43+ // Build mask in a way that works across SDKs (UInt vs UInt64)
44+ let downShift = CGEventMask ( CGEventType . otherMouseDown. rawValue)
45+ let upShift = CGEventMask ( CGEventType . otherMouseUp. rawValue)
46+ let downBit : CGEventMask = CGEventMask ( 1 ) << downShift
47+ let upBit : CGEventMask = CGEventMask ( 1 ) << upShift
48+ let mask : CGEventMask = downBit | upBit
4249
4350 // Try session default (captures synthetic events, can swallow)
44- if createTap ( tap: . cgSessionEventTap, options: . defaultTap, eventsOfInterest: CGEventMask ( mask) ) {
51+ if createTap ( tap: . cgSessionEventTap, options: . defaultTap, eventsOfInterest: mask) {
4552 canSwallow = true
4653 print ( " EventTap: using cgSessionEventTap (default) — can swallow " )
4754 }
4855 // Else try HID default (captures hardware events, can swallow)
49- else if createTap ( tap: . cghidEventTap, options: . defaultTap, eventsOfInterest: CGEventMask ( mask) ) {
56+ else if createTap ( tap: . cghidEventTap, options: . defaultTap, eventsOfInterest: mask) {
5057 canSwallow = true
5158 print ( " EventTap: using cghidEventTap (default) — can swallow " )
5259 }
5360 // Else, try session listen-only (observe only, cannot swallow)
54- else if createTap ( tap: . cgSessionEventTap, options: . listenOnly, eventsOfInterest: CGEventMask ( mask) ) {
61+ else if createTap ( tap: . cgSessionEventTap, options: . listenOnly, eventsOfInterest: mask) {
5562 canSwallow = false
5663 print ( " EventTap: using cgSessionEventTap (listenOnly) — cannot swallow " )
5764 } else {
@@ -88,15 +95,16 @@ final class EventTapManager {
8895 return Unmanaged . passUnretained ( event)
8996 }
9097
91- // Check activation mode against current modifier flags
92- if !manager. matchesActivationMode ( event: event) {
93- return Unmanaged . passUnretained ( event)
94- }
95-
9698 let loc = event. location
97- if let shouldConsume = manager. handler ? ( loc) , shouldConsume, manager. canSwallow {
98- manager. swallowNextOtherMouseUp = true
99- return nil // swallow down
99+
100+ // CGEvent.flags.rawValue is UInt64; NSEvent.ModifierFlags.RawValue is UInt.
101+ let flags = NSEvent . ModifierFlags ( rawValue: UInt ( event. flags. rawValue) )
102+
103+ if let action = manager. actionResolver ? ( flags) {
104+ if let shouldConsume = manager. handler ? ( loc, action) , shouldConsume, manager. canSwallow {
105+ manager. swallowNextOtherMouseUp = true
106+ return nil // swallow down
107+ }
100108 }
101109 return Unmanaged . passUnretained ( event)
102110 } else {
@@ -123,28 +131,6 @@ final class EventTapManager {
123131 return false
124132 }
125133
126- private func matchesActivationMode( event: CGEvent ) -> Bool {
127- let flags = event. flags
128- let mode = activationModeProvider ? ( ) ?? . none
129-
130- switch mode {
131- case . none:
132- // Require no primary modifiers (ignore caps lock, numeric pad, function)
133- return !flags. contains ( . maskCommand)
134- && !flags. contains ( . maskAlternate)
135- && !flags. contains ( . maskControl)
136- && !flags. contains ( . maskShift)
137- case . command:
138- return flags. contains ( . maskCommand)
139- case . option:
140- return flags. contains ( . maskAlternate)
141- case . control:
142- return flags. contains ( . maskControl)
143- case . shift:
144- return flags. contains ( . maskShift)
145- }
146- }
147-
148134 func stop( ) {
149135 if let tap = eventTap {
150136 CGEvent . tapEnable ( tap: tap, enable: false )
@@ -157,7 +143,7 @@ final class EventTapManager {
157143 handler = nil
158144 swallowNextOtherMouseUp = false
159145 canSwallow = false
160- activationModeProvider = nil
146+ actionResolver = nil
161147 }
162148}
163149
0 commit comments