mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
feat(tui): add vim composer mode (#18595)
## Why Codex now has configurable TUI keymaps, but the composer still behaves like a plain text field. Users who prefer modal editing need a way to keep Vim muscle memory while drafting prompts, and the keymap picker needs to expose Vim-specific actions if those bindings are configurable instead of hardcoded. ## What Changed - Adds composer Vim mode with insert/normal state, common normal-mode movement and editing commands, `d`/`y` operator-pending flows, and mode-aware footer and cursor indicators. - Adds `/vim`, an optional global `toggle_vim_mode` binding, and `tui.vim_mode_default` so Vim mode can be toggled per session or enabled as the default composer state. - Extends runtime and config keymaps with `vim_normal` and `vim_operator` contexts, exposes those contexts in `/keymap`, refreshes the config schema, and validates Vim bindings separately. - Integrates Vim normal mode with existing composer behavior: `/` opens slash command entry, `!` enters shell mode, `j`/`k` navigate history at history boundaries, successful submissions reset back to normal mode, and paste burst handling remains insert-mode only. - Teaches the TUI render path to apply and restore cursor style so Vim insert mode can use a bar cursor without leaving the terminal in that state after exit. ## Validation - `cargo test -p codex-tui keymap -- --nocapture` on the keymap/Vim coverage - `cargo insta pending-snapshots` ## Docs This introduces user-facing `/vim`, `tui.vim_mode_default`, and Vim keymap contexts under `tui.keymap`, so the public CLI configuration and slash-command docs should be updated before the feature ships.
This commit is contained in:
1 parent
a5ebedef67
commit
b6f81257f8
33 files changed
+3328
-214
No files matched your search
@@ -102,6 +102,8 @@ pub struct TuiGlobalKeymap {
|
||||
pub queue: Option<KeybindingsSpec>,
|
||||
/// Toggle the composer shortcut overlay.
|
||||
pub toggle_shortcuts: Option<KeybindingsSpec>,
|
||||
/// Toggle Vim mode for the composer input.
|
||||
pub toggle_vim_mode: Option<KeybindingsSpec>,
|
||||
}
|
||||
|
||||
/// Chat context keybindings.
|
||||
@@ -173,6 +175,95 @@ pub struct TuiEditorKeymap {
|
||||
pub yank: Option<KeybindingsSpec>,
|
||||
}
|
||||
|
||||
/// Vim normal-mode keybindings for modal editing inside text areas.
|
||||
///
|
||||
/// Actions that use uppercase letters (like `A` for append-line-end) should
|
||||
/// be specified as `shift-a` in config; the runtime matcher handles
|
||||
/// cross-terminal shift-reporting differences automatically.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct TuiVimNormalKeymap {
|
||||
/// Enter insert mode at cursor (`i`).
|
||||
pub enter_insert: Option<KeybindingsSpec>,
|
||||
/// Enter insert mode after cursor (`a`).
|
||||
pub append_after_cursor: Option<KeybindingsSpec>,
|
||||
/// Enter insert mode at end of line (`A`).
|
||||
pub append_line_end: Option<KeybindingsSpec>,
|
||||
/// Enter insert mode at first non-blank of line (`I`).
|
||||
pub insert_line_start: Option<KeybindingsSpec>,
|
||||
/// Open a new line below and enter insert mode (`o`).
|
||||
pub open_line_below: Option<KeybindingsSpec>,
|
||||
/// Open a new line above and enter insert mode (`O`).
|
||||
pub open_line_above: Option<KeybindingsSpec>,
|
||||
/// Move cursor left (`h`).
|
||||
pub move_left: Option<KeybindingsSpec>,
|
||||
/// Move cursor right (`l`).
|
||||
pub move_right: Option<KeybindingsSpec>,
|
||||
/// Move cursor up (`k`), or recall older composer history at history boundaries.
|
||||
pub move_up: Option<KeybindingsSpec>,
|
||||
/// Move cursor down (`j`), or recall newer composer history at history boundaries.
|
||||
pub move_down: Option<KeybindingsSpec>,
|
||||
/// Move cursor to start of next word (`w`).
|
||||
pub move_word_forward: Option<KeybindingsSpec>,
|
||||
/// Move cursor to start of previous word (`b`).
|
||||
pub move_word_backward: Option<KeybindingsSpec>,
|
||||
/// Move cursor to end of current/next word (`e`).
|
||||
pub move_word_end: Option<KeybindingsSpec>,
|
||||
/// Move cursor to start of line (`0`).
|
||||
pub move_line_start: Option<KeybindingsSpec>,
|
||||
/// Move cursor to end of line (`$`).
|
||||
pub move_line_end: Option<KeybindingsSpec>,
|
||||
/// Delete character under cursor (`x`).
|
||||
pub delete_char: Option<KeybindingsSpec>,
|
||||
/// Delete from cursor to end of line (`D`).
|
||||
pub delete_to_line_end: Option<KeybindingsSpec>,
|
||||
/// Yank the entire line (`Y`).
|
||||
pub yank_line: Option<KeybindingsSpec>,
|
||||
/// Paste after cursor (`p`).
|
||||
pub paste_after: Option<KeybindingsSpec>,
|
||||
/// Begin delete operator; next key selects motion (`d`).
|
||||
pub start_delete_operator: Option<KeybindingsSpec>,
|
||||
/// Begin yank operator; next key selects motion (`y`).
|
||||
pub start_yank_operator: Option<KeybindingsSpec>,
|
||||
/// Cancel a pending operator and return to normal mode.
|
||||
pub cancel_operator: Option<KeybindingsSpec>,
|
||||
}
|
||||
|
||||
/// Vim operator-pending keybindings for modal editing inside text areas.
|
||||
///
|
||||
/// This context is active only while waiting for a motion after `d` or `y`.
|
||||
/// Repeating the operator key (`dd`, `yy`) targets the entire line. Pressing
|
||||
/// `Esc` cancels the pending operator and returns to normal mode without
|
||||
/// modifying text.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct TuiVimOperatorKeymap {
|
||||
/// Repeat delete operator to delete the whole line (`dd`).
|
||||
pub delete_line: Option<KeybindingsSpec>,
|
||||
/// Repeat yank operator to yank the whole line (`yy`).
|
||||
pub yank_line: Option<KeybindingsSpec>,
|
||||
/// Motion: left (`h`).
|
||||
pub motion_left: Option<KeybindingsSpec>,
|
||||
/// Motion: right (`l`).
|
||||
pub motion_right: Option<KeybindingsSpec>,
|
||||
/// Motion: up one line (`k`).
|
||||
pub motion_up: Option<KeybindingsSpec>,
|
||||
/// Motion: down one line (`j`).
|
||||
pub motion_down: Option<KeybindingsSpec>,
|
||||
/// Motion: to start of next word (`w`).
|
||||
pub motion_word_forward: Option<KeybindingsSpec>,
|
||||
/// Motion: to start of previous word (`b`).
|
||||
pub motion_word_backward: Option<KeybindingsSpec>,
|
||||
/// Motion: to end of current/next word (`e`).
|
||||
pub motion_word_end: Option<KeybindingsSpec>,
|
||||
/// Motion: to start of line (`0`).
|
||||
pub motion_line_start: Option<KeybindingsSpec>,
|
||||
/// Motion: to end of line (`$`).
|
||||
pub motion_line_end: Option<KeybindingsSpec>,
|
||||
/// Cancel the pending operator and return to normal mode.
|
||||
pub cancel: Option<KeybindingsSpec>,
|
||||
}
|
||||
|
||||
/// Pager context keybindings for transcript and static overlays.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -261,6 +352,10 @@ pub struct TuiKeymap {
|
||||
#[serde(default)]
|
||||
pub editor: TuiEditorKeymap,
|
||||
#[serde(default)]
|
||||
pub vim_normal: TuiVimNormalKeymap,
|
||||
#[serde(default)]
|
||||
pub vim_operator: TuiVimOperatorKeymap,
|
||||
#[serde(default)]
|
||||
pub pager: TuiPagerKeymap,
|
||||
#[serde(default)]
|
||||
pub list: TuiListKeymap,
|
||||
|
||||
@@ -38,6 +38,8 @@ pub use crate::tui_keymap::TuiGlobalKeymap;
|
||||
pub use crate::tui_keymap::TuiKeymap;
|
||||
pub use crate::tui_keymap::TuiListKeymap;
|
||||
pub use crate::tui_keymap::TuiPagerKeymap;
|
||||
pub use crate::tui_keymap::TuiVimNormalKeymap;
|
||||
pub use crate::tui_keymap::TuiVimOperatorKeymap;
|
||||
|
||||
pub const DEFAULT_OTEL_ENVIRONMENT: &str = "dev";
|
||||
pub const DEFAULT_MEMORIES_MAX_ROLLOUTS_PER_STARTUP: usize = 2;
|
||||
@@ -609,6 +611,11 @@ pub struct Tui {
|
||||
#[serde(default = "default_true")]
|
||||
pub show_tooltips: bool,
|
||||
|
||||
/// Start the composer in Vim mode (`Normal`) by default.
|
||||
/// Defaults to `false`.
|
||||
#[serde(default)]
|
||||
pub vim_mode_default: bool,
|
||||
|
||||
/// Controls whether the TUI uses the terminal's alternate screen buffer.
|
||||
///
|
||||
/// - `auto` (default): Disable alternate screen in Zellij, enable elsewhere.
|
||||
|
||||
Reference in new issue
Block a user