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 definition. 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
to_exec="${widgets[self-insert]#"user:"}"
else
# Nothing defined (or we're chaining on ourself), call the built-in we want directly
to_exec="zle .self-insert"
fi
# Prints a newline before each char
eval "newline-custom-func() {
${to_exec}
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.