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

3

24

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.

Capability answered 26/4, 2011 at 3:37 Comment(0)
F
58

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 <Cocoa/Cocoa.h>

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

Felting answered 26/4, 2011 at 4:21 Comment(8)
Rather new to Objective-C... help with a bit of sample code? Is it at all similar to what I have above?Capability
in case it helps: CGEvent apis are all C. no objc required.Jewfish
If you are using ARC, you will want to replace NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; ... [pool release]; with @autoreleasepool {...}Trifle
I had to "Enable access for assistive devices" in System Preferences > Accessibility to get this to work. I did not think this was necessary for CGEventTap if you were logged in with an Admin account.Trifle
You will also need to restart Xcode after enabling assistive device access.Freese
Is there anyway to inject? Like not modify a key event, but add one? I am trying to do this.Pogonia
Has anyone seen this working in High Sierra? The example program does not work, and after adding logging I am seeing only certain keys come in the callback (command=0x37, option=0x31, etc.) None of the letters seem to work. I checked the Accessibility->Privacy section in Mac OS in system preferences but the test app doesn't show up there, nor can I add it.Smile
I tried running the same app outside of Xcode (as root), and now I get the other key codes, and the b=>v works. If anyone knows why, please let me know.Smile
P
6

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

    }

}
Prude answered 28/4, 2015 at 12:47 Comment(0)
R
2

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)
        }
    }
}
Race answered 4/4, 2019 at 18:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.