Question or issue on macOS:
I’m trying to create an OS X keyboard hook for assistive technology purposes (i.e. don’t worry, not a keylogger).
When a user presses a key, I want to prevent the real keypress and send a fake keypress (character of my choosing) instead.
I have the following code:
- (void) hookTheKeyboard { CGEventMask keyboardMask = CGEventMaskBit(kCGEventKeyDown); id eventHandler = [NSEvent addGlobalMonitorForEventsMatchingMask:keyboardMask handler:^(NSEvent *keyboardEvent) { NSLog(@"keyDown: %c", [[keyboardEvent characters] characterAtIndex:0]); //Want to: Stop the keyboard input //Want to: Send another key input instead }]; }
Any help accomplishing either of those goals? Basically modifying the NSEvent “keyboardEvent” to send a different character. Thanks.
How to solve this problem?
Solution no. 1:
You can’t do this with the NSEvent
API, but you can do this with a CGEventTap
. You can create an active event tap and register a callback that receives a CGEventRef
and can modify it (if necessary) and return it to modify the actual event stream.
EDIT
Here’s a simple program that, while running, replaces every “b” keystroke with a “v”:
#import CGEventRef myCGEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) { //0x0b is the virtual keycode for "b" //0x09 is the virtual keycode for "v" if (CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode) == 0x0B) { CGEventSetIntegerValueField(event, kCGKeyboardEventKeycode, 0x09); } return event; } int main(int argc, char *argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; CFRunLoopSourceRef runLoopSource; CFMachPortRef eventTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, kCGEventMaskForAllEvents, myCGEventCallback, NULL); if (!eventTap) { NSLog(@"Couldn't create event tap!"); exit(1); } runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0); CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes); CGEventTapEnable(eventTap, true); CFRunLoopRun(); CFRelease(eventTap); CFRelease(runLoopSource); [pool release]; exit(0); }
(Funny story: as I was editing this post, I kept on trying to write “replaces every ‘b’ keystroke”, but it kept on coming out as “replaces every ‘v’ keystroke”. I was confused. Then I remembered that I hadn’t stopped the app yet.)
Solution no. 2:
I happened across this answer, needing to do the same but only for events within my own application not global . There is a much simpler solution, for this much simpler problem, which I am noting here incase it’s useful for anyone else:
- I intercepted the event at the window, by creating an override for sendEvent:. I then check for key events (KeyUp or KeyDown) and then simply create a new event using nearly all the data from the prevous event, then call NSWindow superclass with this event instead.
This seems to work perfectly for me and I didn’t have to even modify the keyCode part – but maybe this could be an issue…
Example in Swift:
class KeyInterceptorWindow : NSWindow { override func sendEvent(theEvent: NSEvent) { if theEvent.type == .KeyDown || theEvent.type == .KeyUp { println(theEvent.description) let newEvent = NSEvent.keyEventWithType(theEvent.type, location: theEvent.locationInWindow, modifierFlags: theEvent.modifierFlags, timestamp: theEvent.timestamp, windowNumber: theEvent.windowNumber, context: theEvent.context, characters: "H", charactersIgnoringModifiers: theEvent.charactersIgnoringModifiers!, isARepeat: theEvent.ARepeat, keyCode: theEvent.keyCode) super.sendEvent(newEvent!) } else { super.sendEvent(theEvent) } } }
Solution no. 3:
Swift 4+ version of james_alvarez’s answer:
class KeyInterceptorWindow: NSWindow { override func sendEvent(_ event: NSEvent) { if [.keyDown, .keyUp].contains(event.type) { let newEvent = NSEvent.keyEvent(with: event.type, location: event.locationInWindow, modifierFlags: event.modifierFlags, timestamp: event.timestamp, windowNumber: event.windowNumber, context: nil, characters: "H", charactersIgnoringModifiers: event.charactersIgnoringModifiers ?? "", isARepeat: event.isARepeat, keyCode: event.keyCode) if let newEvent = newEvent { super.sendEvent(newEvent) } } else { super.sendEvent(event) } } }