This project is a reusable Vim-mode "plugin" for kanata. It provides mode switching and common Vim-like editing motions/actions on top of a custom kanata keymap. Essentially, this brings Vim-like text manipulation to any application and operating system.
This works by introducing extra kanata layers where the keys are mapped to OS native keyboard shortcuts to navigate text. So switching to the vim normal layer, the h j k l keys are now remapped to the arrow keys, w is mapped to Alt-Right, and so on. This approach is inspired by the Karabiner-Elements Vim Mode Plus mod.
Demo Video:
screenrecording-2026-06-27_17-40-33.mp4
- What the plugin contains
- Inner workings (mode model)
- How to integrate in your kanata keymap
- Minimal setups
- Visualize active vim mode
- Supported Vim movements and actions
The "plugin" consists of a collection of kanata .kbd config files that you can include in your kanata configuration.
1-interface.kbd: OS-specific primitive actions (@left,@end-of-word,@copy,@undo, ...). This is the only place that should use raw OS key combos.2-shared.kbd: higher-level combos/macros (@delete-word,@copy-line,@paste,@replace, ...).3-bootstrapping.kbd: mode state machine and entrypoint templates.4-normal-layer.kbd: Vim normal mode keymap.5-visual-layer.kbd: Vim visual mode keymap.6-visual-line-layer.kbd: Vim visual-line mode keymap.
- The plugin orchestrates 4 logical modes/layers:
insert,vim-normal,vim-visual,vim-visual-line. - For each mode, it also activates a respective override layer:
insert-override,vim-normal-override,vim-visual-override,vim-visual-line-override. - Override layers let you add custom remaps while reusing the shared Vim logic.
Include the vim plugin files in this strict order at the top of your keymap.
(include kanata-vim/1-interface.kbd)
(include kanata-vim/2-shared.kbd)
(include kanata-vim/3-bootstrapping.kbd)
(include kanata-vim/4-normal-layer.kbd)
(include kanata-vim/5-visual-layer.kbd)
(include kanata-vim/6-visual-line-layer.kbd)
Then wire your own config.
Minimal plugin-relevant settings (optional):
(defcfg
process-unmapped-keys yes
alias-to-trigger-on-load switch-insert
)
process-unmapped-keys yes: lets unmapped keys pass through while Vim layers are active.alias-to-trigger-on-load switch-insert: starts Kanata in insert mode.
You need:
- an
insertlayer (i.e. rename your normal typing layer to "insert") - these override layermaps (can be empty initially):
insert-overridevim-normal-overridevim-visual-overridevim-visual-line-override
Example empty scaffolding:
(deflayer insert <your custom keymap here>)
(deflayermap (insert-override))
(deflayermap (vim-normal-override))
(deflayermap (vim-visual-override))
(deflayermap (vim-visual-line-override))
In the examples below, the vim entrypoint is mapped to esc.
But of course you can map it to any key instead of esc, such as caps etc.
Map any key to the switch-vim-normal alias:
(deflayermap (insert)
esc @switch-vim-normal
)
Or get creative in how to enter vim mode. E.g. by double tap on escape:
(deflayermap (insert)
esc (tap-dance 200 (esc @switch-vim-normal))
)
It can be very nice to have a different vim mode trigger depending on the currently used app.
E.g. in the terminal I need my escape key to behave normally (for actual vim use).
But in my browser, I want the escape key to toggle the vim mode immediately.
You can use the vim-entrypoint template for such a setup:
(deflayermap (insert)
esc (t! vim-entrypoint rsft)
)
Behavior:
- tap
esc: normal key behavior - hold
right shift+ tapesc: entervim-normal - if virtual key
vim-direct-enteris active, always entersvim-normalon tapesc
This only makes sense in conjunction with an external script that toggles the vim-direct-enter virtual key based on the active app.
For an example script that works with Hyprland on Linux, refer to examples/hypr-window-listener.sh.
The plugin implementations of the vim layers assume the default vim mappings.
E.g. in normal mode layer, the "j" key is mapped to the down arrow key.
If you're using homerow mods, you most likely want to map a long tap of "j" to the shift key.
Or maybe you're using caps as escape key and want to use it to exit normal mode.
This can easily be achieved by using the override layers without touching the Vim layer implementation. Here are some simple examples (same principle applies to all override layers):
(deflayermap (vim-normal-override)
caps @switch-insert ;; exit normal mode by tapping the `caps` key.
esc (multi esc @switch-insert) ;; Exit normal, but also tap the "Escape" key for the outside to use
j (tap-hold 200 200 _ rsft) ;; add a homerow mod to normal mode (`_` will pass the keypress to the default vim normal layer action)
)
(defcfg
process-unmapped-keys yes
alias-to-trigger-on-load switch-insert
)
(defsrc
esc
caps
j
k
;; ... all other physical keys
)
(deflayer insert
esc
caps (t! vim-entrypoint rsft)
f (tap-hold 200 200 f lsft)
j (tap-hold 200 200 j rsft)
;; ... all other mappings
)
(deflayermap (insert-override)
)
(deflayermap (vim-normal-override)
esc (multi esc @switch-insert)
caps (multi esc @switch-insert)
f (tap-hold 200 200 _ rsft)
j (tap-hold 200 200 _ rsft)
)
(deflayermap (vim-visual-override)
esc (multi @deselect @switch-vim-normal)
caps (multi @deselect @switch-vim-normal)
)
(deflayermap (vim-visual-line-override)
esc (multi @deselect @switch-vim-normal)
caps (multi @deselect @switch-vim-normal)
)
Good for external keyboards with custom firmware (such as QMK or ZMK). In this case, kanata is only used to enable the vim mode for this keyboard.
(defcfg
process-unmapped-keys yes
alias-to-trigger-on-load switch-insert
)
(defsrc
esc
)
(deflayermap (insert)
esc (t! vim-entrypoint rsft)
___ use-defsrc ;; needed for the @replace action to work properly
)
(deflayermap (insert-override))
(deflayermap (vim-normal-override)
esc (multi esc @switch-insert)
4 (fork _ @end-of-line (lsft rsft)) ;; ZMK will send a shifted 4 when I press the `$` key
)
(deflayermap (vim-visual-override)
4 (fork _ @select-end-of-line (lsft rsft))
)
(deflayermap (vim-visual-line-override))
It can be very confusing to use this plugin without a way to see which vim mode is currently active.
Luckily the kanata TCP port allows to build simple integrations with external tools.
For example, refer to examples/waybar-kanata-vim-status.sh for a simple integration with Linux Waybar.
(A more simple approach is to activate CAPS mode while vim layers are active - this shows a small caps-indicator on some OSes.)
| Key / Combo | Action | Normal | Visual | Visual-line |
|---|---|---|---|---|
h, j, k, l |
Move/select left down up right | ✅ | ✅ | ✅ |
b |
Move/select to start of word | ✅ | ✅ | |
w |
Move/select to end of word | ✅ | ✅ | |
0 |
Move/select to start of line | ✅ | ✅ | |
gg |
Go/select to start of document | ✅ | ✅ | ✅ |
G |
Go/select to end of document | ✅ | ✅ | ✅ |
i, a |
Enter insert mode | ✅ | ||
I |
Insert at start of line | ✅ | ||
A |
Append at end of line | ✅ | ||
o |
Open line below | ✅ | ||
O |
Open line above | ✅ | ||
x |
Delete selection/character | ✅ | ✅ | ✅ |
X |
Delete previous character | ✅ | ||
d |
Delete selection | ✅ | ✅ | |
dd |
Delete line | ✅ | ||
D |
Delete to end of line | ✅ | ||
diw, daw |
Delete word | ✅ | ||
cc |
Change line | ✅ | ||
C |
Change to end of line | ✅ | ||
ciw, caw |
Change inner word | ✅ | ||
y |
Yank/copy selection | ✅ | ✅ | |
yy |
Yank/copy line | ✅ | ||
Y |
Yank/copy to end of line | ✅ | ||
yiw, yaw |
Yank/copy word | ✅ | ||
v |
Enter visual mode | ✅ | ||
V |
Enter visual-line mode | ✅ | ||
viw, vaw |
Select word | ✅ | ||
p |
Paste | ✅ | ✅ | ✅ |
P |
Paste above | ✅ | ✅ | ✅ |
r |
Replace one character | ✅ | ||
u |
Undo | ✅ | ||
C-r |
Redo | ✅ | ||
C-d |
Half-page down | ✅ | ✅ | ✅ |
C-u |
Half-page up | ✅ | ✅ | ✅ |
. |
Repeat last action (limited support) | ✅ | ||
Esc |
Exit to normal / insert mode | ✅ | ✅ | ✅ |