Chaining Custom ZSH Widget Functions

Jun 11, 2018 04:14 · 864 words · 5 minutes read

The documented and seemingly acccepted way to hook into a ZSH function with a widget is to override the builtin with your own custom function.

Let’s say we wanted to duplicate everything typed at the prompt, we might do something like this:

my-magic-func() {
    zle .self-insert
    zle .self-insert

zle -N self-insert my-magic-func

Note: I chose self-insert from the ZSH docs as it triggers on every insertion.

This would yield the following:

Intially it looks like this works and we can call it a day, but there’s a problem with this approach: it overrides any previously defined custom self-insert handler.

To demonstrate, let’s declare a second custom function that just calls zle .self-insert once and observe what happens.

my-magic-func() {
    zle .self-insert
    zle .self-insert

bad-custom-func() {
    zle .self-insert

zle -N self-insert my-magic-func
zle -N self-insert bad-custom-func

To understand why this happens we need to take a closer look at what zle -N does. Looking at the ZSH Docs again shows the following (emphasis mine):

zle -N widget [ function ]

Create a user-defined widget. If there is already a widget with the specified name, it is overwritten. When the new widget is invoked from within the editor, the specified shell function is called. If no function name is specified, it defaults to the same name as the widget. For further information, see Widgets.

So each call to zle -N for a given widget overrides the previous definition1. You may be wondering how we are still able to call the builtin widget even after we’ve overridden it; the answer lies just a little further down the documentation:

Each built-in widget has two names: its normal canonical name, and the same name preceded by a ‘.’. The ‘.’ name is special: it can’t be rebound to a different widget. This makes the widget available even when its usual name has been redefined.

Which is why the custom functions above use .self-insert instead of self-insert: to call the built-in widget directly. If the functions instead called zle self-insert they would be recursively called until the stack overflowed.

recursive-custom-func() {
    zle self-insert

zle -N self-insert recursive-custom-func

So now we know how to define our own custom widgets and how to call the built-in widgets they’ve replaced. We’re getting closer to something that can be chained together more intelligently, but first we need a way to inspect the currently defined widgets.

ZSH provides the $widgets variable that allows exactly this:

% for k in "${(@k)widgets}"; do echo "${k}: ${widgets[$k]}"; done
.beginning-of-buffer-or-history: builtin
.history-search-forward: builtin
forward-word: builtin
vi-add-next: builtin
.backward-char: builtin

You can do this in your own shell, the variable is defined (almost) everywhere. The interesting thing here to note is that there are three distinct types: builtin, those prefixed with completion:, and those prefixed with user:.

% for k in "${(@k)widgets}"; do echo "${k}: ${widgets[$k]}"; done | cut -d ' ' -f2 | cut -d ':' -f 1 | sort | uniq -c
 377 builtin
  24 completion
  10 user

Ignoring completions because they can be chained together already by design, lot’s take a look at the layout of the user: items.

% for k in "${(@k)widgets}"; do echo "${k}: ${widgets[$k]}"; done | rg user
edit-command-line: user:edit-command-line
zle-line-finish: user:_zle_line_finish
_end_paste: user:_end_paste
self-insert: user:url-quote-magic
bracketed-paste: user:bracketed-paste-magic
up-line-or-beginning-search: user:up-line-or-beginning-search
zle-line-init: user:_zle_line_init
paste-insert: user:_paste_insert
down-line-or-beginning-search: user:down-line-or-beginning-search
_start_paste: user:_start_paste

The format of these isn’t really surprising because we already know that there can only be one function associated with a widget at a time, but what is worth remembering is that these are just user shell functions. This means they can be inspected, copied, and invoked as such.

% . ./absurd.zsh
% whence -f my-magic-func
my-magic-func () {
    zle .self-insert
    zle .self-insert

Tying it all together, we can use $widgets to see if there is already a custom widget defined. With a little creativity, this allows us to have multiple custom widget functions that recursively call each other in the order they were declared with zle -N.

# Check for existence of a custom user func
if [[ $widgets[self-insert] != "user:*" && $widgets[self-insert] != "user:newline-custom-func" ]]; ]]; then
    # drop the user: prefix
    # Nothing defined (or we're chaining on ourself), call the built-in we want directly
    to_exec="zle .self-insert"

# Prints a newline before each char
eval "newline-custom-func() {
    print -n '\n'

zle -N self-insert newline-custom-func

The eval might seem a little strange here, but it’s necessary because of variable scoping. Specifically, we need to use the externally-defined $to_exec in a templating capacity to fill in the function.

This is by no means perfect though. I can think of at least a few failure scenarios, not least of which is that declaring the above twice will actually wipe out any chained function. I unfortunately don’t have a good solution to these problems though, but hopefully what I’ve shared so far will be helpful to some.

  1. I have checked for something that is designed for appending to widgets similar to how add-zsh-hook works, but was unable to find anything. If anyone is aware of a better way to do this than I’ve presented here please email me so I can update this post. [return]