3993b15f054265071730cdb450f43457dcf4c64a

Author
Pascal Getreuer <50221757+getreuer@users.noreply.github.com>
Committer
GitHub <noreply@github.com>
Date

Message

[Core] Add Repeat Key ("repeat last key") as a core feature. (#19700)

Co-authored-by: casuanoob <96005765+casuanoob@users.noreply.github.com>
Co-authored-by: Sergey Vlasov <sigprof@gmail.com>

Diff

This diff is truncated to protect this page.

   1diff --git a/builddefs/generic_features.mk b/builddefs/generic_features.mk
   2index 5a1ef5c6f0a523ec43e096d9f2b4c58d326f82dc..4e058dcd2659e08d947674d58b8998580a8b1240 100644
   3--- a/builddefs/generic_features.mk
   4+++ b/builddefs/generic_features.mk
   5@@ -32,6 +32,7 @@ GENERIC_FEATURES = \
   6     KEY_OVERRIDE \
   7     LEADER \
   8     PROGRAMMABLE_BUTTON \
   9+    REPEAT_KEY \
  10     SECURE \
  11     SPACE_CADET \
  12     SWAP_HANDS \
  13diff --git a/builddefs/show_options.mk b/builddefs/show_options.mk
  14index 9723b45438a823bd2b28555aeec146b74e114710..8bcc02083bc53e823bc1e5e8a6a9cd2c0ef5d53d 100644
  15--- a/builddefs/show_options.mk
  16+++ b/builddefs/show_options.mk
  17@@ -85,7 +85,8 @@ OTHER_OPTION_NAMES = \
  18   SECURE_ENABLE \
  19   CAPS_WORD_ENABLE \
  20   AUTOCORRECT_ENABLE \
  21-  TRI_LAYER_ENABLE
  22+  TRI_LAYER_ENABLE \
  23+  REPEAT_KEY_ENABLE
  24 
  25 define NAME_ECHO
  26        @printf "  %-30s = %-16s # %s\\n" "$1" "$($1)" "$(origin $1)"
  27diff --git a/data/constants/keycodes/keycodes_0.0.3.hjson b/data/constants/keycodes/keycodes_0.0.3.hjson
  28new file mode 100644
  29index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
  30--- /dev/null
  31+++ b/data/constants/keycodes/keycodes_0.0.3.hjson
  32diff --git a/data/constants/keycodes/keycodes_0.0.3_quantum.hjson b/data/constants/keycodes/keycodes_0.0.3_quantum.hjson
  33new file mode 100644
  34index 0000000000000000000000000000000000000000..23a3c9b06d3bbf0d350ec5b7e50f1081c6542e6b
  35--- /dev/null
  36+++ b/data/constants/keycodes/keycodes_0.0.3_quantum.hjson
  37@@ -0,0 +1,18 @@
  38+{
  39+    "keycodes": {
  40+       "0x7C79": {
  41+            "group": "quantum",
  42+            "key": "QK_REPEAT_KEY",
  43+            "aliases": [
  44+                "QK_REP"
  45+            ]
  46+        },
  47+        "0x7C7A": {
  48+            "group": "quantum",
  49+            "key": "QK_ALT_REPEAT_KEY",
  50+            "aliases": [
  51+                "QK_AREP"
  52+            ]
  53+        }
  54+    }
  55+}
  56diff --git a/docs/_summary.md b/docs/_summary.md
  57index ce579cb0712634508bca24acdb3e5f2ee7c980cb..3d9bde6b1795148559a1f7677e419c7f50160016 100644
  58--- a/docs/_summary.md
  59+++ b/docs/_summary.md
  60@@ -70,6 +70,7 @@
  61     * [Macros](feature_macros.md)
  62     * [Mouse Keys](feature_mouse_keys.md)
  63     * [Programmable Button](feature_programmable_button.md)
  64+    * [Repeat Key](feature_repeat_key.md)
  65     * [Space Cadet Shift](feature_space_cadet.md)
  66     * [US ANSI Shifted Keys](keycodes_us_ansi_shifted.md)
  67 
  68diff --git a/docs/feature_repeat_key.md b/docs/feature_repeat_key.md
  69new file mode 100644
  70index 0000000000000000000000000000000000000000..6fa8a724ef75687e82edfb71cc85adcb14871f09
  71--- /dev/null
  72+++ b/docs/feature_repeat_key.md
  73@@ -0,0 +1,457 @@
  74+# Repeat Key
  75+
  76+The Repeat Key performs the action of the last pressed key. Tapping the Repeat
  77+Key after tapping the <kbd>Z</kbd> key types another "`z`." This is useful for
  78+typing doubled letters, like the `z` in "`dazzle`": a double tap on <kbd>Z</kbd>
  79+can instead be a roll from <kbd>Z</kbd> to <kbd>Repeat</kbd>, which is
  80+potentially faster and more comfortable. The Repeat Key is also useful for
  81+hotkeys, like repeating Ctrl + Shift + Right Arrow to select by word. 
  82+
  83+Repeat Key remembers mods that were active with the last key press. These mods
  84+are combined with any additional mods while pressing the Repeat Key. If the last
  85+press key was <kbd>Ctrl</kbd> + <kbd>Z</kbd>, then <kbd>Shift</kbd> +
  86+<kbd>Repeat</kbd> performs Ctrl + Shift + `Z`.
  87+
  88+## How do I enable Repeat Key
  89+
  90+In your `rules.mk`, add:
  91+
  92+```make
  93+REPEAT_KEY_ENABLE = yes
  94+```
  95+
  96+Then pick a key in your keymap and assign it the keycode `QK_REPEAT_KEY` (short
  97+alias `QK_REP`). Optionally, use the keycode `QK_ALT_REPEAT_KEY` (short alias
  98+`QK_AREP`) on another key.
  99+
 100+## Keycodes
 101+
 102+|Keycode                |Aliases  |Description                          |
 103+|-----------------------|---------|-------------------------------------|
 104+|`QK_REPEAT_KEY`        |`QK_REP` |Repeat the last pressed key          |
 105+|`QK_ALT_REPEAT_KEY`    |`QK_AREP`|Perform alternate of the last key    |
 106+
 107+## Alternate Repeating
 108+
 109+The Alternate Repeat Key performs the "alternate" action of the last pressed key
 110+if it is defined. By default, Alternate Repeat is defined for navigation keys to
 111+act in the reverse direction. When the last key is the common "select by word"
 112+hotkey Ctrl + Shift + Right Arrow, the Alternate Repeat Key performs Ctrl +
 113+Shift + Left Arrow, which together with the Repeat Key enables convenient
 114+selection by words in either direction.
 115+
 116+Alternate Repeat is enabled with the Repeat Key by default. Optionally, to
 117+reduce firmware size, Alternate Repeat may be disabled by adding in config.h:
 118+
 119+```c
 120+#define NO_ALT_REPEAT_KEY
 121+```
 122+
 123+The following alternate keys are defined by default. See
 124+`get_alt_repeat_key_keycode_user()` below for how to change or add to these
 125+definitions. Where it makes sense, these definitions also include combinations 
 126+with mods, like Ctrl + Left &harr; Ctrl + Right Arrow.
 127+
 128+**Navigation** 
 129+
 130+|Keycodes                           |Description                        |
 131+|-----------------------------------|-----------------------------------|
 132+|`KC_LEFT` &harr; `KC_RGHT`         | Left &harr; Right Arrow           |
 133+|`KC_UP` &harr; `KC_DOWN`           | Up &harr; Down Arrow              |
 134+|`KC_HOME` &harr; `KC_END`          | Home &harr; End                   |
 135+|`KC_PGUP` &harr; `KC_PGDN`         | Page Up &harr; Page Down          |
 136+|`KC_MS_L` &harr; `KC_MS_R`         | Mouse Cursor Left &harr; Right    |
 137+|`KC_MS_U` &harr; `KC_MS_D`         | Mouse Cursor Up &harr; Down       |
 138+|`KC_WH_L` &harr; `KC_WH_R`         | Mouse Wheel Left &harr; Right     |
 139+|`KC_WH_U` &harr; `KC_WH_D`         | Mouse Wheel Up &harr; Down        |
 140+
 141+**Misc** 
 142+
 143+|Keycodes                           |Description                        |
 144+|-----------------------------------|-----------------------------------|
 145+|`KC_BSPC` &harr; `KC_DEL`          | Backspace &harr; Delete           |
 146+|`KC_LBRC` &harr; `KC_RBRC`         | `[` &harr; `]`                    |
 147+|`KC_LCBR` &harr; `KC_RCBR`         | `{` &harr; `}`                    |
 148+
 149+**Media** 
 150+
 151+|Keycodes                           |Description                        |
 152+|-----------------------------------|-----------------------------------|
 153+|`KC_WBAK` &harr; `KC_WFWD`         | Browser Back &harr; Forward       |
 154+|`KC_MNXT` &harr; `KC_MPRV`         | Next &harr; Previous Media Track  |
 155+|`KC_MFFD` &harr; `KC_MRWD`         | Fast Forward &harr; Rewind Media  |
 156+|`KC_VOLU` &harr; `KC_VOLD`         | Volume Up &harr; Down             |
 157+|`KC_BRIU` &harr; `KC_BRID`         | Brightness Up &harr; Down         |
 158+
 159+**Hotkeys in Vim, Emacs, and other programs**
 160+
 161+|Keycodes                           |Description                        |
 162+|-----------------------------------|-----------------------------------|
 163+|mod + `KC_F` &harr; mod + `KC_B`   | Forward &harr; Backward           |
 164+|mod + `KC_D` &harr; mod + `KC_U`   | Down &harr; Up                    |
 165+|mod + `KC_N` &harr; mod + `KC_P`   | Next &harr; Previous              |
 166+|mod + `KC_A` &harr; mod + `KC_E`   | Home &harr; End                   |
 167+|mod + `KC_O` &harr; mod + `KC_I`   | Vim jump list Older &harr; Newer  |
 168+|`KC_J` &harr; `KC_K`               | Down &harr; Up                    |
 169+|`KC_H` &harr; `KC_L`               | Left &harr; Right                 |
 170+|`KC_W` &harr; `KC_B`               | Forward &harr; Backward by Word   |
 171+
 172+(where above, "mod" is Ctrl, Alt, or GUI)
 173diff --git a/docs/ja/_summary.md b/docs/ja/_summary.md
 174index e49853bfd435a996a2709faa1994c7e2e764c07d..4d6f2348d5cf138077cc86290d186d4d23a00287 100644
 175--- a/docs/ja/_summary.md
 176+++ b/docs/ja/_summary.md
 177@@ -68,6 +68,7 @@
 178     * [モッドタップ](ja/mod_tap.md)
 179     * [マクロ](ja/feature_macros.md)
 180     * [マウスキー](ja/feature_mouse_keys.md)
 181+    * [Repeat Key](ja/feature_repeat_key.md)
 182     * [Space Cadet Shift](ja/feature_space_cadet.md)
 183     * [US ANSI シフトキー](ja/keycodes_us_ansi_shifted.md)
 184 
 185diff --git a/docs/keycodes.md b/docs/keycodes.md
 186index cad050ccf776d41fc60fe9737b2ada8e2fdb81fc..e5b6246af794ab94efa0f74d92f2a2428c5c81bb 100644
 187--- a/docs/keycodes.md
 188+++ b/docs/keycodes.md
 189@@ -803,6 +803,15 @@ See also: [Programmable Button](feature_programmable_button.md)
 190 |`QK_PROGRAMMABLE_BUTTON_31`|`PB_31`|Programmable button 31|
 191 |`QK_PROGRAMMABLE_BUTTON_32`|`PB_32`|Programmable button 32|
 192 
 193+## Repeat Key :id=repeat-key
 194+
 195+See also: [Repeat Key](feature_repeat_key.md)
 196+
 197+|Keycode                |Aliases  |Description                          |
 198+|-----------------------|---------|-------------------------------------|
 199+|`QK_REPEAT_KEY`        |`QK_REP` |Repeat the last pressed key          |
 200+|`QK_ALT_REPEAT_KEY`    |`QK_AREP`|Perform alternate of the last key    |
 201+
 202 ## Space Cadet :id=space-cadet
 203 
 204 See also: [Space Cadet](feature_space_cadet.md)
 205diff --git a/docs/zh-cn/_summary.md b/docs/zh-cn/_summary.md
 206index b8c26ac2753556b3adbee4d675da99520575a7b6..0fc92e33d3aed463df92c9b1813d8e05b3df3e61 100644
 207--- a/docs/zh-cn/_summary.md
 208+++ b/docs/zh-cn/_summary.md
 209@@ -73,6 +73,7 @@
 210     * [Mod-Tap](zh-cn/mod_tap.md)
 211     * [宏](zh-cn/feature_macros.md)
 212     * [鼠标键](zh-cn/feature_mouse_keys.md)
 213+    * [Repeat Key](zh-cn/feature_repeat_key.md)
 214     * [Space Cadet Shift](zh-cn/feature_space_cadet.md)
 215     * [US ANSI上档键值](zh-cn/keycodes_us_ansi_shifted.md)
 216 
 217diff --git a/quantum/action.c b/quantum/action.c
 218index 59bfefc4954b335d95f191f52a4c3bf8727b24ae..a45e70c5572921e9835ebbfecbf868a708233587 100644
 219--- a/quantum/action.c
 220+++ b/quantum/action.c
 221@@ -285,7 +285,7 @@ void process_record(keyrecord_t *record) {
 222 }
 223 
 224 void process_record_handler(keyrecord_t *record) {
 225-#ifdef COMBO_ENABLE
 226+#if defined(COMBO_ENABLE) || defined(REPEAT_KEY_ENABLE)
 227     action_t action;
 228     if (record->keycode) {
 229         action = action_for_keycode(record->keycode);
 230@@ -1109,7 +1109,7 @@ bool is_tap_record(keyrecord_t *record) {
 231         return false;
 232     }
 233 
 234-#ifdef COMBO_ENABLE
 235+#if defined(COMBO_ENABLE) || defined(REPEAT_KEY_ENABLE)
 236     action_t action;
 237     if (record->keycode) {
 238         action = action_for_keycode(record->keycode);
 239diff --git a/quantum/action.h b/quantum/action.h
 240index 2a2c294c5a9e50acf2e3b77c6828ffdf5ea062a9..d5b15c6f173286f479fe1ce8aac667ea036bd235 100644
 241--- a/quantum/action.h
 242+++ b/quantum/action.h
 243@@ -50,7 +50,7 @@ typedef struct {
 244 #ifndef NO_ACTION_TAPPING
 245     tap_t tap;
 246 #endif
 247-#ifdef COMBO_ENABLE
 248+#if defined(COMBO_ENABLE) || defined(REPEAT_KEY_ENABLE)
 249     uint16_t keycode;
 250 #endif
 251 } keyrecord_t;
 252diff --git a/quantum/keycodes.h b/quantum/keycodes.h
 253index 34b13c29af528f5efe72931ab02ad1160361c0d3..bbf10da36d972ef7110d43ca02e6258dabf3a794 100644
 254--- a/quantum/keycodes.h
 255+++ b/quantum/keycodes.h
 256@@ -721,6 +721,8 @@ enum qk_keycode_defines {
 257     QK_AUTOCORRECT_TOGGLE = 0x7C76,
 258     QK_TRI_LAYER_LOWER = 0x7C77,
 259     QK_TRI_LAYER_UPPER = 0x7C78,
 260+    QK_REPEAT_KEY = 0x7C79,
 261+    QK_ALT_REPEAT_KEY = 0x7C7A,
 262     QK_KB_0 = 0x7E00,
 263     QK_KB_1 = 0x7E01,
 264     QK_KB_2 = 0x7E02,
 265@@ -1362,6 +1364,8 @@ enum qk_keycode_defines {
 266     AC_TOGG    = QK_AUTOCORRECT_TOGGLE,
 267     TL_LOWR    = QK_TRI_LAYER_LOWER,
 268     TL_UPPR    = QK_TRI_LAYER_UPPER,
 269+    QK_REP     = QK_REPEAT_KEY,
 270+    QK_AREP    = QK_ALT_REPEAT_KEY,
 271 };
 272 
 273 // Range Helpers
 274@@ -1413,6 +1417,6 @@ enum qk_keycode_defines {
 275 #define IS_MACRO_KEYCODE(code) ((code) >= QK_MACRO_0 && (code) <= QK_MACRO_31)
 276 #define IS_BACKLIGHT_KEYCODE(code) ((code) >= QK_BACKLIGHT_ON && (code) <= QK_BACKLIGHT_TOGGLE_BREATHING)
 277 #define IS_RGB_KEYCODE(code) ((code) >= RGB_TOG && (code) <= RGB_MODE_TWINKLE)
 278-#define IS_QUANTUM_KEYCODE(code) ((code) >= QK_BOOTLOADER && (code) <= QK_TRI_LAYER_UPPER)
 279+#define IS_QUANTUM_KEYCODE(code) ((code) >= QK_BOOTLOADER && (code) <= QK_ALT_REPEAT_KEY)
 280 #define IS_KB_KEYCODE(code) ((code) >= QK_KB_0 && (code) <= QK_KB_31)
 281 #define IS_USER_KEYCODE(code) ((code) >= QK_USER_0 && (code) <= QK_USER_31)
 282diff --git a/quantum/process_keycode/process_repeat_key.c b/quantum/process_keycode/process_repeat_key.c
 283new file mode 100644
 284index 0000000000000000000000000000000000000000..f819aa226e910b6089ce3be2790c8f2afb8f5438
 285--- /dev/null
 286+++ b/quantum/process_keycode/process_repeat_key.c
 287@@ -0,0 +1,109 @@
 288+// Copyright 2022-2023 Google LLC
 289+//
 290+// Licensed under the Apache License, Version 2.0 (the "License");
 291+// you may not use this file except in compliance with the License.
 292+// You may obtain a copy of the License at
 293+//
 294+//     https://www.apache.org/licenses/LICENSE-2.0
 295+//
 296+// Unless required by applicable law or agreed to in writing, software
 297+// distributed under the License is distributed on an "AS IS" BASIS,
 298+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 299+// See the License for the specific language governing permissions and
 300+// limitations under the License.
 301+
 302+#include "process_repeat_key.h"
 303+
 304+// Default implementation of remember_last_key_user().
 305+__attribute__((weak)) bool remember_last_key_user(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) {
 306+    return true;
 307+}
 308+
 309+static bool remember_last_key(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) {
 310+    switch (keycode) {
 311+        // Ignore MO, TO, TG, TT, and TL layer switch keys.
 312+        case QK_MOMENTARY ... QK_MOMENTARY_MAX:
 313+        case QK_TO ... QK_TO_MAX:
 314+        case QK_TOGGLE_LAYER ... QK_TOGGLE_LAYER_MAX:
 315+        case QK_LAYER_TAP_TOGGLE ... QK_LAYER_TAP_TOGGLE_MAX:
 316+        // Ignore mod keys.
 317+        case KC_LCTL ... KC_RGUI:
 318+        case KC_HYPR:
 319+        case KC_MEH:
 320+#ifndef NO_ACTION_ONESHOT // Ignore one-shot keys.
 321+        case QK_ONE_SHOT_LAYER ... QK_ONE_SHOT_LAYER_MAX:
 322+        case QK_ONE_SHOT_MOD ... QK_ONE_SHOT_MOD_MAX:
 323+#endif                  // NO_ACTION_ONESHOT
 324+#ifdef TRI_LAYER_ENABLE // Ignore Tri Layer keys.
 325+        case QK_TRI_LAYER_LOWER:
 326+        case QK_TRI_LAYER_UPPER:
 327+#endif // TRI_LAYER_ENABLE
 328+            return false;
 329+
 330+            // Ignore hold events on tap-hold keys.
 331+#ifndef NO_ACTION_TAPPING
 332+        case QK_MOD_TAP ... QK_MOD_TAP_MAX:
 333+#    ifndef NO_ACTION_LAYER
 334+        case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
 335+#    endif // NO_ACTION_LAYER
 336+            if (record->tap.count == 0) {
 337+                return false;
 338+            }
 339+            break;
 340+#endif // NO_ACTION_TAPPING
 341+
 342+#ifdef SWAP_HANDS_ENABLE
 343+        case QK_SWAP_HANDS ... QK_SWAP_HANDS_MAX:
 344+            if (IS_SWAP_HANDS_KEYCODE(keycode) || record->tap.count == 0) {
 345+                return false;
 346+            }
 347+            break;
 348+#endif // SWAP_HANDS_ENABLE
 349+
 350+        case QK_REPEAT_KEY:
 351+#ifndef NO_ALT_REPEAT_KEY
 352+        case QK_ALT_REPEAT_KEY:
 353+#endif // NO_ALT_REPEAT_KEY
 354+            return false;
 355+    }
 356+
 357+    return remember_last_key_user(keycode, record, remembered_mods);
 358+}
 359+
 360+bool process_last_key(uint16_t keycode, keyrecord_t* record) {
 361+    if (get_repeat_key_count()) {
 362+        return true;
 363+    }
 364+
 365+    if (record->event.pressed) {
 366+        uint8_t remembered_mods = get_mods() | get_weak_mods();
 367+#ifndef NO_ACTION_ONESHOT
 368+        remembered_mods |= get_oneshot_mods();
 369+#endif // NO_ACTION_ONESHOT
 370+
 371+        if (remember_last_key(keycode, record, &remembered_mods)) {
 372+            set_last_record(keycode, record);
 373+            set_last_mods(remembered_mods);
 374+        }
 375+    }
 376+
 377+    return true;
 378+}
 379+
 380+bool process_repeat_key(uint16_t keycode, keyrecord_t* record) {
 381+    if (get_repeat_key_count()) {
 382+        return true;
 383+    }
 384+
 385+    if (keycode == QK_REPEAT_KEY) {
 386+        repeat_key_invoke(&record->event);
 387diff --git a/quantum/process_keycode/process_repeat_key.h b/quantum/process_keycode/process_repeat_key.h
 388new file mode 100644
 389index 0000000000000000000000000000000000000000..eddc50f254c99e470653631ee2272f289127dbf0
 390--- /dev/null
 391+++ b/quantum/process_keycode/process_repeat_key.h
 392@@ -0,0 +1,62 @@
 393+// Copyright 2022-2023 Google LLC
 394+//
 395+// Licensed under the Apache License, Version 2.0 (the "License");
 396+// you may not use this file except in compliance with the License.
 397+// You may obtain a copy of the License at
 398+//
 399+//     https://www.apache.org/licenses/LICENSE-2.0
 400+//
 401+// Unless required by applicable law or agreed to in writing, software
 402+// distributed under the License is distributed on an "AS IS" BASIS,
 403+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 404+// See the License for the specific language governing permissions and
 405+// limitations under the License.
 406+
 407+#pragma once
 408+
 409+#include "quantum.h"
 410+
 411+/**
 412+ * @brief Process handler for remembering the last key.
 413+ *
 414+ * @param keycode  Keycode registered by matrix press, per keymap
 415+ * @param record   keyrecord_t structure
 416+ * @return true    Continue processing keycodes, and send to host
 417+ * @return false   Stop processing keycodes, and don't send to host
 418+ */
 419+bool process_last_key(uint16_t keycode, keyrecord_t* record);
 420+
 421+/**
 422+ * @brief Optional callback defining which keys are remembered.
 423+ *
 424+ * @param keycode          Keycode that was just pressed
 425+ * @param record           keyrecord_t structure
 426+ * @param remembered_mods  Mods that will be remembered with this key
 427+ * @return true            Key is remembered
 428+ * @return false           Key is ignored
 429+ *
 430+ * Modifier and layer switch keys are always ignored. For all other keys, this
 431+ * callback is called on every key press. Returning true means that the key is
 432+ * remembered, false means it is ignored. By default, all non-modifier,
 433+ * non-layer switch keys are remembered.
 434+ *
 435+ * The `remembered_mods` arg represents the mods that will be remembered with
 436+ * this key. It can be modified to forget certain mods, for instance to forget
 437+ * capitalization when repeating shifted letters:
 438+ *
 439+ *     // Forget Shift on letter keys.
 440+ *     if (KC_A <= keycode && keycode <= KC_Z && (*remembered_mods & ~MOD_MASK_SHIFT) == 0) {
 441+ *         *remembered_mods = 0;
 442+ *     }
 443+ */
 444+bool remember_last_key_user(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods);
 445+
 446+/**
 447+ * @brief Process handler for Repeat Key feature.
 448+ *
 449+ * @param keycode  Keycode registered by matrix press, per keymap
 450+ * @param record   keyrecord_t structure
 451+ * @return true    Continue processing keycodes, and send to host
 452+ * @return false   Stop processing keycodes, and don't send to host
 453+ */
 454+bool process_repeat_key(uint16_t keycode, keyrecord_t* record);
 455diff --git a/quantum/quantum.c b/quantum/quantum.c
 456index fdc24fa2d0b9cf00a11753216a2ece7c9a094628..091cf298f73fa87562b694c438e4e64b45e5f668 100644
 457--- a/quantum/quantum.c
 458+++ b/quantum/quantum.c
 459@@ -176,7 +176,7 @@ void soft_reset_keyboard(void) {
 460 
 461 /* Convert record into usable keycode via the contained event. */
 462 uint16_t get_record_keycode(keyrecord_t *record, bool update_layer_cache) {
 463-#ifdef COMBO_ENABLE
 464+#if defined(COMBO_ENABLE) || defined(REPEAT_KEY_ENABLE)
 465     if (record->keycode) {
 466         return record->keycode;
 467     }
 468@@ -273,6 +273,9 @@ bool process_record_quantum(keyrecord_t *record) {
 469             // Must run asap to ensure all keypresses are recorded.
 470             process_dynamic_macro(keycode, record) &&
 471 #endif
 472+#ifdef REPEAT_KEY_ENABLE
 473+            process_last_key(keycode, record) && process_repeat_key(keycode, record) &&
 474+#endif
 475 #if defined(AUDIO_ENABLE) && defined(AUDIO_CLICKY)
 476             process_clicky(keycode, record) &&
 477 #endif
 478diff --git a/quantum/quantum.h b/quantum/quantum.h
 479index fec92a5244a5bfdcd5c1907305802083045824e0..31a1a63a7acb1483ec807efe20e34ac628a7dd74 100644
 480--- a/quantum/quantum.h
 481+++ b/quantum/quantum.h
 482@@ -251,6 +251,11 @@ extern layer_state_t layer_state;
 483 #    include "process_tri_layer.h"
 484 #endif
 485 
 486+#ifdef REPEAT_KEY_ENABLE
 487+#    include "repeat_key.h"
 488+#    include "process_repeat_key.h"
 489+#endif
 490+
 491 void set_single_persistent_default_layer(uint8_t default_layer);
 492 
 493 #define IS_LAYER_ON(layer) layer_state_is(layer)
 494diff --git a/quantum/repeat_key.c b/quantum/repeat_key.c
 495new file mode 100644
 496index 0000000000000000000000000000000000000000..0689c6d45476d3f49b92cde5996488bc587a2751
 497--- /dev/null
 498+++ b/quantum/repeat_key.c
 499@@ -0,0 +1,282 @@
 500+// Copyright 2022-2023 Google LLC
 501+//
 502+// Licensed under the Apache License, Version 2.0 (the "License");
 503+// you may not use this file except in compliance with the License.
 504+// You may obtain a copy of the License at
 505+//
 506+//     https://www.apache.org/licenses/LICENSE-2.0
 507+//
 508+// Unless required by applicable law or agreed to in writing, software
 509+// distributed under the License is distributed on an "AS IS" BASIS,
 510+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 511+// See the License for the specific language governing permissions and
 512+// limitations under the License.
 513+
 514+#include "repeat_key.h"
 515+
 516+// Variables saving the state of the last key press.
 517+static keyrecord_t last_record = {0};
 518+static uint8_t     last_mods   = 0;
 519+// Signed count of the number of times the last key has been repeated or
 520+// alternate repeated: it is 0 when a key is pressed normally, positive when
 521+// repeated, and negative when alternate repeated.
 522+static int8_t last_repeat_count = 0;
 523+// The repeat_count, but set to 0 outside of repeat_key_invoke() so that it is
 524+// nonzero only while a repeated key is being processed.
 525+static int8_t processing_repeat_count = 0;
 526+
 527+uint16_t get_last_keycode(void) {
 528+    return last_record.keycode;
 529+}
 530+
 531+uint8_t get_last_mods(void) {
 532+    return last_mods;
 533+}
 534+
 535+void set_last_keycode(uint16_t keycode) {
 536+    set_last_record(keycode, &(keyrecord_t){
 537+#ifndef NO_ACTION_TAPPING
 538+                                 .tap.interrupted = false,
 539+                                 .tap.count       = 1,
 540+#endif
 541+                             });
 542+}
 543+
 544+void set_last_mods(uint8_t mods) {
 545+    last_mods = mods;
 546+}
 547+
 548+void set_last_record(uint16_t keycode, keyrecord_t* record) {
 549+    last_record         = *record;
 550+    last_record.keycode = keycode;
 551+    last_repeat_count   = 0;
 552+}
 553+
 554+/** @brief Updates `last_repeat_count` in direction `dir`. */
 555+static void update_last_repeat_count(int8_t dir) {
 556+    if (dir * last_repeat_count < 0) {
 557+        last_repeat_count = dir;
 558+    } else if (dir * last_repeat_count < 127) {
 559+        last_repeat_count += dir;
 560+    }
 561+}
 562+
 563+int8_t get_repeat_key_count(void) {
 564+    return processing_repeat_count;
 565+}
 566+
 567+void repeat_key_invoke(const keyevent_t* event) {
 568+    // It is possible (e.g. in rolled presses) that the last key changes while
 569+    // the Repeat Key is pressed. To prevent stuck keys, it is important to
 570+    // remember separately what key record was processed on press so that the
 571+    // the corresponding record is generated on release.
 572+    static keyrecord_t registered_record       = {0};
 573+    static int8_t      registered_repeat_count = 0;
 574+    // Since this function calls process_record(), it may recursively call
 575+    // itself. We return early if `processing_repeat_count` is nonzero to
 576+    // prevent infinite recursion.
 577+    if (processing_repeat_count || !last_record.keycode) {
 578+        return;
 579+    }
 580+
 581+    if (event->pressed) {
 582+        update_last_repeat_count(1);
 583+        // On press, apply the last mods state, stacking on top of current mods.
 584+        register_weak_mods(last_mods);
 585+        registered_record       = last_record;
 586+        registered_repeat_count = last_repeat_count;
 587+    }
 588+
 589+    // Generate a keyrecord and plumb it into the event pipeline.
 590+    registered_record.event = *event;
 591+    processing_repeat_count = registered_repeat_count;
 592+    process_record(&registered_record);
 593+    processing_repeat_count = 0;
 594+
 595+    // On release, restore the mods state.
 596+    if (!event->pressed) {
 597+        unregister_weak_mods(last_mods);
 598+    }
 599diff --git a/quantum/repeat_key.h b/quantum/repeat_key.h
 600new file mode 100644
 601index 0000000000000000000000000000000000000000..06e83645295cd083fc75ff539863a43a1f729039
 602--- /dev/null
 603+++ b/quantum/repeat_key.h
 604@@ -0,0 +1,80 @@
 605+// Copyright 2022-2023 Google LLC
 606+//
 607+// Licensed under the Apache License, Version 2.0 (the "License");
 608+// you may not use this file except in compliance with the License.
 609+// You may obtain a copy of the License at
 610+//
 611+//     https://www.apache.org/licenses/LICENSE-2.0
 612+//
 613+// Unless required by applicable law or agreed to in writing, software
 614+// distributed under the License is distributed on an "AS IS" BASIS,
 615+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 616+// See the License for the specific language governing permissions and
 617+// limitations under the License.
 618+
 619+#pragma once
 620+
 621+#include "quantum.h"
 622+
 623+uint16_t get_last_keycode(void);             /**< Keycode of the last key. */
 624+uint8_t  get_last_mods(void);                /**< Mods active with the last key. */
 625+void     set_last_keycode(uint16_t keycode); /**< Sets the last key. */
 626+void     set_last_mods(uint8_t mods);        /**< Sets the last mods. */
 627+
 628+/** @brief Gets the record for the last key. */
 629+keyrecord_t* get_last_record(void);
 630+
 631+/** @brief Sets keycode and record info for the last key. */
 632+void set_last_record(uint16_t keycode, keyrecord_t* record);
 633+
 634+/**
 635+ * @brief Signed count of times the key has been repeated or alternate repeated.
 636+ *
 637+ * @note The count is nonzero only while a repeated or alternate-repeated key is
 638+ *       being processed.
 639+ *
 640+ * When a key is pressed normally, the count is 0. When the Repeat Key is used
 641+ * to repeat a key, the count is 1 on the first repeat, 2 on the second repeat,
 642+ * and continuing up to 127.
 643+ *
 644+ * Negative counts are used similarly for alternate repeating. When the
 645+ * Alternate Repeat Key is used, the count is -1 on the first alternate repeat,
 646+ * -2 on the second, continuing down to -127.
 647+ */
 648+int8_t get_repeat_key_count(void);
 649+
 650+/**
 651+ * @brief Calls `process_record()` on a generated record repeating the last key.
 652+ * @param event Event information in the generated record.
 653+ */
 654+void repeat_key_invoke(const keyevent_t* event);
 655+
 656+#ifndef NO_ALT_REPEAT_KEY
 657+
 658+/**
 659+ * @brief Keycode to be used for alternate repeating.
 660+ *
 661+ * Alternate Repeat performs this keycode based on the last eligible pressed key
 662+ * and mods, get_last_keycode() and get_last_mods(). For example, when the last
 663+ * key was KC_UP, this function returns KC_DOWN. The function returns KC_NO if
 664+ * the last key doesn't have a defined alternate.
 665+ */
 666+uint16_t get_alt_repeat_key_keycode(void);
 667+
 668+/**
 669+ * @brief Calls `process_record()` to alternate repeat the last key.
 670+ * @param event Event information in the generated record.
 671+ */
 672+void alt_repeat_key_invoke(const keyevent_t* event);
 673+
 674+/**
 675+ * @brief Optional user callback to define additional alternate keys.
 676+ *
 677+ * When `get_alt_repeat_key_keycode()` is called, it first calls this callback.
 678+ * It should return a keycode representing the "alternate" of the given keycode
 679+ * and mods. Returning KC_NO defers to the default definitions in
 680+ * `get_alt_repeat_key_keycode()`.
 681+ */
 682+uint16_t get_alt_repeat_key_keycode_user(uint16_t keycode, uint8_t mods);
 683+
 684+#endif // NO_ALT_REPEAT_KEY
 685diff --git a/tests/repeat_key/alt_repeat_key/config.h b/tests/repeat_key/alt_repeat_key/config.h
 686new file mode 100644
 687index 0000000000000000000000000000000000000000..d0c4ddadbd3b0a08a7ceb9b64b9616399c3825e3
 688--- /dev/null
 689+++ b/tests/repeat_key/alt_repeat_key/config.h
 690@@ -0,0 +1,18 @@
 691+// Copyright 2023 Google LLC
 692+//
 693+// This program is free software: you can redistribute it and/or modify
 694+// it under the terms of the GNU General Public License as published by
 695+// the Free Software Foundation, either version 2 of the License, or
 696+// (at your option) any later version.
 697+//
 698+// This program is distributed in the hope that it will be useful,
 699+// but WITHOUT ANY WARRANTY; without even the implied warranty of
 700+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 701+// GNU General Public License for more details.
 702+//
 703+// You should have received a copy of the GNU General Public License
 704+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 705+
 706+#pragma once
 707+
 708+#include "test_common.h"
 709diff --git a/tests/repeat_key/alt_repeat_key/test.mk b/tests/repeat_key/alt_repeat_key/test.mk
 710new file mode 100644
 711index 0000000000000000000000000000000000000000..080871c816453ff0f7c1c3bfeeb2bb503a23a139
 712--- /dev/null
 713+++ b/tests/repeat_key/alt_repeat_key/test.mk
 714@@ -0,0 +1,18 @@
 715+# Copyright 2023 Google LLC
 716+#
 717+# This program is free software: you can redistribute it and/or modify
 718+# it under the terms of the GNU General Public License as published by
 719+# the Free Software Foundation, either version 2 of the License, or
 720+# (at your option) any later version.
 721+#
 722+# This program is distributed in the hope that it will be useful,
 723+# but WITHOUT ANY WARRANTY; without even the implied warranty of
 724+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 725+# GNU General Public License for more details.
 726+#
 727+# You should have received a copy of the GNU General Public License
 728+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 729+
 730+REPEAT_KEY_ENABLE = yes
 731+
 732+EXTRAKEY_ENABLE = yes
 733diff --git a/tests/repeat_key/alt_repeat_key/test_alt_repeat_key.cpp b/tests/repeat_key/alt_repeat_key/test_alt_repeat_key.cpp
 734new file mode 100644
 735index 0000000000000000000000000000000000000000..ae525acb4554f9e733a7b412d44327d724aca613
 736--- /dev/null
 737+++ b/tests/repeat_key/alt_repeat_key/test_alt_repeat_key.cpp
 738@@ -0,0 +1,523 @@
 739+// Copyright 2023 Google LLC
 740+//
 741+// This program is free software: you can redistribute it and/or modify
 742+// it under the terms of the GNU General Public License as published by
 743+// the Free Software Foundation, either version 2 of the License, or
 744+// (at your option) any later version.
 745+//
 746+// This program is distributed in the hope that it will be useful,
 747+// but WITHOUT ANY WARRANTY; without even the implied warranty of
 748+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 749+// GNU General Public License for more details.
 750+//
 751+// You should have received a copy of the GNU General Public License
 752+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 753+
 754+#include <functional>
 755+
 756+#include "keyboard_report_util.hpp"
 757+#include "keycode.h"
 758+#include "test_common.hpp"
 759+#include "test_fixture.hpp"
 760+#include "test_keymap_key.hpp"
 761+
 762+using ::testing::AnyNumber;
 763+using ::testing::InSequence;
 764+
 765+namespace {
 766+
 767+bool process_record_user_default(uint16_t keycode, keyrecord_t* record) {
 768+    return true;
 769+}
 770+
 771+bool remember_last_key_user_default(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) {
 772+    return true;
 773+}
 774+
 775+uint16_t get_alt_repeat_key_keycode_user_default(uint16_t keycode, uint8_t mods) {
 776+    return KC_TRNS;
 777+}
 778+
 779+// Indirections so that process_record_user() can be replaced with other
 780+// functions in the test cases below.
 781+std::function<bool(uint16_t, keyrecord_t*)>           process_record_user_fun             = process_record_user_default;
 782+std::function<bool(uint16_t, keyrecord_t*, uint8_t*)> remember_last_key_user_fun          = remember_last_key_user_default;
 783+std::function<uint16_t(uint16_t, uint8_t)>            get_alt_repeat_key_keycode_user_fun = get_alt_repeat_key_keycode_user_default;
 784+
 785+extern "C" bool process_record_user(uint16_t keycode, keyrecord_t* record) {
 786+    return process_record_user_fun(keycode, record);
 787+}
 788+
 789+extern "C" bool remember_last_key_user(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) {
 790+    return remember_last_key_user_fun(keycode, record, remembered_mods);
 791+}
 792+
 793+extern "C" uint16_t get_alt_repeat_key_keycode_user(uint16_t keycode, uint8_t mods) {
 794+    return get_alt_repeat_key_keycode_user_fun(keycode, mods);
 795+}
 796+
 797+class AltRepeatKey : public TestFixture {
 798+   public:
 799+    bool process_record_user_was_called_;
 800+
 801+    void SetUp() override {
 802+        process_record_user_fun             = process_record_user_default;
 803+        remember_last_key_user_fun          = remember_last_key_user_default;
 804+        get_alt_repeat_key_keycode_user_fun = get_alt_repeat_key_keycode_user_default;
 805+    }
 806+
 807+    void ExpectProcessRecordUserCalledWith(bool expected_press, uint16_t expected_keycode, int8_t expected_repeat_key_count) {
 808+        process_record_user_was_called_ = false;
 809+        process_record_user_fun         = [=](uint16_t keycode, keyrecord_t* record) {
 810+            EXPECT_EQ(record->event.pressed, expected_press);
 811+            EXPECT_KEYCODE_EQ(keycode, expected_keycode);
 812+            EXPECT_EQ(get_repeat_key_count(), expected_repeat_key_count);
 813+            // Tests below use this to verify process_record_user() was called.
 814+            process_record_user_was_called_ = true;
 815+            return true;
 816+        };
 817+    }
 818+
 819+    // Expects that the characters of `s` are sent.
 820+    // NOTE: This implementation is limited to chars a-z, A-Z.
 821+    void ExpectString(TestDriver& driver, const std::string& s) {
 822+        InSequence seq;
 823+        for (int c : s) {
 824+            switch (c) {
 825+                case 'a' ... 'z': { // Lowercase letter.
 826+                    uint16_t keycode = c - ('a' - KC_A);
 827+                    EXPECT_REPORT(driver, (keycode));
 828+                } break;
 829+
 830+                case 'A' ... 'Z': { // Capital letter = KC_LSFT + letter key.
 831+                    uint16_t keycode = c - ('A' - KC_A);
 832+                    EXPECT_REPORT(driver, (KC_LSFT, keycode));
 833+                } break;
 834+            }
 835+        }
 836+    }
 837+};
 838diff --git a/tests/repeat_key/config.h b/tests/repeat_key/config.h
 839new file mode 100644
 840index 0000000000000000000000000000000000000000..003d980c824b55ccac311858f6c26c24564c97cc
 841--- /dev/null
 842+++ b/tests/repeat_key/config.h
 843@@ -0,0 +1,20 @@
 844+// Copyright 2023 Google LLC
 845+//
 846+// This program is free software: you can redistribute it and/or modify
 847+// it under the terms of the GNU General Public License as published by
 848+// the Free Software Foundation, either version 2 of the License, or
 849+// (at your option) any later version.
 850+//
 851+// This program is distributed in the hope that it will be useful,
 852+// but WITHOUT ANY WARRANTY; without even the implied warranty of
 853+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 854+// GNU General Public License for more details.
 855+//
 856+// You should have received a copy of the GNU General Public License
 857+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 858+
 859+#pragma once
 860+
 861+#include "test_common.h"
 862+
 863+#define NO_ALT_REPEAT_KEY
 864diff --git a/tests/repeat_key/repeat_key_combo/config.h b/tests/repeat_key/repeat_key_combo/config.h
 865new file mode 100644
 866index 0000000000000000000000000000000000000000..d0c4ddadbd3b0a08a7ceb9b64b9616399c3825e3
 867--- /dev/null
 868+++ b/tests/repeat_key/repeat_key_combo/config.h
 869@@ -0,0 +1,18 @@
 870+// Copyright 2023 Google LLC
 871+//
 872+// This program is free software: you can redistribute it and/or modify
 873+// it under the terms of the GNU General Public License as published by
 874+// the Free Software Foundation, either version 2 of the License, or
 875+// (at your option) any later version.
 876+//
 877+// This program is distributed in the hope that it will be useful,
 878+// but WITHOUT ANY WARRANTY; without even the implied warranty of
 879+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 880+// GNU General Public License for more details.
 881+//
 882+// You should have received a copy of the GNU General Public License
 883+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 884+
 885+#pragma once
 886+
 887+#include "test_common.h"
 888diff --git a/tests/repeat_key/repeat_key_combo/test.mk b/tests/repeat_key/repeat_key_combo/test.mk
 889new file mode 100644
 890index 0000000000000000000000000000000000000000..db6ea7789a2d29354e1eec2b1f2ebd31d8158827
 891--- /dev/null
 892+++ b/tests/repeat_key/repeat_key_combo/test.mk
 893@@ -0,0 +1,18 @@
 894+# Copyright 2023 Google LLC
 895+#
 896+# This program is free software: you can redistribute it and/or modify
 897+# it under the terms of the GNU General Public License as published by
 898+# the Free Software Foundation, either version 2 of the License, or
 899+# (at your option) any later version.
 900+#
 901+# This program is distributed in the hope that it will be useful,
 902+# but WITHOUT ANY WARRANTY; without even the implied warranty of
 903+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 904+# GNU General Public License for more details.
 905+#
 906+# You should have received a copy of the GNU General Public License
 907+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 908+
 909+REPEAT_KEY_ENABLE = yes
 910+
 911+COMBO_ENABLE = yes
 912diff --git a/tests/repeat_key/repeat_key_combo/test_repeat_key_combo.cpp b/tests/repeat_key/repeat_key_combo/test_repeat_key_combo.cpp
 913new file mode 100644
 914index 0000000000000000000000000000000000000000..2d2fbaa9665a468c05ffdb722fd11b1558f50f6a
 915--- /dev/null
 916+++ b/tests/repeat_key/repeat_key_combo/test_repeat_key_combo.cpp
 917@@ -0,0 +1,67 @@
 918+// Copyright 2023 Google LLC
 919+//
 920+// This program is free software: you can redistribute it and/or modify
 921+// it under the terms of the GNU General Public License as published by
 922+// the Free Software Foundation, either version 2 of the License, or
 923+// (at your option) any later version.
 924+//
 925+// This program is distributed in the hope that it will be useful,
 926+// but WITHOUT ANY WARRANTY; without even the implied warranty of
 927+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 928+// GNU General Public License for more details.
 929+//
 930+// You should have received a copy of the GNU General Public License
 931+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 932+
 933+#include "keyboard_report_util.hpp"
 934+#include "keycode.h"
 935+#include "test_common.hpp"
 936+#include "test_fixture.hpp"
 937+#include "test_keymap_key.hpp"
 938+
 939+using ::testing::AnyNumber;
 940+using ::testing::InSequence;
 941+
 942+namespace {
 943+
 944+extern "C" {
 945+// Define a combo: KC_X + KC_Y = KC_Q.
 946+const uint16_t xy_combo[] PROGMEM = {KC_X, KC_Y, COMBO_END};
 947+combo_t        key_combos[]       = {COMBO(xy_combo, KC_Q)};
 948+uint16_t       COMBO_LEN          = sizeof(key_combos) / sizeof(*key_combos);
 949+} // extern "C"
 950+
 951+class RepeatKey : public TestFixture {};
 952+
 953+// Tests repeating a combo, KC_X + KC_Y = KC_Q, by typing
 954+// "X, Repeat, Repeat, {X Y}, Repeat, Repeat". This produces "xxxqqq".
 955+TEST_F(RepeatKey, Combo) {
 956+    TestDriver driver;
 957+    KeymapKey  key_x(0, 0, 0, KC_X);
 958+    KeymapKey  key_y(0, 1, 0, KC_Y);
 959+    KeymapKey  key_repeat(0, 2, 0, QK_REP);
 960+    set_keymap({key_x, key_y, key_repeat});
 961+
 962+    // Allow any number of empty reports.
 963+    EXPECT_EMPTY_REPORT(driver).Times(AnyNumber());
 964+    {
 965+        InSequence seq;
 966+        EXPECT_REPORT(driver, (KC_X));
 967+        EXPECT_REPORT(driver, (KC_X));
 968+        EXPECT_REPORT(driver, (KC_X));
 969+        EXPECT_REPORT(driver, (KC_Q));
 970+        EXPECT_REPORT(driver, (KC_Q));
 971+        EXPECT_REPORT(driver, (KC_Q));
 972+    }
 973+
 974+    tap_keys(key_x, key_repeat, key_repeat);
 975+    tap_combo({key_x, key_y});
 976+
 977+    EXPECT_KEYCODE_EQ(get_last_keycode(), KC_Q);
 978+
 979+    tap_keys(key_repeat, key_repeat);
 980+
 981+    testing::Mock::VerifyAndClearExpectations(&driver);
 982+}
 983+
 984+} // namespace
 985diff --git a/tests/repeat_key/test.mk b/tests/repeat_key/test.mk
 986new file mode 100644
 987index 0000000000000000000000000000000000000000..aec8ff3bfb890fef659f244274c2a28613aaf0f8
 988--- /dev/null
 989+++ b/tests/repeat_key/test.mk
 990@@ -0,0 +1,18 @@
 991+# Copyright 2023 Google LLC
 992+#
 993+# This program is free software: you can redistribute it and/or modify
 994+# it under the terms of the GNU General Public License as published by
 995+# the Free Software Foundation, either version 2 of the License, or
 996+# (at your option) any later version.
 997+#
 998+# This program is distributed in the hope that it will be useful,
 999+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1000+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
1001+# GNU General Public License for more details.
1002+#
1003+# You should have received a copy of the GNU General Public License
1004+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
1005+
1006+REPEAT_KEY_ENABLE = yes
1007+
1008+AUTO_SHIFT_ENABLE = yes
1009diff --git a/tests/repeat_key/test_repeat_key.cpp b/tests/repeat_key/test_repeat_key.cpp
1010new file mode 100644
1011index 0000000000000000000000000000000000000000..eee44fc104481039c69020700188cf5976426767
1012--- /dev/null
1013+++ b/tests/repeat_key/test_repeat_key.cpp
1014@@ -0,0 +1,754 @@
1015+// Copyright 2023 Google LLC
1016+//
1017+// This program is free software: you can redistribute it and/or modify
1018+// it under the terms of the GNU General Public License as published by
1019+// the Free Software Foundation, either version 2 of the License, or
1020+// (at your option) any later version.
1021+//
1022+// This program is distributed in the hope that it will be useful,
1023+// but WITHOUT ANY WARRANTY; without even the implied warranty of
1024+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
1025+// GNU General Public License for more details.
1026+//
1027+// You should have received a copy of the GNU General Public License
1028+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
1029+
1030+#include <functional>
1031+
1032+#include "keyboard_report_util.hpp"
1033+#include "keycode.h"
1034+#include "test_common.hpp"
1035+#include "test_fixture.hpp"
1036+#include "test_keymap_key.hpp"
1037+
1038+using ::testing::AnyNumber;
1039+using ::testing::AnyOf;
1040+using ::testing::InSequence;
1041+
1042+#define FOO_MACRO SAFE_RANGE
1043+
1044+namespace {
1045+
1046+bool process_record_user_default(uint16_t keycode, keyrecord_t* record) {
1047+    return true;
1048+}
1049+
1050+bool remember_last_key_user_default(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) {
1051+    return true;
1052+}
1053+
1054+// Indirection so that process_record_user() and remember_last_key_user()
1055+// can be replaced with other functions in the test cases below.
1056+std::function<bool(uint16_t, keyrecord_t*)>           process_record_user_fun    = process_record_user_default;
1057+std::function<bool(uint16_t, keyrecord_t*, uint8_t*)> remember_last_key_user_fun = remember_last_key_user_default;
1058+
1059+extern "C" bool process_record_user(uint16_t keycode, keyrecord_t* record) {
1060+    return process_record_user_fun(keycode, record);
1061+}
1062+extern "C" bool remember_last_key_user(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) {
1063+    return remember_last_key_user_fun(keycode, record, remembered_mods);
1064+}
1065+
1066+class RepeatKey : public TestFixture {
1067+   public:
1068+    bool process_record_user_was_called_;
1069+
1070+    void SetUp() override {
1071+        autoshift_disable();
1072+        process_record_user_fun    = process_record_user_default;
1073+        remember_last_key_user_fun = remember_last_key_user_default;
1074+    }
1075+
1076+    void ExpectProcessRecordUserCalledWith(bool expected_press, uint16_t expected_keycode, int8_t expected_repeat_key_count) {
1077+        process_record_user_was_called_ = false;
1078+        process_record_user_fun         = [=](uint16_t keycode, keyrecord_t* record) {
1079+            EXPECT_EQ(record->event.pressed, expected_press);
1080+            EXPECT_KEYCODE_EQ(keycode, expected_keycode);
1081+            EXPECT_EQ(get_repeat_key_count(), expected_repeat_key_count);
1082+            // Tests below use this to verify process_record_user() was called.
1083+            process_record_user_was_called_ = true;
1084+            return true;
1085+        };
1086+    }
1087+
1088+    // Expects that the characters of `s` are sent.
1089+    // NOTE: This implementation is limited to chars a-z, A-Z.
1090+    void ExpectString(TestDriver& driver, const std::string& s) {
1091+        InSequence seq;
1092+        for (int c : s) {
1093+            switch (c) {
1094+                case 'a' ... 'z': { // Lowercase letter.
1095+                    uint16_t keycode = c - ('a' - KC_A);
1096+                    EXPECT_REPORT(driver, (keycode));
1097+                } break;
1098+
1099+                case 'A' ... 'Z': { // Capital letter = KC_LSFT + letter key.
1100+                    uint16_t keycode = c - ('A' - KC_A);
1101+                    EXPECT_REPORT(driver, (KC_LSFT, keycode));
1102+                } break;
1103+            }
1104+        }
1105+    }
1106+};
1107+
1108+// Tests that "A, Repeat, Repeat, B, Repeat" produces "aaabb".
1109+TEST_F(RepeatKey, Basic) {
1110+    TestDriver driver;
1111+    KeymapKey  key_a(0, 0, 0, KC_A);
1112+    KeymapKey  key_b(0, 1, 0, KC_B);
1113+    KeymapKey  key_repeat(0, 2, 0, QK_REP);
1114diff --git a/tests/test_common/keycode_table.cpp b/tests/test_common/keycode_table.cpp
1115index d21630c01be1f8b8af80c2fd0db35b6d2d8d7b37..9ed80cdbcf6603755b32247e8750daea5f73cb75 100644
1116--- a/tests/test_common/keycode_table.cpp
1117+++ b/tests/test_common/keycode_table.cpp
1118@@ -663,6 +663,8 @@ std::map<uint16_t, std::string> KEYCODE_ID_TABLE = {
1119     {QK_AUTOCORRECT_TOGGLE, "QK_AUTOCORRECT_TOGGLE"},
1120     {QK_TRI_LAYER_LOWER, "QK_TRI_LAYER_LOWER"},
1121     {QK_TRI_LAYER_UPPER, "QK_TRI_LAYER_UPPER"},
1122+    {QK_REPEAT_KEY, "QK_REPEAT_KEY"},
1123+    {QK_ALT_REPEAT_KEY, "QK_ALT_REPEAT_KEY"},
1124     {QK_KB_0, "QK_KB_0"},
1125     {QK_KB_1, "QK_KB_1"},
1126     {QK_KB_2, "QK_KB_2"},
1127diff --git a/tests/test_common/test_driver.hpp b/tests/test_common/test_driver.hpp
1128index 8d09e448405a1620b9ebf62bf9bf811d848decb1..d8a6885d0ff80b57bb5b07cf9c1fe63a0c1ceca1 100644
1129--- a/tests/test_common/test_driver.hpp
1130+++ b/tests/test_common/test_driver.hpp
1131@@ -20,6 +20,7 @@
1132 #include <stdint.h>
1133 #include "host.h"
1134 #include "keyboard_report_util.hpp"
1135+#include "keycode_util.hpp"
1136 #include "test_logger.hpp"
1137 
1138 class TestDriver {
1139@@ -98,6 +99,17 @@ class TestDriver {
1140  */
1141 #define EXPECT_NO_REPORT(driver) EXPECT_ANY_REPORT(driver).Times(0)
1142 
1143+/** @brief Tests whether keycode `actual` is equal to `expected`. */
1144+#define EXPECT_KEYCODE_EQ(actual, expected) EXPECT_THAT((actual), KeycodeEq((expected)))
1145+
1146+MATCHER_P(KeycodeEq, expected_keycode, "is equal to " + testing::PrintToString(expected_keycode) + ", keycode " + get_keycode_identifier_or_default(expected_keycode)) {
1147+    if (arg == expected_keycode) {
1148+        return true;
1149+    }
1150+    *result_listener << "keycode " << get_keycode_identifier_or_default(arg);
1151+    return false;
1152+}
1153+
1154 /**
1155  * @brief Verify and clear all gmock expectations that have been setup until
1156  * this point.