Skip to content

Swift 6 ready, thread-safe abstraction layer for asynchronous processing of CGEvents

License

Notifications You must be signed in to change notification settings

Akazm/osx-tap-guard

Repository files navigation

TapGuard

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.

Features

  • 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.

Installation

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")
    ]

Usage

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
)

Satisfying prerequesites

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 .

Processing events

HIDEventDispatcher allows you to register multiple event receivers. This is also refered to as event processing pipeline.

Using an AsyncStream:

let stream = dispatcher.stream()

Task {
    for await event in stream {
        print("Received event: \(event)")
    }
}

For more information, see stream(withPriority:)

Custom conformance to HIDEventReceiver & AnyObject

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.

Using closures

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.

Removing receivers

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.

Enable or disable dispatcher

Manually enable or disable the dispatcher:

dispatcher.setEnabled(true) // Enable

Suspend dispatcher

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().

Notes on async event processing

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:

  1. MacOS disables a dispatcher's backing event tap (kCGEventTapDisabledByTimeout). It will be re-enabled automatically.
  2. MacOS ignores the PostProcessHIDEventInstruction, effectively replacing it with .bypass behaviour.

Documentation

See here.

Contributing

Contributions are welcome! Please submit issues or pull requests on the official repository.

About

Swift 6 ready, thread-safe abstraction layer for asynchronous processing of CGEvents

Resources

License

Stars

Watchers

Forks