Mac
Karabiner
karabiner-Elements

Karabiner-Elementsの設定項目が増えてEmacsライクな設定が楽になった

従来のキーバインド設定方法

Karabiner-Elementsを使うと、MacでJISキーボードを使うときにWindowsのようなIME切り替えができるようになったり、Vimのようなカーソル移動を行ったり、あるいは色々なアプリの操作を自分流にアレンジしたりできるのですが、Emacsのctrl-x ctrl-fのような2つのキーを連続してタイプした場合のキー変換の設定は一筋縄ではいきませんでした。
正確に言えば、ctrl-xをタイプした後に未設定のキーを誤ってタイプした場合のエラー処理を省略すれば簡単に書けるのですが、エラー処理を行うとなると、Karabiner-Elementsで設定可能なキーが200以上あるため非常に面倒な作業になります。私がひとまずの対応策として採った方法は、pqrs-org/KE-complex_modificationsで公開されているツールを使って、設定をerbファイルに書いてjsonファイルに変換するという方法です。(私が書いたこちらの記事で簡単に紹介しています)

新しいキーバインド設定方法

まだBeta版(Ver.11.1.12)でしか実現できないですが、Karabiner-Elementsに「ctrl-xをタイプした後に未設定のキーをタイプした場合の動作を指定する設定」が加わりましたので、この設定を活用することでキー変換の設定が随分と楽になります。

新しい設定項目について

Beta版(Ver.11.1.12)でto_delayed_action, to_if_invoked, to_if_canceledという3つの項目が設定項目に追加されました。この設定項目を使った場合に何ができるかを以下のコードで紹介します。

test.json
{
  "description": "test to_delayed_action",
  "manipulators": [
    {
      "type": "basic",
      "from": {
        "key_code": "a",
        "modifiers": { "mandatory": [ "control" ] }
      },
      "to": [
        { "set_variable": { "name": "ctrl-a", "value": 1 } }
      ],
      "to_delayed_action": {
        "to_if_invoked": [
          { "set_variable": { "name": "ctrl-a", "value": 2 } }
        ],
        "to_if_canceled": [
          { "set_variable": { "name": "ctrl-a", "value": 0 } }
        ]
      }
    }
  ]
}

このコードの動作は以下のとおりです。

  1. ctrl-aをタイプするとctrl-a変数に1を代入する(toset_variableが実行される)
  2. そのまま何もタイプしないでいると、ctrl-a変数に2を代入する(to_if_invokedset_variableが実行される)
  3. ctrl-aに続けて別のキーをタイプすると、ctrl-a変数に0を代入する(to_if_canceledset_variableが実行される)

実際のコード

これらの設定項目を利用してEmacsのような2ストロークのキーバインドを実現するコードは以下のとおりです。

{
  "description": "Two stroke key_bind",
  "manipulators": [
    {
      "type": "basic",
      "from": {
        "key_code": "c", "modifiers": { "mandatory": [ "control" ] }
      },
      "to": [
        { "key_code": "q", "modifiers": [ "command" ] }
      ],
      "conditions": [
        { "type": "variable_if", "name": "ctrl-x", "value": 1 }
      ]
    },
    {
      "type": "basic",
      "from": {
        "key_code": "f", "modifiers": { "mandatory": [ "control" ] }
      },
      "to": [
        { "key_code": "o", "modifiers": [ "command" ] }
      ],
      "conditions": [
        { "type": "variable_if", "name": "ctrl-x", "value": 1 }
      ]
    },
    {
      "type": "basic",
      "from": {
        "key_code": "x",
        "modifiers":
         { "mandatory": [ "control" ], "optional": [ "caps_lock" ] }
      },
      "to": [
        { "set_variable": { "name": "ctrl-x", "value": 1 } }
      ],
      "to_if_alone": [
         { "key_code": "x" }
      ],
      "to_delayed_action": {
        "to_if_invoked": [
          { "set_variable": { "name": "ctrl-x", "value": 0 } }
        ],
        "to_if_canceled": [
          { "set_variable": { "name": "ctrl-x", "value": 0 } }
        ]
      },
      "conditions": [
        { "type": "variable_if", "name": "ctrl-x", "value": 0 }
      ]
    }
  ]
}

このコードの動作は以下のとおりです。

  1. ctrl-xをタイプするとctrl-x変数に1を代入する(toset_variableが実行される)
  2. 次にタイプしたキーにより、以下の処理が行われる。
    1. ctrl-c -> command-qに変換
    2. ctrl-f -> command-fに変換
    3. 上記1〜2以外 -> ctrl-x変数に0を代入する。(to_if_canceledset_variableが実行される)
  3. ctrl-xをタイプした後、一定の時間が経過するとctrl-x変数に0を代入する。(to_if_invokedset_variableが実行される)

補足

上記のJSONファイルを、冒頭に挙げたpqrs-org/KE-complex_modificationsで公開されているツールを使ってerbファイルで書いた場合のコードも掲載します。
KE-complex_modificationsのツールの使い方は、こちらの記事で解説しています。

{
  "title": "Two stroke key_bind",
  "rules": [
    {
    "description": "Two stroke key_bind",
    "manipulators": [
        <%=
          # control-xの次にタイプするキーを指定する
          used_keys = [
            ["c", []],
            ["c", ["control"]],
            ["d", ["control"]],
            ["f", ["control"]]
          ]

          # 変換後の組み合わせなどを指定する
          to_keys = [
            set_shell_command(["open -a 'Google Chrome.app'"]),
            to([["q", ["command"]]]),
            to([["vk_mission_control", ["command"]]]),
            to([["o", ["command"]]])
          ]

          types = ""
          used_keys.each_with_index do |use_key, index|
            types += "{
              \"type\": \"basic\",
              \"from\": #{from(use_key[0], use_key[1])},
              \"to\": #{to_keys[index]},
              \"conditions\": [
                #{variable_if(["press_control_x_key"], [1])}
              ]
            },"
          end
          types
        %>
        <%# set_variable, variable_if, set_shell_commandメソッドは自作 %>
        {
          "type": "basic",
          "from": <%= from("x", ["control"], ["caps_lock"]) %>,
          "to": [
            <%= set_variable(["press_control_x_key"], [1]) %>
          ],
          "to_if_alone": [
            <%= to([["x"]]) %>
          ],
          "to_delayed_action": {
            "to_if_invoked": [
              <%= set_variable(["press_control_x_key"], [0]) %>
            ],
            "to_if_canceled": [
              <%= set_variable(["press_control_x_key"], [0]) %>
            ]
          },
          "conditions": [
            <%= variable_if(["press_control_x_key"], [0]) %>,
          ]
        }
      ]
    }
  ]
}