0

I'm trying to programmaticaly send a custom key event (function keys, media keys) but it only works in interpreted mode, not in compiled code.

I tried using the following answer: emulate media key press on Mac

The python example works perfectly, the swift example works when called as a script, ie this code:

#!/usr/bin/swift
    
import Quartz

let NX_KEYTYPE_SOUND_UP: UInt32 = 0
let NX_KEYTYPE_SOUND_DOWN: UInt32 = 1
let NX_KEYTYPE_PLAY: UInt32 = 16
let NX_KEYTYPE_NEXT: UInt32 = 17
let NX_KEYTYPE_PREVIOUS: UInt32 = 18
let NX_KEYTYPE_FAST: UInt32 = 19
let NX_KEYTYPE_REWIND: UInt32 = 20

let supportedKeys: [String: UInt32] = ["playpause": NX_KEYTYPE_PLAY, "next": NX_KEYTYPE_NEXT, "prev": NX_KEYTYPE_PREVIOUS, "volup": NX_KEYTYPE_SOUND_UP, "voldown": NX_KEYTYPE_SOUND_DOWN]

func HIDPostAuxKey(key: UInt32) {
  func keyDown(_ down: Bool) {
    let flags = NSEvent.ModifierFlags(rawValue: (down ? 0xa00 : 0xb00))
    let data1 = Int((key << 16) | (down ? 0xa00 : 0xb00))

    let ev = NSEvent.otherEvent(with: NSEvent.EventType.systemDefined,
                                location: NSPoint(x:0,y:0),
                                modifierFlags: flags,
                                timestamp: 0,
                                windowNumber: 0,
                                context: nil,
                                subtype: 8,
                                data1: data1,
                                data2: -1)
    let cev = ev?.cgEvent
    cev?.post(tap: CGEventTapLocation.cghidEventTap)
  }

  keyDown(true)
  keyDown(false)
}

HIDPostAuxKey(key: supportedKeys[CommandLine.arguments[1]]!)

called via terminal (after doing chmod a+x keypress.swift) works perfectly.

./keypress.swift volup

increases the volume, the HID for the volume even appears on screen.

If I try to compile the exact same code with

swiftc -o keypress keypress.swift

And adding the keypress binary to Security & Privacy -> accessibility, it does nothing. No error message, nothing.

First I tried to write a CLI app in Xcode using this code, it doesnt work (no error, but no key pressed). I've tried in Obj-C, in Swift, I checked that sandboxing wasnt enabled, no luck.

I'm stumpled that it works in interpreted mode but not in compiled mode.

Is there some flag to add when compiling to enable posting events? I'm out of ideas.

I'm running MacOS 12.6.2, Xcode 14.2, Swift 5

Stephan Burlot
  • 5,077
  • 3
  • 25
  • 26

2 Answers2

0

The difference between interpreted swift and compiled swift is ... speed

So adding

    usleep(useconds_t(1 * 1_000)) //will sleep for 1 millisecond (.001 seconds)

after the cgEvent.post solves the problem. (Note that I tested this on my iMac i5 3.2 GHz, you may need to change this delay)

This is not an perfect solution: the program should manage a queue of the events posted, but better, MacOS should handle this.

EDIT: I was wrong. Sending a dummy event at start solves the problem.

func initEventQueue(){
    // Send a command up key event
    if let ev = CGEvent(keyboardEventSource: nil, virtualKey: 55, keyDown: false) {
        ev.post(tap: .cghidEventTap)
    }
}

Calling this at start solves the problem. I've read all the documentation I could find, but there's no mention on how or when the event queue is created and why this solves my problem.

I'm still curious why this happens and what's the correct way to solve this.

Stephan Burlot
  • 5,077
  • 3
  • 25
  • 26
0

As @StephanBurlot mentioned, sending a dummy event helps. Then I tried explicitly creating an instance of CGEventSourceCreate to pass it to CGEventCreate() (or attach it to an event returned from -[NSEvent cgEvent ], and I think it made the trick. However, after that the original issue stopped reproducing on my machine. I mean that out of the sudden the author's code started working just fine without messing with CGEventSourceCreate.

Anyway, that's my version in Objective-C:

@import CoreGraphics;

static const CGEventField kCGEventSubtype = 0x53;
static const CGEventField kCGEventData1 = 0x95;
static const CGEventField kCGEventData2 = 0x96;

static CGEventRef CGEventCreateMediaKeyEvent(CGEventSourceRef _Nullable source, int64_t key, BOOL down) {
    CGEventFlags flags = (down ? NX_KEYDOWN : NX_KEYUP) << 8;

    CGEventRef e = CGEventCreate(source);
    CGEventSetType(e, NX_SYSDEFINED);
    CGEventSetFlags(e, flags);
    CGEventSetIntegerValueField(e, kCGEventSubtype, 8);
    CGEventSetIntegerValueField(e, kCGEventData1, flags | (key << 16));
    CGEventSetIntegerValueField(e, kCGEventData2, -1);
    return e;
}

static void PostKey(CGEventSourceRef _Nullable source, uint64_t key) {
    {
        CGEventRef e = CGEventCreateMediaKeyEvent(source, key, YES);
        CGEventPost(kCGHIDEventTap, e);
        CFRelease(e);
    }
    {
        CGEventRef e = CGEventCreateMediaKeyEvent(source, key, NO);
        CGEventPost(kCGHIDEventTap, e);
        CFRelease(e);
    }
}

int main(int argc, const char * argv[]) {
    CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStateHIDSystemState);
    PostKey(source, NX_KEYTYPE_SOUND_UP);
    CFRelease(source);
    return 0;
}
storoj
  • 1,851
  • 2
  • 18
  • 25