40%キーボードを使い始めて、最初にぶつかる壁が「記号どこにあるの?」問題だと思います。
フルキーボードが 100 キー以上あるのに対して、40%キーボードは 40〜50 キー程度。当然ながら記号キーを並べる余裕はありません。
レイヤー切り替えは必須。ただ、レイヤーを増やしすぎると今度は「どのレイヤーに何があるか覚えられない」という別の問題が出てきます。
海外の自作キーボードコミュニティを調べていると、この問題に対するいくつかのアプローチが見つかりました。今回は、その中でもSequential Comboという手法を紹介します。僕が実際に使っている Lumberjack(48 キー)で実装している方法です。

40%キーボードの記号入力、何が大変なのか
まず、問題を整理しておきます。
フルキーボードでは、数字キーの上段に記号が割り当てられていますよね。Shift を押しながら1で!、9で(、みたいに。でも 40%キーボードには数字キー列がありません。
さらに言うと、プログラミングでよく使う記号って結構多いんです。
( ) [ ] { } < > // かっこ系
= + - * / % // 演算子
; : , . // 区切り
! ? & | ~ // その他
これらを全部レイヤーに配置しようとすると、最低でも 2〜3 レイヤーは必要になります。
海外の記事(Pascal Getreuer 氏のブログ)によると、記号の出現頻度はプログラミング言語によってかなり違うそうです。C/C++では_(アンダースコア)が最頻出、LaTeX では\(バックスラッシュ)が多い、といった具合に。
つまり、「自分がよく使う言語で頻出する記号」を優先的にアクセスしやすい位置に置くのが合理的ということです。
Sequential Comboという発想
ここで本題。Sequential Combo は、連続して同じキーを押すと別のキーが出力されるという仕組みです。
具体的にはこんな感じ。
( を2回連続で押す → )
[ を2回連続で押す → ]
{ を2回連続で押す → }
< を2回連続で押す → >
かっこを開いたら、大抵の場合は閉じますよね?だったら、開きかっこを 2 回叩けば閉じかっこが出る、というのは直感的だと思いませんか。
これ、実は QMK 標準の Combo 機能とは違います。標準 Combo は「複数キーの同時押し」で発火しますが、Sequential Combo は「同じキーの連続押し」で発火します。
この Sequential Combo は僕が自作した機能です。着想を得たのは、海外の Pascal Getreuer 氏が「Triggers」という名前で紹介している仕組み。
直近のキー入力をバッファに保持しておいて、パターンマッチングで判定するというアプローチです。これを参考に、かっこ系の記号に特化した形で実装しました。
実際の実装
僕の keymap.c では、以下のような Combo 定義をしています。
static const sequential_combo_entry_t my_combos[] = {
// かっこ系:開きを2回で閉じる
{LSFT(KC_9), LSFT(KC_9), LSFT(KC_0)}, // (( → )
{KC_LCBR, KC_LCBR, KC_RCBR}, // {{ → }
{KC_LBRC, KC_LBRC, KC_RBRC}, // [[ → ]
{LSFT(KC_COMM), LSFT(KC_COMM), LSFT(KC_DOT)}, // << → >
// 記号の組み合わせ
{KC_DOT, KC_COMM, KC_SCLN}, // ., → ;
{KC_COMM, KC_DOT, KC_EQL}, // ,. → =
{KC_COMM, KC_COMM, LSFT(KC_SCLN)}, // ,, → :
};
この定義は{最初のキー, 2番目のキー, 出力するキー}という構造です。
面白いのは、かっこ系だけでなく.と,の組み合わせでセミコロンやイコールを出せるようにしている点。プログラミング中に;や=は頻出するので、ホームロウから手を動かさずに打てるのは地味に便利です。
処理の流れ
Sequential Combo の判定ロジックはこんな感じです。
#define SEQUENTIAL_COMBO_TIMEOUT_MS 200
static uint16_t pending_key = KC_NO;
static uint16_t pending_time = 0;
bool process_sequential_combo(uint16_t keycode, keyrecord_t *record) {
if (!record->event.pressed) return true;
uint16_t now = timer_read();
// タイムアウトチェック(200ms以内かどうか)
if (pending_key != KC_NO &&
timer_elapsed(pending_time) > SEQUENTIAL_COMBO_TIMEOUT_MS) {
pending_key = KC_NO;
}
// Comboテーブルをチェック
for (int i = 0; i < SEQUENTIAL_COMBO_COUNT; i++) {
if (my_combos[i].first == pending_key &&
my_combos[i].second == keycode) {
// マッチ!出力キーを送信
tap_code16(my_combos[i].output);
pending_key = KC_NO;
return false; // 元のキーは送信しない
}
}
// マッチしなければ、現在のキーを次の判定用に保持
pending_key = keycode;
pending_time = now;
return true;
}
ポイントは200msのタイムアウトです。これより遅い入力は「連続」とみなしません。
なぜ 200ms かというと、QMK の Tapping Term(mod-tap の判定時間)が一般的に 150〜200ms に設定されることが多いからです。これより短いとタイプミスで誤爆しやすく、長いと意図的な連打との区別がつきにくくなります。
Home Row Modsとの組み合わせ
Sequential Combo と相性がいいのがHome Row Modsです。
Home Row Mods は、ホームロウのキー(A, S, D, F など)を長押しすると Ctrl や Shift として機能させるテクニック。40%キーボードでは、修飾キーを置くスペースも限られているので、これを使うと親指をレイヤー切り替えに専念させられます。
#define C_A LCTL_T(KC_A) // AキーをCtrl兼用に
#define S_Z LSFT_T(KC_Z) // ZキーをShift兼用に
海外のガイド(precondition.github.io)によると、Home Row Mods の設定で重要なのは以下の点です。
- Tapping Term は 150〜220ms で調整する(デフォルト 200ms)
- Quick Tap Term を 0 に設定すると、同じキーを素早く 2 回押したときに auto-repeat を防げる
- Permissive Hold を有効にすると、Tapping Term 前でも他のキーを押せば modifier として認識される
特に Quick Tap Term = 0 は Sequential Combo と組み合わせるときに重要です。これがないと、aaと打とうとしたときにaaaaaa...とリピートしてしまう可能性があります。
#define QUICK_TAP_TERM 0
#define TAPPING_TERM 200
#define PERMISSIVE_HOLD
使ってみた感想
正直なところ、最初の 1 週間は「あれ、どのキー2 回押すんだっけ」と迷うことがありました。でも、体が覚えてしまえば意識せずに打てるようになります。
特に効果を感じるのは、JSX や TypeScript を書いているとき。<div>のような山かっこを頻繁に打つ場面で、<<で>が出るのは本当に楽です。
逆に、慣れるまでは「普通に)を打ちたいのに(を 2 回打ってしまった」みたいなミスも起きます。Sequential Combo を導入するなら、閉じかっこ単体も別の方法でアクセスできるようにしておくといいかもしれません。
まとめ
40%キーボードで記号入力を効率化する Sequential Combo について紹介しました。
- かっこを 2 回押すと閉じるという直感的なルール
- 200ms のタイムアウトで Tapping Term と整合性を取る
- Home Row Mods と組み合わせると、さらにキー数を節約できる
海外の自作キーボードコミュニティでは、こういった「少ないキーでいかに効率よく打つか」の工夫がいろいろ共有されています。Pascal Getreuer 氏のブログは特に参考になるので、興味があれば覗いてみてください。
キーマップは結局「自分の手と使い方に合わせて育てていくもの」だと思います。この記事が、あなたのキーマップ改善のヒントになれば幸いです。