Skip to content

Instantly share code, notes, and snippets.

@baquaz
Forked from stephancasas/CGEventSupervisor.swift
Created January 10, 2025 21:28
Show Gist options
  • Save baquaz/387048aea7b70a07766fd69e3f0184e6 to your computer and use it in GitHub Desktop.
Save baquaz/387048aea7b70a07766fd69e3f0184e6 to your computer and use it in GitHub Desktop.
A service class for monitoring global keyboard events in macOS Swift applications.
//
// CGEventSupervisor.swift
// Mouseless Messenger Pro
//
// Created by Stephan Casas on 4/24/23.
//
import Cocoa;
class CGEventSupervisor {
static let shared = CGEventSupervisor();
// MARK: - Constants
private let kDefaultEvents: [CGEventType] = [.keyDown, .leftMouseDown];
// MARK: - Public / Control
/// Begin listening for events of the given type.
///
func listen(for events: CGEventType...) { self.setup(for: events) }
/// Begin listening for the last-assigned type of events or, when no
/// events have been assigned, the default set of events.
///
func listen() { self.setup() }
/// Stop listening for events.
func hangup() { self.teardown() }
// MARK: - Public / Callbacks
var nsEventCallbacks: NSEventCallbackStore { self.__nsEventCallbacks }
var cgEventCallbacks: CGEventCallbackStore { self.__cgEventCallbacks }
/// Install a named callback to evaluate to captured events as
/// instances of `NSEvent`.
///
func install(_ callback: CallbackDescriptor<NSEventCallback>, as name: String) {
self.__nsEventCallbacks.updateValue(callback.rawValue, forKey: name);
}
/// Install a named callback to evaluate to captured events as
/// instances of `CGEvent`.
///
func install(_ callback: CallbackDescriptor<CGEventCallback>, as name: String) {
self.__cgEventCallbacks.updateValue(callback.rawValue, forKey: name);
}
/// Uninstall a named callback which may be evaluating captured
/// events.
///
func uninstall(callback: String) {
self.__nsEventCallbacks.removeValue(forKey: callback);
self.__cgEventCallbacks.removeValue(forKey: callback);
}
// MARK: - Private
private var listeningEvents: [CGEventType] = [];
private var __nsEventCallbacks: NSEventCallbackStore = [:];
private var __cgEventCallbacks: CGEventCallbackStore = [:];
private var eventTapMachPort: CFMachPort? = nil;
private func setup(for events: [CGEventType] = []) {
var events = events;
events = events.isEmpty ? (
self.listeningEvents.isEmpty ? kDefaultEvents : self.listeningEvents
) : events;
/// Use existing mach port if no events changed.
///
if let eventTapMachPort = self.eventTapMachPort,
self.listeningEvents == events
{
CGEvent.tapEnable(tap: eventTapMachPort, enable: true);
return;
}
self.listeningEvents = events;
let eventMask = CGEventMask(self.listeningEvents.reduce(0, {
$0 | (1 << $1.rawValue)
}));
guard let eventTapMachPort = CGEvent.tapCreate(
tap: .cgSessionEventTap,
place: .headInsertEventTap,
options: .defaultTap,
eventsOfInterest: eventMask,
callback: CGEventSupervisorCallback,
userInfo: Unmanaged.passUnretained(self).toOpaque()
) else {
NSLog("CGEventSupervisor could not allocate the required mach port for CGEventTap.")
return;
}
/// Responding to Carbon/Quartz events -- use the main run loop.
///
let runLoop = CFRunLoopGetCurrent();
let runLoopSrc = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTapMachPort, 0);
CFRunLoopAddSource(runLoop, runLoopSrc, .commonModes);
CGEvent.tapEnable(tap: eventTapMachPort, enable: true);
self.eventTapMachPort = eventTapMachPort;
}
private func teardown() {
guard let eventTapMachPort = self.eventTapMachPort else { return }
CGEvent.tapEnable(tap: eventTapMachPort, enable: false);
}
// MARK: - Types
/// # Captured Event Callbacks
///
/// The callbacks which will evaluate captured events.
///
/// Callbacks may be configured to receive instances of
/// either `NSEvent` or `CGEvent` — depending on the use
/// case.
///
/// From within a callback, `stop()` can be called to end
/// event propagation — preventing any subsequent callbacks
/// and the current first responder from receiving the event.
///
typealias NSEventCallback = (_ event: NSEvent) -> Void;
typealias CGEventCallback = (_ event: CGEvent) -> Void;
/// # Captured Event Callbacks Dictionary
///
/// The dictionary store for captured event
/// callbacks.
///
typealias NSEventCallbackStore = [String: NSEventCallback];
typealias CGEventCallbackStore = [String: CGEventCallback];
struct CallbackDescriptor<T>: RawRepresentable {
var rawValue: T;
}
}
// MARK: - Event Supervisory Extensions
fileprivate var kSupervisedCGEventShouldCancelKey: String = "__SupervisedCGEventShouldCancel";
fileprivate var kSupervisedNSEventShouldCancelKey: String = "__SupervisedNSEventShouldCancel";
extension CGEvent {
/// If the event is supervised by `CGEventSupervisor`, setting this
/// to `true` will stop the event from propagating to the next-installed
/// callback and will prevent the event from reaching its intended target.
///
fileprivate var supervisorShouldCancel: Bool {
set { objc_setAssociatedObject(
self,
&kSupervisedCGEventShouldCancelKey,
newValue,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
) }
get { objc_getAssociatedObject(
self,
&kSupervisedCGEventShouldCancelKey
) as? Bool ?? false }
}
/// If the event is supervised by `CGEventSupervisor`, end
/// propagation into the next-installed callback and do not
/// send the event to its user-intended target.
///
func cancel() {
self.supervisorShouldCancel = true;
}
}
extension NSEvent {
/// If the event is supervised by `CGEventSupervisor`, setting this
/// to `true` will stop the event from propagating to the next-installed
/// callback and will prevent the event from reaching its intended target.
///
fileprivate var supervisorShouldCancel: Bool {
set { objc_setAssociatedObject(
self,
&kSupervisedNSEventShouldCancelKey,
newValue,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
) }
get { objc_getAssociatedObject(
self,
&kSupervisedNSEventShouldCancelKey
) as? Bool ?? false }
}
/// If the event is supervised by `CGEventSupervisor`, end
/// propagation into the next-installed callback and do not
/// send the event to its user-intended target.
///
func cancel() {
self.supervisorShouldCancel = true;
}
}
// MARK: - Callback Type Descriptors
extension CGEventSupervisor.CallbackDescriptor where T == CGEventSupervisor.NSEventCallback {
static func nsEventCallback(_ callback: T) -> Self { .init(rawValue: callback) }
}
extension CGEventSupervisor.CallbackDescriptor where T == CGEventSupervisor.CGEventCallback {
static func cgEventCallback(_ callback: T) -> Self { .init(rawValue: callback) }
}
// MARK: - Main Event Callback
/// The main event callback which will perform casting
/// for `CGEvent` to `NSEvent` and resolution of the
/// given opaque pointer into `CGEventSupervisor`.
///
fileprivate func CGEventSupervisorCallback(
_ eventTap: CGEventTapProxy,
_ eventType: CGEventType,
_ event: CGEvent,
_ userData: UnsafeMutableRawPointer?
) -> Unmanaged<CGEvent>? {
guard
let supervisorRef = userData,
let nsEvent = NSEvent(cgEvent: event)
else {
return Unmanaged.passUnretained(event);
}
let supervisor = Unmanaged<CGEventSupervisor>
.fromOpaque(supervisorRef)
.takeUnretainedValue();
var bubbles = true;
for callback in supervisor.cgEventCallbacks.values {
callback(event);
bubbles = !event.supervisorShouldCancel;
if !bubbles { return nil }
}
for callback in supervisor.nsEventCallbacks.values {
callback(nsEvent);
bubbles = !nsEvent.supervisorShouldCancel;
if !bubbles { return nil }
}
return Unmanaged.passUnretained(event);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment