/kyria-keymap

My QMK keymap for the SplitKB Kyria

Primary LanguageCGNU General Public License v2.0GPL-2.0

Kyria Keymap

This keymap has a RSTHD-based alpha layer and lots of fun features.

images/kyria.png *This image may be out of date in relation to the code.

Table of Contents

About

This is my QMK Kyria keymap, it has some nifty features and an interesting base layer.

The base alpha layer I use is a modified RSTHD, which has some interesting (perhaps dubious), choices. To understand my thought process you can read my Keymap Logs.

Features

Vim Mode

Vim mode is a pretty self explanatory feature, it’s a vim emulator on your keyboard because why not!

Since it’s such large feature set to try and implement, it lives in it’s own repository, over at qmk-vim.

I can toggle vim mode using the ldr-t-v leader sequence.

Case Modes

Case modes is a feature that implements different case handling modes, Caps Word, and X-Case. See features/casemodes.c for implementation details.

Also I’d like to shout out the splitkb.com discord users for all their input and ideas with this feature.

Caps Word

Caps word is a feature I came up with a while back that essentially acts as a caps lock key but only for the duration of a “word”. This makes macros like CAPS_WORD really easy to type, it feels a lot like using one shot shift, and it pairs very nicely with it. What defines a “word” is sort of up for debate, I started out with a simple check to see if I had hit space or ESC but found that there were other things I wanted to exit on, like punctuation. So now I detect whether space, backspace, -, _, or an alphanumeric is hit, if so we stay in caps word state, if not, it gets disabled. I also check for mod chording with these keys and if you are chording, it will also disable caps word (e.g. on Ctrl+S).

The actual behavior of when to disable caps word can be tweaked using terminate_case_mode().

By default caps lock is used as the underlying capitalization method, however you can also choose to individually shift each keycode as the go out. This is useful for people who have changed the functionality of caps lock at the OS level. To do this simply add #define CAPSWORD_USE_SHIFT in you config.h.

To use this feature enable_caps_word() or toggle_caps_word() can be called from a macro, combo, tap dance, or whatever else you can think of.

X-Case

X-Case is an idea from @baffalop, it’s takes the idea of caps word but applies it to different kinds of programming cases. So for example say you want to type my_snake_case_variable, rather than pressing _ every time (which is almost certainly behind a layer), you can hit a “snake_case” macro that turns all your spaces into underscores, it can then be exited using whatever you define as the end of a word in terminate_case_mode(). @baffalop also suggested using a double tap space as an exit condition, which is also implemented here.

Now this is just a snake case macro, what if you want kebab-case? Well x-case can be applied here, but now instead of replacing space with an _ it replaces it with a - instead. The idea of x-case is to make it easy to achieve these kinds of case modes. For example to enable snake_case mode, you just need to call enable_xcase_with(KC_UNDS) and for kebab it’s simply enable_xcase_with(KC_MINS).

So you might ask, what about camelCase? Well, we got that covered too! If you call enable_xcase_with(OSM(MOD_LSFT)), your spaces will be turned into one shot shifts enabling you to write camelCase.

Finally, because you might want to use this for some more obscure use cases, there’s the enable_xcase() function. This function will intercept your next keystroke and use that as it’s case delimiter. For example, calling enable_xcase() and then hitting your / key will result in your spaces begin turned into slashes. (This is the equivalent of calling enable_xcase_with(KC_SLSH)))

To make this a little more flexible, you can define a default separator so that typing non-symbols defaults to that separator. This default separator can be configured with the DEFAULT_XCASE_SEPARATOR macro, by default it is KC_UNDS for snake_case. To enable this you need to specify what keys will enter default mode, this configured with the use_default_xcase_separator() function.

To just accept alpha-numerics you can use this function in your keymap.c:

bool use_default_xcase_separator(uint16_t keycode, const keyrecord_t *record) {
    switch (keycode) {
        case KC_A ... KC_Z:
        case KC_1 ... KC_0:
            return true;
    }
    return false;
}

Note that terminate_case_mode() also determines the stop conditions for x-case, however the spaces (and their new values) will not be passed through the terminate function.

It should also be noted that if you want some thing like SCREAMING_SNAKE_CASE, you just have to enable caps word and xcase (in either order).

@mihaiolteanu also made an elisp implementation for Emacs in this ticket which is super nifty!

Configuration / Usage

  • Add features/casemodes.c to your SRCS by calling adding SRC += features/casemodes.c to your rules.mk.
  • Add process_case_modes() to your process_record_user(), it can either go at the top or the bottom, it depends on how you want it to interact with your keycodes.

    If you process at the beginning it will look something like this, make sure that you return false when process_case_modes returns false.

    #include "features/casemodes.h"
    
    bool process_record_user(uint16_t keycode, keyrecord_t *record) {
        // Process case modes
        if (!process_case_modes(keycode, record)) {
            return false;
        }
        ...
        
  • Add ways to enable the modes in your keymap.c, for example you could use custom keycodes (macros):

    Remember to always start your custom keycodes at SAFE_RANGE.

    enum custom_keycodes {
        CAPSWORD = SAFE_RANGE,
        SNAKECASE,
    };
    
    bool process_record_user(uint16_t keycode, keyrecord_t *record) {
        // Process case modes
        if (!process_case_modes(keycode, record)) {
            return false;
        }
    
        // Regular user keycode case statement
        switch (keycode) {
            case CAPSWORD:
                if (record->event.pressed) {
                    enable_caps_word();
                }
                return false;
            case SNAKECASE:
                if (record->event.pressed) {
                    enable_xcase_with(KC_UNDS);
                }
                return false;
            default:
                return true;
        }
    }
        
  • (Optional) Change the mode termination conditions by creating a custom terminate_case_mode() function in your keymap.c: In the below example I’ve added the macros defined earlier to the terminate function as keycodes to ignore (ie not terminate on).
    // Returns true if the case modes should terminate, false if they continue
    // Note that the keycodes given to this function will be stripped down to
    // basic keycodes if they are dual function keys. Meaning a modtap on 'a'
    // will pass KC_A rather than LSFT_T(KC_A).
    // Case delimiters will also not be passed into this function.
    bool terminate_case_modes(uint16_t keycode, const keyrecord_t *record) {
        switch (keycode) {
            // Keycodes to ignore (don't disable caps word)
            case KC_A ... KC_Z:
            case KC_1 ... KC_0:
            case KC_MINS:
            case KC_UNDS:
            case KC_BSPC:
            case CAPSWORD:
            case SNAKECASE:
                // If mod chording disable the mods
                if (record->event.pressed && (get_mods() != 0)) {
                    return true;
                }
                break;
            default:
                if (record->event.pressed) {
                    return true;
                }
                break;
        }
        return false;
    }
        

    You can of course tweak this to get the exact functionality you want. Some people prefer to use a switch statement where they look for keys to end on, and default to keeping the mode enabled otherwise. I prefer the above method because I would rather exit the mode than stay in it.

  • (Optional) Use shift rather than caps lock in caps word. To do this simply add #define CAPSWORD_USE_SHIFT in you config.h.

Userspace Leader Sequences

I don’t like the default behavior of QMK’s leader key sequences, the timeout based approach is not something I’m used to coming from vim/doom-emacs. So I whipped up a quick little userspace version in features/leader.c. This version doesn’t timeout, but can be escaped using the LEADER_ESC_KEY which defaults to KC_ESC.

The implementation uses function pointers to carry out the leader sequence logic, which means it only needs to store one pointer, rather than an array of the captured keys. This makes it more memory efficient, but also a little more dangerous for the user to implement. That being said there is no possibility for infinite loops as long as the LEADER_ESC_KEY is accessible on the keyboard.

While this implementation is perhaps a little less user friendly, it’s easy to organize your different categories as each one will be it’s own function.

I also implemented a leader_display_str() function, which returns an ASCII representation of the current leader sequence. This won’t be enabled unless you put #define LEADER_DISPLAY_STR in your config.h. The maximum length of this string defaults to 19, but can be redefined with the LEADER_DISPLAY_LEN macro, note that this is the length excluding the null terminator.

How it works

Once a leader sequence has started each keystroke is intercepted, stripped of any mod-taps or hold-taps, and passed to the current leader_func. The leader function is a function pointer that is passed the current keycode, and will return the pointer to the next leader function, or NULL if done with the leader sequence.

The signatures of the these function pointers are defined by leader_func_t.

typedef void *(*leader_func_t)(uint16_t);
static leader_func_t leader_func = NULL;

Note that I return a void* because otherwise we have an awfully recursive definition.

The entry point to the leader sequence will always be the leader_start_func, this can be defined by you in your keymap.c. Here’s an example:

void *leader_start_func(uint16_t keycode) {
    switch (keycode) {
        case KC_L:
            return leader_layers_func; // function that will choose new base layers
        case KC_O:
            return leader_open_func; // function that opens common applications
        case KC_T:
            return leader_toggles_func; // function that toggles keyboard settings
        case KC_R:
            reset_keyboard(); // here LDR r will reset the keyboard
            return NULL; // signal that we're done
        default:
            return NULL;
    }
}

The leader_layers_func could then look something like this:

void *leader_layers_func(uint16_t keycode) {
    switch (keycode) {
        case KC_C:
            layer_move(_COLEMAK);
            break;
        case KC_R:
            layer_move(_RSTHD);
            break;
        case KC_Q:
            layer_move(_QWERTY);
            break;
        default:
            break;
    }
    return NULL; // this function is always an endpoint
}

Similar functions would then exist for leader_open_func and leader_toggles_func. Of course this is just an example, you can do whatever you want.

Configuration

  • Add features/leader.c to your SRCS by calling adding SRC += features/leader.c to your rules.mk.
  • Add process_leader() to your process_record_user(), this must go at the top of your process_record_user() if you have made a macro for the leader key that triggers on press. This is because it will attempt to be processed as part of the sequence. To get around this you could also just make your macro trigger on release rather than on press.

    If you process at the beginning it will look something like this, make sure that you return false when process_leader() returns false.

    #include "features/leader.h"
    
    bool process_record_user(uint16_t keycode, keyrecord_t *record) {
        // Process leader key sequences
        if (!process_leader(keycode, record)) {
            return false;
        }
        ...
        
  • Add ways to enable the modes in your keymap.c, for example you could use custom keycodes (macros). To start a leader sequence use the start_leading() and to stop use stop_leading(). If you want to know whether a leader sequence is currently underway, use is_leading().

Displaying on the OLED

  • To display the leader sequence on your OLED, you first need to enable it in your config.h:
    #define LEADER_DISPLAY_STR
        
  • Then you simply need to add the display macro to your oled_task_user():
    void oled_task_user(void) {
        ...
        OLED_LEADER_DISPLAY();
        ...
    }
        

    This macro simply prints the current leader sequence on a line of your display. Under the hood it’s quite simple and just uses the leader_display_str() function but displays it for a little while after it’s finished.

    #define OLED_LEADER_DISPLAY() {                     \
        static uint16_t timer = 0;                      \
        if (is_leading()) {                             \
            oled_write_ln(leader_display_str(), false); \
            timer = timer_read();                       \
        }                                               \
        else if (timer_elapsed(timer) < 175){           \
            oled_write_ln(leader_display_str(), false); \
        }                                               \
        else {                                          \
            /* prevent it from ever looping around */   \
            timer = timer_read() - 200;                 \
            oled_write_ln("", false);                   \
        }                                               \
    }
    #endif