The native line editor that makes Lush different
- Why LLE Exists
- Editing Modes
- Emacs Mode Reference
- Vi Mode
- Completion System
- Syntax Highlighting
- History Integration
- Multi-Line Editing
- Kill Ring
- Customization
- Troubleshooting
Every major shell wraps GNU Readline or implements something similar. Bash uses Readline. Zsh has ZLE. Fish has its own editor. They all follow the same pattern: a text input library adapted for shell use.
Lush took a different approach. LLE (Lush Line Editor) is a line editor designed from the ground up for interactive shell use. This isn't academic distinction - it enables capabilities that bolted-on solutions can't provide:
-
Deep completion integration: LLE's completion system understands shell syntax at a fundamental level. It doesn't just complete filenames -- it knows what arguments each shell builtin accepts and presents context-appropriate options. It also switches its first-word vocabulary to the debugger command set at the
(lush-debug)break prompt. -
Real-time syntax highlighting: Every character you type is tokenized and colored. Commands are validated as you type. You see errors before pressing Enter.
-
Unified editing experience: Multi-line editing, history navigation, and completion all work together without the seams you feel in other shells.
-
Foundation for the future: Features like the integrated debugger, hook system, and shell modes all build on LLE's architecture.
LLE is not readline. It doesn't load .inputrc. It doesn't try to be compatible with readline's API. It's a purpose-built editor for a purpose-built shell.
LLE supports two editing paradigms:
Emacs-style editing is the default and provides complete functionality. The full keybinding-action set is implemented and production-ready.
Vi mode has the framework in place -- mode switching, state machine, command parsing -- but is not yet wired to editing operations. Track progress in lle_specification/ and the issue tracker. Use Emacs mode in the meantime.
To check the current mode:
display lle diagnosticsLLE implements the full set of GNU Readline-compatible keybinding actions.
| Keybinding | Action | Description |
|---|---|---|
Ctrl-A |
Beginning of line | Move to start of current line |
Ctrl-E |
End of line | Move to end of current line |
Ctrl-F |
Forward char | Move forward one character |
Ctrl-B |
Backward char | Move backward one character |
Alt-F |
Forward word | Move forward one word |
Alt-B |
Backward word | Move backward one word |
Ctrl-P / Up |
Previous history | Previous history entry (or previous line in multi-line) |
Ctrl-N / Down |
Next history | Next history entry (or next line in multi-line) |
Alt-< |
Beginning of history | Go to first history entry |
Alt-> |
End of history | Go to last history entry / current input |
| Keybinding | Action | Description |
|---|---|---|
Ctrl-D |
Delete char | Delete character at cursor (EOF on empty line) |
Backspace |
Backward delete | Delete character before cursor |
Ctrl-K |
Kill line | Kill from cursor to end of line |
Ctrl-U |
Backward kill line | Kill from beginning of line to cursor |
Alt-D |
Kill word | Kill from cursor to end of word |
Ctrl-W |
Backward kill word | Kill from beginning of word to cursor |
Alt-Backspace |
Backward kill word | Same as Ctrl-W |
| Keybinding | Action | Description |
|---|---|---|
Ctrl-Y |
Yank | Paste most recent kill |
Alt-Y |
Yank pop | After Ctrl-Y, cycle through kill ring |
| Keybinding | Action | Description |
|---|---|---|
Ctrl-T |
Transpose chars | Swap character at cursor with previous |
Alt-T |
Transpose words | Swap word at cursor with previous word |
Alt-U |
Upcase word | Uppercase from cursor to end of word |
Alt-L |
Downcase word | Lowercase from cursor to end of word |
Alt-C |
Capitalize word | Capitalize first char, lowercase rest |
| Keybinding | Action | Description |
|---|---|---|
Tab |
Complete | Trigger completion |
Alt-? |
Possible completions | List all possible completions |
Alt-* |
Insert completions | Insert all possible completions |
| Keybinding | Action | Description |
|---|---|---|
Ctrl-R |
Reverse search | Incremental reverse history search |
Ctrl-S |
Forward search | Incremental forward history search |
Ctrl-G |
Abort | Cancel search, return to original line |
| Keybinding | Action | Description |
|---|---|---|
Ctrl-_ |
Undo | Undo last editing action |
Ctrl-X Ctrl-U |
Undo | Same as Ctrl-_ |
| Keybinding | Action | Description |
|---|---|---|
Ctrl-L |
Clear screen | Clear terminal, redraw prompt |
Ctrl-C |
Interrupt | Send SIGINT, cancel current input |
Ctrl-Z |
Suspend | Send SIGTSTP, suspend shell |
Ctrl-D |
EOF | On empty line: exit shell |
Enter |
Accept line | Execute the current line |
| Keybinding | Action | Description |
|---|---|---|
Ctrl-Q |
Quoted insert | Insert next character literally |
Ctrl-V |
Quoted insert | Insert next character literally |
Alt-# |
Insert comment | Comment out line, add to history |
Ctrl-X Ctrl-E |
Edit and execute | Open in $EDITOR, execute on save |
Vi mode is in active development. The framework includes:
- Mode state machine (normal, insert, command-line)
- Motion commands parser
- Operator-pending mode
- Count prefixes
Target: Full vi mode in the v1.4.0 release.
When complete, vi mode will support:
Normal Mode
h j k l- Movementw b e- Word movement0 $ ^- Line movementx X- Delete characterd- Delete operatorc- Change operatory- Yank operatorp P- Pastei a I A- Enter insert modeu Ctrl-R- Undo/redo/?- Searchn N- Search repeat
Insert Mode
- All text input
Escape- Return to normal modeCtrl-[- Return to normal mode
LLE's completion system is context-aware. It understands what you're typing and provides appropriate completions.
| Type | Description | Example |
|---|---|---|
| Command | Executables in $PATH | gi<Tab> -> git |
| Builtin | Shell builtins | ec<Tab> -> echo |
| File | Files and directories | /etc/pa<Tab> -> /etc/passwd |
| Variable | Shell variables | $HO<Tab> -> $HOME |
| Option | Command options | ls --co<Tab> -> ls --color |
Every completable shell builtin has specific completion logic:
# set builtin knows its options
set -o <Tab> # Shows: errexit nounset xtrace ...
# config builtin knows its structure
config set <Tab> # Shows: shell completion display ...
config set shell.<Tab> # Shows: errexit nounset pipefail ...
# debug builtin knows its subcommands
debug <Tab> # Shows: on off vars print trace profile analyze ...
# display builtin knows its arguments (including LLE Phase 3 trio)
display <Tab> # Shows: lle features themes status stats widget hook segment
display lle <Tab> # Shows: diagnostics status info widget hook segmentAt the (lush-debug) break prompt, LLE's first-word completion
switches to the debugger command vocabulary (step, next, out,
continue, print, vars, stack, type, t, watch,
break, quit, ...). The shell-side first-word sources sit out
via a lle_in_debug_prompt() gate; variable completion ($x) and
file completion remain available for argument positions.
When multiple completions are available, a menu appears:
| Key | Action |
|---|---|
Up / Ctrl-P |
Move up in menu |
Down / Ctrl-N |
Move down in menu |
Left / Ctrl-B |
Move left (multi-column) |
Right / Ctrl-F |
Move right (multi-column) |
Tab |
Accept selection |
Enter |
Accept and execute |
Escape |
Cancel completion |
When an external command shadows a builtin (like /usr/bin/echo vs the echo builtin), selecting the external command inserts its full path so you explicitly use the external version.
LLE tokenizes your input in real-time and applies colors based on token types.
LLE recognizes a broad token vocabulary covering every construct the shell can parse:
Commands
- Valid external command (green)
- Invalid command (red)
- Shell builtin (cyan)
- Alias (cyan)
- Function (blue)
Strings
- Single-quoted string
- Double-quoted string
- ANSI-C quoted string (
$'...') - Backtick command substitution
Variables
- Variable reference (
$var,${var}) - Special variables (
$?,$#,$@,$$,$!)
Paths
- Valid file/directory path (underlined green)
- Invalid path (underlined red)
Operators
- Pipe (
|) - Redirections (
>,<,>>,2>,&>) - Background (
&) - Command separator (
;) - Logical AND/OR (
&&,||) - Subshell parentheses
- Brace groups
Other
- Keywords (
if,then,else,fi,for,while,do,done,case,esac) - Comments (
# ...) - Options (
-v,--verbose) - Glob patterns (
*,?,[...]) - Here-documents
- Process substitution
- Arithmetic expansion
- Errors
LLE checks whether commands exist as you type:
- Commands that exist appear in green
- Commands that don't exist appear in red
- Builtins appear in cyan
- Aliases and functions have their own colors
This validation uses a cache for performance - commands are only checked once per session unless the cache is cleared.
Syntax highlighting colors come from the current theme:
# List available themes
theme list
# Switch themes
theme set dark
# Colors update immediatelyLLE integrates with Lush's history system.
| Key | Action |
|---|---|
Up / Ctrl-P |
Previous history entry |
Down / Ctrl-N |
Next history entry |
Alt-< |
First history entry |
Alt-> |
Last entry / current line |
| Key | Action |
|---|---|
Ctrl-R |
Start reverse search |
Ctrl-S |
Start forward search |
| Type text | Narrow search |
Ctrl-R again |
Find previous match |
Ctrl-S again |
Find next match |
Enter |
Accept match |
Ctrl-G |
Cancel, restore original |
Standard history expansion works.
Event designators select a prior command:
!! # Last command
!n # Command number n
!-n # n commands ago
!string # Most recent command starting with string
!?string? # Most recent command containing string (closing ? optional)
^old^new # Repeat the last command, replacing old with newWord designators select words from the referenced command, written after a
: separator (the !$, !^, !* forms are shorthands for the last
command):
!$ # Last argument of the last command
!^ # First argument of the last command
!* # All arguments of the last command
!!:0 # The command word of the last command
!!:2 # Word 2 of the last command
!!:1-2 # Words 1 through 2 of the last commandModifiers transform the result:
!!:p # Print the expansion without executing it
!!:s/old/new/ # Substitute the first occurrence of old with new
!!:gs/old/new/ # Substitute every occurrenceA leading space disables expansion for that line, and a failed reference
(!nope) reports event not found and runs nothing. With setopt histverify
(shopt -s histverify), an expansion is loaded into the editor for review
instead of being executed immediately.
Multi-line commands are stored as single history entries. Navigating to a multi-line command displays all lines.
LLE can deduplicate adjacent identical history entries:
config set history.dedup_navigation trueWith this enabled, pressing Up will skip repeated commands.
LLE handles multi-line input naturally.
When a line is syntactically incomplete, LLE provides a continuation prompt:
$ for i in 1 2 3; do
> echo "Number: $i"
> done
Number: 1
Number: 2
Number: 3Incomplete constructs that trigger continuation:
- Unclosed quotes
- Unclosed parentheses/braces
if/then/else/fiblocksfor/while/do/doneloopscase/esacstatements- Backslash at end of line
| Key | Action |
|---|---|
Up |
Move to previous line (not history) |
Down |
Move to next line (not history) |
Ctrl-A |
Start of current line |
Ctrl-E |
End of current line |
Alt-< |
Start of buffer |
Alt-> |
End of buffer |
LLE maintains column position when moving between lines (sticky column).
To insert a newline without executing:
- Backslash at end of line:
\<Enter> - Within quotes, just press Enter
LLE maintains a kill ring for cut/paste operations.
These operations add to the kill ring:
Ctrl-K- Kill to end of lineCtrl-U- Kill to beginning of lineAlt-D- Kill word forwardCtrl-W/Alt-Backspace- Kill word backward
| Key | Action |
|---|---|
Ctrl-Y |
Yank (paste) most recent kill |
Alt-Y |
After Ctrl-Y, cycle through older kills |
- Consecutive kill commands append to the same entry
- The ring holds multiple entries (typically 10)
Alt-Yonly works immediately afterCtrl-Y
LLE exposes three first-class customization surfaces -- widgets, hooks, and segments -- all routed through the central configuration registry. They are the LLE Phase 3 customization trio.
A widget is a named shell function bound to a key sequence so it
behaves as a one-shot editing operation. Equivalent to zle -N in
zsh, but available in every mode.
# Define a widget body
my-uppercase-word() {
# Widget callbacks operate on the active LLE buffer
lle buffer transform "${WIDGET_WORD}" upper
}
# Register and bind
display lle widget add my-uppercase-word my-uppercase-word
display lle widget bind '\eU' my-uppercase-word
# Inspect
display lle widget list
display lle widget show my-uppercase-word
display lle widget remove my-uppercase-wordUser widgets sit alongside builtin widgets in the same dispatcher;
they can be bound from ~/.config/lush/lushrc or a keybindings.toml
exactly like built-ins.
precmd, preexec, chpwd, and periodic shell functions are the
canonical hook surface (see HOOKS_AND_PLUGINS.md).
For symmetric configuration alongside widgets and segments, the same
hooks can be registered with display lle hook:
display lle hook add precmd my_precmd_widget
display lle hook list
display lle hook remove precmd my_precmd_widgetThe full hook-type catalog covers ten LLE-internal events; the shell-event hub bridges to PRE_COMMAND / POST_COMMAND at editor init.
A segment is a prompt fragment whose visibility and contents are driven by a shell variable. Useful for context indicators (active kubernetes context, current AWS profile, virtualenv name) that update on every prompt redraw without re-parsing PS1.
# A precmd hook keeps the variable current
precmd() {
KUBE_CTX="${KUBECONFIG:+k8s:$(basename "$KUBECONFIG")}"
}
# Register a segment backed by that variable
display lle segment add kube_ctx KUBE_CTX
display lle segment list
display lle segment show kube_ctx
display lle segment remove kube_ctxThe segment renders whenever $KUBE_CTX is non-empty; visibility
flips automatically without conditional logic in the prompt
template.
LLE keybindings can be configured through the config system:
# View current bindings
config show keybindings
# Modify a binding (example syntax)
config set keybindings.ctrl_a beginning_of_line# Enable/disable completion
config set completion.enabled true
# Case sensitivity
config set completion.case_sensitive false
# Menu display
config set completion.menu_complete true# Enable/disable
config set display.syntax_highlighting true
# Command validation
config set display.validate_commands true
# Path validation
config set display.validate_paths true# History size
config set history.size 10000
# Deduplication
config set history.dedup_navigation true
# Ignore patterns
config set history.ignore_patterns "^\\s*$"Problem: Colors don't appear
# Check terminal color support
echo $TERM
tput colors
# Ensure color is enabled
config set display.syntax_highlighting trueProblem: Characters display incorrectly
# Check locale
echo $LANG $LC_ALL
# LLE requires UTF-8
export LANG=en_US.UTF-8Problem: Width calculation wrong
# Force terminal size refresh
# Resize terminal window, or:
kill -WINCH $$Problem: No completions appear
# Check completion is enabled
config get completion.enabled
# Check for errors
display lle diagnosticsProblem: Wrong completions
# Clear completion cache
# (Rebuilds command cache)Problem: History not saving
# Check history file permissions
ls -la ~/.lush_history
# Check history size
config get history.sizeProblem: Slow response
# Check display stats
display stats
# Disable expensive features temporarily
config set display.validate_paths false
config set display.validate_commands false# Full LLE status
display lle diagnostics
# Performance statistics
display stats
# Feature status
display featuresLLE is composed of several subsystems:
| Subsystem | Responsibility |
|---|---|
| Buffer | Text storage, cursor management |
| Input | Key parsing, sequence detection |
| Keybinding | Action dispatch |
| Completion | Context analysis, candidate generation |
| History | Storage, navigation, search |
| Display | Rendering, syntax highlighting |
| Terminal | ANSI output, capability detection |
LLE fully supports Unicode:
- UTF-8 input and output
- Grapheme cluster handling (emoji, combining characters)
- Correct cursor positioning for wide characters
- Unicode-aware word boundaries
LLE uses:
- Arena allocators for temporary data
- Memory pools for frequent allocations
- Reference counting for shared data
Zero memory leaks on normal operation (verified with macOS leaks tool).
LLE is not thread-safe. All operations must occur on the main thread. The async worker (for completion) uses careful synchronization.
- USER_GUIDE.md - Complete shell reference
- COMPLETION_SYSTEM.md - Detailed completion documentation
- CONFIGURATION.md - Configuration reference