If you’re obsessed with productivity hacks, you’ve probably heard of Karabiner – the ultimate tool for keyboard customization on macOS. But maintaining Karabiner’s config is a pain. In this post I’ll show you a simpler, Kotlin-powered way to wrangle your Karabiner setup, so you don’t have to wrestle with massive, unwieldy JSON.

On Karabiner

Here’s the gist: Karabiner lets you intercept any keystroke and remap it to… well, pretty much anything. Want to turn Caps Lock into a Hyper key? Make your keyboard launch confetti? It’s all possible.

Take the classic usecase: remap Caps Lock to Hyper (⌘⌥⌃⇧) when held, Escape when tapped. That’s easy to do but just scratches the surface. With Karabiner, you can build many more customizations that supercharge your workflow.

Maintaining that karabiner.json

I was watching a video by Max Stoiber where he uses Raycast1 & Karabiner. But what struck me was how he customized and maintained his karabiner setup.

Karabiner runs off a single .json config file. But that .json configuration can become unwieldy and difficult to manage, as your rules grow in complexity. For example: I require a ~2700 line .json for all my hacks. It’s impossible to maintain it as pure json.

Max uses TypeScript to maintain his rules -which then compiles down to a .json file- that Karabiner can consume. I really like this approach. If you read my previous blog post, I also ran into a very similar problem and used goku, which in turn used the very esoteric and terse edn format.

I like TypeScript over edn but you know what I’d like even better? Kotlin! So on a ✈️ ride back home, I decided to whip up karabiner-kt.

I’m really happy with my solution. Kotlin affords a much more pleasant DSL than most other2 languages.

Here’s what a Karabiner rule looks like in Kotlin :

karabinerRule {
    description = "Right Cmd -> Ctrl (Enter alone)"
    mapping {
        fromKey = RightCommand
        toKey = RightControl
        toKeyIfAlone = KeyCode.ReturnOrEnter
        forDevice { identifiers = DeviceIdentifier.APPLE_KEYBOARDS }
    }
},

Clean, expressive, and type-safe!

Or here’s a fun one. Hold the “O” key and tap “0” for showing the Raycast Confetti:

karabinerRuleSingle {
    description = "O + 0 -> Raycast Confetti"
    layerKey = KeyCode.O
    fromKey = KeyCode.Num0
    shellCommand = "open raycast://extensions/raycast/raycast/confetti"
},

Here’s a slightly more complex modification: If I hold the “f” key and tap j/k, it types out an open and closed bracket respectively.

karabinerRule {
    description = "F-key layer mappings"
    layerKey = KeyCode.F
    // J K
    // ( )
    mapping {
        fromKey = KeyCode.J
        toKey = KeyCode.Num9
        toModifiers = listOf(LeftShift)
    }
    mapping {
        fromKey = KeyCode.K
        toKey = KeyCode.Num0
        toModifiers = listOf(LeftShift)
    }

    // M ,
    // [ ]
    mapping {
        fromKey = KeyCode.M
        toKey = KeyCode.OpenBracket
    }
    mapping {
        fromKey = KeyCode.Comma
        toKey = KeyCode.CloseBracket
    }

    // . /
    // { }
    mapping {
        fromKey = KeyCode.Period
        toKey = KeyCode.OpenBracket
        toModifiers = listOf(LeftShift)
    }
    mapping {
        fromKey = KeyCode.Slash
        toKey = KeyCode.CloseBracket
        toModifiers = listOf(LeftShift)
    }
}

Installation

The repo is open source, so feel free to take a look and customize.

Getting started is easy. Here’s how to set it up from scratch:

# install karabiner-elements
brew install --cask karabiner-elements

# app uses a simple gradle app
brew install gradle

# let's clean up the karabiner folder if they existed
rm -rf ~/.config/karabiner
mkdir -p ~/.config/karabiner

# clone the repo
git clone https://github.com/kaushikgopal/karabiner-kt.git ~/.config/karabiner/karabiner-kt

Run the configurator

Now every time you want to run the configurator:

cd ~/.config/karabiner/karabiner-kt
make

# you might have to do this once a while (if you restart your mac etc.)
make restart-karabiner

Try your own Rules!

All your customizations live in Rules.kt. My own config is about 300 lines of Kotlin. Compare that to the monstrous 2736-line JSON it generates. 😅

You should start with a really simple setup and build your Rules over time. Give it a shot and let me know if you run into issues!


  1. my launcher of choice. ↩︎

  2. maybe TypeScript allows this but I understand Kotlin better ↩︎