エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

Karabiner-Elementsでキーボードにレイヤーを作る

【Unit4 ブログリレー3日目】 永山です。 この記事はUnit4チーム (m3.com開発チーム) メンバーにより投稿されるブログリレー3日目の記事です。

2日目は堺澤による「Professional Cloud Architectを取得した話」でした。

www.m3tech.blog

さて、自作キーボードのファームウェアには大抵「レイヤー」と呼ばれる機能が含まれています。 レイヤーは1つのキースイッチに複数のキーを割り当て、特に60%以下 *1 のサイズのコンパクトなキーボードを不便なく、むしろ便利なものにするための重要な機能です。

salicylic-acid3.hatenablog.com

この記事ではmacOS用のキーリマッピングソフトウェアKarabiner-Elementsとその設定ファイルを生成するためのJavaScriptライブラリ、karabiner.tsを使用してソフトウェア的にレイヤーを再現する方法について説明します。

京都水族館のミズクラゲ

キーボードのレイヤーとは

コンパクトなキーボードは当然ながらフルサイズのキーボードに比べキースイッチの数が少ないため、単純にキーとスイッチを一対一に割り当てただけでは入力できないキーが発生してしまいます。 例えば日本のITエンジニアに人気の高いハイエンドキーボードであるHappy Hacking Keyboard (HHKB) シリーズのほとんどでは通常のキーボードにあるようなアローキーが用意されていません。

そこで、HHKBでは押下中にキースイッチの割り当てを別のものに変更するFnキーが設置されています。

HHKBのFnキー押下時のキーマップ (公式サイトから引用)

レイヤーや、レイヤーを変更するためのレイヤーキーはこのようなFnキーの動作をより柔軟に一般化した機能と言えます。

Fnキーとレイヤーには主に以下のような違いがあります:

  • レイヤーは複数異なるものを用意できる
  • Fnキーのような同時押しのみでなく、トグル切り替えにもできる
  • レイヤーキーの単押しに異なる動作を割り当てられる (Mod-Tap)

弊ブログでは過去に末永がレイヤーの活用に関する知見を含む記事を投稿しております。

www.m3tech.blog

Karabiner-Elementsとは

Karabiner-ElementsはmacOS用のキーリマッピングアプリケーションです。

karabiner-elements.pqrs.org

GUIから設定できるSimple Modificationsと、複雑な条件付けやキーコンビネーションの再割り当て、マクロなどを設定できるComplex Modificationsという 2 つの機能を持ちます。

Simple Modificationsでは名前の通り単純なキーの再割り当てができます。 設定にはGUIが提供されており、手軽に設定を追加できます。

Karabiner-ElementsのSimple Modificationsの設定画面

一方Complex ModifitionsはSimple Modificationsでは表現ができないようなとても柔軟な設定が可能ですが、GUIの編集画面が提供されていないため、独自のルールを追加したい場合はJSON形式の設定ファイルを直接編集しなければなりません。

Complex Modificationsの設定例:

{
  "description": "Caps Lockを左Controlにマッピングする",
  "manipulators": [{
    "type": "basic",
    "from": {"key_code": "caps_lock"},
    "to": [{"key_code": "left_control"}]
  }]
}

ところがこのJSONファイルは手書きするのに適した形式ではないため、ドキュメントでは設定ファイルを書き出せる外部ツールやライブラリが紹介されています。

karabiner-elements.pqrs.org

karabiner.tsとは

karabiner.tsは上記のドキュメントでも紹介されている、Karabiner-ElementsのComplex ModificationsをTypeScriptで記述できるNode.js/Denoのライブラリです。

github.com

TypeScriptの言語機能により、似た設定の共通化や関数化が行え、また型システムによるチェックや補完といった支援が受けられます。

例えば先程のComplex Modificationsの設定例はkarabiner.tsを用いると次のように書けます:

rule("Caps Lockを左Controlにマッピングする").manipulators([
  map("caps_lock").to("left_control"),
])

Mod-Tapの設定方法

karabiner.tsの詳細な使用方法の説明は公式のドキュメントに譲るとして、まずは簡単なMod-Tapの設定を記述する方法を説明します。 Mod-Tapとは1つのキーの長押しと短い単押しに異なるキーアクションを割り当てる機能です。

キーの長押し (複数入力) とタップに異なるキーコードを割り当てるには BasicManipulatorBuilderto() メソッド と toIfAlone() メソッドを用います。

map(/* 元のキー */)
  .to(/* 長押しの動作 */)
  .toIfAlone(/* Tapの動作 */)

例えば左Cmdを長押しした場合には通常通り左Cmdを、タップに英数キーを割り当てるには以下のように記述します。

map("left_command")
  .to("left_command") // 長押しは左Cmd
  .toIfAlone("japanese_eisuu") // Tapは英数

レイヤーの実現方法

では改めて次のような記号と数字の入力に特化したレイヤーを作成してみます。

実現したいレイヤー

このレイヤーはスペースバーの右隣のキー (右Optionキー/右Altキー) の押下によって発火されます。 ただし、今回はそれに加えて右Optionキーが単独で押された場合はEscapeキーを割り当ててみます。

変数の利用

右Optionをレイヤーキーとして用いるのであれば、

rule("symbol layer").manipulators([
  map("q", "right_option").to("1", "shift"), // !
  map("w", "right_option").to("2", "shift"), // @
  map("e", "right_option").to("3", "shift"), // #
  ...
])

のように各キーと right_option のコンビネーションをリマッピングしていくことでも実現できそうです。 もちろんこのようにしてもよいのですが、Karabiner-Elementsの機能である「変数」を用いると、より管理がしやすくなります。

karabiner.tsから変数を利用するには toSetVar()ifVar() を使用します。

rule("symbol layer").manipulators([
  // 右Optionを押したとき、変数 layer を 1 に変更、離したとき 0 に戻す
  map("right_option").to(toSetVar("layer", 1, 0)).toIfAlone("escape"),

  // 変数 layer が 1 (right_option が押されている) のときのみ各キーをリマッピング
  map("q").condition(ifVar("layer", 1)).to("1", "shift"), // !
  map("w".condition(ifVar("layer", 1)).to("2", "shift"), // @
  map("e").condition(ifVar("layer", 1)).to("3", "shift"), // #
  ...
])

変数を用いる方法ではFnキーと同じような同時押しのみではなく、トグルスイッチのような動作を表現できるなど、より柔軟な制御も可能になります。

-  // 右Optionを押したとき、変数 layer を 1 に変更、離したとき 0 に戻す
-  map("right_option").to(toSetVar("layer", 1, 0)),
+  // 右Optionを押すたび、変数 layer を 1 -> 0 -> 1 -> ... と切り替えるようにする
+  map("right_option").condition(ifVar("layer", 0)).to(toSetVar("layer", 1)),
+  map("right_option").condition(ifVar("layer", 1)).to(toSetVar("layer", 0)),

また、各キーをリマッピングする際の .condition(ifVar("layer", 1)) という条件の反復が気になったかもしれません。

karabiner.tsではこのような条件の繰り返しを、ヘルパー関数 withCondition() を用いて削減できます。

karabiner.ts.evanliu.dev

  // 変数 layer が 1 (right_option が押されている) のときのみ各キーをリマッピング
  map("q").condition(ifVar("layer", 1)).to("1", "shift"), // !
  map("w").condition(ifVar("layer", 1)).to("2", "shift"), // @
  map("e",).condition(ifVar("layer", 1)).to("3", "shift"), // #

  // 上記と同じ
  withCondition(ifVar("layer", 1))([
    map("q".to("1", "shift"), // !
    map("w").to("2", "shift"), // @
    map("e").to("3", "shift"), // #
  ]),  

今回は変数による条件付けしか用いませんでしたが、Karabiner-Elements (およびkarabiner.ts) では、特定のアプリケーションでのみ有効なリマッピング (ifApp()) や特定のキーボードでのみ有効なリマッピング (ifDevice()) 、さらに特定のキーボードが接続されているときのみ有効なリマッピング (ifDeviceExists()) など様々な条件付けを含むような動作も定義できます。

コードの全体

最終的に今回の記号レイヤーを再現するComplex Modificationの設定の全体像は以下のようになりました (Denoを使用しています)。

#!/usr/bin/env -S deno run --check --allow-env --allow-read --allow-write
import { writeToProfile, rule, ifVar, withCondition, map, toSetVar } from "https://deno.land/x/karabinerts@1.34.0/deno.ts";

const layerVar = "layer"; // レイヤー変数の名前

const layers = {
  default: 0, // 通常レイヤー
  symbol: 1, // 記号レイヤー
} as const;

function symbolLayerRule() {
  return rule("symbol layer").manipulators([
    // 右Optionを押したとき、変数 layer を 1 に変更、離したとき 0 に戻す
    map("right_option").to(toSetVar("layer", 1, 0)).toIfAlone("escape"),

    // 記号キーの割り当て
    withCondition(ifVar(layerVar, layers.symbol))([
      map("q").to("1", "shift"), // !
      map("w").to("2", "shift"), // @
      map("e").to("3", "shift"), // #
      map("r").to("4", "shift"), // $
      map("t").to("5", "shift"), // %
      map("y").to("keypad_7"),
      map("u").to("keypad_8"),
      map("i").to("keypad_9"),

      map("a").to("6", "shift"), // ^
      map("s").to("7", "shift"), // &
      map("d").to("8", "shift"), // *
      map("f").to("="),
      map("g").to("=", "shift"), // +
      map("h").to("keypad_4"),
      map("j").to("keypad_5"),
      map("k").to("keypad_6"),

      map("z").to("9", "shift"), // (
      map("x").to("0", "shift"), // )
      map("c").to("/"),
      map("v").to("/", "shift"), // ?
      map("b").to("'"),
      map("n").to("keypad_1"),
      map("m").to("keypad_2"),
      map(",").to("keypad_3"),
      map(".").to("keypad_0"),
    ]),
  ]);
}

writeToProfile("Default profile", [symbolLayerRule()]);

まとめ

Karabiner-ElementsのComplex Modificationという機能と、その設定を型安全にTypeScriptで定義できるライブラリのkarabiner.tsを用い、自作キーボードにおけるレイヤーのような機能を再現する方法を簡単に紹介しました。

このような工夫により、MacBookの内蔵キーボードや、市販のキーボードでも自作キーボードのような便利な入力体験を得られるようになります。 また、(macOS同士に限りますが) Karabiner-Elementsの設定ファイルを共有することで、趣味PCと業務PCのような複数のマシン間で同様の入力体験を再現できます。

以下に公開されているTech Talk (社内勉強会) のアーカイブでは、筆者が実際の作業環境で行っている工夫などについても触れています。よければご笑覧ください。

www.youtube.com

We're hiring!

エムスリーでは作業環境にこだわりを持つエンジニア、プロダクトマネージャー、プロダクトデザイナーを絶賛募集中です。 ご興味を持たれた方はぜひご応募ください。

jobs.m3.com

*1:60%キーボードはキーボードサイズのカテゴリ。フルサイズキーボードと比較しキー数が約60% (60~65キー程度) の、テンキーやF1, F2, F3 などを持たないキーボードを指す。