A closer look at the zsh line editor and creating custom widgets
The zsh line editor, aka “the command prompt”, is the bridge between you and zsh. If you regularly work in text-based environments (and are using zsh), chances are you’re spending a good amount of time in there.
While intimidating at first, unforgivingly blunt and boring, you’ve gotten familiar with it over time. You might even have become reasonably efficient at entering commands and one-liner scripts.
And you’ve probably learned to navigate it. You’ve mastered common tasks, such as: repeat previously entered commands, search the history, move to beginning/end of line, move one word backward/forward, delete the previous word, delete until end of line, etc.
Let’s take a closer look at the zsh line editor and extend it with custom functionality. To be even faster and even more efficient. And for coolness, of course.
What are keymaps?
The zsh line editor comes with the concept of keymaps. You can think of them as a collection of keyboard shortcuts and interaction modes. When a new keymap is chosen, all keyboard shortcuts are replaced with the ones defined in that new keymap.
The following keymaps are set up by default in zsh:
emacs
: emacs emulationviins
: vi emulation (insert mode)vicmd
: vi emulation (command mode)isearch
: incremental search modecommand
: command reading mode.safe
: fallback keymap (cannot be deleted)
emacs
is the default mode in most shells (such as zsh, Bash, etc) and also on
most readline-based applications. Shortcuts like <alt>+b
, <alt>+f
,
<ctrl>+a
, <ctrl>+e
and <ctrl>+w
are straight out of emacs emulation. The
emacs emulation mode is enabled with bindkey -e
.
viins
and vicmd
are based on vi and will be familiar to people working in
vim. To switch from insert mode to command mode, press esc
. To switch from
command mode to insert mode, press i
. The vi emulation mode usually starts in
insert mode and is enabled with bindkey -v
.
In vi emulation, command mode is when you can navigate the line editor and execute widgets. We’ll get to widgets in a minute.
The other keymaps (isearch
, command
and .safe
) are of lesser importance
for now and won’t be covered here.
Zsh creates additional keymaps on different occasions (for example when selecting an entry from an autocomplete list). You can also create your own keymap and base it off a default one. Please refer to zsh’s documentation for more information on these topics.
Most people don’t bother changing the keymap and leave it at emacs
. Emacs
emulation is the default in many text-based environments and getting familiar
with it assures you’ll be efficient in applications that don’t offer the
less-popular vi emulation. Some GUIs support (parts of) emacs emulation as well,
such as the text input system on Mac OS X.
Alt, Meta and Escape
You might have seen different spellings for what appears to be the same
shortcut. For instance, backward-word
is often spelled out as <alt>+b
,
\eb
, <esc>+b
, <Meta>+b
or M-b
. These are all the same. Depending on the
settings of your terminal, you can type <alt>+b
or alternatively hit esc
and
then b
. They effectively map to the same keycode, namely \eb
. Hence the
different spellings. <Meta>
is a leftover from older times and is essentially
a synonym for \e
.
I usually spell it as <alt>+b
.
What are zle commands and widgets?
ZLE stands for Zsh Line Editor, as described earlier. A zle command is a specific functionality provided by the zle. A zle widget is a synonym for zle command – albeit a more specific term since the word command is rather overloaded already. I will use widget for the remainder of this article.
Pretty much any interaction with zsh is provided by a widget. Move to beginning
of line? beginning-of-line
. Move one word backward? backward-word
. Delete
previous word? backward-kill-word
. Delete until end of line?
backward-kill-line
. You get the idea, everything is a widget.
List of widgets
You can get the list of all available widgets with:
% zle -la
.accept-and-hold
.accept-and-infer-next-history
.accept-and-menu-complete
.accept-line
.accept-line-and-down-history
.accept-search
# [... many more widgets ...]
which-command
yank
yank-pop
zle-keymap-select
zle-line-finish
zle-line-init
The built-in widgets are explained here.
Notice the widgets starting with a dot? These always refer to the zsh built-in version of a widget. Widgets can we overridden with custom functionality and it is sometimes necessary to unambiguously refer to the built-in one.
Executing a widget
A widget is usually invoked:
- through a key binding (essentially a keyboard shortcut, such as
<ctrl>+a
), - from inside another widget via
zle <widget_name>
, - from a special interactive widget called
execute-named-cmd
.
Now, if a widget isn’t bound to any key, how do you execute execute-named-cmd
in order to invoke the widget?
That depends on your keymap: <alt>+x
in emacs emulation,
:
in command mode of vi emulation.
You will be presented a prompt to enter a widget name. Any of the widgets listed
by zle -la
is a valid input. Widget names can be autocompleted with <tab>
.
You can execute the execute-named-cmd
widget from the middle of a command
line, invoke a specific widget and resume editing the command line after the
widget ran.
For example:
% echo this is a test[<alt>+x]
execute: backward-kill-[<tab>-<tab>]
backward-kill-line backward-kill-word
execute: backward-kill-word[<enter>]
% echo this is a
And yes, you can even invoke execute-named-cmd
from inside
execute-named-cmd
, although that’s of limited practical use.
Which widget is invoked by a given shortcut?
To find out which widget is invoked by a given shortcut you can use bindkey
followed by the string representation of the key. \e
stands for <alt>
(see
here) and ^
for <ctrl>
.
For example:
% bindkey '\eb'
"^[b" emacs-backward-word
% bindkey '^w'
"^W" backward-kill-word
By default bindkey
will operate on the currently selected keymap. You can
operate on a different keymap by passing the -M
flag, such as:
% bindkey -M vicmd 'b'
"b" vi-backward-word
The complete list of shortcuts and associated widgets for the current keymap is printed with:
% bindkey
"^@" set-mark-command
"^A" beginning-of-line
"^B" backward-char
"^D" delete-char-or-list
"^E" end-of-line
# [... many more ...]
To list the shortcuts for a keymap different from the current one, pass the -M
flag:
% bindkey -M vicmd
"^D" list-choices
"^G" list-expand
"^H" vi-backward-char
"^J" accept-line
"^L" clear-screen
# [... many more ...]
Which shortcut keys invoke a given widget?
In the inverse situation where you want to find out which shortcut invokes a
given widget, execute the where-is
widget as described
previously. Tab autocompletion works there as well.
For example:
% [<alt>+x]
execute: where-is[<cr>]
Where is: backward-kill-[<tab>-<tab>]
backward-kill-line backward-kill-word
Where is: backward-kill-word
backward-kill-word is on "^W" "^[^H" "^[^?"
The current keymap (in this case, emacs) binds backward-kill-word
to
<ctrl>+w
(and to two other shortcuts).
Alternatively you can list all shortcuts and grep by the name of the widget:
% bindkey | grep backward-kill-word
"^W" backward-kill-word
"^[^H" backward-kill-word
"^[^?" backward-kill-word
Shortcut to simulate keyboard input
It is possible to bind a shortcut to a hardcoded string, as if directly entered on the keyboard. For example:
% bindkey -s '\es' 'hello world!'
Now every time <alt>+s
is pressed on the command line, the string “hello
world!” is inserted.
The simulated keyboard input can have shortcuts itself, making things like the following possible:
% bindkey -s '\eg' '^Ugit status^M'
This binds <alt>+g
to deleting the whole line (^U
), entering git status
and then executing the line (^M
).
While not as powerful as a full-blown widget this kind of shortcut-to-keyboard-input mapping is quick to set up and straightforward to implement.
But there’s a major drawback: these mappings can only invoke widgets by their
respective shortcuts. They often won’t work if a different keymap mode is
selected (e.g. viins
instead of emacs
) or if the shortcut to invoke a widget
has been assigned to a different widget. Furthermore they cannot contain any
logic or functionality besides simulating keyboard input.
Gimme, gimme, gimme a widget!
To work around the drawback of hardcoded strings zle provides the ability to create custom widgets.
First create a function. We’ll reuse the previous example of killing the whole
line, entering git status
and executing the line.
function _git-status {
zle kill-whole-line
zle -U "git status"
zle accept-line
}
The function reads quite fluently. You’ll note that zle <widget_name>
invokes
a named widget and zle -U <string>
adds the string to the line buffer at the
current location. I like to prepend the function name with an underscore to
highlight its use as a special shell function rather than a user-callable
function.
The next step is to declare that function as a widget:
zle -N _git-status
Our new widget appears in the list of available widgets:
% zle -la | grep git-status
_git-status
Custom functions can also override built-in widgets:
function _accept-line-with-echo {
echo "Executing: $BUFFER"
zle .accept-line
}
zle -N accept-line _accept-line-with-echo
The variable $BUFFER
holds the current content of the zle. There are many more
interesting
variables.
As explained earlier, the widget name prepended by a dot always invokes the built-in version of the widget.
Our function is now a full-blown widget. We can therefore assign a shortcut to it, as we can for any other widget:
bindkey '\eg' _git-status
That’s it, <alt>+g
now executes git status
, regardless of other
configurations in the keymap.
To list only user-defined widgets and overridden built-in ones, run the following:
% zle -l
accept-and-hold (_zsh_highlight_widget_accept-and-hold)
accept-and-infer-next-history (_zsh_highlight_widget_accept-and-infer-next-history)
accept-and-menu-complete (_zsh_highlight_widget_accept-and-menu-complete)
accept-line (_zsh_highlight_widget_accept-line)
accept-line-and-down-history (_zsh_highlight_widget_accept-line-and-down-history)
accept-search (_zsh_highlight_widget_accept-search)
argument-base (_zsh_highlight_widget_argument-base)
auto-suffix-remove (_zsh_highlight_widget_auto-suffix-remove)
# [... many more widgets ...]
The function name is given in parenthesis.
The list might contain widgets with a -C
parameter. These are used for
autocompletion. It’s an extensive topic which I won’t cover in this article.
Please refer to the zsh
documentation.
Configure zle widgets at zsh startup
In the previous examples we’ve seen a mix of function definitions and zsh
commands, such as zle
and bindkey
. Where do you put these so that the custom
widgets are available in every zsh instance?
I usually put all zle-related instructions into a file zle.sh
and source it at
zsh startup. It boils down to something like the following.
File ~/.zsh/zle.zsh
:
# Bind \eg to `git status`
function _git-status {
zle kill-whole-line
zle -U "git status"
zle accept-line
}
zle -N _git-status
bindkey '\eg' _git-status
Somewhere in ~/.zshrc
:
source ~/.zsh/zle.zsh
The actual code is a bit more involved and available in my dotfiles.
Where to next?
Widgets are a very powerful feature in the zsh line editor. They let you customize and automate a lot of mundane, repetitive tasks. There is more to it than what we’ve covered in this article. The zle documentation is quite extensive and a good read to explore the topic in more depth.
We’ve scratched the surface with contrived examples. But hopefully that’s enough to get you started on writing your own widgets and become more efficient at using the zsh line editor.
I’d love to hear back from you. Please don’t hesitate to post questions or your own favourite custom widgets in the comments below. Thanks!
Comments
Comments were disabled in March 2022. Since this page was created earlier, there may have been previous comments which are now inaccessible. Sorry.