--- title: karabiner.ts is Absolutely Brilliant! date: 2024-10-19 isPublished: true lang: 'en' --- > [日本語版](/blog/2024-05-23-zenn-85373aaf0c92e0-ja) # Introduction If you're a macOS user, you're likely familiar with Karabiner-Elements, the famous keyboard customisation tool. [@preview](https://karabiner-elements.pqrs.org/) [Karabiner-Elements](https://karabiner-elements.pqrs.org/docs/manual/configuration/configure-complex-modifications) 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` to `Ctrl` - 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: - [ Using JSON Schema for auto-completion ](https://github.com/pqrs-org/Karabiner-Elements/issues/1918) - [ Writing settings in Edn format ](https://github.com/yqrashawn/GokuRakuJoudo) - [ Using TypeScript ](https://github.com/mxstbr/karabiner) After trying various options, I found `karabiner.ts` to be the most suitable for me. # `karabiner.ts` [`karabiner.ts`](https://karabiner.ts.evanliu.dev/) 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: ```typescript 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: ```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. [@preview](https://github.com/ryoppippi/dotfiles/tree/65dc955a4187c9c375793a99271b8af4c2014d3e/karabiner) 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. ```typescript 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`](https://karabiner-elements.pqrs.org/docs/json/complex-modifications-manipulator-definition/to-if-held-down/). ```typescript 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. ```typescript 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` ```typescript 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/](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. ```typescript /** 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.