|
| 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 | +} |
0 commit comments