diff --git a/codex-rs/config/src/tui_keymap.rs b/codex-rs/config/src/tui_keymap.rs index 5b95b39cee..fe842e7deb 100644 --- a/codex-rs/config/src/tui_keymap.rs +++ b/codex-rs/config/src/tui_keymap.rs @@ -175,48 +175,91 @@ pub struct TuiEditorKeymap { } /// 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, + /// Enter insert mode after cursor (`a`). pub append_after_cursor: Option, + /// Enter insert mode at end of line (`A`). pub append_line_end: Option, + /// Enter insert mode at first non-blank of line (`I`). pub insert_line_start: Option, + /// Open a new line below and enter insert mode (`o`). pub open_line_below: Option, + /// Open a new line above and enter insert mode (`O`). pub open_line_above: Option, + /// Move cursor left (`h`). pub move_left: Option, + /// Move cursor right (`l`). pub move_right: Option, + /// Move cursor up (`k`). pub move_up: Option, + /// Move cursor down (`j`). pub move_down: Option, + /// Move cursor to start of next word (`w`). pub move_word_forward: Option, + /// Move cursor to start of previous word (`b`). pub move_word_backward: Option, + /// Move cursor to end of current/next word (`e`). pub move_word_end: Option, + /// Move cursor to start of line (`0`). pub move_line_start: Option, + /// Move cursor to end of line (`$`). pub move_line_end: Option, + /// Delete character under cursor (`x`). pub delete_char: Option, + /// Delete from cursor to end of line (`D`). pub delete_to_line_end: Option, + /// Yank the entire line (`Y`). pub yank_line: Option, + /// Paste after cursor (`p`). pub paste_after: Option, + /// Begin delete operator; next key selects motion (`d`). pub start_delete_operator: Option, + /// Begin yank operator; next key selects motion (`y`). pub start_yank_operator: Option, + /// Cancel a pending operator and return to normal mode. pub cancel_operator: Option, } /// 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, + /// Repeat yank operator to yank the whole line (`yy`). pub yank_line: Option, + /// Motion: left (`h`). pub motion_left: Option, + /// Motion: right (`l`). pub motion_right: Option, + /// Motion: up one line (`k`). pub motion_up: Option, + /// Motion: down one line (`j`). pub motion_down: Option, + /// Motion: to start of next word (`w`). pub motion_word_forward: Option, + /// Motion: to start of previous word (`b`). pub motion_word_backward: Option, + /// Motion: to end of current/next word (`e`). pub motion_word_end: Option, + /// Motion: to start of line (`0`). pub motion_line_start: Option, + /// Motion: to end of line (`$`). pub motion_line_end: Option, + /// Cancel the pending operator and return to normal mode. pub cancel: Option, } diff --git a/codex-rs/tui/src/key_hint.rs b/codex-rs/tui/src/key_hint.rs index 24aab6a672..73782e9583 100644 --- a/codex-rs/tui/src/key_hint.rs +++ b/codex-rs/tui/src/key_hint.rs @@ -1,3 +1,12 @@ +//! Key binding primitives and input matching for the TUI. +//! +//! This module provides `KeyBinding`, the runtime representation of a single +//! keybinding (key code + modifier set), along with matching logic that handles +//! cross-terminal inconsistencies in how shifted letters are reported. +//! +//! It also supplies rendering helpers that convert bindings into styled +//! `ratatui::text::Span` values for UI hint display. + use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; @@ -15,6 +24,12 @@ const ALT_PREFIX: &str = "alt + "; const CTRL_PREFIX: &str = "ctrl + "; const SHIFT_PREFIX: &str = "shift + "; +/// A single key chord (key code + modifier set) used for input matching. +/// +/// Matching via `is_press` handles both exact equality and a shifted-letter +/// compatibility fallback for terminals that report uppercase letters without +/// the SHIFT modifier flag. This means a binding defined as `shift-a` will +/// match a terminal event of either `Shift+a` or plain `A`. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub(crate) struct KeyBinding { key: KeyCode, @@ -38,6 +53,17 @@ impl KeyBinding { self.matches_shifted_ascii_letter_compat(event) } + /// Cross-terminal compatibility for shifted ASCII letter bindings. + /// + /// Some terminals (e.g. certain Kitty/Alacritty builds) report `Shift+a` as + /// `KeyCode::Char('A')` with `KeyModifiers::NONE`, while others keep the + /// lowercase char and set the SHIFT flag. This method accepts both forms + /// when the binding specifies SHIFT + an ASCII lowercase letter, and + /// preserves any additional modifiers (e.g. Ctrl) so `ctrl-shift-i` + /// also matches `Ctrl+I`. + /// + /// Only applies to ASCII lowercase letters — non-ASCII shift behavior + /// (accented characters on European layouts, for instance) is not handled. fn matches_shifted_ascii_letter_compat(&self, event: KeyEvent) -> bool { let (KeyCode::Char(bound), KeyCode::Char(observed)) = (self.key, event.code) else { return false; diff --git a/codex-rs/tui/src/keymap.rs b/codex-rs/tui/src/keymap.rs index 0dc58fc081..b954ae52fa 100644 --- a/codex-rs/tui/src/keymap.rs +++ b/codex-rs/tui/src/keymap.rs @@ -58,6 +58,12 @@ pub(crate) struct AppKeymap { pub(crate) toggle_vim_mode: Vec, } +/// Chat-level keybindings evaluated at the app event layer. +/// +/// These participate in the first app-scope conflict validation pass alongside +/// `AppKeymap` actions because both are checked before input reaches the +/// composer. Dispatch gating (empty-composer guard for backtrack) happens in +/// handler code, not here. #[derive(Clone, Debug)] pub(crate) struct ChatKeymap { /// Start/advance edit-previous flow when composer is empty. @@ -66,6 +72,11 @@ pub(crate) struct ChatKeymap { pub(crate) confirm_edit_previous_message: Vec, } +/// Composer-level keybindings validated in the second app-scope conflict pass. +/// +/// App-level handlers execute before the composer receives input, so any key +/// bound here that also appears in `AppKeymap` would be silently intercepted. +/// The conflict validator prevents this by checking app + composer uniqueness. #[derive(Clone, Debug)] pub(crate) struct ComposerKeymap { /// Submit current draft. @@ -100,6 +111,14 @@ pub(crate) struct EditorKeymap { pub(crate) yank: Vec, } +/// Vim normal-mode keybindings for modal editing in the composer textarea. +/// +/// Normal mode is the resting state when Vim is enabled. Pressing a movement +/// or editing key here either moves the cursor, triggers an operator-pending +/// state (via `start_delete_operator` / `start_yank_operator`), or transitions +/// to insert mode. Default bindings include both `shift(letter)` and +/// `plain(UPPERCASE)` variants for uppercase commands like `A`, `I`, `O` to +/// handle cross-terminal shift-reporting inconsistencies. #[derive(Clone, Debug)] pub(crate) struct VimNormalKeymap { pub(crate) enter_insert: Vec, @@ -126,6 +145,12 @@ pub(crate) struct VimNormalKeymap { pub(crate) cancel_operator: Vec, } +/// Vim operator-pending keybindings active after `d` or `y` in normal mode. +/// +/// When an operator (`start_delete_operator` or `start_yank_operator`) is +/// pressed, the next keypress is matched against this context to determine the +/// motion range. Repeating the operator key (`dd`, `yy`) acts on the whole +/// line. `Esc` cancels the pending operator and returns to normal mode. #[derive(Clone, Debug)] pub(crate) struct VimOperatorKeymap { pub(crate) delete_line: Vec,