← More about keyboards

Overview

This post is third in a series on interesting effects in QMK:

  1. QMK macros 1: intro and assortment of practical examples ← start here
  2. QMK macros 2: triggers, reacting to interesting events
  3. 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.
          registered_key = KC_DEL;
          // 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
            del_oneshot_mods(MOD_MASK_SHIFT);
#endif  // NO_ACTION_ONESHOT
            unregister_mods(MOD_MASK_SHIFT);
          }
        } else {
          registered_key = KC_BSPC;
        }

        register_code(registered_key);
        set_mods(mods);
      } else {  // On key release.
        unregister_code(registered_key);
      }
    } 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:

  1. Create a layer PREFIX_C_A of all transparent keys.

  2. Define (or add to) process_record_user() as

    bool process_record_user(uint16_t keycode, keyrecord_t* record) {
      if (IS_LAYER_ON(PREFIX_C_A) && record->event.pressed) {
        tap_code16(C(KC_A));  // Tap Ctrl+A.
      }
    
      // 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

  1. hold Tab down,
  2. tap N three times,
  3. 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;
  random *= UINT16_C(36563);
  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; }
      last_index = index;

      // Produce the emoji.
      send_unicode_string(emojis[index]);
    }
    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) {
        tap_code(KC_QUOT);
      } else {
        SEND_STRING("\"\"" SS_TAP(X_LEFT));
      }
    }
    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; }
      keycode = QK_MOD_TAP_GET_TAP_KEYCODE(keycode);
      break;
#ifndef NO_ACTION_LAYER
    case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
      if (record->tap.count == 0) { return true; }
      keycode = QK_LAYER_TAP_GET_TAP_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:
      within_word = true;
      break;

    default:
      within_word = false;
  }

  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.
    cancel_deferred_exec(token);
    token = INVALID_DEFERRED_TOKEN;
  } else if (!token) {  // Backspace pressed: start repeating.
    tap_code(KC_BSPC);  // Initial tap of Backspace key.
    rep_count = 0;

    uint32_t bspc_callback(uint32_t trigger_time, void* cb_arg) {
      tap_code(KC_BSPC);
      if (rep_count < sizeof(REP_DELAY_MS)) { ++rep_count; }
      return pgm_read_byte(REP_DELAY_MS - 1 + rep_count); 
    }

    token = defer_exec(INIT_DELAY_MS, bspc_callback, NULL); 
  }
} return false;  // Skip normal handling.

How it works:

  1. On press, KC_BSPC is tapped and bspc_callback() is scheduled through deferred execution to run after INIT_DELAY_MS.

  2. When bspc_callback() runs, it taps KC_BSPC and schedules to run again with progressively shorter delays. These delays are defined in the array REP_DELAY_MS. Once 10 ms is reached, repetition continues at that period. The variable rep_count keeps track where it is in this sequence.

  3. 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.
    cancel_deferred_exec(token);
    token = INVALID_DEFERRED_TOKEN;
    // Ensure the pattern always ends on a "~".
    if ((phase & 1) == 0) { tap_code16(KC_TILD); }
    phase = 0;
  } else if (!token) {  // On press.

    uint32_t wave_callback(uint32_t trigger_time, void* cb_arg) {
      tap_code16((++phase & 1) ? KC_TILD : KC_EQL);
      return 16;  // Call the callback every 16 ms.
    }

    token = defer_exec(1, wave_callback, NULL);
  }
} 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.

Active mouse jiggler.

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.
      cancel_deferred_exec(token);
      token = INVALID_DEFERRED_TOKEN;
      report = (report_mouse_t){};  // Clear the mouse.
      host_mouse_send(&report);
    } 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.
        report.x = deltas[phase];
        report.y = deltas[(phase + 8) & 31];
        phase = (phase + 1) & 31;
        host_mouse_send(&report);
        return 16;  // Call the callback every 16 ms.
      }

      token = defer_exec(1, jiggler_callback, NULL);  // Schedule callback.
    }
  }

  // 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
radius = 20  # Circle radius in pixels.
n = 32       # Frames per cycle.
x = radius * np.cos((2 * np.pi / n) * np.arange(n + 1))
deltas = np.round(np.diff(x))
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 = (phase + 1) % 8;

    uint8_t bit = 1 << phase;
    // Adjust according to keyboard. See notes below.
    writePin(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);
    
    return LED_BLINK_FAST_PERIOD_MS / 2;
  }

  defer_exec(1, led_blink_callback, NULL);
}

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:

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 layer_state_set_user(layer_state_t state) {
  led_blink_state[0] = get_layer_indicator();
  return 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) {
    led_blink_state[1] = LED_ON;
  } else if (is_caps_word_on()) {
    led_blink_state[1] = LED_BLINK_FAST;
  } else {
    led_blink_state[1] = LED_OFF;
  }
}

bool led_update_user(led_t led_state) {
  is_caps_lock_on = led_state.caps_lock;
  update_caps_indicator();
  return 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:

← More about keyboards