Simulate keypress using Swift
Asked Answered
M

5

20

I'm searching for a way to simulate keystrokes in OSX. I found another solution (Simulate keypress for system wide hotkeys) using Objective-C, but i need to do it with Swift. How can i adapt CGEventCreateKeyboardEvent?

Mclaurin answered 15/12, 2014 at 12:40 Comment(1)
hi, if you can help me on this: #68902754Hesione
E
12

The code on that linked answer is fairly readily convertible to Swift code, however there are a handful of gotchas you will need to take care of along the way:

CGEventSourceCreate takes a CGEventSourceStateID, which is a typealiase for a UInt32, but the constants such as kCGEventSourceStateHIDSystemState are defined as Int, so you’ll have to cast them i.e. CGEventSourceStateID(kCGEventSourceStateHIDSystemState). Likewise with CGEventFlags.

CGEventSourceCreate and CGEventCreateKeyboardEvent return an Unmanaged<CGEventSource> (or Unmanaged<CGEvent>). The auto-generated Swift API for Core Graphics doesn’t know whether the returned objects need to be released by you or not so you need to check the API docs for these calls and then use takeRetainedValue() or takeUnretainedValue() accordingly on the returned value, to convert them into the underlying type you want to work with.

Finally, they return implicitly unwrapped optionals, so you’ll need to decide if you want to check for nils or just live with the excitement of runtime explosions if they ever return one.

Given all that it’s pretty simple to turn the Objective-C in that answer demonstrating pressing Cmd-Space to Swift, I just tried pasting this into a scratch app and it worked fine:

(though I haven't checked the API docs for whether the retain is the correct thing to do or not)

let src = CGEventSourceCreate(CGEventSourceStateID(kCGEventSourceStateHIDSystemState)).takeRetainedValue()

let cmdd = CGEventCreateKeyboardEvent(src, 0x38, true).takeRetainedValue()
let cmdu = CGEventCreateKeyboardEvent(src, 0x38, false).takeRetainedValue()
let spcd = CGEventCreateKeyboardEvent(src, 0x31, true).takeRetainedValue()
let spcu = CGEventCreateKeyboardEvent(src, 0x31, false).takeRetainedValue()

CGEventSetFlags(spcd, CGEventFlags(kCGEventFlagMaskCommand));
CGEventSetFlags(spcd, CGEventFlags(kCGEventFlagMaskCommand));

let loc = CGEventTapLocation(kCGHIDEventTap)

CGEventPost(loc, cmdd)
CGEventPost(loc, spcd)
CGEventPost(loc, spcu)
CGEventPost(loc, cmdu)
Exact answered 15/12, 2014 at 15:31 Comment(3)
@Airspeed: nice answer. Although can we make the "hex key values" to something more readable like KVK_ANSI_ABass
I have transalted, I post it in an answerNiple
hi, if you can help me on this #68902754Hesione
N
16

Working with Swift 3

let src = CGEventSource(stateID: CGEventSourceStateID.hidSystemState)

let cmdd = CGEvent(keyboardEventSource: src, virtualKey: 0x38, keyDown: true)
let cmdu = CGEvent(keyboardEventSource: src, virtualKey: 0x38, keyDown: false)
let spcd = CGEvent(keyboardEventSource: src, virtualKey: 0x31, keyDown: true)
let spcu = CGEvent(keyboardEventSource: src, virtualKey: 0x31, keyDown: false)

spcd?.flags = CGEventFlags.maskCommand;

let loc = CGEventTapLocation.cghidEventTap

cmdd?.post(tap: loc)
spcd?.post(tap: loc)
spcu?.post(tap: loc)
cmdu?.post(tap: loc)
Niple answered 11/12, 2016 at 18:17 Comment(1)
In my case (Swift 5), I had to use: let src = CGEventSource(stateID: .privateState)Affray
E
12

The code on that linked answer is fairly readily convertible to Swift code, however there are a handful of gotchas you will need to take care of along the way:

CGEventSourceCreate takes a CGEventSourceStateID, which is a typealiase for a UInt32, but the constants such as kCGEventSourceStateHIDSystemState are defined as Int, so you’ll have to cast them i.e. CGEventSourceStateID(kCGEventSourceStateHIDSystemState). Likewise with CGEventFlags.

CGEventSourceCreate and CGEventCreateKeyboardEvent return an Unmanaged<CGEventSource> (or Unmanaged<CGEvent>). The auto-generated Swift API for Core Graphics doesn’t know whether the returned objects need to be released by you or not so you need to check the API docs for these calls and then use takeRetainedValue() or takeUnretainedValue() accordingly on the returned value, to convert them into the underlying type you want to work with.

Finally, they return implicitly unwrapped optionals, so you’ll need to decide if you want to check for nils or just live with the excitement of runtime explosions if they ever return one.

Given all that it’s pretty simple to turn the Objective-C in that answer demonstrating pressing Cmd-Space to Swift, I just tried pasting this into a scratch app and it worked fine:

(though I haven't checked the API docs for whether the retain is the correct thing to do or not)

let src = CGEventSourceCreate(CGEventSourceStateID(kCGEventSourceStateHIDSystemState)).takeRetainedValue()

let cmdd = CGEventCreateKeyboardEvent(src, 0x38, true).takeRetainedValue()
let cmdu = CGEventCreateKeyboardEvent(src, 0x38, false).takeRetainedValue()
let spcd = CGEventCreateKeyboardEvent(src, 0x31, true).takeRetainedValue()
let spcu = CGEventCreateKeyboardEvent(src, 0x31, false).takeRetainedValue()

CGEventSetFlags(spcd, CGEventFlags(kCGEventFlagMaskCommand));
CGEventSetFlags(spcd, CGEventFlags(kCGEventFlagMaskCommand));

let loc = CGEventTapLocation(kCGHIDEventTap)

CGEventPost(loc, cmdd)
CGEventPost(loc, spcd)
CGEventPost(loc, spcu)
CGEventPost(loc, cmdu)
Exact answered 15/12, 2014 at 15:31 Comment(3)
@Airspeed: nice answer. Although can we make the "hex key values" to something more readable like KVK_ANSI_ABass
I have transalted, I post it in an answerNiple
hi, if you can help me on this #68902754Hesione
C
11

Swift 3

For me the hexadecimal key values like: 0x124 didn't work, but simple UInt 124 did the trick!

A nice collection of keycodes can be found here! This copy-paste code snippet simulates a right arrow keypress. Change the key number for whatever you want to simulate:

    // Simulate Right Arrow keypress
    let rightArrowKeyCode: UInt16 = 124

    let keyDownEvent = CGEvent(keyboardEventSource: nil, virtualKey: rightArrowKeyCode, keyDown: true)
    keyDownEvent?.flags = CGEventFlags.maskCommand
    keyDownEvent?.post(tap: CGEventTapLocation.cghidEventTap)

    let keyUpEvent = CGEvent(keyboardEventSource: nil, virtualKey: rightArrowKeyCode, keyDown: false)
    keyUpEvent?.flags = CGEventFlags.maskCommand
    keyUpEvent?.post(tap: CGEventTapLocation.cghidEventTap)

Update: For macOS Mojave and above you should allow your app to control your computer in System Preferences > Security & Privacy > Accessibility

Crowned answered 18/9, 2017 at 11:11 Comment(5)
There's a list of key symbols in Carbon.HIToolbox in the format kVK_... for example kVK_ANSI_M is the M key.Comeau
it is amazing how we have to relate on other developers posting this kind of info. Apple shoud fire satan from running the documentation department and hire some competent people. Their docs stink.Hermia
It does work under Mojave but you need to allow your app to control your computer in the system preferences > security & privacy > accessibilityEdlyn
This seems like it is not working with macOS Mojave. Can anyone confirm? It waw working perfectly fine before..Bergstrom
while I can see my app (extension of Mail) in System Preferences > Security & Privacy > Accessibility, when I try to activate it, it deactivates itself right away.. where is this coming from?Caput
C
7

I made this for a Swift 4 project, don't forget that App sandboxing will not allow an app to send keystrokes like this so it'll need to be turned off. This means your app would prohibited from the AppStore.

import Foundation

class FakeKey {
    static func send(_ keyCode: CGKeyCode, useCommandFlag: Bool) {
        let sourceRef = CGEventSource(stateID: .combinedSessionState)

        if sourceRef == nil {
            NSLog("FakeKey: No event source")
            return
        }

        let keyDownEvent = CGEvent(keyboardEventSource: sourceRef,
                                   virtualKey: keyCode,
                                   keyDown: true)
        if useCommandFlag {
            keyDownEvent?.flags = .maskCommand
        }

        let keyUpEvent = CGEvent(keyboardEventSource: sourceRef,
                                 virtualKey: keyCode,
                                 keyDown: false)

        keyDownEvent?.post(tap: .cghidEventTap)
        keyUpEvent?.post(tap: .cghidEventTap)
    }
}
Comeau answered 27/3, 2018 at 0:25 Comment(4)
Unfortunately I don't have Mojave installed, please post the fix that you find as a new answer. You may want to file a bug with Apple. (This code is basically using the same APIs as the other answers.)Comeau
The issue seems related to the "accessibility" API. You need to grant it permission in order to use it. In my case it failed "silently" b/c I denied the permission prompt once.Supervision
That's "good" news, There's no problem, it's just the way Apple designed it.Comeau
A version I keep updated for cutbox/CutBox gist.github.com/jasonm23/3d2c4040a5f879a945264d1bd0a630adComeau
B
-1

A combination of the answers to stimulate a shortcut/hotkey. Swift 5.1

let source = CGEventSource(stateID: CGEventSourceStateID.hidSystemState)

let cmdKey: UInt16 = 0x38
let numberThreeKey: UInt16 = 0x14

let cmdDown = CGEvent(keyboardEventSource: source, virtualKey: cmdKey, keyDown: true)
let cmdUp = CGEvent(keyboardEventSource: source, virtualKey: cmdKey, keyDown: false)
let keyThreeDown = CGEvent(keyboardEventSource: source, virtualKey: numberThreeKey, keyDown: true)
let keyThreeUp = CGEvent(keyboardEventSource: source, virtualKey: numberThreeKey, keyDown: false)


fileprivate func testShortcut() {

let loc = CGEventTapLocation.cghidEventTap

cmdDown?.flags = CGEventFlags.maskCommand
cmdUp?.flags = CGEventFlags.maskCommand
keyThreeDown?.flags = CGEventFlags.maskCommand
keyThreeUp?.flags = CGEVentFlags.maskCommand

cmdDown?.post(tap: loc)
keyThreeDown?.post(tap: loc)
cmdUp?.post(tap: loc)
keyThreeUp?.post(tap: loc)

}

Manually written may contain mistakes.

Beryllium answered 25/5, 2020 at 8:36 Comment(2)
Hi Sam, this is close enough to my Swift 4 version that it makes sense to just paste them together merging the diff. Happy to do it for you, but I figured I'd give you the opportunity to do the edit first.Comeau
hi, none of the examples are working (most of all are nearly the same). Am I missing some extra code to ask for permission. I tried to unsanbox as well but nothing. First time i ran the app, it asked for permission and I granted. thanksCaput

© 2022 - 2024 — McMap. All rights reserved.