Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ Toggle via command palette > `Toggle minimal ui`.
| ------------------------------- | ---------------------------------------- |
| `prompt_max_height` | Set max prompt input height |
| `prompt_scrollbar` | Show the prompt scrollbar |
| `vim_initial_mode` | Start in `insert` (default) or `normal` |
| `vim_enter_submit` | Submit with Enter from insert mode |
| `vim_insert_after_submit` | Return to insert mode after submit |
| `vim_system_clipboard_register` | Use the system clipboard as Vim register |
Expand All @@ -163,6 +164,16 @@ Toggle via command palette > `Toggle minimal ui`.
| `vim_showbreak` | Mark wrapped prompt rows |
| `vim_escape_sequence` | Use a two-key escape sequence like `jk` |

### Initial mode

Vim mode starts in insert mode by default. To start in normal mode instead:

```json
{
"vim_initial_mode": "normal"
}
```

### Mode-scoped keybinds

Bind existing commands only in normal mode by nesting overrides under `vim.normal`.
Expand Down Expand Up @@ -349,6 +360,6 @@ vim.g.opencode_opts = {
}
```

## Contributors
## Thanks to all contributors!

<a href="https://github.com/reobin"><img src="https://github.com/reobin.png" width="40" height="40" /></a> <a href="https://github.com/BrettKulp"><img src="https://github.com/BrettKulp.png" width="40" height="40" /></a> <a href="https://github.com/lamiphil"><img src="https://github.com/lamiphil.png" width="40" height="40" /></a> <a href="https://github.com/XPhyro"><img src="https://github.com/XPhyro.png" width="40" height="40" /></a> <a href="https://github.com/shaheislam"><img src="https://github.com/shaheislam.png" width="40" height="40" /></a> <a href="https://github.com/semi710"><img src="https://github.com/semi710.png" width="40" height="40" /></a>
4 changes: 4 additions & 0 deletions packages/opencode/src/config/tui-migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const TUI_SCHEMA_URL = "https://opencode.ai/tui.json"
const decodeTheme = Schema.decodeUnknownOption(Schema.String)
const decodeRecord = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Unknown))
const decodeLangmap = Schema.decodeUnknownOption(TuiConfig.VimLangmap)
const decodeInitialMode = Schema.decodeUnknownOption(TuiConfig.VimInitialMode)
const decodeLineMotions = Schema.decodeUnknownOption(TuiConfig.VimLineMotions)
const decodeScrollSpeed = Schema.decodeUnknownOption(TuiConfig.ScrollSpeed)
const decodeScrollAcceleration = Schema.decodeUnknownOption(TuiConfig.ScrollAcceleration)
Expand Down Expand Up @@ -77,6 +78,7 @@ function normalizeTui(data: Record<string, unknown>):
diff_style: "auto" | "stacked" | undefined
vim_enter_submit: boolean | undefined
vim_insert_after_submit: boolean | undefined
vim_initial_mode: "normal" | "insert" | undefined
vim_system_clipboard_register: boolean | undefined
vim_modal_input: boolean | undefined
vim_langmap: Record<string, string> | undefined
Expand All @@ -90,6 +92,7 @@ function normalizeTui(data: Record<string, unknown>):
diff_style: Option.getOrUndefined(decodeDiffStyle(data.diff_style)),
vim_enter_submit: Option.getOrUndefined(decodeBoolean(data.vim_enter_submit)),
vim_insert_after_submit: Option.getOrUndefined(decodeBoolean(data.vim_insert_after_submit)),
vim_initial_mode: Option.getOrUndefined(decodeInitialMode(data.vim_initial_mode)),
vim_system_clipboard_register: Option.getOrUndefined(decodeBoolean(data.vim_system_clipboard_register)),
vim_modal_input: Option.getOrUndefined(decodeBoolean(data.vim_modal_input)),
vim_langmap: Option.getOrUndefined(decodeLangmap(data.vim_langmap)),
Expand All @@ -101,6 +104,7 @@ function normalizeTui(data: Record<string, unknown>):
parsed.scroll_acceleration === undefined &&
parsed.vim_enter_submit === undefined &&
parsed.vim_insert_after_submit === undefined &&
parsed.vim_initial_mode === undefined &&
parsed.vim_system_clipboard_register === undefined &&
parsed.vim_modal_input === undefined &&
parsed.vim_langmap === undefined &&
Expand Down
15 changes: 15 additions & 0 deletions packages/opencode/test/config/tui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ it.instance("loads tui config with the same precedence order as server config pa
diff_style: "stacked",
vim_enter_submit: true,
vim_insert_after_submit: true,
vim_initial_mode: "normal",
vim_modal_input: false,
},
null,
Expand All @@ -149,6 +150,7 @@ it.instance("loads tui config with the same precedence order as server config pa
expect(config.diff_style).toBe("stacked")
expect(config.vim_enter_submit).toBe(true)
expect(config.vim_insert_after_submit).toBe(true)
expect(config.vim_initial_mode).toBe("normal")
expect(config.vim_modal_input).toBe(false)
}),
),
Expand Down Expand Up @@ -216,6 +218,7 @@ it.instance("migrates tui-specific keys from opencode.json when tui.json does no
scroll_speed: 5,
vim_enter_submit: true,
vim_insert_after_submit: true,
vim_initial_mode: "insert",
vim_modal_input: false,
vim_langmap: { д: "l" },
vim_line_motions: "display",
Expand All @@ -229,6 +232,7 @@ it.instance("migrates tui-specific keys from opencode.json when tui.json does no
expect(config.scroll_speed).toBe(5)
expect(config.vim_enter_submit).toBe(true)
expect(config.vim_insert_after_submit).toBe(true)
expect(config.vim_initial_mode).toBe("insert")
expect(config.vim_modal_input).toBe(false)
expect(config.vim_langmap).toEqual({ д: "l" })
expect(config.vim_line_motions).toBe("display")
Expand All @@ -239,6 +243,7 @@ it.instance("migrates tui-specific keys from opencode.json when tui.json does no
scroll_speed: 5,
vim_enter_submit: true,
vim_insert_after_submit: true,
vim_initial_mode: "insert",
vim_modal_input: false,
vim_langmap: { д: "l" },
vim_line_motions: "display",
Expand All @@ -264,6 +269,16 @@ it.instance("validates vim langmap entries are single characters", () =>
}),
)

it.instance("validates vim initial modes", () =>
Effect.sync(() => {
const decode = Schema.decodeUnknownOption(TuiConfig.Info)

expect(Option.isSome(decode({ vim_initial_mode: "normal" }))).toBe(true)
expect(Option.isSome(decode({ vim_initial_mode: "insert" }))).toBe(true)
expect(Option.isNone(decode({ vim_initial_mode: "visual" }))).toBe(true)
}),
)

it.instance("validates vim line motion modes", () =>
Effect.sync(() => {
const decode = Schema.decodeUnknownOption(TuiConfig.Info)
Expand Down
4 changes: 2 additions & 2 deletions packages/tui/src/component/prompt/vim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { clearSelection } from "../vim/vim-motions"
import { useVimIndicator } from "../vim/vim-indicator"
import type { CopyModeAdapter } from "../vim/copy-adapter"

let lastVimMode: VimMode = "insert"
let lastVimMode: VimMode | undefined

// Fork-owned prompt wiring kept out of the upstream prompt component.
export function usePromptVim(opts: {
Expand Down Expand Up @@ -43,7 +43,7 @@ export function usePromptVim(opts: {

const vimState = createVimState({
enabled: vimEnabled,
initial: () => lastVimMode,
initial: () => lastVimMode ?? cfg.vim_initial_mode ?? "insert",
})
let popCopyMode: (() => void) | undefined
onCleanup(() => {
Expand Down
6 changes: 6 additions & 0 deletions packages/tui/src/config/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export const VimLangmap = Schema.Record(VimLangmapCharacter, VimLangmapCharacter
description: "Map keyboard-layout characters to Vim command keys in normal, visual, and copy modes",
})

export const VimInitialMode = Schema.Literals(["normal", "insert"]).annotate({
description: "Initial Vim mode for the prompt",
})
export type VimInitialMode = Schema.Schema.Type<typeof VimInitialMode>

export const VimLineMotions = Schema.Literals(["logical", "display_vertical", "display"]).annotate({
description: "Use logical lines, display lines for j/k only, or display lines for j/k and 0/^/$ Vim motions",
})
Expand Down Expand Up @@ -95,6 +100,7 @@ export const Info = Schema.Struct({
vim_insert_after_submit: Schema.optional(Schema.Boolean).annotate({
description: "Return prompt to Vim insert mode after submitting",
}),
vim_initial_mode: Schema.optional(VimInitialMode),
vim_system_clipboard_register: Schema.optional(Schema.Boolean).annotate({
description: "Use the system clipboard instead of Vim's internal register for yank and paste",
}),
Expand Down
Loading