QMK Firmware に Mod-Tap と呼ばれるとても魅力的な機能がある。 タップした際は任意のキーとして扱われ、押し続けた際には Mod キー (Control や Shift、Alt) として認識されるというものだ。
MT(mod, kc)
というキーコードで利用することができる。 例えば MT(MOD_LCTL, KC_ESC)
とすると、タップした際は Escape として、押し続けた際は Control として扱われるキーを配置することができる。
魅力的な機能だが、タップ時に送るキーコード (kc
) に制約がある。
現在のところ、
MT()
の引数kc
は基本的なキーコードセットに制限されています。 つまり、LCTL()
、KC_TILD
、あるいは0xFF
より大きなキーコードを使うことができません。
IME の切り替えに使う目的で以下のようなキーを定義したい:
- タップ→ Control + F13 / 押したまま→ Meta (Windows/Command キー)
- タップ→ Shift + F13 / 押したまま→ Control
が、どちらも Mod-Tap では実現できないため、自分で実装することにした。 実装した機能を Meta-Tap と呼ぶことにする。
以下のコードはすべて keymap.c からの抜粋。
はじめに、Meta-Tap 用のキーコード metatap
を新しく宣言する:
enum metatap {
MT_LGUI = SAFE_RANGE,
MT_LCTL,
};
キーコードの宣言はレイヤーの定義の前に行う必要がある。
キーコードが metatap
に含まれているか判定する関数 is_metatap_code
と、タップか押し続けているかを判定する関数 is_tapped
を定義する:
bool is_metatap_code(uint16_t keycode) {
return MT_LGUI <= keycode && keycode <= MT_LCTL;
}
bool is_tapped(uint16_t pressed_time, uint16_t released_time) {
return TIMER_DIFF_16(released_time, pressed_time) < TAPPING_TERM;
}
Meta-Tap キーの状態を管理するため、それ用の struct
を定義する:
#define METATAP_MAX_NUM 2
struct Metatap_State {
uint16_t meta; // Keycode sent when held
bool pressed; // Pressed or not
bool registered; // Registered or not
uint16_t pressed_time; // Time that key is pressed. Used to determine it's tapped
uint16_t tapcode[METATAP_MAX_NUM]; // Keycodes array sent when tapped
};
欲しいキーの変数を保持しておく:
struct Metatap_State mt_lgui = {KC_LGUI, false, false, 0, {KC_LEFT_CTRL, KC_F13}};
struct Metatap_State mt_lctl = {KC_LCTL, false, false, 0, {KC_LEFT_SHIFT, KC_F13}};
metatap
に含まれるキーの、すべてのイベントを処理する関数を定義する:
void handle_metatap(uint16_t keycode, keyrecord_t *record) {
if (!is_metatap_code(keycode)) return;
struct Metatap_State *mt;
if (keycode == MT_LGUI) mt = &mt_lgui;
if (keycode == MT_LCTL) mt = &mt_lctl;
if (record->event.pressed) {
mt->pressed = true;
mt->pressed_time = record->event.time;
} else {
if (mt->registered) {
unregister_code(mt->meta);
mt->registered = false;
} else if (mt->pressed &&
is_tapped(mt->pressed_time, record->event.time))
{
caps_word_off();
// Register keycodes defined in mt->tapcode in order
for (int i=0; i<METATAP_MAX_NUM;i++) {
if (!mt->tapcode[i]) break;
register_code(mt->tapcode[i]);
}
// Unregister keycodes defined in mt->tapcode in reversed order
for (int i=METATAP_MAX_NUM-1;i>=0;i--) {
if (!mt->tapcode[i]) break;
unregister_code(mt->tapcode[i]);
}
}
mt->pressed = false;
}
}
metatap
キーが押されて離されるまでの間に別のキーが押された場合、そのキーと mt->meta
のキーの組み合わせを即座に送信したい:
void register_meta_if_needed(void) {
if (mt_lgui.pressed && !mt_lgui.registered) {
register_code(mt_lgui.meta);
mt_lgui.registered = true;
}
if (mt_lctl.pressed && !mt_lctl.registered) {
register_code(mt_lctl.meta);
mt_lctl.registered = true;
}
}
上で定義した handle_metatap
と register_meta_if_needed
は、キーの押し離しを制御するユーザー関数 process_record_user
から呼び出す:
bool process_record_user(uint16_t keycode, keyrecord_t *record) {
switch (keycode) {
case MT_LGUI:
case MT_LCTL:
handle_metatap(keycode, record);
return false;
default:
if (record->event.pressed) {
register_meta_if_needed();
}
}
return true;
}
🎉
もっといい実装があると思うが、動作には満足している。