vim keybindings for Lion

One of the things that bugs me the most about keyboard bindings is how vim’s idiosyncratic hjkl navigation keys have taken over the world - GMail was probably the first true mass-market UI that implemented them, then Google Reader, then Reeder, so it has become somewhat of a pain to not find them available everywhere.

One of those places is Lion’s excellent threaded message list view, which I find counter-intuitive to navigate with the default keyboard bindings.

I can navigate the message list itself just fine with the cursor keys (in fact, I’m used to navigating the whole of using nothing but the keyboard), but after jumping to the thread list using Tab, I somehow find it confusing (and frustrating) that the up/down cursor keys scroll in minute increments and that the left/right ones actually move from one message to the other.

With my diving in to vim even further, and after the past evening’s hack, it was a trivial matter to build a plugin that uses hjkl to navigate the thread view alone.

Again, the source is small enough to publish:

from AppKit import *
from Foundation import *
from Quartz.CoreGraphics import * # CGEvent
import objc

def swizzle(*args):
    """ Brilliant piece of coding from """
    cls, SEL = args
    def decorator(func):
        oldIMP      = cls.instanceMethodForSelector_(SEL)
        def wrapper(self, *args, **kwargs):
            return func(self, oldIMP, *args, **kwargs)
        newMethod   = objc.selector(wrapper, 
                                    selector  = oldIMP.selector,
                                    signature = oldIMP.signature)
        objc.classAddMethod(cls, SEL, newMethod)
        return wrapper
    return decorator

my_keymap = { # when sorting messages in decreasing date order
    4:  115, # h maps to Fn + cursor left
    37: 119, # l maps to Fn + cursor right
    38: 124, # j maps to cursor left  - next message 
    40: 123  # k maps to cursor right - previous message

@swizzle(MessagesTableView, 'keyDown:')
def keyDown_(self, original, event):
    code = event.keyCode()
    NSLog('Handling key %d' % code)
    if code in my_keymap.keys():
        NSLog("Changing key %d to %d" % (code, my_keymap[code]))
        original(self, NSEvent.eventWithCGEvent_(CGEventCreateKeyboardEvent(None,my_keymap[code],True)));
    original(self, event)

MVMailBundle = objc.lookUpClass('MVMailBundle')
class TestPlugin(MVMailBundle):
    def initialize (cls):
        NSLog("TestPlugin registered with Mail")
    initialize = classmethod(initialize)

Have, as they say, the appropriate amount of fun.

See Also: