pi-vim

Vim-style modal editing for Pi's TUI editor

Packages

Package details

extension

Install pi-vim from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:pi-vim
Package
pi-vim
Version
0.12.1
Published
Jun 11, 2026
Downloads
3,071/mo · 1,152/wk
Author
lajarre
License
MIT
Types
extension
Size
144.6 KB
Dependencies
0 dependencies · 2 peers
Pi manifest JSON
{
  "extensions": [
    "./index.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

pi-vim

Modal vim-like editing for Pi's input prompt. Covers the high-frequency 90% command surface.

install

pi install npm:pi-vim

Restart Pi after install.

configure

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 for non-executing settings; project modeColors replaces global modeColors whole, with missing modes defaulting above. modeChange is intentionally absent from the default and is read only from the global settings file because it executes shell commands.

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.

modeChange: user-global shell command to run on every transition into the named mode. Both keys are optional. The command runs asynchronously via the system shell, stdio is discarded, failures are silenced, and a hung command is timed out so editing never blocks or breaks. If mode changes happen while a hook command is still running, pi-vim keeps only the latest pending command. Hooks fire only on actual transitions: not on the initial mode, not on EX entry/exit (EX is a sub-state of normal), and not on no-op Esc from normal. Because this is arbitrary shell, project .pi/settings.json values are ignored. pi-vim also emits pi-vim:mode-change on pi.events with { mode, previousMode } for other extensions. Typical use is IME auto-switching via the third-party im-select CLI (cross-platform: macOS / Windows / Linux). Install per its README, then run im-select with no args to print your current IME id and plug those ids into the global config:

macOS example

{
  "piVim": {
    "modeChange": {
      "insert": "im-select im.rime.inputmethod.Squirrel.Hans",
      "normal": "im-select com.apple.keylayout.ABC"
    }
  }
}

pi-vim does not bundle im-select and does not care which tool you use — any shell command works.

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.

wrapping pi-vim

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

Check: insert text; add/paste image path; see [Image #1]; submit text+image stripped; switch INSERT/NORMAL.

contributor setup

Hooks install with npm install after cloning. To wire them explicitly:

npm run hooks:install

stats

  • 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

30-second quickstart

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 @earendil-works/pi-tui >= 0.74.0. With DECSCUSR support, cursor shape follows mode; otherwise software cursor remains.

why pi-vim

  • 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, …).

common recipes

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>

full reference

mode switching

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.

ex mini-mode

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

navigation (normal mode)

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.


character-find motions (normal mode)

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.


edit operators (normal mode)

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

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</code> / <code>a 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; a includes 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.

delete d{motion} / dd

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

change c{motion} / cc

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

single-key edits

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}

yank y{motion} / yy

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.


put / paste

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.


undo / redo

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)

register and clipboard policy

  • 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 / P read 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 / P use the shadow so delete/yank → put works without touching the OS clipboard.
  • While a mirror is in flight, p / P use 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.

known differences from full Vim

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

out of scope

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) — only r{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-tui redo prerequisite
  • Window / tab / buffer management, plugin ecosystem compatibility

architecture notes

  • index.ts handles modal keys; motions.ts and text-objects.ts hold pure range logic; types.ts holds shared types/constants; test/ uses Node's runner.

Run checks with npm run check.