README.org
March 13, 2022 · View on GitHub
#+TITLE: QMK Vim #+OPTIONS: ^:nil
[[images/demo.gif]]
- Table of Contents :TOC_3:noexport:
- [[#about][About]]
- [[#features][Features]]
- [[#core-features][Core Features]]
- [[#motions][Motions]]
- [[#actions][Actions]]
- [[#normal-mode][Normal Mode]]
- [[#insert-mode][Insert Mode]]
- [[#visual-mode][Visual Mode]]
- [[#visual-line-mode][Visual Line Mode]]
- [[#extra-features][Extra Features]]
- [[#text-objects][Text Objects]]
- [[#dot-repeat][Dot Repeat]]
- [[#w-motions][W Motions]]
- [[#numbered-jumps][Numbered Jumps]]
- [[#oneshot-vim][Oneshot Vim]]
- [[#core-features][Core Features]]
- [[#configuration][Configuration]]
- [[#setup][Setup]]
- [[#adding-keybinds][Adding Keybinds]]
- [[#setting-custom-state][Setting Custom State]]
- [[#mac-support][Mac Support]]
- [[#displaying-modes][Displaying Modes]]
- [[#contributing][Contributing]]
- [[#updating-readme-firmware-sizes][Updating Readme Firmware Sizes]]
- About This project aims to emulate as much of vim as possible in QMK userspace. The idea is to use clever combinations of shift, home, end, and control + arrow keys to emulate vim's modal editing and motions.
The other goal is to make it relatively plug and play in user keymaps, while still providing customization options.
- Features ** Core Features The core features are in in the tables below and takes up roughly 5% of at Atmega32u4. *** Motions Motions are available in [[#normal-mode][normal]] and [[#visual-mode][visual mode]], and can be composed with [[#actions][actions]]. Note that in the table below the mods shown are for Linux/Windows, however if =VIM_FOR_MAC= is defined then these should change to =LOPT=. | Vim Binding | Action | Notes | |-------------+--------------+-----------------------------------------------------------------------------------------------------------------------------------| | h | LEFT | | | j | DOWN | | | k | UP | | | l | RIGHT | | | e / E | LCTL + RIGHT | This may act more like a =w= command depending on the text environment | | w / W | LCTL + RIGHT | By default, this may act more like an =e= command depending on the text environment. Alternatively, see [[#w-motions][W motions]] | | b / B | LCTL + LEFT | | | 0 / ^ | HOME | In some text environments this goes to the true start, or the first piece of text | | $ | END | |
*** Actions Change, delete, and yank actions are all supported and can be combined with [[#motions][motions]] and text objects in normal mode as you would expect. For example =cw= will change until the next word (or end of word depending on your text environment). Note that by default the =d= action will copy the deleted text to the clipboard.
In visual mode each action can be accessed with =c=, =d=, and =y= respectively and they will act on the currently selected visual area.
Normal mode also supports these commands: | Vim Binding | Action | Notes | |-------------+-------------------------------------+----------------------------------------------------| | cc / S | Change line | This doesn't copy the old line to clipboard | | C | Change to end of line | This doesn't copy the changed content to clipboard | | s | Change character to right of cursor | This doesn't copy the changed content to clipboard | | dd | Delete line | This copies the deleted line to clipboard | | D | Delete to end of line | This copies the deleted text to clipboard | | yy | Yank line | | | Y | Yank to end of line | |
**** Paste Action The =paste_action()= handles pasting, which is simple for non lines, but most of the time, I'm pasting lines so this function attempts to consistently handle pasting lines. Yanks and deletes of lines are tracked through the =yanked_line= global, the paste action then pastes accordingly. For copying and pasting lines the line(s) the selection is made from the very start of the line below, up to start of the first line. See the [[#visual-line-mode][visual line mode]] section for more info on how lines are selected. There is also an optional =paste_before_action()=, enabled with the =VIM_PASTE_BEFORE= macro.
*** Normal Mode This below tables lists all of the commands not covered by the [[#motions][motions]] and [[#actions][actions]] sections . Note that in the table below the mods shown are for Linux/Windows, however if =VIM_FOR_MAC= is defined then these should change to =LCMD=. | Vim Binding | Action | Notes | |-------------+-------------------------------------------------+-------------------------------------| | i | [[#insert-mode][insert_mode()]] | | | I | HOME, [[#insert-mode][insert_mode()]] | | | a | RIGHT, [[#insert-mode][insert_mode()]] | | | A | END, [[#insert-mode][insert_mode()]] | | | o | END, ENTER, [[#insert-mode][insert_mode()]] | | | O | HOME, ENTER, UP [[#insert-mode][insert_mode()]] | | | v | [[#visual-mode][visual_mode()]] | | | V | [[#visual-line-mode][visual_line_mode()]] | | | p | [[#paste-action][paste_action()]] | | | u | LCTL + z | This works /most/ places | | CTRL + r | LCTL + y | This may or may not work everywhere | | x | DELETE | | | X | BACKSPACE | |
Note that all keycodes chorded with CTRL, GUI, or ALT, that aren't bound to anything are let through. In other words, you can still alt tab and use shortcuts for whatever editor you're in.
*** Insert Mode Insert mode is rather straight forward, all keystrokes are passed through as normal with the exception of escape, which brings you back to [[#normal-mode][normal mode]].
*** Visual Mode Visual mode behaves largely as one would expect, all [[#motions][motions]] and [[#actions][actions]] are supported. Escape of course returns you to [[#normal-mode][normal mode]]. Note that hitting escape may move your cursor unexpectedly, especially if you don't have =BETTER_VISUAL_MODE= enabled. This is because there isn't a good way to just deselect text in "standard" editing, the best way is to move the text cursor with the arrow keys. The trouble for us is choosing which way to move, by default we always move right. However, with =BETTER_VISUAL_MODE= enabled the first direction moved in visual mode is recorded so that we can move the cursor to either the left or right or the selection as required. Of course this approach breaks down if you double back on the cursor, but I find I don't do that all that often.
*** Visual Line Mode
Visual line mode is very similar to [[#visual-mode][visual mode]] as you would expect however only
the j and k motions are supported and of course the entire line is selected.
However, there is no perfect way (that I know of) to select lines the way vim
does easily. The way I used do it before I used vim, was to get myself to the
start of the line then hit shift and up or down. Going down works almost as
you'd expect in vim, but you'll always be a line behind since it doesn't
highlight the line the cursor is currently on. Going up on the other hand will
select the line the cursor is on, but it will always be missing the first line.
So neither solution quite works on it's own, =BETTER_VISUAL_MODE= does mostly fix
these issues, but at the price of a larger compile size, hence why it's not on
by default.
A note on the default implementation, since most programming environments make
the home key go to the start of the indent or the actual start of the line
dynamically, consistently getting to the start of a line isn't as easy as
hitting home. The most consistent way I've found is to hit end on the line
above, and then right arrow your way to the start of the next line. This works
as long as there is no line wrapping, so in the default implementation, entering
visual line mode sends KC_END, KC_RIGHT, LSFT(KC_UP). Not only is this quite
consistent, it also immediately highlights the current line just as you would
expect. The only downside with the default implementation is that if you then
try to go down that first line will be deselected, so you have to start your
visual selection a line above when moving downwards. Of course
=BETTER_VISUAL_MODE= fixes this as long as you don't double back on the cursor.
** Extra Features
In an effort to reduce the size overhead of the project, any extra features can be enabled and disabled using macros in your config.h.
| Macro | Features Enabled/Disabled | Bytes (gcc 8.3.0) |
|---------------------------+-----------------------------------------------------------------------------------------------------------------------------------+-------------------|
| =NO_VISUAL_MODE= | Disables the normal visual mode. | +256 B |
| =NO_VISUAL_LINE_MODE= | Disables the normal visual line mode. | +336 B |
| =BETTER_VISUAL_MODE= | Makes the visual modes much more vim like, see [[#visual-line-mode][visual_line_mode()]] for details. | -172 B |
| =VIM_I_TEXT_OBJECTS= | Adds the i text objects, which adds the iw and ig text objects, see [[#text-objects][text objects]] for details. | -122 B |
| =VIM_A_TEXT_OBJECTS= | Adds the a text objects, which adds the aw and ag text objects. | -138 B |
| =VIM_G_MOTIONS= | Adds gg and G motions, which only work in some programs. | -116 B |
| =VIM_COLON_CMDS= | Adds the colon command state, but only the w and q commands are supported (can be in combination). | -72 B |
| =VIM_PASTE_BEFORE= | Adds the P command. | -60 B |
| =VIM_REPLACE= | Adds the r command. | -76 B |
| =VIM_DOT_REPEAT= | Adds the . command, allowing you to repeat actions, see [[#dot-repeat][dot repeat]] for details. | -232 B |
| =VIM_W_BEGINNING_OF_WORD= | Makes the w and W motions skip to the beginning of the next word, see [[#w-motions][W motions]] for details. | -104 B |
| =VIM_NUMBERED_JUMPS= | Adds the ability to do numbered motions, ie 10j or 5w, be wary of large numbers however, as they can freeze up your keyboard. | -544 B |
| =ONESHOT_VIM= | Enables running vim in "oneshot" mode, see [[#oneshot-vim][oneshot vim]] for details. | -76 B |
| =VIM_FOR_ALL= | Adds the ability to toggle Mac support on and off at runtime, rather than only at compile time. | -456 B |
*** Text Objects
Unfortunately there is really no way to implement text objects properly,
especially things like brackets. However, word objects in some form are quite
possible. The tricky part is distinguishing between an inner and outer word,
some editors will have a forward word jump go to the end of a word like vim''s
w.
It's easy to get an inner word if word jump acts like e, since you can go to the
end of the word, then hold shift and jump to the start. And similarly it's easy
to get an outer word if word jump acts like w, since you can go to the start of
the next word then hold shift and jump back to the start of your word. However,
getting an inner word with just w and b at your disposal isn't possible without
using arrow keys which won't be consistent in scenarios where the word
punctuated in some way. But, it is possible to get an outer word with b and e.
In vim terms, the sequence looks like eebvb, now in vim that doesn't do exactly
what we want, but with word jumps it does result in an outer word selection.
It should be noted that this always selects extra space to the right of the word, and if the cursor is at the end of a word it will get the wrong word. So it isn't ideal, but it works okay in general.
There is also a the g object, which isn't even a default vim object, but CTRL+A
provides such a nice way to select the entire document that I couldn't help it.
I find it especially nice if I'm sending a message and I want to delete what I
wrote or change the whole thing, with dig or cig.
*** Dot Repeat
The dot repeat feature can be enabled with the =VIM_DOT_REPEAT= macro. This lets
the user hit the . key in normal mode to repeat the last normal mode command.
For example, typing ciw, hello!, will replace the underlying word with hello!,
now going over another word hitting . will repeat the action, just like vim
does. The way this works is that once an action starts, like c or D, or even A
all keycodes are recorded until we return to the normal mode state. Once you
hit . it goes through the recorded keys until it hits normal mode again. The
default size of the recorded keys buffer is =64=, but can be modified with the
=VIM_REPEAT_BUF_SIZE= macro.
*** W Motions
If the =VIM_W_BEGINNING_OF_WORD= macro is defined, the w and W motions (which are
synonymous) will skip to the beginning of the next word by sending LCTL + RIGHT
and then tapping LCTL + RIGHT, LCTL + LEFT on release. Otherwise, their default
behavior is to imitate the e and E motions by sending LCTL + RIGHT. Note that
enabling this feature currently causes unexpected side effects with actions such
as cw and dw, where the w motion acts like an e motion
([[https://github.com/andrewjrae/qmk-vim/pull/1#discussion_r650416367][context]]).
*** Numbered Jumps
The numbered jumps feature allows users to do repeat motions a specified number
of times just like in vim. By enabling =VIM_NUMBERED_JUMPS=, you can now type 10j
to jump down 10 lines, or you can type c3w to change the next three words.
*** Oneshot Vim "Oneshot" vim is not a normal vim feature, rather it's a way to use this enable this vim mode for a brief moment. Inspired by oneshot keys and other features like [[https://github.com/andrewjrae/kyria-keymap#caps-word][caps word]], this mode tries to intelligently leave vim mode automatically. This was added because sometimes I only want to make a small edit, or quickly navigate and yank some text, and I don't like having to toggle the mode on and off. Especially as sometimes I forget to turn it off and get briefly confused why the /real/ vim isn't working quite right.
In essence, this mode works as normal except that any time you press Esc you
exit the mode, same goes if you call any complex action like daw, ciw and the like.
Note that currently simple things like x and Y do not cause oneshot vim to exit.
To enable this mode simply add the =ONESHOT_VIM= macro to your =config.h=. Then add some way to call the =start_oneshot_vim()= function.
- Configuration ** Setup
-
First add the repo as a submodule to your keymap or userspace. #+begin_src bash git submodule add https://github.com/andrewjrae/qmk-vim.git #+end_src
-
Next, you need to source the files in the make file, the easy way to do this is to just add this line to your keymap's
rules.mkfile: #+begin_src make include (KEYMAP)/qmk-vim/rules.mk #+end_src ...or to your userspace'srules.mkfile: #+begin_src make include $(USER_PATH)/qmk-vim/rules.mk #+end_src If this doesn't work, you can either try changing the number in the =KEYBOARD_PATH_2= variable (values 1-5), or simply copy the contents from [[file:rules.mk][qmk-vim/rules.mk]]. -
Now add the header file so you can add =process_vim_mode()= 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_vim_mode()= returns false. #+begin_src C #include "qmk-vim/src/vim.h"
bool process_record_user(uint16_t keycode, keyrecord_t *record) { // Process case modes if (!process_vim_mode(keycode, record)) { return false; } ... #+end_src
-
The last step is to add a way to enter into vim mode. There are many ways to do this, personally I use leader sequences, but using combos or just a macro on a layer are all viable ways to do this. The important part here is ensure that you also have a way to get out of vim mode, since by default there is no way out. Enabling =VIM_COLON_CMDS= will allow you to also use
:qor:wqin order to get out of vim, but in general I would recommend using the =toggle_vim_mode()= function.As a simple example, here is the setup for a simple custom keycode macro: #+begin_src C enum custom_keycodes { TOG_VIM = SAFE_RANGE, };
bool process_record_user(uint16_t keycode, keyrecord_t *record) { // Process case modes if (!process_vim_mode(keycode, record)) { return false; }
// Regular user keycode case statement
switch (keycode) {
case CAPSWORD:
if (record->event.pressed) {
toggle_vim_mode();
}
return false;
default:
return true;
}
} #+end_src ** Adding Keybinds Since most vim users remap a key here or there, I've added hooks for the normal, visual, visual line, and insert modes. These hooks act in the exact same way that =process_record_user()= does, except that keycodes come in with any active modifiers applied to them. And not all keycodes will be passed down to vim, vim mode only intercepts keycodes alphanumeric, and symbolic keycodes (and escape).
For example pressing =KC_LSHIFT= and then =KC_A= will have =LSFT(KC_A)= sent down to
vim mode. It should also be noted that all modifiers will be added to the
keycode as the left mod, ie you can always use =LSFT(KC_A)= for catching A.
The hooks that you can use are: #+begin_src C bool process_insert_mode_user(uint16_t keycode, const keyrecord_t *record); bool process_normal_mode_user(uint16_t keycode, const keyrecord_t *record); bool process_visual_mode_user(uint16_t keycode, const keyrecord_t *record); bool process_visual_line_mode_user(uint16_t keycode, const keyrecord_t *record); #+end_src
As an example, I have the bad habit of hitting CTRL+S all the time. And for a
long time I've had it so that in insert mode, CTRL+S saves and enters
[[#normal-mode][normal_mode()]]. So in my [[https://github.com/andrewjrae/kyria-keymap/blob/master/keymap.c][keymap.c]] file I have this binding added:
#+begin_src C
bool process_insert_mode_user(uint16_t keycode, const keyrecord_t *record) {
if (record->event.pressed && keycode == LCTL(KC_S)) {
normal_mode();
tap_code16(keycode);
return false;
}
return true;
}
#+end_src
** Setting Custom State
The following user hooks are also called whenever the active mode is changed:
#+begin_src C
void insert_mode_user(void);
void normal_mode_user(void);
void visual_mode_user(void);
void visual_line_mode_user(void);
#+end_src
These can optionally be used to set custom state in your keymap.c file; for example, changing the [[https://beta.docs.qmk.fm/using-qmk/hardware-features/lighting/feature_rgblight#enabling-and-disabling-lighting-layers-id-enabling-lighting-layers][RGB lighting layer]] to indicate the current mode: #+begin_src C void insert_mode_user(void) { rgblight_set_layer_state(VIM_LIGHTING_LAYER, false); } void normal_mode_user(void) { rgblight_set_layer_state(VIM_LIGHTING_LAYER, true); } #+end_src ** Mac Support Since Macs have different shortcuts, you need to set the =VIM_FOR_MAC= macro in your config.h. That being said I'm not a Mac user so it's all untested and I'd guess there are some issues.
If you are a Mac user and do encounter issues, feel free to put up a PR or an issue.
If you intend to use your keyboard with both Mac and Windows or Linux computers, you can set the =VIM_FOR_ALL= macro in your config.h. This will allow you to use the following functions in your keymap.c to switch between Mac support mode and non-Mac support mode: #+begin_src C void enable_vim_for_mac(void); void disable_vim_for_mac(void); void toggle_vim_for_mac(void); bool vim_for_mac_enabled(void); #+end_src =VIM_FOR_ALL= overrides the behavior of =VIM_FOR_MAC=.
By default the keyboard will start in non-Mac support mode, but if =VIM_FOR_ALL= and =VIM_FOR_MAC= are both defined the keyboard will start in Mac support mode.
** Displaying Modes To help remind you that you have vim mode enabled, there are two functions available. The =vim_mode_enabled()= function which returns =true= is vim mode is active, and the =get_vim_mode()= function which returns the current vim mode.
In my keymap I use these to display the current mode on my OLED. #+begin_src C if (vim_mode_enabled()) { switch (get_vim_mode()) { case NORMAL_MODE: oled_write_P(PSTR("-- NORMAL --\n"), false); break; case INSERT_MODE: oled_write_P(PSTR("-- INSERT --\n"), false); break; case VISUAL_MODE: oled_write_P(PSTR("-- VISUAL --\n"), false); break; case VISUAL_LINE_MODE: oled_write_P(PSTR("-- VISUAL LINE --\n"), false); break; default: oled_write_P(PSTR("?????\n"), false); break; } #+end_src
- Contributing ** Updating Readme Firmware Sizes If you'd like to submit a pull request, please update the [[#extra-features][table with the firmware sizes for each feature]]. This can be done automatically by running the following commands in the root directory of this repository (it may take a few minutes, since it recompiles for each feature in the table): #+begin_src bash docker build -t qmk-vim-update-readme update-readme docker run -v $PWD:/qmk_firmware/keyboards/uno/keymaps/qmk-vim-update-readme/qmk-vim qmk-vim-update-readme #+end_src