In a previous post, I alluded to the possibility of uniting Vim and Emacs in a shell, and this is that post! For those of you who want to have the flexibility of having both Vim and Emacs keybindings in your shell environment, read on for how to set this up.
In this post, we will incrementally build up a complete ~/.zshrc
config that
will support both Vim and Emacs keybindings, which means we’ll have a dual-mode
command-line environment, just like the Vim editor, with both insert and normal
(command) mode. Emacs keybindings will work in insert mode, since it’s not a
modal editor.
Let’s start by initializing our command-line prompt. We will, of course, need to enable variable substitution:
setopt prompt_subst
We will begin in Vim’s INSERT
mode, which matches Emacs-style command-line
editing:
VIMODE=" I"
The leading space here is intentional: it will separate the mode character from
the text coming before it. The normal or command mode will be " C"
, similarly
with a leading space. You can, of course, choose " N"
for normal mode, if you
wish, by modifying the code below appropriately.
Next, let’s define our left-hand side prompt template:
readonly PS1_TEMPLATE='%B%n%b@%U%m%u${VIMODE}%(!.#.>) '
This string is in single quotes to avoid early evaluation; we will
periodically re-evaluate this string, at which time we will need to
capture the then-current value of $VIMODE
.
This will display as follows:
user@hostname I> # insert mode
user@hostname C> # command mode
Let’s evaluate the current value of $PS1_TEMPLATE
in the current environment,
i.e., where $VIMODE
is " I"
, and store it to initialize the prompt:
PS1="${PS1_TEMPLATE}"
This next part is optional and orthogonal to the Vim+Emacs config, but keeps
the left-hand side at a fixed length by putting the current path on the right
side of the prompt. Here, we keep at most 5 dirs on the right prompt,
collapsing $HOME
to ~
:
RPS1="%5~"
Let’s enable Vim-style keybindings with some standard Emacs-style control keys that you might be used to in a standard shell environment:
bindkey -v
bindkey "^A" beginning-of-line
bindkey "^E" end-of-line
bindkey "^K" kill-line
bindkey "^L" clear-screen
bindkey "^R" history-incremental-search-backward
bindkey "^U" kill-whole-line
bindkey "^W" backward-kill-word
bindkey "^Y" yank
Let’s also tell Zsh to intialize line editing in Vim insert mode. Note that it
must match the $VIMODE
initial setting above, which is " I"
:
function zle-line-init() {
zle -K viins
}
zle -N zle-line-init
Show the mode in the prompt
Now, let’s define how mode switches should update our prompt. Here, we are
using short mode line (command vs. insert mode) on the left-hand side. If you
prefer having the full mode text here, feel free to substitute C
with
NORMAL
and I
with INSERT
— keeping their lengths equal ensures that
your prompt doesn’t shift left/right when changing modes:
function zle-keymap-select {
VIMODE="${${KEYMAP/vicmd/ C}/(main|viins)/ I}"
# Re-evaluate $PS1_TEMPLATE with the updated value of $VIMODE.
PS1="${PS1_TEMPLATE}"
zle reset-prompt
}
zle -N zle-keymap-select
Note that we are not modifying $RPS1
here (which holds our path) because it’s
independent of the insert vs. normal (command) mode in this case.
Open full editor for complex commands.
Another optional feature here is to enable richer command-line editing for much
longer or complex commands is to press v
to open your text editor (defined in
$EDITOR
) to edit your command line. Simply save and exit in the editor to
update your command line in-place.
autoload -U edit-command-line
zle -N edit-command-line
bindkey -M vicmd v edit-command-line
Putting it all together
Let’s combine the snippets of code we’ve worked out above to get our final solution:
# Copyright 2019 Misha Brukman
# SPDX-License-Identifier: Apache-2.0
# https://misha.brukman.net/blog/2019/12/uniting-vim-and-emacs-in-zsh/
setopt prompt_subst
VIMODE=" I"
# This string is in single quotes to avoid early evaluation; we will
# periodically re-evaluate this string, at which time we will need to
# capture then-current value of $VIMODE.
readonly PS1_TEMPLATE='%B%n%b@%U%m%u${VIMODE}%(!.#.>) '
PS1="${PS1_TEMPLATE}"
# Show current path with at most 5 dirs on the right prompt, collapsing
# $HOME as `~`.
RPS1="%5~"
# Vi with some Emacs flavor control keys.
bindkey -v
bindkey "^A" beginning-of-line
bindkey "^E" end-of-line
bindkey "^K" kill-line
bindkey "^L" clear-screen
bindkey "^R" history-incremental-search-backward
bindkey "^U" kill-whole-line
bindkey "^W" backward-kill-word
bindkey "^Y" yank
function zle-line-init() {
# Note: this initial mode must match the $VIMODE initial value above.
zle -K viins
}
zle -N zle-line-init
# Show insert/command mode in vi.
# zle-keymap-select is executed every time KEYMAP changes.
function zle-keymap-select {
VIMODE="${${KEYMAP/vicmd/ C}/(main|viins)/ I}"
PS1="${PS1_TEMPLATE}"
zle reset-prompt
}
zle -N zle-keymap-select
# 'v' in visual mode opens VIM to edit the command in a full editor.
autoload -U edit-command-line
zle -N edit-command-line
bindkey -M vicmd v edit-command-line