karabiner.tsがとてもいいぞ
TL;DR
karabiner.tsがとてもいいぞ

はじめに
macOSユーザーの皆様におかれましては、キーボードのカスタマイズツールとして有名なKarabiner-Elementsをご存知かと思います。
Karabiner-Elementsは、macOSのキーボードイベントをフックして、キーの入力をカスタマイズすることができるツールです。
中でも Complex Rules という機能を使うと、かなり自由度の高いカスタマイズが可能です。
例えば、
- CapsLockをCtrlに変更する
 - Commandを空打ちで英/かなを切り替える
 - アプリを起動するショートカットを追加する
 
など、様々なカスタマイズが可能です。
自分も初めてMacを手にした時からKarabiner-Elementsを使っていて、結構カスタマイズしています。
しかし、Karabiner-Elementsの設定ファイルはJSON形式で記述するため、設定が複雑になると管理が大変になります。
自分もまあまあな量の設定をしているのですが、JSONなので冗長ですし、繰り返しの設定を書くのが面倒です。
可読性も悪いです。
何かいい方法はないかと探していたところ、karabiner.tsというツールを見つけました。
karabinerの設定をいい感じに書く
karabinerの設定をいい感じに書くための試みはいくつかなされています。
JSON Schemaを使って補完を聞かせる方法や、
Edn形式で設定を書く方法などがあります。
またTypeScriptを使って設定を書く方法もあります。
色々試した結果、karabiner.tsが一番自分に合っていると感じました。
karabiner.ts
karabiner.tsは、TypeScriptを使ってkarabinerの設定を書くためのツールです。
karabiner.tsを使うと、TypeScriptの型システムを使ってkarabinerの設定を書くことができます。
そのため、キーの名前に補完が効いたり、型エラーが出たりするので、設定を書く際にとても便利です。
もちろんTypeScriptの構文が使えるので、mapやfilterなどの関数を使って設定を書くこともできます。
APIがだいぶ関数型を意識して設計されているので、とても書きやすかったです。
例えば、CapsLockをCtrlに変更する設定は以下のように書くことができます。
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" }),
])
これがコンパイルされると、以下のような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"
          }
        ]
      }
    ]
}
自分はkarabiner.tsを使って設定を書き、denoでコンパイルしてkarabiner.jsonを生成しています。
deno task watch でファイルを監視して、ファイルが変更されるとkarabiner.jsonが生成されるのはとても体験が良いです。
(deno なので node_mudulesを管理しなくて良いのも👍)
自分の使っている便利設定
ここからは、自分がkarabiner.tsを使って設定している便利な設定を紹介します。
Commandを空打ちで英/かなを切り替える
USキーボードを使っている人には定番の設定ですね。
  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 })
    ),
  ]),
withMapperという関数を使うとarray.mapのように複数の設定を一気に書くことができます。
descriptionもliteral stringで書けてとても良いですね。
JSON
 Command + Q を長押しでアプリケーションを終了する
macOSではCommand + Q でアプリを終了することができますが、これを長押しで終了するようにしています。
間違えて終了してしまうことがなくなります。description
これには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,
      }),
  ]),
JSON
 Ctrl + , で Wezterm を起動する
自分の使っているターミナルエミュレータであるWeztermを起動するHotkeyを設定しています。
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),
        ])
      ),
    ]),
JSON
 Discord の Return と Shift + Return を入れ替える
DiscordのメッセージはReturnを押すと送信されてしまいます。
この挙動が気に入らないのでShift + ReturnとReturnを入れ替えることで
- 通常のメッセージ送信は
Shift + Return - 改行は
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"] }),
    ]),
JSON
 Trackpadに触れている時だけ h/j/k/l を矢印キーにする
自分はVimを使っているので、矢印キーを使わずにh/j/k/lを使ってカーソル移動をしています。
これをVim以外でも使いたいわけです。
以前はfnキーと組み合わせてh/j/k/lを矢印キーに割り当てていましたが、つい最近 fn キーの代わりにTrackpadに触れているかどうかをトリガーにすることにしました。
これには MultitouchExtension というKarabiner-Elementsのプラグインを使っています。
普段は自作キーボードを使っているのでこの設定は不要ですが、いざという時にMacBookのキーボードを使うときにこの設定を有効にするようにしています。
こういった「似たような設定だけど繰り返し書くのが面倒」という場合に、条件を変数にまとめて使い回すことができるのもkarabiner.tsの良いところです。
/** 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}`)
    ),
  ]),
JSON
おわりに
karabiner.ts はいいぞ!
ドキュメントにはより高度な使い方(レイヤーの設定等)も書かれているので、興味がある方はぜひ試してみてください。
Discussion