// ZSH-SED-SUB — ENGINEERING REPORT

zsh plugin · ZLE widget · Ctrl-F Ctrl-P bound in viins / vicmd / emacs · global sed-style rewrite of the current $BUFFER · single-keystroke read loop

>_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.

86
Plugin LOC
1
ZLE Widget
3
Keymaps Bound
1
Keybinding (^F^P)
44
zunit @test Cases
6
Test Files
21
Docs/Repo Gates
37
Git Commits

Plugin Source — 86 LOC

74 widget body / 12 entrypoint · 86% widget

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

FileLinesRole
autoload/basicSedSub74ZLE 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.zsh12Entrypoint: fpath append, autoload -Uz, zle -N basicSedSub, three bindkey -M {viins,vicmd,emacs} '^F^P' basicSedSub lines
2 source files861 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.

KeystrokeCodeEffect
Enter\n / \rCommit: split on >, build s@orig@replace@g, run sed, write BUFFER
ESC\eAbort. Clear ANSI state, return 1 — BUFFER untouched
Backspace^? / ^hDrop the last char of sedArg via ${sedArg[1,-2]}
^U^UClear 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.

StateANSIWhen
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.

KeymapWhen activeBinding
viinsvi insert mode (after i / a / etc.)^F^PbasicSedSub
vicmdvi command mode (after ESC)^F^PbasicSedSub
emacsemacs mode (the default for most users)^F^PbasicSedSub

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@testPins
tests/t-plugin.zsh23End-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.zsh6zle -N basicSedSub registers the widget; basicSedSub appears in $widgets; bindkey for ^F^P in viins / vicmd / emacs all resolve to basicSedSub
tests/t-contract3.zsh5Idempotent re-source: re-sourcing the plugin doesn't double-register; fpath doesn't grow per source
tests/t-contract4.zsh5Entrypoint stem matches plugin directory; autoload/basicSedSub file exists; widget name matches filename
tests/t-contract.zsh3Entrypoint parses cleanly under zsh -n; every _* completion file (vacuously zero) starts with #compdef
tests/t-syntax.zsh2Every autoload/* + entrypoint parses under zsh -n
6 zunit files44+ 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.

StepWhenCost
fpath+=("${0:h}/autoload")source timenegligible
autoload -Uz "${0:h}/autoload/"*(.:t)source timeGlob + 1 autoload register
zle -N basicSedSubsource time1 widget registration
3 bindkey -M linessource time3 keymap updates
First ^F^Puser-drivenAutoload 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.