β¨οΈ ZSHAND Widget Development GuideΒΆ
π This guide covers how ZLE widgets work, how to build new ones, keybinding registration, and testing strategies.
π§ Philosophy
Widgets are the interactive layer of the framework β they turn keystrokes into powerful actions. Keep them fast, safe, and self-documenting. Every widget should be usable without reading the source.
Widgets at a glanceΒΆ
| Category | Key / trigger | Example widget | What it does |
|---|---|---|---|
| Clipboard | Ctrl+P | pastem | Smart paste (strip formatting) |
| Clipboard | Ctrl+O | copym | Copy line to clipboard |
| History | β / β | (ZLE default) | History search |
| Completion | Tab | (ZLE default) | Menu completion |
| Git | (custom) | git-status | Show status in prompt / buffer |
| Edit | (custom) | edit-cmd | Open current line in editor |
Keybindings are set in Config (config.toml or rc.d/) or in core/16_widgets.zsh. Full list: Source Reference (generated from headers).
See also: Config (keybindings) Β· Architecture (widget dir merge)
ποΈ How Widgets WorkΒΆ
Widgets are ZLE (Zsh Line Editor) functions β they run while you're typing a command, with direct access to the command line buffer. They can:
- π Read and modify
$BUFFER(the command line text) - π Move
$CURSOR(cursor position) - βΆοΈ Execute the line (
zle accept-line) - π Redraw the prompt (
zle reset-prompt) - π Copy to clipboard, open editors, run git commands, etc.
π LifecycleΒΆ
π Key ZLE VariablesΒΆ
| Variable | Type | Purpose |
|---|---|---|
BUFFER | string | Entire command line content |
LBUFFER | string | Text left of cursor |
RBUFFER | string | Text right of cursor |
CURSOR | int | Cursor position (0-indexed) |
WIDGET | string | Name of the current widget |
KEYS | string | Keys that triggered the widget |
π Writing a WidgetΒΆ
π§± Basic TemplateΒΆ
#!/usr/bin/env zsh
# ββ my_widget β Short description βββββββββββββββββββββββββββββββββββββββββββ
#
# Description of what this widget does.
#
# ββ TRIGGER βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
#
# Ctrl+H (configured in config.toml or core/16_widgets.zsh)
#
# ββ DEPENDENCIES ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
#
# wl-copy required Clipboard access (Wayland)
#
# ββ EXAMPLES ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
#
# # Type a command, press Ctrl+H
# # β Widget processes the buffer and shows result
#
# ββ SEE ALSO ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
#
# widgets/README.md Widget reference
# core/16_widgets.zsh Widget registration
#
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
my_widget() {
# Guard: check dependencies
(( $+commands[wl-copy] )) || {
zle -M "β οΈ wl-copy required"
return 1
}
# Guard: check buffer not empty
[[ -z "$BUFFER" ]] && {
zle -M "β οΈ Buffer is empty"
return 0
}
# --- Core logic ---
local result
result="processed: $BUFFER"
# Update buffer
BUFFER="$result"
CURSOR=$#BUFFER
# Optional: show status message
zle -M "β Widget completed"
# Optional: telemetry
(( $+functions[zrun] )) && zrun --quiet "my_widget" "${#BUFFER} chars"
}
# Register with ZLE
zle -N my_widget
π Key PatternsΒΆ
π‘οΈ Guard ClausesΒΆ
Always check prerequisites before doing work:
my_widget() {
# Check for required commands
(( $+commands[fzf] )) || { zle -M "β οΈ fzf required"; return 1 }
# Check buffer state
[[ -z "$BUFFER" ]] && { zle -M "β οΈ Nothing to process"; return 0 }
# Check we're in a git repo (if needed)
git rev-parse --is-inside-work-tree &>/dev/null || {
zle -M "β οΈ Not in a git repository"
return 1
}
}
π Buffer ManipulationΒΆ
# Replace entire buffer
BUFFER="new content"
CURSOR=$#BUFFER
# Append to buffer
BUFFER+=" && extra_command"
CURSOR=$#BUFFER
# Insert at cursor position
LBUFFER+="inserted text"
# Clear and execute
BUFFER="cd /some/path"
zle accept-line
π¬ Status MessagesΒΆ
# Show a temporary message below the prompt
zle -M "β Done (42 chars copied)"
# Show colored message (requires print -P)
print -P "%F{green}β Success%f" >&2
zle reset-prompt
π Clipboard IntegrationΒΆ
# Copy to clipboard (use shared function if available)
if (( $+functions[clipboard_copy] )); then
echo -n "$content" | clipboard_copy
elif (( $+commands[wl-copy] )); then
echo -n "$content" | wl-copy 2>/dev/null
elif (( $+commands[xclip] )); then
echo -n "$content" | xclip -selection clipboard
fi
# Paste from clipboard
local content
if (( $+commands[wl-paste] )); then
content=$(wl-paste --no-newline 2>/dev/null)
fi
π FZF IntegrationΒΆ
Many widgets use FZF for interactive selection:
my_picker() {
local selection
selection=$(
some_command |
fzf --height=40% \
--prompt="Pick: " \
--preview='echo {}' \
--preview-window=right:50%
)
[[ -z "$selection" ]] && return 0
LBUFFER+="$selection "
zle reset-prompt
}
zle -N my_picker
π Registration & KeybindingsΒΆ
π Where Widgets Are RegisteredΒΆ
| Method | File | Scope |
|---|---|---|
| Framework default | core/16_widgets.zsh | All users |
| User TOML config | config.toml [keybindings] | Per-user |
| User script | rc.d/ or post.d/ | Per-user |
β¨οΈ Keybinding SyntaxΒΆ
# Ctrl combinations
bindkey '^H' my_widget # Ctrl+H
bindkey '^B' my_widget # Ctrl+B
bindkey '^X' my_widget # Ctrl+X
# Alt combinations
bindkey '\eH' my_widget # Alt+H
bindkey '\ed' my_widget # Alt+D
# Escape sequences
bindkey '\e\e' my_widget # Esc Esc
# Function keys
bindkey '^[[15~' my_widget # F5
βοΈ TOML KeybindingsΒΆ
In ~/.config/zshand/config.toml:
Parsed by startup/17_az_bindkey_from_toml.zsh.
π Keybinding ReferenceΒΆ
Current framework bindings:
| Key | Widget | Category |
|---|---|---|
Ctrl+P | pastem | π Clipboard |
Alt+P | pasteraw | π Clipboard |
Alt+H | cliph | π Clipboard |
Ctrl+O | bufcopy | π Clipboard |
Ctrl+G | cplast | π Clipboard |
Ctrl+E | aidebug | π€ AI |
Ctrl+B | aicmit | π€ AI |
Ctrl+K | qlog | π€ AI |
Ctrl+X | bufedit | βοΈ Editing |
Ctrl+J | jtog | βοΈ Editing |
Ctrl+A | aliexp | βοΈ Editing |
Alt+A | aliasf | βοΈ Editing |
Esc Esc | sudot | βοΈ Editing |
Alt+D | pjump | π Navigation |
Alt+B | bpick | π Navigation |
Alt+F | fpick | π Navigation |
Alt+Enter | ctxpick | π Navigation |
Alt+? | whelp | π Navigation |
π§ͺ Testing WidgetsΒΆ
Widget testing is challenging because ZLE is interactive. Strategies:
π Unit-Test the LogicΒΆ
Extract core logic into a regular function, test that:
# In widget file:
_my_widget_process() {
local input="$1"
# ... processing logic ...
echo "$result"
}
my_widget() {
local result=$(_my_widget_process "$BUFFER")
BUFFER="$result"
}
# In test file:
test_my_widget_process() {
source "$PROJECT_ROOT/widgets/my_widget.zsh"
local result=$(_my_widget_process "test input")
assert_equals "expected output" "$result"
}
π§ Test Guard ClausesΒΆ
test_my_widget_requires_fzf() {
# Temporarily remove fzf from PATH
local old_path="$PATH"
PATH="/usr/bin"
# Widget should fail gracefully
my_widget 2>/dev/null
assert_equals 1 $?
PATH="$old_path"
}
β Widget Test ChecklistΒΆ
- β Empty buffer β handles gracefully
- β Missing deps β shows helpful message, doesn't crash
- β Long buffer β handles large input
- β Special characters β quotes, newlines, unicode
- β No git repo β (if git-dependent) fails gracefully
- β Clipboard empty β (if clipboard-dependent) handles
- β Telemetry β zrun called with correct event name
π Related DocumentsΒΆ
| Document | Purpose |
|---|---|
| π‘ API / Source | Per-widget generated docs (source/widgets/) |
| βοΈ Core | 16_widgets.zsh and keybinding from TOML |
| π Header Standard | TRIGGER section format for widgets |
| π€ Config | Custom widgets and keybindings.toml |
| π Documentation Index | Full doc nav |