aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--builddefs/generic_features.mk1
-rw-r--r--docs/faq_debug.md11
-rw-r--r--docs/unit_testing.md28
-rw-r--r--quantum/keycode_string.c564
-rw-r--r--quantum/keycode_string.h134
-rw-r--r--quantum/quantum.h1
-rw-r--r--tests/keycode_string/config.h19
-rw-r--r--tests/keycode_string/test.mk22
-rw-r--r--tests/keycode_string/test_keycode_string.cpp153
9 files changed, 933 insertions, 0 deletions
diff --git a/builddefs/generic_features.mk b/builddefs/generic_features.mk
index f14f440877..015a804d91 100644
--- a/builddefs/generic_features.mk
+++ b/builddefs/generic_features.mk
@@ -34,6 +34,7 @@ GENERIC_FEATURES = \
DYNAMIC_TAPPING_TERM \
GRAVE_ESC \
HAPTIC \
+ KEYCODE_STRING \
KEY_LOCK \
KEY_OVERRIDE \
LAYER_LOCK \
diff --git a/docs/faq_debug.md b/docs/faq_debug.md
index 35a4160e27..269049afb8 100644
--- a/docs/faq_debug.md
+++ b/docs/faq_debug.md
@@ -77,6 +77,17 @@ KL: kc: 172, col: 2, row: 0, pressed: 1, time: 16303, int: 0, count: 0
KL: kc: 172, col: 2, row: 0, pressed: 0, time: 16411, int: 0, count: 0
```
+### Which keycode is this keypress?
+
+Keycodes are logged in the example above as numerical codes, which may be difficult to interpret. For more readable logging, add `KEYCODE_STRING_ENABLE = yes` in your `rules.mk` and use `get_keycode_string(kc)`. For example:
+
+```c
+uprintf("kc: %s\n", get_keycode_string(keycode));
+```
+
+This logs the keycode as a human-readable string like "`LT(2,KC_D)`" rather than a numerical code like "`0x4207`." See the [Keycode String](unit_testing#keycode-string) section of the Unit Testing page for more information.
+
+
### How long did it take to scan for a keypress?
When testing performance issues, it can be useful to know the frequency at which the switch matrix is being scanned. To enable logging for this scenario, add the following code to your keymaps `config.h`
diff --git a/docs/unit_testing.md b/docs/unit_testing.md
index 3e4c914bf1..aec4ec8334 100644
--- a/docs/unit_testing.md
+++ b/docs/unit_testing.md
@@ -58,6 +58,34 @@ It's not yet possible to do a full integration test, where you would compile the
In that model you would emulate the input, and expect a certain output from the emulated keyboard.
+# Keycode String {#keycode-string}
+
+It's much nicer to read keycodes as names like "`LT(2,KC_D)`" than numerical codes like "`0x4207`." To convert keycodes to human-readable strings, add `KEYCODE_STRING_ENABLE = yes` to the `rules.mk` file, then use the `get_keycode_string(kc)` function to convert a given 16-bit keycode to a string.
+
+```c
+const char *key_name = get_keycode_string(keycode);
+dprintf("kc: %s\n", key_name);
+```
+
+The stringified keycode may then be logged to console output with `dprintf()` or elsewhere.
+
+::: warning
+Use the result of `get_keycode_string()` immediately. Subsequent invocations reuse the same static buffer and overwrite the previous contents.
+:::
+
+Many common QMK keycodes are recognized by `get_keycode_string()`, but not all. These include some common basic keycodes, layer switch keycodes, mod-taps, one-shot keycodes, tap dance keycodes, and Unicode keycodes. As a fallback, an unrecognized keycode is written as a hex number.
+
+Optionally, `KEYCODE_STRING_NAMES_USER` may be defined to add names for additional keycodes. For example, supposing keymap.c defines `MYMACRO1` and `MYMACRO2` as custom keycodes, the following adds their names:
+
+```c
+KEYCODE_STRING_NAMES_USER(
+ KEYCODE_STRING_NAME(MYMACRO1),
+ KEYCODE_STRING_NAME(MYMACRO2),
+);
+```
+
+Similarly, `KEYCODE_STRING_NAMES_KB` may be defined to add names at the keyboard level.
+
# Tracing Variables {#tracing-variables}
Sometimes you might wonder why a variable gets changed and where, and this can be quite tricky to track down without having a debugger. It's of course possible to manually add print statements to track it, but you can also enable the variable trace feature. This works for both variables that are changed by the code, and when the variable is changed by some memory corruption.
diff --git a/quantum/keycode_string.c b/quantum/keycode_string.c
new file mode 100644
index 0000000000..18044f2ef6
--- /dev/null
+++ b/quantum/keycode_string.c
@@ -0,0 +1,564 @@
+// Copyright 2024-2025 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.
+
+#include "keycode_string.h"
+
+#include <string.h>
+#include "bitwise.h"
+#include "keycode.h"
+#include "progmem.h"
+#include "quantum_keycodes.h"
+#include "util.h"
+
+typedef int_fast8_t index_t;
+
+// clang-format off
+/** Packs a 7-char keycode name, ignoring the third char, as 3 words. */
+#define KEYCODE_NAME7(c0, c1, unused_c2, c3, c4, c5, c6) \
+ ((uint16_t)c0) | (((uint16_t)c1) << 8), \
+ ((uint16_t)c3) | (((uint16_t)c4) << 8), \
+ ((uint16_t)c5) | (((uint16_t)c6) << 8)
+
+/**
+ * @brief Names of some common keycodes.
+ *
+ * Each (keycode, name) entry is stored flat in 8 bytes in PROGMEM. Names in
+ * this table must be at most 7 chars long and have an underscore '_' for the
+ * third char. This underscore is assumed and not actually stored.
+ *
+ * To save memory, feature-specific key entries are ifdef'd to include them only
+ * when their feature is enabled.
+ */
+static const uint16_t common_names[] PROGMEM = {
+ KC_TRNS, KEYCODE_NAME7('K', 'C', '_', 'T', 'R', 'N', 'S'),
+ KC_ENT , KEYCODE_NAME7('K', 'C', '_', 'E', 'N', 'T', 0 ),
+ KC_ESC , KEYCODE_NAME7('K', 'C', '_', 'E', 'S', 'C', 0 ),
+ KC_BSPC, KEYCODE_NAME7('K', 'C', '_', 'B', 'S', 'P', 'C'),
+ KC_TAB , KEYCODE_NAME7('K', 'C', '_', 'T', 'A', 'B', 0 ),
+ KC_SPC , KEYCODE_NAME7('K', 'C', '_', 'S', 'P', 'C', 0 ),
+ KC_MINS, KEYCODE_NAME7('K', 'C', '_', 'M', 'I', 'N', 'S'),
+ KC_EQL , KEYCODE_NAME7('K', 'C', '_', 'E', 'Q', 'L', 0 ),
+ KC_LBRC, KEYCODE_NAME7('K', 'C', '_', 'L', 'B', 'R', 'C'),
+ KC_RBRC, KEYCODE_NAME7('K', 'C', '_', 'R', 'B', 'R', 'C'),
+ KC_BSLS, KEYCODE_NAME7('K', 'C', '_', 'B', 'S', 'L', 'S'),
+ KC_NUHS, KEYCODE_NAME7('K', 'C', '_', 'N', 'U', 'H', 'S'),
+ KC_SCLN, KEYCODE_NAME7('K', 'C', '_', 'S', 'C', 'L', 'N'),
+ KC_QUOT, KEYCODE_NAME7('K', 'C', '_', 'Q', 'U', 'O', 'T'),
+ KC_GRV , KEYCODE_NAME7('K', 'C', '_', 'G', 'R', 'V', 0 ),
+ KC_COMM, KEYCODE_NAME7('K', 'C', '_', 'C', 'O', 'M', 'M'),
+ KC_DOT , KEYCODE_NAME7('K', 'C', '_', 'D', 'O', 'T', 0 ),
+ KC_SLSH, KEYCODE_NAME7('K', 'C', '_', 'S', 'L', 'S', 'H'),
+ KC_CAPS, KEYCODE_NAME7('K', 'C', '_', 'C', 'A', 'P', 'S'),
+ KC_PSCR, KEYCODE_NAME7('K', 'C', '_', 'P', 'S', 'C', 'R'),
+ KC_PAUS, KEYCODE_NAME7('K', 'C', '_', 'P', 'A', 'U', 'S'),
+ KC_INS , KEYCODE_NAME7('K', 'C', '_', 'I', 'N', 'S', 0 ),
+ KC_HOME, KEYCODE_NAME7('K', 'C', '_', 'H', 'O', 'M', 'E'),
+ KC_PGUP, KEYCODE_NAME7('K', 'C', '_', 'P', 'G', 'U', 'P'),
+ KC_DEL , KEYCODE_NAME7('K', 'C', '_', 'D', 'E', 'L', 0 ),
+ KC_END , KEYCODE_NAME7('K', 'C', '_', 'E', 'N', 'D', 0 ),
+ KC_PGDN, KEYCODE_NAME7('K', 'C', '_', 'P', 'G', 'D', 'N'),
+ KC_RGHT, KEYCODE_NAME7('K', 'C', '_', 'R', 'G', 'H', 'T'),
+ KC_LEFT, KEYCODE_NAME7('K', 'C', '_', 'L', 'E', 'F', 'T'),
+ KC_DOWN, KEYCODE_NAME7('K', 'C', '_', 'D', 'O', 'W', 'N'),
+ KC_UP , KEYCODE_NAME7('K', 'C', '_', 'U', 'P', 0 , 0 ),
+ KC_NUBS, KEYCODE_NAME7('K', 'C', '_', 'N', 'U', 'B', 'S'),
+ KC_HYPR, KEYCODE_NAME7('K', 'C', '_', 'H', 'Y', 'P', 'R'),
+ KC_MEH , KEYCODE_NAME7('K', 'C', '_', 'M', 'E', 'H', 0 ),
+#ifdef EXTRAKEY_ENABLE
+ KC_WHOM, KEYCODE_NAME7('K', 'C', '_', 'W', 'H', 'O', 'M'),
+ KC_WBAK, KEYCODE_NAME7('K', 'C', '_', 'W', 'B', 'A', 'K'),
+ KC_WFWD, KEYCODE_NAME7('K', 'C', '_', 'W', 'F', 'W', 'D'),
+ KC_WSTP, KEYCODE_NAME7('K', 'C', '_', 'W', 'S', 'T', 'P'),
+ KC_WREF, KEYCODE_NAME7('K', 'C', '_', 'W', 'R', 'E', 'F'),
+ KC_MNXT, KEYCODE_NAME7('K', 'C', '_', 'M', 'N', 'X', 'T'),
+ KC_MPRV, KEYCODE_NAME7('K', 'C', '_', 'M', 'P', 'R', 'V'),
+ KC_MPLY, KEYCODE_NAME7('K', 'C', '_', 'M', 'P', 'L', 'Y'),
+ KC_MUTE, KEYCODE_NAME7('K', 'C', '_', 'M', 'U', 'T', 'E'),
+ KC_VOLU, KEYCODE_NAME7('K', 'C', '_', 'V', 'O', 'L', 'U'),
+ KC_VOLD, KEYCODE_NAME7('K', 'C', '_', 'V', 'O', 'L', 'D'),
+#endif // EXTRAKEY_ENABLE
+#ifdef MOUSEKEY_ENABLE
+ MS_LEFT, KEYCODE_NAME7('M', 'S', '_', 'L', 'E', 'F', 'T'),
+ MS_RGHT, KEYCODE_NAME7('M', 'S', '_', 'R', 'G', 'H', 'T'),
+ MS_UP , KEYCODE_NAME7('M', 'S', '_', 'U', 'P', 0 , 0 ),
+ MS_DOWN, KEYCODE_NAME7('M', 'S', '_', 'D', 'O', 'W', 'N'),
+ MS_WHLL, KEYCODE_NAME7('M', 'S', '_', 'W', 'H', 'L', 'L'),
+ MS_WHLR, KEYCODE_NAME7('M', 'S', '_', 'W', 'H', 'L', 'R'),
+ MS_WHLU, KEYCODE_NAME7('M', 'S', '_', 'W', 'H', 'L', 'U'),
+ MS_WHLD, KEYCODE_NAME7('M', 'S', '_', 'W', 'H', 'L', 'D'),
+#endif // MOUSEKEY_ENABLE
+#ifdef SWAP_HANDS_ENABLE
+ SH_ON , KEYCODE_NAME7('S', 'H', '_', 'O', 'N', 0 , 0 ),
+ SH_OFF , KEYCODE_NAME7('S', 'H', '_', 'O', 'F', 'F', 0 ),
+ SH_MON , KEYCODE_NAME7('S', 'H', '_', 'M', 'O', 'N', 0 ),
+ SH_MOFF, KEYCODE_NAME7('S', 'H', '_', 'M', 'O', 'F', 'F'),
+ SH_TOGG, KEYCODE_NAME7('S', 'H', '_', 'T', 'O', 'G', 'G'),
+ SH_TT , KEYCODE_NAME7('S', 'H', '_', 'T', 'T', 0 , 0 ),
+# if !defined(NO_ACTION_ONESHOT)
+ SH_OS , KEYCODE_NAME7('S', 'H', '_', 'O', 'S', 0 , 0 ),
+# endif // !defined(NO_ACTION_ONESHOT)
+#endif // SWAP_HANDS_ENABLE
+#ifdef LEADER_ENABLE
+ QK_LEAD, KEYCODE_NAME7('Q', 'K', '_', 'L', 'E', 'A', 'D'),
+#endif // LEADER_ENABLE
+#ifdef KEY_LOCK_ENABLE
+ QK_LOCK, KEYCODE_NAME7('Q', 'K', '_', 'L', 'O', 'C', 'K'),
+#endif // KEY_LOCK_ENABLE
+#ifdef TRI_LAYER_ENABLE
+ TL_LOWR, KEYCODE_NAME7('T', 'L', '_', 'L', 'O', 'W', 'R'),
+ TL_UPPR, KEYCODE_NAME7('T', 'L', '_', 'U', 'P', 'P', 'R'),
+#endif // TRI_LAYER_ENABLE
+#ifdef GRAVE_ESC_ENABLE
+ QK_GESC, KEYCODE_NAME7('Q', 'K', '_', 'G', 'E', 'S', 'C'),
+#endif // GRAVE_ESC_ENABLE
+#ifdef CAPS_WORD_ENABLE
+ CW_TOGG, KEYCODE_NAME7('C', 'W', '_', 'T', 'O', 'G', 'G'),
+#endif // CAPS_WORD_ENABLE
+#ifdef SECURE_ENABLE
+ SE_LOCK, KEYCODE_NAME7('S', 'E', '_', 'L', 'O', 'C', 'K'),
+ SE_UNLK, KEYCODE_NAME7('S', 'E', '_', 'U', 'N', 'L', 'K'),
+ SE_TOGG, KEYCODE_NAME7('S', 'E', '_', 'T', 'O', 'G', 'G'),
+ SE_REQ , KEYCODE_NAME7('S', 'E', '_', 'R', 'E', 'Q', 0 ),
+#endif // SECURE_ENABLE
+#ifdef LAYER_LOCK_ENABLE
+ QK_LLCK, KEYCODE_NAME7('Q', 'K', '_', 'L', 'L', 'C', 'K'),
+#endif // LAYER_LOCK_ENABLE
+ EE_CLR , KEYCODE_NAME7('E', 'E', '_', 'C', 'L', 'R', 0 ),
+ QK_BOOT, KEYCODE_NAME7('Q', 'K', '_', 'B', 'O', 'O', 'T'),
+ DB_TOGG, KEYCODE_NAME7('D', 'B', '_', 'T', 'O', 'G', 'G'),
+};
+// clang-format on
+
+/** Users can override this to define names of additional keycodes. */
+__attribute__((weak)) const keycode_string_name_t* keycode_string_names_data_user = NULL;
+__attribute__((weak)) uint16_t keycode_string_names_size_user = 0;
+/** Keyboard vendors can override this to define names of additional keycodes. */
+__attribute__((weak)) const keycode_string_name_t* keycode_string_names_data_kb = NULL;
+__attribute__((weak)) uint16_t keycode_string_names_size_kb = 0;
+/** Names of the 4 mods on each hand. */
+static const char mod_names[] PROGMEM = "CTL\0SFT\0ALT\0GUI";
+/** Internal buffer for holding a stringified keycode. */
+static char buffer[32];
+#define BUFFER_MAX_LEN (sizeof(buffer) - 1)
+static index_t buffer_len;
+
+/** Finds the name of a keycode in `common_names` or returns NULL. */
+static const char* search_common_names(uint16_t keycode) {
+ static uint8_t buffer[8];
+
+ for (int_fast16_t offset = 0; offset < ARRAY_SIZE(common_names); offset += 4) {
+ if (keycode == pgm_read_word(common_names + offset)) {
+ const uint16_t w0 = pgm_read_word(common_names + offset + 1);
+ const uint16_t w1 = pgm_read_word(common_names + offset + 2);
+ const uint16_t w2 = pgm_read_word(common_names + offset + 3);
+ buffer[0] = (uint8_t)w0;
+ buffer[1] = (uint8_t)(w0 >> 8);
+ buffer[2] = '_';
+ buffer[3] = (uint8_t)w1;
+ buffer[4] = (uint8_t)(w1 >> 8);
+ buffer[5] = (uint8_t)w2;
+ buffer[6] = (uint8_t)(w2 >> 8);
+ buffer[7] = 0;
+ return (const char*)buffer;
+ }
+ }
+
+ return NULL;
+}
+
+/**
+ * @brief Finds the name of a keycode in table or returns NULL.
+ *
+ * @param data Pointer to table to be searched.
+ * @param size Numer of entries in the table.
+ * @return Name string for the keycode, or NULL if not found.
+ */
+static const char* search_table(const keycode_string_name_t* data, uint16_t size, uint16_t keycode) {
+ if (data != NULL) {
+ for (uint16_t i = 0; i < size; ++i) {
+ if (data[i].keycode == keycode) {
+ return data[i].name;
+ }
+ }
+ }
+ return NULL;
+}
+
+/** Formats `number` in `base`, either 10 or 16. */
+static char* number_string(uint16_t number, int8_t base) {
+ static char result[7];
+ result[sizeof(result) - 1] = '\0';
+ index_t i = sizeof(result) - 1;
+ do {
+ const uint8_t digit = number % base;
+ number /= base;
+ result[--i] = (digit < 10) ? (char)(digit + UINT8_C('0')) : (char)(digit + (UINT8_C('A') - 10));
+ } while (number > 0 && i > 0);
+
+ if (base == 16 && i >= 2) {
+ result[--i] = 'x';
+ result[--i] = '0';
+ }
+ return result + i;
+}
+
+/** Appends `str` to `buffer`, truncating if the result would overflow. */
+static void append(const char* str) {
+ char* dest = buffer + buffer_len;
+ index_t i;
+ for (i = 0; buffer_len + i < BUFFER_MAX_LEN && str[i]; ++i) {
+ dest[i] = str[i];
+ }
+ buffer_len += i;
+ buffer[buffer_len] = '\0';
+}
+
+/** Same as append(), but where `str` is a PROGMEM string. */
+static void append_P(const char* str) {
+ char* dest = buffer + buffer_len;
+ index_t i;
+ for (i = 0; buffer_len + i < BUFFER_MAX_LEN; ++i) {
+ const char c = pgm_read_byte(&str[i]);
+ if (c == '\0') {
+ break;
+ }
+ dest[i] = c;
+ }
+ buffer_len += i;
+ buffer[buffer_len] = '\0';
+}
+
+/** Appends a single char to `buffer` if there is space. */
+static void append_char(char c) {
+ if (buffer_len < BUFFER_MAX_LEN) {
+ buffer[buffer_len] = c;
+ buffer[++buffer_len] = '\0';
+ }
+}
+
+/** Formats `number` in `base`, either 10 or 16, and appends it to `buffer`. */
+static void append_number(uint16_t number, int8_t base) {
+ append(number_string(number, base));
+}
+
+/** Stringifies 5-bit mods and appends it to `buffer`. */
+static void append_5_bit_mods(uint8_t mods) {
+ const bool is_rhs = mods > 15;
+ const uint8_t csag = mods & 15;
+ if (csag != 0 && (csag & (csag - 1)) == 0) { // One mod is set.
+ append_P(PSTR("MOD_"));
+ append_char(is_rhs ? 'R' : 'L');
+ append_P(&mod_names[4 * biton(csag)]);
+ } else { // Fallback: write the mod as a hex value.
+ append_number(mods, 16);
+ }
+}
+
+/**
+ * @brief Writes a keycode of the format `name` + "(" + `param` + ")".
+ * @note `name` is a PROGMEM string, `param` is not.
+ */
+static void append_unary_keycode(const char* name, const char* param) {
+ append_P(name);
+ append_char('(');
+ append(param);
+ append_char(')');
+}
+
+/**
+ * @brief Writes a keycode of the format `name` + `number`.
+ * @note `name` is a PROGMEM string.
+ */
+static void append_numbered_keycode(const char* name, uint16_t number) {
+ append_P(name);
+ append_number(number, 10);
+}
+
+/** Stringifies `keycode` and appends it to `buffer`. */
+static void append_keycode(uint16_t keycode) {
+ // In case there is overlap among tables, search `keycode_string_names_user`
+ // first so that it takes precedence.
+ const char* keycode_name = search_table(keycode_string_names_data_user, keycode_string_names_size_user, keycode);
+ if (keycode_name) {
+ append(keycode_name);
+ return;
+ }
+ keycode_name = search_table(keycode_string_names_data_kb, keycode_string_names_size_kb, keycode);
+ if (keycode_name) {
+ append(keycode_name);
+ return;
+ }
+ keycode_name = search_common_names(keycode);
+ if (keycode_name) {
+ append(keycode_name);
+ return;
+ }
+
+ if (keycode <= 255) { // Basic keycodes.
+ switch (keycode) {
+ // Modifiers KC_LSFT, KC_RCTL, etc.
+ case MODIFIER_KEYCODE_RANGE: {
+ const uint8_t i = keycode - KC_LCTL;
+ const bool is_rhs = i > 3;
+ append_P(PSTR("KC_"));
+ append_char(is_rhs ? 'R' : 'L');
+ append_P(&mod_names[4 * (i & 3)]);
+ }
+ return;
+
+ // Letters A-Z.
+ case KC_A ... KC_Z:
+ append_P(PSTR("KC_"));
+ append_char((char)(keycode + (UINT8_C('A') - KC_A)));
+ return;
+
+ // Digits 0-9 (NOTE: Unlike the ASCII order, KC_0 comes *after* KC_9.)
+ case KC_1 ... KC_0:
+ append_numbered_keycode(PSTR("KC_"), (keycode - (KC_1 - 1)) % 10);
+ return;
+
+ // Keypad digits.
+ case KC_KP_1 ... KC_KP_0:
+ append_numbered_keycode(PSTR("KC_KP_"), (keycode - (KC_KP_1 - 1)) % 10);
+ return;
+
+ // Function keys. F1-F12 and F13-F24 are coded in separate ranges.
+ case KC_F1 ... KC_F12:
+ append_numbered_keycode(PSTR("KC_F"), keycode - (KC_F1 - 1));
+ return;
+
+ case KC_F13 ... KC_F24:
+ append_numbered_keycode(PSTR("KC_F"), keycode - (KC_F13 - 13));
+ return;
+ }
+ }
+
+ // clang-format off
+ switch (keycode) {
+ // A modified keycode, like S(KC_1) for Shift + 1 = !. This implementation
+ // only covers modified keycodes where one modifier is applied, e.g. a
+ // Ctrl + Shift + kc or Hyper + kc keycode is not formatted.
+ case QK_MODS ... QK_MODS_MAX: {
+ uint8_t mods = QK_MODS_GET_MODS(keycode);
+ const bool is_rhs = mods > 15;
+ mods &= 15;
+ if (mods != 0 && (mods & (mods - 1)) == 0) { // One mod is set.
+ const char* name = &mod_names[4 * biton(mods)];
+ if (is_rhs) {
+ append_char('R');
+ append_P(name);
+ } else {
+ append_char(pgm_read_byte(&name[0]));
+ }
+ append_char('(');
+ append_keycode(QK_MODS_GET_BASIC_KEYCODE(keycode));
+ append_char(')');
+ return;
+ }
+ } break;
+
+#if !defined(NO_ACTION_ONESHOT)
+ case QK_ONE_SHOT_MOD ... QK_ONE_SHOT_MOD_MAX: // One-shot mod OSM(mod) key.
+ append_P(PSTR("OSM("));
+ append_5_bit_mods(QK_ONE_SHOT_MOD_GET_MODS(keycode));
+ append_char(')');
+ return;
+#endif // !defined(NO_ACTION_ONESHOT)
+
+ // Various layer switch keys.
+ case QK_LAYER_TAP ... QK_LAYER_TAP_MAX: // Layer-tap LT(layer,kc) key.
+ append_P(PSTR("LT("));
+ append_number(QK_LAYER_TAP_GET_LAYER(keycode), 10);
+ append_char(',');
+ append_keycode(QK_LAYER_TAP_GET_TAP_KEYCODE(keycode));
+ append_char(')');
+ return;
+
+ case QK_LAYER_MOD ... QK_LAYER_MOD_MAX: // LM(layer,mod) key.
+ append_P(PSTR("LM("));
+ append_number(QK_LAYER_MOD_GET_LAYER(keycode), 10);
+ append_char(',');
+ append_5_bit_mods(QK_LAYER_MOD_GET_MODS(keycode));
+ append_char(')');
+ return;
+
+ case QK_TO ... QK_TO_MAX: // TO(layer) key.
+ append_unary_keycode(PSTR("TO"), number_string(QK_TO_GET_LAYER(keycode), 10));
+ return;
+
+ case QK_MOMENTARY ... QK_MOMENTARY_MAX: // MO(layer) key.
+ append_unary_keycode(PSTR("MO"), number_string(QK_MOMENTARY_GET_LAYER(keycode), 10));
+ return;
+
+ case QK_DEF_LAYER ... QK_DEF_LAYER_MAX: // DF(layer) key.
+ append_unary_keycode(PSTR("DF"), number_string(QK_DEF_LAYER_GET_LAYER(keycode), 10));
+ return;
+
+ case QK_TOGGLE_LAYER ... QK_TOGGLE_LAYER_MAX: // TG(layer) key.
+ append_unary_keycode(PSTR("TG"), number_string(QK_TOGGLE_LAYER_GET_LAYER(keycode), 10));
+ return;
+
+#if !defined(NO_ACTION_ONESHOT)
+ case QK_ONE_SHOT_LAYER ... QK_ONE_SHOT_LAYER_MAX: // OSL(layer) key.
+ append_unary_keycode(PSTR("OSL"), number_string(QK_ONE_SHOT_LAYER_GET_LAYER(keycode), 10));
+ return;
+#endif // !defined(NO_ACTION_ONESHOT)
+
+ case QK_LAYER_TAP_TOGGLE ... QK_LAYER_TAP_TOGGLE_MAX: // TT(layer) key.
+ append_unary_keycode(PSTR("TT"), number_string(QK_LAYER_TAP_TOGGLE_GET_LAYER(keycode), 10));
+ return;
+
+ case QK_PERSISTENT_DEF_LAYER ... QK_PERSISTENT_DEF_LAYER_MAX: // PDF(layer) key.
+ append_unary_keycode(PSTR("PDF"), number_string(QK_PERSISTENT_DEF_LAYER_GET_LAYER(keycode), 10));
+ return;
+
+ // Mod-tap MT(mod,kc) key. This implementation formats the MT keys where
+ // one modifier is applied. For MT keys with multiple modifiers, the mod
+ // arg is written numerically as a hex code.
+ case QK_MOD_TAP ... QK_MOD_TAP_MAX: {
+ uint8_t mods = QK_MOD_TAP_GET_MODS(keycode);
+ const bool is_rhs = mods > 15;
+ const uint8_t csag = mods & 15;
+ if (csag != 0 && (csag & (csag - 1)) == 0) { // One mod is set.
+ append_char(is_rhs ? 'R' : 'L');
+ append_P(&mod_names[4 * biton(csag)]);
+ append_P(PSTR("_T("));
+ } else if (mods == MOD_HYPR) {
+ append_P(PSTR("HYPR_T("));
+ } else if (mods == MOD_MEH) {
+ append_P(PSTR("MEH_T("));
+ } else {
+ append_P(PSTR("MT("));
+ append_number(mods, 16);
+ append_char(',');
+ }
+ append_keycode(QK_MOD_TAP_GET_TAP_KEYCODE(keycode));
+ append_char(')');
+ } return;
+
+ case QK_TAP_DANCE ... QK_TAP_DANCE_MAX: // Tap dance TD(i) key.
+ append_unary_keycode(PSTR("TD"), number_string(QK_TAP_DANCE_GET_INDEX(keycode), 10));
+ return;
+
+#ifdef UNICODE_ENABLE
+ case QK_UNICODE ... QK_UNICODE_MAX: // Unicode UC(codepoint) key.
+ append_unary_keycode(PSTR("UC"), number_string(QK_UNICODE_GET_CODE_POINT(keycode), 16));
+ return;
+#elif defined(UNICODEMAP_ENABLE)
+ case QK_UNICODEMAP ... QK_UNICODEMAP_MAX: // Unicode Map UM(i) key.
+ append_unary_keycode(PSTR("UM"), number_string(QK_UNICODEMAP_GET_INDEX(keycode), 10));
+ return;
+
+ case QK_UNICODEMAP_PAIR ... QK_UNICODEMAP_PAIR_MAX: { // UP(i,j) key.
+ const uint8_t i = QK_UNICODEMAP_PAIR_GET_UNSHIFTED_INDEX(keycode);
+ const uint8_t j = QK_UNICODEMAP_PAIR_GET_SHIFTED_INDEX(keycode);
+ append_P(PSTR("UP("));
+ append_number(i, 10);
+ append_char(',');
+ append_number(j, 10);
+ append_char(')');
+ } return;
+#endif
+#ifdef MOUSEKEY_ENABLE
+ case MS_BTN1 ... MS_BTN8: // Mouse button keycode.
+ append_numbered_keycode(PSTR("MS_BTN"), keycode - (MS_BTN1 - 1));
+ return;
+#endif // MOUSEKEY_ENABLE
+#ifdef SWAP_HANDS_ENABLE
+ case QK_SWAP_HANDS ... QK_SWAP_HANDS_MAX: // Swap Hands SH_T(kc) key.
+ if (!IS_SWAP_HANDS_KEYCODE(keycode)) {
+ append_P(PSTR("SH_T("));
+ append_keycode(QK_SWAP_HANDS_GET_TAP_KEYCODE(keycode));
+ append_char(')');
+ return;
+ }
+ break;
+#endif // SWAP_HANDS_ENABLE
+#ifdef JOYSTICK_ENABLE
+ case JOYSTICK_KEYCODE_RANGE: // Joystick JS_ key.
+ append_numbered_keycode(PSTR("JS_"), keycode - JS_0);
+ return;
+#endif // JOYSTICK_ENABLE
+#ifdef PROGRAMMABLE_BUTTON_ENABLE
+ case PROGRAMMABLE_BUTTON_KEYCODE_RANGE: // Programmable button PB_ key.
+ append_numbered_keycode(PSTR("PB_"), keycode - (PB_1 - 1));
+ return;
+#endif // PROGRAMMABLE_BUTTON_ENABLE
+
+ case MACRO_KEYCODE_RANGE: // Macro range MC_ keycode.
+ append_numbered_keycode(PSTR("MC_"), keycode - MC_0);
+ return;
+
+ case KB_KEYCODE_RANGE: // Keyboard range keycode.
+ append_numbered_keycode(PSTR("QK_KB_"), keycode - QK_KB_0);
+ return;
+
+ case USER_KEYCODE_RANGE: // User range keycode.
+ append_numbered_keycode(PSTR("QK_USER_"), keycode - QK_USER_0);
+ return;
+
+ // It would take a nontrivial amount of string data to cover some
+ // feature-specific keycodes, such as those for MIDI and lighting. As a
+ // fallback while still providing some information, we stringify
+ // remaining keys in known code ranges as "QK_<feature>+<number>".
+#ifdef MAGIC_ENABLE
+ case MAGIC_KEYCODE_RANGE:
+ append_numbered_keycode(PSTR("QK_MAGIC+"), keycode - QK_MAGIC);
+ return;
+#endif // MAGIC_ENABLE
+#ifdef MIDI_ENABLE
+ case MIDI_KEYCODE_RANGE:
+ append_numbered_keycode(PSTR("QK_MIDI+"), keycode - QK_MIDI);
+ return;
+#endif // MIDI_ENABLE
+#ifdef SEQUENCER_ENABLE
+ case SEQUENCER_KEYCODE_RANGE:
+ append_numbered_keycode(PSTR("QK_SEQUENCER+"), keycode - QK_SEQUENCER);
+ return;
+#endif // SEQUENCER_ENABLE
+#ifdef AUDIO_ENABLE
+ case AUDIO_KEYCODE_RANGE:
+ append_numbered_keycode(PSTR("QK_AUDIO+"), keycode - QK_AUDIO);
+ return;
+#endif // AUDIO_ENABLE
+#if defined(BACKLIGHT_ENABLE) || defined(LED_MATRIX_ENABLE) || defined(RGBLIGHT_ENABLED) || defined(RGB_MATRIX_ENABLE) // Lighting-related features.
+ case QK_LIGHTING ... QK_LIGHTING_MAX:
+ append_numbered_keycode(PSTR("QK_LIGHTING+"), keycode - QK_LIGHTING);
+ return;
+#endif // defined(BACKLIGHT_ENABLE) || defined(LED_MATRIX_ENABLE) || defined(RGBLIGHT_ENABLED) || defined(RGB_MATRIX_ENABLE)
+#ifdef STENO_ENABLE
+ case STENO_KEYCODE_RANGE:
+ append_numbered_keycode(PSTR("QK_STENO+"), keycode - QK_STENO);
+ return;
+#endif // AUDIO_ENABLE
+#ifdef BLUETOOTH_ENABLE
+ case CONNECTION_KEYCODE_RANGE:
+ append_numbered_keycode(PSTR("QK_CONNECTION+"), keycode - QK_CONNECTION);
+ return;
+#endif // BLUETOOTH_ENABLE
+ case QUANTUM_KEYCODE_RANGE:
+ append_numbered_keycode(PSTR("QK_QUANTUM+"), keycode - QK_QUANTUM);
+ return;
+ }
+ // clang-format on
+
+ append_number(keycode, 16); // Fallback: write keycode as hex value.
+}
+
+const char* get_keycode_string(uint16_t keycode) {
+ buffer_len = 0;
+ buffer[0] = '\0';
+ append_keycode(keycode);
+ return buffer;
+}
diff --git a/quantum/keycode_string.h b/quantum/keycode_string.h
new file mode 100644
index 0000000000..1315613a80
--- /dev/null
+++ b/quantum/keycode_string.h
@@ -0,0 +1,134 @@
+// Copyright 2024-2025 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.
+
+#pragma once
+
+#include <stdint.h>
+
+#if KEYCODE_STRING_ENABLE
+
+/**
+ * @brief Formats a QMK keycode as a human-readable string.
+ *
+ * Given a keycode, like `KC_A`, this function returns a formatted string, like
+ * "KC_A". This is useful for debugging and diagnostics so that keys are more
+ * easily identified than they would be by raw numerical codes.
+ *
+ * @note 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()`.
+ *
+ * Many common QMK keycodes are understood by this function, but not all.
+ * Recognized keycodes include:
+ *
+ * - Most basic keycodes, including letters `KC_A` - `KC_Z`, digits `KC_0` -
+ * `KC_9`, function keys `KC_F1` - `KC_F24`, and modifiers like `KC_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)`.
+ *
+ * - Swap hands keycodes `SH_T(kc)`, `SH_TOGG`, etc.
+ *
+ * - Joystick keycodes `JS_n`.
+ *
+ * - Programmable button keycodes `PB_n`.
+ *
+ * - Unicode `UC(codepoint)` and Unicode Map `UM(i)` and `UP(i,j)` keycodes.
+ *
+ * - Keyboard range keycodes `QK_KB_*`.
+ *
+ * - User range (SAFE_RANGE) keycodes `QK_USER_*`.
+ *
+ * Keycodes involving mods like `OSM`, `LM`, `MT` are fully supported only where
+ * a single mod is applied.
+ *
+ * Unrecognized keycodes are printed numerically as hex values like `0x1ABC`.
+ *
+ * Optionally, use `keycode_string_names_user` or `keycode_string_names_kb` to
+ * define names for additional keycodes or override how any of the above are
+ * formatted.
+ *
+ * @param keycode QMK keycode.
+ * @return Stringified keycode.
+ */
+const char* get_keycode_string(uint16_t keycode);
+
+/** Defines a human-readable name for a keycode. */
+typedef struct {
+ uint16_t keycode;
+ const char* name;
+} keycode_string_name_t;
+
+// clang-format off
+/**
+ * @brief Defines names for additional keycodes for `get_keycode_string()`.
+ *
+ * Define `KEYCODE_STRING_NAMES_USER` in your keymap.c to add names for
+ * additional keycodes to `keycode_string()`. This table may also be used to
+ * override how `keycode_string()` formats a keycode. 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 as "KC_EXLM" instead of the default "S(KC_1)".
+ */
+# define KEYCODE_STRING_NAMES_USER(...) \
+ static const keycode_string_name_t keycode_string_names_user[] = {__VA_ARGS__}; \
+ uint16_t keycode_string_names_size_user = \
+ sizeof(keycode_string_names_user) / sizeof(keycode_string_name_t); \
+ const keycode_string_name_t* keycode_string_names_data_user = \
+ keycode_string_names_user
+
+/** Same as above, but defines keycode string names at the keyboard level. */
+# define KEYCODE_STRING_NAMES_KB(...) \
+ static const keycode_string_name_t keycode_string_names_kb[] = {__VA_ARGS__}; \
+ uint16_t keycode_string_names_size_kb = \
+ sizeof(keycode_string_names_kb) / sizeof(keycode_string_name_t); \
+ const keycode_string_name_t* keycode_string_names_data_kb = \
+ keycode_string_names_kb
+
+/** Helper to define a keycode_string_name_t. */
+# define KEYCODE_STRING_NAME(kc) \
+ { (kc), #kc }
+// clang-format on
+
+extern const keycode_string_name_t* keycode_string_names_data_user;
+extern uint16_t keycode_string_names_size_user;
+extern const keycode_string_name_t* keycode_string_names_data_kb;
+extern uint16_t keycode_string_names_size_kb;
+
+#else
+
+// When keycode_string is disabled, fall back to printing keycodes numerically
+// as decimal values, using get_u16_str() from quantum.c.
+# define get_keycode_string(kc) get_u16_str(kc, ' ')
+
+const char* get_u16_str(uint16_t curr_num, char curr_pad);
+
+# define KEYCODE_STRING_NAMES_USER(...)
+# define KEYCODE_STRING_NAMES_KB(...)
+# define KEYCODE_STRING_NAME(kc)
+
+#endif // KEYCODE_STRING_ENABLE
diff --git a/quantum/quantum.h b/quantum/quantum.h
index 59a415ead4..3a994e9a03 100644
--- a/quantum/quantum.h
+++ b/quantum/quantum.h
@@ -39,6 +39,7 @@
#include "keymap_common.h"
#include "quantum_keycodes.h"
#include "keycode_config.h"
+#include "keycode_string.h"
#include "action_layer.h"
#include "eeconfig.h"
#include "bootloader.h"
diff --git a/tests/keycode_string/config.h b/tests/keycode_string/config.h
new file mode 100644
index 0000000000..7fc76d7c2e
--- /dev/null
+++ b/tests/keycode_string/config.h
@@ -0,0 +1,19 @@
+/* Copyright 2017 Fred Sundvik
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "test_common.h"
diff --git a/tests/keycode_string/test.mk b/tests/keycode_string/test.mk
new file mode 100644
index 0000000000..aa255a1b6b
--- /dev/null
+++ b/tests/keycode_string/test.mk
@@ -0,0 +1,22 @@
+# Copyright 2025 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.
+
+EXTRAKEY_ENABLE = yes
+KEYCODE_STRING_ENABLE = yes
+KEY_LOCK_ENABLE = yes
+MAGIC_ENABLE = yes
+MOUSEKEY_ENABLE = yes
+PROGRAMMABLE_BUTTON_ENABLE = yes
+SECURE_ENABLE = yes
+SWAP_HANDS_ENABLE = yes
diff --git a/tests/keycode_string/test_keycode_string.cpp b/tests/keycode_string/test_keycode_string.cpp
new file mode 100644
index 0000000000..e1dec70e7a
--- /dev/null
+++ b/tests/keycode_string/test_keycode_string.cpp
@@ -0,0 +1,153 @@
+// Copyright 2025 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.
+
+#include <iostream>
+
+#include "test_common.hpp"
+
+enum {
+ MYMACRO1 = SAFE_RANGE,
+ MYMACRO2,
+};
+
+// clang-format off
+extern "C" {
+
+KEYCODE_STRING_NAMES_KB(
+ KEYCODE_STRING_NAME(MYMACRO1),
+);
+
+KEYCODE_STRING_NAMES_USER(
+ KEYCODE_STRING_NAME(MYMACRO2),
+ KEYCODE_STRING_NAME(KC_EXLM),
+);
+
+const keypos_t PROGMEM hand_swap_config[MATRIX_ROWS][MATRIX_COLS] = {
+ {{9, 0}, {8, 0}, {7, 0}, {6, 0}, {5, 0}, {4, 0}, {3, 0}, {2, 0}, {1, 0}, {0, 0}},
+ {{9, 1}, {8, 1}, {7, 1}, {6, 1}, {5, 1}, {4, 1}, {3, 1}, {2, 1}, {1, 1}, {0, 1}},
+ {{9, 2}, {8, 2}, {7, 2}, {6, 2}, {5, 2}, {4, 2}, {3, 2}, {2, 2}, {1, 2}, {0, 2}},
+ {{9, 3}, {8, 3}, {7, 3}, {6, 3}, {5, 3}, {4, 3}, {3, 3}, {2, 3}, {1, 3}, {0, 3}},
+};
+
+} // extern "C"
+// clang-format on
+
+class KeycodeStringTest : public TestFixture {};
+
+TEST_F(KeycodeStringTest, get_keycode_string) {
+ struct TestParams {
+ uint16_t keycode;
+ std::string expected;
+ };
+ for (const auto [keycode, expected] : std::vector<TestParams>({
+ {KC_TRNS, "KC_TRNS"},
+ {KC_ESC, "KC_ESC"},
+ {KC_A, "KC_A"},
+ {KC_Z, "KC_Z"},
+ {KC_0, "KC_0"},
+ {KC_9, "KC_9"},
+ {KC_KP_0, "KC_KP_0"},
+ {KC_KP_9, "KC_KP_9"},
+ {KC_LBRC, "KC_LBRC"},
+ {KC_NUHS, "KC_NUHS"},
+ {KC_NUBS, "KC_NUBS"},
+ {KC_CAPS, "KC_CAPS"},
+ {DB_TOGG, "DB_TOGG"},
+ {KC_LCTL, "KC_LCTL"},
+ {KC_LSFT, "KC_LSFT"},
+ {KC_RALT, "KC_RALT"},
+ {KC_RGUI, "KC_RGUI"},
+ {KC_UP, "KC_UP"},
+ {KC_HYPR, "KC_HYPR"},
+ {KC_MEH, "KC_MEH"},
+ // F1-F24 keycodes.
+ {KC_F1, "KC_F1"},
+ {KC_F12, "KC_F12"},
+ {KC_F13, "KC_F13"},
+ {KC_F24, "KC_F24"},
+ // Macro keycodes.
+ {MC_0, "MC_0"},
+ {MC_31, "MC_31"},
+ // Keyboard range keycodes.
+ {QK_KB_0, "QK_KB_0"},
+ {QK_KB_31, "QK_KB_31"},
+ // User range keycodes.
+ {QK_USER_2, "QK_USER_2"},
+ {QK_USER_31, "QK_USER_31"},
+ // Modified keycodes.
+ {KC_COLN, "S(KC_SCLN)"},
+ {C(KC_PGUP), "C(KC_PGUP)"},
+ {RALT(KC_BSPC), "RALT(KC_BSPC)"},
+ // One-shot mods.
+ {OSM(MOD_LSFT), "OSM(MOD_LSFT)"},
+ {OSM(MOD_RGUI), "OSM(MOD_RGUI)"},
+ {OSM(MOD_RCTL | MOD_RGUI), "OSM(0x19)"},
+ // Layer switch keycodes.
+ {DF(2), "DF(2)"},
+ {PDF(12), "PDF(12)"},
+ {MO(3), "MO(3)"},
+ {TO(0), "TO(0)"},
+ {TT(1), "TT(1)"},
+ {TG(3), "TG(3)"},
+ {OSL(3), "OSL(3)"},
+ {LM(3, MOD_RALT), "LM(3,MOD_RALT)"},
+ {LT(15, KC_QUOT), "LT(15,KC_QUOT)"},
+ // Tap dance keycodes.
+ {TD(0), "TD(0)"},
+ {TD(31), "TD(31)"},
+ // Mod-tap keycodes.
+ {LSFT_T(KC_ENT), "LSFT_T(KC_ENT)"},
+ {RCTL_T(KC_RGHT), "RCTL_T(KC_RGHT)"},
+ {HYPR_T(KC_GRV), "HYPR_T(KC_GRV)"},
+ {MEH_T(KC_EQL), "MEH_T(KC_EQL)"},
+ {RSA_T(KC_LBRC), "MT(0x16,KC_LBRC)"},
+ // Extrakey keycodes.
+ {KC_WBAK, "KC_WBAK"},
+ {KC_WFWD, "KC_WFWD"},
+ {KC_WREF, "KC_WREF"},
+ {KC_VOLU, "KC_VOLU"},
+ {KC_VOLD, "KC_VOLD"},
+ // Mouse Key keycodes.
+ {MS_LEFT, "MS_LEFT"},
+ {MS_RGHT, "MS_RGHT"},
+ {MS_UP, "MS_UP"},
+ {MS_WHLU, "MS_WHLU"},
+ {MS_WHLD, "MS_WHLD"},
+ {MS_BTN1, "MS_BTN1"},
+ {MS_BTN8, "MS_BTN8"},
+ // Swap Hands keycodes.
+ {SH_MON, "SH_MON"},
+ {SH_TOGG, "SH_TOGG"},
+ {SH_T(KC_PSCR), "SH_T(KC_PSCR)"},
+ // Secure keycodes.
+ {SE_LOCK, "SE_LOCK"},
+ {SE_UNLK, "SE_UNLK"},
+ {SE_TOGG, "SE_TOGG"},
+ {SE_REQ, "SE_REQ"},
+ // Programmable button keycodes.
+ {PB_1, "PB_1"},
+ {PB_32, "PB_32"},
+ // Magic button keycodes.
+ {QK_MAGIC + 7, "QK_MAGIC+7"},
+ // Quantum keycodes.
+ {QK_LOCK, "QK_LOCK"},
+ {QK_QUANTUM + 7, "QK_QUANTUM+7"},
+ // Custom keycode names.
+ {MYMACRO1, "MYMACRO1"},
+ {MYMACRO2, "MYMACRO2"},
+ {KC_EXLM, "KC_EXLM"},
+ })) {
+ EXPECT_EQ(get_keycode_string(keycode), expected) << "where keycode = 0x" << std::hex << keycode;
+ }
+}