Friday, September 19, 2008

Modifiers Across Keyboards

I use a MacBook Pro with an external screen on its left (desktop expansion). I'm also using a bluetooth keyboard as a second keyboard on the left. This allows me to put each hand on one keyboard (left on BT, right on the MacBook) when I type. If I were to type on only one keyboard, my neck will be in a constant uncomfortable position. Besides, I need to access the multi-touch pad constantly.

By default, OS X seems to keep track of modifiers (CMD, ALT, CTRL, SHIFT, etc.) on a per-keyboard basis, i.e. pressing the SHIFT on one keyboard while pressing another letter on another won't have any shifting effect.

However, in another PC setup, this is the default behavior so I can comfortably use one keyboard for each hand. (Sort of the split keyboard concept).

I posted my question on Apple Support forum without any response so I had to do it myself. Hopefully this little code would save somebody in the same situation some time.

PS: Later I realized that it doesn't work for password fields in Firefox. I guess Firefox somehow bypasses the standard hooking mechanism (?).

UPDATE: 2008/09/25

The current program does not aggregate modifiers across different keyboards, i.e. if I press CMD on one keyboard and then SHIFT on another, it only recognizes the later one. It's not hard to modify the code (basically apply the CHANGES from a specific keyboard to the universal Modifier flag holder). I'll come back to this if I get some time.

UPDATE: 2009/03/13

Per Nathan Schmidt's suggestion, I'm sharing my compiled binary here for those who don't have convenient access to compilers: Cross Keyboard Modifiers binary.

UPDATE: 2009/07/07

Thanks to Jonathan for asking for detailed steps. Here they are:

1. Download the binary (see the link in the previous update) into your home directory (or anywhere of your choice. I downloaded it into a directory called "bin" under my home directory, i.e. /Users/tk/bin . "tk" is my username, "bin" is a directory I created under my home directory.)

2. Open up Terminal, type "chmod 755 /Users/tk/bin/cross_keyboard_modifiers". This is one time setup. You can close the Terminal after this.

3. Now you should be able to double click to run the program via Finder. Finder will open Terminal to run the program. Note that this way you'll need to keep Terminal open for the program to stay running. If you want to the process to run without going through Terminal, read on.

4. If you want this program to run every time you boot OS X, you can do so in System Preference > Accounts > Select your account > Login Items > + sign > select the program, i.e. "/Users/tk/bin/cross_keyboard_modifiers". After this, you'll need to reboot for this to work.

UPDATE: 2011/08/16

Thanks to brookswift for pointing out a solution that works with newer OS. I haven't tried it myself but it seems others have found it helpful.

Source code:


// Name:    cross_keyboard_modifiers.c
// Version: 1.0.0
// Date:    2008/09/19
// Author:  Tsan-Kuang Lee 
// Site:    http://blog.tklee.org
//
// What?
//
// This program allows OS X users to use multiple keyboards at the same time.  By default (10.5.4, as far as I know), the modifiers
// (CMD, CTL, SHIFT, etc.) only work with other keys ON THE SAME KEYBOARD.  For example, if you press SHIFT key and the key "a" on
// your laptop, you get a capital "A"; if you press SHIFT on your laptop keyboard and the "a" key on an external keyboard (say, a
// bluetooth keyboard), you get a lowercase "a".
//
// Why?
//
// Keyboard is my primary input device.  After long time use, I found it helpful if I can use one keybaord for each hand so my wrist
// can type in a more comfortable position.  Having two keyboards help me a lot; however, I learned to type in a way that whenever
// SHIFT key is needed, I'll use a different hand for it's combined key.  For example, I'll use left shift for SHIFT-P, but right shift
// for SHIFT-A.  This increases the speed, too. I searched everywhere online but couldn't find any solution that can change OS X's
// default behavior.  Therefore I had to learn how OS X handles things.
//
// How?
//
// By default, OS X keeps different modifier flags for different devices.  We tap into the System's event handling system and keep a 
// local copy of the modifiers' flags.  Whenever a non-modifier key is pressed, we reset the modifers' flags with the value we keep.  
//
// Credit
//
// I studied the code from the following sources.  They are very helpful.  Thanks to their authors.
//  * alterkeys.c http://osxbook.com
//  * http://lists.apple.com/archives/quartz-dev/2007/Jan/msg00049.html
//  * http://developer.apple.com/documentation/Carbon/Reference/QuartzEventServicesRef/Reference/reference.html
//
// Usage
//
// Complile using the following command line:
//     gcc -Wall -o cross_keyboard_modifiers cross_keyboard_modifiers.c -framework ApplicationServices
// Then run cross_keyboard_modifiers.  (Be sure not to let the process go into sleep, e.g. ctrl-z at the shell.  
// Otherwise, you may lose your keybaord input because of the never wakened process.  The easiest way to do so is
// put it in the background right away, i.e. "cross_keyboard_modifiers &")
//
// You need superuser privileges to create the event tap, unless accessibility
// is enabled. To do so, select the "Enable access for assistive devices"
// checkbox in the Universal Access system preference pane.

#include <ApplicationServices/ApplicationServices.h>

// This callback will be invoked every time there is a keystroke.
CGEventRef myCGEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
{
static CGEventFlags flags = (CGEventFlags) NULL;

if (flags == (CGEventFlags)NULL)
{
flags = CGEventGetFlags(event);
}

switch (type) {
case kCGEventKeyDown:
case kCGEventKeyUp:
{
CGEventSetFlags(event, flags);
break;
}
case kCGEventFlagsChanged:
{
flags = CGEventGetFlags(event);
break;
}
}
return event;
}

int main(void)
{
CFMachPortRef      eventTap;
CGEventMask        eventMask;
CFRunLoopSourceRef runLoopSource;

//CGEventFlags oldFlags = CGEventSourceFlagsState(kCGEventSourceStateCombinedSessionState);
CGEventFlags oldFlags = CGEventSourceFlagsState(kCGEventSourceStateHIDSystemState);

// Create an event tap. We are interested in key presses.
eventMask = CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventKeyUp) | CGEventMaskBit(kCGEventFlagsChanged);
eventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, 0, eventMask, myCGEventCallback, &oldFlags);
if (!eventTap) {
fprintf(stderr, "failed to create event tap\n");
exit(1);
}

// Create a run loop source.
runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);

CFRelease(eventTap);

// Add to the current run loop.
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);

// Enable the event tap.
CGEventTapEnable(eventTap, true);

CFRelease(runLoopSource); 

// Set it all running.
CFRunLoopRun();

// In a real program, one would have arranged for cleaning up.
exit(0);
}