>_EXECUTIVE SUMMARY
zsh-sed-sub adds one ZLE widget — basicSedSub — bound to Ctrl-F Ctrl-P across all three default zsh keymaps (viins / vicmd / emacs). When invoked, the widget reads a before>after sed expression a single keystroke at a time via read -k, paints the prompt-line buffer in ANSI cyan/magenta to indicate edit state, and on Enter rewrites the entire $BUFFER via sed -E "s@before@after@g". Backspace deletes one char, Ctrl-U clears the input, ESC aborts. The widget is the canonical "fix the typo in this long command line" tool — the same intent as Emacs query-replace, but at the zsh command-line layer rather than the editor layer. 74 LOC of autoload code, 12 LOC of entrypoint, 44 zunit @test cases across 6 files pinning the keybinding, widget registration, the auto-up trigger on empty BUFFER, and the entire interactive flow.
Plugin Source — 86 LOC
All real logic lives in autoload/basicSedSub (74 LOC). The entrypoint (12 LOC) only configures fpath, autoloads the widget, calls zle -N basicSedSub to register it, and emits three identical bindkey -M lines for viins, vicmd, and emacs — the user's current keymap doesn't matter.
#FILE STRUCTURE
| File | Lines | Role |
|---|---|---|
| autoload/basicSedSub | 74 | ZLE widget: empty-BUFFER guard, read-loop with per-keystroke colour state machine, ESC/^?/^h/^U handling, >-delimiter check, @-escape, s@orig@replace@g dispatch, no-match guard, BUFFER rewrite via print -r -- $BUFFER | sed -E -- "$sedArg" |
| zsh-sed-sub.plugin.zsh | 12 | Entrypoint: fpath append, autoload -Uz, zle -N basicSedSub, three bindkey -M {viins,vicmd,emacs} '^F^P' basicSedSub lines |
| 2 source files | 86 | 1 widget + 1 entrypoint |
@WIDGET FLOW
The widget body is a single read loop with three exit conditions: Enter (commit), ESC (abort), >-missing on Enter (error and abort). On commit it rewrites the entire $BUFFER via a sed subshell. State lives in three locals: sedArg (the accumulating before>after string), orig (split ${sedArg%%>*}), replace (split ${sedArg##*>}).
^F^P pressed (any keymap)
│
▼
┌────────────────────────────────────┐
│ emulate -LR zsh │ ← local-scope zsh emulation,
│ │ no interference from
│ │ user's setopt
└────────────┬───────────────────────┘
│
▼
┌────────────────────────────────────┐
│ BUFFER empty / whitespace-only? │
└────────────┬───────────────────────┘
│ yes │ no
▼ │
zle up-line-or-history │
(pull last command for edit) │
│ │
└────────────┬───────────┘
▼
┌────────────────────────────────────┐
│ paint "Zsh-BsS: ..." status banner │ ← \x1b[1;34m underline
│ enter colored read mode │ \x1b[1;44;37m bg-blue
└────────────┬───────────────────────┘
│
▼
┌────────────────────────────────────┐
│ LOOP: read -k key │
│ key == \n / \r? → break (commit)│
│ key == ESC? → return 1 │
│ key == \x7f|^h? → backspace │
│ key == ^U? → clear sedArg │
│ key == '>'? → flip color │
│ else → append to sedArg │
│ zle -R "...: $sedArg" │ ← live preview redraw
└────────────┬───────────────────────┘
│
▼ (commit)
┌────────────────────────────────────┐
│ sedArg contains '>'? │
│ no → "Needed '>'" error, abort │
│ yes → split on > into orig, replace │
└────────────┬───────────────────────┘
│
▼
┌────────────────────────────────────┐
│ Escape '@' in orig + replace │ ← so we can use @ as
│ sedArg = "s@$orig@$replace@g" │ the sed delimiter
└────────────┬───────────────────────┘
│
▼
┌────────────────────────────────────┐
│ BUFFER contains orig? │
│ no → "No Match." error, abort │
└────────────┬───────────────────────┘
│ yes
▼
┌────────────────────────────────────┐
│ BUFFER = print -r -- $BUFFER | │
│ sed -E -- "$sedArg" │
└────────────────────────────────────┘
$KEYSTROKE TABLE
Inside the read loop, the widget interprets six specific keystrokes; every other byte is appended to the accumulating sedArg buffer.
| Keystroke | Code | Effect |
|---|---|---|
| Enter | \n / \r | Commit: split on >, build s@orig@replace@g, run sed, write BUFFER |
| ESC | \e | Abort. Clear ANSI state, return 1 — BUFFER untouched |
| Backspace | ^? / ^h | Drop the last char of sedArg via ${sedArg[1,-2]} |
| ^U | ^U | Clear sedArg entirely; start over without leaving the widget |
> | > | Append + flip ANSI: switches the live preview into the magenta “you're now typing the replacement” mode |
| (any other) | — | Appended to sedArg |
// CHARACTER COMPARISON IDIOM
All key tests use zsh's numeric-character syntax: (( (#key) == (##\n) )), (( (#key) == (##\e) )), (( (#key) == (##^U) )). $((#x)) evaluates to the code point of the first character of $x; $((##c)) evaluates to the code point of the literal character c in the source. The pattern is the canonical zsh way to write keystroke matches without string-quoting the control character.
&DELIMITER STRATEGY
The widget uses > as the user-facing delimiter between the search and replace halves — that's why the syntax is orig>replace, not orig/replace. Internally, the sed expression uses @ as its delimiter because @ is rare in command-line contexts. The widget escapes any literal @ in either half before assembling the final s@orig@replace@g expression.
User-facing delimiter: >
> is rare in typed-search strings (vs / which appears in every path). The widget paints the live preview a different colour after the first > so the user always knows which side they're typing.
Internal sed delimiter: @
s@orig@replace@g instead of the traditional s/orig/replace/g. Paths and URLs frequently contain /; using @ avoids forcing the user to escape forward slashes in either half.
@-escape pass
If the user types a literal @ in either half, ${orig//@/\\@} + ${replace//@/\\@} escape it before assembly so the sed delimiter still parses correctly.
Global flag baked in
The trailing g in s@...@...@g is hard-coded. The widget exists to fix every occurrence on the line; partial replacement is what cursor-edit is for. Single-occurrence sed substitution is one keystroke away from g-substitution anyway.
~STATE MACHINE
The widget runs a 3-colour ANSI state machine on the prompt line. The colour conveys which phase the user is in, with zero help text required.
| State | ANSI | When |
|---|---|---|
| Banner | \x1b[1;34m (bold blue) | Initial prompt header line |
| Typing 'orig' | \x1b[1;44;37m (blue bg, white fg) | Before any > has been pressed |
| Delimiter pulse | \x1b[0;4;1;34m (underlined bold blue) | The frame that > is pressed |
| Typing 'replace' | \x1b[0;1;37;45m (magenta bg, white fg) | After > has been pressed, while typing the replacement |
| Backspace / ^U flash | \x1b[0m (reset) | Brief reset on destructive edit so the user sees the change land |
| Error | \x1b[0;1;31m (bold red) | "Needed '>'" or "No Match." messages |
!KEYBINDING SURFACE
One chord, three keymaps. The plugin runs bindkey -M viins '^F^P' basicSedSub, then again for vicmd, then again for emacs. The user's bindkey -v / bindkey -e choice doesn't matter; the keybinding fires from every default keymap.
| Keymap | When active | Binding |
|---|---|---|
| viins | vi insert mode (after i / a / etc.) | ^F^P → basicSedSub |
| vicmd | vi command mode (after ESC) | ^F^P → basicSedSub |
| emacs | emacs mode (the default for most users) | ^F^P → basicSedSub |
The chord ^F^P was chosen because neither ^F nor ^P alone is rebound to basicSedSub; each remains free for their default ZLE actions (forward-char, up-line-or-history). The widget itself uses zle up-line-or-history to pre-load the previous command into $BUFFER when invoked on an empty prompt — an inline mirror of ^P's default behaviour.
*TEST COVERAGE
44 zunit @test cases across 6 test files. t-plugin.zsh is the heaviest at 23 cases — it spawns a sub-shell, sources the plugin, drives the widget with stubbed read input, and asserts the resulting $BUFFER. The other files pin structural contracts: keybinding presence in all 3 keymaps, widget registration, autoload coverage, syntax cleanliness, and the entrypoint stem.
| Test file | @test | Pins |
|---|---|---|
| tests/t-plugin.zsh | 23 | End-to-end widget drive: orig>replace rewrites BUFFER; backspace edits; ^U clears; ESC aborts; missing > emits error; no-match leaves BUFFER untouched; empty BUFFER triggers up-line; @-escape in either half |
| tests/t-contract2.zsh | 6 | zle -N basicSedSub registers the widget; basicSedSub appears in $widgets; bindkey for ^F^P in viins / vicmd / emacs all resolve to basicSedSub |
| tests/t-contract3.zsh | 5 | Idempotent re-source: re-sourcing the plugin doesn't double-register; fpath doesn't grow per source |
| tests/t-contract4.zsh | 5 | Entrypoint stem matches plugin directory; autoload/basicSedSub file exists; widget name matches filename |
| tests/t-contract.zsh | 3 | Entrypoint parses cleanly under zsh -n; every _* completion file (vacuously zero) starts with #compdef |
| tests/t-syntax.zsh | 2 | Every autoload/* + entrypoint parses under zsh -n |
| 6 zunit files | 44 | + 21 .sh doc-hygiene gates |
?KEY DESIGN DECISIONS
Each call-out is a decision the implementation could have gone either way on, with the rationale for the path taken.
Custom read loop, not vared
The widget could have used vared to edit a temporary line. Instead it runs a per-keystroke read -k loop because the goal is visible state transitions on the prompt: colour flips, live preview, immediate ESC abort. vared would either buffer too much or commit too early.
Sed, not zsh substitution
Zsh has ${BUFFER//orig/replace} built in, but that's a literal-string substitution. The widget routes through sed -E so the user gets full extended-regex semantics — capture groups, character classes, anchors — without a parser of its own.
> as delimiter
Picked because: (a) rare in typed input, (b) easy to type without a modifier, (c) visually asymmetric so users always know they're past the split point, (d) the colour flip on press makes the role unambiguous.
3-keymap binding
One zle -N + three identical bindkey -M lines. The user's vi/emacs keymap choice doesn't change the keybinding; the chord is the same everywhere. Trades 2 lines of entrypoint code for zero user confusion.
Empty-BUFFER auto-up
If invoked on an empty prompt, the widget calls zle up-line-or-history to pull the previous command into the BUFFER first. The common case is "I just typed something with a typo, hit Enter, want to fix it" — this collapses that into one chord.
Bold-red error states
Two error paths: > missing on Enter, and orig not present in BUFFER. Both repaint the status line in bold red and wait for any keystroke before resetting. Forces the user to acknowledge the error rather than silently dropping the input.
emulate -LR zsh
First line of the widget body. Resets all setopt state to the canonical zsh defaults for the duration of the function (the L) and after the function ends restores the user's options (the R). No user setopt can break the widget's logic.
Single autoload file
The widget body is a single autoload file with its own trailing self-call so the file is both autoloadable (via autoload -Uz) and directly source-able. No registry, no manifest — just fpath + zle -N.
.STRATEGIC POSITION
The smallest meaningful widget in the MenkeTechnologies plugin stack — 86 LOC for a single-chord command-line transform. It's the canonical example of "zsh extensibility lets you carve out a specific friction point and obliterate it." Adjacent plugins in the stack (zsh-sudo, zsh-git-acp, zsh-expand) follow the same shape: one chord, one fn, one BUFFER rewrite.
Long-line edit
The canonical use case: a 200-character pipeline with one wrong arg. Rather than mash arrow keys, ^F^P arg>rightarg rewrites it in one chord.
Repeated typo
A path or variable typo'd 5 times in the same command. Sed-substitution fixes all 5 in one rewrite; manual edit costs N round-trips.
Quick refactor
"Last command was right but for the wrong host." ^P to pull it back, ^F^P olddc>newdc to rewrite.
Plugin stack hub
One of the 5 zsh plugins under MenkeTechnologiesMeta; pairs naturally with zsh-sudo (sudo prepend), zsh-expand (alias expand), and zsh-git-acp (one-chord commit) as command-line transforms.
+LOAD MODEL
The plugin's source-time work is: 1 fpath append, 1 autoload -Uz, 1 zle -N, 3 bindkey -M calls. Total work: well under a millisecond on any zsh. The widget body itself doesn't run until the user actually presses ^F^P.
| Step | When | Cost |
|---|---|---|
fpath+=("${0:h}/autoload") | source time | negligible |
autoload -Uz "${0:h}/autoload/"*(.:t) | source time | Glob + 1 autoload register |
zle -N basicSedSub | source time | 1 widget registration |
3 bindkey -M lines | source time | 3 keymap updates |
First ^F^P | user-driven | Autoload triggers, fn body runs to completion when user hits Enter |
^COMPAT
Hard deps: zsh (with ZLE; the widget assumes interactive zsh), sed (BSD or GNU; the -E flag is portable across both). No optional deps. Compat floor is zsh 5.x — no zsh 6 features. The widget uses zsh's numeric-character syntax ($((#x))) which has been stable since the 4.x line.
If ^F^P conflicts with the user's existing keybindings, they can rebind: bindkey -r '^F^P' followed by a custom bindkey '...' basicSedSub. The widget name doesn't move; only the chord does.