From 4c9e282122b6a31e099ff6567bd17adc91140da7 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Tue, 13 Oct 2026 00:32:28 +0100 Subject: [PATCH 1/2] Add system key events (media, volume, brightness) to hs.eventtap v2 had no equivalent of v1's hs.eventtap.event.newSystemKeyEvent(), so media keys couldn't be posted, and only real media key events go through macOS's Now Playing routing. Add makeSystemKeyEvent() and systemKeyStroke() using v1's key names, a systemKeys table, eventTypes.systemDefined, and an event.systemKey property so watchers can recognise these keys too. Fixes #268 Co-Authored-By: Claude Opus 5.5 --- .../Modules/hs.eventtap/HSEventTapEvent.swift | 23 +++++ .../hs.eventtap/HSEventTapModule.swift | 84 ++++++++++++++++ .../hs.eventtap/HSEventTapSystemKey.swift | 90 ++++++++++++++++++ .../HSEventTapIntegrationTests.swift | 95 +++++++++++++++++++ docs/migration-guide.md | 5 +- 5 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 Hammerspoon 2/Modules/hs.eventtap/HSEventTapSystemKey.swift diff --git a/Hammerspoon 2/Modules/hs.eventtap/HSEventTapEvent.swift b/Hammerspoon 2/Modules/hs.eventtap/HSEventTapEvent.swift index 9f97fb5a..ea752d58 100644 --- a/Hammerspoon 2/Modules/hs.eventtap/HSEventTapEvent.swift +++ b/Hammerspoon 2/Modules/hs.eventtap/HSEventTapEvent.swift @@ -104,6 +104,25 @@ import CoreGraphics /// ``` @objc var characters: String? { get } + /// For a system key event (media, volume, brightness and other special keys), an object + /// describing the key; for any other event, undefined. + /// + /// The object has `key` (a name from `hs.eventtap.systemKeys`, or "unknown"), `keyCode` + /// (the numeric system key code), `isDown` (true for key down, false for key up) and + /// `isRepeat` (true if the key is auto-repeating). Watch for these events with + /// `hs.eventtap.eventTypes.systemDefined`; other kinds of system-defined event, which don't + /// represent a key, also yield undefined. + /// - Example: + /// ```js + /// const tap = hs.eventtap.addWatcher([hs.eventtap.eventTypes.systemDefined], (event) => { + /// const sysKey = event.systemKey + /// if (sysKey && sysKey.key === "PLAY" && sysKey.isDown) console.log("Play pressed") + /// return hs.eventtap.emit + /// }) + /// tap.start() + /// ``` + @objc var systemKey: [String: Any]? { get } + /// Create an independent copy of this event /// - Returns: A new HSEventTapEvent with the same properties, or null if the copy failed /// - Example: @@ -212,6 +231,10 @@ import CoreGraphics return unsafe String(utf16CodeUnits: buffer, count: length) } + @objc var systemKey: [String: Any]? { + return SystemKey.decode(cgEvent) + } + @objc func duplicate() -> HSEventTapEvent? { guard let copy = cgEvent.copy() else { return nil } return HSEventTapEvent(cgEvent: copy) diff --git a/Hammerspoon 2/Modules/hs.eventtap/HSEventTapModule.swift b/Hammerspoon 2/Modules/hs.eventtap/HSEventTapModule.swift index 4c7e4d3f..d317d824 100644 --- a/Hammerspoon 2/Modules/hs.eventtap/HSEventTapModule.swift +++ b/Hammerspoon 2/Modules/hs.eventtap/HSEventTapModule.swift @@ -77,6 +77,20 @@ import AppKit /// ``` @objc var modifierFlags: [String: Int] { get } + /// A dictionary mapping system key names to their numeric codes, for use with + /// `makeSystemKeyEvent()` and `systemKeyStroke()`. + /// + /// System keys are the media, volume, brightness and other special keys that macOS delivers as + /// `systemDefined` events rather than ordinary key events: `SOUND_UP`, `SOUND_DOWN`, `MUTE`, + /// `BRIGHTNESS_UP`, `BRIGHTNESS_DOWN`, `CONTRAST_UP`, `CONTRAST_DOWN`, `POWER`, `LAUNCH_PANEL`, + /// `VIDMIRROR`, `PLAY`, `EJECT`, `NEXT`, `PREVIOUS`, `FAST`, `REWIND`, `ILLUMINATION_UP`, + /// `ILLUMINATION_DOWN`, `ILLUMINATION_TOGGLE`, `CAPS_LOCK`, `HELP` and `NUM_LOCK`. + /// - Example: + /// ```js + /// console.log("PLAY has code " + hs.eventtap.systemKeys.PLAY) + /// ``` + @objc var systemKeys: [String: Int] { get } + /// Return this from an event tap callback to suppress the event (prevent other apps from receiving it). /// - Example: /// ```js @@ -208,6 +222,24 @@ import AppKit /// ``` @objc func makeScrollWheelEvent(_ deltaX: Double, _ deltaY: Double, _ x: Double, _ y: Double) -> HSEventTapEvent? + /// Create a system key event, such as a media, volume or brightness key. + /// + /// Posting these behaves like pressing the key on an Apple keyboard, so media keys are routed + /// to whichever app macOS considers to be playing (including audio in a browser tab). + /// Synthetic brightness keys are ignored by some Macs and external displays; use `hs.screen` + /// to set brightness directly if they have no effect. + /// + /// - Parameters: + /// - key: A system key name from `hs.eventtap.systemKeys` (e.g. "PLAY", "SOUND_UP"), case-insensitive + /// - isDown: true for key down, false for key up + /// - Returns: An HSEventTapEvent, or null if the key name is unknown + /// - Example: + /// ```js + /// hs.eventtap.makeSystemKeyEvent("PLAY", true).post() + /// hs.eventtap.makeSystemKeyEvent("PLAY", false).post() + /// ``` + @objc func makeSystemKeyEvent(_ key: String, _ isDown: Bool) -> HSEventTapEvent? + // MARK: Convenience senders /// Send a key down and key up event with optional modifier keys. @@ -255,6 +287,21 @@ import AppKit /// ``` @objc func keyStrokesAsync(_ text: String) -> JSPromise? + /// Press and release a system key, such as a media, volume or brightness key. + /// + /// A 5 ms pause is inserted between the key-down and key-up events. See + /// `makeSystemKeyEvent()` for details of how these keys behave. + /// + /// Be careful with `POWER` and `EJECT`, which can show a shutdown prompt or eject media. + /// + /// - Parameter key: A system key name from `hs.eventtap.systemKeys` (e.g. "PLAY", "NEXT"), case-insensitive + /// - Example: + /// ```js + /// hs.eventtap.systemKeyStroke("PLAY") // Play/pause whatever is playing + /// hs.eventtap.systemKeyStroke("SOUND_UP") // Raise the volume, showing the system HUD + /// ``` + @objc func systemKeyStroke(_ key: String) + /// Post a left mouse button click at the given position. /// /// Coordinates are in **Hammerspoon screen coordinates** (top-left origin, y increases downward). @@ -550,6 +597,9 @@ import AppKit "otherMouseDown": Int(CGEventType.otherMouseDown.rawValue), "otherMouseUp": Int(CGEventType.otherMouseUp.rawValue), "otherMouseDragged": Int(CGEventType.otherMouseDragged.rawValue), + // Media, volume, brightness and other special keys (see systemKeys). CGEventType has no + // case for this, but CGEventTaps deliver it all the same. + "systemDefined": Int(NSEvent.EventType.systemDefined.rawValue), ] // Device-independent masks (high word of CGEventFlags.rawValue) @@ -584,6 +634,10 @@ import AppKit return _modifierFlags } + @objc var systemKeys: [String: Int] { + return SystemKey.codes + } + @objc var consume: Bool { false } @objc var emit: Bool { true } @@ -663,6 +717,24 @@ import AppKit return HSEventTapEvent(cgEvent: event) } + @objc func makeSystemKeyEvent(_ key: String, _ isDown: Bool) -> HSEventTapEvent? { + guard let code = resolveSystemKeyCode(key, caller: "makeSystemKeyEvent") else { return nil } + guard let event = SystemKey.makeEvent(code: code, isDown: isDown) else { + AKError("hs.eventtap.makeSystemKeyEvent: Failed to create event for \(key)") + return nil + } + return HSEventTapEvent(cgEvent: event) + } + + /// Looks up a system key name, logging an error naming the caller if it isn't a known key. + private func resolveSystemKeyCode(_ key: String, caller: String) -> Int? { + guard let code = SystemKey.code(named: key) else { + AKError("hs.eventtap.\(caller): Unknown system key '\(key)'") + return nil + } + return code + } + /// Resolves a JS key argument (a name/character or a numeric key code) through the current /// keyboard layout, logging an error naming the caller if it isn't a known key. private func resolveKeyCode(_ key: Any?, caller: String) -> CGKeyCode? { @@ -701,6 +773,18 @@ import AppKit } } + @objc func systemKeyStroke(_ key: String) { + guard let code = resolveSystemKeyCode(key, caller: "systemKeyStroke") else { return } + guard let downEvent = SystemKey.makeEvent(code: code, isDown: true), + let upEvent = SystemKey.makeEvent(code: code, isDown: false) else { + AKError("hs.eventtap.systemKeyStroke: Failed to create events for \(key)") + return + } + downEvent.post(tap: .cghidEventTap) + usleep(5000) + upEvent.post(tap: .cghidEventTap) + } + @objc func keyStrokes(_ text: String) { // A private-state source (rather than .hidSystemState) means freshly created events // start with empty flags instead of inheriting whatever real modifier keys are diff --git a/Hammerspoon 2/Modules/hs.eventtap/HSEventTapSystemKey.swift b/Hammerspoon 2/Modules/hs.eventtap/HSEventTapSystemKey.swift new file mode 100644 index 00000000..421dca13 --- /dev/null +++ b/Hammerspoon 2/Modules/hs.eventtap/HSEventTapSystemKey.swift @@ -0,0 +1,90 @@ +// +// HSEventTapSystemKey.swift +// Hammerspoon 2 +// + +import AppKit +import CoreGraphics +import IOKit.hidsystem + +/// Builds and decodes the system-defined (NX_SYSDEFINED) events that macOS uses for media, +/// volume, brightness and other special keys. +/// +/// These events carry the key in `data1`: the NX_KEYTYPE_* code in the high 16 bits, then the +/// key state (0xA = down, 0xB = up) in bits 8-15 and a repeat flag in bit 0. +nonisolated enum SystemKey { + /// NSEvent subtype for auxiliary control button (special key) events, NX_SUBTYPE_AUX_CONTROL_BUTTONS + static let auxControlButtonsSubtype: Int16 = 8 + + private static let keyDownState = 0xA + private static let keyUpState = 0xB + + /// System key names (matching Hammerspoon 1's) mapped to their NX_KEYTYPE_* codes + static let codes: [String: Int] = [ + "SOUND_UP": Int(NX_KEYTYPE_SOUND_UP), + "SOUND_DOWN": Int(NX_KEYTYPE_SOUND_DOWN), + "MUTE": Int(NX_KEYTYPE_MUTE), + "BRIGHTNESS_UP": Int(NX_KEYTYPE_BRIGHTNESS_UP), + "BRIGHTNESS_DOWN": Int(NX_KEYTYPE_BRIGHTNESS_DOWN), + "CONTRAST_UP": Int(NX_KEYTYPE_CONTRAST_UP), + "CONTRAST_DOWN": Int(NX_KEYTYPE_CONTRAST_DOWN), + "POWER": Int(NX_POWER_KEY), + "LAUNCH_PANEL": Int(NX_KEYTYPE_LAUNCH_PANEL), + "VIDMIRROR": Int(NX_KEYTYPE_VIDMIRROR), + "PLAY": Int(NX_KEYTYPE_PLAY), + "EJECT": Int(NX_KEYTYPE_EJECT), + "NEXT": Int(NX_KEYTYPE_NEXT), + "PREVIOUS": Int(NX_KEYTYPE_PREVIOUS), + "FAST": Int(NX_KEYTYPE_FAST), + "REWIND": Int(NX_KEYTYPE_REWIND), + "ILLUMINATION_UP": Int(NX_KEYTYPE_ILLUMINATION_UP), + "ILLUMINATION_DOWN": Int(NX_KEYTYPE_ILLUMINATION_DOWN), + "ILLUMINATION_TOGGLE": Int(NX_KEYTYPE_ILLUMINATION_TOGGLE), + "CAPS_LOCK": Int(NX_KEYTYPE_CAPS_LOCK), + "HELP": Int(NX_KEYTYPE_HELP), + "NUM_LOCK": Int(NX_KEYTYPE_NUM_LOCK), + ] + + private static let names: [Int: String] = Dictionary(uniqueKeysWithValues: codes.map { ($1, $0) }) + + /// Looks up a system key code by name, ignoring case + static func code(named name: String) -> Int? { + codes[name.uppercased()] + } + + /// Creates a system-defined key event, or nil if AppKit can't build one + @MainActor + static func makeEvent(code: Int, isDown: Bool) -> CGEvent? { + let state = isDown ? keyDownState : keyUpState + let nsEvent = NSEvent.otherEvent(with: .systemDefined, + location: .zero, + modifierFlags: NSEvent.ModifierFlags(rawValue: UInt(state << 8)), + timestamp: 0, + windowNumber: 0, + context: nil, + subtype: auxControlButtonsSubtype, + data1: (code << 16) | (state << 8), + data2: -1) + return nsEvent?.cgEvent + } + + /// Decodes a system-defined key event into its key name, code and state, or nil if the + /// event isn't a special key event + @MainActor + static func decode(_ event: CGEvent) -> [String: Any]? { + guard event.type.rawValue == UInt32(NSEvent.EventType.systemDefined.rawValue), + let nsEvent = NSEvent(cgEvent: event), + nsEvent.subtype.rawValue == auxControlButtonsSubtype else { + return nil + } + let data1 = nsEvent.data1 + let code = (data1 & 0xFFFF0000) >> 16 + let state = (data1 & 0xFF00) >> 8 + return [ + "key": names[code] ?? "unknown", + "keyCode": code, + "isDown": state == keyDownState, + "isRepeat": (data1 & 0x1) != 0, + ] + } +} diff --git a/Hammerspoon 2Tests/IntegrationTests/HSEventTapIntegrationTests.swift b/Hammerspoon 2Tests/IntegrationTests/HSEventTapIntegrationTests.swift index 0aeb948d..7fcc4a52 100644 --- a/Hammerspoon 2Tests/IntegrationTests/HSEventTapIntegrationTests.swift +++ b/Hammerspoon 2Tests/IntegrationTests/HSEventTapIntegrationTests.swift @@ -213,6 +213,26 @@ struct HSEventTapTests { #expect(makeHarness().evalTypeOf("hs.eventtap.keyRepeatInterval") == "function") } + @Test("makeSystemKeyEvent is a function") + func testMakeSystemKeyEventIsFunction() { + #expect(makeHarness().evalTypeOf("hs.eventtap.makeSystemKeyEvent") == "function") + } + + @Test("systemKeyStroke is a function") + func testSystemKeyStrokeIsFunction() { + #expect(makeHarness().evalTypeOf("hs.eventtap.systemKeyStroke") == "function") + } + + @Test("systemKeys is an object") + func testSystemKeysIsObject() { + #expect(makeHarness().evalTypeOf("hs.eventtap.systemKeys") == "object") + } + + @Test("eventTypes.systemDefined is the NX_SYSDEFINED type") + func testEventTypesSystemDefined() { + #expect(makeHarness().evalInt("hs.eventtap.eventTypes.systemDefined") == 14) + } + // MARK: Event object API @Test("event.post is a function") @@ -535,6 +555,81 @@ struct HSEventTapTests { #expect(!harness.hasException) } + // MARK: System keys + // + // These only build and decode events - posting them would play/pause media or change the + // volume on the machine running the tests. + + @Test("systemKeys maps v1 key names to their NX_KEYTYPE codes") + func testSystemKeysCodes() { + let harness = makeHarness() + #expect(harness.evalInt("hs.eventtap.systemKeys.SOUND_UP") == 0) + #expect(harness.evalInt("hs.eventtap.systemKeys.MUTE") == 7) + #expect(harness.evalInt("hs.eventtap.systemKeys.PLAY") == 16) + #expect(harness.evalInt("hs.eventtap.systemKeys.NEXT") == 17) + #expect(harness.evalInt("hs.eventtap.systemKeys.PREVIOUS") == 18) + #expect(harness.evalInt("Object.keys(hs.eventtap.systemKeys).length") == 22) + } + + @Test("makeSystemKeyEvent creates a systemDefined event", arguments: [true, false]) + func testMakeSystemKeyEventType(isDown: Bool) { + let harness = makeHarness() + harness.eval("var evt = hs.eventtap.makeSystemKeyEvent('PLAY', \(isDown))") + #expect(harness.evalInt("evt.type") == harness.evalInt("hs.eventtap.eventTypes.systemDefined")) + #expect(!harness.hasException) + } + + @Test("systemKey decodes every key built by makeSystemKeyEvent", + arguments: SystemKey.codes.keys.sorted(), [true, false]) + func testSystemKeyRoundTrip(key: String, isDown: Bool) { + let harness = makeHarness() + harness.eval("var sk = hs.eventtap.makeSystemKeyEvent('\(key)', \(isDown)).systemKey") + #expect(harness.evalString("sk.key") == key) + #expect(harness.evalInt("sk.keyCode") == SystemKey.codes[key]) + #expect(harness.evalBool("sk.isDown") == isDown) + #expect(harness.evalBool("sk.isRepeat") == false) + #expect(!harness.hasException) + } + + @Test("makeSystemKeyEvent ignores the case of the key name") + func testMakeSystemKeyEventCaseInsensitive() { + let harness = makeHarness() + #expect(harness.evalString("hs.eventtap.makeSystemKeyEvent('sound_down', true).systemKey.key") == "SOUND_DOWN") + #expect(!harness.hasException) + } + + @Test("makeSystemKeyEvent returns null for an unknown key") + func testMakeSystemKeyEventUnknownKey() { + let harness = makeHarness() + harness.eval("var evt = hs.eventtap.makeSystemKeyEvent('NOT_A_KEY', true)") + harness.expectTrue("evt === null || evt === undefined") + #expect(!harness.hasException) + } + + @Test("systemKeyStroke with an unknown key does nothing without throwing") + func testSystemKeyStrokeUnknownKey() { + let harness = makeHarness() + harness.eval("hs.eventtap.systemKeyStroke('NOT_A_KEY')") + #expect(!harness.hasException) + } + + @Test("systemKey is undefined for ordinary events") + func testSystemKeyUndefinedForOtherEvents() { + let harness = makeHarness() + #expect(harness.evalTypeOf("hs.eventtap.makeKeyEvent('a', true).systemKey") == "undefined") + #expect(harness.evalTypeOf("hs.eventtap.makeScrollWheelEvent(0, 3, 500, 400).systemKey") == "undefined") + #expect(!harness.hasException) + } + + @Test("duplicate preserves a system key event") + func testDuplicateSystemKeyEvent() { + let harness = makeHarness() + harness.eval("var copy = hs.eventtap.makeSystemKeyEvent('NEXT', false).duplicate()") + #expect(harness.evalString("copy.systemKey.key") == "NEXT") + #expect(harness.evalBool("copy.systemKey.isDown") == false) + #expect(!harness.hasException) + } + // MARK: duplicate @Test("duplicate returns a new event object") diff --git a/docs/migration-guide.md b/docs/migration-guide.md index fba882f8..86c5a642 100644 --- a/docs/migration-guide.md +++ b/docs/migration-guide.md @@ -219,7 +219,10 @@ per-row `contextMenu`. Check the styling-property list before assuming appearanc **`hs.eventtap`** — `hs.eventtap.event` folded directly into `hs.eventtap` (build events with `hs.eventtap.makeKeyEvent()` instead of `hs.eventtap.event.newKeyEvent()`; `.post()` is -unchanged). The tap/watcher pattern also changed shape: `hs.eventtap.addWatcher(types, callback, +unchanged). Media/volume/brightness keys likewise use `hs.eventtap.makeSystemKeyEvent(key, +isDown)` in place of `hs.eventtap.event.newSystemKeyEvent()` (same key names, e.g. `"PLAY"`), or +`hs.eventtap.systemKeyStroke(key)` to press and release one; v1's `event:systemKey()` is now the +`event.systemKey` property, with `isDown`/`isRepeat` in place of `down`/`repeat`. The tap/watcher pattern also changed shape: `hs.eventtap.addWatcher(types, callback, listenOnly)` takes an array of event types from `hs.eventtap.eventTypes`, the callback, and an optional listen-only flag, returning a watcher you still need to call `.start()` on. For a modify tap (`listenOnly` omitted/`false`) the callback's return value does the consuming — From fdc7d8d4f83d45c9a16cd4b64a4554c0220e4f70 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Tue, 13 Oct 2026 01:17:27 +0100 Subject: [PATCH 2/2] Test the system key decoder against hand-built events The existing tests only decoded events made by makeSystemKeyEvent(), so repeats, unknown key codes and other systemDefined subtypes went untested, and a mistake shared by the builder and decoder would pass. Co-Authored-By: Claude Opus 5.5 --- .../HSEventTapIntegrationTests.swift | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/Hammerspoon 2Tests/IntegrationTests/HSEventTapIntegrationTests.swift b/Hammerspoon 2Tests/IntegrationTests/HSEventTapIntegrationTests.swift index 7fcc4a52..ec8d9877 100644 --- a/Hammerspoon 2Tests/IntegrationTests/HSEventTapIntegrationTests.swift +++ b/Hammerspoon 2Tests/IntegrationTests/HSEventTapIntegrationTests.swift @@ -7,6 +7,7 @@ import Testing import JavaScriptCore import ApplicationServices import Carbon +import AppKit @testable import Hammerspoon_2 @Suite("hs.eventtap tests") @@ -630,6 +631,59 @@ struct HSEventTapTests { #expect(!harness.hasException) } + /// Exposes a hand-built systemDefined event to JS as `evt`, without going through + /// SystemKey, so the decoder is checked against fixed data1 values rather than its builder. + private func makeHarness(systemDefinedSubtype subtype: Int16, data1: Int) throws -> JSTestHarness { + let harness = makeHarness() + let nsEvent = try #require(NSEvent.otherEvent(with: .systemDefined, location: .zero, + modifierFlags: [], timestamp: 0, windowNumber: 0, + context: nil, subtype: subtype, + data1: data1, data2: -1)) + let cgEvent = try #require(nsEvent.cgEvent) + harness.context.setObject(HSEventTapEvent(cgEvent: cgEvent), forKeyedSubscript: "evt" as NSString) + return harness + } + + @Test("systemKey decodes a hand-built key down event") + func testSystemKeyDecodesFixtureKeyDown() throws { + // NX_KEYTYPE_NEXT (17) in the high word, 0xA (down) in bits 8-15 + let harness = try makeHarness(systemDefinedSubtype: 8, data1: 0x0011_0A00) + #expect(harness.evalString("evt.systemKey.key") == "NEXT") + #expect(harness.evalInt("evt.systemKey.keyCode") == 17) + #expect(harness.evalBool("evt.systemKey.isDown") == true) + #expect(harness.evalBool("evt.systemKey.isRepeat") == false) + #expect(!harness.hasException) + } + + @Test("systemKey reports auto-repeat") + func testSystemKeyDecodesRepeat() throws { + // NX_KEYTYPE_SOUND_UP (0), key down, with the repeat bit set + let harness = try makeHarness(systemDefinedSubtype: 8, data1: 0x0000_0A01) + #expect(harness.evalString("evt.systemKey.key") == "SOUND_UP") + #expect(harness.evalBool("evt.systemKey.isDown") == true) + #expect(harness.evalBool("evt.systemKey.isRepeat") == true) + #expect(!harness.hasException) + } + + @Test("systemKey names an unrecognised key code 'unknown'") + func testSystemKeyDecodesUnknownCode() throws { + // Key code 0x7F, key up (0xB) + let harness = try makeHarness(systemDefinedSubtype: 8, data1: 0x007F_0B00) + #expect(harness.evalString("evt.systemKey.key") == "unknown") + #expect(harness.evalInt("evt.systemKey.keyCode") == 0x7F) + #expect(harness.evalBool("evt.systemKey.isDown") == false) + #expect(!harness.hasException) + } + + @Test("systemKey is undefined for systemDefined events of other subtypes") + func testSystemKeyUndefinedForOtherSubtypes() throws { + // Same data1 as a PLAY key down, but not the aux control buttons subtype (8) + let harness = try makeHarness(systemDefinedSubtype: 7, data1: 0x0010_0A00) + #expect(harness.evalInt("evt.type") == harness.evalInt("hs.eventtap.eventTypes.systemDefined")) + #expect(harness.evalTypeOf("evt.systemKey") == "undefined") + #expect(!harness.hasException) + } + // MARK: duplicate @Test("duplicate returns a new event object")