← More about keyboards

🚀  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_combos[] = {};
uint16_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 {
  REPEAT = SAFE_RANGE,
  // 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.

Suggested placement of Repeat Key.

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[] = {
    COMBO(repeat_combo, REPEAT),
};
uint16_t COMBO_LEN = sizeof(key_combos) / sizeof(*key_combos);

Alternative implementations

I’m aware of a few existing “repeat last key” implementations:

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:

  1. Add a custom keycode for Alternate Repeat, say ALTREP, and use it in your layout.

  2. In process_record_user(), handle Repeat and Alternate Repeat as

    if (!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:

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) {
          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, 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) {
          SEND_STRING("repeat!");    
        }
      } else if (get_repeat_key_count() < 0) { // Alternate repeating.
        if (record->event.pressed) {
          SEND_STRING("alternate!");    
        }
      } else {                                 // Used normally.
        if (record->event.pressed) {  
          SEND_STRING("macro");
        }
      }
      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:

  1. On press, the stored modifiers are recalled and applied as weak mods, and we we increment the value of get_repeat_key_count().

  2. We set the record.event.pressed and record.event.time fields for the new keyrecord, then pass it into the event pipeline by calling process_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 a processing_repeat_count variable while calling process_record() and have the handler return early when this count is set.

  3. 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.

← More about keyboards