README.org

April 27, 2021 ยท View on GitHub

#+TITLE: Kyria Keymap #+OPTIONS: ^:nil

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 :TOC_3:noexport:
  • [[#about][About]]
  • [[#features][Features]]
    • [[#vim-mode][Vim Mode]]
    • [[#case-modes][Case Modes]]
      • [[#caps-word][Caps Word]]
      • [[#x-case][X-Case]]
      • [[#configuration--usage][Configuration / Usage]]
    • [[#userspace-leader-sequences][Userspace Leader Sequences]]
      • [[#how-it-works][How it works]]
      • [[#configuration][Configuration]]
      • [[#displaying-on-the-oled][Displaying on the OLED]]
  • 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 [[./logs.org][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 [[https://github.com/andrewjrae/qmk-vim][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][Caps Word]], and [[#x-case][X-Case]]. See [[./features/casemodes.c][features/casemodes.c]] for implementation details.

Also I'd like to shout out the [[https://splitkb.com][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 [[https://github.com/baffalop][@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()=. [[https://github.com/baffalop][@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: #+begin_src 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; } #+end_src

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).

[[https://github.com/mihaiolteanu][@mihaiolteanu]] also made an elisp implementation for Emacs in this [[https://github.com/andrewjrae/kyria-keymap/issues/1][ticket]] which is super nifty!

*** Configuration / Usage

  • Add [[./features/casemodes.c][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. #+begin_src C #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; } ... #+end_src

  • 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=. #+begin_src C 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;
}

} #+end_src

  • (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). #+begin_src C // 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; } #+end_src 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][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=. #+begin_src C typedef void *(leader_func_t)(uint16_t); static leader_func_t leader_func = NULL; #+end_src /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: #+begin_src C 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; } } #+end_src

The =leader_layers_func= could then look something like this: #+begin_src C 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 } #+end_src

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][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. #+begin_src C #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; } ... #+end_src

  • 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: #+begin_src C #define LEADER_DISPLAY_STR #+end_src

  • Then you simply need to add the display macro to your =oled_task_user()=: #+begin_src C void oled_task_user(void) { ... OLED_LEADER_DISPLAY(); ... } #+end_src

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. #+begin_src C #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 #+end_src