01 Feb 2025

Achieving the holy grail: vim bindings everywhere

So it turns out it’s possible to get vim bindings everywhere, in software, and sharing the same config on both macs, windows and linux machines. The secret? A cool little tool called keymapper.

Indulgent Backstory

I’m a pretty die-hard (neo)vim fan, and one of the things that’s always killed me about computing is that so many text inputs just don’t support vim bindings at all. Usually I just grin and bear it, but for the past few years I’ve been experimenting with split 60% and 65% mechanical keyboards, (primarily my venerable UHK which recently bit the dust after almost a decade of service) which made me realize that if you use a split keyboard you get a whole extra key under your left thumb, begging the question “What if holding that key down turned on vim mode?”

Turns out, I was far from the first person to ask that question and lots of prior art exists. After hitting the limits of the UHK’s gui configurator I installed a custom firmware (since incorporated into mainline UHK’s firmware) that let me set up sequential keybindings like vim’s gg command to go to the top of the file. And for a while, life was good.

Then I started working from home more often and having to bring a keyboard around in addition to my laptop started to irk me. So I looked into software based solutions, and quickly came upon Karabiner Elements. I was able to get some basic bindings set up, but Karabiner’s JSON syntax is quite verbose and difficult to edit manually, which led me to GokuRakuJoudo, an esoteric tool that uses Clojure syntax as a DSL to generate Karabiner’s overly verbose json for you.

This worked pretty well, and between GokuRakuJoudo and Karabiner Elements I was able to get a pretty slick setup going that I used for years. The only problem was that I bounce between machines pretty often, and it was a little frustrating that I couldn’t use the vim-everywhere mappings I’d gotten used to on my mac on my Linux and Windows machines. I experimented a bit with Autokey on Linux, as well as the old stalwart Autohotkey on Windows, both of which could theoretically be used to achieve the same effect, but never landed on a good system after realizing I’d have to learn and maintain 3 different keyboard configuration tools.

Until finally, a few days ago, some kind soul on HN shared keymapper. And oh my god, what a wonderful piece of software! It solves pretty much every gripe I’d had with my previous setup.

Getting it installed

I use a mac as my primary machine, and the quickest way to get it installed is by doing:

brew tap houmain/tap
brew install --HEAD keymapper

(keep in mind though that if you are currently using Karabiner Elements or another keyboard remapping tool you’ll want to quit it first)

Windows is easier, just download the installer from their releases page, and for Linux check their docs.

Configuration

The configuration language is refreshingly simple, but powerful. In short, all mappings take the form of:

<input-key> >> <output-key> 

You can find the list of valid key names in the source code’s Key.h file.

Check the docs for more info because there are a lot more options than what I’ll show below available, but the 80/20 of keymapping just requires using the below syntax:

  • The simplest mappings are just Key >> Key, for example to remap CapsLock to Control you would just add a line like:

     CapsLock >> Control
    
  • Key{SomeOtherKey} means hold Key and press SomeOtherKey. So to make pressing CapsLock + V paste (normally Cmd+V on a mac) you’d do:

    CapsLock{V} >> Meta{V}
    
  • (Key1 Key2) means press Key1 and Key2 together. So another way to write the previous CapsLock+V to Cmd+V mapping would be:

    (CapsLock V) >> (Meta V)
    

Putting it all together

The configuration I use involves rebinding the left space bar on my split keyboard to act as CapsLock, and while it’s held down I have a vim-like set of keybindings for movement immediately available. As a perk, when I’m using my mac’s keyboard directly, it’s pretty easy to translate the muscle memory to just use CapsLock itself to enter vim mode (though a bit more tiring for the pinky).

You could also use keymapper to be truly vim-like and switch modes when you press escape, but that’s outside the scope of this little note (mostly because I don’t like it since there’s no way to get a visual indicator of which mode you’re in the way you can in real vim).

To get you started, here’s a basic config to get the 80/20 of vim key bindings working:

Basic mac vim bindings

# empty mapping to prevent Capslock alone from turning on caps lock
CapsLock >>

# basic movement keys
CapsLock{H} >> ArrowLeft
CapsLock{J} >> ArrowDown
CapsLock{K} >> ArrowUp
CapsLock{L} >> ArrowRight

# make w and b move forward and back by word
CapsLock{B} >> (Alt ArrowLeft) 
CapsLock{W} >> (Alt ArrowRight) 

# gg and G for moving to the top and bottom of the current doc
CapsLock{G G} >> Meta{ArrowUp}
CapsLock{(Shift G)} >> Meta{ArrowDown}

# $ and 0 to move to the beginning and ending of the line
# (I intentionally don't bind it as CapsLock{(Shift 4)} so that I
#  can still select text with the shift key)
CapsLock{4} >> Meta{ArrowRight}
CapsLock{0} >> Meta{ArrowLeft}

# copy/yank and paste
CapsLock{Y} >> Meta{C}
CapsLock{D} >> Meta{X}
CapsLock{P} >> Meta{V}

# make pressing j+k at the same time (or at least w/in 50ms of each other)
# act as an alias for escape so your fingers never have to leave the home row
J{!50ms K} >> Escape
K{!50ms J} >> Escape

Basic windows vim bindings

# basic movement keys
CapsLock{H} >> ArrowLeft
CapsLock{J} >> ArrowDown
CapsLock{K} >> ArrowUp
CapsLock{L} >> ArrowRight

# make w and b move forward and back by word
CapsLock{B} >> Control{ArrowLeft}
CapsLock{W} >> Control{ArrowRight}

# gg and G for moving to the top and bottom of the current doc
CapsLock{G G} >> Control{Home}
(CapsLock Shift G) >> Control{End}

# $ and 0 to move to the beginning and ending of the line
CapsLock{0} >> Home
CapsLock{4} >> End

# copy/yank and paste
CapsLock{Y} >> Control{C}
CapsLock{D} >> Control{X}
CapsLock{P} >> Control{V}

# make pressing j+k at the same time act as an alias for escape so you never 
# have to leave the home row
J{!50ms K} >> Escape
K{!50ms J} >> Escape

Combined bindings

You probably observed that that was a bit repetitive, and having to maintain two separate config files for different platforms is no fun. But of course, keymapper has your back and has a solution for this! You can make sections that only run on a particular platform using the syntax [system = "<platform>"], so a combined config file might look like this:

# prevent capslock alone from doing anything
CapsLock >>

# basic movement keys (common)
CapsLock{H} >> ArrowLeft
CapsLock{J} >> ArrowDown
CapsLock{K} >> ArrowUp
CapsLock{L} >> ArrowRight

# make pressing j+k at the same time act as an alias for escape so you never 
# have to leave the home row
J{!50ms K} >> Escape
K{!50ms J} >> Escape

[system = "macOS"]
# make w and b move forward and back by word
CapsLock{B} >> (Alt ArrowLeft) 
CapsLock{W} >> (Alt ArrowRight) 

# gg and G for moving to the top and bottom of the current doc
CapsLock{G G} >> Meta{ArrowUp}
CapsLock{(Shift G)} >> Meta{ArrowDown}

# $ and 0 to move to the beginning and ending of the line
CapsLock{4} >> Meta{ArrowRight}
CapsLock{0} >> Meta{ArrowLeft}

# copy/yank and paste
CapsLock{P} >> Meta{V}
CapsLock{Y} >> Meta{C}
CapsLock{D} >> Meta{X}

[system = "Windows"]
# $ and 0 to move to the beginning and ending of the line
CapsLock{0} >> Home
CapsLock{4} >> End

# make w and b move forward and back by word
CapsLock{B} >> Control{ArrowLeft}
CapsLock{W} >> Control{ArrowRight}

# copy/yank and paste
CapsLock{D} >> Control{X}
CapsLock{P} >> Control{V}
CapsLock{Y} >> Control{C}

# gg and G for moving to the top and bottom of the current doc
CapsLock{G G} >> Control{Home}
(CapsLock Shift G) >> Control{End}

There’s a lot more you can do with keymapper, but that should be enough to get you started! Check their docs for info on the advanced stuff (virtual keys that you can use to toggle maps, etc.)

UPDATE: I later learned that keymapper has some handy sugar to make this type of config even easier: you can create sections like [modifier = "CapsLock"] , that indicate to keymapper that anything within that section will have CapsLock held down, so you can just write H >> ArrowLeft instead of writing out the full CapsLock{H} >> ArrowLeft every time.