← More about keyboards

Overview

Do you debug QMK code? If so, this might interest you. 🔎 🪲

Keycodes in QMK are represented as 16-bit codes. This post describes keycode_string(), a diagnostic function that formats a given keycode as a human-readable string. I’ve found this tool immensely useful when logging key events for debugging. Example use:

#include "print.h"
#include "features/keycode_string.h"

bool process_record_user(uint16_t keycode, keyrecord_t* record) {
  // Log a line like "press=(0|1) <keycode>".
  dprintf("press=%u %s\n", record->event.pressed, keycode_string(keycode));

  // Macros...
  return true;
}

It’s much nicer to read names like “LT(2,KC_D)” than numerical codes like “0x4207.”

Set up

Prerequisites: While not a strict requirement to use keycode_string(), you’ll probably first want to enable debug logging to have someplace to write keycode strings to.

Step 1: Add to your rules.mk file:

SRC += features/keycode_string.c

Step 2: In your keymap folder, create a features subdirectory and copy keycode_string.h and keycode_string.c there.

Step 3: At the top of keymap.c, add

#include "features/keycode_string.h"

Use case: logging events

Here is a snippet for how I use keycode_string() to log events passing through process_record_user():

// Copyright 2024 Google LLC.
// SPDX-License-Identifier: Apache-2.0

#ifndef NO_DEBUG
#pragma message "dlog_record enabled."
#include "print.h"
#include "features/keycode_string.h"

static void dlog_record(uint16_t keycode, keyrecord_t* record) {
  if (!debug_enable) { return; }
  uint8_t layer = read_source_layers_cache(record->event.key);
  bool is_tap_hold = IS_QK_MOD_TAP(keycode) || IS_QK_LAYER_TAP(keycode);
  xprintf("L%-2u ", layer);  // Log the layer.
  if (IS_COMBOEVENT(record->event)) {  // Combos don't have a position.
    xprintf("combo   ");
  } else {  // Log the "(row,col)" position.
    xprintf("(%2u,%2u) ", record->event.key.row, record->event.key.col);
  }
  xprintf("%-4s %-7s %s\n",  // "(tap|hold) (press|release) <keycode>".
      is_tap_hold ? (record->tap.count ? "tap" : "hold") : "",
      record->event.pressed ? "press" : "release",
      keycode_string(keycode));
}
#else
#pragma message "dlog_record disabled."
#define dlog_record(keycode, record)
#endif  // NO_DEBUG

bool process_record_user(uint16_t keycode, keyrecord_t* record) {
  dlog_record(keycode, record);

  // ...
  return true;
}

For each event, dlog_record() logs its layer, (row,col) matrix position, whether it is pressed vs. released, whether it is tapped vs. held in the case of tap-hold keys, and last but not least, the stringified keycode.

Example log output:

L0  ( 1, 3) hold press   LT(1,KC_T)
L1  ( 6, 4)      press   KC_SLCN
L1  ( 6, 4)      release KC_SLCN
L0  ( 1, 3) hold release LT(1,KC_T)

In words: “While LT(1,KC_T) on layer 0 is held, KC_SCLN is tapped on layer 1.” (Consider how knowing the keycodes helps understand the log!)

Modify dlog_record() for your purposes. You might of course log other information depending on what your are debugging.

How to use keycode_string()

Given a uint16_t keycode, convert it to a string like this:

char* key_name = keycode_string(keycode);

The stringified keycode can then be written to console output with something like

dprintf("keycode=%s\n", key_name);

Or alternatively, perhaps, send it as if it were typed with send_string(key_name) or written on OLED display with oled_write(key_name, false).

Result lifetime

Beware that the returned char* string should be used right away. The string memory is reused and will be overwritten by the next call to keycode_string().

For instance, this line attempting to log two keycodes will incorrectly produce one of keycodes twice, whichever argument evaluates second (side note: in C/C++, order of evaluation of function arguments is unspecified):

// DON'T DO THIS.
dprintf("kc1=%s kc2=%s\n", keycode_string(KC_A), keycode_string(KC_B));
// Logs: either "kc1=KC_A kc2=KC_A" or "kc1=KC_B kc2=KC_B".

Instead, format and print the keycodes one at a time:

dprintf("kc1=%s ", keycode_string(KC_A));
dprintf("kc2=%s\n", keycode_string(KC_B));
// Logs: "kc1=KC_A kc2=KC_B".

Conserving firmware space

For my keymap, adding keycode_string() increases the firmware size by about 1600 bytes. This is not nothing but doable even on limited AVR processors, especially if enabled only while debugging.

If size is a concern, conditionally compile uses of keycode_string() as

// Log `keycode`, but only if debugging is enabled.
#ifndef NO_DEBUG
char* key_name = keycode_string(keycode);
dprintf("press=%u %s\n", record->event.pressed, key_name);
#endif  // NO_DEBUG

This makes it quick to disable keycode_string() in the build, simply by adding #define NO_DEBUG in config.h. Provided no keycode_string() call is compiled and LTO is enabled, keycode_string() will add nothing to the firmware size. Or more concisely where possible, use keycode_string() directly as an arg to dprintf():

// Log `keycode`, but only if debugging is enabled.
dprintf("press=%u %s\n", record->event.pressed, keycode_string(keycode));

This has the same effect as guarding with #ifndef NO_DEBUG. When NO_DEBUG is defined, the preprocessor removes uses of dprintf() along with its args.

Recognized keycodes

Many common QMK keycodes are understood out of the box by keycode_string(), but not all. Recognized keycodes include:

As a fallback, unrecognized keycodes are printed numerically as hex values like “0x1ABC.” In a pinch, you may look up a numerical code in quantum/keycodes.h to determine its meaning.

Keycodes involving mods like OSM, LM, MT are fully supported only where a single mod is applied. For keys applying a combination of mods like OSM(MOD_LSFT | MOD_LCTL) or LM(1, MOD_HYPR), the mods parameter is printed as a hex value.

Defining names for additional keycodes

Optionally, define custom_keycode_names in keymap.c to supply names for additional keycodes or override how any keycode is formatted. For example, supposing keymap.c defines MYMACRO1 and MYMACRO2 as custom keycodes:

const keycode_string_name_t custom_keycode_names[] = {
  // keycode, name.
  {MYMACRO1, "MYMACRO1"},
  {MYMACRO2, "MYMACRO2"},
  {KC_EXLM, "KC_EXLM"},
  {0, NULL},  // End of table sentinel.
};

The above defines names for MYMACRO1 and MYMACRO2 and overrides KC_EXLM to format it as "KC_EXLM" instead of the default "S(KC_1)".

Notes:

Parsing QMK keycodes

Every key in a QMK keymap has a keycode to represent what it does, stored as 16-bit (uint16_t) values. Much of QMK’s functionality is accessible through keycodes alone, there is a lot packed into them. How they are encoded depends on what parameters are needed to represent the key, so this varies a lot by different QMK features.

⚠  Warning

Avoid hardcoded numerical codes or twiddling bits directly, use QMK’s APIs instead. Keycode encoding is internal to QMK and may change between versions.

For organization, all keys of a particular kind use a contiguous range of codes. This structure helps to determine what kind of key is being handled. Use the range helpers to classify a keycode. See the bottom of quantum/keycodes.h for for the full list:

if (IS_QK_BASIC(keycode)) { ... }      // Basic keycode.
if (IS_QK_MOD_TAP(keycode)) { ... }    // Mod-tap key.
if (IS_QK_MOMENTARY(keycode)) { ... }  // MO layer switch.
if (IS_QK_UNICODE(keycode)) { ... }    // UC Unicode key.

Or equivalently, use the corresponding range constants in case ranges:

switch (keycode) {
  case QK_BASIC ... QK_BASIC_MAX:         // Basic keycode.
  case QK_MOD_TAP ... QK_MOD_TAP_MAX:     // Mod-tap key.
  case QK_MOMENTARY ... QK_MOMENTARY_MAX: // MO layer switch.
  case QK_UNICODE ... QK_UNICODE_MAX:     // UC Unicode key.
}

A notable range are the basic keycodes, codes in the 8-bit range 0–255. The basic keycodes coincide with many of those in the HID Keyboard/Keypad Usage Page (0x07).

QMK defines getters to unpack the parameters of compound keycodes. For instance:

switch (keycode) {
  case QK_MOMENTARY ... QK_MOMENTARY_MAX: {  // MO(layer).
    uint8_t layer = QK_MOMENTARY_GET_LAYER(keycode);
    // Do something with `layer`...
  } break;

  case QK_LAYER_TAP ... QK_LAYER_TAP_MAX: {  // LT(layer, kc).
    uint8_t layer = QK_LAYER_TAP_GET_LAYER(keycode);
    uint8_t tapping_keycode = QK_LAYER_TAP_GET_TAP_KEYCODE(keycode);
    // Do something with `layer` and `tapping_keycode`...
  } break;
}

Code pointers: Definitions for all the above can be found in

Further reading

← More about keyboards