diff --git a/builddefs/generic_features.mk b/builddefs/generic_features.mk index f14f44087702..015a804d91f3 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 35a4160e2765..269049afb881 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 3e4c914bf12d..7ef3817f1860 100644 --- a/docs/unit_testing.md +++ b/docs/unit_testing.md @@ -58,6 +58,35 @@ 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 +const keycode_string_name_t keycode_string_names_user[] = { + KEYCODE_STRING_NAME(MYMACRO1), + KEYCODE_STRING_NAME(MYMACRO2), + KEYCODE_STRING_NAMES_END // End of table sentinel. +}; +``` + +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 000000000000..369065936a8f --- /dev/null +++ b/quantum/keycode_string.c @@ -0,0 +1,413 @@ +// 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 +#include "bitwise.h" +#include "keycode.h" +#include "progmem.h" +#include "quantum_keycodes.h" + +typedef int_fast8_t index_t; + +// clang-format off +/** Names of some common keycodes. */ +static const keycode_string_name_t keycode_names[] = { + KEYCODE_STRING_NAME(KC_TRNS), + KEYCODE_STRING_NAME(KC_ENT), + KEYCODE_STRING_NAME(KC_ESC), + KEYCODE_STRING_NAME(KC_BSPC), + KEYCODE_STRING_NAME(KC_TAB), + KEYCODE_STRING_NAME(KC_SPC), + KEYCODE_STRING_NAME(KC_MINS), + KEYCODE_STRING_NAME(KC_EQL), + KEYCODE_STRING_NAME(KC_LBRC), + KEYCODE_STRING_NAME(KC_RBRC), + KEYCODE_STRING_NAME(KC_BSLS), + KEYCODE_STRING_NAME(KC_NUHS), + KEYCODE_STRING_NAME(KC_SCLN), + KEYCODE_STRING_NAME(KC_QUOT), + KEYCODE_STRING_NAME(KC_GRV), + KEYCODE_STRING_NAME(KC_COMM), + KEYCODE_STRING_NAME(KC_DOT), + KEYCODE_STRING_NAME(KC_SLSH), + KEYCODE_STRING_NAME(KC_CAPS), + KEYCODE_STRING_NAME(KC_INS), + KEYCODE_STRING_NAME(KC_HOME), + KEYCODE_STRING_NAME(KC_PGUP), + KEYCODE_STRING_NAME(KC_DEL), + KEYCODE_STRING_NAME(KC_END), + KEYCODE_STRING_NAME(KC_PGDN), + KEYCODE_STRING_NAME(KC_RGHT), + KEYCODE_STRING_NAME(KC_LEFT), + KEYCODE_STRING_NAME(KC_DOWN), + KEYCODE_STRING_NAME(KC_UP), + KEYCODE_STRING_NAME(KC_NUBS), +#ifdef EXTRAKEY_ENABLE + KEYCODE_STRING_NAME(KC_WBAK), + KEYCODE_STRING_NAME(KC_WFWD), + KEYCODE_STRING_NAME(KC_WREF), + KEYCODE_STRING_NAME(KC_MNXT), + KEYCODE_STRING_NAME(KC_MPRV), + KEYCODE_STRING_NAME(KC_MPLY), + KEYCODE_STRING_NAME(KC_VOLU), + KEYCODE_STRING_NAME(KC_VOLD), +#endif // EXTRAKEY_ENABLE +#ifdef MOUSEKEY_ENABLE + KEYCODE_STRING_NAME(MS_LEFT), + KEYCODE_STRING_NAME(MS_RGHT), + KEYCODE_STRING_NAME(MS_UP), + KEYCODE_STRING_NAME(MS_DOWN), + KEYCODE_STRING_NAME(MS_WHLL), + KEYCODE_STRING_NAME(MS_WHLR), + KEYCODE_STRING_NAME(MS_WHLU), + KEYCODE_STRING_NAME(MS_WHLD), +#endif // MOUSEKEY_ENABLE +#ifdef TRI_LAYER_ENABLE + KEYCODE_STRING_NAME(TL_LOWR), + KEYCODE_STRING_NAME(TL_UPPR), +#endif // TRI_LAYER_ENABLE +#ifdef GRAVE_ESC_ENABLE + KEYCODE_STRING_NAME(QK_GESC), +#endif // GRAVE_ESC_ENABLE +#ifdef CAPS_WORD_ENABLE + KEYCODE_STRING_NAME(CW_TOGG), +#endif // CAPS_WORD_ENABLE +#ifdef LAYER_LOCK_ENABLE + KEYCODE_STRING_NAME(QK_LLCK), +#endif // LAYER_LOCK_ENABLE + KEYCODE_STRING_NAME(DB_TOGG), + KEYCODE_STRING_NAMES_END +}; +// clang-format on +/** Users can override this to define names of additional keycodes. */ +__attribute__((weak)) const keycode_string_name_t keycode_string_names_user[] = {KEYCODE_STRING_NAMES_END}; +/** Keyboard vendors can override this to define names of additional keycodes. */ +__attribute__((weak)) const keycode_string_name_t keycode_string_names_kb[] = {KEYCODE_STRING_NAMES_END}; +/** Names of the 4 mods on each hand. */ +static const char* mod_names[4] = {PSTR("CTL"), PSTR("SFT"), PSTR("ALT"), PSTR("GUI")}; +/** Internal buffer for holding a stringified keycode. */ +static char buffer[32]; +#define BUFFER_MAX_LEN (sizeof(buffer) - 1) +static index_t buffer_len; + +/** + * @brief Finds the name of a keycode in `table` or returns NULL. + * + * The last entry of the table must be `KEYCODE_STRING_NAMES_END`. + * + * @param table A table of keycode_string_name_t to be searched. + * @return Name string for the keycode, or NULL if not found. + */ +static const char* find_keycode_name(const keycode_string_name_t* table, uint16_t keycode) { + for (; table->keycode; ++table) { + if (table->keycode == keycode) { + return table->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; + mods &= 15; + if (mods != 0 && (mods & (mods - 1)) == 0) { // One mod is set. + append_P(PSTR("MOD_")); + append_char(is_rhs ? 'R' : 'L'); + append_P(mod_names[biton(mods)]); + } 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(')'); +} + +/** 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 = find_keycode_name(keycode_string_names_user, keycode); + if (keycode_name) { + append_P(keycode_name); + return; + } + keycode_name = find_keycode_name(keycode_string_names_kb, keycode); + if (keycode_name) { + append_P(keycode_name); + return; + } + keycode_name = find_keycode_name(keycode_names, keycode); + if (keycode_name) { + append_P(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[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_P(PSTR("KC_")); + append_char('0' + (char)((keycode - (KC_1 - 1)) % 10)); + return; + + // Keypad digits. + case KC_KP_1 ... KC_KP_0: + append_P(PSTR("KC_KP_")); + append_char('0' + (char)((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_P(PSTR("KC_F")); + append_number(keycode - (KC_F1 - 1), 10); + return; + + case KC_F13 ... KC_F24: + append_P(PSTR("KC_F")); + append_number(keycode - (KC_F13 - 13), 10); + 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[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; + + // One-shot mod OSM(mod) key. + case QK_ONE_SHOT_MOD ... QK_ONE_SHOT_MOD_MAX: + append_P(PSTR("OSM(")); + append_5_bit_mods(QK_ONE_SHOT_MOD_GET_MODS(keycode)); + append_char(')'); + return; + + // 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; + + 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; + + 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; + mods &= 15; + if (mods != 0 && (mods & (mods - 1)) == 0) { // One mod is set. + append_char(is_rhs ? 'R' : 'L'); + append_P(mod_names[biton(mods)]); + append_P(PSTR("_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 dest; +#endif +#ifdef MOUSEKEY_ENABLE + case MS_BTN1 ... MS_BTN8: // Mouse button keycode. + append_P(PSTR("MS_BTN")); + append_number(keycode - (MS_BTN1 - 1), 10); + return; +#endif // MOUSEKEY_ENABLE + + case KB_KEYCODE_RANGE: // Keyboard range keycode. + append_P(PSTR("QK_KB_")); + append_number(keycode - QK_KB_0, 10); + return; + + case USER_KEYCODE_RANGE: // User range keycode. + append_P(PSTR("QK_USER_")); + append_number(keycode - QK_USER_0, 10); + 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 000000000000..1673fcfeaa97 --- /dev/null +++ b/quantum/keycode_string.h @@ -0,0 +1,112 @@ +// 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 + +#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)`. + * + * - 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; + +/** + * @brief Names for additional keycodes for `get_keycode_string()`. + * + * @note The table *must* end with `KEYCODE_STRING_NAMES_END`. + * + * Define the `custom_keycode_names` table 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: + * + * const keycode_string_name_t keycode_string_names_user[] = { + * KEYCODE_STRING_NAME(MYMACRO1), + * KEYCODE_STRING_NAME(MYMACRO2), + * KEYCODE_STRING_NAME(KC_EXLM), + * KEYCODE_STRING_NAMES_END // End of table sentinel. + * }; + * + * The above defines names for `MYMACRO1` and `MYMACRO2`, and overrides + * `KC_EXLM` to format as "KC_EXLM" instead of the default "S(KC_1)". + */ +extern const keycode_string_name_t keycode_string_names_user[]; +/** Same as `keycode_string_names_user`, but for use at the keyboard level. */ +extern const keycode_string_name_t keycode_string_names_kb[]; + +/** Helper to define a keycode_string_name_t. */ +# define KEYCODE_STRING_NAME(kc) \ + { (kc), PSTR(#kc) } +/** Makes end-of-table sentinel for a table of keycode_string_name_t. */ +# define KEYCODE_STRING_NAMES_END \ + { 0, NULL } + +#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); + +#endif // KEYCODE_STRING_ENABLE diff --git a/quantum/quantum.h b/quantum/quantum.h index 9db88a54d4ba..9ec9507c1713 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 000000000000..7fc76d7c2e7f --- /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 . + */ + +#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 000000000000..23c1f04e2bc4 --- /dev/null +++ b/tests/keycode_string/test.mk @@ -0,0 +1,17 @@ +# 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. + +KEYCODE_STRING_ENABLE = yes +EXTRAKEY_ENABLE = yes +MOUSEKEY_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 000000000000..781b5b72b757 --- /dev/null +++ b/tests/keycode_string/test_keycode_string.cpp @@ -0,0 +1,111 @@ +// 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 + +#include "test_common.hpp" + +enum { + MYMACRO1 = SAFE_RANGE, + MYMACRO2, +}; + +// clang-format off +extern "C" const keycode_string_name_t keycode_string_names_kb[] = { + KEYCODE_STRING_NAME(MYMACRO1), + KEYCODE_STRING_NAMES_END // End of table sentinel. +}; +extern "C" const keycode_string_name_t keycode_string_names_user[] = { + KEYCODE_STRING_NAME(MYMACRO2), + KEYCODE_STRING_NAME(KC_EXLM), + KEYCODE_STRING_NAMES_END // End of table sentinel. +}; +// 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({ + {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_RCTL, "KC_RCTL"}, + {KC_RSFT, "KC_RSFT"}, + // F1-F24 keycodes. + {KC_F1, "KC_F1"}, + {KC_F12, "KC_F12"}, + {KC_F13, "KC_F13"}, + {KC_F24, "KC_F24"}, + // 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)"}, + // 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), "MT(0xF,KC_GRV)"}, + // Extrakey keycodes. + {KC_WBAK, "KC_WBAK"}, + {KC_VOLU, "KC_VOLU"}, + // Mouse Key keycodes. + {MS_LEFT, "MS_LEFT"}, + {MS_WHLD, "MS_WHLD"}, + {MS_BTN1, "MS_BTN1"}, + {MS_BTN8, "MS_BTN8"}, + // Custom keycode names. + {MYMACRO1, "MYMACRO1"}, + {MYMACRO2, "MYMACRO2"}, + {KC_EXLM, "KC_EXLM"}, + })) { + EXPECT_EQ(get_keycode_string(keycode), expected) << "where keycode = 0x" << std::hex << keycode; + } +}