I quickly threw this code together, so be sure to check it for memory leaks, etc. You will see that you need to add other characters that you want to handle, as well as a case (I added only k → k).
This example was created as a command-line utility, so it has no interface and is suitable for running in the background using launchd . Please note that you either need to run this as root, or enable support for auxiliary devices in the system settings and add permission for this application.
Note that you can run as root when developing in Xcode by going to Product -> Scheme -> Edit , and in the "run" section change the radio for "debug process as" to "root".
Edit: Refresh with autorun pool to release temporary objects.
// compile and run from the commandline with: // clang -fobjc-arc -framework Cocoa ./foo.m -o foo // sudo ./foo #import <Foundation/Foundation.h> #import <AppKit/NSEvent.h> typedef CFMachPortRef EventTap; @interface KeyChanger : NSObject { @private EventTap _eventTap; CFRunLoopSourceRef _runLoopSource; CGEventRef _lastEvent; } @end CGEventRef _tapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, KeyChanger* listener); @implementation KeyChanger - (BOOL)tapEvents { if (!_eventTap) { NSLog(@"Initializing an event tap."); _eventTap = CGEventTapCreate(kCGSessionEventTap, kCGTailAppendEventTap, kCGEventTapOptionDefault, CGEventMaskBit(kCGEventKeyDown), (CGEventTapCallBack)_tapCallback, (__bridge void *)(self)); if (!_eventTap) { NSLog(@"unable to create event tap. must run as root or add privlidges for assistive devices to this app."); return NO; } } CGEventTapEnable(_eventTap, TRUE); return [self isTapActive]; } - (BOOL)isTapActive { return CGEventTapIsEnabled(_eventTap); } - (void)listen { if (!_runLoopSource) { if (_eventTap) {//dont use [self tapActive] _runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, _eventTap, 0); // Add to the current run loop. CFRunLoopAddSource(CFRunLoopGetCurrent(), _runLoopSource, kCFRunLoopCommonModes); NSLog(@"Registering event tap as run loop source."); CFRunLoopRun(); }else{ NSLog(@"No Event tap in place! You will need to call listen after tapEvents to get events."); } } } - (CGEventRef)processEvent:(CGEventRef)cgEvent { NSEvent* event = [NSEvent eventWithCGEvent:cgEvent]; // TODO: add other cases and do proper handling of case if ([event.characters caseInsensitiveCompare:@"k"] == NSOrderedSame) { event = [NSEvent keyEventWithType:event.type location:NSZeroPoint modifierFlags:event.modifierFlags timestamp:event.timestamp windowNumber:event.windowNumber context:event.context characters:@"" charactersIgnoringModifiers:@"" isARepeat:event.isARepeat keyCode:event.keyCode]; } _lastEvent = [event CGEvent]; CFRetain(_lastEvent); // must retain the event. will be released by the system return _lastEvent; } - (void)dealloc { if (_runLoopSource){ CFRunLoopRemoveSource(CFRunLoopGetCurrent(), _runLoopSource, kCFRunLoopCommonModes); CFRelease(_runLoopSource); } if (_eventTap){ //kill the event tap CGEventTapEnable(_eventTap, FALSE); CFRelease(_eventTap); } } @end CGEventRef _tapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, KeyChanger* listener) { //Do not make the NSEvent here. //NSEvent will throw an exception if we try to make an event from the tap timout type @autoreleasepool { if(type == kCGEventTapDisabledByTimeout) { NSLog(@"event tap has timed out, re-enabling tap"); [listener tapEvents]; return nil; } if (type != kCGEventTapDisabledByUserInput) { return [listener processEvent:event]; } } return event; } int main(int argc, const char * argv[]) { @autoreleasepool { KeyChanger* keyChanger = [KeyChanger new]; [keyChanger tapEvents]; [keyChanger listen];//blocking call. } return 0; }
source share