Mastering ZSH: Part 2 - Line Editor and Custom Widgets
Learn ZSH Line Editor (ZLE) to create custom keybindings and widgets. Understand BUFFER, LBUFFER, RBUFFER, and how fzf's Ctrl+R and Ctrl+T actually work under the hood. Includes practical examples you can use immediately.
- tags
- #Zsh #Zle #Fzf #Keybindings #Shell-Scripting #Command-Line #Productivity #Terminal #Widgets #Automation
- categories
- Tutorials Shell-Scripting
- published
- reading time
- 12 minutes
📚 Series: Mastering Zsh
- Mastering ZSH: Part 1 - Hooks and Automation
- Mastering ZSH: Part 2 - Line Editor and Custom Widgets (current)
- Mastering ZSH: Part 3 - Understanding Your Prompt: How Powerlevel10k Actually Works
- Mastering ZSH: Part 4 - Completion System Demystified
You press Ctrl+R and get fuzzy history search. Press Ctrl+T and files appear in a searchable list. Press Ctrl+G and your current git branch inserts at the cursor.
The first two are fzf. The last one you can build yourself in 5 lines of ZSH.
Here’s how ZLE (Zsh Line Editor) works and how to create custom keybindings that manipulate your command line.
What is ZLE?
ZLE is ZSH’s built-in line editor–the system that handles everything between pressing a key and executing a command. It manages:
- The command buffer (what you’ve typed)
- Cursor position (where you are in the line)
- Keybindings (what each keystroke does)
- Editing operations (insert, delete, move cursor, etc.)
Every keystroke triggers a widget–a function that manipulates the buffer. You can create your own widgets and bind them to any key.
The Three Core Variables
ZLE exposes the command line as three variables:
| |
Modify these variables in a widget, and the command line updates instantly.
Creating Your First Widget
Let’s build a widget that inserts the current git branch:
| |
Now press Ctrl+G anywhere on the command line, and your branch name appears at the cursor.
How it works:
git branch --show-currentgets the branch nameLBUFFER+="$branch"appends to the left buffer (inserts at cursor)- ZLE redraws the line automatically
Practical Widgets You Can Use
Insert Current Directory Basename
| |
Type cd then press Alt+D to insert the current directory name.
Insert Last Command’s Last Argument
| |
This mimics Bash’s Alt+. for “insert last argument from previous command.”
Clear Line to Kill Ring (Safe Clear)
| |
Clears the line but saves it to kill ring (paste with Ctrl+Y).
Quote Current Word
| |
Type a word, press Alt+Q, and it gets wrapped in quotes.
Understanding BUFFER Manipulation
Inserting Text
| |
Moving the Cursor
| |
Getting Word Under Cursor
| |
How fzf Integration Actually Works
When you press Ctrl+R with fzf, here’s what happens:
| |
Key parts:
fc -rl 1- Get history (reverse chronological)fzf- Pipe to interactive fuzzy finderBUFFER="$cmd"- Replace command line with selectionzle reset-prompt- Force redraw
fzf File Widget (Ctrl+T)
| |
The magic: fzf runs in a subprocess, returns the result, and ZLE updates the buffer. No plugin complexity–just pipes and variable manipulation.
Building a Simple Fuzzy Finder (No fzf)
You can build basic fuzzy selection with pure ZLE:
| |
This isn’t fuzzy search (use real fzf for that), but shows how widgets can spawn interactive selection and insert results.
Advanced: Multi-Line Editing
ZLE can handle multi-line commands:
| |
Pressing Alt+T inserts a complete for-loop template.
Working with the Kill Ring
ZLE has a kill ring (clipboard history):
| |
CUTBUFFER- Currently killed textkillring- Array of previous kills (less commonly used)
Calling Other Widgets
You can chain widgets:
| |
This wraps the default “accept line” behavior with preprocessing.
Redrawing and Prompts
After modifying the buffer, you may need:
| |
Use reset-prompt after any widget that outputs text (echo, print).
Real-World Example: Smart Path Completion
Insert relative path to a file by fuzzy matching:
| |
Type cat then Ctrl+P to fuzzy-find and insert a file path.
Debugging Widgets
Test a Widget Without Binding
| |
Show Widget Info
| |
Trace Widget Execution
| |
Common Pitfalls
1. Forgetting to Redraw
| |
Fix: Always zle reset-prompt after echo/print.
2. Not Handling Empty Input
| |
Fix: Check for empty strings or errors.
3. Breaking Multi-Line Commands
| |
Fix: Be careful replacing $BUFFER when user has multi-line input.
Performance Considerations
Widgets should be fast (<100ms):
| |
Slow widgets make your shell feel broken. Cache data or use background jobs.
Beyond fzf: What Else You Can Build
1. Snippet Expansion
| |
Type gcm then Alt+E → expands to git commit -m "".
2. Smart Parenthesis Matching
| |
Type ( and it inserts () with cursor in the middle.
3. Capitalize Current Word
| |
4. Toggle Sudo Prefix
| |
Press Alt+S to add/remove sudo from the current command.
How fzf Key Bindings Work
fzf’s key-bindings.zsh file creates widgets that:
- Spawn fzf in a subprocess with input (history, files, directories)
- Capture the selection from fzf’s stdout
- Modify BUFFER with the result
- Redraw the prompt with
zle reset-prompt
Here’s a simplified version of fzf’s Ctrl+T (file finder):
| |
Key insight: fzf isn’t magic. It’s just a TUI that reads stdin and writes stdout. The ZLE widget handles the integration.
Understanding ZLE Modes
ZLE has different keymaps (like Vim modes):
- emacs (default) - Emacs-style bindings
- viins - Vi insert mode
- vicmd - Vi command mode
Set your mode:
| |
Check current keymap:
| |
Bind keys for specific modes:
| |
Common Keybinding Syntax
ZSH keybinding syntax can be confusing:
| |
Find what a key sends:
| |
Or use:
| |
Advanced: Widgets with Arguments
Widgets can accept numeric arguments (Alt+5 before a command):
| |
Press Alt+10 then Ctrl+X to insert “xxxxxxxxxx”.
Real-World Integration: Directory Jumping
Build a simple directory jumper:
| |
Press Ctrl+J, select a project, and you’re there.
Widgets That Execute Commands
You can make widgets execute commands instead of just inserting text:
| |
Shows git status without executing a command. Press Alt+G from anywhere.
Combining with ZSH Hooks
Widgets and hooks complement each other:
| |
Hooks maintain state, widgets use that state to manipulate the command line.
Debugging and Development
Test Widget Without Keybinding
| |
Show All Bound Keys
| |
Temporarily Unbind
| |
When to Use Widgets vs Aliases
Use aliases for:
- Simple command substitutions (
alias ll='ls -la') - Fixed command patterns
Use widgets for:
- Context-aware insertion (current dir, git branch)
- Interactive selection (fuzzy finders)
- Buffer manipulation (quoting, expanding)
- Cursor-position-dependent behavior
Aliases run as commands. Widgets manipulate the command line before execution.
Summary
ZLE widgets let you create custom keybindings that manipulate your command line:
- Core variables:
BUFFER,LBUFFER,RBUFFER,CURSOR - Create widgets:
zle -N widget_name - Bind keys:
bindkey '^G' widget_name - Redraw:
zle reset-promptafter output
fzf works by creating widgets that spawn interactive TUIs and capture their output. You can build similar functionality with pure ZLE or integrate any CLI tool that reads stdin and writes stdout.
Start with simple widgets (insert git branch, insert directory name) and build up to complex interactive selection.
For more ZSH automation patterns, see my guide on ZSH hooks .
Further Reading:
- ZSH Manual: Zsh Line Editor
- fzf key-bindings.zsh - See how fzf integration actually works
- ZSH Hooks Guide - Complement widgets with hooks
Shell: ZSH 5.0+
📚 Series: Mastering Zsh
- Mastering ZSH: Part 1 - Hooks and Automation
- Mastering ZSH: Part 2 - Line Editor and Custom Widgets (current)
- Mastering ZSH: Part 3 - Understanding Your Prompt: How Powerlevel10k Actually Works
- Mastering ZSH: Part 4 - Completion System Demystified