keycode_string(): format keycodes as human-readable strings
Pascal Getreuer, 2024-12-30
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>".
("press=%u %s\n", record->event.pressed, keycode_string(keycode));
dprintf
// 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);
("L%-2u ", layer); // Log the layer.
xprintfif (IS_COMBOEVENT(record->event)) { // Combos don't have a position.
("combo ");
xprintf} else { // Log the "(row,col)" position.
("(%2u,%2u) ", record->event.key.row, record->event.key.col);
xprintf}
("%-4s %-7s %s\n", // "(tap|hold) (press|release) <keycode>".
xprintf? (record->tap.count ? "tap" : "hold") : "",
is_tap_hold ->event.pressed ? "press" : "release",
record(keycode));
keycode_string}
#else
#pragma message "dlog_record disabled."
#define dlog_record(keycode, record)
#endif // NO_DEBUG
bool process_record_user(uint16_t keycode, keyrecord_t* record) {
(keycode, record);
dlog_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
("keycode=%s\n", key_name); dprintf
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.
("kc1=%s kc2=%s\n", keycode_string(KC_A), keycode_string(KC_B));
dprintf// 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:
("kc1=%s ", keycode_string(KC_A));
dprintf("kc2=%s\n", keycode_string(KC_B));
dprintf// 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);
("press=%u %s\n", record->event.pressed, key_name);
dprintf#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.
("press=%u %s\n", record->event.pressed, keycode_string(keycode)); dprintf
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:
- 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
,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)
. - 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, 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:
- The table must end with the sentinel entry
{0, NULL}
. - Names exceeding 16 characters will be truncated.
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