QMK macros 2: triggers, reacting to interesting events
Pascal Getreuer, 2022-05-07 (updated 2023-12-17)
Overview
This post is second in a series on interesting effects in QMK:
- QMK macros 1: intro and assortment of practical examples ← start here
- QMK macros 2: triggers, reacting to interesting events ← this post
- QMK macros 3: advanced effects
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 state) {
layer_state_t layer_state_set_user(B0, get_highest_layer(state) > 0);
writePinreturn 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. See Blinking LEDs for an
elaborated example of LED layer indicators.
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 state) {
layer_state_t layer_state_set_user// 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.
(MAJOR_SONG);
PLAY_SONG} else { // Just exited the ADJUST layer.
(GOODBYE_SONG);
PLAY_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(
* record, uint16_t long_press_keycode) {
keyrecord_tif (record->tap.count == 0) { // Key is being held.
if (record->event.pressed) {
(long_press_keycode);
tap_code16}
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(
* record, uint16_t long_press_keycode) {
keyrecord_tif (record->tap.count == 0) { // Key is being held.
if (record->event.pressed) {
(long_press_keycode);
register_code16} else {
(long_press_keycode);
unregister_code16}
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.
(); // If needed, clear the mods.
clear_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.
(); // If needed, clear the mods.
clear_mods// Do something interesting...
}
= true;
tapped = record->event.time + TAPPING_TERM;
tap_timer } else {
// On an event with any other key, reset the double tap state.
= false;
tapped }
}
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:
= record->event.pressed;
bspc_is_held break;
case KC_J: { // Backspace + J = PgDn.
static uint8_t registered_key = KC_NO;
if (record->event.pressed) {
= (bspc_is_held) ? KC_PGDN : KC_J;
registered_key (registered_key);
register_code} else {
(registered_key);
unregister_code}
} return false;
case KC_K: { // Backspace + K = PgUp.
static uint8_t registered_key = KC_NO;
if (record->event.pressed) {
= (bspc_is_held) ? KC_PGUP : KC_K;
registered_key (registered_key);
register_code} else {
(registered_key);
unregister_code}
} 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 for typo
correction and Sentence Case
for automatic capitalizing the first letter of sentences.
There are some technicalities in identifying “typed” keys properly:
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.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.
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) {
(recent, 0, sizeof(recent)); // Set all zeros (KC_NO).
memset}
// 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) {
(); // Avoid interfering with hotkeys.
clear_recent_keysreturn 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; }
&= 0xff; // Get tapping keycode.
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_keysreturn false;
}
// Slide the buffer left by one element.
(recent, recent + 1, (RECENT_SIZE - 1) * sizeof(*recent));
memmove
[RECENT_SIZE - 1] = keycode;
recent= record->event.time + TIMEOUT_MS;
deadline return true;
}
void matrix_scan_user(void) {
if (recent[RECENT_SIZE - 1] && timer_expired(timer_read(), deadline)) {
(); // Timed out; clear the buffer.
clear_recent_keys}
}
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_SIZE - 2] == KC_E &&
recent[RECENT_SIZE - 1] == KC_M) {
recent(SS_TAP(X_BSPC) SS_TAP(X_BSPC) "myname@email.com");
SEND_STRINGreturn false;
}
// Expand "qph" to my phone number.
if (recent[RECENT_SIZE - 3] == KC_Q &&
[RECENT_SIZE - 2] == KC_P &&
recent[RECENT_SIZE - 1] == KC_H) {
recent(SS_TAP(X_BSPC) SS_TAP(X_BSPC) "123-546-7890");
SEND_STRINGreturn 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_SIZE - 1] == KC_U) {
recent(KC_N); // Type 'n' instead of 'u'.
tap_codereturn 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.
("Idle!);
SEND_STRINGreturn 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)) {
= defer_exec(IDLE_TIMEOUT_MS, idle_callback, NULL);
idle_token }
// 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.
("Idle!);
SEND_STRING= 0;
idle_timer }
}
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.
= (record->event.time + IDLE_TIMEOUT_MS) | 1;
idle_timer
// 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).
See this page for more examples of timing effects.
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.
For yet more macros, check out the next post in this series, QMK macros 3: advanced effects, on timing effects, random emojis, and more.