SHIFTING INTO BETTER KEYBOARD ERGONOMICS Over my holidays I developed a pain in my left pinkie. The cause of my wee finger's suffering was the stretch and hold maveuver needed to shift a character into the uppercase. I'd developed a similar repetitive strain injury a few weeks back, and a small change in my keyboard layout had abated the injury for some time. But owing to the feversish amount of writing I did over the course of 10 days the injury returned. Now the pain was much worse and it was happening during a time that was supposed to be unhindered, uninterupted leisure. Unacceptable! I took a deep dive into my QMK layout. I would revise my placement of the shift key and keep the injury from developing again and again much worse. I use the Reviung41 keyboard, designed by gtips and bought as a kit from Boardsource. The keyboard has 41 keys, five of which are available in the thumb cluster. Ideally I would move my shift key into the thumb cluster. But the area is packed! Four of the five keys are mapped to tap and hold actions. For instance: holding the return or space keys activates one of two layers; Tapping the control key inserts a parenthesis. At the time it didn't seem like I had a key to spare. (I'm still not sure it does, though that may change). OSM shift key as mod key tap ---------------------------------------------------------------------- The first solution I attempted was to add a one shot modifier shift key as a tap function for the right most key in the thumb cluster. This solution failed. QMK only supports basic keycodes (which a OSM key is not!) for the tap/hold feature. OSM shift key on a layer ---------------------------------------------------------------------- Next I tried and succeeded at putting the OSM key into a layer that could be turned on my holding the e key, `KC_E'. This solution worked well enough to allow my pinkie to heal. But it introduced two new problems. First: with any key combination involving that e key. For instance, my Emacs command `C-e' became unreliable. Sometimes the keyboard would shift into the OSM layer instead of sending `C-e'. My fingers weren't fast enough. QMK has `#define' statements that can be used to hone in the tap/hold logic with variables like `TAPPING_TERM', `PERMISSIVE_HOLD', `HOLD_ON_OTHER_KEY_PRESS'. I either couldn't find the right value or these were insufficient for my typing style. Second: I now had to type three keys to shift a single character. No good! Leader key reverse shifting ---------------------------------------------------------------------- I liked the OSM approach and thought it could work, if refined. My next iteration of this solution was trigger the OSM shift using a leader key combination. However, I couldn't find a way to send the OSM shift key as a combo. I suspect there is a function or clever way to send it, but I gave up and moved on before finding an answer. What I took with me was the leader key feature. I liked the reliability of the feature. Plus, moving my shifting into a leader key combo would avoid the unreliability of the e's tap/hold functionality. I could turn that key back to just being an e! The leader key was set as the hold function of the left most key in the thumb cluster. In my keymap it looked like this: `LALT_T(KC_O)', alt on hold, "o" on tap. For some reason I didn't use QMK's default keycode for the leader key, `QMK_LEAD'. So I had to add a bit of code to make `KC_O' trigger the leader feature. ,---- | bool process_record_user(uint16_t keycode, keyrecord_t *record) { | switch (keycode) { | case LALT_T(KC_O): | if (record->tap.count && record->event.pressed) { | // Can't send leader key as a basic keycode, so this hack triggers | // the start of the leader key experience with an API call. | leader_start(); | return false; | } | break; | } | return true; | } `---- As I mentioned, I tried to get an OSM shift key as part of a leader key combination but failed. So instead I came up with a very crazy solution, which is most simply stated as "shifting in reverse". In other words: the key is first typed unshifted, followed by a leader key combination, and then transformed into its shifted state. The outcome is produced by a series of clever hacks around the `process_record_user' function and a conversion of QMK's keycode to a shifted ascii character. Here's the code: ,---- | // Place this in rules.mk | LEADER_ENABLE = yes `---- ,---- | uint16_t last_keycode = KC_NO; | bool is_leader = false; | | char keycode_to_char(uint16_t keycode) { | if (keycode >= KC_A && keycode <= KC_Z) { | return 'A' + (keycode - KC_A); | } | return 0; | } | | bool process_record_user(uint16_t keycode, keyrecord_t *record) { | switch (keycode) { | case LALT_T(KC_O): | if (record->tap.count && record->event.pressed) { | leader_start(); | return false; | } | break; | } | if (!is_leader) | last_keycode = keycode; | return true; | } | | void leader_start_user(void) { | is_leader = true; | } | | void leader_end_user(void) { | // I chose KC_H because it sits under my index finger. | // But any key could be used as the leader combo. | if (leader_sequence_one_key(KC_H)) { | SEND_STRING(SS_TAP(X_BSPC)); | char kc = keycode_to_char(last_keycode); | send_char(kc); | } | is_leader = false; | } `---- The way it works is weird but makes sense. The last keycode is maintained inside the `last_keycode' variable. Importantly, this key is not updated when the leader feature (tracked using `is_leader') is enabled. That's to ignore any keys involved in the leader key sequence. (I suppose another approach would be to push and pop keys off an array, but I don't know how to do that in C). Anyways, when the correct leader key sequence is pressed the shifting in reverse occurs. First, a backspace character is typed out, removing the unshifted key. Then `last_keycode' is converted to its shifted ASCII character. Finally, that character is tapped out by the keyboard. This solution I found entirely unintuitive. I also thought it a funny, obscure way of doing a shift action. I was happy that I implemented it using my limited understanding of QMK and C. But it really was not that much of an improvement over my previous solution. There were fewer errors owing to the hold function of the e key, but new errors of a different kind. I had to execute the leader key sequence within the limited term window. And I had to wait for that window to elapes before the key would be shifted. So it was slow. Slow, unintuitive, obscure, and weird. Shifting via tap dance ---------------------------------------------------------------------- I reflected on other approaches. I'd heard of QMK's tap dance feature, which lets you tap a key multiple times to execute a action. I thought this interesting, but imagined issues when typing out a word with the same character repeated in sequence. Shifting with key chords ---------------------------------------------------------------------- Where i eventually landed was with chording. With chording, two or more keys can be held together at the same time to execute an action. I decided to try a chording solution to my shifting woes. Basically, I would create chords for each key that I want to tap out shifted. The chords would share a root key, which would help me easily remember and execute the chords. In the simplest form, this could mean a group of chords, where each chord is Z and a key from A-Y. To shift an arbitrarily selected key, I hold down Z and the selected key. Though simple (and with an obvious flaw: Z can't be shifted this way), I decided the ergonomics of this solution still subpar. I'd have to curl my pinky to reach Z. So I iterated, deciding to use two groups of chords: one for keys below my left hand, one for the keys below my right hand. The root key for each group would be on the other side of the keyboard. Practically, this means that to shift an arbitrary key on the right side of the keyboard I hold down "u" (located on the left side of the keyboard) plus that arbitrary key. To shift an arbitrary key on the left side of the keyboard I hold down "h" (located on the right side of the keyboard) plus that arbitrary key. My solution worked great, with one easily predictable issue: there is a single overlap of keys in my chords: "u" and "h". QMK can't distingush between a chord where "U" is the root and a key where "h" is the root. In otherwords: ,---- | const uint16_t PROGMEM shifted_h[] = {KC_U, KC_H, COMBO_END}; | const uint16_t PROGMEM shifted_u[] = {KC_H, KC_U, COMBO_END}; `---- The two combos above are essentially the same chord. After all, the keys of a chord can be written in any order and the result will always be the same. So with this solution I am without an easily shifted U. But I've accepted this flaw. For after typing this whole article up using this new shifting technique I have to say that I quite enjoy the chording feeling! Included at the end of this entry is my full config for the chording feature. Note that this is for a dvorak layout. If you use a QWERTY keyboard you'll have to adjust the groups quite a bit. While I would have liked for the implementation to have the chords programmatically cerated I couldn't figure out how to do that. So it's all static. But I was at least able to write it out using Emacs macros that saved me many valuable keystrokes! :) Where to from here ---------------------------------------------------------------------- I am very happy with the results of my diversion into better keyboard shifting ergonomics. My end result isn't perfect--fine tuning of the chord typing term remains to avoid erroneous shifts. But my pinkie feels happy and unstarined. Plus, this exercise was a fun way to learn about the leader key and chording features of QMK. I definitely contin to refine how I send shifted keycodes. Already I am learning about other ways of controling shift, like using a 'next sentence' macro. The refinement of a keyboard is endless. Chording setup for DVORAK keyboard ---------------------------------------------------------------------- ,---- | // Place this in rules.mk | COMBO_ENABLE = yes `---- ,---- | // Combos for shifting letters on right side of keyboard | const uint16_t PROGMEM shifted_f[] = {KC_U, KC_F, COMBO_END}; | const uint16_t PROGMEM shifted_g[] = {KC_U, KC_G, COMBO_END}; | const uint16_t PROGMEM shifted_c[] = {KC_U, KC_C, COMBO_END}; | const uint16_t PROGMEM shifted_r[] = {KC_U, KC_R, COMBO_END}; | const uint16_t PROGMEM shifted_l[] = {KC_U, KC_L, COMBO_END}; | const uint16_t PROGMEM shifted_d[] = {KC_U, KC_D, COMBO_END}; | const uint16_t PROGMEM shifted_h[] = {KC_U, KC_H, COMBO_END}; | const uint16_t PROGMEM shifted_t[] = {KC_U, KC_T, COMBO_END}; | const uint16_t PROGMEM shifted_n[] = {KC_U, KC_N, COMBO_END}; | const uint16_t PROGMEM shifted_s[] = {KC_U, KC_S, COMBO_END}; | const uint16_t PROGMEM shifted_b[] = {KC_U, KC_B, COMBO_END}; | const uint16_t PROGMEM shifted_m[] = {KC_U, KC_M, COMBO_END}; | const uint16_t PROGMEM shifted_w[] = {KC_U, KC_W, COMBO_END}; | const uint16_t PROGMEM shifted_v[] = {KC_U, KC_V, COMBO_END}; | const uint16_t PROGMEM shifted_z[] = {KC_U, KC_Z, COMBO_END}; | // Combos for shifting letter son left side of keyboard | const uint16_t PROGMEM shifted_p[] = {KC_H, KC_P, COMBO_END}; | const uint16_t PROGMEM shifted_y[] = {KC_H, KC_Y, COMBO_END}; | const uint16_t PROGMEM shifted_a[] = {KC_H, KC_A, COMBO_END}; | const uint16_t PROGMEM shifted_o[] = {KC_H, KC_O, COMBO_END}; | const uint16_t PROGMEM shifted_e[] = {KC_H, KC_E, COMBO_END}; | const uint16_t PROGMEM shifted_u[] = {KC_H, KC_U, COMBO_END}; | const uint16_t PROGMEM shifted_i[] = {KC_H, KC_I, COMBO_END}; | const uint16_t PROGMEM shifted_q[] = {KC_H, KC_Q, COMBO_END}; | const uint16_t PROGMEM shifted_j[] = {KC_H, KC_J, COMBO_END}; | const uint16_t PROGMEM shifted_k[] = {KC_H, KC_K, COMBO_END}; | const uint16_t PROGMEM shifted_x[] = {KC_H, KC_X, COMBO_END}; | // The action associated with each chord. | combo_t key_combos[] = { | COMBO(shifted_p, LSFT(KC_P)), | COMBO(shifted_y, LSFT(KC_Y)), | COMBO(shifted_a, LSFT(KC_A)), | COMBO(shifted_o, LSFT(KC_O)), | COMBO(shifted_e, LSFT(KC_E)), | COMBO(shifted_u, LSFT(KC_U)), | COMBO(shifted_i, LSFT(KC_I)), | COMBO(shifted_q, LSFT(KC_Q)), | COMBO(shifted_j, LSFT(KC_J)), | COMBO(shifted_k, LSFT(KC_K)), | COMBO(shifted_x, LSFT(KC_X)), | COMBO(shifted_f, LSFT(KC_F)), | COMBO(shifted_g, LSFT(KC_G)), | COMBO(shifted_c, LSFT(KC_C)), | COMBO(shifted_r, LSFT(KC_R)), | COMBO(shifted_l, LSFT(KC_L)), | COMBO(shifted_d, LSFT(KC_D)), | COMBO(shifted_h, LSFT(KC_H)), | COMBO(shifted_t, LSFT(KC_T)), | COMBO(shifted_n, LSFT(KC_N)), | COMBO(shifted_s, LSFT(KC_S)), | COMBO(shifted_b, LSFT(KC_B)), | COMBO(shifted_m, LSFT(KC_M)), | COMBO(shifted_w, LSFT(KC_W)), | COMBO(shifted_v, LSFT(KC_V)), | COMBO(shifted_z, LSFT(KC_Z)), | }; `---- References ---------------------------------------------------------------------- - - - - - - -