← More about keyboards

Overview

We will discuss how to implement different kinds of triggers for an action in QMK. By “triggers,” I mean reacting to events like that a button was double tapped or a layer became active. I assume here you are familiar with things like handling a custom keycode in process_record_user(); if not, check out my QMK macros post first. As covered in that post, a lot of useful effects are possible with a button that performs a custom action. Triggers enable yet more beyond that.

License

Code snippets in this post are shared under Apache 2 license.

Copyright 2022 Google LLC

Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

On entering or exiting a layer

The layer_state_set_user() callback is called every time the layer state changes. Within this callback, use get_highest_layer(state) to get the index of the highest active layer or IS_LAYER_ON_STATE(state, layer) to check whether layer is on.

Implement an LED layer indicator by adding something like this in keymap.c:

layer_state_t layer_state_set_user(layer_state_t state) {
  writePin(B0, get_highest_layer(state) > 0);
  return state;
}

Change B0 to the pin for the LED to use. The above assumes setting the pin high turns the LED on—if that’s flipped, negate the logic on the second arg.

Note that layer_state_set_user() is called on every layer state change. To react only when entering or exiting a specific ADJUST layer, use a pattern like this:

layer_state_t layer_state_set_user(layer_state_t state) {
  // Use `static` variable to remember the previous status.
  static bool adjust_on = false;

  if (adjust_on != IS_LAYER_ON_STATE(state, ADJUST)) {
    adjust_on = !adjust_on;
    if (adjust_on) {  // Just entered the ADJUST layer.
      PLAY_SONG(MAJOR_SONG);
    } else {          // Just exited the ADJUST layer.
      PLAY_SONG(GOODBYE_SONG);
    }
  }

  return state;
}

See also the QMK documentation on Layer Change Code and LED Indicators.

Tap vs. long press

Taking inspiration from Auto Shift, we can have a key perform different actions on a regular tap vs. holding the key a bit longer. While a similar effect could be done with a tap dance, a better way to do it is to customize a layer-tap LT key. Thanks to @filterpaper and @jweickm for teaching me this trick.

Compared to tap dance, customizing an LT gets the tap-hold decision logic for free, which is more finely configurable to avoid accidental fires on rolled presses and so on. Once you have seen the pattern, it’s also arguably simpler to write.

First, define a layer-tap key like LT(0, kc), where kc is a basic keycode to be sent on tap and the layer is a dummy placeholder.

#define COMM_COPY LT(0, KC_COMM)
#define DOT_PASTE LT(0, KC_DOT)
#define MPLY_MNXT LT(0, KC_MPLY)

Then in process_record_user(), we customize the long press action:

// Helper for implementing tap vs. long-press keys. Given a tap-hold
// key event, replaces the hold function with `long_press_keycode`.
static bool process_tap_or_long_press_key(
    keyrecord_t* record, uint16_t long_press_keycode) {
  if (record->tap.count == 0) {  // Key is being held.
    if (record->event.pressed) {
      tap_code16(long_press_keycode);
    }
    return false;  // Skip default handling.
  }
  return true;  // Continue default handling.
}

bool process_record_user(uint16_t keycode, keyrecord_t* record) {
  switch (keycode) {
    case COMM_COPY:  // Comma on tap, Ctrl+C on long press.
      return process_tap_or_long_press_key(record, C(KC_C));

    case DOT_PASTE:  // Dot on tap, Ctrl+V on long press.
      return process_tap_or_long_press_key(record, C(KC_V));

    case MPLY_MNXT:  // Play/pause on tap, next song on long press.
      return process_tap_or_long_press_key(record, KC_MNXT);

    // Other macros...
  }

  return true;
}

Configure the tapping term to determine how long the button needs to be held to be considered a tap vs. long press. See also changing the hold function in the mod-tap documentation. The above definition of process_tap_or_long_press_key() taps the long press keycode just once when held. If you want the keycode repeated, use the following instead:

static bool process_tap_or_long_press_key(
    keyrecord_t* record, uint16_t long_press_keycode) {
  if (record->tap.count == 0) {  // Key is being held.
    if (record->event.pressed) {
      register_code16(long_press_keycode);
    } else {
      unregister_code16(long_press_keycode);
    }
    return false;  // Skip default handling.
  }
  return true;  // Continue default handling.
}

The above requires that the tap action is a basic keycode, a limitation due to how tap-hold keys are represented. If you need an advanced keycode or will execute custom code for the tap action, this is possible by changing the tap function as well. Start by defining a layer-tap key in which both the mod and tap keycode are placeholders.

#define FANCY_KEY  LT(0, KC_1)

If you define multiple such keys with this approach, use different tap keycodes (say, KC_1, KC_2, KC_3, …) to make them distinct.

case FANCY_KEY:
  if (record->tap.count > 0) {    // Key is being tapped.
    if (record->event.pressed) {
      // Handle tap press event...
    } else {
      // Handle tap release event...
    }
  } else {                        // Key is being held.
    if (record->event.pressed) {
      // Handle hold press event...
    } else {
      // Handle hold release event...
    }
  }
  return false;  // Skip default handling.

When mod combo is held

You can use pressing Left Shift and Right Shift at the same time as a way to trigger an action, like I did for Caps Word. The below method works with the normal Shift keys (KC_LSFT, KC_RSFT) as well as one-shot Shift mods and Space Cadet Shift. The action can also be triggered using mod-tap Shift keys by holding both Shift mod-tap keys until the tapping term, then release them.

const uint8_t mods = get_mods() | get_oneshot_mods();
if (mods == MOD_MASK_SHIFT) {  // Left Shift + Right Shift held.
  clear_mods();  // If needed, clear the mods.
  // Do something interesting...
}

Keep in mind that Shift is active when triggered, so call clear_mods() first if needed before performing the action. You could similarly trigger an action when both Ctrl keys are held by changing MOD_MASK_SHIFT to MOD_MASK_CTRL or even use a mix like Left Alt + Right GUI with (MOD_BIT(KC_LEFT_ALT) | MOD_BIT(KC_RIGHT_GUI)).

See also Checking Modifier State.

Note about Command: The Command feature also uses the Left Shift + Right Shift mod combination. To avoid conflict, disable Command by adding in rules.mk:

COMMAND_ENABLE = no

Or set it to use a different mod combination by defining IS_COMMAND() in config.h:

// Activate Command with Left Ctrl + Right Ctrl.
#define IS_COMMAND() (get_mods() == MOD_MASK_CTRL)

Action on double tap, without delay

Suppose you want to perform some action when a Shift or Ctrl key is double tapped, yet otherwise have the key act as usual without any delays. This could again be done with a tap dance, but here is a short code snippet to do it directly.

With this implementation, the Shift key continues to function as usual even as it is double tapped. This is essential in making Shift act without delay. Otherwise when Shift is pressed, we would have to wait out TAPPING_TERM to see whether it gets tapped again before sending keys to the host.

if (record->event.pressed) {
  static bool tapped = false;
  static uint16_t tap_timer = 0;
  if (keycode == KC_LSFT) {
    if (tapped && !timer_expired(record->event.time, tap_timer)) {
      // The key was double tapped.
      clear_mods();  // If needed, clear the mods.
      // Do something interesting...
    }
    tapped = true;
    tap_timer = record->event.time + TAPPING_TERM;
  } else {
    // On an event with any other key, reset the double tap state.
    tapped = false;
  }
}

When KC_LSFT is pressed, we use a software timer to check whether it was recently tapped. If so, this is a double tap (or possibly a triple tap or more). Two taps are considered a double tap if the presses are within TAPPING_TERM (200 ms by default).

For Mouse Turbo Click, I used double tapping to lock Turbo Click. Its implementation follows the code above.

When another key is held

Besides held modifiers, an action can be conditionally triggered based on another key being held.

For instance with the following, the J and K keys become PgDn and PgUp while Backspace is held. The Backspace key continues to function as usual without any added delay, making it an interesting alternative to combos and layer-tap keys.

bool process_record_user(uint16_t keycode, keyrecord_t* record) {
  static bool bspc_is_held = false;

  switch (keycode) {
    case KC_BSPC:
      bspc_is_held = record->event.pressed;
      break;

    case KC_J: {  // Backspace + J = PgDn.
      static uint8_t registered_key = KC_NO;
      if (record->event.pressed) {
        registered_key = (bspc_is_held) ? KC_PGDN : KC_J;
        register_code(registered_key);
      } else {
        unregister_code(registered_key);
      }
    } return false;

    case KC_K: {  // Backspace + K = PgUp.
      static uint8_t registered_key = KC_NO;
      if (record->event.pressed) {
        registered_key = (bspc_is_held) ? KC_PGUP : KC_K;
        register_code(registered_key);
      } else {
        unregister_code(registered_key);
      }
    } return false;
  }

  return true;
} 

Based on previously typed keys

Some effects depend on previously typed keys. For example, an alternative to Leader Key: when a special pattern “qem” is typed, automatically send backspaces to remove it followed by keys to type your email address. Or as I did in Autocorrection, when a typo is typed, automatically send keys to correct it.

There are some technicalities in identifying “typed” keys properly:

  1. Not all key presses type text. For instance, pressing the Left Shift key (KC_LSFT) generates a press event, but it does not on its own type text. Similarly, a mod-tap or layer-tap key types no text when it is held, but it might when it is tapped.

  2. Navigation keys (like arrows) and keys pressed with mods other than shift (hotkeys like Ctrl+W) may move the cursor or change the application state. When this happens, it is best to forget whatever keys had been typed up to that point.

  3. Similarly, the mouse may move the cursor or change application state. Unfortunately, unless the user uses Mouse Keys, there is no practical way for QMK to know what the mouse does.

For the first two points, we can check the keycode for what kind of key it is and get_mods() for non-shift modifiers. For the third point, a mitigation is to use a timer: if no keys are typed within TIMEOUT_MS, the buffer of typed keys is cleared.

The following implements a sliding buffer of the last 8 typed keys.

#include <string.h> 

#define TIMEOUT_MS 5000  // Timeout in milliseconds.
#define RECENT_SIZE 8    // Number of keys in `recent` buffer.

static uint16_t recent[RECENT_SIZE] = {KC_NO};
static uint16_t deadline = 0;

static void clear_recent_keys(void) {
  memset(recent, 0, sizeof(recent));  // Set all zeros (KC_NO).
}

// Handles one event. Returns true if the key was appended to `recent`.
static bool update_recent_keys(uint16_t keycode, keyrecord_t* record) {
  if (!record->event.pressed) { return false; }

  if (((get_mods() | get_oneshot_mods()) & ~MOD_MASK_SHIFT) != 0) {
    clear_recent_keys();  // Avoid interfering with hotkeys.
    return false;
  }

  // Handle tap-hold keys.
  switch (keycode) {
    case QK_MOD_TAP ... QK_MOD_TAP_MAX:
    case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
      if (record->tap.count == 0) { return false; }
      keycode &= 0xff;  // Get tapping keycode.
  }

  switch (keycode) {
    case KC_A ... KC_SLASH:  // These keys type letters, digits, symbols.
      break;

    case KC_LSFT:  // These keys don't type anything on their own.
    case KC_RSFT:
    case QK_ONE_SHOT_MOD ... QK_ONE_SHOT_MOD_MAX:
      return false;

    default:  // Avoid acting otherwise, particularly on navigation keys.
      clear_recent_keys();
      return false;
  }
    
  // Slide the buffer left by one element.
  memmove(recent, recent + 1, (RECENT_SIZE - 1) * sizeof(*recent));

  recent[RECENT_SIZE - 1] = keycode;
  deadline = record->event.time + TIMEOUT_MS;
  return true;
}

void matrix_scan_user(void) {
  if (recent[RECENT_SIZE - 1] && timer_expired(timer_read(), deadline)) {
    clear_recent_keys();  // Timed out; clear the buffer.
  }
}

bool process_record_user(uint16_t keycode, keyrecord_t* record) {
  if (update_recent_keys(keycode, record)) {
    // Continued below...

On each event, update_recent_keys() is called to add the key to the buffer.

The code above is long as it is, yet there is more that can be done in the key handling logic such as ignoring layer switch keys, handling shifted keycodes, and implementing Backspace to remove the last key from the buffer. See the Autocorrection source code for a thorough implementation. For the buffer itself, there are of course other ways it could be done. Particularly, while general keycodes are uint16_t values, you could choose to store them as uint8_t if the logic that follows is limited to basic keycodes.

From there, we can implement the “qem” email address example described above:

bool process_record_user(uint16_t keycode, keyrecord_t* record) {
  if (update_recent_keys(keycode, record)) {
    // Expand "qem" to my email address.
    if (recent[RECENT_SIZE - 3] == KC_Q &&
        recent[RECENT_SIZE - 2] == KC_E &&
        recent[RECENT_SIZE - 1] == KC_M) {
      SEND_STRING(SS_TAP(X_BSPC) SS_TAP(X_BSPC) "myname@email.com");
      return false;
    }
    // Expand "qph" to my phone number.
    if (recent[RECENT_SIZE - 3] == KC_Q &&
        recent[RECENT_SIZE - 2] == KC_P &&
        recent[RECENT_SIZE - 1] == KC_H) {
      SEND_STRING(SS_TAP(X_BSPC) SS_TAP(X_BSPC) "123-546-7890");
      return false;
    }
  }

  return true;
}

Another application is “adaptive keys” to type common bigrams more comfortably. For example (assuming QWERTY layout) an adaptive U key where typing iu produces in. For this use, I would set TIMEOUT_MS to something rather low like 250 ms so that only quick typing triggers it.

bool process_record_user(uint16_t keycode, keyrecord_t* record) {
  if (update_recent_keys(keycode, record)) {
    if (recent[RECENT_SIZE - 2] == KC_I &&
        recent[RECENT_SIZE - 1] == KC_U) {
      tap_code(KC_N);  // Type 'n' instead of 'u'.
      return false;
    }
  }

  return true;
}

If you are testing the recent buffer for a dozen or so patterns, it is reasonable to do it as above, checking for one after another. However, for a larger number of patterns, more elaborate data structures and algorithms may be a win. Autocorrection uses a trie data structure to efficiently test for possibly hundreds to low thousands of typos.

When idle for X milliseconds

You might want to turn off a layer, disable RGB, cancel an effect, etc. if the keyboard has gone idle. For instance with Caps Word, I find it useful to turn it off after 5 seconds of inactivity.

This section describes two ways to do this: using the deferred execution API or using software timers. The deferred execution method is easier to use, but has the drawback that it adds a little more to the firmware size to enable the deferred execution feature.

Using the deferred execution API. This method uses the Deferred Execution API. First, add DEFERRED_EXEC_ENABLE = yes in rules.mk. Then keymap.c, set up a callback method to be called.

#define IDLE_TIMEOUT_MS 5000  // Idle timeout in milliseconds.

static uint32_t idle_callback(uint32_t trigger_time, void* cb_arg) {
  // If execution reaches here, the keyboard has gone idle.
  SEND_STRING("Idle!);
  return 0;
}

bool process_record_user(uint16_t keycode, keyrecord_t* record) {
  // On every key event, start or extend the deferred execution to call
  // `idle_callback()` after IDLE_TIMEOUT_MS.
  static deferred_token idle_token = INVALID_DEFERRED_TOKEN;
  if (!extend_deferred_exec(idle_token, IDLE_TIMEOUT_MS)) {
    idle_token = defer_exec(IDLE_TIMEOUT_MS, idle_callback, NULL);
  }

  // Macros...

  return true;
}

The defer_exec() function schedules idle_callback() to run after IDLE_TIMEOUT_MS. It also returns a “token” idle_token that can be used with other APIs to extend or cancel the execution.

Using a software timer. A lower-lever method is to use a software timer within the matrix_scan_user function as follows.

#define IDLE_TIMEOUT_MS 5000  // Idle timeout in milliseconds.

static uint16_t idle_timer = 0;

void matrix_scan_user(void) {
  if (idle_timer && timer_expired(timer_read(), idle_timer)) {
    // If execution reaches here, the keyboard has gone idle.
    SEND_STRING("Idle!);
    idle_timer = 0;
  }
}

bool process_record_user(uint16_t keycode, keyrecord_t* record) {
  // On every key event, set idle_timer to expire after IDLE_TIMEOUT_MS.
  // We use idle_timer == 0 to indicate that the timer is inactive, so
  // the value is bitwise or'd with 1 to ensure it is nonzero.
  idle_timer = (record->event.time + IDLE_TIMEOUT_MS) | 1;

  // Macros...

  return true;
}

The above uses a 16-bit timer, which wraps around every \(2^{16} = 65536\) milliseconds or about once a minute. The timer_expired() function works for deadlines that are at most half that duration in the future, 32768 milliseconds, so IDLE_TIMEOUT_MS can’t be larger than that. This range is often enough, but if you need longer-term timing, QMK also has a 32-bit flavor of timers with the APIs timer_read32(), timer_elapsed32(), timer_expired32() (code link).

Closing thoughts

There are a variety of ways to trigger an action in QMK, enabling a range of interesting effects. I’m excited in particular about actions based on previously typed keys. Knowing what word was just typed, not just the last key, opens new possibilities.

← More about keyboards