QMK Firmware で1つのキーに Hold/Double Tap を割り当てる方法

長い前置き

私はキー数が右手側・左手側 24 キーずつの合計 48 キーの自作キーボードを使っているのですが、IME の切り替えは、Windows の標準的なトグル式ではなく、Mac のように IME をオンにするキーとオフにするキーを別々に用意する形にしています。

この IME 切り替えキーの置き場所にいつも悩んでいるのですが、一番の希望先は親指周りのキーで、左手側のキーボードには IME オフのキーを、右手側のキーボードには IME オンのキーを割り当てて左右対称のキーマップにして、キーマップを覚えやすくしたいと思っています。

しかし、私のキーボードは親指周りに 8 キーありますが、ここに Win(左右), Alt(左右), Space(左手), Backspace(右手), lower(左手), raise(右手) の 6 種類を割り当てる必要があるため、空きが無い状態となっています。そのため、1 つのキーに一人二役を担わせる必要があるのですが、それができるのは以下のとおり lower/raise キーぐらいという状態です。

Win
単押しとホールドの両方行うので、二つ目の役割は二連打に割り当てることになる。
Alt
単押しとホールドの両方行うので、二つ目の役割は二連打に割り当てることになる。
Backspace
単押しと連打とホールドを行うので、二つ目の役割を担わせるのは不可能。
Space
単押しと連打とホールドを行うので、二つ目の役割を担わせるのは不可能。
lower
ホールドのみなので、単打か二連打に別の処理を割り当て可能。
raise
ホールドのみなので、単打か二連打に別の処理を割り当て可能。

そこで lower/raise キーに一人二役を担ってもらうのですが、単打とホールドを同じキーに割り当てる方法は既に編み出されています。

QMK Firmware で Raise/Lower と変換/無変換を同じキーに割り当てる - Okapies’ Archive

私もこの方法を採用していたのですが、キーボードのキー数が 48 キーなので、記号・数字の入力や矢印キーを使うときに LOWER/RAISE レイヤーに移動する必要があり、その時のタイピングミスでレイヤー移動(ホールド)のつもりが IME 切り替え(単打)になることがちょくちょくありました。これはかなりストレスになるので、やむなくこの方法を断念してコンボ機能を使って JK 同時押しに IME オン、DF 同時押しに IME オフを割り当てていましたが、IME 切り替えのために 2 つのキーを同時押しするのもイマイチという印象を感じていました。

そうした中、私が最初の翻訳を手掛けたタップダンス機能の解説の中に、1 つのキーに 4 つの機能を持たせるというものがあり、ホールドとダブルタップ(二連打)に別々の処理が割り当てられていたことを思い出しました。

4つの機能とその入力方法
  • 1回タップ = x を送信
  • 押し続ける = Control を送信
  • 2回タップ = Escape を送信
  • 2回タップして押し続ける = Alt を送信

この方法でホールドにレイヤー移動を割り当ててダブルタップに IME 切り替えを割り当てようと思ったのですが、処理が結構複雑だったので、ホールドと単打の使い分けの方法を応用してもっと処理を簡単にできないかと思い、一応希望する動作を実現できたので、備忘録としてどんな処理にしたのかをまとめます。

実際のコード

ホールドとダブルタップの判定は、キーを押す度に実行される process_record_user 関数の中で実施します。ただし、最初のタップ時に立てるフラグ (first_lower/raise_pressed) とキーを押した時間を格納する変数 (first_lower/raise_pressed_time) は、process_record_user 関数を終了しても状態を保存する必要がありますので、関数の外でグローバル変数として定義しています。

処理の内容は以下のコードのとおりですが、補足情報をコメントで書き込んでいます。

 1static bool first_lower_pressed = false;
 2static uint16_t first_lower_pressed_time = 0;
 3static bool first_raise_pressed = false;
 4static uint16_t first_raise_pressed_time = 0;
 5
 6bool process_record_user(uint16_t keycode, keyrecord_t *record) {
 7  switch (keycode) {
 8    case LOWER:
 9      if (record->event.pressed) {
10        if (!first_lower_pressed) {
11          first_lower_pressed_time = record->event.time;
12        // 一回目のタップのフラグがオン & 最初のキー押下から2回目のキー押下までの時間が TAPPING_TERM の2倍超なら
13        // 間隔を空けた2回目のタップと判断する
14        } else if (first_lower_pressed && (TIMER_DIFF_16(record->event.time, first_lower_pressed_time) > TAPPING_TERM * 2)) {
15          first_lower_pressed_time = record->event.time;
16          first_lower_pressed = false;
17        }
18        layer_on(_LOWER);
19        update_tri_layer(_LOWER, _RAISE, _ADJUST);
20      } else {
21        layer_off(_LOWER);
22        update_tri_layer(_LOWER, _RAISE, _ADJUST);
23        // タップのフラグがオフ & 最初のキー押下からキーを離した時までの時間が TAPPING_TERM 未満なら
24        // タップと判断する
25        if (!first_lower_pressed && (TIMER_DIFF_16(record->event.time, first_lower_pressed_time) < TAPPING_TERM)) {
26          first_lower_pressed = true;
27        // タップのフラグがオン & 最初のキー押下から2回目のタイプでキーを離した時までの時間が TAPPING_TERM の2倍以下なら
28        // ダブルタップと判断する
29        } else if (first_lower_pressed && (TIMER_DIFF_16(record->event.time, first_lower_pressed_time) <= TAPPING_TERM * 2)) {
30          tap_code(KC_LANG2);
31          first_lower_pressed = false;
32        } else {
33          first_lower_pressed = false;
34        }
35      }
36      return false;
37      break;
38    case RAISE:
39      if (record->event.pressed) {
40        if (!first_raise_pressed) {
41          first_raise_pressed_time = record->event.time;
42        } else if (first_raise_pressed && (TIMER_DIFF_16(record->event.time, first_raise_pressed_time) > TAPPING_TERM * 2)) {
43          first_raise_pressed_time = record->event.time;
44          first_raise_pressed = false;
45        }
46        layer_on(_RAISE);
47        update_tri_layer(_LOWER, _RAISE, _ADJUST);
48      } else {
49        layer_off(_RAISE);
50        update_tri_layer(_LOWER, _RAISE, _ADJUST);
51        if (!first_raise_pressed && (TIMER_DIFF_16(record->event.time, first_raise_pressed_time) < TAPPING_TERM)) {
52          first_raise_pressed = true;
53        } else if (first_raise_pressed && (TIMER_DIFF_16(record->event.time, first_raise_pressed_time) <= TAPPING_TERM * 2)) {
54          tap_code(KC_LANG1);
55          first_raise_pressed = false;
56        } else {
57          first_raise_pressed = false;
58        }
59      }
60      return false;
61      break;
62  }
63  first_lower_pressed = false;
64  first_raise_pressed = false;
65  return true;
66}

終わり

これで lower/raise キーのホールドにレイヤー移動機能を、ダブルタップに IME 切り替えの機能を持たせることができるようになりました。

ダブルタップに処理を割り当てた場合、タイピングミスでレイヤー移動のつもりが IME 切り替えになってしまうという事態はまず発生しませんので、快適に入力できるようになりました。

本記事がどなたかの参考になれば幸いです。