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

17 comments:

  1. Perfect!

    This is turning out to be very useful for my setup, which is very similar - powerbook on my right, apple pro keyboard on my left with a large display on in the middle and right-thumb on the trackpad. Until now it's been especially aggravating for me because I use Emacs a lot and that's pretty tough with the default behavior in OS X.

    Would be great if you could post a compiled binary for those who don't have gcc installed :)

    ReplyDelete
  2. Nathan, thanks very much for your suggestion. I put my binary there for people to download.

    Our configurations are actually the same: a big screen in between two separate keyboards, and the trackpad for the right thumb. I manage to be quite productive with this setting, but I'd like to solve two problems if possible:

    1. Once in a while, part of my palm will touch the trackpad and cause window focus change. I wish there's a hot key for me to quickly switch the trackpad on and off.

    2. I like the multi-touch features of the touchpad but then I have to move my right hand down to use other fingers for that. FingerWorks once had a very nice product that combines keyboard and the touchpad on the same surface, before they were acquired by Apple.

    I'm not sure how many users out there would come to a point like us when moving hand a few inches becomes a bottleneck in productivity. Our special requests may not be business-worthy for products like that.

    ReplyDelete
  3. Thank you for this absolutely wonderful piece of software. I use two logitech keyboards, one for each hand, on my desktop so I can have more ergonomic typing position. With your application I can use proper typing style and lose the need for reaching hard combinations with just one hand. Thank you!

    ReplyDelete
  4. I am new two the mac but I have the same set up as you. Two keyboards and a display in the middle. I have downloaded your binary code but do not know what to do with it. Please post a reply as soon as possible with the answer. Thank you so much. I am really excited to get this up and running.

    ReplyDelete
  5. BlackEagl, you are welcome. Thank you very much for your kind words.

    Jonathan, I'll add instructions tomorrow. Basically you need to download the program, and run it. To run it, you probably need to make the file executable. (See http://support.apple.com/kb/HT2963 ). You can also make it run every time you boot by putting that in your startup setting.

    ReplyDelete
  6. Tk that is amazing!!! I have been trying to get that to work for so long. Thank you. I do have one question though, does the terminal window have to run the entire time?

    ReplyDelete
  7. Jonathan, I'm glad it's working for you. No, you don't need to keep Terminal open after you do that "chmod" command. Actually, it's a one time thing. If you add that to Login items, you don't even have to do anything in the future.

    ReplyDelete
  8. For some reason when I hit command q to quit terminal it says "Do you want to close this window? Closing the window will terminate the process..." I hit close and now it doesn't work. Any suggestions???

    ReplyDelete
  9. Jonathan, Terminal should only be used to do that "chmod" command. After you do it once, you don't need to use Terminal. You should run the program with Finder.

    If you want to run it under Terminal every time, you can do so by adding an "&" sign at the end of the command line so it goes to background and won't be closed when Terminal is closed.

    ReplyDelete
  10. I don't want to use it through terminal but every time I open it, it opens terminal. I don't know what you mean by open it in finder I open a finder window and open the file. I also tried having it open from the program finder (System/Library/CoreServices/Finder). I don't know what I'm doing wrong, just that it's not working.

    ReplyDelete
  11. Jonathan, sorry I misunderstood your description. You are right, if you run the program via Finder, Terminal will need to stay open. Did you try adding the program into Login Items (see step 4 in the post)? That should work.

    ReplyDelete
  12. Is there another way to do it that does not involve leaving the terminal window open?

    ReplyDelete
  13. Yes, Jonathan. See step 4. You can skip step 3.

    ReplyDelete
  14. I don't think this works in snow leopard OS X (10.6.8). I tried downloading your binary, chmod'ing it and running it through terminal. It definitely starts running, but I still can't use the modifier keys across keyboards.

    ReplyDelete
  15. @brookswift, that may likely be the case. The binary was compiled a while ago. My setup is now different so I don't use this anymore. I believe Apple now has released XCode for free so you can compile it from the posted source yourself. I will try to compile it under the new OS at some point but now I don't have the environment to do so.

    ReplyDelete
  16. http://forums.macrumors.com/showthread.php?p=9421743

    has a solution that's worked for me.

    ReplyDelete
  17. @brookswift, thanks a lot! I updated the post to include that resource.

    ReplyDelete