mirror of
https://github.com/qmk/qmk_firmware.git
synced 2026-09-22 14:57:58 +08:00
[Core] get_keycode_string(): function to format keycodes as strings, for more readable debug logging. (#24787)
Update API Data / api_data (push) Waiting to run
CI Build Major Branch / Determine concurrency (push) Waiting to run
CI Build Major Branch / Compile keymap default (push) Blocked by required conditions
CI Build Major Branch / Consolidation (push) Blocked by required conditions
CLI CI / test (push) Waiting to run
Update feature branches after develop merge / feature_branch_update (riot) (push) Waiting to run
Update feature branches after develop merge / feature_branch_update (xap) (push) Waiting to run
Lint Format / lint (push) Waiting to run
Regenerate Files / regen (push) Waiting to run
Unit Tests / test (push) Waiting to run
Update API Data / api_data (push) Waiting to run
CI Build Major Branch / Determine concurrency (push) Waiting to run
CI Build Major Branch / Compile keymap default (push) Blocked by required conditions
CI Build Major Branch / Consolidation (push) Blocked by required conditions
CLI CI / test (push) Waiting to run
Update feature branches after develop merge / feature_branch_update (riot) (push) Waiting to run
Update feature branches after develop merge / feature_branch_update (xap) (push) Waiting to run
Lint Format / lint (push) Waiting to run
Regenerate Files / regen (push) Waiting to run
Unit Tests / test (push) Waiting to run
* keycode_string(): Format keycodes as strings. This adds the `keycode_string()` function described in https://getreuer.info/posts/keyboards/keycode-string/index.html as a core feature. * Fix formatting. * keycode_string review revisions. * Rename keycode_string() -> get_keycode_string() for consistency with existing string utils like get_u8_str(). * Revise custom keycode names with separate _user and _kb tables. * Correct indent in builddefs/generic_features.mk. Co-authored-by: Ryan <fauxpark@gmail.com> * Add KC_NUHS, KC_NUBS, and KC_CAPS. * Fix linking error with custom names. * Attempt at simplifying interface. * Formatting fix. * Several fixes and revisions. * Don't use PSTR in KEYCODE_STRING_NAME, since this fails to build on AVR. Store custom names in RAM. * Revise the internal table of common keycode names to use its own storage representation, still in PROGMEM, and now more efficiently stored flat in 8 bytes per entry. * Support Swap Hands keycodes and a few other keycodes. * Revert "Formatting fix." This reverts commit2a2771068c. * Revert "Attempt at simplifying interface." This reverts commit8eaf67de76. * Simplify custom names API by sigprof's suggestion. * Support more keycodes. * Add QK_LOCK keycode. * Add Secure keycodes. * Add Joystick keycodes. * Add Programmable Button keycodes. * Add macro MC_ keycodes. * For remaining keys in known code ranges, stringify them as "QK_<feature>+<number>". For instance, "QK_MIDI+7". * Bug fix and a few improvements. * Fix missing right-hand bit when displaying 5-bit mods numerically. * Support KC_HYPR, KC_MEH, HYPR_T(kc), MEH_T(kc). * Exclude one-shot keycodes when NO_ACTION_ONESHOT is defined. --------- Co-authored-by: Ryan <fauxpark@gmail.com>
This commit is contained in:
@@ -34,6 +34,7 @@ GENERIC_FEATURES = \
|
||||
DYNAMIC_TAPPING_TERM \
|
||||
GRAVE_ESC \
|
||||
HAPTIC \
|
||||
KEYCODE_STRING \
|
||||
KEY_LOCK \
|
||||
KEY_OVERRIDE \
|
||||
LAYER_LOCK \
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user