macOS: modifier keys posted as keyDown instead of flagsChanged (breaks VoiceOver chords)
Summary
On macOS, injected modifier keys (Control, Option/Alt, Command, Shift) are posted as keyDown/keyUp events instead of the flagsChanged events that real hardware emits. Assistive technologies that track modifier state from flagsChanged — notably VoiceOver — never observe an injected modifier as held, so modifier chords sent via RobotJS are ignored.
Details
In toggleKeyCode() (src/keypress.c):
CGEventRef keyEvent = CGEventCreateKeyboardEvent(NULL, (CGKeyCode)code, down);
CGEventSetType(keyEvent, down ? kCGEventKeyDown : kCGEventKeyUp); // <-- this
CGEventSetFlags(keyEvent, flags);
CGEventPost(kCGSessionEventTap, keyEvent);CGEventCreateKeyboardEvent already creates the event with the correct type from its down argument, so the explicit CGEventSetType is redundant for normal keys. For modifier keycodes it is harmful: it forces them out as key-down/up rather than flags-changed.
Reproduce
- Run a
CGEventTaplistening forkCGEventFlagsChanged. robot.keyToggle('control', 'down').- Observe the event arrives as
keyDown(keycode 59), notflagsChanged. Real hardware, and a rawCGEventCreateKeyboardEventposted without theCGEventSetTypeoverride, produceflagsChanged.
Practical impact: a VoiceOver command chord (Control+Option+key) injected via RobotJS is not recognised, because VoiceOver derives its VO-modifier state from flagsChanged.
Environment
- macOS (reproduced on Apple Silicon and x86_64)
- Present on
master; the line is identical in 0.6.x–0.7.1
Fix
Remove the redundant CGEventSetType call. Normal keys are unaffected (their type is already set); modifier keys then post as flagsChanged. PR follows.
Note: verified behaviourally via an event-tap capture and against live VoiceOver; I was not able to run the project's own CI locally, so a maintainer check on non-accessibility key paths would be welcome.
Source: octalmage/robotjs