### 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 ### The same licensing terms as with zsh apply. ############################################################################ ### CONFIGURATION ########################################################## # 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='yes' # 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 # 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]='c' else psvar[$psvmodeidx]='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]='c';; *) if [[ ${ft_zle_state[overwrite]} == yes ]]; then psvar[$psvmodeidx]='r' else psvar[$psvmodeidx]='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]='c' else psvar[$psvmodeidx]='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_functions=( "${zle_keymap_functions[@]}" 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_functions=( "${zle_finish_functions[@]}" 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_functions=( "${zle_init_functions[@]}" 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' 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'\'' 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 . 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