Extensibility Guide

January 18, 2026 ยท View on GitHub

Step-by-step guide for writing custom previewers and actions.

How It Works

Place your config file anywhere. Point zledit to it via zstyle:

zstyle ':zledit:' config /path/to/config.toml

The plugin loads your config at startup and registers previewers and actions in the order they appear. User-defined bindings take precedence over built-in defaults.

Quick Start

  1. Create a TOML config file:
[[actions]]
binding = 'ctrl-u'
description = 'upper'
script = '/path/to/your/uppercase.sh'
  1. Write your script (put it wherever you like):
#!/usr/bin/env bash
set -eo pipefail

TOKEN="\$1"
INDEX="\$2"

[[ -z "$TOKEN" || -z "$INDEX" || -z "$ZJ_BUFFER" || -z "$ZJ_POSITIONS" ]] && exit 1

IFS=$'\n' read -r -d '' -a positions <<< "$ZJ_POSITIONS" || true
pos="${positions[$((INDEX - 1))]}"
[[ -z "$pos" ]] && exit 1

upper=$(echo "$TOKEN" | tr '[:lower:]' '[:upper:]')
end_pos=$((pos + ${#TOKEN}))

echo "${ZJ_BUFFER:0:$pos}${upper}${ZJ_BUFFER:$end_pos}"
  1. Make it executable:
chmod +x /path/to/your/uppercase.sh
  1. Reload shell and test with Ctrl+X / then Ctrl+U on a token.

Writing Actions

Actions manipulate the command line buffer. They receive the selected token and must output the modified buffer.

Input

Your script receives:

SourceContent
$1Selected token text
$2Token index (1-based)
ZJ_BUFFERCurrent command line
ZJ_POSITIONSNewline-separated start positions
ZJ_WORDSNewline-separated tokens
ZJ_CURSORCurrent cursor position
ZJ_PICKERActive picker (fzf, fzf-tmux, sk)

Output

Scripts communicate back to zledit via two channels:

  • stdout - the new buffer content
  • fd 3 - metadata (mode, cursor position, etc.)

Write key:value pairs to file descriptor 3:

#!/usr/bin/env bash
TOKEN="\$1"
upper=$(echo "$TOKEN" | tr '[:lower:]' '[:upper:]')

# Output new buffer to stdout
echo "${ZJ_BUFFER//$TOKEN/$upper}"

# Send metadata to fd 3
echo "mode:replace" >&3
echo "cursor:10" >&3

Available metadata keys:

KeyValueDescription
modereplaceApply stdout as new buffer (default)
displayPrint stdout to terminal, don't change buffer
pushlineSave buffer, show pushline: command for user to execute
pushline-execSave buffer, execute pushline: command immediately
errorShow message: as error, abort
cursorNSet cursor to position N
pushlinecmdCommand to show/execute (for pushline modes)
messagetextError message (for error mode)

Example: Variable extraction with fd 3

#!/usr/bin/env bash
TOKEN="\$1"
var_name=$(echo "$TOKEN" | tr '[:lower:]-' '[:upper:]_')

# New buffer with variable reference
echo "${ZJ_BUFFER//$TOKEN/\"\$${var_name}\"}"

# Metadata
echo "mode:pushline" >&3
echo "pushline:${var_name}=\"${TOKEN}\"" >&3
echo "cursor:0" >&3

Exit Codes (Legacy)

For backwards compatibility, exit codes still work when fd 3 is empty:

ExitBehavior
0Apply stdout as new buffer
1Error - show stderr message
2Display mode - print stdout, no buffer change
3Push-line (format: buffer\n---ZJ_PUSHLINE---\ncommand)
4Push-line + auto-execute

Legacy cursor override (last line of stdout):

echo "new buffer"
echo "CURSOR:5"

Nested Pickers

If your action needs a secondary picker (like wrap showing wrapper options), use ZJ_PICKER:

if [[ "$ZJ_PICKER" == "fzf-tmux" ]]; then
    selected=$(echo "$options" | fzf-tmux --reverse --prompt="pick> ")
else
    selected=$(echo "$options" | fzf --height=15 --reverse --prompt="pick> ")
fi

Example: Lowercase Action

#!/usr/bin/env bash
set -eo pipefail

TOKEN="\$1"
INDEX="\$2"

[[ -z "$TOKEN" || -z "$INDEX" || -z "$ZJ_BUFFER" || -z "$ZJ_POSITIONS" ]] && exit 1

# Parse positions
IFS=$'\n' read -r -d '' -a positions <<< "$ZJ_POSITIONS" || true
pos="${positions[$((INDEX - 1))]}"
[[ -z "$pos" ]] && exit 1

# Transform
lower=$(echo "$TOKEN" | tr '[:upper:]' '[:lower:]')

# Replace in buffer
end_pos=$((pos + ${#TOKEN}))
echo "${ZJ_BUFFER:0:$pos}${lower}${ZJ_BUFFER:$end_pos}"

Example: Delete Token

#!/usr/bin/env bash
set -eo pipefail

TOKEN="\$1"
INDEX="\$2"

[[ -z "$TOKEN" || -z "$INDEX" || -z "$ZJ_BUFFER" || -z "$ZJ_POSITIONS" ]] && exit 1

IFS=$'\n' read -r -d '' -a positions <<< "$ZJ_POSITIONS" || true
pos="${positions[$((INDEX - 1))]}"
[[ -z "$pos" ]] && exit 1

end_pos=$((pos + ${#TOKEN}))
echo "${ZJ_BUFFER:0:$pos}${ZJ_BUFFER:$end_pos}"

Writing Previewers

Previewers show context in fzf's preview window. They match tokens by regex pattern.

Input

SourceContent
$1Token to preview (with index prefix stripped)

Output

Write preview content to stdout. No special formatting needed.

Example: URL Preview

#!/usr/bin/env bash
TOKEN="\$1"

if command -v curl &>/dev/null; then
    title=$(curl -sL --max-time 3 "$TOKEN" 2>/dev/null | \
            grep -oP '(?<=<title>).*(?=</title>)' | head -1)
    echo "URL: $TOKEN"
    echo "Title: ${title:-Unable to fetch}"
else
    echo "URL: $TOKEN"
    echo "(curl not available)"
fi

Example: JSON Preview

#!/usr/bin/env bash
TOKEN="\$1"

if [[ -f "$TOKEN" ]] && command -v jq &>/dev/null; then
    jq -C '.' "$TOKEN" 2>/dev/null || cat "$TOKEN"
elif [[ -f "$TOKEN" ]]; then
    cat "$TOKEN"
else
    echo "File not found: $TOKEN"
fi

Config

[[previewers]]
pattern = '^https?://'
description = 'URL preview'
script = '/path/to/url-preview.sh'

[[previewers]]
pattern = '\.(json)$'
description = 'JSON files'
script = '/path/to/json-preview.sh'

Patterns are matched in order. First match wins.

Config File Reference

# config.toml

# Previewers - match tokens by pattern
[[previewers]]
pattern = '^https?://'           # regex pattern
description = 'URL preview'      # shown in debug
script = '/path/to/script.sh'    # must be executable

# Actions - triggered by fzf keybinding
[[actions]]
binding = 'ctrl-u'               # fzf key syntax
description = 'upper'            # shown in header
script = '/path/to/script.sh'    # must be executable

Paths starting with ~ expand to $HOME.

Overriding Built-ins

User actions override built-in defaults when bindings collide. To replace Ctrl+S (wrap):

[[actions]]
binding = 'ctrl-s'
description = 'my-wrap'
script = '/path/to/my-wrap.sh'

Built-in defaults load after user config, skipping any binding already registered.

Debugging

Enable debug mode:

zstyle ':zledit:' debug on

Logs go to /tmp/zledit-debug.log.

Test scripts manually:

export ZJ_BUFFER="echo hello world"
export ZJ_POSITIONS=$'0\n5\n11'
./scripts/uppercase.sh "hello" "2"

Reference Examples

See examples/ in the repository:

  • examples/actions/uppercase.sh - case conversion
  • examples/actions/lowercase.sh - case conversion
  • examples/previewers/url-preview.sh - fetch URL title
  • examples/config.toml - sample configuration

Technical Reference

For the complete interface specification, exit codes, and protocol details, see design.md.