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