Introduction
If you’re a macOS user, you’re likely familiar with Karabiner-Elements, the famous keyboard customisation tool.
Karabiner-Elements is a tool that hooks into macOS keyboard events, allowing you to customise key inputs.
The Complex Rules feature, in particular, allows for highly flexible customisation. For example, you can:
- Change
CapsLock
toCtrl
- Toggle between English/Japanese input by tapping the
Command
key - Add shortcuts to launch applications
and much more.
I’ve been using Karabiner-Elements since I first got a Mac and have customised it quite a bit.
However, Karabiner-Elements’ configuration files are written in JSON format, which can become difficult to manage as settings grow more complex. I have a fair amount of settings myself, but JSON is verbose, and writing repetitive settings can be tedious. It’s also not very readable.
I was looking for a better solution when I stumbled upon a tool called karabiner.ts
.
Writing Karabiner Settings in a Nice Way
There have been several attempts to write Karabiner settings in a more pleasant manner:
After trying various options, I found karabiner.ts
to be the most suitable for me.
karabiner.ts
karabiner.ts
is a tool for writing Karabiner settings using TypeScript. With karabiner.ts
, you can write Karabiner settings using TypeScript’s type system. This means you get auto-completion for key names and type errors, which is very convenient when writing settings. Of course, you can use TypeScript syntax, so you can write settings using functions like map and filter. The API seems to be designed with functional programming in mind, which makes it very easy to write.
For example, you can write a setting to change CapsLock
to Ctrl
like this:
import * as k from 'karabiner_ts';
k.rule('Change CapsLock to Ctrl')
.manipulators([
k.map({ key_code: 'caps_lock' })
.to({ key_code: 'left_control' })
.toIfAlone({ key_code: 'caps_lock' }),
]);
When compiled, this generates the following JSON:
{
"description": "Change CapsLock to Ctrl",
"manipulators": [
{
"type": "basic",
"from": {
"key_code": "caps_lock"
},
"to": [
{
"key_code": "left_control"
}
],
"to_if_alone": [
{
"key_code": "caps_lock"
}
]
}
]
}
I write my settings using karabiner.ts
and compile them with deno to generate karabiner.json.
Using deno task watch
to monitor files and generate karabiner.json when files change provides a very nice experience.
(Plus, it’s great not having to manage node_modules
because it’s deno 👍)
My Useful Settings
Here are some useful settings I’ve configured using karabiner.ts
.
Toggle English/Japanese Input by Tapping Command
This is a standard setting for those using a US keyboard.
k.rule('Tap CMD to toggle Kana/Eisuu', ifNotSelfMadeKeyboard).manipulators([
k.withMapper(
{
left_command: 'japanese_eisuu',
right_command: 'japanese_kana',
} as const,
)((cmd, lang) =>
k.map({ key_code: cmd, modifiers: { optional: ['any'] } })
.to({ key_code: cmd, lazy: true })
.toIfAlone({ key_code: lang })
.description(`Tap ${cmd} alone to switch to ${lang}`)
.parameters({ 'basic.to_if_held_down_threshold_milliseconds': 100 })
),
]);
Using the withMapper
function allows you to write multiple settings at once, similar to array.map
. It’s also great that you can write the description
as a literal string.
Quit Application by Holding Command + Q
In macOS, you can quit an application with Command + Q
, but I’ve set it to quit only when held down. This prevents accidental quits.
This uses Karabiner-Elements’ to_if_held_down
.
k.rule('Quit application by holding command-q').manipulators([
k.map({
key_code: 'q',
modifiers: { mandatory: ['command'], optional: ['caps_lock'] },
})
.toIfHeldDown({
key_code: 'q',
modifiers: ['left_command'],
repeat: false,
}),
]);
Launch Wezterm with Ctrl + ,
I’ve set up a hotkey to launch Wezterm, the terminal emulator I use.
function toHideApp(name: string) {
return k.to$(
`osascript -e 'tell application "System Events" to set visible of process "${name}" to false'`,
);
}
k.rule('Toggle WezTerm by ctrl+,')
.manipulators([
k.withMapper(
[
toHideApp('WezTerm'),
k.toApp('WezTerm'),
] as const,
)((event, i) =>
k.withCondition(
...[k.ifApp('wezterm')].map(c => i === 0 ? c : c.unless()),
)([
k.map({ key_code: 'comma', modifiers: { mandatory: ['control'] } })
.to(event),
])
),
]);
Swap Return and Shift + Return
in Discord
In Discord, pressing Return sends the message. I don’t like this behaviour, so I’ve swapped Shift + Return
and Return
to achieve:
- Normal message sending with
Shift + Return
- Line break with
Return
k.rule(
'Swap Enter & Shift+Enter in Discord',
k.ifApp({ bundle_identifiers: ['com.hnc.Discord'] }),
)
.manipulators([
k.map({
key_code: 'return_or_enter',
modifiers: { mandatory: ['shift'] },
})
.to({ key_code: 'return_or_enter' }),
k.map({ key_code: 'return_or_enter' })
.to({ key_code: 'return_or_enter', modifiers: ['shift'] }),
]);
Map h/j/k/l to Arrow Keys Only When Touching the Trackpad
As a Vim user, I use h/j/k/l for cursor movement instead of arrow keys. I wanted to use this outside of Vim as well.
Previously, I mapped h/j/k/l to arrow keys in combination with the fn key, but recently I’ve changed it to trigger based on whether I’m touching the trackpad. This uses a Karabiner-Elements plugin called MultitouchExtension
.
https://karabiner-elements.pqrs.org/docs/json/extra/multitouch-extension/
I usually use a custom keyboard, so this setting is unnecessary, but I enable it when I need to use the MacBook keyboard.
One of the great things about karabiner.ts
is that you can group conditions into variables and reuse them in cases like this where you have similar settings but don’t want to write them repeatedly.
/** not apple keyboard */
const ifNotSelfMadeKeyboard = k.ifDevice([
{ product_id: 1, vendor_id: 22854 }, // Claw44
]).unless();
/**
* trackpad touched
* if not touched, multi touch finger count is 0
*/
const ifTrackpadTouched = k.ifVar('multitouch_extension_finger_count_total', 0)
.unless();
k.rule(
'toggle h/j/k/l to arrow keys',
ifTrackpadTouched,
ifNotSelfMadeKeyboard,
).manipulators([
k.withMapper(
{
h: 'left_arrow',
j: 'down_arrow',
k: 'up_arrow',
l: 'right_arrow',
} as const,
)((key, arrow) =>
k.map({ key_code: key })
.to({ key_code: arrow })
.description(`Tap ${key} to ${arrow}`)
),
]);
Conclusion
karabiner.ts
is brilliant!
The documentation includes more advanced usage (such as layer settings), so if you’re interested, do give it a try.