Skip to content

⌨️ 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ΒΆ

Keypress  β†’  ZLE dispatches to widget function  β†’  Widget modifies buffer/state  β†’  ZLE redraws

πŸ“Š 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:

[keybindings]
"ctrl+h" = "my_widget"
"alt+d"  = "pjump"
"ctrl+b" = "aicmit"

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

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