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
Karabiner-Elements A powerful and stable keyboard customizer for macOS. Download v16.0.0 Toggle Dropdown Download v16.0.0 For macOS 27 Golden Gate For macOS 26 Tahoe For macOS 15 Sequoia For macOS 14 Sonoma For macOS 13 Ventura Download v14.13.0 For macOS 12 Monterey For macOS 11 Big Sur Download v13.7.0 For macOS 10.15 Catalina (10.15.6 or later) Download v12.10.0 For macOS 10.15 Catalina For macOS 10.14 Mojave For macOS 10.13 High Sierra For macOS 10.12 Sierra Download v11.6.0 For macOS 10.11 El Capitan Download v10.22.0 For macOS 10.11 El Capitan For macOS 10.10 Yosemite For macOS 10.9 Mavericks Learn more Privacy Supported systems macOS 27 Golden Gate macOS 26 Tahoe macOS 15 Sequoia macOS 14 Sonoma macOS 13 Ventura Both Intel-based Macs and Apple Silicon Macs are supported. Predefined rules by community Release notes GitHub Sponsor Easy to use You can configure simple key mappings from GUI.
Karabiner-Elements

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.

dotfiles/karabiner at 65dc955a4187c9c375793a99271b8af4c2014d3e · ryoppippi/dotfiles
ryoppippi's dotfiles. Contribute to ryoppippi/dotfiles development by creating an account on GitHub.
GitHub

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