Modal vim-like editing for Pi's input prompt. Covers the high-frequency 90% command surface.
pi install npm:pi-vimRestart Pi after install.
Settings are read from ~/.pi/agent/settings.json and project .pi/settings.json.
Default-equivalent settings.json:
{
"piVim": {
"clipboardMirror": "all",
"modeColors": {
"insert": "borderMuted",
"normal": "borderAccent",
"ex": "warning"
},
"syncBorderColorWithMode": false
}
}All keys are optional; omitting piVim is equivalent. Project overrides global; project modeColors replaces global modeColors, with missing modes defaulting above.
clipboardMirror: all mirrors unnamed writes; yank mirrors yanks; never keeps writes internal. Non-mirrored writes stay local for p / P.
syncBorderColorWithMode: false keeps Pi thinking border; true follows mode colors.
piVim.modeColors accepts Pi theme foreground tokens. Missing, invalid, or unknown tokens use defaults above.
Usual/safest: accent, border, borderAccent, borderMuted, success, error, warning, muted, dim, text, thinkingText.
Supported: pi-vim first, @jordyvd/pi-image-attachments second. pi-vim does not wrap previous editors; wrappers decorate in place or forward the CustomEditor surface: lifecycle (handleInput, render, invalidate), text (getText, setText, insertTextAtCursor, getExpandedText), callbacks, actionHandlers, flags, reads (getLines, getCursor, getMode()). Inverse order, insert delegates, and generic composition are unsupported.
Smoke:
pi -e ./index.ts -e ../pi-image-attachments/index.ts
pi -e ./index.ts -e ../../../pi-image-attachments/index.tsCheck: insert text; add/paste image path; see [Image #1]; submit text+image stripped; switch INSERT/NORMAL.
Hooks install with npm install after cloning. To wire them explicitly:
npm run hooks:install- 192 commands: motions, operators, counts, text objects, undo/redo, ex quit
- sub-µs word motions via precomputed boundary cache (~4ms startup, ~150KB memory)
- 0 dependencies
Try on multi-line input:
Esc # NORMAL mode
3gg # jump to absolute line 3
2dw # delete two words
u # undo
<C-r> # redo last undone edit (safe no-op when empty)
2} # jump two paragraphs forward
Mode indicator (INSERT / NORMAL / EX) appears bottom-right, theme-colored and configurable.
Requires @mariozechner/pi-tui >= 0.47.0. With pi-tui >= 0.49.3 and DECSCUSR support, cursor shape follows mode; otherwise software cursor remains.
- Fast modal editing without leaving Pi.
- Count-aware motions/operators (
2dw,3G,d2j,2}). - REPL-focused defaults; out-of-scope boundaries documented.
- Clipboard/register behavior is explicit and tested.
Use pi-vim for Vim muscle-memory in Pi prompts. Skip it if you need full Vim parity (visual mode, macros, search, extended ex-commands, …).
| goal | keys |
|---|---|
| Jump to exact line 25 | 25gg (or 25G) |
| Delete two words | 2dw |
| Change current whitespace-delimited WORD | ciW |
| Delete WORD plus adjacent whitespace | daW |
| Change inside double quotes | ci" |
| Delete inside parentheses | di( |
| Yank braces with contents | ya{ |
| Change to end of line | C |
| Delete current + 2 lines below | d2j |
| Yank 3 lines | 3yy |
| Join 3 lines with spacing | 3J |
| Jump 2 paragraphs forward | 2} |
| Undo last edit | u |
| Redo last undone edit | <C-r> |
| key | action |
|---|---|
Esc / Ctrl+[ |
Insert → Normal mode |
Esc / Ctrl+[ |
Normal mode → pass to Pi (aborts the agent under default Pi keybindings) |
: |
Normal → EX mini-mode |
i |
Normal → Insert at cursor |
a |
Normal → Insert after cursor |
I |
Normal → Insert at first non-whitespace |
A |
Normal → Insert at line end |
o |
Normal → open line below + Insert |
O |
Normal → open line above + Insert |
Optional: move Pi's app.interrupt off bare escape in ~/.pi/agent/keybindings.json if it overlaps with Insert→Normal; user config wins.
Quit-only ex flows.
| key / command | action |
|---|---|
: |
Enter EX mini-mode |
Enter |
Execute pending ex command |
Esc |
Cancel EX mini-mode |
Backspace / Ctrl+h |
Delete one ex-command character; on bare : exits EX mode |
:q |
Quit the current Pi session only when the prompt is empty or whitespace-only; otherwise show a warning |
:q! |
Force quit the current Pi session even when the prompt has text |
:qa |
Same safe quit policy as :q |
:qa! |
Same force quit policy as :q! |
unsupported :{cmd} |
Show warning notification; no quit |
Insert-mode shortcuts (stay in Insert mode):
| key | action |
|---|---|
Shift+Alt+A |
Go to end of line |
Shift+Alt+I |
Go to start of line |
Alt+o |
Open line below |
Alt+Shift+O |
Open line above |
Most navigation keys accept a {count} prefix (max: 9999); % intentionally does not.
| key | action |
|---|---|
h / l / j / k; {count}h/l/j/k |
Move left/right/down/up; line moves clamp to the buffer |
0 / ^ / _ / $ |
Line start / first non-whitespace / counted first non-whitespace / line end |
gg / G; {count}gg / {count}G |
Buffer start/end or absolute 1-indexed line |
w / b / e; {count}w/b/e |
word start/back/end motions |
W / B / E; {count}W/B/E |
whitespace-delimited WORD motions |
{ / }; {count}{ / {count}} |
Previous/next paragraph start |
% |
Jump to the matching (), [], or {} partner |
word splits punctuation from keyword chars; WORD treats any non-whitespace run as one token (foo-bar, path/to). Paragraph starts are non-blank lines at BOF or after blank lines (^\s*$). { / } are navigation-only; brace operator forms (d{, c}, y{, …) are out of scope.
% uses a delimiter under the cursor or scans forward on the current logical line. It matches (), [], {} buffer-wide with lexical, nested, same-delimiter, parser-unaware matching; quotes/comments and mixed delimiters are not special. Missing/unmatched sources no-op. Counts are unsupported: {count}% consumes the count and no-ops; counted d% / y% / c% cancel without writes.
A {count} prefix finds the Nth occurrence of {char} on the line.
| key | action |
|---|---|
f{char} |
Jump forward to char (inclusive) |
F{char} |
Jump backward to char (inclusive) |
t{char} |
Jump forward to one before char (exclusive) |
T{char} |
Jump backward to one after char (exclusive) |
{count}f{char} |
Jump to Nth occurrence of char forward |
; |
Repeat last f/F/t/T motion |
, |
Repeat last motion in reverse direction |
Char-find motions compose with operators: df{char}, ct{char}, d{count}t{char}, etc.
Register-writing edits write to the unnamed register. With the default clipboard mirror policy, they also mirror to the system clipboard best-effort (clipboard failure never breaks editing).
Text objects compose as d/c/y + i/a + object. i means inner; a means around.
| object | keys | range |
|---|---|---|
| word | iw / aw |
Keyword word; aw includes spaces |
| WORD | iW / aW |
Line-local whitespace-delimited WORD; aW includes adjacent whitespace |
| quotes | i" / a", i' / a', i |
Smallest containing quote pair on the line |
| parentheses | i( / a(; aliases i) / a), ib / ab |
Smallest containing pair |
| brackets | i[ / a[; aliases i] / a] |
Smallest containing pair |
| braces | i{ / a{; aliases i} / a}, iB / aB |
Smallest containing pair |
Semantics:
- WORD objects are line-local and whitespace-delimited.
- Quote objects are line-local; odd-backslash escapes are ignored;
aincludes delimiters only, not surrounding whitespace. - Bracket objects are buffer-aware, nested, lexical, and not parser-aware; brackets inside strings/comments still count.
- Empty inner delimiter objects no-op for delete/yank; change enters Insert at the inner start without writing the register.
- Delimited counts cancel (
d2i",2ci(,y2a{). Counted word/WORD text objects work for delete/change only; counted yank text objects cancel.
A {count} or dual-count prefix ({pfx}d{op}{motion}) is supported for word,
WORD, char-find, and linewise motions. Maximum total count: 9999.
| command | deletes |
|---|---|
dw / de / db; dW / dE / dB |
word/WORD motion ranges; {count} repeats |
d$ / d0 / d^ |
To EOL / BOL / first non-whitespace |
d_ / dd; d{count}_ / {count}dd |
Current or counted whole lines |
d{count}j / d{count}k / dG |
Linewise down/up/to EOF |
df{c} / dt{c} / dF{c} / dT{c}; d{count}f{c} |
Char-find ranges |
d% |
Inclusive range through the matching pair target |
diw / daw; diW / daW |
Inner/around word or WORD |
d{count}iw / d{count}iW; d{count}aw / d{count}aW |
Counted word/WORD text objects |
di" / da" (', `) |
Inside/around quotes |
di( / da(, di[ / da[, di{ / da{ |
Inside/around brackets; aliases ), ], }, b, B |
Same motion and count set as d. Deletes text then enters Insert mode.
| command | action |
|---|---|
cw / ce / cb; cW / cE / cB |
Change word/WORD motion ranges + Insert |
c{count}w/e/b; c{count}W/E/B |
Change counted word/WORD motions + Insert |
ciw / caw; ciW / caW |
Change word/WORD text objects + Insert |
c{count}iw / c{count}iW; c{count}aw / c{count}aW |
Change counted word/WORD text objects + Insert |
ci" / ca" (', `) |
Change inside/around quotes + Insert |
ci( / ca(, ci[ / ca[, ci{ / ca{ |
Change inside/around brackets + Insert |
cc / c_; c{count}_ |
Change current or counted whole lines + Insert |
c$ / c0 / c^ |
Delete to EOL / BOL / first non-whitespace + Insert |
c% |
Change inclusive range through the matching pair target + Insert |
| … | All d motions apply |
A {count} prefix is supported for x, p, P. Maximum: 9999.
| key | action |
|---|---|
x |
Delete char under cursor (no-op at/past EOL) |
{count}x |
Delete {count} chars |
s |
Delete char under cursor + Insert mode |
S |
Delete line content + Insert mode |
D |
Delete cursor to EOL (captures \n if at EOL with next line) |
C |
Delete cursor to EOL + Insert mode |
r{char} |
Replace char under cursor with {char} (stays in Normal) |
{count}r{char} |
Replace next {count} chars with {char} |
Same motion set as d. Writes to register, no text mutation.
| command | yanks |
|---|---|
yy / Y; {count}yy / {count}Y |
Whole line(s) + trailing \n |
y{count}j / y{count}k / yG; y_ / y{count}_ |
Linewise ranges |
yw / ye / yb; yW / yE / yB |
word/WORD motion ranges |
y$ / y0 / y^; yf{c} |
EOL / BOL / first non-whitespace / char-find |
y% |
Inclusive range through the matching pair target |
yiw / yaw; yiW / yaW |
Inner/around word or WORD |
yi" / ya" (', `) |
Inside/around quotes |
yi( / ya(, yi[ / ya[, yi{ / ya{ |
Inside/around brackets; aliases ), ], }, b, B |
Counted word/WORD yank motions and counted yank text objects (y2w,
2yw, y2W, 2yW, y2aw, 2yaw, y2aW, y2a{, …) are intentionally not
implemented and cancel the pending operator. Linewise counted yank ({count}yy,
y{count}j/k) is supported.
| key | action |
|---|---|
p |
Put after cursor (char-wise) / new line below (line-wise) |
P |
Put before cursor (char-wise) / new line above (line-wise) |
{count}p |
Put {count} times after cursor |
{count}P |
Put {count} times before cursor |
Put reads the OS clipboard first unless the last local register write was not mirrored. Paste text ending in \n is line-wise.
| key | action |
|---|---|
u |
Undo one change in normal mode |
{count}u |
Undo up to {count} changes in normal mode; clamps at available history |
Ctrl+_ |
Undo in normal mode (alias for u) |
<C-r> |
Redo one undone change in normal mode; safe no-op when redo history is empty |
{count}<C-r> |
Redo up to {count} undone changes in order; clamps at available history and consumes count state (no leak to the next command) |
piVim.clipboardMirror = "all"is the default: every unnamed-register write mirrors to the OS clipboard best-effort.piVim.clipboardMirror = "yank"mirrors yanks only; deletes and changes update only pi-vim's internal shadow.piVim.clipboardMirror = "never"disables write mirroring while keeping internal register writes synchronous.- Rapid mirrored writes coalesce: only the latest pending value is guaranteed to be mirrored.
p/Pread the OS clipboard first when no local write was skipped by policy, falling back to the shadow on read failure/timeout.- If policy skipped the last local write,
p/Puse the shadow so delete/yank → put works without touching the OS clipboard. - While a mirror is in flight,
p/Puse the shadow so immediate yank/delete → put stays ordered. - Pi owns the terminal clipboard backends; on Wayland external state may lag while the shadow stays authoritative for immediate puts.
| area | this extension | full Vim |
|---|---|---|
$ motion |
Moves past the last char (readline Ctrl+E) |
Moves to the last char |
w / e / b + W / E / B |
Cross-line for both word and WORD motions |
Cross-line |
0 / $ operators |
Exclusive of the anchor col | 0 is inclusive of col 0 |
| Undo / redo | Delegates undo to readline; normal-mode <C-r> redo is supported |
Full per-change undo tree |
| Visual mode | Not implemented | v, V, <C-v> |
| Text objects | iw / aw, iW / aW, quote objects, and paren/bracket/brace objects; delimited counts cancel |
Full text-object set |
% matching |
(), [], {} only; lexical same-delimiter matching with no counts, quote/angle matching, parser/matchit logic, mixed-delimiter validation, or Visual % yet |
Also supports percentage jumps and broader matching |
| Count prefix | Operators, motions, navigation, x, r, p, P; capped at MAX_COUNT=9999 |
Full support |
| Registers / macros / search | Not implemented | Supported |
| Ex commands | Quit-only EX mini-mode (:q, :q!, :qa, :qa!) |
Full ex command-line surface |
| Multi-line operators | d/c/y with w/e/b, W/E/B, j/k, and G; not the full Vim motion matrix |
Rich cross-line semantics |
Explicitly deferred:
- Visual modes (
v,V, block visual), including Visual% - Tag text objects (
it,at) - Paragraph/sentence text objects (
ip,ap,is,as) - Angle bracket text objects (
i<,a<) or angle-bracket%matching - Visual-mode text-object selection
- Quote matching via
%, parser-aware delimiter matching, matchit-style matching, and mixed-delimiter structural validation - Delimited-object counts (
d2i",2ci(,y2a{) - Named registers (
"a,"b, …), macros (q{char},@{char}) - Ex surface beyond quit (
:s,:g,:w,:r, …) - Search (
/,?,n,N), repeat (.) - Replace mode (
R) — onlyr{char}is supported - Count prefix beyond currently supported motions, including
{count}%percent-of-file jumps - No insert-mode
<C-r>expansion, no cross-session redo persistence - No upstream
pi-tuiredo prerequisite - Window / tab / buffer management, plugin ecosystem compatibility
index.tshandles modal keys;motions.tsandtext-objects.tshold pure range logic;types.tsholds shared types/constants;test/uses Node's runner.
Run checks with npm run check.