فهرست منبع

Merge remote-tracking branch 'origin/develop' into xap

QMK Bot 3 سال پیش
والد
کامیت
ecf42c0153

+ 1 - 0
builddefs/generic_features.mk

@@ -32,6 +32,7 @@ GENERIC_FEATURES = \
     KEY_OVERRIDE \
     LEADER \
     PROGRAMMABLE_BUTTON \
+    REPEAT_KEY \
     SECURE \
     SPACE_CADET \
     SWAP_HANDS \

+ 2 - 1
builddefs/show_options.mk

@@ -85,7 +85,8 @@ OTHER_OPTION_NAMES = \
   SECURE_ENABLE \
   CAPS_WORD_ENABLE \
   AUTOCORRECT_ENABLE \
-  TRI_LAYER_ENABLE
+  TRI_LAYER_ENABLE \
+  REPEAT_KEY_ENABLE
 
 define NAME_ECHO
        @printf "  %-30s = %-16s # %s\\n" "$1" "$($1)" "$(origin $1)"

+ 0 - 0
data/constants/keycodes/keycodes_0.0.3.hjson


+ 18 - 0
data/constants/keycodes/keycodes_0.0.3_quantum.hjson

@@ -0,0 +1,18 @@
+{
+    "keycodes": {
+       "0x7C79": {
+            "group": "quantum",
+            "key": "QK_REPEAT_KEY",
+            "aliases": [
+                "QK_REP"
+            ]
+        },
+        "0x7C7A": {
+            "group": "quantum",
+            "key": "QK_ALT_REPEAT_KEY",
+            "aliases": [
+                "QK_AREP"
+            ]
+        }
+    }
+}

+ 1 - 0
docs/_summary.md

@@ -70,6 +70,7 @@
     * [Macros](feature_macros.md)
     * [Mouse Keys](feature_mouse_keys.md)
     * [Programmable Button](feature_programmable_button.md)
+    * [Repeat Key](feature_repeat_key.md)
     * [Space Cadet Shift](feature_space_cadet.md)
     * [US ANSI Shifted Keys](keycodes_us_ansi_shifted.md)
 

+ 457 - 0
docs/feature_repeat_key.md

@@ -0,0 +1,457 @@
+# Repeat Key
+
+The Repeat Key performs the action of the last pressed key. Tapping the Repeat
+Key after tapping the <kbd>Z</kbd> key types another "`z`." This is useful for
+typing doubled letters, like the `z` in "`dazzle`": a double tap on <kbd>Z</kbd>
+can instead be a roll from <kbd>Z</kbd> to <kbd>Repeat</kbd>, which is
+potentially faster and more comfortable. The Repeat Key is also useful for
+hotkeys, like repeating Ctrl + Shift + Right Arrow to select by word. 
+
+Repeat Key remembers mods that were active with the last key press. These mods
+are combined with any additional mods while pressing the Repeat Key. If the last
+press key was <kbd>Ctrl</kbd> + <kbd>Z</kbd>, then <kbd>Shift</kbd> +
+<kbd>Repeat</kbd> performs Ctrl + Shift + `Z`.
+
+## How do I enable Repeat Key
+
+In your `rules.mk`, add:
+
+```make
+REPEAT_KEY_ENABLE = yes
+```
+
+Then pick a key in your keymap and assign it the keycode `QK_REPEAT_KEY` (short
+alias `QK_REP`). Optionally, use the keycode `QK_ALT_REPEAT_KEY` (short alias
+`QK_AREP`) on another key.
+
+## Keycodes
+
+|Keycode                |Aliases  |Description                          |
+|-----------------------|---------|-------------------------------------|
+|`QK_REPEAT_KEY`        |`QK_REP` |Repeat the last pressed key          |
+|`QK_ALT_REPEAT_KEY`    |`QK_AREP`|Perform alternate of the last key    |
+
+## Alternate Repeating
+
+The Alternate Repeat Key performs the "alternate" action of the last pressed key
+if it is defined. By default, Alternate Repeat is defined for navigation keys to
+act in the reverse direction. When the last key is the common "select by word"
+hotkey Ctrl + Shift + Right Arrow, the Alternate Repeat Key performs Ctrl +
+Shift + Left Arrow, which together with the Repeat Key enables convenient
+selection by words in either direction.
+
+Alternate Repeat is enabled with the Repeat Key by default. Optionally, to
+reduce firmware size, Alternate Repeat may be disabled by adding in config.h:
+
+```c
+#define NO_ALT_REPEAT_KEY
+```
+
+The following alternate keys are defined by default. See
+`get_alt_repeat_key_keycode_user()` below for how to change or add to these
+definitions. Where it makes sense, these definitions also include combinations 
+with mods, like Ctrl + Left &harr; Ctrl + Right Arrow.
+
+**Navigation** 
+
+|Keycodes                           |Description                        |
+|-----------------------------------|-----------------------------------|
+|`KC_LEFT` &harr; `KC_RGHT`         | Left &harr; Right Arrow           |
+|`KC_UP` &harr; `KC_DOWN`           | Up &harr; Down Arrow              |
+|`KC_HOME` &harr; `KC_END`          | Home &harr; End                   |
+|`KC_PGUP` &harr; `KC_PGDN`         | Page Up &harr; Page Down          |
+|`KC_MS_L` &harr; `KC_MS_R`         | Mouse Cursor Left &harr; Right    |
+|`KC_MS_U` &harr; `KC_MS_D`         | Mouse Cursor Up &harr; Down       |
+|`KC_WH_L` &harr; `KC_WH_R`         | Mouse Wheel Left &harr; Right     |
+|`KC_WH_U` &harr; `KC_WH_D`         | Mouse Wheel Up &harr; Down        |
+
+**Misc** 
+
+|Keycodes                           |Description                        |
+|-----------------------------------|-----------------------------------|
+|`KC_BSPC` &harr; `KC_DEL`          | Backspace &harr; Delete           |
+|`KC_LBRC` &harr; `KC_RBRC`         | `[` &harr; `]`                    |
+|`KC_LCBR` &harr; `KC_RCBR`         | `{` &harr; `}`                    |
+
+**Media** 
+
+|Keycodes                           |Description                        |
+|-----------------------------------|-----------------------------------|
+|`KC_WBAK` &harr; `KC_WFWD`         | Browser Back &harr; Forward       |
+|`KC_MNXT` &harr; `KC_MPRV`         | Next &harr; Previous Media Track  |
+|`KC_MFFD` &harr; `KC_MRWD`         | Fast Forward &harr; Rewind Media  |
+|`KC_VOLU` &harr; `KC_VOLD`         | Volume Up &harr; Down             |
+|`KC_BRIU` &harr; `KC_BRID`         | Brightness Up &harr; Down         |
+
+**Hotkeys in Vim, Emacs, and other programs**
+
+|Keycodes                           |Description                        |
+|-----------------------------------|-----------------------------------|
+|mod + `KC_F` &harr; mod + `KC_B`   | Forward &harr; Backward           |
+|mod + `KC_D` &harr; mod + `KC_U`   | Down &harr; Up                    |
+|mod + `KC_N` &harr; mod + `KC_P`   | Next &harr; Previous              |
+|mod + `KC_A` &harr; mod + `KC_E`   | Home &harr; End                   |
+|mod + `KC_O` &harr; mod + `KC_I`   | Vim jump list Older &harr; Newer  |
+|`KC_J` &harr; `KC_K`               | Down &harr; Up                    |
+|`KC_H` &harr; `KC_L`               | Left &harr; Right                 |
+|`KC_W` &harr; `KC_B`               | Forward &harr; Backward by Word   |
+
+(where above, "mod" is Ctrl, Alt, or GUI)
+
+
+## Defining alternate keys
+
+Use the `get_alt_repeat_key_keycode_user()` callback to define the "alternate"
+for additional keys or override the default definitions. For example, to define
+Ctrl + Y as the alternate of Ctrl + Z, and vice versa, add the following in
+keymap.c:
+
+```c
+uint16_t get_alt_repeat_key_keycode_user(uint16_t keycode, uint8_t mods) {
+    if ((mods & MOD_MASK_CTRL)) {  // Was Ctrl held?
+        switch (keycode) {
+            case KC_Y: return C(KC_Z);  // Ctrl + Y reverses to Ctrl + Z.
+            case KC_Z: return C(KC_Y);  // Ctrl + Z reverses to Ctrl + Y.
+        }
+    }
+
+    return KC_TRNS;  // Defer to default definitions.
+}
+```
+
+The `keycode` and `mods` args are the keycode and mods that were active with the
+last pressed key. The meaning of the return value from this function is:
+
+* `KC_NO` &ndash; do nothing (any predefined alternate key is not used);
+* `KC_TRNS` &ndash; use the default alternate key if it exists;
+* anything else &ndash; use the specified keycode. Any keycode may be returned
+  as an alternate key, including custom keycodes.
+
+Another example, defining Shift + Tab as the alternate of Tab, and vice versa:
+
+```c
+uint16_t get_alt_repeat_key_keycode_user(uint16_t keycode, uint8_t mods) {
+    bool shifted = (mods & MOD_MASK_SHIFT);  // Was Shift held?
+    switch (keycode) {
+        case KC_TAB:
+            if (shifted) {        // If the last key was Shift + Tab,
+                return KC_TAB;    // ... the reverse is Tab.
+            } else {              // Otherwise, the last key was Tab,
+                return S(KC_TAB); // ... and the reverse is Shift + Tab.
+            }
+    }
+
+    return KC_TRNS;
+}
+```
+
+#### Eliminating SFBs
+
+Alternate Repeat can be configured more generally to perform an action that
+"complements" the last key. Alternate Repeat is not limited to reverse
+repeating, and it need not be symmetric. You can use it to eliminate cases of
+same-finger bigrams in your layout, that is, pairs of letters typed by the same
+finger. The following addresses the top 5 same-finger bigrams in English on
+QWERTY, so that for instance "`ed`" may be typed as <kbd>E</kbd>, <kbd>Alt
+Repeat</kbd>.
+
+```c
+uint16_t get_alt_repeat_key_keycode_user(uint16_t keycode, uint8_t mods) {
+    switch (keycode) {
+        case KC_E: return KC_D;  // For "ED" bigram.
+        case KC_D: return KC_E;  // For "DE" bigram.
+        case KC_C: return KC_E;  // For "CE" bigram.
+        case KC_L: return KC_O;  // For "LO" bigram.
+        case KC_U: return KC_N;  // For "UN" bigram.
+    }
+
+    return KC_TRNS;
+}
+```
+
+#### Typing shortcuts
+
+A useful possibility is having Alternate Repeat press [a
+macro](feature_macros.md). This way macros can be used without having to
+dedicate keys to them. The following defines a couple shortcuts.
+
+* Typing <kbd>K</kbd>, <kbd>Alt Repeat</kbd> produces "`keyboard`," with the
+  initial "`k`" typed as usual and the "`eybord`" produced by the macro. 
+* Typing <kbd>.</kbd>, <kbd>Alt Repeat</kbd> produces "`../`," handy for "up
+  directory" on the shell. Similary, <kbd>.</kbd> types the initial "`.`" and 
+  "`./`" is produced by the macro.
+
+```c
+enum custom_keycodes {
+    M_KEYBOARD = SAFE_RANGE,
+    M_UPDIR,
+    // Other custom keys...
+};
+
+uint16_t get_alt_repeat_key_keycode_user(uint16_t keycode, uint8_t mods) {
+    switch (keycode) {
+        case KC_K: return M_KEYBOARD;
+        case KC_DOT: return M_UPDIR;
+    }
+
+    return KC_TRNS;
+}
+
+bool process_record_user(uint16_t keycode, keyrecord_t* record) {
+    switch (keycode) {
+        case M_KEYBOARD: SEND_STRING(/*k*/"eyboard"); break;
+        case M_UPDIR: SEND_STRING(/*.*/"./"); break;
+    }
+    return true;
+}
+```
+
+## Ignoring certain keys and mods
+
+In tracking what is "the last key" to be repeated or alternate repeated,
+modifier and layer switch keys are always ignored. This makes it possible to set
+some mods and change layers between pressing a key and repeating it. By default,
+all other (non-modifier, non-layer switch) keys are remembered so that they are
+eligible for repeating. To configure additional keys to be ignored, define
+`remember_last_key_user()` in your keymap.c.
+
+#### Ignoring a key
+
+The following ignores the Backspace key:
+
+```c
+bool remember_last_key_user(uint16_t keycode, keyrecord_t* record,
+                            uint8_t* remembered_mods) {
+    switch (keycode) {
+        case KC_BSPC:
+            return false;  // Ignore backspace.
+    }
+
+    return true;  // Other keys can be repeated.
+}
+```
+
+Then for instance, the Repeat key in <kbd>Left Arrow</kbd>,
+<kbd>Backspace</kbd>, <kbd>Repeat</kbd> sends Left Arrow again instead of
+repeating Backspace.
+
+The `remember_last_key_user()` callback is called on every key press excluding
+modifiers and layer switches. Returning true indicates the key is remembered,
+while false means it is ignored.
+
+#### Filtering remembered mods
+
+The `remembered_mods` arg represents the mods that will be remembered with
+this key. It can be modified to forget certain mods. This may be
+useful to forget capitalization when repeating shifted letters, so that "Aaron"
+does not becom "AAron":
+
+```c
+bool remember_last_key_user(uint16_t keycode, keyrecord_t* record,
+                            uint8_t* remembered_mods) {
+    // Forget Shift on letter keys when Shift or AltGr are the only mods.
+    switch (keycode) {
+        case KC_A ... KC_Z:
+            if ((*remembered_mods & ~(MOD_MASK_SHIFT | MOD_BIT(KC_RALT))) == 0) {
+                *remembered_mods &= ~MOD_MASK_SHIFT;
+            }
+            break;
+    }
+
+    return true;
+}
+```
+
+#### Further conditions
+
+Besides checking the keycode, this callback could also make conditions based on
+the current layer state (with `IS_LAYER_ON(layer)`) or mods (`get_mods()`). For
+example, the following ignores keys on layer 2 as well as key combinations
+involving GUI:
+
+```c
+bool remember_last_key_user(uint16_t keycode, keyrecord_t* record,
+                            uint8_t* remembered_mods) {
+    if (IS_LAYER_ON(2) || (get_mods() & MOD_MASK_GUI)) {
+        return false;  // Ignore layer 2 keys and GUI chords.
+    }
+
+    return true;  // Other keys can be repeated.
+}
+```
+
+?> See [Layer Functions](feature_layers.md#functions) and [Checking Modifier
+State](feature_advanced_keycodes.md#checking-modifier-state) for further
+details.
+ 
+
+## Handle how a key is repeated
+
+By default, pressing the Repeat Key will simply behave as if the last key
+were pressed again. This also works with macro keys with custom handlers,
+invoking the macro again. In case fine-tuning is needed for sensible repetition,
+you can handle how a key is repeated with `get_repeat_key_count()` within
+`process_record_user()`. 
+
+The `get_repeat_key_count()` function returns a signed count of times the key
+has been repeated or alternate repeated. When a key is pressed as usual,
+`get_repeat_key_count()` is 0. On the first repeat, it is 1, then the second
+repeat, 2, and so on. Negative counts are used similarly for alternate
+repeating. For instance supposing `MY_MACRO` is a custom keycode used in the
+layout:
+
+```c
+bool process_record_user(uint16_t keycode, keyrecord_t* record) {
+    switch (keycode) {
+        case MY_MACRO:
+            if (get_repeat_key_count() > 0) {
+                // MY_MACRO is being repeated!
+                if (record->event.pressed) {
+                    SEND_STRING("repeat!");    
+                }
+            } else {                          
+                // MY_MACRO is being used normally.
+                if (record->event.pressed) {  
+                    SEND_STRING("macro");
+                }
+            }
+            return false;
+     
+        // Other macros...
+    }
+    return true;
+}
+```
+
+## Handle how a key is alternate repeated
+
+Pressing the Alternate Repeat Key behaves as if the "alternate" of the last
+pressed key were pressed, if an alternate is defined. To define how a particular
+key is alternate repeated, use the `get_alt_repeat_key_keycode_user()` callback
+as described above to define which keycode to use as its alternate. Beyond this,
+`get_repeat_key_count()` may be used in custom handlers to fine-tune behavior
+when alternate repeating.
+
+The following example defines `MY_MACRO` as its own alternate, and specially
+handles repeating and alternate repeating:
+
+```c
+uint16_t get_alt_repeat_key_keycode_user(uint16_t keycode, uint8_t mods) {
+    switch (keycode) {
+        case MY_MACRO: return MY_MACRO;  // MY_MACRO is its own alternate.
+    }
+    return KC_TRNS;
+}
+
+bool process_record_user(uint16_t keycode, keyrecord_t* record) {
+    switch (keycode) {
+        case MY_MACRO:
+            if (get_repeat_key_count() > 0) {        // Repeating.
+                if (record->event.pressed) {
+                    SEND_STRING("repeat!");    
+                }
+            } else if (get_repeat_key_count() < 0) { // Alternate repeating.
+                if (record->event.pressed) {
+                    SEND_STRING("alt repeat!");
+                }
+            } else {                                 // Used normally.
+                if (record->event.pressed) {  
+                    SEND_STRING("macro");
+                }
+            }
+            return false;
+     
+        // Other macros...
+    }
+    return true;
+}
+```
+
+
+## Functions
+
+| Function                       | Description                                                            |
+|--------------------------------|------------------------------------------------------------------------|
+| `get_last_keycode()`           | The last key's keycode, the key to be repeated.                        |
+| `get_last_mods()`              | Mods to apply when repeating.                                          |
+| `set_last_keycode(kc)`         | Set the keycode to be repeated.                                        |
+| `set_last_mods(mods)`          | Set the mods to apply when repeating.                                  |
+| `get_repeat_key_count()`       | Signed count of times the key has been repeated or alternate repeated. |
+| `get_alt_repeat_key_keycode()` | Keycode to be used for alternate repeating.                            |
+ 
+
+## Additional "Alternate" keys
+
+By leveraging `get_last_keycode()` in macros, it is possible to define
+additional, distinct "Alternate Repeat"-like keys. The following defines two
+keys `ALTREP2` and `ALTREP3` and implements ten shortcuts with them for common
+English 5-gram letter patterns, taking inspiration from
+[Stenotype](feature_stenography.md):
+
+
+| Typing                           | Produces | Typing                           | Produces |
+|----------------------------------|----------|----------------------------------|----------|
+| <kbd>A</kbd>, <kbd>ALTREP2</kbd> | `ation`  | <kbd>A</kbd>, <kbd>ALTREP3</kbd> | `about`   |
+| <kbd>I</kbd>, <kbd>ALTREP2</kbd> | `ition`  | <kbd>I</kbd>, <kbd>ALTREP3</kbd> | `inter`   |
+| <kbd>S</kbd>, <kbd>ALTREP2</kbd> | `ssion`  | <kbd>S</kbd>, <kbd>ALTREP3</kbd> | `state`   |
+| <kbd>T</kbd>, <kbd>ALTREP2</kbd> | `their`  | <kbd>T</kbd>, <kbd>ALTREP3</kbd> | `there`   |
+| <kbd>W</kbd>, <kbd>ALTREP2</kbd> | `which`  | <kbd>W</kbd>, <kbd>ALTREP3</kbd> | `would`   |
+
+```c
+enum custom_keycodes {
+    ALTREP2 = SAFE_RANGE,
+    ALTREP3,
+};
+
+// Use ALTREP2 and ALTREP3 in your layout...
+
+bool remember_last_key_user(uint16_t keycode, keyrecord_t* record,
+                            uint8_t* remembered_mods) {
+    switch (keycode) {
+        case ALTREP2:
+        case ALTREP3:
+            return false;  // Ignore ALTREP keys.
+    }
+
+    return true;  // Other keys can be repeated.
+}
+
+static void process_altrep2(uint16_t keycode, uint8_t mods) {
+    switch (keycode) {
+        case KC_A: SEND_STRING(/*a*/"tion"); break;
+        case KC_I: SEND_STRING(/*i*/"tion"); break;
+        case KC_S: SEND_STRING(/*s*/"sion"); break;
+        case KC_T: SEND_STRING(/*t*/"heir"); break;
+        case KC_W: SEND_STRING(/*w*/"hich"); break;
+    }
+}
+
+static void process_altrep3(uint16_t keycode, uint8_t mods) {
+    switch (keycode) {
+        case KC_A: SEND_STRING(/*a*/"bout"); break;
+        case KC_I: SEND_STRING(/*i*/"nter"); break;
+        case KC_S: SEND_STRING(/*s*/"tate"); break;
+        case KC_T: SEND_STRING(/*t*/"here"); break;
+        case KC_W: SEND_STRING(/*w*/"ould"); break;
+    }
+}
+
+bool process_record_user(uint16_t keycode, keyrecord_t* record) {
+    switch (keycode) {
+        case ALTREP2: 
+            if (record->event.pressed) {
+                process_altrep2(get_last_keycode(), get_last_mods());
+            }
+            return false;
+
+        case ALTREP3:
+            if (record->event.pressed) {
+                process_altrep3(get_last_keycode(), get_last_mods());
+            }
+            return false;
+    }
+
+    return true;
+}
+```
+

+ 1 - 0
docs/ja/_summary.md

@@ -68,6 +68,7 @@
     * [モッドタップ](ja/mod_tap.md)
     * [マクロ](ja/feature_macros.md)
     * [マウスキー](ja/feature_mouse_keys.md)
+    * [Repeat Key](ja/feature_repeat_key.md)
     * [Space Cadet Shift](ja/feature_space_cadet.md)
     * [US ANSI シフトキー](ja/keycodes_us_ansi_shifted.md)
 

+ 9 - 0
docs/keycodes.md

@@ -803,6 +803,15 @@ See also: [Programmable Button](feature_programmable_button.md)
 |`QK_PROGRAMMABLE_BUTTON_31`|`PB_31`|Programmable button 31|
 |`QK_PROGRAMMABLE_BUTTON_32`|`PB_32`|Programmable button 32|
 
+## Repeat Key :id=repeat-key
+
+See also: [Repeat Key](feature_repeat_key.md)
+
+|Keycode                |Aliases  |Description                          |
+|-----------------------|---------|-------------------------------------|
+|`QK_REPEAT_KEY`        |`QK_REP` |Repeat the last pressed key          |
+|`QK_ALT_REPEAT_KEY`    |`QK_AREP`|Perform alternate of the last key    |
+
 ## Space Cadet :id=space-cadet
 
 See also: [Space Cadet](feature_space_cadet.md)

+ 1 - 0
docs/zh-cn/_summary.md

@@ -73,6 +73,7 @@
     * [Mod-Tap](zh-cn/mod_tap.md)
     * [宏](zh-cn/feature_macros.md)
     * [鼠标键](zh-cn/feature_mouse_keys.md)
+    * [Repeat Key](zh-cn/feature_repeat_key.md)
     * [Space Cadet Shift](zh-cn/feature_space_cadet.md)
     * [US ANSI上档键值](zh-cn/keycodes_us_ansi_shifted.md)
 

+ 2 - 2
quantum/action.c

@@ -285,7 +285,7 @@ void process_record(keyrecord_t *record) {
 }
 
 void process_record_handler(keyrecord_t *record) {
-#ifdef COMBO_ENABLE
+#if defined(COMBO_ENABLE) || defined(REPEAT_KEY_ENABLE)
     action_t action;
     if (record->keycode) {
         action = action_for_keycode(record->keycode);
@@ -1109,7 +1109,7 @@ bool is_tap_record(keyrecord_t *record) {
         return false;
     }
 
-#ifdef COMBO_ENABLE
+#if defined(COMBO_ENABLE) || defined(REPEAT_KEY_ENABLE)
     action_t action;
     if (record->keycode) {
         action = action_for_keycode(record->keycode);

+ 1 - 1
quantum/action.h

@@ -50,7 +50,7 @@ typedef struct {
 #ifndef NO_ACTION_TAPPING
     tap_t tap;
 #endif
-#ifdef COMBO_ENABLE
+#if defined(COMBO_ENABLE) || defined(REPEAT_KEY_ENABLE)
     uint16_t keycode;
 #endif
 } keyrecord_t;

+ 5 - 1
quantum/keycodes.h

@@ -721,6 +721,8 @@ enum qk_keycode_defines {
     QK_AUTOCORRECT_TOGGLE = 0x7C76,
     QK_TRI_LAYER_LOWER = 0x7C77,
     QK_TRI_LAYER_UPPER = 0x7C78,
+    QK_REPEAT_KEY = 0x7C79,
+    QK_ALT_REPEAT_KEY = 0x7C7A,
     QK_KB_0 = 0x7E00,
     QK_KB_1 = 0x7E01,
     QK_KB_2 = 0x7E02,
@@ -1362,6 +1364,8 @@ enum qk_keycode_defines {
     AC_TOGG    = QK_AUTOCORRECT_TOGGLE,
     TL_LOWR    = QK_TRI_LAYER_LOWER,
     TL_UPPR    = QK_TRI_LAYER_UPPER,
+    QK_REP     = QK_REPEAT_KEY,
+    QK_AREP    = QK_ALT_REPEAT_KEY,
 };
 
 // Range Helpers
@@ -1413,6 +1417,6 @@ enum qk_keycode_defines {
 #define IS_MACRO_KEYCODE(code) ((code) >= QK_MACRO_0 && (code) <= QK_MACRO_31)
 #define IS_BACKLIGHT_KEYCODE(code) ((code) >= QK_BACKLIGHT_ON && (code) <= QK_BACKLIGHT_TOGGLE_BREATHING)
 #define IS_RGB_KEYCODE(code) ((code) >= RGB_TOG && (code) <= RGB_MODE_TWINKLE)
-#define IS_QUANTUM_KEYCODE(code) ((code) >= QK_BOOTLOADER && (code) <= QK_TRI_LAYER_UPPER)
+#define IS_QUANTUM_KEYCODE(code) ((code) >= QK_BOOTLOADER && (code) <= QK_ALT_REPEAT_KEY)
 #define IS_KB_KEYCODE(code) ((code) >= QK_KB_0 && (code) <= QK_KB_31)
 #define IS_USER_KEYCODE(code) ((code) >= QK_USER_0 && (code) <= QK_USER_31)

+ 109 - 0
quantum/process_keycode/process_repeat_key.c

@@ -0,0 +1,109 @@
+// Copyright 2022-2023 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 "process_repeat_key.h"
+
+// Default implementation of remember_last_key_user().
+__attribute__((weak)) bool remember_last_key_user(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) {
+    return true;
+}
+
+static bool remember_last_key(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) {
+    switch (keycode) {
+        // Ignore MO, TO, TG, TT, and TL layer switch keys.
+        case QK_MOMENTARY ... QK_MOMENTARY_MAX:
+        case QK_TO ... QK_TO_MAX:
+        case QK_TOGGLE_LAYER ... QK_TOGGLE_LAYER_MAX:
+        case QK_LAYER_TAP_TOGGLE ... QK_LAYER_TAP_TOGGLE_MAX:
+        // Ignore mod keys.
+        case KC_LCTL ... KC_RGUI:
+        case KC_HYPR:
+        case KC_MEH:
+#ifndef NO_ACTION_ONESHOT // Ignore one-shot keys.
+        case QK_ONE_SHOT_LAYER ... QK_ONE_SHOT_LAYER_MAX:
+        case QK_ONE_SHOT_MOD ... QK_ONE_SHOT_MOD_MAX:
+#endif                  // NO_ACTION_ONESHOT
+#ifdef TRI_LAYER_ENABLE // Ignore Tri Layer keys.
+        case QK_TRI_LAYER_LOWER:
+        case QK_TRI_LAYER_UPPER:
+#endif // TRI_LAYER_ENABLE
+            return false;
+
+            // Ignore hold events on tap-hold keys.
+#ifndef NO_ACTION_TAPPING
+        case QK_MOD_TAP ... QK_MOD_TAP_MAX:
+#    ifndef NO_ACTION_LAYER
+        case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
+#    endif // NO_ACTION_LAYER
+            if (record->tap.count == 0) {
+                return false;
+            }
+            break;
+#endif // NO_ACTION_TAPPING
+
+#ifdef SWAP_HANDS_ENABLE
+        case QK_SWAP_HANDS ... QK_SWAP_HANDS_MAX:
+            if (IS_SWAP_HANDS_KEYCODE(keycode) || record->tap.count == 0) {
+                return false;
+            }
+            break;
+#endif // SWAP_HANDS_ENABLE
+
+        case QK_REPEAT_KEY:
+#ifndef NO_ALT_REPEAT_KEY
+        case QK_ALT_REPEAT_KEY:
+#endif // NO_ALT_REPEAT_KEY
+            return false;
+    }
+
+    return remember_last_key_user(keycode, record, remembered_mods);
+}
+
+bool process_last_key(uint16_t keycode, keyrecord_t* record) {
+    if (get_repeat_key_count()) {
+        return true;
+    }
+
+    if (record->event.pressed) {
+        uint8_t remembered_mods = get_mods() | get_weak_mods();
+#ifndef NO_ACTION_ONESHOT
+        remembered_mods |= get_oneshot_mods();
+#endif // NO_ACTION_ONESHOT
+
+        if (remember_last_key(keycode, record, &remembered_mods)) {
+            set_last_record(keycode, record);
+            set_last_mods(remembered_mods);
+        }
+    }
+
+    return true;
+}
+
+bool process_repeat_key(uint16_t keycode, keyrecord_t* record) {
+    if (get_repeat_key_count()) {
+        return true;
+    }
+
+    if (keycode == QK_REPEAT_KEY) {
+        repeat_key_invoke(&record->event);
+        return false;
+#ifndef NO_ALT_REPEAT_KEY
+    } else if (keycode == QK_ALT_REPEAT_KEY) {
+        alt_repeat_key_invoke(&record->event);
+        return false;
+#endif // NO_ALT_REPEAT_KEY
+    }
+
+    return true;
+}

+ 62 - 0
quantum/process_keycode/process_repeat_key.h

@@ -0,0 +1,62 @@
+// Copyright 2022-2023 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 "quantum.h"
+
+/**
+ * @brief Process handler for remembering the last key.
+ *
+ * @param keycode  Keycode registered by matrix press, per keymap
+ * @param record   keyrecord_t structure
+ * @return true    Continue processing keycodes, and send to host
+ * @return false   Stop processing keycodes, and don't send to host
+ */
+bool process_last_key(uint16_t keycode, keyrecord_t* record);
+
+/**
+ * @brief Optional callback defining which keys are remembered.
+ *
+ * @param keycode          Keycode that was just pressed
+ * @param record           keyrecord_t structure
+ * @param remembered_mods  Mods that will be remembered with this key
+ * @return true            Key is remembered
+ * @return false           Key is ignored
+ *
+ * Modifier and layer switch keys are always ignored. For all other keys, this
+ * callback is called on every key press. Returning true means that the key is
+ * remembered, false means it is ignored. By default, all non-modifier,
+ * non-layer switch keys are remembered.
+ *
+ * The `remembered_mods` arg represents the mods that will be remembered with
+ * this key. It can be modified to forget certain mods, for instance to forget
+ * capitalization when repeating shifted letters:
+ *
+ *     // Forget Shift on letter keys.
+ *     if (KC_A <= keycode && keycode <= KC_Z && (*remembered_mods & ~MOD_MASK_SHIFT) == 0) {
+ *         *remembered_mods = 0;
+ *     }
+ */
+bool remember_last_key_user(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods);
+
+/**
+ * @brief Process handler for Repeat Key feature.
+ *
+ * @param keycode  Keycode registered by matrix press, per keymap
+ * @param record   keyrecord_t structure
+ * @return true    Continue processing keycodes, and send to host
+ * @return false   Stop processing keycodes, and don't send to host
+ */
+bool process_repeat_key(uint16_t keycode, keyrecord_t* record);

+ 4 - 1
quantum/quantum.c

@@ -176,7 +176,7 @@ void soft_reset_keyboard(void) {
 
 /* Convert record into usable keycode via the contained event. */
 uint16_t get_record_keycode(keyrecord_t *record, bool update_layer_cache) {
-#ifdef COMBO_ENABLE
+#if defined(COMBO_ENABLE) || defined(REPEAT_KEY_ENABLE)
     if (record->keycode) {
         return record->keycode;
     }
@@ -273,6 +273,9 @@ bool process_record_quantum(keyrecord_t *record) {
             // Must run asap to ensure all keypresses are recorded.
             process_dynamic_macro(keycode, record) &&
 #endif
+#ifdef REPEAT_KEY_ENABLE
+            process_last_key(keycode, record) && process_repeat_key(keycode, record) &&
+#endif
 #if defined(AUDIO_ENABLE) && defined(AUDIO_CLICKY)
             process_clicky(keycode, record) &&
 #endif

+ 5 - 0
quantum/quantum.h

@@ -255,6 +255,11 @@ extern layer_state_t layer_state;
 #    include "process_tri_layer.h"
 #endif
 
+#ifdef REPEAT_KEY_ENABLE
+#    include "repeat_key.h"
+#    include "process_repeat_key.h"
+#endif
+
 void set_single_persistent_default_layer(uint8_t default_layer);
 
 #define IS_LAYER_ON(layer) layer_state_is(layer)

+ 282 - 0
quantum/repeat_key.c

@@ -0,0 +1,282 @@
+// Copyright 2022-2023 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 "repeat_key.h"
+
+// Variables saving the state of the last key press.
+static keyrecord_t last_record = {0};
+static uint8_t     last_mods   = 0;
+// Signed count of the number of times the last key has been repeated or
+// alternate repeated: it is 0 when a key is pressed normally, positive when
+// repeated, and negative when alternate repeated.
+static int8_t last_repeat_count = 0;
+// The repeat_count, but set to 0 outside of repeat_key_invoke() so that it is
+// nonzero only while a repeated key is being processed.
+static int8_t processing_repeat_count = 0;
+
+uint16_t get_last_keycode(void) {
+    return last_record.keycode;
+}
+
+uint8_t get_last_mods(void) {
+    return last_mods;
+}
+
+void set_last_keycode(uint16_t keycode) {
+    set_last_record(keycode, &(keyrecord_t){
+#ifndef NO_ACTION_TAPPING
+                                 .tap.interrupted = false,
+                                 .tap.count       = 1,
+#endif
+                             });
+}
+
+void set_last_mods(uint8_t mods) {
+    last_mods = mods;
+}
+
+void set_last_record(uint16_t keycode, keyrecord_t* record) {
+    last_record         = *record;
+    last_record.keycode = keycode;
+    last_repeat_count   = 0;
+}
+
+/** @brief Updates `last_repeat_count` in direction `dir`. */
+static void update_last_repeat_count(int8_t dir) {
+    if (dir * last_repeat_count < 0) {
+        last_repeat_count = dir;
+    } else if (dir * last_repeat_count < 127) {
+        last_repeat_count += dir;
+    }
+}
+
+int8_t get_repeat_key_count(void) {
+    return processing_repeat_count;
+}
+
+void repeat_key_invoke(const keyevent_t* event) {
+    // It is possible (e.g. in rolled presses) that the last key changes while
+    // the Repeat Key is pressed. To prevent stuck keys, it is important to
+    // remember separately what key record was processed on press so that the
+    // the corresponding record is generated on release.
+    static keyrecord_t registered_record       = {0};
+    static int8_t      registered_repeat_count = 0;
+    // Since this function calls process_record(), it may recursively call
+    // itself. We return early if `processing_repeat_count` is nonzero to
+    // prevent infinite recursion.
+    if (processing_repeat_count || !last_record.keycode) {
+        return;
+    }
+
+    if (event->pressed) {
+        update_last_repeat_count(1);
+        // On press, apply the last mods state, stacking on top of current mods.
+        register_weak_mods(last_mods);
+        registered_record       = last_record;
+        registered_repeat_count = last_repeat_count;
+    }
+
+    // Generate a keyrecord and plumb it into the event pipeline.
+    registered_record.event = *event;
+    processing_repeat_count = registered_repeat_count;
+    process_record(&registered_record);
+    processing_repeat_count = 0;
+
+    // On release, restore the mods state.
+    if (!event->pressed) {
+        unregister_weak_mods(last_mods);
+    }
+}
+
+#ifndef NO_ALT_REPEAT_KEY
+/**
+ * @brief Find alternate keycode from a table of opposing keycode pairs.
+ * @param table Array of pairs of basic keycodes, declared as PROGMEM.
+ * @param table_size_bytes The size of the table in bytes.
+ * @param target The basic keycode to find.
+ * @return The alternate basic keycode, or KC_NO if none was found.
+ *
+ * @note The table keycodes and target must be basic keycodes.
+ *
+ * This helper is used several times below to define alternate keys. Given a
+ * table of pairs of basic keycodes, the function finds the pair containing
+ * `target` and returns the other keycode in the pair.
+ */
+static uint8_t find_alt_keycode(const uint8_t (*table)[2], uint8_t table_size_bytes, uint8_t target) {
+    const uint8_t* keycodes = (const uint8_t*)table;
+    for (uint8_t i = 0; i < table_size_bytes; ++i) {
+        if (target == pgm_read_byte(keycodes + i)) {
+            // Xor (i ^ 1) the index to get the other element in the pair.
+            return pgm_read_byte(keycodes + (i ^ 1));
+        }
+    }
+    return KC_NO;
+}
+
+uint16_t get_alt_repeat_key_keycode(void) {
+    uint16_t keycode = last_record.keycode;
+    uint8_t  mods    = last_mods;
+
+    // Call the user callback first to give it a chance to override the default
+    // alternate key definitions that follow.
+    uint16_t alt_keycode = get_alt_repeat_key_keycode_user(keycode, mods);
+
+    if (alt_keycode != KC_TRANSPARENT) {
+        return alt_keycode;
+    }
+
+    // Convert 8-bit mods to the 5-bit format used in keycodes. This is lossy:
+    // if left and right handed mods were mixed, they all become right handed.
+    mods = ((mods & 0xf0) ? /* set right hand bit */ 0x10 : 0)
+           // Combine right and left hand mods.
+           | (((mods >> 4) | mods) & 0xf);
+
+    switch (keycode) {
+        case QK_MODS ... QK_MODS_MAX: // Unpack modifier + basic key.
+            mods |= QK_MODS_GET_MODS(keycode);
+            keycode = QK_MODS_GET_BASIC_KEYCODE(keycode);
+            break;
+
+#    ifndef NO_ACTION_TAPPING
+        case QK_MOD_TAP ... QK_MOD_TAP_MAX:
+            keycode = QK_MOD_TAP_GET_TAP_KEYCODE(keycode);
+            break;
+#        ifndef NO_ACTION_LAYER
+        case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
+            keycode = QK_LAYER_TAP_GET_TAP_KEYCODE(keycode);
+            break;
+#        endif // NO_ACTION_LAYER
+#    endif     // NO_ACTION_TAPPING
+
+#    ifdef SWAP_HANDS_ENABLE
+        case QK_SWAP_HANDS ... QK_SWAP_HANDS_MAX:
+            if (IS_SWAP_HANDS_KEYCODE(keycode)) {
+                return KC_NO;
+            }
+            keycode = QK_SWAP_HANDS_GET_TAP_KEYCODE(keycode);
+            break;
+#    endif // SWAP_HANDS_ENABLE
+    }
+
+    if (IS_QK_BASIC(keycode)) {
+        if ((mods & (MOD_LCTL | MOD_LALT | MOD_LGUI))) {
+            // The last key was pressed with a modifier other than Shift.
+            // The following maps
+            //   mod + F <-> mod + B
+            // and a few others, supporting several core hotkeys used in
+            // Emacs, Vim, less, and other programs.
+            // clang-format off
+            static const uint8_t pairs[][2] PROGMEM = {
+                {KC_F   , KC_B   },  // Forward / Backward.
+                {KC_D   , KC_U   },  // Down / Up.
+                {KC_N   , KC_P   },  // Next / Previous.
+                {KC_A   , KC_E   },  // Home / End.
+                {KC_O   , KC_I   },  // Older / Newer in Vim jump list.
+            };
+            // clang-format on
+            alt_keycode = find_alt_keycode(pairs, sizeof(pairs), keycode);
+        } else {
+            // The last key was pressed with no mods or only Shift. The
+            // following map a few more Vim hotkeys.
+            // clang-format off
+            static const uint8_t pairs[][2] PROGMEM = {
+                {KC_J   , KC_K   },  // Down / Up.
+                {KC_H   , KC_L   },  // Left / Right.
+                // These two lines map W and E to B, and B to W.
+                {KC_W   , KC_B   },  // Forward / Backward by word.
+                {KC_E   , KC_B   },  // Forward / Backward by word.
+            };
+            // clang-format on
+            alt_keycode = find_alt_keycode(pairs, sizeof(pairs), keycode);
+        }
+
+        if (!alt_keycode) {
+            // The following key pairs are considered with any mods.
+            // clang-format off
+            static const uint8_t pairs[][2] PROGMEM = {
+                {KC_LEFT, KC_RGHT},  // Left / Right Arrow.
+                {KC_UP  , KC_DOWN},  // Up / Down Arrow.
+                {KC_HOME, KC_END },  // Home / End.
+                {KC_PGUP, KC_PGDN},  // Page Up / Page Down.
+                {KC_BSPC, KC_DEL },  // Backspace / Delete.
+                {KC_LBRC, KC_RBRC},  // Brackets [ ] and { }.
+#ifdef EXTRAKEY_ENABLE
+                {KC_WBAK, KC_WFWD},  // Browser Back / Forward.
+                {KC_MNXT, KC_MPRV},  // Next / Previous Media Track.
+                {KC_MFFD, KC_MRWD},  // Fast Forward / Rewind Media.
+                {KC_VOLU, KC_VOLD},  // Volume Up / Down.
+                {KC_BRIU, KC_BRID},  // Brightness Up / Down.
+#endif  // EXTRAKEY_ENABLE
+#ifdef MOUSEKEY_ENABLE
+                {KC_MS_L, KC_MS_R},  // Mouse Cursor Left / Right.
+                {KC_MS_U, KC_MS_D},  // Mouse Cursor Up / Down.
+                {KC_WH_L, KC_WH_R},  // Mouse Wheel Left / Right.
+                {KC_WH_U, KC_WH_D},  // Mouse Wheel Up / Down.
+#endif  // MOUSEKEY_ENABLE
+            };
+            // clang-format on
+            alt_keycode = find_alt_keycode(pairs, sizeof(pairs), keycode);
+        }
+
+        if (alt_keycode) {
+            // Combine basic keycode with mods.
+            return (mods << 8) | alt_keycode;
+        }
+    }
+
+    return KC_NO; // No alternate key found.
+}
+
+void alt_repeat_key_invoke(const keyevent_t* event) {
+    static keyrecord_t registered_record       = {0};
+    static int8_t      registered_repeat_count = 0;
+    // Since this function calls process_record(), it may recursively call
+    // itself. We return early if `processing_repeat_count` is nonzero to
+    // prevent infinite recursion.
+    if (processing_repeat_count) {
+        return;
+    }
+
+    if (event->pressed) {
+        registered_record = (keyrecord_t){
+#    ifndef NO_ACTION_TAPPING
+            .tap.interrupted = false,
+            .tap.count       = 0,
+#    endif
+            .keycode = get_alt_repeat_key_keycode(),
+        };
+    }
+
+    // Early return if there is no alternate key defined.
+    if (!registered_record.keycode) {
+        return;
+    }
+
+    if (event->pressed) {
+        update_last_repeat_count(-1);
+        registered_repeat_count = last_repeat_count;
+    }
+
+    // Generate a keyrecord and plumb it into the event pipeline.
+    registered_record.event = *event;
+    processing_repeat_count = registered_repeat_count;
+    process_record(&registered_record);
+    processing_repeat_count = 0;
+}
+
+// Default implementation of get_alt_repeat_key_keycode_user().
+__attribute__((weak)) uint16_t get_alt_repeat_key_keycode_user(uint16_t keycode, uint8_t mods) {
+    return KC_TRANSPARENT;
+}
+#endif // NO_ALT_REPEAT_KEY

+ 80 - 0
quantum/repeat_key.h

@@ -0,0 +1,80 @@
+// Copyright 2022-2023 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 "quantum.h"
+
+uint16_t get_last_keycode(void);             /**< Keycode of the last key. */
+uint8_t  get_last_mods(void);                /**< Mods active with the last key. */
+void     set_last_keycode(uint16_t keycode); /**< Sets the last key. */
+void     set_last_mods(uint8_t mods);        /**< Sets the last mods. */
+
+/** @brief Gets the record for the last key. */
+keyrecord_t* get_last_record(void);
+
+/** @brief Sets keycode and record info for the last key. */
+void set_last_record(uint16_t keycode, keyrecord_t* record);
+
+/**
+ * @brief Signed count of times the key has been repeated or alternate repeated.
+ *
+ * @note The count is nonzero only while a repeated or alternate-repeated key is
+ *       being processed.
+ *
+ * When a key is pressed normally, the count is 0. When the Repeat Key is used
+ * to repeat a key, the count is 1 on the first repeat, 2 on the second repeat,
+ * and continuing up to 127.
+ *
+ * Negative counts are used similarly for alternate repeating. When the
+ * Alternate Repeat Key is used, the count is -1 on the first alternate repeat,
+ * -2 on the second, continuing down to -127.
+ */
+int8_t get_repeat_key_count(void);
+
+/**
+ * @brief Calls `process_record()` on a generated record repeating the last key.
+ * @param event Event information in the generated record.
+ */
+void repeat_key_invoke(const keyevent_t* event);
+
+#ifndef NO_ALT_REPEAT_KEY
+
+/**
+ * @brief Keycode to be used for alternate repeating.
+ *
+ * Alternate Repeat performs this keycode based on the last eligible pressed key
+ * and mods, get_last_keycode() and get_last_mods(). For example, when the last
+ * key was KC_UP, this function returns KC_DOWN. The function returns KC_NO if
+ * the last key doesn't have a defined alternate.
+ */
+uint16_t get_alt_repeat_key_keycode(void);
+
+/**
+ * @brief Calls `process_record()` to alternate repeat the last key.
+ * @param event Event information in the generated record.
+ */
+void alt_repeat_key_invoke(const keyevent_t* event);
+
+/**
+ * @brief Optional user callback to define additional alternate keys.
+ *
+ * When `get_alt_repeat_key_keycode()` is called, it first calls this callback.
+ * It should return a keycode representing the "alternate" of the given keycode
+ * and mods. Returning KC_NO defers to the default definitions in
+ * `get_alt_repeat_key_keycode()`.
+ */
+uint16_t get_alt_repeat_key_keycode_user(uint16_t keycode, uint8_t mods);
+
+#endif // NO_ALT_REPEAT_KEY

+ 18 - 0
tests/repeat_key/alt_repeat_key/config.h

@@ -0,0 +1,18 @@
+// Copyright 2023 Google LLC
+//
+// 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"

+ 18 - 0
tests/repeat_key/alt_repeat_key/test.mk

@@ -0,0 +1,18 @@
+# Copyright 2023 Google LLC
+#
+# 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/>.
+
+REPEAT_KEY_ENABLE = yes
+
+EXTRAKEY_ENABLE = yes

+ 523 - 0
tests/repeat_key/alt_repeat_key/test_alt_repeat_key.cpp

@@ -0,0 +1,523 @@
+// Copyright 2023 Google LLC
+//
+// 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/>.
+
+#include <functional>
+
+#include "keyboard_report_util.hpp"
+#include "keycode.h"
+#include "test_common.hpp"
+#include "test_fixture.hpp"
+#include "test_keymap_key.hpp"
+
+using ::testing::AnyNumber;
+using ::testing::InSequence;
+
+namespace {
+
+bool process_record_user_default(uint16_t keycode, keyrecord_t* record) {
+    return true;
+}
+
+bool remember_last_key_user_default(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) {
+    return true;
+}
+
+uint16_t get_alt_repeat_key_keycode_user_default(uint16_t keycode, uint8_t mods) {
+    return KC_TRNS;
+}
+
+// Indirections so that process_record_user() can be replaced with other
+// functions in the test cases below.
+std::function<bool(uint16_t, keyrecord_t*)>           process_record_user_fun             = process_record_user_default;
+std::function<bool(uint16_t, keyrecord_t*, uint8_t*)> remember_last_key_user_fun          = remember_last_key_user_default;
+std::function<uint16_t(uint16_t, uint8_t)>            get_alt_repeat_key_keycode_user_fun = get_alt_repeat_key_keycode_user_default;
+
+extern "C" bool process_record_user(uint16_t keycode, keyrecord_t* record) {
+    return process_record_user_fun(keycode, record);
+}
+
+extern "C" bool remember_last_key_user(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) {
+    return remember_last_key_user_fun(keycode, record, remembered_mods);
+}
+
+extern "C" uint16_t get_alt_repeat_key_keycode_user(uint16_t keycode, uint8_t mods) {
+    return get_alt_repeat_key_keycode_user_fun(keycode, mods);
+}
+
+class AltRepeatKey : public TestFixture {
+   public:
+    bool process_record_user_was_called_;
+
+    void SetUp() override {
+        process_record_user_fun             = process_record_user_default;
+        remember_last_key_user_fun          = remember_last_key_user_default;
+        get_alt_repeat_key_keycode_user_fun = get_alt_repeat_key_keycode_user_default;
+    }
+
+    void ExpectProcessRecordUserCalledWith(bool expected_press, uint16_t expected_keycode, int8_t expected_repeat_key_count) {
+        process_record_user_was_called_ = false;
+        process_record_user_fun         = [=](uint16_t keycode, keyrecord_t* record) {
+            EXPECT_EQ(record->event.pressed, expected_press);
+            EXPECT_KEYCODE_EQ(keycode, expected_keycode);
+            EXPECT_EQ(get_repeat_key_count(), expected_repeat_key_count);
+            // Tests below use this to verify process_record_user() was called.
+            process_record_user_was_called_ = true;
+            return true;
+        };
+    }
+
+    // Expects that the characters of `s` are sent.
+    // NOTE: This implementation is limited to chars a-z, A-Z.
+    void ExpectString(TestDriver& driver, const std::string& s) {
+        InSequence seq;
+        for (int c : s) {
+            switch (c) {
+                case 'a' ... 'z': { // Lowercase letter.
+                    uint16_t keycode = c - ('a' - KC_A);
+                    EXPECT_REPORT(driver, (keycode));
+                } break;
+
+                case 'A' ... 'Z': { // Capital letter = KC_LSFT + letter key.
+                    uint16_t keycode = c - ('A' - KC_A);
+                    EXPECT_REPORT(driver, (KC_LSFT, keycode));
+                } break;
+            }
+        }
+    }
+};
+
+TEST_F(AltRepeatKey, AlternateBasic) {
+    TestDriver driver;
+    KeymapKey  key_bspc(0, 0, 0, KC_BSPC);
+    KeymapKey  key_pgdn(0, 1, 0, KC_PGDN);
+    KeymapKey  key_pgup(0, 2, 0, KC_PGUP);
+    KeymapKey  key_repeat(0, 4, 0, QK_REP);
+    KeymapKey  key_alt_repeat(0, 5, 0, QK_AREP);
+    set_keymap({key_bspc, key_pgdn, key_pgup, key_repeat, key_alt_repeat});
+
+    // Allow any number of empty reports.
+    EXPECT_EMPTY_REPORT(driver).Times(AnyNumber());
+    {
+        InSequence seq;
+        EXPECT_REPORT(driver, (KC_BSPC));
+        EXPECT_REPORT(driver, (KC_DEL));
+        EXPECT_REPORT(driver, (KC_DEL));
+        EXPECT_REPORT(driver, (KC_BSPC));
+        EXPECT_REPORT(driver, (KC_DEL));
+        EXPECT_REPORT(driver, (KC_PGDN));
+        EXPECT_REPORT(driver, (KC_PGUP));
+        EXPECT_REPORT(driver, (KC_PGUP));
+        EXPECT_REPORT(driver, (KC_PGDN));
+    }
+
+    tap_key(key_bspc);
+
+    for (int n = 1; n <= 2; ++n) { // Tap the Alternate Repeat Key twice.
+        ExpectProcessRecordUserCalledWith(true, KC_DEL, -n);
+        key_alt_repeat.press(); // Press the Alternate Repeat Key.
+        run_one_scan_loop();
+        EXPECT_TRUE(process_record_user_was_called_);
+
+        // Expect the corresponding release event.
+        ExpectProcessRecordUserCalledWith(false, KC_DEL, -n);
+        key_alt_repeat.release(); // Release the Repeat Key.
+        run_one_scan_loop();
+        EXPECT_TRUE(process_record_user_was_called_);
+    }
+
+    process_record_user_fun = process_record_user_default;
+    tap_keys(key_repeat, key_alt_repeat);
+    tap_keys(key_pgdn, key_alt_repeat);
+    tap_keys(key_pgup, key_alt_repeat);
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+struct TestParamsAlternateKeyCodes {
+    uint16_t keycode;
+    uint8_t  mods;
+    uint16_t expected_alt_keycode;
+};
+
+// Tests `get_alt_repeat_key_keycode()` for various keycodes.
+TEST_F(AltRepeatKey, GetAltRepeatKeyKeycode) {
+    for (const auto& params : std::vector<TestParamsAlternateKeyCodes>({
+             // clang-format off
+          // Each line tests one call to `get_alt_repeat_key_keycode()`:
+          // {keycode, mods, expected_alt_keycode}.
+          // Arrows.
+          {KC_LEFT, 0, KC_RGHT},
+          {KC_RGHT, 0, KC_LEFT},
+          {KC_LEFT, MOD_BIT(KC_LSFT), LSFT(KC_RGHT)},
+          {KC_LEFT, MOD_BIT(KC_RSFT), RSFT(KC_RGHT)},
+          {KC_LEFT, MOD_BIT(KC_LCTL) | MOD_BIT(KC_LSFT), C(S(KC_RGHT))},
+          {KC_LEFT, MOD_BIT(KC_LGUI), LGUI(KC_RGHT)},
+          {C(KC_LEFT), MOD_BIT(KC_LSFT), C(S(KC_RGHT))},
+          {KC_UP, 0, KC_DOWN},
+          // Navigation keys.
+          {KC_PGUP, 0, KC_PGDN},
+          {KC_HOME, 0, KC_END },
+          // Media keys.
+          {KC_WBAK, 0, KC_WFWD},
+          {KC_MNXT, 0, KC_MPRV},
+          {KC_MRWD, 0, KC_MFFD},
+          {KC_VOLU, 0, KC_VOLD},
+          {KC_BRIU, 0, KC_BRID},
+          // Emacs navigation.
+          {KC_N, MOD_BIT(KC_LCTL), C(KC_P)},
+          {KC_B, MOD_BIT(KC_LCTL), LCTL(KC_F)},
+          {KC_B, MOD_BIT(KC_RCTL), RCTL(KC_F)},
+          {KC_B, MOD_BIT(KC_LALT), LALT(KC_F)},
+          {KC_F, MOD_BIT(KC_LCTL), C(KC_B)},
+          {KC_A, MOD_BIT(KC_LCTL), C(KC_E)},
+          {KC_D, MOD_BIT(KC_LCTL), C(KC_U)},
+          // Vim navigation.
+          {KC_J, 0, KC_K},
+          {KC_K, 0, KC_J},
+          {KC_H, 0, KC_L},
+          {KC_B, 0, KC_W},
+          {KC_W, 0, KC_B},
+          {KC_E, 0, KC_B},
+          {KC_B, MOD_BIT(KC_LSFT), S(KC_W)},
+          {KC_W, MOD_BIT(KC_LSFT), S(KC_B)},
+          {KC_E, MOD_BIT(KC_LSFT), S(KC_B)},
+          {KC_O, MOD_BIT(KC_LCTL), C(KC_I)},
+          {KC_I, MOD_BIT(KC_LCTL), C(KC_O)},
+          // Other.
+          {KC_DEL, 0, KC_BSPC},
+          {KC_LBRC, 0, KC_RBRC},
+          {KC_LCBR, 0, KC_RCBR},
+          // Some keys where the last key is a tap-hold key.
+          {LSFT_T(KC_F), MOD_BIT(KC_RCTL), RCTL(KC_B)},
+          {LT(1, KC_A), MOD_BIT(KC_RGUI), RGUI(KC_E)},
+          {RALT_T(KC_J), 0, KC_K},
+          // Some keys where no alternate is defined.
+          {KC_A, 0, KC_NO},
+          {KC_F1, 0, KC_NO},
+          {QK_LEAD, 0, KC_NO},
+          {MO(1), 0, KC_NO},
+             // clang-format on
+         })) {
+        SCOPED_TRACE(std::string("Input keycode: ") + get_keycode_identifier_or_default(params.keycode));
+        set_last_keycode(params.keycode);
+        set_last_mods(params.mods);
+
+        const uint16_t actual = get_alt_repeat_key_keycode();
+
+        EXPECT_KEYCODE_EQ(get_alt_repeat_key_keycode(), params.expected_alt_keycode);
+    }
+}
+
+// Test adding to and overriding the above through the
+// `get_alt_repeat_key_keycode_user()` callback.
+TEST_F(AltRepeatKey, GetAltRepeatKeyKeycodeUser) {
+    get_alt_repeat_key_keycode_user_fun = [](uint16_t keycode, uint8_t mods) -> uint16_t {
+        bool shifted = (mods & MOD_MASK_SHIFT);
+        switch (keycode) {
+            case KC_LEFT:
+                return KC_ENT;
+            case MO(1):
+                return TG(1);
+            case KC_TAB: // Tab <-> Shift + Tab example.
+                if (shifted) {
+                    return KC_TAB;
+                } else {
+                    return S(KC_TAB);
+                }
+        }
+
+        // Ctrl + Y <-> Ctrl + Z example.
+        if ((mods & MOD_MASK_CTRL)) {
+            switch (keycode) {
+                case KC_Y:
+                    return C(KC_Z);
+                case KC_Z:
+                    return C(KC_Y);
+            }
+        }
+
+        return KC_NO;
+    };
+
+    set_last_keycode(KC_LEFT);
+    EXPECT_KEYCODE_EQ(get_alt_repeat_key_keycode(), KC_ENT);
+
+    set_last_keycode(MO(1));
+    EXPECT_KEYCODE_EQ(get_alt_repeat_key_keycode(), TG(1));
+
+    set_last_keycode(KC_TAB);
+    EXPECT_KEYCODE_EQ(get_alt_repeat_key_keycode(), S(KC_TAB));
+
+    set_last_keycode(KC_TAB);
+    set_last_mods(MOD_BIT(KC_LSFT));
+    EXPECT_KEYCODE_EQ(get_alt_repeat_key_keycode(), KC_TAB);
+
+    set_last_keycode(KC_Z);
+    set_last_mods(MOD_BIT(KC_LCTL));
+    EXPECT_KEYCODE_EQ(get_alt_repeat_key_keycode(), C(KC_Y));
+
+    set_last_keycode(KC_Y);
+    set_last_mods(MOD_BIT(KC_LCTL));
+    EXPECT_KEYCODE_EQ(get_alt_repeat_key_keycode(), C(KC_Z));
+}
+
+// Tests rolling from a key to Alternate Repeat.
+TEST_F(AltRepeatKey, RollingToAltRepeat) {
+    TestDriver driver;
+    KeymapKey  key_left(0, 0, 0, KC_LEFT);
+    KeymapKey  key_alt_repeat(0, 1, 0, QK_AREP);
+    set_keymap({key_left, key_alt_repeat});
+
+    {
+        InSequence seq;
+        EXPECT_REPORT(driver, (KC_LEFT));
+        EXPECT_REPORT(driver, (KC_LEFT, KC_RGHT));
+        EXPECT_REPORT(driver, (KC_RGHT));
+        EXPECT_EMPTY_REPORT(driver);
+        EXPECT_REPORT(driver, (KC_RGHT));
+        EXPECT_EMPTY_REPORT(driver);
+    }
+
+    // Perform a rolled press from Left to Alternate Repeat.
+
+    ExpectProcessRecordUserCalledWith(true, KC_LEFT, 0);
+    key_left.press();
+    run_one_scan_loop();
+    EXPECT_TRUE(process_record_user_was_called_);
+
+    ExpectProcessRecordUserCalledWith(true, KC_RGHT, -1);
+    key_alt_repeat.press(); // Press the Alternate Repeat Key.
+    run_one_scan_loop();
+    EXPECT_TRUE(process_record_user_was_called_);
+
+    ExpectProcessRecordUserCalledWith(false, KC_LEFT, 0);
+    key_left.release();
+    run_one_scan_loop();
+    EXPECT_TRUE(process_record_user_was_called_);
+
+    ExpectProcessRecordUserCalledWith(false, KC_RGHT, -1);
+    key_alt_repeat.release(); // Release the Alternate Repeat Key.
+    run_one_scan_loop();
+    EXPECT_TRUE(process_record_user_was_called_);
+
+    process_record_user_fun = process_record_user_default;
+    tap_key(key_alt_repeat);
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Tests rolling from Alternate Repeat to another key.
+TEST_F(AltRepeatKey, RollingFromAltRepeat) {
+    TestDriver driver;
+    KeymapKey  key_left(0, 0, 0, KC_LEFT);
+    KeymapKey  key_up(0, 1, 0, KC_UP);
+    KeymapKey  key_alt_repeat(0, 2, 0, QK_AREP);
+    set_keymap({key_left, key_up, key_alt_repeat});
+
+    {
+        InSequence seq;
+        EXPECT_REPORT(driver, (KC_LEFT));
+        EXPECT_EMPTY_REPORT(driver);
+        EXPECT_REPORT(driver, (KC_RGHT));
+        EXPECT_REPORT(driver, (KC_RGHT, KC_UP));
+        EXPECT_REPORT(driver, (KC_UP));
+        EXPECT_EMPTY_REPORT(driver);
+        EXPECT_REPORT(driver, (KC_DOWN));
+        EXPECT_EMPTY_REPORT(driver);
+    }
+
+    tap_key(key_left);
+
+    // Perform a rolled press from Alternate Repeat to Up.
+
+    ExpectProcessRecordUserCalledWith(true, KC_RGHT, -1);
+    key_alt_repeat.press(); // Press the Alternate Repeat Key.
+    run_one_scan_loop();
+    EXPECT_TRUE(process_record_user_was_called_);
+
+    ExpectProcessRecordUserCalledWith(true, KC_UP, 0);
+    key_up.press();
+    run_one_scan_loop();
+    EXPECT_TRUE(process_record_user_was_called_);
+
+    EXPECT_KEYCODE_EQ(get_last_keycode(), KC_UP);
+
+    ExpectProcessRecordUserCalledWith(false, KC_RGHT, -1);
+    key_alt_repeat.release(); // Release the Alternate Repeat Key.
+    run_one_scan_loop();
+    EXPECT_TRUE(process_record_user_was_called_);
+
+    ExpectProcessRecordUserCalledWith(false, KC_UP, 0);
+    key_up.release();
+    run_one_scan_loop();
+    EXPECT_TRUE(process_record_user_was_called_);
+
+    process_record_user_fun = process_record_user_default;
+    tap_key(key_alt_repeat);
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Tests using the Alternate Repeat Key on a macro that doesn't have an
+// alternate keycode defined.
+TEST_F(AltRepeatKey, AlternateUnsupportedMacro) {
+    TestDriver driver;
+    KeymapKey  key_foo(0, 0, 0, QK_USER_0);
+    KeymapKey  key_alt_repeat(0, 1, 0, QK_AREP);
+    set_keymap({key_foo, key_alt_repeat});
+
+    process_record_user_fun = [=](uint16_t keycode, keyrecord_t* record) {
+        process_record_user_was_called_ = true;
+        switch (keycode) {
+            case QK_USER_0:
+                if (record->event.pressed) {
+                    SEND_STRING("foo");
+                }
+                break;
+        }
+        return true;
+    };
+
+    // Allow any number of empty reports.
+    EXPECT_EMPTY_REPORT(driver).Times(AnyNumber());
+    ExpectString(driver, "foofoo");
+
+    process_record_user_was_called_ = false;
+    tap_key(key_foo);
+
+    EXPECT_TRUE(process_record_user_was_called_);
+    EXPECT_KEYCODE_EQ(get_last_keycode(), QK_USER_0);
+    EXPECT_KEYCODE_EQ(get_alt_repeat_key_keycode(), KC_NO);
+
+    process_record_user_was_called_ = false;
+    key_alt_repeat.press(); // Press Alternate Repeat.
+    run_one_scan_loop();
+
+    EXPECT_FALSE(process_record_user_was_called_);
+
+    process_record_user_was_called_ = false;
+    key_alt_repeat.release(); // Release Alternate Repeat.
+    run_one_scan_loop();
+
+    EXPECT_FALSE(process_record_user_was_called_);
+
+    process_record_user_was_called_ = false;
+    tap_key(key_foo);
+
+    EXPECT_TRUE(process_record_user_was_called_);
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Tests a macro with custom alternate behavior.
+TEST_F(AltRepeatKey, MacroCustomAlternate) {
+    TestDriver driver;
+    KeymapKey  key_foo(0, 0, 0, QK_USER_0);
+    KeymapKey  key_alt_repeat(0, 1, 0, QK_AREP);
+    set_keymap({key_foo, key_alt_repeat});
+
+    get_alt_repeat_key_keycode_user_fun = [](uint16_t keycode, uint8_t mods) -> uint16_t {
+        switch (keycode) {
+            case QK_USER_0:
+                return QK_USER_0; // QK_USER_0 handles its own alternate.
+            default:
+                return KC_NO; // No key by default.
+        }
+    };
+    process_record_user_fun = [=](uint16_t keycode, keyrecord_t* record) {
+        process_record_user_was_called_ = true;
+        switch (keycode) {
+            case QK_USER_0:
+                if (record->event.pressed) {
+                    if (get_repeat_key_count() >= 0) {
+                        SEND_STRING("foo");
+                    } else { // Key is being alternate repeated.
+                        SEND_STRING("bar");
+                    }
+                }
+                break;
+        }
+        return true;
+    };
+
+    // Allow any number of empty reports.
+    EXPECT_EMPTY_REPORT(driver).Times(AnyNumber());
+    ExpectString(driver, "foobarbar");
+
+    tap_keys(key_foo, key_alt_repeat, key_alt_repeat);
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Tests the Additional "Alternate" keys example from the documentation page.
+TEST_F(AltRepeatKey, AdditionalAlternateKeysExample) {
+    TestDriver driver;
+    KeymapKey  key_a(0, 0, 0, KC_A);
+    KeymapKey  key_w(0, 1, 0, KC_W);
+    KeymapKey  key_altrep2(0, 2, 0, QK_USER_0);
+    KeymapKey  key_altrep3(0, 3, 0, QK_USER_1);
+    set_keymap({key_a, key_w, key_altrep2, key_altrep3});
+
+    remember_last_key_user_fun = [](uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) {
+        switch (keycode) {
+            case QK_USER_0:
+            case QK_USER_1:
+                return false; // Ignore ALTREP keys.
+        }
+        return true; // Other keys can be repeated.
+    };
+    process_record_user_fun = [=](uint16_t keycode, keyrecord_t* record) {
+        switch (keycode) {
+            case QK_USER_0:
+                if (record->event.pressed) {
+                    const uint16_t last_key = get_last_keycode();
+                    switch (last_key) {
+                        case KC_A:
+                            SEND_STRING(/*a*/ "tion");
+                            break;
+                        case KC_W:
+                            SEND_STRING(/*w*/ "hich");
+                            break;
+                    }
+                }
+                return false;
+            case QK_USER_1:
+                if (record->event.pressed) {
+                    const uint16_t last_key = get_last_keycode();
+                    switch (last_key) {
+                        case KC_A:
+                            SEND_STRING(/*a*/ "bout");
+                            break;
+                        case KC_W:
+                            SEND_STRING(/*w*/ "ould");
+                            break;
+                    }
+                }
+                return false;
+        }
+        return true;
+    };
+
+    // Allow any number of empty reports.
+    EXPECT_EMPTY_REPORT(driver).Times(AnyNumber());
+    ExpectString(driver, "ationwhichaboutwould");
+
+    tap_keys(key_a, key_altrep2, key_w, key_altrep2);
+    tap_keys(key_a, key_altrep3, key_w, key_altrep3);
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+} // namespace

+ 20 - 0
tests/repeat_key/config.h

@@ -0,0 +1,20 @@
+// Copyright 2023 Google LLC
+//
+// 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"
+
+#define NO_ALT_REPEAT_KEY

+ 18 - 0
tests/repeat_key/repeat_key_combo/config.h

@@ -0,0 +1,18 @@
+// Copyright 2023 Google LLC
+//
+// 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"

+ 18 - 0
tests/repeat_key/repeat_key_combo/test.mk

@@ -0,0 +1,18 @@
+# Copyright 2023 Google LLC
+#
+# 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/>.
+
+REPEAT_KEY_ENABLE = yes
+
+COMBO_ENABLE = yes

+ 67 - 0
tests/repeat_key/repeat_key_combo/test_repeat_key_combo.cpp

@@ -0,0 +1,67 @@
+// Copyright 2023 Google LLC
+//
+// 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/>.
+
+#include "keyboard_report_util.hpp"
+#include "keycode.h"
+#include "test_common.hpp"
+#include "test_fixture.hpp"
+#include "test_keymap_key.hpp"
+
+using ::testing::AnyNumber;
+using ::testing::InSequence;
+
+namespace {
+
+extern "C" {
+// Define a combo: KC_X + KC_Y = KC_Q.
+const uint16_t xy_combo[] PROGMEM = {KC_X, KC_Y, COMBO_END};
+combo_t        key_combos[]       = {COMBO(xy_combo, KC_Q)};
+uint16_t       COMBO_LEN          = sizeof(key_combos) / sizeof(*key_combos);
+} // extern "C"
+
+class RepeatKey : public TestFixture {};
+
+// Tests repeating a combo, KC_X + KC_Y = KC_Q, by typing
+// "X, Repeat, Repeat, {X Y}, Repeat, Repeat". This produces "xxxqqq".
+TEST_F(RepeatKey, Combo) {
+    TestDriver driver;
+    KeymapKey  key_x(0, 0, 0, KC_X);
+    KeymapKey  key_y(0, 1, 0, KC_Y);
+    KeymapKey  key_repeat(0, 2, 0, QK_REP);
+    set_keymap({key_x, key_y, key_repeat});
+
+    // Allow any number of empty reports.
+    EXPECT_EMPTY_REPORT(driver).Times(AnyNumber());
+    {
+        InSequence seq;
+        EXPECT_REPORT(driver, (KC_X));
+        EXPECT_REPORT(driver, (KC_X));
+        EXPECT_REPORT(driver, (KC_X));
+        EXPECT_REPORT(driver, (KC_Q));
+        EXPECT_REPORT(driver, (KC_Q));
+        EXPECT_REPORT(driver, (KC_Q));
+    }
+
+    tap_keys(key_x, key_repeat, key_repeat);
+    tap_combo({key_x, key_y});
+
+    EXPECT_KEYCODE_EQ(get_last_keycode(), KC_Q);
+
+    tap_keys(key_repeat, key_repeat);
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+} // namespace

+ 18 - 0
tests/repeat_key/test.mk

@@ -0,0 +1,18 @@
+# Copyright 2023 Google LLC
+#
+# 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/>.
+
+REPEAT_KEY_ENABLE = yes
+
+AUTO_SHIFT_ENABLE = yes

+ 754 - 0
tests/repeat_key/test_repeat_key.cpp

@@ -0,0 +1,754 @@
+// Copyright 2023 Google LLC
+//
+// 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/>.
+
+#include <functional>
+
+#include "keyboard_report_util.hpp"
+#include "keycode.h"
+#include "test_common.hpp"
+#include "test_fixture.hpp"
+#include "test_keymap_key.hpp"
+
+using ::testing::AnyNumber;
+using ::testing::AnyOf;
+using ::testing::InSequence;
+
+#define FOO_MACRO SAFE_RANGE
+
+namespace {
+
+bool process_record_user_default(uint16_t keycode, keyrecord_t* record) {
+    return true;
+}
+
+bool remember_last_key_user_default(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) {
+    return true;
+}
+
+// Indirection so that process_record_user() and remember_last_key_user()
+// can be replaced with other functions in the test cases below.
+std::function<bool(uint16_t, keyrecord_t*)>           process_record_user_fun    = process_record_user_default;
+std::function<bool(uint16_t, keyrecord_t*, uint8_t*)> remember_last_key_user_fun = remember_last_key_user_default;
+
+extern "C" bool process_record_user(uint16_t keycode, keyrecord_t* record) {
+    return process_record_user_fun(keycode, record);
+}
+extern "C" bool remember_last_key_user(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) {
+    return remember_last_key_user_fun(keycode, record, remembered_mods);
+}
+
+class RepeatKey : public TestFixture {
+   public:
+    bool process_record_user_was_called_;
+
+    void SetUp() override {
+        autoshift_disable();
+        process_record_user_fun    = process_record_user_default;
+        remember_last_key_user_fun = remember_last_key_user_default;
+    }
+
+    void ExpectProcessRecordUserCalledWith(bool expected_press, uint16_t expected_keycode, int8_t expected_repeat_key_count) {
+        process_record_user_was_called_ = false;
+        process_record_user_fun         = [=](uint16_t keycode, keyrecord_t* record) {
+            EXPECT_EQ(record->event.pressed, expected_press);
+            EXPECT_KEYCODE_EQ(keycode, expected_keycode);
+            EXPECT_EQ(get_repeat_key_count(), expected_repeat_key_count);
+            // Tests below use this to verify process_record_user() was called.
+            process_record_user_was_called_ = true;
+            return true;
+        };
+    }
+
+    // Expects that the characters of `s` are sent.
+    // NOTE: This implementation is limited to chars a-z, A-Z.
+    void ExpectString(TestDriver& driver, const std::string& s) {
+        InSequence seq;
+        for (int c : s) {
+            switch (c) {
+                case 'a' ... 'z': { // Lowercase letter.
+                    uint16_t keycode = c - ('a' - KC_A);
+                    EXPECT_REPORT(driver, (keycode));
+                } break;
+
+                case 'A' ... 'Z': { // Capital letter = KC_LSFT + letter key.
+                    uint16_t keycode = c - ('A' - KC_A);
+                    EXPECT_REPORT(driver, (KC_LSFT, keycode));
+                } break;
+            }
+        }
+    }
+};
+
+// Tests that "A, Repeat, Repeat, B, Repeat" produces "aaabb".
+TEST_F(RepeatKey, Basic) {
+    TestDriver driver;
+    KeymapKey  key_a(0, 0, 0, KC_A);
+    KeymapKey  key_b(0, 1, 0, KC_B);
+    KeymapKey  key_repeat(0, 2, 0, QK_REP);
+    set_keymap({key_a, key_b, key_repeat});
+
+    // Allow any number of empty reports.
+    EXPECT_EMPTY_REPORT(driver).Times(AnyNumber());
+    ExpectString(driver, "aaabb");
+
+    // When KC_A is pressed, process_record_user() should be called
+    // with a press event with keycode == KC_A and repeat_key_count() == 0.
+    ExpectProcessRecordUserCalledWith(true, KC_A, 0);
+    key_a.press();
+    run_one_scan_loop();
+    EXPECT_TRUE(process_record_user_was_called_);
+
+    // After pressing A, the keycode of the key to be repeated is KC_A.
+    EXPECT_KEYCODE_EQ(get_last_keycode(), KC_A);
+    EXPECT_EQ(get_last_mods(), 0);
+
+    // Expect the corresponding release event when A is released.
+    ExpectProcessRecordUserCalledWith(false, KC_A, 0);
+    key_a.release();
+    run_one_scan_loop();
+
+    for (int n = 1; n <= 2; ++n) { // Tap the Repeat Key twice.
+        // When Repeat is pressed, process_record_user() should be called with a
+        // press event with keycode == KC_A and repeat_key_count() == n.
+        ExpectProcessRecordUserCalledWith(true, KC_A, n);
+        key_repeat.press(); // Press the Repeat Key.
+        run_one_scan_loop();
+        EXPECT_TRUE(process_record_user_was_called_);
+
+        // Expect the corresponding release event.
+        ExpectProcessRecordUserCalledWith(false, KC_A, n);
+        key_repeat.release(); // Release the Repeat Key.
+        run_one_scan_loop();
+        EXPECT_TRUE(process_record_user_was_called_);
+    }
+
+    process_record_user_fun = process_record_user_default;
+    tap_key(key_b);
+    // Then after tapping key_b, the keycode to be repeated becomes KC_B.
+    EXPECT_KEYCODE_EQ(get_last_keycode(), KC_B);
+
+    tap_key(key_repeat);
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Tests repeating a macro. The keycode FOO_MACRO sends "foo" when pressed. The
+// test taps "FOO_MACRO, Repeat, Repeat", producing "foofoofoo".
+TEST_F(RepeatKey, Macro) {
+    TestDriver driver;
+    KeymapKey  key_foo(0, 0, 0, FOO_MACRO);
+    KeymapKey  key_repeat(0, 1, 0, QK_REP);
+    set_keymap({key_foo, key_repeat});
+
+    // Define process_record_user() to handle FOO_MACRO.
+    process_record_user_fun = [](uint16_t keycode, keyrecord_t* record) {
+        switch (keycode) {
+            case FOO_MACRO:
+                if (record->event.pressed) {
+                    SEND_STRING("foo");
+                }
+                break;
+        }
+        return true;
+    };
+
+    // Allow any number of empty reports.
+    EXPECT_EMPTY_REPORT(driver).Times(AnyNumber());
+    ExpectString(driver, "foofoofoo");
+
+    tap_key(key_foo);
+
+    EXPECT_KEYCODE_EQ(get_last_keycode(), FOO_MACRO);
+
+    tap_keys(key_repeat, key_repeat);
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Tests a macro with customized repeat behavior: "foo" is sent normally, "bar"
+// on the first repeat, and "baz" on subsequent repeats. The test taps
+// "FOO_MACRO, Repeat, Repeat, FOO_MACRO, Repeat", producing "foobarbazfoobar".
+TEST_F(RepeatKey, MacroCustomRepeat) {
+    TestDriver driver;
+    KeymapKey  key_foo(0, 0, 0, FOO_MACRO);
+    KeymapKey  key_repeat(0, 1, 0, QK_REP);
+    set_keymap({key_foo, key_repeat});
+
+    process_record_user_fun = [](uint16_t keycode, keyrecord_t* record) {
+        switch (keycode) {
+            case FOO_MACRO:
+                if (record->event.pressed) {
+                    switch (get_repeat_key_count()) {
+                        case 0: // When pressed normally.
+                            SEND_STRING("foo");
+                            break;
+                        case 1: // On first repeat.
+                            SEND_STRING("bar");
+                            break;
+                        default: // On subsequent repeats.
+                            SEND_STRING("baz");
+                            break;
+                    }
+                }
+                break;
+        }
+        return true;
+    };
+
+    // Allow any number of empty reports.
+    EXPECT_EMPTY_REPORT(driver).Times(AnyNumber());
+    ExpectString(driver, "foobarbazfoobar");
+
+    tap_key(key_foo);
+
+    EXPECT_KEYCODE_EQ(get_last_keycode(), FOO_MACRO);
+
+    tap_keys(key_repeat, key_repeat, key_foo, key_repeat);
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Tests repeating keys on different layers. A 2-layer keymap is defined:
+//   Layer 0:   QK_REP , MO(1)  , KC_A
+//   Layer 1:   KC_TRNS, KC_TRNS, KC_B
+// The test does the following, which should produce "bbbaaa":
+// 1. Hold MO(1), switching to layer 1.
+// 2. Tap KC_B on layer 1.
+// 3. Release MO(1), switching back to layer 0.
+// 4. Tap Repeat twice.
+// 5. Tap KC_A on layer 0.
+// 6. Hold MO(1), switching to layer 1.
+// 7. Tap Repeat twice.
+TEST_F(RepeatKey, AcrossLayers) {
+    TestDriver driver;
+    KeymapKey  key_repeat(0, 0, 0, QK_REP);
+    KeymapKey  key_mo_1(0, 1, 0, MO(1));
+    KeymapKey  regular_key(0, 2, 0, KC_A);
+    set_keymap({// Layer 0.
+                key_repeat, key_mo_1, regular_key,
+                // Layer 1.
+                KeymapKey{1, 0, 0, KC_TRNS}, KeymapKey{1, 1, 0, KC_TRNS}, KeymapKey{1, 2, 0, KC_B}});
+
+    // Allow any number of empty reports.
+    EXPECT_EMPTY_REPORT(driver).Times(AnyNumber());
+    ExpectString(driver, "bbbaaa");
+
+    key_mo_1.press(); // Hold the MO(1) layer key.
+    run_one_scan_loop();
+    tap_key(regular_key); // Taps the KC_B key on layer 1.
+
+    EXPECT_KEYCODE_EQ(get_last_keycode(), KC_B);
+
+    key_mo_1.release(); // Release the layer key.
+    run_one_scan_loop();
+    tap_keys(key_repeat, key_repeat);
+    tap_key(regular_key); // Taps the KC_A key on layer 0.
+
+    EXPECT_KEYCODE_EQ(get_last_keycode(), KC_A);
+
+    key_mo_1.press(); // Hold the layer key.
+    run_one_scan_loop();
+    tap_keys(key_repeat, key_repeat);
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Tests "A(down), Repeat(down), A(up), Repeat(up), Repeat" produces "aaa".
+TEST_F(RepeatKey, RollingToRepeat) {
+    TestDriver driver;
+    KeymapKey  key_a(0, 0, 0, KC_A);
+    KeymapKey  key_repeat(0, 1, 0, QK_REP);
+    set_keymap({key_a, key_repeat});
+
+    {
+        InSequence seq;
+        EXPECT_REPORT(driver, (KC_A));
+        EXPECT_EMPTY_REPORT(driver);
+        EXPECT_REPORT(driver, (KC_A));
+        EXPECT_EMPTY_REPORT(driver);
+        EXPECT_REPORT(driver, (KC_A));
+        EXPECT_EMPTY_REPORT(driver);
+    }
+
+    // Perform a rolled press from A to Repeat.
+
+    ExpectProcessRecordUserCalledWith(true, KC_A, 0);
+    key_a.press();
+    run_one_scan_loop();
+    EXPECT_TRUE(process_record_user_was_called_);
+
+    ExpectProcessRecordUserCalledWith(true, KC_A, 1);
+    key_repeat.press(); // Press the Repeat Key.
+    run_one_scan_loop();
+    EXPECT_TRUE(process_record_user_was_called_);
+
+    ExpectProcessRecordUserCalledWith(false, KC_A, 0);
+    key_a.release();
+    run_one_scan_loop();
+    EXPECT_TRUE(process_record_user_was_called_);
+
+    ExpectProcessRecordUserCalledWith(false, KC_A, 1);
+    key_repeat.release(); // Release the Repeat Key.
+    run_one_scan_loop();
+    EXPECT_TRUE(process_record_user_was_called_);
+
+    process_record_user_fun = process_record_user_default;
+    tap_key(key_repeat);
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Tests "A, Repeat(down), B(down), Repeat(up), B(up), Repeat" produces "aabb".
+TEST_F(RepeatKey, RollingFromRepeat) {
+    TestDriver driver;
+    KeymapKey  key_a(0, 0, 0, KC_A);
+    KeymapKey  key_b(0, 1, 0, KC_B);
+    KeymapKey  key_repeat(0, 2, 0, QK_REP);
+    set_keymap({key_a, key_b, key_repeat});
+
+    {
+        InSequence seq;
+        EXPECT_REPORT(driver, (KC_A));
+        EXPECT_EMPTY_REPORT(driver);
+        EXPECT_REPORT(driver, (KC_A));
+        EXPECT_REPORT(driver, (KC_A, KC_B));
+        EXPECT_REPORT(driver, (KC_B));
+        EXPECT_EMPTY_REPORT(driver);
+        EXPECT_REPORT(driver, (KC_B));
+        EXPECT_EMPTY_REPORT(driver);
+    }
+
+    tap_key(key_a);
+
+    // Perform a rolled press from Repeat to B.
+
+    ExpectProcessRecordUserCalledWith(true, KC_A, 1);
+    key_repeat.press(); // Press the Repeat Key.
+    run_one_scan_loop();
+    EXPECT_TRUE(process_record_user_was_called_);
+
+    ExpectProcessRecordUserCalledWith(true, KC_B, 0);
+    key_b.press();
+    run_one_scan_loop();
+    EXPECT_TRUE(process_record_user_was_called_);
+
+    EXPECT_KEYCODE_EQ(get_last_keycode(), KC_B);
+
+    ExpectProcessRecordUserCalledWith(false, KC_A, 1);
+    key_repeat.release(); // Release the Repeat Key.
+    run_one_scan_loop();
+    EXPECT_TRUE(process_record_user_was_called_);
+
+    ExpectProcessRecordUserCalledWith(false, KC_B, 0);
+    key_b.release();
+    run_one_scan_loop();
+    EXPECT_TRUE(process_record_user_was_called_);
+
+    process_record_user_fun = process_record_user_default;
+    tap_key(key_repeat);
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Tests Repeat Key with a modifier, types "AltGr+C, Repeat, Repeat, C".
+TEST_F(RepeatKey, RecallMods) {
+    TestDriver driver;
+    KeymapKey  key_c(0, 0, 0, KC_C);
+    KeymapKey  key_altgr(0, 1, 0, KC_RALT);
+    KeymapKey  key_repeat(0, 2, 0, QK_REP);
+    set_keymap({key_c, key_altgr, key_repeat});
+
+    // Allow any number of reports with no keys or only KC_RALT.
+    // clang-format off
+    EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
+                KeyboardReport(),
+                KeyboardReport(KC_RALT))))
+        .Times(AnyNumber());
+    // clang-format on
+
+    { // Expect: "AltGr+C, AltGr+C, AltGr+C, C".
+        InSequence seq;
+        EXPECT_REPORT(driver, (KC_RALT, KC_C));
+        EXPECT_REPORT(driver, (KC_RALT, KC_C));
+        EXPECT_REPORT(driver, (KC_RALT, KC_C));
+        EXPECT_REPORT(driver, (KC_C));
+    }
+
+    key_altgr.press();
+    run_one_scan_loop();
+    tap_key(key_c);
+    key_altgr.release();
+    run_one_scan_loop();
+
+    EXPECT_KEYCODE_EQ(get_last_keycode(), KC_C);
+    EXPECT_EQ(get_last_mods(), MOD_BIT(KC_RALT));
+
+    tap_keys(key_repeat, key_repeat, key_c);
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Tests that Repeat Key stacks mods, types
+// "Ctrl+Left, Repeat, Shift+Repeat, Shift+Repeat, Repeat, Left".
+TEST_F(RepeatKey, StackMods) {
+    TestDriver driver;
+    KeymapKey  key_left(0, 0, 0, KC_LEFT);
+    KeymapKey  key_shift(0, 1, 0, KC_LSFT);
+    KeymapKey  key_ctrl(0, 2, 0, KC_LCTL);
+    KeymapKey  key_repeat(0, 3, 0, QK_REP);
+    set_keymap({key_left, key_shift, key_ctrl, key_repeat});
+
+    // Allow any number of reports with no keys or only mods.
+    // clang-format off
+    EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
+                KeyboardReport(),
+                KeyboardReport(KC_LCTL),
+                KeyboardReport(KC_LSFT),
+                KeyboardReport(KC_LCTL, KC_LSFT))))
+        .Times(AnyNumber());
+    // clang-format on
+
+    { // Expect: "Ctrl+Left, Ctrl+Shift+Left".
+        InSequence seq;
+        EXPECT_REPORT(driver, (KC_LCTL, KC_LEFT));
+        EXPECT_REPORT(driver, (KC_LCTL, KC_LEFT));
+        EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT, KC_LEFT));
+        EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT, KC_LEFT));
+        EXPECT_REPORT(driver, (KC_LCTL, KC_LEFT));
+        EXPECT_REPORT(driver, (KC_LEFT));
+    }
+
+    key_ctrl.press();
+    run_one_scan_loop();
+    tap_key(key_left);
+    run_one_scan_loop();
+    key_ctrl.release();
+    run_one_scan_loop();
+
+    EXPECT_KEYCODE_EQ(get_last_keycode(), KC_LEFT);
+    EXPECT_EQ(get_last_mods(), MOD_BIT(KC_LCTL));
+
+    tap_key(key_repeat);
+
+    key_shift.press();
+    run_one_scan_loop();
+    tap_keys(key_repeat, key_repeat);
+    key_shift.release();
+    run_one_scan_loop();
+
+    EXPECT_EQ(get_last_mods(), MOD_BIT(KC_LCTL));
+
+    tap_keys(key_repeat, key_left);
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Types: "S(KC_1), Repeat, Ctrl+Repeat, Ctrl+Repeat, Repeat, KC_2".
+TEST_F(RepeatKey, ShiftedKeycode) {
+    TestDriver driver;
+    KeymapKey  key_exlm(0, 0, 0, S(KC_1));
+    KeymapKey  key_2(0, 1, 0, KC_2);
+    KeymapKey  key_ctrl(0, 2, 0, KC_LCTL);
+    KeymapKey  key_repeat(0, 3, 0, QK_REP);
+    set_keymap({key_exlm, key_2, key_ctrl, key_repeat});
+
+    // Allow any number of reports with no keys or only mods.
+    // clang-format off
+    EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
+                KeyboardReport(),
+                KeyboardReport(KC_LCTL),
+                KeyboardReport(KC_LSFT),
+                KeyboardReport(KC_LCTL, KC_LSFT))))
+        .Times(AnyNumber());
+    // clang-format on
+
+    { // Expect: "Shift+1, Shift+1, Ctrl+Shift+1, Ctrl+Shift+1, Shift+1, 2".
+        InSequence seq;
+        EXPECT_REPORT(driver, (KC_LSFT, KC_1));
+        EXPECT_REPORT(driver, (KC_LSFT, KC_1));
+        EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT, KC_1));
+        EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT, KC_1));
+        EXPECT_REPORT(driver, (KC_LSFT, KC_1));
+        EXPECT_REPORT(driver, (KC_2));
+    }
+
+    tap_key(key_exlm);
+
+    EXPECT_KEYCODE_EQ(get_last_keycode(), S(KC_1));
+
+    tap_key(key_repeat);
+
+    key_ctrl.press();
+    run_one_scan_loop();
+    tap_keys(key_repeat, key_repeat);
+    key_ctrl.release();
+    run_one_scan_loop();
+
+    tap_keys(key_repeat, key_2);
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Tests Repeat Key with a one-shot Shift, types
+// "A, OSM(MOD_LSFT), Repeat, Repeat".
+TEST_F(RepeatKey, WithOneShotShift) {
+    TestDriver driver;
+    KeymapKey  key_a(0, 0, 0, KC_A);
+    KeymapKey  key_oneshot_shift(0, 1, 0, OSM(MOD_LSFT));
+    KeymapKey  key_repeat(0, 2, 0, QK_REP);
+    set_keymap({key_a, key_oneshot_shift, key_repeat});
+
+    // Allow any number of reports with no keys or only KC_RALT.
+    // clang-format off
+    EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
+                KeyboardReport(),
+                KeyboardReport(KC_LSFT))))
+        .Times(AnyNumber());
+    // clang-format on
+    ExpectString(driver, "aAa");
+
+    tap_keys(key_a, key_oneshot_shift, key_repeat, key_repeat);
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Tests Repeat Key with a mod-tap key, types
+// "A, Repeat, Repeat, A(down), Repeat, Repeat, A(up), Repeat".
+TEST_F(RepeatKey, ModTap) {
+    TestDriver driver;
+    KeymapKey  key_mt_a(0, 0, 0, LSFT_T(KC_A));
+    KeymapKey  key_repeat(0, 1, 0, QK_REP);
+    set_keymap({key_mt_a, key_repeat});
+
+    // Allow any number of reports with no keys or only KC_LSFT.
+    // clang-format off
+    EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
+                KeyboardReport(),
+                KeyboardReport(KC_LSFT))))
+        .Times(AnyNumber());
+    // clang-format on
+    ExpectString(driver, "aaaAAa");
+
+    tap_key(key_mt_a);
+
+    EXPECT_KEYCODE_EQ(get_last_keycode(), LSFT_T(KC_A));
+
+    tap_keys(key_repeat, key_repeat);
+    key_mt_a.press();
+    run_one_scan_loop();
+    tap_key(key_repeat, TAPPING_TERM + 1);
+    tap_key(key_repeat);
+    key_mt_a.release();
+    run_one_scan_loop();
+    tap_key(key_repeat);
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Tests with Auto Shift. When repeating an autoshiftable key, it does not
+// matter how long the original key was held, rather, quickly tapping vs.
+// long-pressing the Repeat Key determines whether the shifted key is repeated.
+//
+// The test does the following, which should produce "aaABbB":
+// 1. Tap KC_A quickly.
+// 2. Tap Repeat Key quickly.
+// 3. Long-press Repeat Key.
+// 4. Long-press KC_B.
+// 5. Tap Repeat Key quickly.
+// 6. Long-press Repeat Key.
+TEST_F(RepeatKey, AutoShift) {
+    TestDriver driver;
+    KeymapKey  key_a(0, 0, 0, KC_A);
+    KeymapKey  key_b(0, 1, 0, KC_B);
+    KeymapKey  key_repeat(0, 2, 0, QK_REP);
+    set_keymap({key_a, key_b, key_repeat});
+
+    autoshift_enable();
+
+    // Allow any number of reports with no keys or only KC_LSFT.
+    // clang-format off
+    EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
+                KeyboardReport(),
+                KeyboardReport(KC_LSFT))))
+        .Times(AnyNumber());
+    // clang-format on
+    ExpectString(driver, "aaABbB");
+
+    tap_key(key_a); // Tap A quickly.
+
+    EXPECT_KEYCODE_EQ(get_last_keycode(), KC_A);
+    EXPECT_EQ(get_last_mods(), 0);
+
+    tap_key(key_repeat);
+    tap_key(key_repeat, AUTO_SHIFT_TIMEOUT + 1);
+
+    tap_key(key_b, AUTO_SHIFT_TIMEOUT + 1); // Long press B.
+
+    EXPECT_KEYCODE_EQ(get_last_keycode(), KC_B);
+    EXPECT_EQ(get_last_mods(), 0);
+
+    tap_key(key_repeat);
+    tap_key(key_repeat, AUTO_SHIFT_TIMEOUT + 1);
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Defines `remember_last_key_user()` to forget the Shift mod and types:
+// "Ctrl+A, Repeat, Shift+A, Repeat, Shift+Repeat".
+TEST_F(RepeatKey, FilterRememberedMods) {
+    TestDriver driver;
+    KeymapKey  key_a(0, 0, 0, KC_A);
+    KeymapKey  key_ctrl(0, 1, 0, KC_LCTL);
+    KeymapKey  key_shift(0, 2, 0, KC_LSFT);
+    KeymapKey  key_repeat(0, 3, 0, QK_REP);
+    set_keymap({key_a, key_ctrl, key_shift, key_repeat});
+
+    remember_last_key_user_fun = [](uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) {
+        *remembered_mods &= ~MOD_MASK_SHIFT;
+        return true;
+    };
+
+    // Allow any number of reports with no keys or only mods.
+    // clang-format off
+    EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
+                KeyboardReport(),
+                KeyboardReport(KC_LCTL),
+                KeyboardReport(KC_LSFT),
+                KeyboardReport(KC_LCTL, KC_LSFT))))
+        .Times(AnyNumber());
+    // clang-format on
+
+    { // Expect: "Ctrl+A, Ctrl+A, Shift+A, A, Shift+A".
+        InSequence seq;
+        EXPECT_REPORT(driver, (KC_LCTL, KC_A));
+        EXPECT_REPORT(driver, (KC_LCTL, KC_A));
+        EXPECT_REPORT(driver, (KC_LSFT, KC_A));
+        EXPECT_REPORT(driver, (KC_A));
+        EXPECT_REPORT(driver, (KC_LSFT, KC_A));
+    }
+
+    key_ctrl.press();
+    run_one_scan_loop();
+    tap_key(key_a);
+
+    EXPECT_EQ(get_last_mods(), MOD_BIT(KC_LCTL));
+
+    key_ctrl.release();
+    run_one_scan_loop();
+
+    tap_key(key_repeat);
+    key_shift.press();
+    run_one_scan_loop();
+    tap_key(key_a);
+
+    EXPECT_EQ(get_last_mods(), 0); // Shift should be forgotten.
+
+    key_shift.release();
+    run_one_scan_loop();
+
+    tap_key(key_repeat);
+
+    key_shift.press();
+    run_one_scan_loop();
+    tap_key(key_repeat);
+    key_shift.release();
+    run_one_scan_loop();
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Tests set_last_keycode() and set_last_mods().
+TEST_F(RepeatKey, SetRepeatKeyKeycode) {
+    TestDriver driver;
+    KeymapKey  key_repeat(0, 0, 0, QK_REP);
+    set_keymap({key_repeat});
+
+    // Allow any number of reports with no keys or only KC_LSFT.
+    // clang-format off
+    EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
+                KeyboardReport(),
+                KeyboardReport(KC_LSFT))))
+        .Times(AnyNumber());
+    // clang-format on
+    ExpectString(driver, "aaBB");
+
+    set_last_keycode(KC_A);
+    EXPECT_KEYCODE_EQ(get_last_keycode(), KC_A);
+
+    for (int n = 1; n <= 2; ++n) { // Tap the Repeat Key twice.
+        // When Repeat is pressed, process_record_user() should be called with a
+        // press event with keycode == KC_A and repeat_key_count() == n.
+        ExpectProcessRecordUserCalledWith(true, KC_A, n);
+        key_repeat.press(); // Press the Repeat Key.
+        run_one_scan_loop();
+        EXPECT_TRUE(process_record_user_was_called_);
+
+        // Expect the corresponding release event.
+        ExpectProcessRecordUserCalledWith(false, KC_A, n);
+        key_repeat.release(); // Release the Repeat Key.
+        run_one_scan_loop();
+        EXPECT_TRUE(process_record_user_was_called_);
+    }
+
+    process_record_user_fun = process_record_user_default;
+    set_last_keycode(KC_B);
+    set_last_mods(MOD_BIT(KC_LSFT));
+
+    tap_keys(key_repeat, key_repeat);
+
+    set_last_keycode(KC_NO);
+    tap_keys(key_repeat, key_repeat); // Has no effect.
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Tests the `repeat_key_invoke()` function.
+TEST_F(RepeatKey, RepeatKeyInvoke) {
+    TestDriver driver;
+    KeymapKey  key_s(0, 0, 0, KC_S);
+    set_keymap({key_s});
+
+    // Allow any number of empty reports.
+    EXPECT_EMPTY_REPORT(driver).Times(AnyNumber());
+    ExpectString(driver, "ss");
+
+    tap_key(key_s);
+
+    EXPECT_KEYCODE_EQ(get_last_keycode(), KC_S);
+
+    // Calling repeat_key_invoke() should result in process_record_user()
+    // getting a press event with keycode KC_S.
+    ExpectProcessRecordUserCalledWith(true, KC_S, 1);
+    keyevent_t event;
+    event.key     = {0, 0};
+    event.pressed = true;
+    event.time    = timer_read();
+    event.type    = KEY_EVENT;
+    repeat_key_invoke(&event);
+    run_one_scan_loop();
+    EXPECT_TRUE(process_record_user_was_called_);
+
+    // Make the release event.
+    ExpectProcessRecordUserCalledWith(false, KC_S, 1);
+    event.pressed = false;
+    event.time    = timer_read();
+    repeat_key_invoke(&event);
+    run_one_scan_loop();
+    EXPECT_TRUE(process_record_user_was_called_);
+
+    testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+} // namespace

+ 2 - 0
tests/test_common/keycode_table.cpp

@@ -663,6 +663,8 @@ std::map<uint16_t, std::string> KEYCODE_ID_TABLE = {
     {QK_AUTOCORRECT_TOGGLE, "QK_AUTOCORRECT_TOGGLE"},
     {QK_TRI_LAYER_LOWER, "QK_TRI_LAYER_LOWER"},
     {QK_TRI_LAYER_UPPER, "QK_TRI_LAYER_UPPER"},
+    {QK_REPEAT_KEY, "QK_REPEAT_KEY"},
+    {QK_ALT_REPEAT_KEY, "QK_ALT_REPEAT_KEY"},
     {QK_KB_0, "QK_KB_0"},
     {QK_KB_1, "QK_KB_1"},
     {QK_KB_2, "QK_KB_2"},

+ 12 - 0
tests/test_common/test_driver.hpp

@@ -20,6 +20,7 @@
 #include <stdint.h>
 #include "host.h"
 #include "keyboard_report_util.hpp"
+#include "keycode_util.hpp"
 #include "test_logger.hpp"
 
 class TestDriver {
@@ -98,6 +99,17 @@ class TestDriver {
  */
 #define EXPECT_NO_REPORT(driver) EXPECT_ANY_REPORT(driver).Times(0)
 
+/** @brief Tests whether keycode `actual` is equal to `expected`. */
+#define EXPECT_KEYCODE_EQ(actual, expected) EXPECT_THAT((actual), KeycodeEq((expected)))
+
+MATCHER_P(KeycodeEq, expected_keycode, "is equal to " + testing::PrintToString(expected_keycode) + ", keycode " + get_keycode_identifier_or_default(expected_keycode)) {
+    if (arg == expected_keycode) {
+        return true;
+    }
+    *result_listener << "keycode " << get_keycode_identifier_or_default(arg);
+    return false;
+}
+
 /**
  * @brief Verify and clear all gmock expectations that have been setup until
  * this point.