QMK: Repeat Key
Pascal Getreuer, 2022-12-29 (updated 2023-05-28)
🚀 Launched
Repeat Key is now a core QMK feature! It was released on 2023-05-28. Update your QMK set up and see QMK Repeat Key. Or if you want, you may continue to use the userspace implementation described in this page.
“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
.” Repeat Key is useful especially for repeating hotkey
chords, like Ctrl + Shift + Right Arrow to select by words.
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
Alternate Repeat Key
An idea thanks to Jonas Hietala is a complementary “Reverse” or “Alternate Repeat Key.” By default it is defined for navigation keys to act in the reverse direction. When the last key is Page Down, the Alternate Repeat Key performs Page Up. Or 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.
Do the following to use the Alternate Repeat Key in your keymap.c:
Add a custom keycode for Alternate Repeat, say
ALTREP
, and use it in your layout.In
process_record_user()
, handle Repeat and Alternate Repeat asif (!process_repeat_key_with_alt(keycode, record, REPEAT, ALTREP)) { return false; }
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 ↔︎ 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 |
mod + KC_O ↔︎ mod + KC_I |
Vim Jumplist Older ↔︎ Newer |
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 alternate keys
Use the get_alt_repeat_key_keycode_user()
callback to
define the “alternate” key for additional keys or override the default
definitions described above. For example, to define Ctrl + Y as the
alternate of Ctrl + Z, and vice versa, add the following in
keymap.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 the defaults.
}
The keycode
and mods
args are the keycode
and mods that were active with the last pressed key. The meaning of the
return value is:
KC_NO
: do nothing (any default definition is not used),KC_TRNS
: use the default alt repeat key if it exists,- anything else: use the specified keycode.
Any keycode may be returned as a alternate key, including custom keycodes.
Another example, defining Shift + Tab as the alternate of Tab, and vice versa:
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;
}
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 the worst same-finger bigrams in your layout. The following
addresses the top 5 same-finger bigrams in QWERTY, so that for instance
“ed
” may be typed as E, Alt
Repeat.
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;
}
Ignoring certain keys
In tracking what the “last key” to be repeated is, the Repeat Key and
Alternate 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 ignore
additional keys, define remember_last_key_user()
in your
keymap.c. The default implementation simply defines that all
non-modifier, non-layer switch keys are remembered:
bool remember_last_key_user(uint16_t keycode, keyrecord_t* record,
uint8_t* remembered_mods) {
return true;
}
This callback is called on every key press. Returning true indicates the key is remembered, and therefore 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()
).
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 become “AAron”:
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;
}
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 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:
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 alternate repeated
Pressing the Alternate Repeat Key behaves as if the “alternate” of
the last pressed key were pressed, provided an alternate is defined. To
define how a particular key is alternate repeated, use the
get_alt_repeat_key_keycode_user()
callback to define which
keycode to use as its alternate. Beyond this, use
get_repeat_key_count()
to specially consider alternate
repeating in custom handlers.
The following example defines MY_MACRO
as its own
alternate, and defines distinct behavior for repeating and alternate
repeating:
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) {
if (!process_repeat_key_with_alt(keycode, record, REPEAT, ALTREP)) {
return false;
}
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) { // Alternate repeating.
if (record->event.pressed) {
("alternate!");
SEND_STRING}
} else { // Used normally.
if (record->event.pressed) {
("macro");
SEND_STRING}
}
return false;
// Other macros...
}
return true;
}
Functions
Function | Description |
---|---|
get_last_keycode() |
The last 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. |
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 Alternate Repeat Key:
Function | Description |
---|---|
get_alt_repeat_key_keycode() |
Keycode used for alternate repeating, or KC_NO if the
last key has no alternate. |
alt_repeat_key_register() |
Press down the Alternate Repeat Key. |
alt_repeat_key_unregister() |
Release the Alternate Repeat Key. |
alt_repeat_key_tap() |
Tap the Alternate Repeat Key. |
The repeat_key_register()
,
repeat_key_unregister()
, and repeat_key_tap()
functions and corresponding functions for Alternate Repeat Key are
useful for invoking Repeat and Alternate Repeat Key from an encoder,
leader key sequence, or other custom handler. Note that if doing so, you
likely want to define remember_last_key()
as described
below to ignore the keycode associated with that handler so that the
Repeat Key does not attempt to repeat or alternate-repeat itself.
Implementation details
If you are interested in the technical details, this section is an explanation of how Repeat Key and Alternate 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.
Alternate Repeat Key
The Alternate Repeat Key is implemented by looking up an alternate
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 alternate keycode in the
keyrecord_t
’s .keycode
field, and this field
is only present when Combos are enabled.
Acknowledgements
Thanks to @drashna, @Ikcelaks, @sigprof, and @DreymaR for helpful review and feedback to improve Repeat Key.