├── .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 |
--------------------------------------------------------------------------------