A closer look at the zsh line editor and creating custom widgets

Last update:

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 emulation
  • viins: vi emulation (insert mode)
  • vicmd: vi emulation (command mode)
  • isearch: incremental search mode
  • command: 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!


See all posts in the archive.

Comments

Comments were disabled in March 2022. Since this page was created earlier, there may have been previous comments which are now inaccessible. Sorry.