The most surprising thing about Zsh’s vi mode is how it fundamentally changes your relationship with the command line, transforming it from a linear text editor into a powerful, modal editing environment.
Let’s see it in action. Imagine you’re typing a command, maybe kubectl get pods -n production.
kubectl get pods -n production
Now, you realize you forgot to add --all-namespaces. Instead of reaching for the arrow keys or Ctrl+A to go to the beginning, you hit Esc to enter normal mode. Then, you press i to go back into insert mode before the n.
kubectl get pods -n production
(Hit Esc, then i)
kubectl get pods -n production
Now you’re back in insert mode, right where you want to be.
But vi mode is much more than just editing text. It’s about efficient navigation and manipulation. Let’s say you have a long command history. You want to find that docker-compose up -d command you ran last week. Instead of endlessly pressing Ctrl+R, you hit Esc to enter normal mode, then /docker-compose and Enter. Boom, there it is. You can then press n to cycle through other matches.
Here’s how Zsh’s vi mode builds its mental model:
-
Modal Editing: The core concept is borrowing from Vim. You’re not always in "insert mode" where typing inserts characters. You have "normal mode" (accessed by
Esc) where keys are commands, and "insert mode" (accessed byi,a,o, etc.) where typing inserts characters. This separation allows for much more powerful and less error-prone editing. -
Command Line as a Buffer: Zsh treats the current command line as a buffer, just like in Vim. This means all of Vim’s motion commands (
h,j,k,l,w,b,0,$,^,G,gg) and text manipulation commands (dfor delete,cfor change,yfor yank,pfor paste,.for repeat) become available. -
History as a Buffer (of sorts): While not a direct buffer, your command history behaves like one. Commands like
/(search) and?(reverse search) let you navigate it with vi-like precision.Ctrl+PandCtrl+Nalso work for previous/next command, butEscfollowed bykorjcan also be used if configured. -
Key Bindings and Configuration: You enable vi mode with
bindkey -v. You can customize it extensively. For example,bindkey '^[' vi-insertensures thatEscalways puts you in insert mode, which is often what people expect. Many users also mapjandkin normal mode to navigate history:bindkey ' ' vi-cmd # Space in insert mode goes to normal mode bindkey '^[k' up-line-or-history # Use Esc-k to go up history bindkey '^[j' down-line-or-history # Use Esc-j to go down historyThis is configured in your
.zshrc.
Let’s say you’ve typed a long command and want to delete everything up to the first slash. In insert mode, you’d have to backspace a lot. In vi mode: Esc (normal mode), f/ (move to the next /), d (delete) then 0 (to the beginning of the line). The entire command up to that point is deleted.
The true power comes from combining these commands. For instance, if you want to change the word production to staging in kubectl get pods -n production, you can:
Esc (normal mode)
w (move to production)
ciw (change inner word)
Then type staging.
One common point of confusion is the difference between vi-cmd and vi-insert. When you type bindkey -v, Zsh sets up the default vi mode bindings. vi-insert is the mode where typing inserts text, and vi-cmd is the mode where keys are commands. The Esc key is the primary way to switch from vi-insert to vi-cmd. If you’ve been typing and hit Esc twice, the second Esc usually doesn’t do anything because you’re already in vi-cmd. Conversely, pressing i or a in vi-cmd switches you to vi-insert.
Once you’re comfortable, you’ll find yourself editing commands with a speed and precision that feels alien to traditional shell editing. You’ll start thinking in terms of motions and edits, not just typing and backspacing.
The next natural step is to explore more advanced Vim text objects and operators within Zsh, like dit (delete inner tag) or yap (yank around parenthesis), and how they can be applied to your shell commands.