Home, End, Insert, Delete, PageUp, PageDown for those keyboards that have them and for those terminals that send some unique characters or sequences of characters when they're pressed are not bound by default in any of zsh's keymaps (emacs, vi-insert, vi-command..., see bindkey -l
or the $keymaps
array for the full list).
There's nothing bound to function keys, or those above or arrow keys or Tab, Backspace, Escape when combined with Shift or Control or Alt either (again for those terminals that do send unique sequences for those).
Arrow keys (UpDownLeftRight) are bound by default as most terminals send the same escape sequences upon those. That's almost always either ^[[A
...^[[D
or ^[OA
...^[OD
depending on the terminal and/or whether it's in keypad transmit mode (see smkx
in terminfo(5)
) or not.
You can see zsh
's default key bindings in the various keymaps, by running zsh
with the -f
option (which skips system or user initialisation files except the zshenv ones) and run:
for m ($keymaps) bindkey -M $m | grep -H --label=$m .
The manual will also show you what widgets are found to which keys in the emacs
/vicmd
/viins
keymaps.
If you pipe that loop above to grep '\^\[[[O][A-D]'
, you see, regardless of the value of $TERM
:
% (for m ($keymaps) bindkey -M $m | grep -H --label=$m .) | grep '\^\[[[O][A-D]'
visual:"^[OA" up-line
visual:"^[OB" down-line
visual:"^[[A" up-line
visual:"^[[B" down-line
viopp:"^[OA" up-line
viopp:"^[OB" down-line
viopp:"^[[A" up-line
viopp:"^[[B" down-line
vicmd:"^[OA" up-line-or-history
vicmd:"^[OB" down-line-or-history
vicmd:"^[OC" vi-forward-char
vicmd:"^[OD" vi-backward-char
vicmd:"^[[A" up-line-or-history
vicmd:"^[[B" down-line-or-history
vicmd:"^[[C" vi-forward-char
vicmd:"^[[D" vi-backward-char
main:"^[OA" up-line-or-history
main:"^[OB" down-line-or-history
main:"^[OC" forward-char
main:"^[OD" backward-char
main:"^[[A" up-line-or-history
main:"^[[B" down-line-or-history
main:"^[[C" forward-char
main:"^[[D" backward-char
viins:"^[OA" up-line-or-history
viins:"^[OB" down-line-or-history
viins:"^[OC" vi-forward-char
viins:"^[OD" vi-backward-char
viins:"^[[A" up-line-or-history
viins:"^[[B" down-line-or-history
viins:"^[[C" vi-forward-char
viins:"^[[D" vi-backward-char
emacs:"^[OA" up-line-or-history
emacs:"^[OB" down-line-or-history
emacs:"^[OC" forward-char
emacs:"^[OD" backward-char
emacs:"^[[A" up-line-or-history
emacs:"^[[B" down-line-or-history
emacs:"^[[C" forward-char
emacs:"^[[D" backward-char
Both flavours of escape sequences for those 4 arrow keys are bound in most keymaps to the actions you usually expect them to have in the given contexts.
Those widgets are also bound to the usual emacs/vi keys as well (like ^B
, ^F
, ^P
, ^N
in emacs mode or h
, j
, k
, l
in vi-cmd mode).
You'll also find bindings of course for Esc, Tab, Backspace, Enter for which all terminals send very well known single control characters (though for Backspace, there are those that send BS and those that send DEL).
But you won't find anything about any other function key.
Here's a way for instance to get a summary of what is sent by the terminals known to the terminfo database upon pressing End:
$ (typeset -A count; for TERM (/usr/share/terminfo/*/*(.:t)) (( count[\$terminfo[kend]]++ )); typeset -p1 count)
typeset -A count=(
[$'\M-\C-@O']=8
['']=1341
[$'\M-\C-?\M-(']=6
[$'\C-Ak\C-M']=1
[$'\C-SI']=3
[$'\C-[)4\C-M']=4
[$'\C-[0']=8
[$'\C-[F']=1
[$'\C-[K']=4
[$'\C-[OF']=99
[$'\C-[T']=13
[$'\C-[Y']=1
[$'\C-[[146q']=23
[$'\C-[[1~']=11
[$'\C-[[220z']=21
[$'\C-[[24;1H']=3
[$'\C-[[4~']=139
[$'\C-[[5~']=9
[$'\C-[[8~']=28
[$'\C-[[F']=49
[$'\C-[[K']=1
[$'\C-[[OF']=1
[$'\C-[[U']=15
[$'\C-[[Y']=16
[$'\C-[[d']=1
[$'\C-[_1\C-[\\']=1
[$'\C-[k']=3
[$'\C-[z']=13
['- @']=1
['-45~']=1
['-4~']=5
[1!]=1
)
You'll see that for most terminals, what they send if any is not known. $'\C-[[4~'
is the most common.
Delete nowadays commonly sends \e[3~
, but sometimes DEL (^?
) instead (the one most commonly sent upon backspace these days) and for many, that can be configured in the terminal emulator settings. Some terminal emulators can also be told what type keyboard to emulate and different sequences would be sent for those function keys. See for instance for xterm, quoting its manual:
-kt
keyboardtype
This option sets the keyboardType
resource. Possible values include: “unknown”, “default”, “legacy”, “hp”, “sco”, “sun”, “tcap” and “vt220”.
Now, a user will know what kind a keyboard, what terminal emulators and on what system they will use. An operating system vendor can also make a more educated guess than zsh (which has been used on thousands of different systems for over 30 years).
For instance, a distribution of Debian GNU/Linux for x86 PCs that tries to maintain a terminfo database relatively faithful to the few dozen terminal emulators it includes among its packages (most of them being xterm
-like) can reasonably know what escape sequences are sent upon those few function keys commonly found on PC keyboards as long as users don't decide to change the configuration of their terminal emulators from the default and don't login remotely from alien operating systems.
So you'll find that Debian adds this to /etc/zsh/zshrc
(the system customisation file for the interactive invocations of zsh
):
# /etc/zsh/zshrc: system-wide .zshrc file for zsh(1).
#
# This file is sourced only for interactive shells. It
# should contain commands to set up aliases, functions,
# options, key bindings, etc.
#
# Global Order: zshenv, zprofile, zshrc, zlogin
READNULLCMD=${PAGER:-/usr/bin/pager}
An array to note missing features to ease diagnosis in case of problems.
typeset -ga debian_missing_features
if [[ -z "${DEBIAN_PREVENT_KEYBOARD_CHANGES-}" ]] &&
[[ "$TERM" != 'emacs' ]]
then
typeset -A key
key=(
BackSpace "${terminfo[kbs]}"
Home "${terminfo[khome]}"
End "${terminfo[kend]}"
Insert "${terminfo[kich1]}"
Delete "${terminfo[kdch1]}"
Up "${terminfo[kcuu1]}"
Down "${terminfo[kcud1]}"
Left "${terminfo[kcub1]}"
Right "${terminfo[kcuf1]}"
PageUp "${terminfo[kpp]}"
PageDown "${terminfo[knp]}"
)
function bind2maps () {
local i sequence widget
local -a maps
while [[ "$1" != "--" ]]; do
maps+=( "$1" )
shift
done
shift
sequence="${key[$1]}"
widget="$2"
[[ -z "$sequence" ]] && return 1
for i in "${maps[@]}"; do
bindkey -M "$i" "$sequence" "$widget"
done
}
bind2maps emacs -- BackSpace backward-delete-char
bind2maps viins -- BackSpace vi-backward-delete-char
bind2maps vicmd -- BackSpace vi-backward-char
bind2maps emacs -- Home beginning-of-line
bind2maps viins vicmd -- Home vi-beginning-of-line
bind2maps emacs -- End end-of-line
bind2maps viins vicmd -- End vi-end-of-line
bind2maps emacs viins -- Insert overwrite-mode
bind2maps vicmd -- Insert vi-insert
bind2maps emacs -- Delete delete-char
bind2maps viins vicmd -- Delete vi-delete-char
bind2maps emacs viins vicmd -- Up up-line-or-history
bind2maps emacs viins vicmd -- Down down-line-or-history
bind2maps emacs -- Left backward-char
bind2maps viins vicmd -- Left vi-backward-char
bind2maps emacs -- Right forward-char
bind2maps viins vicmd -- Right vi-forward-char
# Make sure the terminal is in application mode, when zle is
# active. Only then are the values from $terminfo valid.
if (( ${+terminfo[smkx]} )) && (( ${+terminfo[rmkx]} )); then
function zle-line-init () {
emulate -L zsh
printf '%s' ${terminfo[smkx]}
}
function zle-line-finish () {
emulate -L zsh
printf '%s' ${terminfo[rmkx]}
}
zle -N zle-line-init
zle -N zle-line-finish
else
for i in {s,r}mkx; do
(( ${+terminfo[$i]} )) || debian_missing_features+=($i)
done
unset i
fi
unfunction bind2maps
fi # [[ -z "$DEBIAN_PREVENT_KEYBOARD_CHANGES" ]] && [[ "$TERM" != 'emacs' ]]
[...]
As you can see above, it defines an associative array that maps key names to corresponding escape sequences. Those escape sequences are retrieved from the terminfo database which Debian happens to ship by default (though with a limited list of entries by default).
But because the terminfo database only gives the escape sequences that terminal send when in keypad transmit mode, you'll see that zshrc also tells ZLE to enter that mode when starting and leave it upon exiting.
That explains why you see different sequences when at the prompt (within ZLE) of an interactive zsh shell (where that zshrc
is run) and when not.
Scripts and more generally non-interactive invocations of zsh don't read zshrc. Since zshrc is where you put all your customisations, aliases, functions... if scripts read them, that would be sure to break them¹.
Now vared
also uses the ZLE, and when invoked in scripts where zshrc is not read, that means it won't get the bindings supplied by your operating system in /etc/zsh/zshrc.
So, if you like those bindings and you want them to be available to users of those scripts whether they are using the same operating system as you or not, you'll need to include them in your script by yourself.
Also beware that ZLE is in emacs
or vi
mode by default depending on whether $EDITOR
or $VISUAL
starts with vi
or contains /vi
or not, and you may also want to force them into one mode or another if you want consistent key bindings for users of your script. bindkey
by default configures the bindings of the main
keymap which is an alias to either emacs
or viins
depending on whether the emacs or vi mode is selected (with bindkey -e
or bindkey -v
).
Besides the Debian ones, you'll find many other suggestions of customisations around to bind the function keys including in combinations with Shift/Alt/Ctrl to mimic the behaviour of some other editors (on terminals that send unique sequences for those).
See for instance Zsh zle shift selection on Stack Overflow of oh-my-zsh
's key bindings which you could get inspiration from.
¹ see how csh scripts usually have a #! /bin/csh -f
shebang, where -f
skips reading the ~/.cshrc
to avoid them running into this kind of problem.
/etc/zsh/zshrc
– Stéphane Chazelas Feb 05 '24 at 10:03/etc/zsh/zshenv
. Or~/.zshenv
for only your invocations. – Stéphane Chazelas Feb 06 '24 at 15:15vared
in that script, those three keys (and possibly others I'm unaware of) behave differently to how they do in the parent zsh, from which I run the script. I really don't want to make permanent changes to a system as the script is intended to be portable. – Walf Feb 07 '24 at 03:24