← More about keyboards

“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_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:

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:

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

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

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

  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.

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.

← More about keyboards