QMK: Custom shift keys
Pascal Getreuer, 2021-10-30 (updated 2024-10-04)
Overview
A frequently asked question about QMK is how to change what a key
types when it is shifted. For instance, how to make a key with
“inverted” shifting such that it types :
when pressed
normally and ;
when pressed shifted. Or how to implement
“programmer” layouts having keys that type symbols normally and type the
digits when pressed shifted.
It’s surprisingly tricky to get a custom shift key implemented just right. I’ve seen a lot of proposed solutions, and tried a few things myself. Some subtle gotchas:
Key repeating. When you hold a key down long enough, it normally repeats the character. E.g. you may want this repeating behavior to type a row of stars
****************
without having to tap the * key for each star.Rolled presses. Real typing is not always clean, individual key presses, especially in fast typing. You may press down one key, then while it is held, begin pressing another key—a “roll” across the keys. A common failure mode is a rolled press involving custom shift keys causes a key to get “stuck” until it is pressed again.
Interoperating with QMK features. Does the custom shift key implementation support shifting when done as a one-shot mod? Or with a mod-tap? Auto Shift? Space Cadet Shift?
Implementation
Here is my solution. It correctly handles key repeating and rolled presses, and I’ve tested that it works predictably in combination with one-shot mods, mod-taps, and Space Cadet Shift. It does not work with Auto Shift. To get the analogous effect with Auto Shift, use Auto Shift’s custom shifted values configuration.
Step 1: In your keymap.c
, define a
table of “custom_shift_key_t
” structs. Each row defines one
key. The keycode
is the keycode as it appears in your
layout and determines what is typed normally. The
shifted_keycode
is what you want the key to type when
shifted. (See the QMK keycodes
documentation for possible keycodes.)
Here is an example. The first row in my table has a .
(KC_DOT
) key that types ?
(KC_QUES
) when pressed shifted.
#include "features/custom_shift_keys.h"
const custom_shift_key_t custom_shift_keys[] = {
{KC_DOT , KC_QUES}, // Shift . is ?
{KC_COMM, KC_EXLM}, // Shift , is !
{KC_MINS, KC_EQL }, // Shift - is =
{KC_COLN, KC_SCLN}, // Shift : is ;
};
uint8_t NUM_CUSTOM_SHIFT_KEYS =
sizeof(custom_shift_keys) / sizeof(custom_shift_key_t);
Special cases:
For tap-hold keys, write the full mod-tap or layer-tap keycode in the first column and the shifted keycode in the second column. Suppose we have a mod-tap key that is dot
.
on tap and Ctrl on hold, and we want to customize the shifted tap action to be question mark?
, then its entry would be:{LCTL_T(KC_DOT), KC_QUES}, // Shift . is ?
Shift has no effect: It is allowed to put the same keycode in both columns to say that Shift has no effect on that key. For example, if we want the minus key
-
to type-
regardless of Shift, its entry would be:{KC_MINS, KC_MINS}, // Shift - is - (Shift has no effect)
Step 2: Handle custom shift keys from your
process_record_user()
function like so:
bool process_record_user(uint16_t keycode, keyrecord_t* record) {
if (!process_custom_shift_keys(keycode, record)) { return false; }
// Your macros ...
return true;
}
Step 3: In your rules.mk file, add
SRC += features/custom_shift_keys.c
Step 4: In the directory containing your
keymap.c
, create a features
subdirectory and
copy custom_shift_keys.h
and custom_shift_keys.c
there. This is the meat of the implementation.
custom_shift_keys.h
// Copyright 2021 Google LLC.
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include QMK_KEYBOARD_H
typedef struct {
uint16_t keycode;
uint16_t shifted_keycode;
} custom_shift_key_t;
extern const custom_shift_key_t custom_shift_keys[];
extern uint8_t NUM_CUSTOM_SHIFT_KEYS;
bool process_custom_shift_keys(uint16_t keycode, keyrecord_t *record);
custom_shift_keys.c
// Copyright 2021-2024 Google LLC.
// SPDX-License-Identifier: Apache-2.0
#include "custom_shift_keys.h"
bool process_custom_shift_keys(uint16_t keycode, keyrecord_t *record) {
static uint16_t registered_keycode = KC_NO;
// If a custom shift key is registered, then this event is either
// releasing it or manipulating another key at the same time. Either way,
// we release the currently registered key.
if (registered_keycode != KC_NO) {
(registered_keycode);
unregister_code16= KC_NO;
registered_keycode }
if (record->event.pressed) { // Press event.
const uint8_t saved_mods = get_mods();
#ifndef NO_ACTION_ONESHOT
const uint8_t mods = saved_mods | get_weak_mods() | get_oneshot_mods();
#else
const uint8_t mods = saved_mods | get_weak_mods();
#endif // NO_ACTION_ONESHOT
#if CUSTOM_SHIFT_KEYS_LAYER_MASK != 0
const uint8_t layer = read_source_layers_cache(record->event.key);
#endif // CUSTOM_SHIFT_KEYS_LAYER_MASK
if ((mods & MOD_MASK_SHIFT) != 0 // Shift is held.
#if CUSTOM_SHIFT_KEYS_NEGMODS != 0
// Nothing in CUSTOM_SHIFT_KEYS_NEGMODS is held.
&& (mods & (CUSTOM_SHIFT_KEYS_NEGMODS)) == 0
#endif // CUSTOM_SHIFT_KEYS_NEGMODS != 0
#if CUSTOM_SHIFT_KEYS_LAYER_MASK != 0
// Pressed key is on a layer appearing in the layer mask.
&& ((1 << layer) & (CUSTOM_SHIFT_KEYS_LAYER_MASK)) != 0
#endif // CUSTOM_SHIFT_KEYS_LAYER_MASK
) {
// Continue default handling if this is a tap-hold key being held.
if ((IS_QK_MOD_TAP(keycode) || IS_QK_LAYER_TAP(keycode)) &&
->tap.count == 0) {
recordreturn true;
}
// Search for a custom shift key whose keycode is `keycode`.
for (int i = 0; i < NUM_CUSTOM_SHIFT_KEYS; ++i) {
if (keycode == custom_shift_keys[i].keycode) {
= custom_shift_keys[i].shifted_keycode;
registered_keycode if (IS_QK_MODS(registered_keycode) && // Should key be shifted?
(QK_MODS_GET_MODS(registered_keycode) & MOD_LSFT) != 0) {
(registered_keycode); // If so, press directly.
register_code16} else {
// If not, cancel shift mods, press the key, and restore mods.
(MOD_MASK_SHIFT);
del_weak_mods#ifndef NO_ACTION_ONESHOT
(MOD_MASK_SHIFT);
del_oneshot_mods#endif // NO_ACTION_ONESHOT
(MOD_MASK_SHIFT);
unregister_mods(registered_keycode);
register_code16(mods);
set_mods}
return false;
}
}
}
}
return true; // Continue with default handling.
}
Troubleshooting: If your keymap fails to build, a
likely reason is that your QMK installation needs to be updated. If you
have the qmk_firmware git repo cloned locally, do a
git pull
. Or see Updating
your master branch for more details.
Customization
Negmods
By default, custom shift keys are applied whenever a shift mod is
active, including in combination with non-shift mods. The non-shift mods
remain active with the shifted tap action. For instance with the entry
{KC_DOT, KC_QUES}
, pressing Ctrl +
Shift + . sends Ctrl + ?
. To disable
custom shift keys with certain mods, define
CUSTOM_SHIFT_KEYS_NEGMODS
in your config.h using the
MOD_MASK_<modifier>
constants or
MOD_BIT(KC_<modifier>)
as described
here. Examples:
// Don't apply custom shift keys when a Ctrl key is held.
#define CUSTOM_SHIFT_KEYS_NEGMODS MOD_MASK_CTRL
// Don't apply custom shift keys together with right Alt (AltGr).
#define CUSTOM_SHIFT_KEYS_NEGMODS MOD_BIT(KC_RALT)
Or to enable custom shift keys only with shift mods, add in config.h:
// Don't apply custom shift keys when any non-shift mod is held.
#define CUSTOM_SHIFT_KEYS_NEGMODS ~MOD_MASK_SHIFT
Layer mask
By default, custom shift keys apply to keys on all layers. The option
CUSTOM_SHIFT_KEYS_LAYER_MASK
may be defined in config.h to
restrict custom shift keys to a set of specified layers.
CUSTOM_SHIFT_KEYS_LAYER_MASK
is a bit mask indicating on
which layers the feature is enabled. When a key on the ith layer is
pressed, custom shifting is applied only if the ith bit of
CUSTOM_SHIFT_KEYS_LAYER_MASK
is on.
Examples:
// Apply custom shift keys only on layer 3.
#define CUSTOM_SHIFT_KEYS_LAYER_MASK (1 << 3)
// Apply custom shift keys only on layer 0 and 2.
#define CUSTOM_SHIFT_KEYS_LAYER_MASK (1 << 0) | (1 << 2)
// Apply custom shift keys on all layers except layer 0.
#define CUSTOM_SHIFT_KEYS_LAYER_MASK ~(1 << 0)
Compared to Key Overrides
QMK’s Key
Overrides feature “overrides” the keys sent for specified
modifier-key combinations. In particular, it can be used to implement
custom shift keys. Add “KEY_OVERRIDE_ENABLE = yes
” in
rules.mk to enable it, then the example above is analogously implemented
as:
const key_override_t dot_key_override =
(MOD_MASK_SHIFT, KC_DOT, KC_QUES); // Shift . is ?
ko_make_basicconst key_override_t comm_key_override =
(MOD_MASK_SHIFT, KC_COMM, KC_EXLM); // Shift , is !
ko_make_basicconst key_override_t mins_key_override =
(MOD_MASK_SHIFT, KC_MINS, KC_EQL); // Shift - is =
ko_make_basicconst key_override_t coln_key_override =
(MOD_MASK_SHIFT, KC_COLN, KC_SCLN); // Shift : is ;
ko_make_basic
const key_override_t* key_overrides[] = {
&dot_key_override,
&comm_key_override,
&mins_key_override,
&coln_key_override,
};
Advantages of custom_shift_keys
:
Costs less firmware space:
custom_shift_keys
adds 192 bytes to my keymap vs. 1956 bytes for Key Overrides (building with LTO enabled).Simpler configuration syntax.
Advantages of Key Overrides:
Easily enabled, since it is part of QMK.
More general and configurable.
If you are already using Key Overrides for other purposes or have a couple kilobytes to spare, it is a great solution.
Explanation
The registered_keycode
variable is the keycode of the
custom shift key that is currently pressed or otherwise
KC_NO
. Only one custom key can be pressed at a time.
Attempting to hold multiple custom shift keys releases all but the last
one.
On each press or release of any key:
If
registered_keycode
is notKC_NO
, we release the currently active custom shift key (unregister_code16
). To avoid stuck keys, this is always the right thing to do: either the event is releasing the active custom shift key (so we should release it), or it is a rolled press manipulating another key while the active custom shift key is still held (so again, we should release it).In the loop, we check whether the current key event is pressing a custom shift key. If so, we clear the shift mods, press the appropriate key depending on whether shift was held (
register_code16
), and restore the mods.
Acknowledgements
Thanks a bunch to @wheredoesyourmindgo on GitHub and u/uolot and u/zardvark on Reddit for feedback and improvements to make custom shift keys better.