├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Package.swift ├── README.md └── Sources └── CGEventSupervisor ├── CGEventSupervisor+Callback.swift ├── CGEventSupervisor+Setup.swift ├── CGEventSupervisor+Subscribers.swift ├── CGEventSupervisor+Types.swift ├── CGEventSupervisor.swift └── Extensions ├── CGEvent.swift └── NSEvent.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CGEventSupervisor", 8 | products: [ 9 | .library( 10 | name: "CGEventSupervisor", 11 | targets: ["CGEventSupervisor"]), 12 | ], 13 | targets: [ 14 | .target( 15 | name: "CGEventSupervisor", 16 | dependencies: []), 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CGEventSupervisor 2 | 3 | `CGEventSupervisor` offers you modernized utilities for global supervision and interception of HID events in your macOS Swift application — regardless of the application's frontmost status: 4 | 5 | ```swift 6 | // Example: Create a global shortcut using ⌘𝗙𝟯 7 | 8 | import Carbon; 9 | import Foundation; 10 | import CGEventSupervisor; 11 | 12 | let kCGEventSubscriberGlobalShortcut = "GlobalShortcut"; 13 | 14 | CGEventSupervisor.shared.subscribe( 15 | as: kCGEventSubscriberGlobalShortcut, 16 | to: .nsEvents(.keyDown), 17 | using: { 18 | guard 19 | $0.modifierFlags.contains(.command), 20 | $0.keyCode == kVK_F3 // Provided by Carbon 21 | else { return } 22 | 23 | NSLog("Shortcut did activate."); 24 | 25 | $0.cancel(); // Stop event propagation 26 | }); 27 | ``` 28 | 29 | ## Install 30 | 31 | Add the package to your project using Swift Package Manager: 32 | 33 | ```plaintext 34 | https://github.com/stephancasas/CGEventSupervisor 35 | ``` 36 | 37 | ## Usage 38 | 39 | After importing `CGEventSupervisor` in your Swift file, you can access the singleton `CGEventSupervisor.shared` service instance to subscribe event handlers using `CGEvent` and/or `NSEvent` objects. 40 | 41 | Subscribers are identified by a unique `String`, and can subscribe or cancel throughout the entire lifecycle of your application. For those subscribers which are persistent, it is recommended that you establish a global constant for to avoid collisions and/or memory leaks. 42 | 43 | Use the `.subscribe(as:to:using:)` function to create a new subscription for a specified collection of events. For example, you could use the following sample to log left and right mouse clicks with their on-screen coordinates: 44 | 45 | ```swift 46 | import Foundation; 47 | import CGEventSupervisor; 48 | 49 | let kCGEventSubscriberLogMouseClicks = "LogMouseClicks"; 50 | 51 | // ... 52 | 53 | CGEventSupervisor.shared.subscribe( 54 | as: kCGEventSubscriberLogMouseClicks, 55 | to: .cgEvents(.leftMouseDown, .rightMouseDown), 56 | using: { 57 | let btn = $0.type == .leftMouseDown ? "Left" : "Right"; 58 | NSLog("\(btn) click at \($0.location)"); 59 | }); 60 | ``` 61 | 62 | Only the specified event types will be provided to the subscriber's callback — even if you've added other subscribers which specify events of different types. 63 | 64 | To cancel a subscriber's event subscription, call the `.cancel(subscriber:)` function using the subscriber's unique `String` identity: 65 | 66 | ```swift 67 | import Foundation; 68 | import CGEventSupervisor; 69 | 70 | // ... 71 | 72 | CGEventSupervisor.shared.cancel( 73 | subscriber: kCGEventSubscriberLogMouseClicks); 74 | ``` 75 | 76 | When a subscription is canceled, `CGEventSupervisor` will evaluate event subscriptions for all other subscribers and re-define the `CGEventMask` if necessary or, if no subscribers remain, the `CGEventTap` will be disabled and the `CFMachPort` will be disposed. 77 | 78 | ### `CGEvent` vs. `NSEvent` 79 | 80 | While a `CGEventTap` is setup to provide access to events using `CGEvent`, in some cases, it makes more sense to work with `NSEvent`. The event descriptor passed to the `to:` argument of `.subscribe(as:to:using:)` can be given either as `.cgEvents(...)` or `.nsEvents(...)` — depending on your subscriber's specific needs. 81 | 82 | ## Cancelling/Intercepting HID Events 83 | 84 | In many circumstances, such as a global keyboard shortcut, you will want your application to exclusively handle the HID event without allowing the event to propagate to the frontmost application. To this end, `CGEventSupervisor` extends both `CGEvent` and `NSEvent` with a `.cancel()` method — callable from the context of an event subscriber's callback. 85 | 86 | Calling `.cancel()` on either event object will cause the event to cease propagation among both other event subscribers as well as propagation to the user-focused application responder. 87 | 88 | For example, the following sample will [disable the 𝗚 keyboard key](https://youtu.be/RgBDdDdSqNE?t=158) in every application: 89 | 90 | ```swift 91 | import Carbon; 92 | import Foundation; 93 | import CGEventSupervisor; 94 | 95 | let kCGEventSubscriberDisableGKey = "DisableGKey"; 96 | 97 | // ... 98 | 99 | CGEventSupervisor.shared.subscribe( 100 | as: kCGEventSubscriberDisableGKey, 101 | to: .nsEvents(.keyDown), 102 | using: { event in 103 | // Constant provided in Carbon 104 | if event.keyCode != kVK_ANSI_G { return } 105 | event.cancel(); 106 | }); 107 | ``` 108 | 109 | Similarly, you can use `.cancel()` on mouse events, too. For example, the following sample will prevent the user from clicking anywhere in the menubar: 110 | 111 | ```swift 112 | import Foundation; 113 | import CGEventSupervisor; 114 | 115 | let kCGEventSubscriberRestrictMenubar = "RestrictMenubar"; 116 | let kDefaultMenubarRect = CGRectMake(0, 0, .greatestFiniteMagnitude, 25); 117 | 118 | // ... 119 | 120 | CGEventSupervisor.shared.subscribe( 121 | as: kCGEventSubscriberRestrictMenubar, 122 | to: .cgEvents(.leftMouseDown), 123 | using: { 124 | if kDefaultMenubarRect.contains($0.location) { 125 | $0.cancel(); 126 | } 127 | }); 128 | ``` 129 | 130 | ## Say Hi 131 | 132 | [Follow Stephan on X](https://x.com/stephancasas) 133 | 134 | ## License 135 | 136 | MIT 137 | -------------------------------------------------------------------------------- /Sources/CGEventSupervisor/CGEventSupervisor+Callback.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGEventSupervisor+Callback.swift 3 | // 4 | // 5 | // Created by Stephan Casas on 8/6/23. 6 | // 7 | 8 | import Cocoa; 9 | import Foundation; 10 | 11 | /// The main event callback which will perform casting 12 | /// for `CGEvent` to `NSEvent` and resolution of the 13 | /// given opaque pointer into `CGEventSupervisor`. 14 | /// 15 | internal func CGEventSupervisorCallback( 16 | _ eventTap: CGEventTapProxy, 17 | _ eventType: CGEventType, 18 | _ event: CGEvent, 19 | _ userData: UnsafeMutableRawPointer? 20 | ) -> Unmanaged? { 21 | guard 22 | let supervisorRef = userData 23 | else { 24 | return Unmanaged.passUnretained(event); 25 | } 26 | 27 | let supervisor = Unmanaged 28 | .fromOpaque(supervisorRef) 29 | .takeUnretainedValue(); 30 | 31 | var bubbles = true; 32 | for subscriber in supervisor.cgEventSubscribers.values { 33 | if subscriber.events.contains(event.type){ 34 | subscriber.callback(event); 35 | bubbles = !event.supervisorShouldCancel; 36 | } 37 | if !bubbles { return nil } 38 | } 39 | 40 | /// Is the event eligible for casting to `NSEvent`? 41 | /// 42 | if event.type.rawValue > 0, 43 | event.type.rawValue <= kCGEventLastUniversalType.rawValue, 44 | supervisor.nsEventSubscribers.count > 0, 45 | let nsEvent = NSEvent(cgEvent: event) { 46 | 47 | for subscriber in supervisor.nsEventSubscribers.values { 48 | if subscriber.events.contains(event.type) { 49 | subscriber.callback(nsEvent); 50 | bubbles = !nsEvent.supervisorShouldCancel; 51 | } 52 | if !bubbles { return nil } 53 | } 54 | 55 | } 56 | 57 | return Unmanaged.passUnretained(event); 58 | } 59 | 60 | /// The last-supported universal event type in the enumerations 61 | /// of `NSEvent.EventType` and `CGEventType`. 62 | /// 63 | fileprivate let kCGEventLastUniversalType: CGEventType = .otherMouseDragged; 64 | -------------------------------------------------------------------------------- /Sources/CGEventSupervisor/CGEventSupervisor+Setup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGEventSupervisor+Setup.swift 3 | // 4 | // 5 | // Created by Stephan Casas on 8/6/23. 6 | // 7 | 8 | import Cocoa; 9 | import Foundation; 10 | 11 | extension CGEventSupervisor { 12 | 13 | /// Setup the event tap and receiving mach port for subscription 14 | /// to the currently-subscribed event types. 15 | /// 16 | internal func setup() { 17 | self.teardown(); 18 | 19 | if self.totalSubscribers == 0 { return } 20 | 21 | guard let eventTapMachPort = CGEvent.tapCreate( 22 | tap: .cgSessionEventTap, 23 | place: .headInsertEventTap, 24 | options: .defaultTap, 25 | eventsOfInterest: self.eventMask, 26 | callback: CGEventSupervisorCallback, 27 | userInfo: Unmanaged.passUnretained(self).toOpaque() 28 | ) else { 29 | NSLog("[CGEventSupervisor]: Could not allocate the required mach port for CGEventTap.") 30 | return; 31 | } 32 | 33 | let runLoop = CFRunLoopGetCurrent(); 34 | let runLoopSrc = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTapMachPort, 0); 35 | 36 | CFRunLoopAddSource(runLoop, runLoopSrc, .commonModes); 37 | CGEvent.tapEnable(tap: eventTapMachPort, enable: true); 38 | 39 | self.eventTapMachPort = eventTapMachPort; 40 | } 41 | 42 | /// Disable the event tap, and dispose of the receiving mach port. 43 | /// 44 | internal func teardown() { 45 | guard let eventTapMachPort = self.eventTapMachPort else { return } 46 | CGEvent.tapEnable(tap: eventTapMachPort, enable: false); 47 | self.eventTapMachPort = nil; 48 | } 49 | 50 | /// The `CGEventTap` mask/filter. 51 | /// 52 | var eventMask: CGEventMask { 53 | CGEventMask(self.subscribedEvents.reduce(0, { 54 | $0 | (1 << $1.rawValue) 55 | })) 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Sources/CGEventSupervisor/CGEventSupervisor+Subscribers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGEventSupervisor+Subscribers.swift 3 | // 4 | // 5 | // Created by Stephan Casas on 8/6/23. 6 | // 7 | 8 | import Cocoa; 9 | import Foundation; 10 | 11 | // MARK: - Subscribe/Unsubscribe 12 | 13 | public extension CGEventSupervisor { 14 | 15 | /// Register a subscription to global `NSEvent` events of 16 | /// the given type. 17 | /// - Parameters: 18 | /// - subscriber: The unique name of the subscriber. 19 | /// - events: The event types to which the subscriber will subscribe. 20 | /// - callback: The callback which will handle the subscribed event types. 21 | func subscribe( 22 | as subscriber: String, 23 | to events: EventDescriptor, 24 | using callback: @escaping NSEventCallback 25 | ) { 26 | if events.rawValue.isEmpty { return } 27 | 28 | let needsSetup = !events.rawValue.allSatisfy({ 29 | self.subscribedEvents.contains($0) 30 | }); 31 | 32 | self.__nsEventSubscribers.updateValue( 33 | (callback, events.rawValue), forKey: subscriber); 34 | 35 | if needsSetup { self.setup() } 36 | } 37 | 38 | /// Register a subscription to global `CGEvent` events of 39 | /// the given type. 40 | /// - Parameters: 41 | /// - subscriber: The unique name of the subscriber. 42 | /// - events: The event types to which the subscriber will subscribe. 43 | /// - callback: The callback which will handle the subscribed event types. 44 | func subscribe( 45 | as subscriber: String, 46 | to events: EventDescriptor, 47 | using callback: @escaping CGEventCallback 48 | ) { 49 | if events.rawValue.isEmpty { return } 50 | 51 | let needsSetup = !events.rawValue.allSatisfy({ 52 | self.subscribedEvents.contains($0) 53 | }); 54 | 55 | self.__cgEventSubscribers.updateValue( 56 | (callback, events.rawValue), forKey: subscriber); 57 | 58 | if needsSetup { self.setup() } 59 | } 60 | 61 | /// Cancel the named subscriber's event subscription. 62 | /// 63 | func cancel(subscriber: String) { 64 | let preSubscribedEvents = self.subscribedEvents; 65 | 66 | self.__nsEventSubscribers.removeValue(forKey: subscriber); 67 | self.__cgEventSubscribers.removeValue(forKey: subscriber); 68 | 69 | let postSubscribedEvents = self.subscribedEvents; 70 | let needsSetup = !preSubscribedEvents.allSatisfy({ 71 | postSubscribedEvents.contains($0) 72 | }); 73 | 74 | /// Remove events from the mask which no longer 75 | /// have any subscribers, or disable the tap if 76 | /// no subscribers remain. 77 | /// 78 | if needsSetup { self.setup() } 79 | } 80 | 81 | /// Cancel all event subscriptions and dispose of the `CGEventTap`. 82 | /// 83 | func cancelAll() { 84 | self.__cgEventSubscribers.removeAll(); 85 | self.__nsEventSubscribers.removeAll(); 86 | 87 | self.teardown(); 88 | } 89 | 90 | } 91 | 92 | // MARK: - Computed 93 | 94 | public extension CGEventSupervisor { 95 | 96 | /// All event types having at least one subscriber. 97 | /// 98 | var subscribedEvents: [CGEventType] { 99 | var allEvents = self.nsEventSubscribers.values.flatMap({ $0.events }); 100 | allEvents.append(contentsOf: self.cgEventSubscribers.values.flatMap({ $0.events })); 101 | 102 | var uniqueEvents = [CGEventType](); 103 | for event in allEvents { 104 | if uniqueEvents.contains(event) { continue } 105 | uniqueEvents.append(event); 106 | } 107 | 108 | return uniqueEvents; 109 | } 110 | 111 | /// The total number of event subscribers. 112 | /// 113 | var totalSubscribers: Int { 114 | self.nsEventSubscribers.count + self.cgEventSubscribers.count 115 | } 116 | 117 | } 118 | 119 | -------------------------------------------------------------------------------- /Sources/CGEventSupervisor/CGEventSupervisor+Types.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGEventSupervisor+Types.swift 3 | // 4 | // 5 | // Created by Stephan Casas on 8/6/23. 6 | // 7 | 8 | import Cocoa; 9 | import Foundation; 10 | 11 | // MARK: - Subscribers 12 | 13 | public extension CGEventSupervisor { 14 | 15 | typealias NSEventCallback = (_ event: NSEvent) -> Void 16 | typealias CGEventCallback = (_ event: CGEvent) -> Void 17 | 18 | typealias NSEventSubscriber = (callback: NSEventCallback, events: [CGEventType]); 19 | typealias CGEventSubscriber = (callback: CGEventCallback, events: [CGEventType]); 20 | 21 | typealias NSEventSubscriberStore = [String: NSEventSubscriber]; 22 | typealias CGEventSubscriberStore = [String: CGEventSubscriber]; 23 | 24 | } 25 | 26 | // MARK: - Events 27 | 28 | public extension CGEventSupervisor { 29 | 30 | /// A type which describes both the events to which a subscriber 31 | /// will subscribe as well as the format in which the event is 32 | /// expected — `NSEvent` or `CGEvent`. 33 | /// 34 | struct EventDescriptor: RawRepresentable { 35 | public var rawValue: [CGEventType]; 36 | 37 | public init(rawValue: [CGEventType]) { 38 | self.rawValue = rawValue; 39 | } 40 | } 41 | 42 | } 43 | 44 | //MARK: - CGEvent 45 | 46 | public extension CGEventSupervisor.EventDescriptor where T == CGEventType { 47 | 48 | /// The subscriber receives events from the `CGEventTap` which are of the 49 | /// given event types. 50 | /// 51 | static func cgEvents(_ events: CGEventType...) -> Self { 52 | .init(rawValue: events) 53 | } 54 | 55 | } 56 | 57 | // MARK: - NSEvent 58 | 59 | public extension CGEventSupervisor.EventDescriptor where T == NSEvent.EventType { 60 | 61 | /// The subscriber receives events from the `CGEventTap` which are of the 62 | /// given universal event types and which have been cast to `NSEvent`. 63 | /// 64 | static func nsEvents(_ events: UniversalEventType...) -> Self { 65 | .init(rawValue: events.compactMap({ CGEventType(rawValue: $0.rawValue)} )) 66 | } 67 | 68 | /// Events available in both the `CGEventType` and `NSEvent.EventType` 69 | /// enumerations. 70 | /// 71 | enum UniversalEventType: UInt32 { 72 | /* Mouse events. */ 73 | case leftMouseDown = 1; 74 | case leftMouseUp = 2; 75 | case rightMouseDown = 3; 76 | case rightMouseUp = 4; 77 | case mouseMoved = 5; 78 | case leftMouseDragged = 6; 79 | case rightMouseDragged = 7; 80 | 81 | /* Keyboard events. */ 82 | case keyDown = 10; 83 | case keyUp = 11; 84 | case flagsChanged = 12; 85 | 86 | /* Specialized control devices. */ 87 | case scrollWheel = 22; 88 | case tabletPointer = 23; 89 | case tabletProximity = 24; 90 | case otherMouseDown = 25; 91 | case otherMouseUp = 26; 92 | case otherMouseDragged = 27; 93 | } 94 | 95 | } 96 | 97 | -------------------------------------------------------------------------------- /Sources/CGEventSupervisor/CGEventSupervisor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGEventSupervisor.swift 3 | // 4 | // Created by Stephan Casas on 4/24/23. 5 | // 6 | 7 | import Foundation; 8 | 9 | public class CGEventSupervisor { 10 | 11 | public static let shared = CGEventSupervisor(); 12 | 13 | // MARK: - Public 14 | 15 | var nsEventSubscribers: NSEventSubscriberStore { self.__nsEventSubscribers } 16 | var cgEventSubscribers: CGEventSubscriberStore { self.__cgEventSubscribers } 17 | 18 | // MARK: - Internal 19 | 20 | internal var eventTapMachPort: CFMachPort? = nil; 21 | internal var __nsEventSubscribers: NSEventSubscriberStore = [:]; 22 | internal var __cgEventSubscribers: CGEventSubscriberStore = [:]; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Sources/CGEventSupervisor/Extensions/CGEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGEvent.swift 3 | // 4 | // 5 | // Created by Stephan Casas on 8/6/23. 6 | // 7 | 8 | import Cocoa; 9 | import Foundation 10 | 11 | fileprivate var kSupervisedCGEventShouldCancelKey: String = "__SupervisedCGEventShouldCancel"; 12 | 13 | public extension CGEvent { 14 | 15 | /// If the event is supervised by `CGEventSupervisor`, setting this 16 | /// to `true` will stop the event from propagating to the next-installed 17 | /// callback and will prevent the event from reaching its intended target. 18 | /// 19 | internal var supervisorShouldCancel: Bool { 20 | set { objc_setAssociatedObject( 21 | self, 22 | &kSupervisedCGEventShouldCancelKey, 23 | newValue, 24 | .OBJC_ASSOCIATION_RETAIN_NONATOMIC 25 | ) } 26 | get { objc_getAssociatedObject( 27 | self, 28 | &kSupervisedCGEventShouldCancelKey 29 | ) as? Bool ?? false } 30 | } 31 | 32 | /// If the event is supervised by `CGEventSupervisor`, end 33 | /// propagation into the next subscriber and do not 34 | /// send the event to its user-intended target. 35 | /// 36 | func cancel() { 37 | self.supervisorShouldCancel = true; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Sources/CGEventSupervisor/Extensions/NSEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSEvent.swift 3 | // 4 | // 5 | // Created by Stephan Casas on 8/6/23. 6 | // 7 | 8 | import Cocoa; 9 | import Foundation; 10 | 11 | fileprivate var kSupervisedNSEventShouldCancelKey: String = "__SupervisedNSEventShouldCancel"; 12 | 13 | public extension NSEvent { 14 | 15 | /// If the event is supervised by `CGEventSupervisor`, setting this 16 | /// to `true` will stop the event from propagating to the next-installed 17 | /// callback and will prevent the event from reaching its intended target. 18 | /// 19 | internal var supervisorShouldCancel: Bool { 20 | set { objc_setAssociatedObject( 21 | self, 22 | &kSupervisedNSEventShouldCancelKey, 23 | newValue, 24 | .OBJC_ASSOCIATION_RETAIN_NONATOMIC 25 | ) } 26 | get { objc_getAssociatedObject( 27 | self, 28 | &kSupervisedNSEventShouldCancelKey 29 | ) as? Bool ?? false } 30 | } 31 | 32 | /// If the event is supervised by `CGEventSupervisor`, end 33 | /// propagation into the next subscriber and do not 34 | /// send the event to its user-intended target. 35 | /// 36 | func cancel() { 37 | self.supervisorShouldCancel = true; 38 | } 39 | 40 | } 41 | --------------------------------------------------------------------------------