KE-complex_modificationsを使ってKarabiner-Elementsの設定を楽にする方法


Karabiner-Elementsを使うには設定ファイルを自分で書く必要がありますが、設定の数が少ないうちはまだしも、設定が増えるに従い、だんだんと設定ファイルを書く作業が面倒になります。また、同じような設定を複数書く必要がある場合、コピペを繰り返して一部だけ修正するというような作業を強いられたりします。
こうした問題を解消するツールとして、[pqrs-org/KE-complex_modifications](https://github.com/pqrs-org/KE-complex_modifications)というツールがあります。このツールを簡単に紹介しますと、karabiner-elementsの設定を'erb'ファイルに書いて'json'ファイルに変換するとともに、以下のような設定をImportできるページまで作成するというツールです。'erb'ファイルに設定をかけることから、Rubyの`each`などのメソッドが利用できるうえ、ツール自体にもいくつかの便利なメソッドが用意されており、これを使うとKarabiner-Elementsの設定が楽になります。

![Karabiner-Elements complex_modification.png](https://qiita-image-store.s3.amazonaws.com/0/136890/53489be6-1bf3-b990-005a-edf5fc34f6bc.png "Karabiner-Elements complex_modification.png")

なお、Karabiner-Elementsの設定自体は、こちらのページを参考にしてください。

[Karabiner-Elementsの設定項目をまとめました - Qiita](https://qiita.com/s-show/items/a1fd228b04801477729c)

また、karabiner-elementsの設定例はこちらを参考にしてください。

[Karabiner-Elementの設定例について - Qiita] (https://qiita.com/s-show/items/40ad22c4ee4a0465fad5)
[Karabiner-Elementsを使ってJISキーボードの"全角/半角"でIMEを切り替える方法 - Qiita] (https://qiita.com/s-show/items/08a7c1b558e4d7e6f1b0)
[Karabiner-Elementsの設定項目が増えてEmacsライクな設定が楽になった - Qiita] (https://qiita.com/s-show/items/e83215f4ee10422abd7c)
[Karabiner-Elementsでenthumble(Windows App)のような操作を実現する方法 – 考えるために書くブログ](https://writeforthinking.wordpress.com/2017/11/12/karabiner-elements%e3%81%a7enthumblewindows-app%e3%81%ae%e3%82%88%e3%81%86%e3%81%aa%e6%93%8d%e4%bd%9c%e3%82%92%e5%ae%9f%e7%8f%be%e3%81%99%e3%82%8b%e6%96%b9%e6%b3%95/)

# 導入方法

GitHubで公開されていますので、以下のコマンドで導入できます。

```bash
git clone https://github.com/pqrs-org/KE-complex_modifications 
```

また、上記のリポジトリは本家ですが、その本家からフォークして、'erb'ファイルに追加した説明文をHTMLに出力する際のオプションを指定できたり、メソッドも本家より豊富で使いやすいリポジトリがありますので、こちらを使うのも良いかと思います。

```bash
git clone https://github.com/rcmdnk/KE-complex_modifications
```

なお、単にツールを使うだけなら`clone`でローカルにコピーすれば良いのですが、自分の設定や設定をインポートする画面も公開したい場合は、上記のリポジトリをフォークして使用する方が良いです。

# 導入後のカスタマイズ

## ファイルの整理

'src/json'に、色々な人が作成した設定ファイルが多数ありますので、不要なものを削除し、あわせて自分用のファイルを作成します（既存のファイルをリネームするのがオススメです）。また、'docs/json'にあるファイルは、'src/json'にある'erb'ファイルから`make`コマンドで作成されるファイルですので、全て削除しても大丈夫です。また、'src/example.html.erb'ファイルも削除します。

## Makefileの編集

`Makefile`を以下のとおり編集します。（'example.html.erb'ファイルを操作することはないため）

```diff
 all:
 	scripts/update-json.sh
 	scripts/erb2html.rb < src/index.html.erb > docs/index.html
-	scripts/erb2html.rb < src/example.html.erb > docs/example.html
 	scripts/apply-lint.sh
 
 rebuild:
 	touch src/json/*
 	$(MAKE) all
```

## script/updata-json.shの編集

`make`コマンドを実施すると、'*.erb'ファイルとそれに対応する'*.json'ファイルのタイムスタンプを確認し、'erb'ファイルの更新日時が'json'ファイルより古い場合、'json'ファイルへの変換は行わない設定になっています。
不要な変換を行わないこの設定は確かに合理的ですが、これにより、新しいメソッドを追加するために'erb2json.rb'だけ編集した場合、`make`コマンドを実行しても'json'への変換が行われません。`make`コマンドの所要時間は短いので、いつでも全ての'erb'ファイルを'json'ファイルに変換するように設定するように'script/update-json.sh'を編集しました。この編集を行うかどうかは自由です。なお、この編集を行わない場合で、全ての'erb'ファイルを強制的に変換したい場合は`make rebuild`コマンドでOKです。

```diff
#!/bin/sh

for srcfile in src/json/*.erb; do
    dstfile="docs/json/`basename $srcfile .erb`"
-   if [ "$srcfile" -nt "$dstfile" ]; then
+   # if [ "$srcfile" -nt "$dstfile" ]; then
      if scripts/erb2json.rb < "$srcfile" > "$dstfile"; then
        echo "$dstfile"
      else
        echo "Failed to convert $srcfile to $dstfile"
        rm -f "$dstfile"
        exit 1
-     fi
+   # fi
done
```

## src/index.htmlの編集

'src/index.html.erb'ファイルのうち、`<div class="container">`タグの中にある`<% ~ %>`の箇所を編集します。

変更前

```HTML
<div class="container" style="margin-bottom: 100px">
  <div class="text-right" style="margin-bottom: 30px">
    <a href="https://github.com/pqrs-org/KE-complex_modifications">GitHub</a>
  </div>

  <%
      $toc = []
      $groups = ""
      add_group("Modifier Keys","modifier_keys",[
          "docs/json/caps_and_return_to_ctrl.json",
          "docs/json/caps_lock.json",
          "docs/json/control.json",
          "docs/json/shift.json",
          "docs/json/escape.json",
          "docs/json/change_grave_accent_to_escape.json",
          "docs/json/modifiers_to_f-keys.json",
          "docs/json/delete.json",
          "docs/json/change_command_l.json",
          "docs/json/change_command_r.json",
          "docs/json/ctrl_command_fn_letter_specials.json"
      ])
    //中略
```

変更後

```HTML
<div class="container" style="margin-bottom: 100px">
  <div class="text-right" style="margin-bottom: 30px">
    <a href="https://github.com/pqrs-org/KE-complex_modifications">GitHub</a>
  </div>

  <%
      $toc = []
      $groups = ""
      add_group("Personal Settings","personal_settings",[
          "personal_s-show.json"
      ])
  %>
    //中略
```

`add_group`の引数は、一番目が「画面に表示する文字列（上のスクショならPersonal Settings）」、二番目が設定毎に割り当てたID値、三番目が公開するJSONファイルのパスです。そのため、上の変更後の設定は、「personal_s-show.jsonというファイルの設定を、Personal Settingsという名前で公開する」という設定になります。

ここまで行えば、ひとまずの導入作業は完了です。自分の設定を上のスクリーンショットのような形で公開したい場合、GitHub Pagesの設定も必要になりますが、本筋から外れそうなので省略します。


# メソッド（当初から用意されているもの）

KE-complex_modificationsの'scripts'ディレクトリにある'erb2json.rb'というファイルに、'erb'ファイルで設定を書くときに便利なメソッドが用意されています。用意されているメソッドのうち、使用頻度が高そうなものを紹介していきます。なお、自分でメソッドを作る際は、このファイルに書くことになります。私が作ったメソッドも紹介します。

## from

`from`に設定するキーの組み合わせを生成するメソッドです。一番目の引数が設定したいキー、二番目の引数が`modifiers`の`mandatory`に設定するキー（Shift, Controlなど）、三番目の引数が`modifiers`の`optional`に設定するキーです。

### 使用例

`from`に、"CapsLock"のオンオフに関わらず、"ctrl-shift-PageUp"の組み合わせに反応するよう指定する設定です。

```
"from": <%= from("page_up", ["control", "shift"], ["caps_lock"]) %>
```

`from`に、"全角/半角"キーを指定する設定です。

```
"from": <%= from("grave_accent_and_tilde") %>
```

### 変換結果

```json
"from": {
  "key_code": "page_up",
  "modifiers": {
    "mandatory": [ "control", "shift" ],
    "optional": [ "caps_lock" ]
  }
}
```

```JSON
"from": { "key_code": "grave_accent_and_tilde" }
```

なお、変換結果については、ページを少しでも短くするため、改行された出力結果の一部を1行にまとめています。（以下同じ）

## to

`to`に設定するキーの組み合わせを生成するメソッドです。メソッドの返り値は配列になりますので、以下の使用例のように、返り値を`[]`で囲む必要はありません。


### 使用例

`to`に「何かのキーの組み合わせを"command-k"に変換し、続けて、続けて"command-左矢印"に変換」するという動作を指定する設定です。なお、引数は配列で渡す必要があるのですが、配列が入れ子になっていて頭が混乱しやすいので、以下の使用例ではわざと改行しています。

```
"to":
  <%=
    to([
        ["k", ["command"]],
        ["left_arrow", ["command", "shift"]]
      ])
  %>
```

何かのキーの組み合わせを"tab"に変換する設定です。

```
"to": <%= to(["tab"]) %>
```

### 変換結果

```json
"to": [
  {
    "key_code": "k",
    "modifiers": [ "command" ]
  },
  {
    "key_code": "left_arrow",
    "modifiers": [ "command", "shift" ]
  }
]
```

```json
"to": [
  { "key_code": "tab" }
]
```

## frontmost_application_if, frontmost_application_unless

変換ルールを適用（または除外）するアプリを指定するため、`conditions`の`frontmost_application_if(or unless)`を設定するときに使用します。

### 使用例

```
"conditions": [ <%= frontmost_application_if("finder") %> ]
```

### 変換結果

```json
"conditions": [
  {
    "type": "frontmost_application_if",
    "bundle_identifiers": [ "^com\\.apple\\.finder$" ]
  }
]
```

### アプリの指定方法

対象となるアプリは、引数に所定のアプリ名で指定します。この所定のアプリ名は、'erb2json.rb'の`frontmost_application`メソッドの中で指定されていますので、それと同じものを使う必要があります。所定のアプリ名は、以下のコードの形で行われていますので、自分でアプリ名を追加したい場合は、以下のコードを参考にして自分で'erb2json.rb'ファイルを編集する必要があります。

```ruby
def frontmost_application(type, app_aliases)
  # 中略
  finder_bundle_identifiers = [
    '^com\.apple\.finder$',
  ]
  # 中略
  bundle_identifiers = []
  to_array(app_aliases).each do |app_alias|
    case app_alias
    when 'finder'
      bundle_identifiers.concat(finder_bundle_identifiers)
  # 以下省略
```

## each_key

複数の同じような設定がある場合に、それらの設定をまとめて書くために使います。文章で説明するよりも実例を見た方が分かりやすいので、以下の使用例と変換結果をみてください。
なお、`each_key`メソッドは、`manipulators`の直下で使うことが想定されているメソッドで、返り値は配列です。そのため、以下の「上手くいかない例」のように、`manipulators`の直下に`each_key`を使わずに設定を書いた後、`each_key`を使って設定を書くという使い方はできません。

### 使用例

"control-command-h, j, k, l"を"カーソルキー"に変換する処理です。

```
"description": "Change cmd-ctrl-h/j/k/l to arrow keys",
"manipulators":
<%=
  each_key(
    source_keys_list: ["h", "j", "k", "l"],
    dest_keys_list: ["left_arrow", "down_arrow", "up_arrow", "right_arrow"],
    from_mandatory_modifiers: ["control", "command"],
    as_json: true
  )
%>
}
```

### 上手くいかない例

```
{
  "description": "(Enthumble) IJKL Mode / normal",
  "manipulators": [
    {
      "type": "basic",
      "from": <%= from("international5", [], ["any"]) %>,
      "to": [ <%= set_variable(["enthumble_mode"], [1]) %> ]
    },
    <%=
      each_key(
        source_keys_list: ["j", "k", "i", "l"],
        dest_keys_list: ["left_arrow", "down_arrow", "up_arrow", "right_arrow"],
        from_mandatory_modifiers: ["control", "command"],
        as_json: true
      )
    %>
  ]
}
```

### 変換結果

```json
{
  "description": "Change cmd-ctrl-h/j/k/l to arrow keys",
  "manipulators": [
    {
      "type": "basic",
      "from": {
        "key_code": "h",
        "modifiers": { "mandatory": [ "control" ] }
      },
      "to": [ { "key_code": "left_arrow" } ]
    },
    {
      "type": "basic",
      "from": {
        "key_code": "j",
        "modifiers": { "mandatory": [ "control" ] }
      },
      "to": [ { "key_code": "down_arrow" } ]
    },
    {
      "type": "basic",
      "from": {
        "key_code": "k",
        "modifiers": { "mandatory": [ "control" ] }
      },
      "to": [ { "key_code": "up_arrow" } ]
    },
    {
      "type": "basic",
      "from": {
        "key_code": "l",
        "modifiers": { "mandatory": [ "control" ] }
      },
      "to": [ { "key_code": "right_arrow" } ]
    }
  ]
}
```

# メソッド（自分で作ったもの）

ここからは、'erb2json.rb'ファイルに私が追加したメソッドを紹介していきます。

## make_data

他のメソッドから呼び出されて、JSON形式の文字列を生成するメソッドです。`erb`ファイルから直接使うことはないですが、以下の自作メソッドから呼び出されることが多いメソッドです。

### コード

```Ruby
def make_data(data, as_json=true)
  if as_json
    JSON.generate(data)
  else
    data
  end
end
```

## set_variable

`to`に`set_variable`を設定する場合に使うメソッドです。一番目の引数に変数名を、二番目の引数に変数の値を指定します。

### 使用例

`enthumble_mode`という変数に`1`を代入する処理です。

```
"to": [ <%= set_variable(["enthumble_mode"], [1]) %> ]
```

### 変換結果

```json
"to": [
  {
    "set_variable": { "name": "enthumble_mode", "value": 1 }
  }
]
```

### コード

```Ruby
def set_variable(names, values, as_json=true)
  data =[]
  unless names.empty?
    names.each_with_index do |name, index|
      data = {
        "set_variable" => {
          "name" => name,
          "value" => values[index]
        }
      }
    end
  else
    $stderr << "name empty.\n"
  end
  make_data(data, as_json)
end
```

## variable_if, variable_unless

`conditions`に`"type": "variable_if"`または`"type": "variable_unless"`を設定する場合に使うメソッドです。一番目の引数に変数名を、二番目の引数に変数の値を指定します。

### 使用例

「`enthumble_mode`変数の値が`0`なら」という設定です。

```
"conditions": [ <%= variable_if(["enthumble_mode"], [0]) %> ]
```

### 変換結果

```json
"conditions": [
  { "type": "variable_if", "name": "enthumble_mode", "value": 0 }
]
```

### コード

```Ruby
def variable(type, names, values, as_json=true)
  data =[]
  unless names.empty?
    names.each_with_index do |name, index|
      data = {
        "type" => type,
        "name" => name,
        "value" => values[index]
      }
    end
  else
    $stderr << "name empty.\n"
  end
  make_data(data, as_json)
end

def variable_if(names, values, as_json=true)
  variable('variable_if', names, values, as_json)
end

def variable_unless(names, values, as_json=true)
  variable('variable_unless', names, values, as_json)
end
```

## input_source_if, input_source_unless

IMEの状態に応じて処理を振り分けるため、`conditions`に`"type": "input_source_if"`または`"type": "input_sources_unless"`を設定する場合に使うメソッドです。引数にIMEの状態を示す文字列を渡します。IMEオンなら`"ja"`、IMEオフなら`"en"`を指定します。

### 使用例

```
"conditions": [ <%= input_source_if("ja") %> ]
```

### 変換結果

```json
"conditions": [
  {
    "type": "input_source_if",
    "input_sources": [ { "language": "ja" } ]
  }
]
```

### コード

```Ruby
def input_source(type, input_source_aliases, as_json=true)
  input_sources = []
  to_array(input_source_aliases).each do |input_source_alias|
    if input_source_alias.is_a? Hash
      input_sources << input_source_alias
    end
    if input_source_alias.include?("keylayout")
      input_sources << { "input_source_id": input_source_alias}
    elsif input_source_alias.include?("inputmethod")
      input_sources << { "input_mode_id": input_source_alias}
    else
      input_sources << { "language": input_source_alias}
    end
  end

  unless input_sources.empty?
    data = {
      "type" => type,
      "input_sources" => input_sources
    }
    make_data(data, as_json)
  end
end

def input_source_if(input_source_aliases, as_json=true)
  input_source('input_source_if', input_source_aliases, as_json)
end

def input_source_unless(input_source_aliases, as_json=true)
  input_source('input_source_unless', input_source_aliases, as_json)
end
```

## set_shell_command

`to`に変換後のキーの組み合わせではなく、シェルコマンドの実行を割り当てたいときに使用します。引数にはシェルコマンドダブルクオテーションで囲んで渡します。


### 使用例

何らかのキーの組み合わせに対して、Finderを開くという処理を設定するものです。ターミナルで入力するときは`open -a 'finder'`と入力しますので、このコマンドをダブルクオテーションで囲んで引数にしています。

```
"to": <%= set_shell_command(["open -a 'finder'"]) %>
```

### 変換結果

```json
"to": [ { "shell_command": "open -a 'finder'" } ]
```

### コード

```Ruby
def set_shell_command(commands, as_json=true)
  data =[]
  unless commands.empty?
    commands.each do |command|
      data << { "shell_command" => command }
    end
  else
    $stderr << "name empty.\n"
  end
  make_data(data, as_json)
end
```

# Rubyのメソッドでよく使うもの

Rubyのメソッドでは、`each`や`zip`を使っています。以下の使用例のように、複数のキーにまとめて類似の設定を割り当てる際に、これらのメソッドが重宝します。

## each, zip

### 使用例

`enthumble_mode`変数の値が`1`なら、"j, k, i, l"をカーソルキーに変換するという処理です。文字列型の`types`変数に`types`以下の設定を追加していく方法で4つのキーの設定をひとまとめに設定しているのですが、末尾の","を削除する必要がありますので、`types.chop!`で末尾の１文字を削除しています。

```
<%=
  from_keys = ["j", "k", "i", "l"]
  allow_keys   = ["left_arrow", "down_arrow", "up_arrow", "right_arrow"]

  types = ""
  from_keys.zip(allow_keys) do |from_key, allow_key|
    types += "{
      \"type\": \"basic\",
      \"from\": #{from(from_key, [], ["any"])},
      \"to\": #{to([[allow_key]])},
      \"conditions\": [
        #{variable_if(["enthumble_mode"], [1])}
      ]
    },"
  end
  types.chop!
  types
%>
```

### 変換結果

```json
{
  "type": "basic",
  "from": {
    "key_code": "j",
    "modifiers": { "optional": [ "any" ] }
  },
  "to": [ { "key_code": "left_arrow" } ],
  "conditions": [
    { "type": "variable_if", "name": "enthumble_mode", "value": 1 }
  ]
},
{
  "type": "basic",
  "from": {
    "key_code": "k",
    "modifiers": { "optional": [ "any" ] }
  },
  "to": [ { "key_code": "down_arrow" } ],
  "conditions": [
    { "type": "variable_if", "name": "enthumble_mode", "value": 1 }
  ]
},
{
  "type": "basic",
  "from": {
    "key_code": "i",
    "modifiers": { "optional": [ "any" ] }
  },
  "to": [ { "key_code": "up_arrow" } ],
  "conditions": [
    { "type": "variable_if", "name": "enthumble_mode", "value": 1 }
  ]
},
{
  "type": "basic",
  "from": {
    "key_code": "l",
    "modifiers": { "optional": [ "any" ] } },
  "to": [ { "key_code": "right_arrow" } ],
  "conditions": [
    { "type": "variable_if", "name": "enthumble_mode", "value": 1 }
  ]
}
```

---

参考したページ

[KE-complex_modificationsを使ってKarabiner-Elementsの定義を色々作って公開する ](https://rcmdnk.com/blog/2017/08/30/computer-mac-karabiner/)

[Karabiner-Elements用のKE-complex_modificationsのextra_descriptionsを自動で生成する ](https://rcmdnk.com/blog/2017/09/01/computer-mac-karabiner/)

---

私の設定

[s-show/KE-complex_modifications](https://github.com/s-show/KE-complex_modifications)