Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Hammerspoon 2/Modules/hs.eventtap/HSEventTapEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
84 changes: 84 additions & 0 deletions Hammerspoon 2/Modules/hs.eventtap/HSEventTapModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 }

Expand Down Expand Up @@ -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? {
Expand Down Expand Up @@ -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
Expand Down
90 changes: 90 additions & 0 deletions Hammerspoon 2/Modules/hs.eventtap/HSEventTapSystemKey.swift
Original file line number Diff line number Diff line change
@@ -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,
]
}
}
Loading
Loading