Tech Nuggets (15) In English

QMK: Mod-Tap を代替する機能を実装する

Posted:

QMK FirmwareMod-Tap と呼ばれるとても魅力的な機能がある。 タップした際は任意のキーとして扱われ、押し続けた際には Mod キー (Control や Shift、Alt) として認識されるというものだ。

MT(mod, kc) というキーコードで利用することができる。 例えば MT(MOD_LCTL, KC_ESC) とすると、タップした際は Escape として、押し続けた際は Control として扱われるキーを配置することができる。

魅力的な機能だが、タップ時に送るキーコード (kc) に制約がある。

現在のところ、MT() の引数 kc は基本的なキーコードセットに制限されています。 つまり、LCTL()KC_TILD、あるいは 0xFF より大きなキーコードを使うことができません。

モッドタップ - 注意事項 | QMK Firmware

IME の切り替えに使う目的で以下のようなキーを定義したい:

が、どちらも 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_metatapregister_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;
}

🎉


もっといい実装があると思うが、動作には満足している。