TapGuard provides a Swift 6 ready, thread-safe abstraction layer for asynchronous processing of CGEvents
with support for macOS Catalina (.macOS(.v10_15)
) and above.
It offers tools for managing multiple event streams and receivers in a concurrent environment.
- Asynchronous Event Processing: Utilize Swift Concurrency for modern, non-blocking event handling.
- Thread-Safe: Built with thread safety in mind.
- Event Suspension: Temporarily suspend event processing for scenarios like UI interactions or recording shortcuts.
- System Prerequisites: Automatically handles system-level conditions (e.g., Accessibility API requirements) for enabling or disabling event sources.
Add the package to your Package.swift
package dependencies:
dependencies: [
.package(url: "https://github.com/Akazm/osx-tap-guard", from: "1.0.0")
]
Then, add TapGuard
to your target's dependencies:
dependencies: [
.product(name: "TapGuard", package: "tap-guard")
]
Initialize HIDEventDispatcher
TapGuard provides a convenient way to initialize a HIDEventDispatcher
with a backing
CGEventTap.
import TapGuard
let dispatcher = HIDEventDispatcher.systemDispatcher(
enabled: true,
eventsOfInterest: CGEventMask(1 << kCGEventKeyDown | 1 << kCGEventKeyUp),
eventTapLocation: .cgSessionEventTap
)
For event processing to function as one might expect already by now, several conditions must be met.
- Screens & Device must be awake
- At least one receiver must be present
- HIDEventDispatcher is not suspended
- HIDEventDispatcher is enabled
- Accessibility API access must have been granted: Access to the macOS Accessibility API must be granted by the
application's user. Prompting the user to grant access is out of scope for this package, but a
HIDEventDispatcher
attributes automatically for the Accessibility API access status.
Unless or when all of the above conditions have been met, a HIDEventDispatcher
will automatically remove or
install the backing CGEventTap.
For more information, see HIDEventDispatcherEnabledPrerequisite .
HIDEventDispatcher
allows you to register multiple
event receivers. This is also refered
to as event processing pipeline.
let stream = dispatcher.stream()
Task {
for await event in stream {
print("Received event: \(event)")
}
}
For more information, see stream(withPriority:)
class MyReceiver: HIDEventReceiver {
var hidEventProcessor: HIDEventProcessor {
.sync { event in
print("Event: \(event)")
return PostProcessHIDEventInstruction.pass
}
}
// Optional (default: UInt64(UInt32.max))
var hidEventReceiverPriority: UInt64 {
UInt64(UInt32.max)
}
// Optional (default: true)
var hidEventReceiverEnabled: Bool {
true
}
}
For more information, see HIDEventReceiver.
Event processing happens within the closure specified for the defined HIDEventReceiver. In the example, the closure returns a PostProcessHIDEventInstruction to specify how to postprocess the event after the closure exited.
To enable async event processing, use an async
processor instead.
class MyAsyncReceiver: HIDEventReceiver {
var hidEventProcessor: HIDEventProcessor {
.async { event in
print("Event received: \(event)")
//await something here
return PostProcessHIDEventInstruction.pass
}
}
// Optional (default: UInt64(UInt32.max))
var hidEventReceiverPriority: UInt64 {
UInt64(UInt32.max)
}
// Optional (default: true)
var hidEventReceiverEnabled: Bool {
true
}
}
For more information, see HIDEventProcessor
.
Add a receiver with a synchronous callback:
let receiver = dispatcher.addReceiver { event in
print("Event received: \(event)")
return .pass
}
Add a receiver with an asynchronous callback:
let receiver = dispatcher.addReceiver { event in
print("Event received: \(event)")
//await something here
return .pass
}
For more information, see HIDEventDispatcher
.
Any receiver can be removed by calling the remove()
method:
let receiver = MyReceiver()
let registration = dispatcher.addReceiver(myReceiver)
// Remove the receiver
registration.remove()
For more information, see DisposableHIDEventReceiver
.
Manually enable or disable the dispatcher:
dispatcher.setEnabled(true) // Enable
Aside from enabling or disabling a dispatcher, it can also be suspended. Like a disabled dispatcher, a suspended dispatcher will not process events - this is semantically idential. However, analogous to adding and removing receivers, suspensions can be acquired and released indepentendly from each other.
let suspension = dispatcher.acquireSuspension()
// Release the suspension when done
suspension.release()
For more information, see acquireSuspension()
.
Async event processing is supported in order to achieve formal thread safety first and foremost, but not to await time-intensive tasks that rely on networking, File I/O or CPU-intensive tasks.
Doing so nonetheless might result in the following:
- MacOS disables a dispatcher's backing event tap (kCGEventTapDisabledByTimeout). It will be re-enabled automatically.
- MacOS ignores the
PostProcessHIDEventInstruction
, effectively replacing it with .bypass behaviour.
See here.
Contributions are welcome! Please submit issues or pull requests on the official repository.