Skip to content

Commit 3b1c3b3

Browse files
committed
1.0.0
1 parent a334103 commit 3b1c3b3

15 files changed

+1159
-1
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/.swiftpm/xcode/package.xcworkspace/xcuserdata/phil.xcuserdatad/UserInterfaceState.xcuserstate
2+
.DS_Store
3+
.swiftpm

Package.resolved

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// swift-tools-version: 6.0
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: "EventTapCore",
8+
platforms: [
9+
.macOS("11.0"),
10+
],
11+
products: [
12+
// Products define the executables and libraries a package produces, making them visible to other packages.
13+
.library(
14+
name: "EventTapCore",
15+
targets: ["EventTapCore"]),
16+
],
17+
dependencies: [
18+
.package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"),
19+
],
20+
targets: [
21+
// Targets are the basic building blocks of a package, defining a module or a test suite.
22+
// Targets can depend on other targets in this package and products from dependencies.
23+
.target(
24+
name: "EventTapCore",
25+
dependencies: [
26+
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
27+
]
28+
),
29+
30+
]
31+
)

README.md

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,124 @@
11
# EventTapCore
2-
Swift wrapper around CGEvent and the Event Tap set of APIs.
2+
3+
EventTapCore is a Swift module that provides a wrapper around `CGEvent` and related types, as well as a high-level, type-safe interface for monitoring and handling system events on macOS. It wraps the low-level Core Graphics event tap APIs in a modern, Swift-friendly way with support for async streams and a SwiftUI integration.
4+
5+
## Features
6+
7+
- Rich event metadata and field information
8+
- Type-safe event monitoring with async/await support
9+
- SwiftUI-ready with `@Observable` integration
10+
11+
## Installation
12+
13+
### Swift Package Manager
14+
15+
Add the following to your `Package.swift` file:
16+
17+
```swift
18+
dependencies: [
19+
.package(url: "https://github.com/philptr/EventTapCore.git", from: "1.0.0")
20+
]
21+
```
22+
23+
## Basic Usage
24+
25+
### Simple Event Monitoring
26+
27+
```swift
28+
import EventTapCore
29+
30+
let tap = EventTap(tapLocation: .session, tapPlacement: .head)
31+
32+
try tap.startMonitoring { type, event in
33+
print("Received event of type \(type): \(event).")
34+
}
35+
```
36+
37+
### Using Async Streams
38+
39+
```swift
40+
import EventTapCore
41+
42+
let eventStream = EventTap.events(at: .session, placement: .head)
43+
44+
for await event in eventStream {
45+
print("Received event: \(event).")
46+
}
47+
```
48+
49+
### SwiftUI Integration with EventTapCoordinator
50+
51+
```swift
52+
import EventTapCore
53+
54+
@main
55+
struct MyApp: App {
56+
@State private var coordinator = EventTapCoordinator()
57+
58+
var body: some Scene {
59+
WindowGroup {
60+
EventList(events: coordinator.events)
61+
.onAppear {
62+
coordinator.startMonitoring()
63+
}
64+
}
65+
}
66+
}
67+
```
68+
69+
## Interface
70+
71+
### `EventTap`
72+
73+
The primary class for creating and managing event taps, as well as receiving callbacks. It provides both closure-based and async stream APIs for event monitoring.
74+
75+
### `EventTapCoordinator`
76+
77+
A higher level, SwiftUI-friendly coordinator that manages event monitoring and state, providing:
78+
- Observable event collection;
79+
- Automatic event throttling;
80+
- Thread-safe event handling;
81+
- Lifecycle management.
82+
83+
### Event
84+
85+
A structure that represents a system event with rich metadata including timestamp, location, event type, and associated field values.
86+
87+
## Security and Permissions
88+
89+
EventTapCore requires Input Monitoring permissions to monitor events from other applications. This requirement is imposed by the low-level API. In your application, you might want to include an experience to allow the user to grant your application access.
90+
91+
```swift
92+
// Check if we have permission to monitor events.
93+
if CGPreflightListenEventAccess() {
94+
// Start monitoring...
95+
} else {
96+
// Request permission.
97+
CGRequestListenEventAccess()
98+
99+
// Offer to take the user to System Settings...
100+
}
101+
```
102+
103+
## Best Practices
104+
105+
1. Always handle tap lifecycle properly:
106+
```swift
107+
// Start monitoring when needed
108+
try tap.startMonitoring(...)
109+
110+
// Stop monitoring when done
111+
tap.stopMonitoring()
112+
```
113+
114+
2. Consider event throttling, especially when displaying events in the UI or altering state:
115+
```swift
116+
coordinator.startMonitoring(throttledFor: .milliseconds(500))
117+
```
118+
119+
3. Check for Input Monitoring permissions before starting.
120+
121+
## Requirements
122+
123+
- macOS 11.0 or later
124+
- Swift 6.0 or later
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
//
2+
// Event.swift
3+
// EventTapper
4+
//
5+
// Created by Phil Zakharchenko on 2/23/24.
6+
//
7+
8+
import AppKit
9+
10+
/// A structure that represents a system event with its associated metadata and properties.
11+
/// Events can represent various user interactions such as keyboard input, mouse movements,
12+
/// and gesture recognition.
13+
public struct Event: Hashable, Sendable {
14+
/// A field containing an integer value associated with an event.
15+
/// Used for properties like key codes, click counts, and other discrete measurements.
16+
public struct IntField: EventField {
17+
/// The key identifying what this field represents.
18+
public let key: EventFieldKey
19+
20+
/// The integer value associated with this field.
21+
public let value: Int64
22+
}
23+
24+
/// A field containing a floating-point value associated with an event.
25+
/// Used for properties like mouse deltas, gesture scales, and other continuous measurements.
26+
public struct DoubleField: EventField {
27+
/// The key identifying what this field represents.
28+
public let key: EventFieldKey
29+
30+
/// The floating-point value associated with this field.
31+
public let value: Double
32+
}
33+
34+
/// The raw Quartz timestamp of when the event occurred.
35+
/// Measured in nanoseconds since system startup.
36+
public let rawTimestamp: CGEventTimestamp
37+
38+
/// The date when the event occurred, converted from the raw timestamp.
39+
public let date: Date
40+
41+
/// The type of event (e.g., mouse click, key press, gesture).
42+
public let type: EventTypeKey
43+
44+
/// The mouse cursor location in screen coordinates.
45+
/// Origin is at the bottom-left corner of the primary screen.
46+
public let mouseLocation: CGPoint
47+
48+
/// The mouse cursor location with Y-coordinate unflipped.
49+
/// Origin is at the top-left corner of the primary screen.
50+
public let unflippedLocation: CGPoint
51+
52+
/// The Unicode character associated with a keyboard event, if any.
53+
/// This will be nil for non-keyboard events or special keys.
54+
public let keyboardKey: UniChar?
55+
56+
/// A string representation of the keyboard character, if available.
57+
/// This will be nil for non-keyboard events or special keys.
58+
public let keyboardKeyString: String?
59+
60+
/// The set of modifier flags active during the event.
61+
public let flags: Set<EventFlag>
62+
63+
/// Additional integer-valued fields associated with the event.
64+
/// These vary based on the event type and may include things like:
65+
/// - Mouse click count.
66+
/// - Keyboard repeat count.
67+
/// - Process IDs.
68+
public let intFields: [IntField]
69+
70+
/// Additional floating-point fields associated with the event.
71+
/// These vary based on the event type and may include things like:
72+
/// - Scroll wheel deltas.
73+
/// - Gesture scales.
74+
/// - Mouse movement deltas.
75+
public let doubleFields: [DoubleField]
76+
77+
/// Creates a new `Event` instance from a CoreGraphics event and its type.
78+
///
79+
/// This initializer processes the raw `CGEvent` to extract all relevant information
80+
/// and store it in a more accessible format.
81+
///
82+
/// - Parameters:
83+
/// - event: The CoreGraphics event to process
84+
/// - eventType: The type of the event
85+
public init(event: CGEvent, of eventType: CGEventType) {
86+
rawTimestamp = event.timestamp
87+
88+
// Convert the timestamp to a Date for easier handling.
89+
let eventTime = TimeInterval(Double(rawTimestamp) / 10e8)
90+
date = Date(timeInterval: eventTime, since: .systemStartup)
91+
92+
// Store basic event properties.
93+
type = EventTypeKey(rawValue: eventType.rawValue)
94+
mouseLocation = event.location
95+
unflippedLocation = event.unflippedLocation
96+
flags = event.flags.eventFlagSet
97+
98+
// Extract keyboard character if present.
99+
var char = UniChar()
100+
var length = 0
101+
event.keyboardGetUnicodeString(maxStringLength: 1, actualStringLength: &length, unicodeString: &char)
102+
103+
if char != 0 {
104+
keyboardKey = char
105+
106+
if keyboardKey != 0, let scalar = UnicodeScalar(char) {
107+
keyboardKeyString = String(Character(scalar))
108+
} else {
109+
keyboardKeyString = nil
110+
}
111+
} else {
112+
keyboardKey = nil
113+
keyboardKeyString = nil
114+
}
115+
116+
// Extract additional event fields.
117+
var intFields: [IntField] = []
118+
var doubleFields: [DoubleField] = []
119+
120+
// Check all possible field indices.
121+
// The range 0..<200 covers all currently defined CGEventFields.
122+
(0..<200).forEach { fieldIndex in
123+
let rawValue = UInt32(fieldIndex)
124+
guard let field = CGEventField(rawValue: rawValue) else { return }
125+
126+
// Try to get both integer and double values.
127+
let intValue = event.getIntegerValueField(field)
128+
let doubleValue = event.getDoubleValueField(field)
129+
130+
// Store non-zero values in appropriate arrays.
131+
if intValue != 0 {
132+
intFields.append(.init(key: EventFieldKey(rawValue: rawValue), value: intValue))
133+
} else if doubleValue != 0 {
134+
doubleFields.append(.init(key: EventFieldKey(rawValue: rawValue), value: doubleValue))
135+
}
136+
}
137+
138+
self.intFields = intFields
139+
self.doubleFields = doubleFields
140+
}
141+
}
142+
143+
extension Date {
144+
/// The time when the system was started up.
145+
/// Calculated by subtracting the system uptime from the current date.
146+
static let systemStartup = Date(timeIntervalSinceNow: -ProcessInfo.processInfo.systemUptime)
147+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// EventField.swift
3+
// EventTapper
4+
//
5+
// Created by Phil Zakharchenko on 2/23/24.
6+
//
7+
8+
import Foundation
9+
10+
public protocol EventField: Identifiable, Hashable, Sendable {
11+
associatedtype Key: EventInfoKey where Key.RawValue: BinaryInteger
12+
associatedtype Value: Numeric & CustomStringConvertible
13+
14+
var key: Key { get }
15+
var value: Value { get }
16+
}
17+
18+
extension EventField {
19+
public var id: Key.RawValue { key.rawValue }
20+
}

0 commit comments

Comments
 (0)