@ryoppippi

karabiner.ts is Absolutely Brilliant!

19 Oct 2024 ・ 5 min read


日本語版

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 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:

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.

comment on bluesky / twitter
CC BY-NC-SA 4.0 2022-PRESENT © ryoppippi