### vi-mode.zsh - example vi mode setup for zsh.
###
### Zsh's line editor has two built-in editing models. One is modeled
### after the `emacs' editor's input system, the other is modeled after
### the `vi' editor's modal input mode.
###
### To use the latter, it is helpful to track the current input mode
### and update the prompt dynamically to reflect the current mode at
### all times.
###
### This setup tracks the current mode in `$psvar[x]', which is
### available in prompts in the `%xv' expansion. It'll be one of the
### following strings:
###
### "i" - insert mode
### "c" - command mode
### "im" - insert mode, with a minibuffer being active
### "cm" - ditto, but for command mode
### "r" - replace mode: insert mode with the overwrite bit set.
###
### The `x' is configurable via the `psvmodeidx' variable below.
###
### The code in this file requires zsh version 4.3.1 or newer.
###
### Copyright (c) 2010, Frank Terbeck <ft@bewatermyfriend.org>
### The same licensing terms as with zsh apply.
############################################################################
### CONFIGURATION ##########################################################
# by <ccx@wpr.cz>
# make the text put into $psvar[x] configurable
if ! (($+psvmodetext)); then
typeset -g -A psvmodetext
psvmodetext=( i i c c r r im im cm cm )
fi
# Use either `ins' or `cmd' as the default input mode.
zle_default_mode='ins'
# This defines in which index of `$psvar[]' the information is tracked.
# The default is `1'. Set this to whatever fits you best.
psvmodeidx='1'
# If this is set to `yes', use `C-d' instead of `ESC' to switch from insert
# mode to command mode. `ESC' may require a timeout to actually take effect.
# Using `C-d' will work immediately. Therefore that is the default.
#zle_use_ctrl_d='yes'
# If set to `yes', make viins mode behave more like `emacs' mode. Bindings
# such as `^r' and `^s' will work. Backspace and `^w' will work more like
# emacs would, too.
: ${zle_ins_more_like_emacs:=no}
# This is an example prompt to show how this can be used. Yours may
# obviously be a lot more colourful and whatnot. As ugly as you like.
#PS1='%(?..[%?]-)%1v-(%!) %3~ %% '
############################################################################
### MODE SETUP - Do not change anything below! #############################
# Create _functions[] arrays for `zle-line-init', `zle-line-finish'
# and `zle-keymap-select', too. Analogous to precmd_functions[]
# etc. The actual functions only cycle through these arrays and
# execute all existing functions in order.
typeset -ga zle_init_functions
typeset -ga zle_finish_functions
typeset -ga zle_keymap_functions
zle_init_add() {
(($+zle_init_functions[(k)$1])) || zle_init_functions+=( $1 )
}
zle_finish_add() {
(($+zle_finish_functions[(k)$1])) || zle_finish_functions+=( $1 )
}
zle_keymap_add() {
(($+zle_keymap_functions[(k)$1])) || zle_keymap_functions+=( $1 )
}
# This is an associative array, that tracks the current zle
# state. This may contain the following key-value pairs:
# minibuffer - values: yes/no; yes if a minibuffer is active.
# overwrite - values: yes/no; yes if zle is in overwrite mode.
typeset -gA ft_zle_state
# This makes sure the first prompt is drawn correctly.
if [[ ${zle_default_mode} == 'cmd' ]]; then
psvar[$psvmodeidx]=${psvmodetext[c]}
else
psvar[$psvmodeidx]=${psvmodetext[i]}
fi
# We're not in overwrite mode, when zsh starts.
ft_zle_state[overwrite]=no
# When this is hooked into `zle-keymap-select', keymap changes are
# correctly tracked in `psvar[x]', which may be used in PS1 as `%xv'.
function ft-psvx() {
if [[ ${ft_zle_state[minibuffer]} == yes ]]; then
if [[ ${psvar[$psvmodeidx]} != *m ]]; then
psvar[$psvmodeidx]="${psvar[${psvmodeidx}m]}"
fi
else
case ${KEYMAP} in
vicmd) psvar[$psvmodeidx]=${psvmodetext[c]};;
*)
if [[ ${ft_zle_state[overwrite]} == yes ]]; then
psvar[$psvmodeidx]=${psvmodetext[r]}
else
psvar[$psvmodeidx]=${psvmodetext[i]}
fi
;;
esac
fi
zle 'reset-prompt'
}
# This needs to be hooked into `zle-line-finish' to make sure the next
# newly drawn prompt has the correct mode display.
function ft-psvx-default() {
if [[ ${zle_default_mode} == 'cmd' ]]; then
psvar[$psvmodeidx]=${psvmodetext[c]}
else
psvar[$psvmodeidx]=${psvmodetext[i]}
fi
}
# Need to handle SIGINT, too (which is sent by ^C).
# If we don't do this, being in a minibuffer and pressing ^C confuses
# the zle state variable; the `minibuffer' state won't be turned off.
function TRAPINT() {
ft_zle_state[minibuffer]=no
ft-psvx-default
zle reset-prompt 2>/dev/null
return 127
}
# If a keymap change is done, we do need a status update, obviously.
zle_keymap_add ft-psvx
# When a command line finishes, the next keyboard mode needs to be set
# up, so that `psvar[x]' is correct when the next prompt is drawn.
zle_finish_add ft-psvx-default
if [[ ${zle_default_mode} == 'cmd' ]]; then
# We cannot simply link `vicmd' to `main'. If we'd do that the
# whole input system could not be set into insert mode. See the
# zshzle(1) manual for details. To make vicmd the default mode
# for new command lines, we simply turn it on in `zle-line-init()'.
zle_init_add ft-vi-cmd
fi
function zle-line-init() {
local w
for w in "${zle_init_functions[@]}"; do
(( ${+functions[$w]} )) && "$w"
done
}
zle -N zle-line-init
function zle-line-finish() {
local w
for w in "${zle_finish_functions[@]}"; do
(( ${+functions[$w]} )) && "$w"
done
}
zle -N zle-line-finish
function zle-keymap-select() {
local w
for w in "${zle_keymap_functions[@]}"; do
(( ${+functions[$w]} )) && "$w"
done
}
zle -N zle-keymap-select
# Link `viins' to `main'.
bindkey -v
############################################################################
### VI WIDGETS #############################################################
# This setup may change the `ESC' keybinding to `C-d'. That defeats the
# possibility to exit zsh by pressing `C-d' (which usually sends EOF).
# With this widget, you can type `:q<RET>' to exit the shell from vicmd.
function ft-zshexit {
[[ -o hist_ignore_space ]] && BUFFER=' '
BUFFER="${BUFFER}exit"
zle .accept-line
}
zle -N q ft-zshexit
# First the ones that change the input method directly; namely cmd mode,
# insert mode and replace mode.
function ft-vi-replace() {
ft_zle_state[overwrite]=yes
zle vi-replace
ft-psvx
}
function ft-vi-insert() {
ft_zle_state[overwrite]=no
zle vi-insert
}
# Since I want to bind `vi-cmd-mode' to Ctrl-D (which is what I'm doing in
# vim and emacs-viper, too) I need to wrap this widget into a user-widget,
# because only those have an effect with empty command buffers and bindings
# to the key, which sends `EOF'. This also needs the ignore_eof option set.
function ft-vi-cmd() {
ft_zle_state[overwrite]=no
zle vi-cmd-mode
}
function ft-vi-cmd-cmd() {
zle -M 'Use `:q<RET>'\'' to exit the shell.'
}
# ...and now the widgets that open minibuffers...
# Oh, yeah. You cannot wrap `execute-named-cmd', so no minibuffer-signaling
# for that. See <http://www.zsh.org/mla/workers/2005/msg00384.html>.
function ft-markminibuf() {
ft_zle_state[minibuffer]=yes
ft-psvx
zle "$1"
ft_zle_state[minibuffer]=no
ft-psvx
}
if (( ${+widgets[.history-incremental-pattern-search-backward]} )); then
function history-incremental-pattern-search-backward() {
ft-markminibuf .history-incremental-pattern-search-backward
}
else
function history-incremental-search-backward() {
ft-markminibuf .history-incremental-search-backward
}
fi
if (( ${+widgets[.history-incremental-pattern-search-forward]} )); then
function history-incremental-pattern-search-forward() {
ft-markminibuf .history-incremental-pattern-search-forward
}
else
function history-incremental-search-forward() {
ft-markminibuf .history-incremental-search-forward
}
fi
function ft-vi-search-back() {
ft-markminibuf vi-history-search-backward
}
function ft-vi-search-fwd() {
ft-markminibuf vi-history-search-forward
}
function ft-replace-pattern() {
ft-markminibuf replace-pattern
}
# register the created widgets
for w in \
ft-replace-pattern \
ft-vi-{cmd,cmd-cmd,replace,insert,search-back,search-fwd}
do
zle -N "$w"
done; unset w
############################################################################
### ALTERED KEYBINDINGS ####################################################
if [[ ${zle_use_ctrl_d} == 'yes' ]]; then
setopt ignore_eof
bindkey -M viins '^d' ft-vi-cmd
bindkey -M vicmd '^d' ft-vi-cmd-cmd
# Remove the escape key binding.
bindkey -r '^['
fi
bindkey -M vicmd '/' ft-vi-search-fwd
bindkey -M vicmd '?' ft-vi-search-back
bindkey -M vicmd 'i' ft-vi-insert
bindkey -M vicmd 'R' ft-vi-replace
# The following four widgets require something like the following, to
# load and initialise the `replace-pattern' and `replace-string-again'
# widgets:
#
# zstyle ':zle:replace-pattern' edit-previous false
# autoload -Uz replace-string
# autoload -Uz replace-string-again
# zle -N replace-pattern replace-string
# zle -N replace-string-again
#
# ...and that *before* this file is sourced.
if (( ${+widgets[replace-pattern]} )); then
bindkey -M vicmd '^x,' ft-replace-pattern
bindkey -M viins '^x,' ft-replace-pattern
fi
if (( ${+widgets[replace-string-again]} )); then
bindkey -M vicmd '^x.' replace-string-again
bindkey -M viins '^x.' replace-string-again
fi
if [[ ${zle_ins_more_like_emacs} == 'yes' ]]; then
if (( ${+widgets[.history-incremental-pattern-search-backward]} )); then
bindkey -M viins '^r' history-incremental-pattern-search-backward
bindkey -M vicmd '^r' history-incremental-pattern-search-backward
else
bindkey -M viins '^r' history-incremental-search-backward
bindkey -M vicmd '^r' history-incremental-search-backward
fi
if (( ${+widgets[.history-incremental-pattern-search-forward]} )); then
bindkey -M viins '^s' history-incremental-pattern-search-forward
bindkey -M vicmd '^s' history-incremental-pattern-search-forward
else
bindkey -M viins '^s' history-incremental-search-forward
bindkey -M vicmd '^s' history-incremental-search-forward
fi
bindkey -M vicmd '^[h' run-help
bindkey -M viins '^[h' run-help
bindkey -M viins '^p' up-line-or-history
bindkey -M viins '^n' down-line-or-history
bindkey -M viins '^w' backward-kill-word
bindkey -M viins '^h' backward-delete-char
bindkey -M viins '^?' backward-delete-char
fi
true