Keycode String: format keycodes as human-readable strings
Pascal Getreuer, 2024-12-30 (updated 2025-07-11)
🚀 Launched
Keycode String is now a core QMK feature! It was released in QMK on 2025-05-26. Update your QMK set up and see Keycode String.
Overview
Do you debug QMK code? If so, this might interest you. 🔎 🪲
Keycodes in QMK are
represented as 16-bit codes. This post describes
get_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:
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, get_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
Install my community
modules. Then enable module getreuer/keycode_string
in your keymap.json file. Or if keymap.json
does not exist, create it with the following content:
{
  "modules": ["getreuer/keycode_string"]
}While not a strict requirement to use
get_keycode_string(), you’ll probably also want to enable debug
logging to have someplace to write keycode strings to.
Use case: logging events
Here is a snippet for how I use get_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"
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",
      get_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 get_keycode_string()
Given a uint16_t keycode, convert it to a string like
this:
const char* key_name = get_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 get_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",
    get_keycode_string(KC_A), get_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 ", get_keycode_string(KC_A));
dprintf("kc2=%s\n", get_keycode_string(KC_B));
// Logs: "kc1=KC_A kc2=KC_B".Conserving firmware space
For my keymap, adding get_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
get_keycode_string() as
// Log `keycode`, but only if debugging is enabled.
#ifndef NO_DEBUG
const char* key_name = get_keycode_string(keycode);
dprintf("%u %s\n", record->event.pressed, key_name);
#endif  // NO_DEBUGThis makes it quick to disable get_keycode_string() in
the build, simply by adding #define NO_DEBUG in config.h.
Provided no get_keycode_string() call is compiled and LTO
is enabled, get_keycode_string() will add nothing to the
firmware size. Or more concisely where possible, use
get_keycode_string() directly as an arg to
dprintf():
// Log `keycode`, but only if debugging is enabled.
dprintf("%u %s\n", record->event.pressed, get_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
get_keycode_string(), but not all. Recognized keycodes
include:
- Common basic
keycodes, including letters KC_A–KC_Z, digitsKC_0–KC_9, function keysKC_F1–KC_F24, and modifiers likeKC_LSFT.
- Modified basic keycodes, like S(KC_1)(Shift + 1 = !).
- MO,- TO,- TG,- TT,- DF,- PDF,- OSL,- LM(layer,mod),- LT(layer,kc)layer switches.
- One-shot mod
OSM(mod)keycodes.
- Mod-tap
MT(mod, kc)keycodes.
- Tap dance
keycodes TD(i).
- Swap hands keycodes.
- Unicode
UC(codepoint)and Unicode MapUM(i)andUP(i,j)keycodes.
- Keyboard range keycodes QK_KB_*.
- User range (SAFE_RANGE) keycodesQK_USER_*.
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, use KEYCODE_STRING_NAMES_USER in keymap.c to
define names for additional keycodes or override how any keycode is
formatted. For example, supposing keymap.c defines MYMACRO1
and MYMACRO2 as custom keycodes:
KEYCODE_STRING_NAMES_USER(
  KEYCODE_STRING_NAME(MYMACRO1),
  KEYCODE_STRING_NAME(MYMACRO2),
  KEYCODE_STRING_NAME(KC_EXLM),
);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)".
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
- Troubleshooting QMK
- Debugging QMK
- Developing QMK features – developing userspace libraries and contributing to QMK