QMKでTapとHoldのタイミングを考える

Planckなどの40%キーボードや、それ以下のキー数のキーボードになると、組合せ(dual-role)キーが必要になって来る。`Space`のホールドで`Shift`扱いにする SandS (Space and Shift)が代表例だ。特に親指で押すキーは激戦区で、複数の役割を持たせざるを得ない。

ただ、キーを押して、キーを離す、と文字が出る。という当たり前のことなんだけど、これが複数のキーと機能が絡みだした途端、割と複雑になっていく。きちんと整理しておかないと、後でわからなくなってしまいそうなので、知見を本稿に落とし込んでおこうと思う。

## QMKの既存の方法

QMKでも関連の議論は長く、いろいろな方法が編み出された。[長押しシフト](https://docs.qmk.fm/#/feature_auto_shift)や[複数タップ](https://docs.qmk.fm/#/feature_tap_dance)などはその一例で、確かに面白い...けど、筆者は実際に使うところまで至らなかった。

本命のSandS的な議論、「タップとホールドを使い分ける」話としては、QMKのドキュメント"[Tap-Hold Configuration Options](https://docs.qmk.fm/#/tap_hold)"に一番まとまっているので、ぜひ一読を。要点としては、次の3つの設定を押さえておく必要がある。

- `TAPPIG_TERM`: ミリ秒で指定。これ以上押していればホールド扱い(例. `Shift`)
- `PERMISIVE_HOLD`: TAPPING_TERM以下でも他のキーを`down`/`up`した時点でホールド扱い 
- `HOLD_ON_OTHER_KEY_PRESS`: TAPPING_TERM以下でも他のキーを`down`した時点でホールド扱い

### キーが1つの場合

ひとつめの`TAPPING_TERM`が仮250msだとすると、次図のようなことになる。
![timing-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1858/23596954-a26e-47f6-fa31-1f45b09e4471.png)
図の例では、「み」のキーをホールド時にShiftとして使う想定だ。SandSでも同様なので、適宜自分のキーボードに読み替えて読んでほしい。 *※ちなみに筆者はカナ打ちユーザ*

通常のキーボードであれば、キーが押された段階で「み」が入力されるところ、Dual-roleになっていると、キーを押した段階ではShiftが送られて、一旦入力を待機する。次にキーが離された時点で、押下時間に応じて「み」と入力されるか、Shiftだけになるかが決まる(A, B)。

### キーが2つの場合

ふたつのキーのときを考えてみる。ひとつひとつ打てば、当然それぞれのキーが入力される(C)。※ただし、`TAPPING_TERM`より短い場合
![timing-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1858/74ac63aa-cf29-e140-ea53-6df676ccfef1.png)
ひとつ目のキーを押し下げたままふたつ目のキーを押すと、どうなるか。デフォルトの設定では残念ながらこれも「みわ」と入力される。ここで`PERMISIVE_HOLD`を有効にしておくと、「を」が出力される(D)。

ただ、問題になるのはひとつ目のキーを先に離してしまう場合だ。そうすると、これも「みわ」と入力されてしまう。なお、`HOLD_ON_OTHER_KEY_PRESS`が有効であれば、「を」が出力される(E)。

## QMKの実装で足りない部分

`PERMISIVE_HOLD`では中途半端だし、多くのOSの実装と整合しない。QMKでできる範囲としては、`HOLD_ON_OTHER_KEY_PRESS`を設定しておくのが自然に思える。むしろこれがデフォルトで良さそうなものだけど...、後述のようにそうも行かない理由がある。

### 高速に打つと、キーが重なってしまう

`HOLD_ON_OTHER_KEY_PRESS`で「めでたしめでたし」なら良かった。でも、実はこの設定結構使いにくい。一定以上速く打つと、直前のキーを離す前に次のキーを押してしまいがちだ。通常のキーであれば、OS側で良しなにハンドリングしてくれるので、それでも特に問題ない。問題はdual-roleキーで、`HOLD_ON_OTHER_KEY_PRESS`にしたことで、一瞬でもキーが重なるとホールド扱いになってしまうのだ(G)。
![timing-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1858/e1f09202-ec30-e06d-7cd4-239998161d5f.png)
ホールドを優先したいケースと、タップを優先したいケースがあり、どちらも**キーの押下順序では区別がつかない**。単押しの判断に使う`TAPPIG_TERM`では、いささか長すぎてこれを基準にするのも難しい。

### 根本的な欠点と妥協点

これは Tap & Hold そのものの根本的な欠点で、完璧な解決方法は存在しない。論理的にも存在しない。でも、妥協点を探ることは可能で、筆者が採用したのは、`TAPPIG_TERM`の内側にもうひとつの基準を設けて、それ以下であればタップ扱いにするというものだ。図の例では、150msを基準にしている(H)。
![timing-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1858/7cc5c0f8-3c05-b42e-8537-5afca9803012.png)
上図のように、文字の流れの中で考えるとすこしわかりやすい。「棲み分け(J)」と「戸を蹴る(K)」と打つとき、完全に同じテンポで打ってしまうと、途中の文字の区別がつかない。シフト側を使わない方を基本と考えるなら、前者については何も考えずに打ちたい。一方で、後者を意識的に区別することはできる。なるべくなら、打鍵のリズムを崩したくないから、押し込むタイミングはある程度一定を保ちつつ、シフトのところだけ意識的に少しホールドを長くすることで、150msを超えるようにする。

## タイミング調整

前節までをまとめると、Tap&holdの理想は、次の表のようになるのではないか。

タイミング | 方針 | ▼▲ | ▼▽▲△ | ▼▽△▲
:-- | :-- | :-- | :-- | :--
0 - 150ms | タップ優先 | タップ | タップ | ホールド
150 - 250ms | ホールド優先 | タップ | ホールド | ホールド
250ms以上 | ホールドのみ | ホールド | ホールド | ホールド

250msと150msの基準で説明してきたけど、これはあくまでも例なので、各人で調整が要る。

できれば、親指と小指で続けてキーを打ってみて、何ミリ秒程度かかっているかを計測してみてみほしい。指によっても意外なほどスピードが異なるので、それも考慮したい。大体は、親指人差し指の組み合わせが最速で、小指薬指とかは下手すると倍くらい運指に時間がかかる。

### 微調整

`TAPPING_TERM`については、`TAPPING_TERM_PER_KEY`を有効にするとキーごとに調整が効く。小指だけ長めにとっておくと、少し扱いやすくなるかもしれない。

`TAPPING_TERM`を細かく調整したいニーズは多いようで、QMKに[Dynamic Tapping Term](https://docs.qmk.fm/#/tap_hold?id=dynamic-tapping-term)という機能も提供されている。有効にしておけば、`DT_UP`と`DT_DOWN`キーを配置しておくだけで、ドライバの書き換えなしに調整ができる。

### TAPPIG_TERMは2種類 (だと良かった)

QMKの`TAPPIG_TERM`は、設定によってタップを優先したいかホールドを優先したいかが、180度変わってしまう。今から作り直すなら、次の2つに分けてしまいたい(※願望)。

- `TAPPIG_TERM`: これ以下をタップ判定 (例. 150ms)
- `HOLDING_TERM`: これ以上をホールド判定 (例. 250ms)

なお、前者は、通常の運指スピードより、少しだけ大きい数字を設定する。後者は、ゆっくり目の単打より若干大きい数字を設定することを意図している。

## まとめ

残念ながら、前述のような「ふたつの`TAPPIG_TERM`」設定が、QMKではできない。[その部分については独自実装に](https://github.com/cognitom/windmill/blob/v2.0.1/firmware/windmill.c#L499-L547)なってしまった。本来であればQMK本体に手を入れて、プルリクエストを送りたいが、様々な設定との整合性をとるのはかなり厄介だ。現状すでに複雑でイシューとドキュメントが混乱気味。そこにさらに複雑さを増す提案を入れるのも気が引ける。[若干シンプルにしよう](https://github.com/qmk/qmk_firmware/pull/15741)という提案も出ているようなので、引き続き様子を見ていきたいと思う。

筆者はカナ打ちなので48キーがちょうどよいというか限界のキー数だけど、英数字のみなら42キーや36キーも視野に入って来る。パズルとしては30キーの配列を考えるのも楽しそうだ。

-----------

### メモ

なお、もし独自実装を改めて試みるなら、TAPPING_TERMの内側ではなく、外側の処理を追加する方針のほうが、コード量を減らせそうだ。レイヤー切り替えなどにも独自実装が必要だったので、今回その手が使えなかったが、今後の課題。

### 追加の参考資料

- [Okke](https://github.com/okke-formsma/)’s [cheatsheet on MT and LT behaviour](https://cdn.discordapp.com/attachments/663573863480950808/757162393209012304/modtap.pdf)
- [A guide to home row mods](https://precondition.github.io/home-row-mods)