This is my personal self-contained QMK keymap repository that can be built in the userspace folder or using GitHub Actions with its workflow.
- Contextual mod-taps
- Layout wrapper macros
- Combos with preprocessors
- Autocorrect word processing
- OLED indicators and animation
- RGB matrix indicators and custom effects
Home row mods are very useful on small split keyboards and they can be enhanced through contextual configuration. By considering both preceding and subsequent keys, trigger accuracy can be significantly improved by enhancing QMK's Tap-Hold Configuration functions.
Setup the pre_process_record_user
function to capture every input key record before it is passed into QMK's quantum functions. The captured information will be used to influence the decisions of the current mod-tap key that is undergoing tapping action processing:
static uint16_t inter_keycode;
static keyrecord_t inter_record;
bool pre_process_record_user(uint16_t keycode, keyrecord_t *record) {
if (record->event.pressed) {
// Cache incoming input for in-progress tap-hold decisions
inter_keycode = keycode;
inter_record = *record;
}
return true;
}
Add the following boolean macros to make the mod-tap decision functions more concise and easier to read:
// Matches rows on a 3x5_2 split keyboard
#define IS_HOMEROW(r) (r->event.key.row == 1 || r->event.key.row == 5)
// Home row mod-tap and the following key are on the same side of the keyboard
#define IS_UNILATERAL(r, n) ( \
(r->event.key.row == 1 && 0 <= n.event.key.row && n.event.key.row <= 2) || \
(r->event.key.row == 5 && 4 <= n.event.key.row && n.event.key.row <= 6) )
// Home row mod-tap and the following key are on opposite sides of the keyboard
#define IS_BILATERAL(r, n) ( \
(r->event.key.row == 1 && 4 <= n.event.key.row && n.event.key.row <= 6) || \
(r->event.key.row == 5 && 0 <= n.event.key.row && n.event.key.row <= 2) )
These macros will compare the current keyrecord_t *record
pointer values with the incoming keyrecord_t inter_record
.
In QMK's code, rows on right side is stacked below the left for a split keyboard. See this page for more details.
Modifiers should not be triggered when a mod-tap key is pressed together with another key on the same hand. To accomplish this, the mod-tap key is resolved to a tap using the get_hold_on_other_key_press
function when the overlapping incoming input is on the same side of the keyboard:
#ifdef HOLD_ON_OTHER_KEY_PRESS_PER_KEY
bool get_hold_on_other_key_press(uint16_t keycode, keyrecord_t *record) {
// Tap keycode with an overlapping unilateral key press on the same hand
if (IS_UNILATERAL(record, inter_record)) {
record->tap.count++;
process_record(record);
}
return false;
}
#endif
Modifiers should be triggered when a mod-tap key is held down and another key is tapped with the opposite hand. This is achieved using the get_permissive_hold
function with the mod-tap key and the nested input on the opposite side of the keyboard:
#ifdef PERMISSIVE_HOLD_PER_KEY
bool get_permissive_hold(uint16_t keycode, keyrecord_t *record) {
// Hold modifier with a nested bilateral tap on the opposite hand
return IS_BILATERAL(record, inter_record);
}
#endif
The conditional statement can be tweaked to match specific modifiers for frequent use-cases like Shift or exclude destructive ones like Ctrl.
When combined, unilateral tap and bilateral hold will function similarly to ZMK's positional hold tap. Additionally, all matches in get_permissive_hold
will be bilateral, as unilateral matches are already filtered in the preceding get_hold_on_other_key_press
function. Therefore, the explicit IS_BILATERAL
check is unnecessary.
To avoid tap-hold delays during regular typing, the tap-hold key is replaced with its tap keycode when preceded by alphabetical text input within the QUICK_TAP_TERM
interval. This implementation is integrated into the pre_process_record_user
function with the "Cache key record" configuration:
#define IS_TYPING(k) ( \
((uint8_t)(k) <= KC_Z || (uint8_t)(k) == KC_SPC) && \
(last_input_activity_elapsed() < QUICK_TAP_TERM) )
bool pre_process_record_user(uint16_t keycode, keyrecord_t *record) {
static bool is_pressed[UINT8_MAX];
const uint8_t tap_keycode = QK_MOD_TAP_GET_TAP_KEYCODE(keycode);
if (record->event.pressed) {
// Press the tap keycode if the homerow mod-tap follows the previous key swiftly
if (IS_HOMEROW(record) && IS_QK_MOD_TAP(keycode) && IS_TYPING(inter_keycode)) {
is_pressed[tap_keycode] = true;
record->keycode = tap_keycode;
}
// Cache incoming input for in-progress and subsequent tap-hold decisions
inter_keycode = keycode;
inter_record = *record;
}
// Release the tap keycode if pressed
else if (is_pressed[tap_keycode]) {
is_pressed[tap_keycode] = false;
record->keycode = tap_keycode;
}
return true;
}
This approach uses the keycode
container in the keyrecord_t
structure which requires either REPEAT_KEY_ENABLE
or COMBO_ENABLE
feature. The output experience will be similar to ZMK's require-prior-idle-ms feature.
If the previous "Instant Tap" feature is too aggressive, a gentler approach to avoid unintended modifier activation is to increase the tapping term interval time while typing. A tap timer is placed in the process_record_user
function to record the time of each key press:
static fast_timer_t tap_timer = 0;
bool process_record_user(uint16_t keycode, keyrecord_t *record) {
if (record->event.pressed) {
tap_timer = timer_read_fast();
}
return true;
}
To prevent accidental triggering of modifiers, the tapping term is increased for mod-tap keys that are preceded by a short typing interval measured with tap_timer
. This is implemented in the get_tapping_term
function:
#ifdef TAPPING_TERM_PER_KEY
uint16_t get_tapping_term(uint16_t keycode, keyrecord_t *record) {
// Increase tapping term for the home row mod-tap while typing
if (IS_HOMEROW(record) && timer_elapsed_fast(tap_timer) < TAPPING_TERM * 2) {
return TAPPING_TERM * 2;
}
return TAPPING_TERM;
}
#endif
This solution might be the only one that is needed if unintended modifier activations were simply caused by slow releasing fingers.
These decision functions are only evaluated within TAPPING_TERM
interval, before QMK decides to register a tap or hold event. Each configuration should be used independently to resolve specific accuracy problems with tap-hold keys. The conditional statements of each solution should also be fine-tuned for personal use cases.
A single keymap layout can be shared with multiple keyboards by using C preprocessor macros. These macros are referenced in the keyboard JSON files, and the build process will expand them into a transient keymap.c
file during compile time.
The split_3x5_2
layout is used as the base, with layers defined in layout.h
. The following is an example of a default layer:
#define BASE \
KC_Q, KC_W, KC_E, KC_R, KC_T, KC_Y, KC_U, KC_I, KC_O, KC_P, \
KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_QUOT, \
KC_Z, KC_X, KC_C, KC_V, KC_B, KC_N, KC_M, KC_COMM, KC_DOT, KC_SLSH, \
LT(SYM,KC_TAB), LCA_T(KC_ENT), RSFT_T(KC_SPC), LT(NUM,KC_BSPC)
Next, a wrapper alias to the layout used by the keyboard is also defined in the layout.h
file. For example, the following defines a wrapper alias for the Cradio layout:
#define LAYOUT_34key_w(...) LAYOUT_split_3x5_2(__VA_ARGS__)
Macros are not replaced recursively in a single step. Wrapper alias is required for the compiler to expand them on different iterations.
Both layout and layer macros are referenced in the keyboard JSON file (cradio.json
) as follows:
{
"keyboard": "cradio",
"keymap": "filterpaper",
"layout": "LAYOUT_34key_w",
"layers": [
[ "BASE" ],
[ "NUMB" ],
[ "SYMB" ],
[ "FUNC" ]
]
}
To include the layout macros in the layout.h
file, add the following line into the config.h
file:
#ifndef __ASSEMBLER__
# include layout.h
#endif
The assembler definition will prevent that file from being assembled in any build process where C opcodes are not valid.
Running qmk compile cradio.json
will cause the build process to construct a transient keymap.c
using the wrapper macros for compilation.
Home row mods can be added to the layout macros in the same manner. The order of the home row modifiers is defined by these two macros:
#define HRML(k1,k2,k3,k4) LCTL_T(k1), LALT_T(k2), LGUI_T(k3), LSFT_T(k4)
#define HRMR(k1,k2,k3,k4) RSFT_T(k1), RGUI_T(k2), RALT_T(k3), RCTL_T(k4)
Both are then used to transform the home row elements in the following HRM
wrapper macro for the split_3x5_2
layout:
#define HRM(k) HRM_TAPHOLD(k)
#define HRM_TAPHOLD( \
l01, l02, l03, l04, l05, r01, r02, r03, r04, r05, \
l06, l07, l08, l09, l10, r06, r07, r08, r09, r10, \
l11, l12, l13, l14, l15, r11, r12, r13, r14, r15, \
l16, l17, r16, r17 \
) \
l01, l02, l03, l04, l05, r01, r02, r03, r04, r05, \
HRML(l06, l07, l08, l09), l10, r06, HRMR(r07, r08, r09, r10), \
l11, l12, l13, l14, l15, r11, r12, r13, r14, r15, \
l16, l17, r16, r17
The HRM()
macro can now be used in the JSON file to add home row modifiers for layers that require them. For example:
"layers": [
[ "HRM(BASE)" ],
[ "HRM(COLE)" ],
[ "NUMB" ],
[ "SYMB" ],
[ "FUNC" ]
],
When setup this way, the home row modifier order can be easily edited in the
HRML
andHRMR
macros.
The base layout can be adapted for other split keyboards by expanding it with macros. The following example expands the split_3x5_2
layout to Corne's 42-key 3x6_3 layout (6 columns, 3 thumb keys) using the following wrapper to add additional keys to the outer columns:
#define LAYOUT_corne_w(...) LAYOUT_split_3x6_3(__VA_ARGS__)
// 3x5_2 to 42-key conversion
#define C_42(k) CONV_42(k)
#define CONV_42( \
l01, l02, l03, l04, l05, r01, r02, r03, r04, r05, \
l06, l07, l08, l09, l10, r06, r07, r08, r09, r10, \
l11, l12, l13, l14, l15, r11, r12, r13, r14, r15, \
l16, l17, r16, r17 \
) \
KC_TAB, l01, l02, l03, l04, l05, r01, r02, r03, r04, r05, KC_BSPC, \
QK_GESC, l06, l07, l08, l09, l10, r06, r07, r08, r09, r10, KC_SCLN, \
KC_LSFT, l11, l12, l13, l14, l15, r11, r12, r13, r14, r15, KC_ENT, \
RSA_T(KC_ESC), l16, l17, r16, r17, RAG_T(KC_DEL)
The JSON file for Corne (corne.json
) will use the conversion and HRM macro in the following format:
{
"keyboard": "crkbd/rev1",
"keymap": "filterpaper",
"layout": "LAYOUT_corne_w",
"layers": [
[ "C_42(HRM(BASE))" ],
[ "C_42(NUMB)" ],
[ "C_42(SYMB)" ],
[ "C_42(FUNC)" ]
]
}
bool rgb_matrix_indicators_user(void) {
if (get_highest_layer(layer_state) > 0) {
uint8_t const layer = get_highest_layer(layer_state);
for (uint8_t row = 0; row < MATRIX_ROWS; ++row) {
for (uint8_t col = 0; col < MATRIX_COLS; ++col) {
uint_fast8_t const led = g_led_config.matrix_co[row][col];
uint_fast16_t const key = keymap_key_to_keycode(layer, (keypos_t){col, row});
if (led != NO_LED && key != KC_TRNS) {
rgb_matrix_set_color(g_led_config.matrix_co[row][col], RGB_BLUE);
}
}
}
}
return false;
}
This code iterates over every row and column on a per-key RGB keyboard, searching for keys on the layer that have been configured (not KC_TRANS
) and lighting the corresponding index location. It is set to activate on layers other than the default.
The NeoPixel LED can be enabled for RGB Matrix with the following settings:
rules.mk
RGB_MATRIX_ENABLE = yes
RGB_MATRIX_DRIVER = WS2812
config.h
#define RGBW
#define WS2812_DI_PIN 17U
// Additional directives for a pair on a split keyboard:
#define RGB_MATRIX_LED_COUNT 2
#define RGB_MATRIX_SPLIT {1, 1}
#define SPLIT_TRANSPORT_MIRROR
g_led_config
structure that matches the host PCB:
// An example for 3x5_2 split
led_config_t g_led_config = { {
{ 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0 },
{ 1, 1, 1, 1, 1 }, { 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1 }, { 1, 1, 1, 1, 1 }
}, {
{109, 48}, {115, 48}
}, {
0x0f, 0xf0
} };
The data LEDs on an Atmega32u4 Pro Micro can be used as indicators. They are located on pins B0
(RX) and D5
(TX) of the microcontroller. To use them with QMK's LED Indicators, flag the pin in the config.h file:
#define LED_CAPS_LOCK_PIN B0
#define LED_PIN_ON_STATE 0
For advance usage, set up the following macros to call both pins with GPIO functions:
// Pro Micro data LED pins
#define RXLED B0
#define TXLED D5
// GPIO control macros
#define RXLED_INIT setPinOutput(RXLED)
#define TXLED_INIT setPinOutput(TXLED)
#define RXLED_ON writePinLow(RXLED)
#define TXLED_ON writePinLow(TXLED)
#define RXLED_OFF writePinHigh(RXLED)
#define TXLED_OFF writePinHigh(TXLED)
Initialise both LEDs with the *_INIT
macro on startup in the matrix_init_user(void)
function. They can then be used as indicators with the *_ON
and *_OFF
macros.
Corne keyboard can be build with few OLED display options using -e OLED=
environment variable to select pet animation on primary display.
Bongocat animation is the default. Use the following parameters to select Luna or Felix:
qmk compile -e OLED=LUNA corne.json
qmk compile -e OLED=FELIX corne.json
The icons used to render keyboard state are stored in the glcdfont.c
file. The images in this file can be viewed and edited with the following tools:
To wire the USBasp programmer to the target controller, use the following connections:
USBasp GND <-> Pro Micro GND
USBasp RST <-> Pro Micro RST
USBasp VCC <-> Pro Micro VCC
USBasp SCLK <-> Pro Micro 15/B1 (SCLK)
USBasp MISO <-> Pro Micro 14/B3 (MISO)
USBasp MOSI <-> Pro Micro 16/B2 (MOSI)
To replace the Pro Micro's default Caterina bootloader with Atmel-DFU, use the following USBasp command and fuses parameter:
avrdude -c usbasp -P usb -p atmega32u4 \
-U flash:w:bootloader_atmega32u4_1.0.0.hex:i \
-U lfuse:w:0x5E:m -U hfuse:w:0xD9:m -U efuse:w:0xF3:m
See the QMK ISP Flashing Guide for more details.
To flash firmware to an AVR controller with Atmel DFU bootloader on macOS, use the following bash or zsh shell alias. It requires dfu-programmer
from Homebrew to be installed:
dfu-flash() {
if [ ! -f $1 ] || [ -z $1 ]; then
echo "Usage: dfu-flash <firmware.hex> [left|right]"
return 1
fi
until [ -n "$(ioreg -p IOUSB | grep ATm32U4DFU)" ]; do
echo "Waiting for ATm32U4DFU bootloader..."; sleep 3
done
dfu-programmer atmega32u4 erase --force
if [ $2 = "left" ]; then
echo -e "\nFlashing left EEPROM" && \
echo -e ':0F000000000000000000000000000000000001F0\n:00000001FF' | \
dfu-programmer atmega32u4 flash --force --suppress-validation --eeprom STDIN
elif [ $2 = "right" ]; then
echo -e "\nFlashing right EEPROM" && \
echo -e ':0F000000000000000000000000000000000000F1\n:00000001FF' | \
dfu-programmer atmega32u4 flash --force --suppress-validation --eeprom STDIN
fi
echo -e "\nFlashing $1" && dfu-programmer atmega32u4 flash --force $1
dfu-programmer atmega32u4 reset
}
- Seniply 34 key layout
- Callum-style mods
- Paroxysm PCB
- Split Keyboard database
- Sockets
- Git Purr
- Data in Program Space
- Autocorrections with QMK
- Helios RP2040 clone
- Adafruit KB2040
- Elite-Pi
- Mill-Max 315-43-112-41-003000 low profile sockets
- Mill-Max 315-43-164-41-001000 mid profile sockets
- Mill-Max connector pins
- PJ320A jack
- TRRS cable
- Silicone bumpers feet
- Kailh gchoc v1 switches