Vim mode

Congratulations, you learned Vim. Now, like me, you are probably completely unable to write an email in your browser, or use things like Google Docs without accidentally tapping <C-w> and closing the tab, or writing jkjkkjj until you realize you are not using your favourite text editor.

My solution to this problem was to make a feature-rich firmware-level vim-like modal editing layer, to take most of the pain away from occasionally having to use an inferior1 editor. Features include:

  • Normal mode navigation, both when the layer is toggled and in "hold" (MO) mode
  • Change (c ) and delete (d ) modes, which accept a motion
  • Insert mode, with some bindings like <C-w>
  • Undo, redo, search, open line (o /O )
  • Indicator LEDs for your mode
  • Doesn't interfere with actual Vim2, since it only affects one layer

Content

Start using Vim mode

How can you make use of these wonders? First, you should copy my vim_mode.c file (and its corresponding header file) to a features/ directory. Include it in your keymap.c:

#include "features/vim_mode.h"

Don't forget to add the source file as a SRC in your rules.mk file too:

SRC += features/vim_mode.c

Then, in your process_record_user (that's probably in your keymap.c file) you add a call to process_record_vim , and in layer_state_set_user , a call to layer_state_set_vim , like so:

layer_state_t layer_state_set_user(layer_state_t state) {
    layer_state_set_vim(state);
    ...
    return state;
}

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
    if (!process_record_vim(keycode, record)) { return false; }
    ...
}

After that, you have to add your VIMISH layer. In my case, it's pretty much a copy of my regular QWERTY layer (if you want it exactly the same, you could just make the entire layer transparent. A layer is still needed for the logic), and it's below my symbols/numbers layer, so that those can be used while in VIMISH's "insert mode". Note that vim_mode.c uses the constant VIMISH, so it should be defined there. My solution was to define a layer enum in a separate header file. You can also now add your layer toggle (I have a LT in my home row to use hjkl comfortably, and a TG )

Finally, I recommend enabling VISUAL mode by adding #define VIM_VISUAL_MODE to your config.h . It adds about 370 bytes to your binary size, but IMO it makes this much more usable, due to the many limitations that this mod has.

Now, some things you'll want to change. First of all, unless you're using a moonlander keyboard, you will have to change the ML_LED_* LED control macros to whatever works for your keyboard (or remove that altogether). Second of all, there are some keycodes I use in vim_mode.c that are specific to my layout (e.g. instead of MT(MOD_RCTL, KC_SLASH) , you might use just KC_SLASH ). Also, some of the macros used to emulate motions are different in MacOS, for example, AFAIK you'll have to replace ctrl with alt. It works fine in Linux and Windows. Finally, out of personal preference, I made the NM,. keys (the ones below HJKL in QWERTY) behave like home, page down, page up and end respectively, which I find quite useful especially when I'm in MO mode.

Limitations

Sadly, there are a few limitations on what can be done on a firmware level:

  • Anything that requires contextual information cannot be done, since the firmware has no way of knowing what's on your screen. This includes motions like f /t /F /T , gv , and text objects like di( , cap , etc.
  • Detecting which application is being used isn't possible either. This can be kind of annoying if you accidentally use the VIMISH layer when you're already using Vim. This can be somewhat prevented by using some layer indicator (I use the keyboard's RGB), or by using an idle timeout for that layer.

How it works

As I already mentioned, this feature builds on top of QMK's existing layers, to easily allow for both toggle and hold mode. In layer_state_set_vim , it saves whether or not the vim layer is active (apart from handling LEDs). When you leave the vim layer, the mode is reset to NORMAL for the next time. Also notice that I only check if the layer is active, not if it's the top layer (which could be done with get_highest_layer() . This is done to be able to use keys from other layers as vim shortcuts (mainly from my symbols layer, like ^ and $ )

process_record_vim is run on each keypress, and if your vim layer is active, it handles the keypress according to the current mode. I use register_code16 and unregister_code16 instead of just tap_code for most keys, to allow for key repeat. It's important to unregister any key that was registered at some point, not to end up with a key stuckkk. To simplify that, I wrapped the operation with a function register_or_unregister , that unregisters the keycode either when you stop pressing the key, or when a new one is pressed.

CHANGE and DELETE mode are basically the same, except for which mode they end up in after doing the deletion. One interesting thing is the implementation of i in these modes. Since the only compatible motion that starts with i is iw , i just taps Ctrl- , to end up at the beginning of the word. Then, the w deletes the following word. This avoids needing another "operator pending" mode, with a custom behaviour for w , saving some bytes.

In NORMAL mode, some keys act differently depending on whether you're pressing shift or not (I , A , O ). For those, notice that the shift mod has to be deleted while performing the action, to be added back later, to avoid selecting and/or deleting text.

Possible improvements

Since my idea was merely to make other editors somewhat usable, while being as simple and lightweight as possible (as of writing, the whole things adds 2280 bytes to my keymap, or 1908 bytes if you don't enable VISUAL mode), there are some features that I didn't implement, but could be useful to you.

For instance, you could implement dot repeat, or numbers before actions/motions. Also, holding some keys does not work in its current implementation.

Another idea you may find useful is to use dynamic macros, recording them with q and playing them with @ , like in Vim. I personally don't love keyboard-level recordings, since they don't offer the level of flexibility that you get with Vim's text objects and similar things.

Finally, c and d only copy the deleted text if they are used in visual mode. This was done to simplify their implementations, but it could be changed.


  • 1 I'm not trying to start an editor war. I'm just trying to be funny
  • 2 Kind of. See limitations