Exploring zsh vi mode
Exploring zsh vi mode
One thing that I always wish I had when using my shell is a way of moving fast wherever I need or deleting words in a breeze. Sure, you can use Ctrl+<Left/Right Arrow>
to jump back and forth words or keep pressing backspace/delete to get rid of undesired characters. But there must be a better way, right? (Plus, a few weeks ago I got a mac and this didn’t work because the binding was already used by the system to switch virtual desktops and I couldn’t be bothered to change it).
The answer is yes, there’s a better way of navigating your shell. In this blog, I’ll try to show you a few of the capabilities that zsh offers in its vi mode and how to set them up.
DISCLAIMER: Zsh comes with the emacs shortcuts by default. If you are not comfortable and not interested in learning those based on vim, feel free to stick with the emac ones. Knowing either should be enough to make your life and your craft a bit smoother.
What can the vi mode do?
If you are familiar with vim, what you get when you enable the vi mode is access to the insert, normal and visual modes, plus bindings that are close to the motions we know and love from vim.
For those unfamiliar with vim, the text editor allows users to control everything through their keyboard. To enable a user to do all it needs to do the text editor needs to be able to allocate multiple behaviors to the same keys based on the context in which it’s pressed. Sometimes we are focused on writing text (insert mode), others we want to move around the text (normal mode), and others we just want to select it (visual mode) to paste it elsewhere.
The zsh vi mode makes it possible to get an experience similar to vim but in our shell where we are capable of changing modes and performing motions as if we were in the text editor without actually needing to open it. You can press ESC
and i
/a
to switch between the normal and insert modes, do CTRL+h
/CTRL+w
/CTRL+u
to delete the character/word/line to the left of the cursor’s current position, F+<character>
to find the character
backward in the line, and more.
How to set it up?
To enable the vi mode you need to execute the following line or add it to your $HOME/.zshrc
.
1bindkey -v
From this point forward you are all set! Now you are in the insert mode. If you added it to your zshrc do source $HOME/.zshrc
so it takes effect in your current terminal, or open a new one. Sometimes this is all you need, but there are a few modifications explained next that I like to do to get an even greater experience.
Improved vi mode
NOTE: If you want to know more about these commands you can check the documentation (Zsh Line Editor).
Fast transition to normal mode
When you are in insert mode and press ESC
to enable normal mode you will notice that it takes a moment to transition into that mode. To speed up things, you need the following line to reduce the timeout.
1export KEYTIMEOUT=1
The videos below show the effects of using this env var. It is worth noting that the cursor shown has been modified to show a bean when in insert mode and a block in normal mode; the default behavior is to only show the block. Down below you can find how to customize your cursor if you want.
Backspace
Imagine you are in your terminal in insert mode typing some command and you decide to go into normal mode to move your cursor one word back, you press i
to change into insert mode and decide to delete a few characters that are at the left of your cursor’s current position by pressing the backspace key. In any other situation, this should remove the characters you want, but not here; that will do nothing. X
(SHIFT+x
) in normal mode is the default key to delete characters to the left, though this is not the natural behavior (almost) anyone would expect.
To get backspace working every time that insert mode is enabled, the following line is required.
1bindkey '^?' backward-delete-char
Note that the character ^
normally represents the key CTRL
, but in this case ^?
represents the escape code sequence used by my keyboard backspace key. If you are wondering if pressing CTRL+?
would also delete the character, the answer is yes.
Delete
If you move your cursor to the left after writing something (without deleting anything) and try to delete the characters to the right of the cursor’s current position, your cursor will move to the right changing a few characters from lower to upper case (or vice versa).
To fix this we need the following commands.
1bindkey '^[[3~' delete-char # Enabled in insert mode
2bindkey -a '^[[3~' delete-char # Enabled in normal mode
Note that the flag -a
specifies that the binding should be available in the normal mode. If you don’t feel like having the Delete key enabled in normal mode, just remove the second line from your rc and all should be good.
Search history
In vim, you can use press /
or ?
in normal mode to search words in your document either forward or backward relative to your cursor’s current position. In zsh, this is the same but it works on your commands history and in reverse order. If you press /+<command>+ENTER
, zsh will search your history for anything containing <command>
, and pressing n
will iterate from newest to oldest. Pressing N
will iterate in reverse.
I have found the default behavior of ?
in vi zsh mode a bit useless as it will search forward from the history’s newest entry, and since zsh is unable to see the future and know what are our future commands, it will show nothing. For this reason, I have bound an incremental backward search to it for now.
1bindkey -a '?' history-incremental-search-backwards
Deleting characters shortcuts
In vim insert mode it’s possible to delete characters in a few ways. For example, you can use CTRL+h
to delete one character to the left, CTRL+w
to delete all the characters on a word between the beginning of the word to the cursor’s current position, or CTRL+u
to delete all the characters between the beginning of the line to the cursor’s current position. However, this is not enabled by default in zsh vi mode in some circumstances.
The following lines make this behavior available in zsh.
1bindkey '^h' backward-delete-char
2bindkey '^w' backward-kill-word
3bindkey '^u' backward-kill-line
Edit command in vim
There are times when I write commands that are too long (looking at you, curl) and wish vim was open to edit them. Fortunately, we can set zsh to send the current command in the prompt to vim, continue editing there, and then get it back with ease. The command below sets CTRL+e
on insert
and normal
modes to do so.
1autoload edit-command-line; zle -N edit-command-line
2bindkey '^e' edit-command-line # Enabled in insert mode
3bindkey -a '^e' edit-command-line # Enabled in normal mode
NOTE: This will use whatever you have set for the env var EDITOR
. Mine is set to nvim
.
Cursor
One last thing that we can do to improve the vi mode is to make the cursor style change based on if we are in insert
or normal
mode. That can be achieved by adding the snippet below, and you can specify if the cursor should be solid or blinking.
1# Change cursor shape for different vi modes.
2# - Taken from https://github.com/LukeSmithxyz/voidrice/blob/e0331ad0e76dcbcfcc08cb991d9e7f99382517db/.config/zsh/.zshrc
3# - Information on how to change the cursor style from https://vim.fandom.com/wiki/Change_cursor_shape_in_different_modes
4#
5# Cursor styles:
6# 1 -> blinking block
7# 2 -> solid block
8# 3 -> blinking underscore
9# 4 -> solid underscore
10# 5 -> blinking vertical bar
11# 6 -> solid vertical bar
12#
13# To change the cursor style we need to modify `\e[# q` where `#` is the cursor style.
14function zle-keymap-select () {
15 case $KEYMAP in
16 vicmd) echo -ne '\e[2 q';; # block
17 viins|main) echo -ne '\e[6 q';; # beam
18 esac
19}
20zle -N zle-keymap-select
21zle-line-init() {
22 zle -K viins # initiate `vi insert` as keymap (can be removed if `bindkey -V` has been set elsewhere)
23 echo -ne "\e[6 q"
24}
25zle -N zle-line-init
26echo -ne '\e[6 q' # Use beam shape cursor on startup.
27preexec() { echo -ne '\e[6 q' ;} # Use beam shape cursor for each new prompt.