Tech Nuggets (3) 日本語版はこちら

QMK: Implement Mod-Tap alternative

Posted:

QMK Firmware has a great feature called Mod-Tap. You can have a key that sends a regular key when you tap it, but sends mod key (like Control or Shift) when you hold it.

While this feature is very attractive, it does have a limitation that cannot be ignored.

Currently, the kc argument of MT() is limited to the Basic Keycode set, meaning you can’t use keycodes like LCTL(), KC_TILD, or anything greater than 0xFF.

Mod-Tap - Caveats | QMK Firmware

I want to have two keys like:

but both cannot be done with Mod-Tap. So I decided to implement it by myself and named it as Meta-Tap.


All of the following code is in keymap.c.


First of all, I defined designated metatap keycodes:

enum metatap {
  MT_LGUI = SAFE_RANGE,
  MT_LCTL,
};

These codes must be defined before defining layers.


Also defined two helper functions:
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;
}

I have to manage the state of *Meta-Tap* keys, so created `struct` for that:
#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
};

Make two keys I wanted:
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}};

Create a new function that handles all events of `metatap` keys:
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;
  }
}

If another key is pressed while the metatap key is being pressed, I want to send that key immediately with the meta key:

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;
  }
}

Call above two functions handle_metatap and register_meta_if_needed from 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;
}

That's it.


I'm not familiar with C, so I think there are better ways for this. If you have any suggestions for improvements, no matter what, I would be glad to hear from you.