Vim keybindings for the OpenCode prompt.
Experimental. Things will break.
vimcodefull.mp4
Install · Configuration · What it does · Keybindings · Known gaps · How it works · Contributing
Use the instructions for your OpenCode major version. Both versions load the same vimcode package, but their config files and plugin entries differ.
Add to your tui.json (or .opencode/tui.json):
{
"plugin": ["vimcode@git+https://github.com/oribarilan/vimcode.git#v0.18.1"]
}Why a versioned ref? OpenCode resolves
@latestonce and caches it forever. Bumping the version in your config is the only reliable way to get updates.
You'll see a toast when a newer version is available (can be turned off).
v2 support is unreleased. The existing v0.18.1 release does not support v2. To try the tested PR code, pin this commit in your global cli.json:
{
"plugins": [
{
"package": "vimcode@git+https://github.com/oribarilan/vimcode.git#d8050f765b6c2c4e7fdc700d8345c1c5752644cb"
}
]
}Cold and warm Git installs were tested on OpenCode 2.0.15 on macOS. Change the pinned ref when updating. See the v2 POC instructions for local development, tarball installation, and compatibility limits.
Use the tuple form in tui.json:
{
"plugin": [["vimcode@git+https://github.com/oribarilan/vimcode.git#v0.18.1", { "updateCheck": false }]]
}Put options alongside package in cli.json:
{
"plugins": [
{
"package": "vimcode@git+https://github.com/oribarilan/vimcode.git#d8050f765b6c2c4e7fdc700d8345c1c5752644cb",
"options": { "updateCheck": false }
}
]
}| Option | Type | Default | Description |
|---|---|---|---|
updateCheck |
boolean |
true |
On startup, check GitHub for new versions (at most once per day). This is the only network request vimcode makes. Set to false to disable. |
modeIndicator |
"toast" | "none" |
"toast" |
How to show the current mode. "toast" flashes a brief notification on each switch. "none" disables it, relying on cursor shape alone. |
startMode |
"insert" | "normal" |
"insert" |
Which mode to start in when OpenCode launches. |
experimentalV2Leader |
string | string[] | false |
"ctrl+x" |
v2 only. Must match the host's leader key. Use false or "none" if the host leader is disabled. |
Adds normal/insert mode to OpenCode's prompt input. Escape enters normal mode, i goes back to insert. A brief toast shows the current mode on each switch (configurable).
In insert mode, typing works normally. Enter adds a newline, Ctrl+Enter submits. The file picker and autocomplete keep working: Enter picks the selected item, Escape closes the picker without leaving insert.
In normal mode, keys are vim commands. Unrecognized keys get swallowed so you don't accidentally type into the prompt. : opens the command palette.
When OpenCode shows its own UI (command palette, /sessions, the @ file picker, question prompts, permission prompts) vimcode steps aside. All keys pass through to the overlay until it closes.
First Escape in insert mode switches to normal - it won't trigger OpenCode's double-escape interrupt. So canceling a running response from insert mode takes 3 escapes: one for normal, two more for the interrupt.
On OpenCode v1, vimcode reads the leader key from your tui.json keybinds automatically. On v2, it cannot read the host's configured leader, so experimentalV2Leader must match it. The default ctrl+x needs no extra plugin option.
In normal and visual mode, the leader key and the follow-up key pass straight through to OpenCode, so leader shortcuts (<leader>c for copy, etc.) work as expected.
In insert mode, printable leaders (like space) insert their character. Non-printable leaders (like ctrl+x) pass through to OpenCode, so leader shortcuts work from any mode.
For a space leader on v1, set it in tui.json:
{
"keybinds": {
"leader": "space"
}
}On v2, set both the host keybind and the plugin option in cli.json:
{
"keybinds": { "leader": "space" },
"plugins": [
{
"package": "vimcode@git+https://github.com/oribarilan/vimcode.git#d8050f765b6c2c4e7fdc700d8345c1c5752644cb",
"options": { "experimentalV2Leader": "space" }
}
]
}If they do not match, vimcode can swallow leader shortcuts or printable input.
Clipboard (y, yy, p) uses the system clipboard: pbcopy on macOS, clip.exe on Windows, xclip on Linux. Linux users need xclip installed (apt install xclip or equivalent). If the clipboard tool is missing, yank/paste still works within the session via an internal register.
Cursor shape (block in normal, bar in insert) works across all terminals. No special terminal support required.
The plugin checks GitHub for new versions once per day on startup. No other network requests, no telemetry.
| Key | Action |
|---|---|
h j k l |
Left, down, up, right |
w b e |
Word forward, backward, end of word |
0 ^ |
Line start |
$ |
Line end |
gg |
Buffer start |
G |
Buffer end |
All motions take counts: 3j moves down 3 lines.
When the input is empty, j/k scroll through prompt history instead of moving the cursor.
d (delete), c (change), and y (yank) combine with motions:
| Combo | Action |
|---|---|
dd cc yy |
Operate on whole line |
D C |
Delete/change to end of line |
dw cw yw |
To next word |
db cb yb |
To previous word |
de ce ye |
To end of word |
d$ c$ y$ |
To end of line |
d0 c0 y0 |
To start of line |
d^ c^ y^ |
To start of line |
dh ch yh |
Character left |
dl cl yl |
Character right |
dj cj yj |
Current + line below |
dk ck yk |
Current + line above |
dG cG yG |
To end of buffer |
Counts work on both operator and motion: 2dd deletes 2 lines, d3w deletes 3 words.
Text objects pair with d, c, y and select in visual mode (viw, vi(, vib, …). i takes the inside; a takes the delimiters too (for words, the trailing whitespace):
| Object | Selects |
|---|---|
iw aw |
The word under the cursor |
i" i' i` |
Inside the quotes |
i( i{ i[ i< |
Inside the bracket pair (opener or closer) |
iq |
The nearest quote of any type (" ' `) |
ib |
The nearest bracket of any type (() {} []) |
So diw, ci", da(, yi{, vib, ciq all work. iw covers the run under the cursor — word, punctuation, or whitespace; aw also takes the trailing whitespace (or leading, when there's none). Brackets nest and can span lines; quotes pair within the current line.
ib note: it means "any bracket" — the nearest of () {} [] — not vim's parens-only ib. Use i( when you specifically want parentheses.
| Key | Action |
|---|---|
i |
Insert at cursor |
a |
Insert after cursor |
A |
Insert at end of line |
o |
Open line below |
O |
Open line above |
Ctrl+O runs one normal-mode command and returns to insert. Motions, operators, counts, and r{char} all work.
Press v in normal mode to enter character-wise visual mode. Press V to select the current line. Motions extend the selection, operators act on it:
| Key | Action |
|---|---|
d x |
Delete selection |
c |
Delete selection, enter insert mode |
y |
Yank (copy) selection |
V |
Select current line |
Escape v |
Exit visual mode |
All normal-mode motions work for extending the selection: h j k l w b e 0 $ G, with counts.
| Key | Action |
|---|---|
Ctrl+O |
One-shot normal mode (execute one command, return to insert) |
r{char} |
Replace character under cursor with {char} |
x |
Delete character |
u |
Undo |
Ctrl+r |
Redo |
p |
Paste from yank register |
: |
Command palette |
:w :write |
Send prompt (via command palette) |
:q :quit :wq |
Quit OpenCode (via command palette) |
:vim |
Toggle vim mode on/off (persisted across restarts) |
/ |
Jump to message (session timeline) |
[ ] |
Scroll conversation half-page up/down |
{ } |
Jump to previous/next message |
X |
Backspace |
J |
Join current line with next |
j k |
Cycle prompt history (when input is empty) |
Enter |
Submit prompt |
Escape |
Pass through for double-escape interrupt |
Ctrl+v- block visual mode is not supported- No persistent mode indicator - the toast fades after about a second. A cache-installed JSX slot still fails to resolve
@opentui/solid/jsx-dev-runtime; last reproduced on 2026-09-08 with OpenCode 1.18.21. OpenCode 1.18.25 uses the same relevant runtime code and OpenTUI version (#3).
Configurable key bindings are next once the core vim coverage stabilizes.
On v1, vimcode registers a host key intercept. The experimental v2 adapter uses a renderer key listener instead. Pure handlers in src/vim/ take the current mode and key and return actions without touching the plugin API. src/index.ts applies those actions through editor methods and host commands; src/v2.ts adapts the v2 context to the shared controller.
- Try it
- If it's useful, a star helps others find it
- Open issues for bugs or missing keybindings
- PRs welcome. See CONTRIBUTING.md for dev setup and the release process.
MIT