QMK: Repeat Key
Pascal Getreuer, 2022-12-29 (updated 2023-01-28)
“Experience isn’t interesting until it begins to repeat itself. In fact, till it does that, it hardly is experience.” — Elizabeth Bowen
Overview
This post describes an extensible “repeat last key” implementation.
Repeat Key performs the action of the last pressed key. For instance,
tapping the Repeat Key after tapping the Z key types another
“z
.”
A big interest in keyboard layout design is reduction of “same-finger
bigrams” or SFBs, 2-letter patterns where the same finger is used to
type successive keys. The finger motions to type SFBs tend to be slow
and awkward. Doubled letters like the z
in
“dazzle
” are essentially SFBs as well when typed
conventionally by double tapping. The Repeat Key breaks up these double
taps, e.g. rolling from Z to Repeat, which is
potentially faster and more comfortable. As described by
NotGate:
The repeat key fits the theme of reducing SFBs by as much as possible and has the bonus of including the 10th finger. Words like follow become ‘fol[rep]ow’ so your flow is never broken by the speed of a single finger. It might seem overkill but you quickly get used to it and it’s great for keeping a steady pace. There are actually far more double letters than SFBs in typing, so technically this matters more for preserving flow than the layout’s SFB minimization 🙃.
The implementation is a generic event-plumbing strategy that interoperates predictably with most QMK features, including tap-hold keys, Auto Shift, Combos, and userspace macros.
Add it to your keymap
If you are new to QMK macros, see my macro buttons post for an intro.
Step 1: Repeat Key requires that Combos are enabled. If you aren’t using Combos already, set in rules.mk:
COMBO_ENABLE = yes
It is not required that you use combos for anything in your keymap.
But at a minimum, you need to define an empty key_combos
array by adding the following in keymap.c:
[] = {};
combo_t key_combosuint16_t COMBO_LEN = 0;
Step 2: In your keymap.c
, add a custom
keycode for the Repeat Key and use the new keycode in your layout. I’ll
name it REPEAT
, but you can call it anything you like.
enum custom_keycodes {
= SAFE_RANGE,
REPEAT // Other custom keys...
};
A suggestion thanks to NotGate is to put REPEAT
where
Right Alt would be on a traditional keyboard or on a key pressed by the
thumb that doesn’t press Space.

Step 3: Handle Repeat Key from your
process_record_user()
function by calling
process_repeat_key()
, passing your custom keycode as the
third argument:
#include "features/repeat_key.h"
bool process_record_user(uint16_t keycode, keyrecord_t* record) {
if (!process_repeat_key(keycode, record, REPEAT)) { return false; }
// Your macros ...
return true;
}
If your process_record_user()
has other handlers or
macros, Repeat Key’s handler process_repeat_key()
should
preferably be called before anything else. (If you also use Achordion, then call Achordion’s
handler first, Repeat Key’s handler second, and then other
handlers.)
Step 4: In rules.mk, add the line
SRC += features/repeat_key.c
Step 5: Finally, in the directory containing your
keymap.c
, create a features
subdirectory and
copy repeat_key.h
and repeat_key.c
there.
With the above flashed to your keyboard, Repeat Key is now ready to
use. To test, tap the Z key and then tap the
Repeat key a few times. This should produce
“zzzz
.”
Repeat Key remembers modifiers that were active with the last key
press. These modifiers are combined with any additional modifiers while
pressing the Repeat Key. If the last press key was Ctrl +
Z, then Shift + Repeat performs Ctrl +
Shift + Z
.
Repeat using a Combo
Instead of dedicating a key for Repeat, you might want to trigger it
with a Combo. This can
be done by adding COMBO_ENABLE = yes
in rules.mk and
defining a combo in keymap.c like
// Period + Backspace => Repeat.
const uint16_t repeat_combo[] PROGMEM = {KC_DOT, KC_BSPC, COMBO_END};
[] = {
combo_t key_combos(repeat_combo, REPEAT),
COMBO};
uint16_t COMBO_LEN = sizeof(key_combos) / sizeof(*key_combos);
Alternative implementations
I’m aware of a few existing “repeat last key” implementations:
Kanata is a software key remapper for Windows and Linux with a repeat key feature, among many other features.
With AutoHotkey, NotGate’s script implements repeat based on
A_PriorKey
.With ZMK firmware, repeat is a core feature.
With QMK firmware, there are
- Jonas Hietala’s repeat key (also reverse repeat key)
- Precondition’s repeat key, with contributions from @Apsu and @KapJI
Reverse Repeat Key
An idea thanks to Jonas Hietala is a complementary “Reverse Repeat Key.” When the last key is Page Down, the Reverse Repeat Key performs Page Up. Or when the last key is the common “select by word” hotkey Ctrl + Shift + Right Arrow, the Reverse Repeat Key performs Ctrl + Shift + Left Arrow, which together with the Repeat Key enables convenient selection by words in either direction.
Do the following to use the Reverse Repeat Key in your keymap.c:
Add a custom keycode for Reverse Repeat, say
REVREP
, and use it in your layout.In
process_record_user()
, handle Repeat and Reverse Repeat asif (!process_repeat_key_with_rev(keycode, record, REPEAT, REVREP)) { return false; }
The following reverse keys are defined by default. See
get_rev_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 ↔︎ Ctrl + Right
Arrow.
Navigation
Keycodes | Description |
---|---|
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_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 |
Misc
Keycodes | Description |
---|---|
KC_BSPC ↔︎ KC_DEL |
Backspace ↔︎ Delete |
KC_LBRC ↔︎ KC_RBRC |
[ ↔︎ ] |
KC_LCBR ↔︎ KC_RCBR |
{ ↔︎ } |
Media
Keycodes | Description |
---|---|
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 |
Hotkeys in Vim, Emacs, and other programs
Keycodes | Description |
---|---|
mod + KC_F ↔︎ mod + KC_B |
Forward ↔︎ Backward |
mod + KC_D ↔︎ mod + KC_U |
Down ↔︎ Up |
mod + KC_N ↔︎ mod + KC_P |
Next ↔︎ Previous |
mod + KC_A ↔︎ mod + KC_E |
Home ↔︎ End |
KC_J ↔︎ KC_K |
Down ↔︎ Up |
KC_H ↔︎ KC_L |
Left ↔︎ Right |
KC_W ↔︎ KC_B |
Forward ↔︎ Backward by Word |
(where above, “mod” is Ctrl, Alt, or GUI)
Customization
Defining reverse keys
Use the get_rev_repeat_key_keycode_user()
callback to
define the “reverse” key for additional keys or override the default
definitions described above. For example, to define Ctrl + Y as the
reverse of Ctrl + Z, and vice versa, add the following in keymap.c:
uint16_t get_rev_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_NO;
}
The keycode
and mods
args are the keycode
and mods that were active with the last pressed key. The function
returns the keycode for the reverse key, or KC_NO
to defer
to the default definitions. Any keycode may be returned as a reverse
key, including custom keycodes.
Another example, defining Shift + Tab as the reverse of Tab, and vice versa:
uint16_t get_rev_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_NO;
}
Ignoring certain keys
By default, the Repeat Key and Reverse Repeat Key ignore modifier and
layer switch keys in tracking what the “last” key was. This enables
possibly setting some mods and changing layers between pressing a key
and repeating it. To customize which keys are ignored, define
get_repeat_key_eligible()
in your keymap.c. The default
implementation is
bool get_repeat_key_eligible(uint16_t keycode, keyrecord_t* record) {
switch (keycode) {
// Ignore MO, TO, TG, and TT 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:
// Ignore one-shot keys.
#ifndef NO_ACTION_ONESHOT
case QK_ONE_SHOT_LAYER ... QK_ONE_SHOT_LAYER_MAX:
case QK_ONE_SHOT_MOD ... QK_ONE_SHOT_MOD_MAX:
#endif // NO_ACTION_ONESHOT
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
}
return true;
}
This callback is called on every key press. Returning true indicates the key is eligible for repeating, while false means it is ignored.
Besides checking the keycode, this callback could also make
conditions based on the current layer state (e.g. with
IS_LAYER_ON(layer)
) or mods (get_mods()
).
Handle how a key is repeated
By default, pressing Repeat 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 how many times the key has been repeated or reverse 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 reverse repeating. For instance
supposing MY_MACRO
is a custom keycode used in the
layout:
bool process_record_user(uint16_t keycode, keyrecord_t* record) {
if (!process_repeat_key(keycode, record, REPEAT)) { return false; }
switch (keycode) {
case MY_MACRO:
if (get_repeat_key_count() > 0) { // MY_MACRO is being repeated!
if (record->event.pressed) {
("repeat!");
SEND_STRING}
} else { // MY_MACRO is being used normally.
if (record->event.pressed) {
("macro");
SEND_STRING}
}
return false;
// Other macros...
}
return true;
}
Handle how a key is reverse repeated
Pressing the Reverse Repeat Key behaves as if the “reverse” of the
last pressed key were pressed, provided a reverse is defined. To define
how a particular key is reverse repeated, use the
get_rev_repeat_key_keycode_user()
callback to define which
keycode to use as its reverse. Beyond this, use
get_repeat_key_count()
to specially consider reverse
repeating in custom handlers.
The following example defines MY_MACRO
as its own
reverse, and defines distinct behavior for repeating and reverse
repeating:
uint16_t get_rev_repeat_key_keycode_user(uint16_t keycode, uint8_t mods) {
switch (keycode) {
case MY_MACRO: return MY_MACRO; // Define MY_MACRO as its own reverse.
}
return KC_NO;
}
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) {
("repeat!");
SEND_STRING}
} else if (get_repeat_key_count() < 0) { // Reverse repeating.
if (record->event.pressed) {
("reverse!");
SEND_STRING}
} else { // Used normally.
if (record->event.pressed) {
("macro");
SEND_STRING}
}
return false;
// Other macros...
}
return true;
}
Functions
Function | Description |
---|---|
get_repeat_key_count() |
Signed count of times the key has been repeated or reverse repeated. |
get_repeat_key_keycode() |
The last keycode, the key to be repeated. |
get_repeat_key_mods() |
Mods to apply when repeating. |
set_repeat_key_keycode(kc) |
Set the keycode to be repeated. |
set_repeat_key_mods(mods) |
Set the mods to apply when repeating. |
repeat_key_register() |
Press down the Repeat Key. |
repeat_key_unregister() |
Release the Repeat Key. |
repeat_key_tap() |
Tap the Repeat Key. |
There are a few analogous functions for the Reverse Repeat Key:
Function | Description |
---|---|
get_rev_repeat_key_keycode() |
Keycode used for reverse repeating, or KC_NO if the
last key has no reverse. |
rev_repeat_key_register() |
Press down the Reverse Repeat Key. |
rev_repeat_key_unregister() |
Release the Reverse Repeat Key. |
rev_repeat_key_tap() |
Tap the Reverse Repeat Key. |
The repeat_key_register()
,
repeat_key_unregister()
, and repeat_key_tap()
functions and corresponding functions for Reverse Repeat Key are useful
for invoking Repeat and Reverse Repeat Key from an encoder, leader key
sequence, or other custom handler. Note that if doing so, you likely
want to define get_repeat_key_eligible()
as described below
to ignore the keycode associated with that handler so that the Repeat
Key does not attempt to repeat or reverse-repeat itself.
Implementation details
If you are interested in the technical details, this section is an explanation of how Repeat Key and Reverse Repeat Key are implemented.
Tracking the last key
On every key press event, repeat_key_press_user()
is
called to check whether that key is eligible for repeating. If so, the
keycode, keyrecord_t
record, and modifiers state are all
stored. The value of repeat_key_count()
is reset to zero to
indicate to subsequent handlers that the key is being pressed
normally.
Repeat Key
When the Repeat Key is pressed or released, we synthesize a new keyrecord on the last key using the stored variables, then pass it back into the event pipeline:
On press, the stored modifiers are recalled and applied as weak mods, and we we increment the value of
get_repeat_key_count()
.We set the
record.event.pressed
andrecord.event.time
fields for the new keyrecord, then pass it into the event pipeline by callingprocess_record()
to process it. However, as the Repeat Key handler is itself a part the event pipeline, a complication is that this call may recurse back into the Repeat Key handler. To avoid the possibility for an infinite loop, we set aprocessing_repeat_count
variable while callingprocess_record()
and have the handler return early when this count is set.On release, the weak mods are removed.
Reverse Repeat Key
The Reverse Repeat Key is implemented by looking up a reverse keycode
for the last key and then, similarly, plumbing an event into
process_record()
for that keycode. An essential piece to
making this work is that we set the desired reverse keycode in the
keyrecord_t
’s .keycode
field, and this field
is only present when Combos are enabled.