QMK macros 3: advanced effects
Pascal Getreuer, 2023-12-17
Overview
This post is third 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
- QMK macros 3: advanced effects ← this post
A fantastic feature of QMK is that users have the freedom to insert their own C code to define custom keymap behaviors. This enables an immense range of possibilities.
The examples in this post are more involved in use of QMK APIs and the C language, so prior familiarity with these is helpful. I’ve tried to present code snippets in such a way that it’s hopefully yet doable to incorporate them into your keymap without having to sift through all the details. If you have trouble, please read QMK macros 1 first. You may also find helpful info in What is this weird C syntax.
License
Code snippets in this post are shared under Apache 2 license.
Copyright 2023 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.
Shift + Backspace = Delete
This macro eliminates the need for a dedicated key for (forward) Delete. The code below implements the following behavior:
Keys | Sends |
---|---|
Backspace | Backspace as usual |
Shift + Backspace | Delete |
Both shift keys + Backspace | Shift + Delete |
Being able to send Shift + Delete is handy since this is a hotkey in some programs. Thanks to @precondition for this idea.
bool process_record_user(uint16_t keycode, keyrecord_t* record) {
switch (keycode) {
case KC_BSPC: {
static uint16_t registered_key = KC_NO;
if (record->event.pressed) { // On key press.
const uint8_t mods = get_mods();
#ifndef NO_ACTION_ONESHOT
uint8_t shift_mods = (mods | get_oneshot_mods()) & MOD_MASK_SHIFT;
#else
uint8_t shift_mods = mods & MOD_MASK_SHIFT;
#endif // NO_ACTION_ONESHOT
if (shift_mods) { // At least one shift key is held.
= KC_DEL;
registered_key // If one shift is held, clear it from the mods. But if both
// shifts are held, leave as is to send Shift + Del.
if (shift_mods != MOD_MASK_SHIFT) {
#ifndef NO_ACTION_ONESHOT
(MOD_MASK_SHIFT);
del_oneshot_mods#endif // NO_ACTION_ONESHOT
(MOD_MASK_SHIFT);
unregister_mods}
} else {
= KC_BSPC;
registered_key }
(registered_key);
register_code(mods);
set_mods} else { // On key release.
(registered_key);
unregister_code}
} return false;
// Other macros...
}
return true;
}
Prefixing layer
Some programs, like tmux and screen, make extensive use of a prefix key to indicate command sequences, such as “Ctrl+a, n” to switch to the next window. Such commands are easily cumbersome, especially if repeated, e.g. “Ctrl+a, n, Ctrl+a, n, Ctrl+a, n” to navigate across several windows.
The following implements a special prefixing layer such that, while active, a prefix key is automatically sent before every key press:
Create a layer
PREFIX_C_A
of all transparent keys.Define (or add to)
process_record_user()
asbool process_record_user(uint16_t keycode, keyrecord_t* record) { if (IS_LAYER_ON(PREFIX_C_A) && record->event.pressed) { (C(KC_A)); // Tap Ctrl+A. tap_code16} // Other macros... return true; }
Finally, a method to activate the prefixing layer is needed. For
instance, replace the Tab key with the layer-tap
LT(PREFIX_C_A, KC_TAB)
so that the layer is activated while
Tab is held. Then, a sequence like “Ctrl+a, n, Ctrl+a, n, Ctrl+a, n” is
expediently performed as
- hold Tab down,
- tap N three times,
- release Tab.
Random emojis
Enable a Unicode input method for this macro, either Basic Unicode, Unicode Map, or UCIS; see Unicode Support. See also Typing non-English letters.
When pseudorandom values are needed, you could use the C standard
library rand()
to generate pseudorandom values. However,
this function is a bit expensive. On my set up, using
rand()
adds about 400 bytes to the firmware size.
Here is a cheap substitute that costs about 50 bytes. It is a 16-bit
multiplicative
congruential generator, a dirt simple method whose output yet
appears random on casual inspection. Of course, don’t use this for
generating passwords or other cryptographic purposes. The magic number
36563
could be reasonably replaced with other any number
having high
multiplicative order modulo 216:
// Generates a pseudorandom value in 0-255.
static uint8_t simple_rand(void) {
static uint16_t random = 1;
*= UINT16_C(36563);
random return (uint8_t)(random >> 8);
}
The first few calls of this function return 142, 193, 17, 38, 206.
The pseudorandom sequence repeats with a period of 214 =
16384. To sample a number x
in the closed range [0,
max
], use
x = ((max + 1) * simple_rand()) >> 8
.
The following uses simple_rand()
to pseudorandomly pick
an emoji from an array and prints it, plus some logic to avoid picking
the same emoji twice in a row. Define a custom keycode
HAPPY
and use it in your keymap (for further explanation,
see QMK macros 1). Then in
process_record_user()
, add
switch (keycode) {
case HAPPY: // Types a happy random emoji.
if (record->event.pressed) {
static const char* emojis[] = {"🤩", "🌞", "👾", "👍", "😁"};
const int NUM_EMOJIS = sizeof(emojis) / sizeof(*emojis);
// Pseudorandomly pick an index between 0 and NUM_EMOJIS - 2.
uint8_t index = ((NUM_EMOJIS - 1) * simple_rand()) >> 8;
// Don't pick the same emoji twice in a row.
static uint8_t last_index = 0;
if (index >= last_index) { ++index; }
= index;
last_index
// Produce the emoji.
(emojis[index]);
send_unicode_string}
return false;
// Other macros...
}
See also this multi-codepoint emoji example.
Quopostrokey
The “Quopostrokey” is a key whose function depends on the previous
key. If the previous key typed was a letter, then the Quopostrokey types
a single quote '
as usual. Otherwise, it produces a pair of
double quotes ""
and taps ← to put the cursor in
between. Thanks to Reddit user u/Keybug for this idea.
Define a custom keycode QUOP
and add the following in
keymap.c:
static bool process_quopostrokey(uint16_t keycode, keyrecord_t* record) {
static bool within_word = false;
if (keycode == QUOP) {
if (record->event.pressed) {
if (within_word) {
(KC_QUOT);
tap_code} else {
("\"\"" SS_TAP(X_LEFT));
SEND_STRING}
}
return false;
}
switch (keycode) { // Unpack tapping keycode for tap-hold keys.
#ifndef NO_ACTION_TAPPING
case QK_MOD_TAP ... QK_MOD_TAP_MAX:
if (record->tap.count == 0) { return true; }
= QK_MOD_TAP_GET_TAP_KEYCODE(keycode);
keycode break;
#ifndef NO_ACTION_LAYER
case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
if (record->tap.count == 0) { return true; }
= QK_LAYER_TAP_GET_TAP_KEYCODE(keycode);
keycode break;
#endif // NO_ACTION_LAYER
#endif // NO_ACTION_TAPPING
}
// Determine whether the key is a letter.
switch (keycode) {
case KC_A ... KC_Z:
= true;
within_word break;
default:
= false;
within_word }
return true;
}
bool process_record_user(uint16_t keycode, keyrecord_t* record) {
if (!process_quopostrokey(keycode, record)) { return false; }
// ...
return true;
}
See also triggering effects based on previously typed keys.
Timing effects
Enable Deferred Execution for macros in this section by adding in rules.mk:
DEFERRED_EXEC_ENABLE = yes
These macros could be implemented without Deferred Execution by instead using software timers and the matrix_scan_user function. See this example for comparison of these two approaches for implementing timing effects.
Exponential key repeating
Conventionally, key repeating produces repeats of the held key at a fixed rate. The following snippet customizes the Backspace key to repeat exponentially. The longer it is held, the faster it deletes, up to a limit. Thanks to Reddit user u/BubkisBobby for this idea.
case KC_BSPC: { // Backspace with exponential repeating.
// Initial delay before the first repeat.
static const uint8_t INIT_DELAY_MS = 250;
// This array customizes the rate at which the Backspace key
// repeats. The delay after the ith repeat is REP_DELAY_MS[i].
// Values must be between 1 and 255.
static const uint8_t REP_DELAY_MS[] PROGMEM = {
99, 79, 65, 57, 49, 43, 40, 35, 33, 30, 28, 26, 25, 23, 22, 20,
20, 19, 18, 17, 16, 15, 15, 14, 14, 13, 13, 12, 12, 11, 11, 10};
static deferred_token token = INVALID_DEFERRED_TOKEN;
static uint8_t rep_count = 0;
if (!record->event.pressed) { // Backspace released: stop repeating.
(token);
cancel_deferred_exec= INVALID_DEFERRED_TOKEN;
token } else if (!token) { // Backspace pressed: start repeating.
(KC_BSPC); // Initial tap of Backspace key.
tap_code= 0;
rep_count
uint32_t bspc_callback(uint32_t trigger_time, void* cb_arg) {
(KC_BSPC);
tap_codeif (rep_count < sizeof(REP_DELAY_MS)) { ++rep_count; }
return pgm_read_byte(REP_DELAY_MS - 1 + rep_count);
}
= defer_exec(INIT_DELAY_MS, bspc_callback, NULL);
token }
} return false; // Skip normal handling.
How it works:
On press,
KC_BSPC
is tapped andbspc_callback()
is scheduled through deferred execution to run afterINIT_DELAY_MS
.When
bspc_callback()
runs, it tapsKC_BSPC
and schedules to run again with progressively shorter delays. These delays are defined in the arrayREP_DELAY_MS
. Once 10 ms is reached, repetition continues at that period. The variablerep_count
keeps track where it is in this sequence.When Backspace is released, the callback is canceled and repeating stops.
The “acceleration curve” of the repeating behavior can be tuned by
changing the REP_DELAY_MS
array. Its elements are values
between 1 and 255, with smaller values for faster repeating.
Technical note: the code above defines
bspc_callback()
within process_record_user()
as a nested
function, a non-standard GNU extension. This is convenient to write
the callback next to the defer_exec()
line that schedules
it. If you prefer instead to stick with standard C, move the definitions
of the callback and rep_count
above
process_record_user()
at global scope.
Repeating a pattern
Here is a macro which types an ongoing pattern of
“~=~=~=~=~=~
…” when held:
case WAVE: { // Types ~=~=~=~=~=~...
static deferred_token token = INVALID_DEFERRED_TOKEN;
static uint8_t phase = 0;
if (!record->event.pressed) { // On release.
(token);
cancel_deferred_exec= INVALID_DEFERRED_TOKEN;
token // Ensure the pattern always ends on a "~".
if ((phase & 1) == 0) { tap_code16(KC_TILD); }
= 0;
phase } else if (!token) { // On press.
uint32_t wave_callback(uint32_t trigger_time, void* cb_arg) {
((++phase & 1) ? KC_TILD : KC_EQL);
tap_code16return 16; // Call the callback every 16 ms.
}
= defer_exec(1, wave_callback, NULL);
token }
} return false;
When WAVE
is pressed, wave_callback()
is
scheduled through deferred execution to run every 16 ms. The callback
alternates between typing ~
and =
based on the
phase
variable. When WAVE
is released, the
deferred execution is canceled to end the pattern.
Other patterns can be made similarly through tweaking the logic in
wave_callback()
.
A mouse jiggler
This section implements a mouse jiggler. When a JIGGLE
key is pressed, the jiggler spins the mouse in a circle until another
key is pressed.
Additionally enable Mouse Keys for this macro by adding in rules.mk:
MOUSE_ENABLE = yes
DEFERRED_EXEC_ENABLE = yes
In keymap.c, define a custom keycode JIGGLE
and use it
in your keymap. Then, define or add to your
process_record_user()
function as follows:
bool process_record_user(uint16_t keycode, keyrecord_t* record) {
if (record->event.pressed) {
static deferred_token token = INVALID_DEFERRED_TOKEN;
static report_mouse_t report = {0};
if (token) {
// If jiggler is currently running, stop when any key is pressed.
(token);
cancel_deferred_exec= INVALID_DEFERRED_TOKEN;
token = (report_mouse_t){}; // Clear the mouse.
report (&report);
host_mouse_send} else if (keycode == JIGGLE) {
uint32_t jiggler_callback(uint32_t trigger_time, void* cb_arg) {
// Deltas to move in a circle of radius 20 pixels over 32 frames.
static const int8_t deltas[32] = {
0, -1, -2, -2, -3, -3, -4, -4, -4, -4, -3, -3, -2, -2, -1, 0,
0, 1, 2, 2, 3, 3, 4, 4, 4, 4, 3, 3, 2, 2, 1, 0};
static uint8_t phase = 0;
// Get x delta from table and y delta by rotating a quarter cycle.
.x = deltas[phase];
report.y = deltas[(phase + 8) & 31];
report= (phase + 1) & 31;
phase (&report);
host_mouse_sendreturn 16; // Call the callback every 16 ms.
}
= defer_exec(1, jiggler_callback, NULL); // Schedule callback.
token }
}
// Other macros...
return true;
}
The jiggler_callback()
function executes every 16 ms,
fast enough to animate the mouse smoothly at 60 frames per second, using
the deferred
execution API. While we could use QMK’s mouse keys to
move the mouse, the way it does so is complicated and configuration
dependent. Instead, we construct and send mouse reports directly.
The deltas
table stores a sequence of x deltas to move
the mouse in a circle. We get the y deltas by rotating the table by a
quarter cycle (leveraging trigonometric identity \(\cos(x) = \sin(x + \pi/2)\)). Using the
phase
variable, the callback iterates through this table,
moving in a circle of radius 20 pixels at 32 frames per cycle.
The table was generated in Python with:
import numpy as np
= 20 # Circle radius in pixels.
radius = 32 # Frames per cycle.
n = radius * np.cos((2 * np.pi / n) * np.arange(n + 1))
x = np.round(np.diff(x))
deltas print(deltas.astype(int))
See also Orbital Mouse for more customized control of the mouse.
Blinking LEDs
Some boards have LEDs, which can be turned on and off by setting a
pin high or low with writePin(pin, state)
, see also LED Indicators.
Besides simply turning the LED on or off, an interesting possibility is
to blink the LEDs. Blinking makes important states more noticeable and
enables communication of more information with each LED.
Setting up blinking LEDs
Add the following in keymap.c:
// Number of LEDs on the keyboard.
#define NUM_LEDS 3
// Period for LED_BLINK_FAST blinking. Smaller value implies faster.
#define LED_BLINK_FAST_PERIOD_MS 300
// Possible LED states.
enum { LED_OFF = 0, LED_ON = 1, LED_BLINK_SLOW = 2, LED_BLINK_FAST = 3 };
static uint8_t led_blink_state[NUM_LEDS] = {0};
void keyboard_post_init_user(void) {
uint32_t led_blink_callback(uint32_t trigger_time, void* cb_arg) {
static const uint8_t pattern[4] = {0x00, 0xff, 0x0f, 0xaa};
static uint8_t phase = 0;
= (phase + 1) % 8;
phase
uint8_t bit = 1 << phase;
// Adjust according to keyboard. See notes below.
(B0, (pattern[led_blink_state[0]] & bit) != 0);
writePin(B1, (pattern[led_blink_state[1]] & bit) != 0);
writePin(B2, (pattern[led_blink_state[2]] & bit) != 0);
writePin
return LED_BLINK_FAST_PERIOD_MS / 2;
}
(1, led_blink_callback, NULL);
defer_exec}
This defines a callback at startup running every 150 ms to animate
the LEDs. On each call, phase
is incremented. The value of
led_blink_state
determines which entry is read from the
pattern
array, and that entry is read one bit at a time to
define the animation.
Notes:
Define
NUM_LEDS
as the number of LEDs on the board, and changeB0
,B1
,B2
to their pins. The above assumes setting the pin high turns the LED on. If that’s backwards, negate the second arg inwritePin()
like(B0, (pattern[led_blink_state[0]] & bit) == 0); writePin
For the Ergodox EZ keyboard: Replace
writePin
withergodox_right_led_on(
n)
andergodox_right_led_off(
n)
, where n = 1, 2, 3 is the LED index:// Set the Ergodox EZ's LEDs. for (uint8_t i = 0; i < 3; ++i) { if ((pattern[led_blink_state[i]] & bit) != 0) { (i + 1); ergodox_right_led_on} else { (i + 1); ergodox_right_led_off} }
For the Moonlander keyboard: Make the following changes. Change
NUM_LEDS
to 6:#define NUM_LEDS 6
Add the following at the top of
keyboard_post_init_user()
:// I want to control the Moonlander's LEDs myself. .led_level = false; keyboard_config
Finally, replace
writePin
withML_LED_
n, where n between 1 and 6 is the LED index:// Set the Moonlander's LEDs. ((pattern[led_blink_state[0]] & bit) != 0); ML_LED_1((pattern[led_blink_state[1]] & bit) != 0); ML_LED_2((pattern[led_blink_state[2]] & bit) != 0); ML_LED_3((pattern[led_blink_state[3]] & bit) != 0); ML_LED_4((pattern[led_blink_state[4]] & bit) != 0); ML_LED_5((pattern[led_blink_state[5]] & bit) != 0); ML_LED_6
Usage to display keyboard state
With the above in place, led_blink_state[i]
may be
changed at any time to set the animation state of the ith LED to one
of
State | Description |
---|---|
LED_OFF |
LED is off |
LED_ON |
LED is on |
LED_BLINK_SLOW |
LED blinks slowly |
LED_BLINK_FAST |
LED blinks quickly |
Here are examples of a couple possible use cases.
Layer indicator. Supposing the keyboard has layers
BASE
, NAV
, ADJUST
(and possibly
others), the following sets the first LED to indicate the current
highest layer:
static uint8_t get_layer_indicator(void) {
switch (get_highest_layer(state)) {
case BASE: return LED_OFF; // LED off for BASE layer.
case NAV: return LED_BLINK_SLOW; // Blink slowly for NAV layer.
case ADJUST: return LED_BLINK_FAST; // Blink quickly for ADJUST layer.
default: return LED_ON; // LED on for any other layer.
}
}
(layer_state_t state) {
layer_state_t layer_state_set_user[0] = get_layer_indicator();
led_blink_statereturn state;
}
Caps Lock / Caps Word indicator. The following sets
the second LED to LED_ON
when Caps Lock is on,
LED_BLINK_FAST
when Caps Word is on, and
LED_OFF
otherwise.
static bool is_caps_lock_on = false;
static void update_caps_indicator(void) {
if (is_caps_lock_on) {
[1] = LED_ON;
led_blink_state} else if (is_caps_word_on()) {
[1] = LED_BLINK_FAST;
led_blink_state} else {
[1] = LED_OFF;
led_blink_state}
}
bool led_update_user(led_t led_state) {
= led_state.caps_lock;
is_caps_lock_on ();
update_caps_indicatorreturn true;
}
void caps_word_set_user(bool active) {
();
update_caps_indicator}
Acknowledgements
Thanks to @precondition, @drashna and Reddit users u/Keybug, u/BubkisBobby, u/Gattomarino for ideas and feedback on these macros.
Other cool things
We’ve seen a lot of interesting QMK behaviors here and in the previous two posts. Looking for more? Check out the following:
As macros get more elaborate, it is a good idea to encapsulate them as feature libraries, especially if you want to share them with others or contribute them to QMK. See Developing QMK features.
The configurable Alternate Repeat Key is a great way to pack more functionality in each key. I use this to implement the “magic key” in Magic Sturdy.
“Case modes” or “smart cases” is an idea of extending Caps Word to facilitate other conventional identifier patterns like
camelCase
,snake_case
,kebab-case
, andpath/to/file
. Several implementations:Inspired by stenotype, precondition’s keymap has a “steno-lite” system to type common n-grams using a suite of combos. Or for full-fledged stenotype, see QMK’s stenography feature.
QMK’s Raw HID feature can send information from the computer to the keyboard. It no doubt has untapped potential for useful effects. Imagine a keymap that adapts to the current focused window or the OS input method selection.