golden-luckyの日記

ツイッターより長くなるやつ

QMKの「タップ」と「ホールド」を極める

自作キーボードを始めるとお世話になるQMKというファームウェアがあります。 キーボードは要するにスイッチなので、「どのスイッチが押されたときにどのキーの情報としてPCに伝えるか」を制御する必要があるのだけど、これはキーマップと呼ばれる情報をATmegaやARMのマイコン向けにコンパイルすることで開発します。そのための開発環境を提供してくれるのがQMKという感じ。

で、自作キーボードといってもたいていはキットを使うわけで、そういうキットの多くにはデフォルトのキーマップがあるから、そのデフォルトのキーマップをターゲットのマイコン向けにコンパイルしてそれをインストールすれば事足ります。 QMKには、代表的な自作キーボードのキット向けのデフォルトのキーマップもあらかじめほとんど用意されているので、そのキーマップに手を加えることで自分の好きな設定のキーボードに調整することも可能です。

調整というと、やりたい人がやればいい作業に聞こえるかもしれませんが、40%キーボードとか30%キーボードのように極端に物理キーが少なくなると、デフォルトでそのまま使える人はむしろ少なく、たいていは自分の癖や好みに応じたキーマップのカスタマイズが必須になると思います。 CtrlとかShiftとかAltといったモディファイヤキーはもちろん、数字キーすら足りなくなるので、1つの物理キーに4つとか5つとかのキーを割り当てることになるからです。

「どの物理キーを叩いたら何を入力できるようにするか」という基本がおおむね整ったら、その次にカスタマイズ攻略にとって重要になるのは「タップ」と「ホールド」を自分の運指の癖に合わせて設定することでしょう。 QMKでは調整可能な項目がけっこう多く、そのわりにケーススタディ的な解説が少なくてわりと試行錯誤したので、その記録を整理することにしました。

「タップ」と「ホールド」とは

1つの物理キーに複数の挙動を割り当てる際に、もっとも手頃なのは、「長押ししたときには別の役割を与える」という仕掛けでしょう。 たとえば、ある物理キーにこんなふうな設定を与えることで、[Ctrl]キーが物理的には存在しないキーボードが作れます。

  • 「タップ」すると、PCに[A]が送出される
  • 「ホールド」すると、PCには[A]ではなく[Ctrl]が送られる

この場合、このキーを「ホールド」しながら[S]のキーを押すことで[Ctrl-S]を入力できるので、[Ctrl-S]に関する操作性は[Ctrl]キーが物理的に存在しているキーボードとさほど変わりません。 これだけだと[Ctrl-A]を入力したいときにちょっと困ってしまいますが、同様の仕掛けを他のキー、たとえば「タップ」すると[Z]になるキーに設定しておけば、その[Z]のキーを「ホールド」してから[A]を「タップ」することで[Ctrl-A]を作れます。

30%キーボードとかになると、物理キーが30個くらいしかないので、この「ホールド」と「タップ」の設定をたくさんの物理キーに仕込むことになります。 しかし、そういうキーボードで高速にテキストを入力していると、「ホールド+タップ」のつもりの動作が「タップ+タップ」とみなされてイライラしたり、逆に「タップ+タップ」のつもりが「ホールド+タップ」になってびっくりしたりするといった症状にとても悩まされることになります。

物理キーが少ないと、キーマップを忘れて困るということはほとんどなくなりますが(単純に覚えることが少ないので)、この「タップ」と「ホールド」の細かな違いでフラストレーションがたまることがよくあります。 そこでQMKでは、この「タップ」と「ホールド」の挙動を細かく制御できる仕組みがいろいろ用意されています。

用意されているんですが、この「タップ」と「ホールド」の挙動の制御、リファレンスの説明では「どういうときにどんな設定をすればいいか」というのがわかりにくくて、「機能の説明」と「これは高速なタイピングをする人むけの設定です」みたいな雑な情報しかありません。 それでも試行錯誤の結果、現在は実用的に30キー(タップのみではアルファベット26文字とカンマ、ピリオド、リターン、バックスペースのみ)で生活できているので、ここまでに自分が読み取ったQMKにおける「タップ」と「ホールド」調整の勘所みたいなものをまとめたのが以下の記事です。 そのままコピペして使える情報ではないので、必ずQMKのリファレンスも参照してください。

「ホールド+タップ」のつもりが「タップ+タップ」になってイライラする場合

QMKでは、ホールドとみなされる時間をTAPPING_TERMという変数で管理しています。 この変数を小さくするほど、キーの押下がホールドとして認識されやすくなります。

TAPPING_TERMは、一般には200ミリ秒くらいが妥当とされていますが、タイピングが高速になると、この時間をもっと短くしないと打鍵のテンポが狂います。 これは、QMKでは2つのキーを同時に押すだけでは「片方がホールドされて同時に押された状態」とはみなされず、TAPPING_TERM以上ホールドを維持してはじめて同時に押された状態とみなされるからです。 「ホールド+タップ」をしたつもりでも、この期間内に指を離してしまったら、「タップ+タップ」になってしまうわけです。 先の例だと、[Ctrl-S]を入力したつもりが[A][S]という入力になってイラっとすることになります。

原因が「TAPPING_TERM以内に「ホールド+タップ」を完了してしまって最初の打鍵が「ホールド」とみなされないこと」にあるので、自分が「ホールド+タップ」の打鍵を完了する時間より短い値を最初のキーのTAPPING_TERMとして設定する、というのが解決策になります。 このために利用できるのがget_tapping_termという関数で、こんな感じにキーごとの設定をかなり細かく指定できます。

uint16_t get_tapping_term(uint16_t keycode, keyrecord_t *record) {
    switch (keycode) {
        case LCTL_T(KC_A):
            return 150;
        case LT(2,KC_B):
            return 100;
        case LT(1,KC_N):
            return 200;
        default:
            return TAPPING_TERM;
    }
}

ただし、get_tapping_termで短すぎる値を設定すると、今度は単発の「タップ」が難しくなります。現実的に自分は100ミリ秒くらいがストレスなくタップを実現できる下限でした。そのため、これより短い時間のうちに「ホールド+タップ」を完遂してしまうと、依然として「タップ+タップ」になってしまいます。

そこでQMKにはPERMISSIVE_HOLDという設定が用意されています。 この設定を有効にすると、TAPPING_TERMより短に時間内に「ホールド+タップ」を完遂しても「タップ+タップ」にならず、あくまでも「ホールド+タップ」として扱われるようになります。

PERMISSIVE_HOLDは案外と使えない

ただし、PERMISSIVE_HOLDは別種のイライラを引き起こします。 高速に連続して押した場合に「ホールド+タップ」とみなされては困るパターンがけっこうあるからです。

たとえば、QWERTY配列の最上段を数字キーと兼用していて、切り替えを[N]キーの「ホールド」にしているというケースを考えてみてください。 このとき、[N]キーを「ホールド」しながら[I]を「タップ」すると、「8」が入力されます。 この状況で、[N]キーにPERMISSIVE_HOLDを設定していると、高速なタイピングで「ni」と入力したつもりが毎回「8」とみなされるようになります。 これはうざい。

なので、PERMISSIVE_HOLDは実はあまり使えません。

幸い、get_permissive_holdという関数を使うことで、この設定を特定のキーのみで有効にできます。 次のキーに指をかけてしまうのを待てず、ほぼ同時に2つのキーを押してしまうようなパターン(自分の場合は「親指+人差指」とか)について、「ホールド」に倒すという設定が可能です。 自分の場合は、[B]キーと[F]キーの同時押しでIME起動をやっていて、これが日本語を高速に入力しているときに「bf」になることが多くてうっとうしいので、[B]キーに対してのみget_permissive_holdを使うことにしました。

bool get_permissive_hold(uint16_t keycode, keyrecord_t *record) {
    switch (keycode) {
        case LT(2,KC_B):
            return true;
        default:
            return false;
    }
}

「タップ+タップ」のつもりが「ホールド+タップ」になってびっくりする場合

PERMISSIVE_HOLDには、ゆるい打鍵がだいたい「ホールド」になるので、「タップ」のつもりの打鍵が思わず「ホールド」になってしまってびっくりするという弊害もあります。 たとえば、[A]キーに「ホールド」で[Ctrl]を割り当てているとしましょう。 そして、「ホールド」として多用したいのでget_tapping_termを短く、かつget_permissive_holdtrueにしたとしましょう。

この設定で、たとえばウェブフォームで「荒川」と入力すると、最初の「ar」で[Ctrl-r]というショートカットが発動してリロードが走り、それまでフォームに入力していた情報が消滅するといった状況がわりとよく起こります。

このようなびっくりを防ぐのに有効なのが、IGNORE_MOD_TAP_INTERRUPTという設定です。 この設定を有効にすると、TAPPING_TERMの時間だけしっかり同時に押されていた場合にのみ「ホールド」とみなされるようになります。 get_tapping_termが短いキーが「ホールド」になって次のキーとの組み合わせで予期しない入力になるという事態を防いでくれるわけです。

もちろん、get_permissive_holdを設定しているようなことがなければ、TAPPING_TERMの間に同時押しを完遂した打鍵は無事に「タップ+タップ」とみなされるので、「あるキーを「ホールド」とみなされにくくする」という目的に対しては「get_tapping_termを長めに設定する」のがセオリーです。 しかし、あまり長くすると「ホールド」になるまでの待ち時間で打鍵のリズムが狂うので、せいぜい225ミリ秒くらいが限界でしょう。 get_tapping_termはこの程度の長さにとどめておいて、IGNORE_MOD_TAP_INTERRUPTを有効にしておけば、うっかりホールドになってイライラするケースはかなり潰せると思います。

IGNORE_MOD_TAP_INTERRUPTは、「タップ+タップ」のつもりが「ホールド+タップ」になってびっくりするという症状に対して万能に思えますが、どうしても「ホールド」に倒したいというキーもあります。 その場合はget_ignore_mod_tap_interruptという関数で特定のキーをfalseにできます。

自分の場合は、左手の親指をわりとはやく上げてしまう癖があるようで、IGNORE_MOD_TAP_INTERRUPTが有効だと「ホールド+タップ」のつもりが「タップ+タップ」になってイライラするという症状に悩まされました。 なので、[B]キーだけはget_ignore_mod_tap_interruptfalseにしています。

bool get_ignore_mod_tap_interrupt(uint16_t keycode, keyrecord_t *record) {
    switch (keycode) {
        case LT(2,KC_B):
            return false;
        default:
            return true;
    }
}

「ホールド」のつもりが「タップ」の繰り返しになってしまってイライラする場合

これは実際にはタイプミスで、最初にしっかり「ホールド」ができていなくて一瞬指が浮いてしまったような場合に起こります。 ふつうのキーボードに期待されるような長押しによる連続入力を実現できるように、QMKでは「タップ」して「ホールド」するとこういう挙動を示すようになっているからです。

しかし、たとえば[Enter]キーに「ホールド」の挙動を付加するような場合、この挙動を抑制するほうが心穏やかな入力が可能です。

そこでQMKで用意されているのがTAPPING_FORCE_HOLDという設定です。 get_tapping_force_holdtrueに指定したキーは、単純に上記のような挙動が抑制されます。

bool get_tapping_force_hold(uint16_t keycode, keyrecord_t *record) {
    switch (keycode) {
        case LT(3,KC_ENT):
            return true;
        default:
            return false;
    }
}

本稿の執筆時は、この挙動を抑制するためにTAPPING_FORCE_HOLDという真偽値が使われていましたが、2023年2月26日の変更で、「タップ」の繰り返しとみなされる時間をミリ秒単位で指定できるようになりました(参考)。 すべてのキーに対する設定はQUICK_TAP_TERMであり、これはデフォルトでは上述したTAPPING_TERMと同一になっています。 キーごとに制御するためには、下記のようにget_quick_tap_termという関数を使います。

uint16_t get_quick_tap_term(uint16_t keycode, keyrecord_t *record) {
    switch (keycode) {
        case LT(3,KC_ENT):
            return 0;
        default:
            return QUICK_TAP_TERM;
    }
}

「タップ」したつもりが「ホールド」になっていて、指を離しても何も入力されない場合

TAPPING_TERMを短くしていると、細かい動作が苦手な薬指で押すキーなどで、すばやい「タップ」をしそこなって「ホールド」とみなされてしまうというケースが起こりえます。 そういう場合、そのキーにretro_typingを設定おくと、仮に「ホールド」になってしまっても何も別のキーを押さずに指を離せば「タップ」になります。

bool get_retro_tapping(uint16_t keycode, keyrecord_t *record) {
    switch (keycode) {
        case LCTL_T(KC_S):
            return true;
        default:
            return false;
    }
}

「ダブルタップ」という裏技

ここまでで「ホールド」と「タップ」に関する勘所はだいたい抑えたと思うんですが、実際のところ30キーだと「ホールド」と「タップ」だけでは機能が足りません。 たとえば自分は、[q]を二回素早く押すと[Esc]になるというトリックを混ぜていて、これはとても便利に使えています。 これはQMKのTap Danceという機能でプログラム可能で、ほかにもいろいろ応用できます。

enum {
      TD_Q_ESC,
};

void dance_q_finished(qk_tap_dance_state_t *state, void *user_data) {
    if (state->count == 1) {
        register_code16(KC_Q);
    } else {
        register_code(KC_ESCAPE);
    }
}

void dance_q_reset(qk_tap_dance_state_t *state, void *user_data) {
    if (state->count == 1) {
        unregister_code16(KC_Q);
    } else {
        unregister_code(KC_ESCAPE);
    }
}

qk_tap_dance_action_t tap_dance_actions[] = {
    [TD_Q_ESC] = ACTION_TAP_DANCE_FN_ADVANCED(NULL, dance_q_finished, dance_q_reset),
};

まとめ的な

一時期は仕事で疲れるとQMKのSoftware Featuresのリファレンスを眺めるという感じで、今では30キーでもほとんどストレスなく入力ができる状態になりました。

https://beta.docs.qmk.fm/using-qmk/software-featuresbeta.docs.qmk.fm

これからも便利そうな設定が見つかったら追記していこうと思います。