Modify NSEvent to send a different key than the one that was pressed

MacOS

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)
        }
    }
}

Hope this helps!