Codex Handbook
tui/src/keymap.rs 2947 lines
//! Runtime keymap resolution for the TUI.//!//! This module converts deserialized config (`TuiKeymap`) into a concrete//! `RuntimeKeymap` used by input handlers at runtime.//!//! Key responsibilities://!//! 1. Apply deterministic precedence (`context -> global fallback -> defaults`).//! 2. Parse canonical key spec strings into `KeyBinding` values.//! 3. Enforce uniqueness across runtime surfaces so one key cannot trigger//!    multiple actions on the same focused input path.//! 4. Return actionable, user-facing error messages with config paths and next//!    steps.//!//! Non-responsibilities://!//! 1. This module does not decide which action should run in a given screen.//!    Callers resolve actions by checking the relevant action binding set.//! 2. This module does not persist configuration; it only resolves loaded config.use crate::key_hint;use crate::key_hint::KeyBinding;use codex_config::types::KeybindingsSpec;use codex_config::types::MAX_FUNCTION_KEY;use codex_config::types::TuiKeymap;use crossterm::event::KeyCode;use crossterm::event::KeyModifiers;use serde::Serialize;use std::collections::HashMap;/// Runtime keymap used by TUI input handlers.////// Resolution precedence is:////// 1. Context-specific binding (`tui.keymap.<context>`)./// 2. `tui.keymap.global` for actions that support global fallback./// 3. Built-in defaults.////// This is the only shape UI code should use for dispatch. It represents a/// fully resolved snapshot with parsing, fallback, explicit unbinding, and/// duplicate-key validation already applied. If a caller keeps using an older/// snapshot after config changes, visible hints and active handlers can drift.#[derive(Clone, Debug)]pub(crate) struct RuntimeKeymap {    pub(crate) app: AppKeymap,    pub(crate) chat: ChatKeymap,    pub(crate) composer: ComposerKeymap,    pub(crate) editor: EditorKeymap,    pub(crate) vim_normal: VimNormalKeymap,    pub(crate) vim_operator: VimOperatorKeymap,    pub(crate) vim_text_object: VimTextObjectKeymap,    pub(crate) pager: PagerKeymap,    pub(crate) list: ListKeymap,    pub(crate) approval: ApprovalKeymap,}#[derive(Clone, Debug)]pub(crate) struct AppKeymap {    /// Open transcript overlay.    pub(crate) open_transcript: Vec<KeyBinding>,    /// Open external editor for the current draft.    pub(crate) open_external_editor: Vec<KeyBinding>,    /// Copy the last agent response to the clipboard.    pub(crate) copy: Vec<KeyBinding>,    /// Clear the terminal UI.    pub(crate) clear_terminal: Vec<KeyBinding>,    /// Toggle Vim mode for the composer input.    pub(crate) toggle_vim_mode: Vec<KeyBinding>,    /// Toggle Fast mode.    pub(crate) toggle_fast_mode: Vec<KeyBinding>,    /// Toggle raw scrollback mode for copy-friendly transcript selection.    pub(crate) toggle_raw_output: Vec<KeyBinding>,}/// 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 {    /// Interrupt the active turn.    pub(crate) interrupt_turn: Vec<KeyBinding>,    /// Decrease the active reasoning effort.    pub(crate) decrease_reasoning_effort: Vec<KeyBinding>,    /// Increase the active reasoning effort.    pub(crate) increase_reasoning_effort: Vec<KeyBinding>,    /// Edit the most recently queued message.    pub(crate) edit_queued_message: Vec<KeyBinding>,}/// 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.    pub(crate) submit: Vec<KeyBinding>,    /// Queue current draft while a task is running.    pub(crate) queue: Vec<KeyBinding>,    /// Toggle composer shortcut overlay.    pub(crate) toggle_shortcuts: Vec<KeyBinding>,    /// Open reverse history search or move to the previous match.    pub(crate) history_search_previous: Vec<KeyBinding>,    /// Move to the next match in reverse history search.    pub(crate) history_search_next: Vec<KeyBinding>,}/// Editor-specific keybindings used by the composer textarea.////// These bindings are interpreted only by text-editing widgets and do not/// participate in global/chat fallback resolution.#[derive(Clone, Debug)]pub(crate) struct EditorKeymap {    pub(crate) insert_newline: Vec<KeyBinding>,    pub(crate) move_left: Vec<KeyBinding>,    pub(crate) move_right: Vec<KeyBinding>,    pub(crate) move_up: Vec<KeyBinding>,    pub(crate) move_down: Vec<KeyBinding>,    pub(crate) move_word_left: Vec<KeyBinding>,    pub(crate) move_word_right: Vec<KeyBinding>,    pub(crate) move_line_start: Vec<KeyBinding>,    pub(crate) move_line_end: Vec<KeyBinding>,    pub(crate) delete_backward: Vec<KeyBinding>,    pub(crate) delete_forward: Vec<KeyBinding>,    pub(crate) delete_backward_word: Vec<KeyBinding>,    pub(crate) delete_forward_word: Vec<KeyBinding>,    pub(crate) kill_line_start: Vec<KeyBinding>,    pub(crate) kill_whole_line: Vec<KeyBinding>,    pub(crate) kill_line_end: Vec<KeyBinding>,    pub(crate) yank: Vec<KeyBinding>,}/// 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, Default)]pub(crate) struct VimNormalKeymap {    pub(crate) enter_insert: Vec<KeyBinding>,    pub(crate) append_after_cursor: Vec<KeyBinding>,    pub(crate) append_line_end: Vec<KeyBinding>,    pub(crate) insert_line_start: Vec<KeyBinding>,    pub(crate) open_line_below: Vec<KeyBinding>,    pub(crate) open_line_above: Vec<KeyBinding>,    pub(crate) move_left: Vec<KeyBinding>,    pub(crate) move_right: Vec<KeyBinding>,    pub(crate) move_up: Vec<KeyBinding>,    pub(crate) move_down: Vec<KeyBinding>,    pub(crate) move_word_forward: Vec<KeyBinding>,    pub(crate) move_word_backward: Vec<KeyBinding>,    pub(crate) move_word_end: Vec<KeyBinding>,    pub(crate) move_line_start: Vec<KeyBinding>,    pub(crate) move_line_end: Vec<KeyBinding>,    pub(crate) delete_char: Vec<KeyBinding>,    pub(crate) substitute_char: Vec<KeyBinding>,    pub(crate) delete_to_line_end: Vec<KeyBinding>,    pub(crate) change_to_line_end: Vec<KeyBinding>,    pub(crate) yank_line: Vec<KeyBinding>,    pub(crate) paste_after: Vec<KeyBinding>,    pub(crate) start_delete_operator: Vec<KeyBinding>,    pub(crate) start_yank_operator: Vec<KeyBinding>,    pub(crate) start_change_operator: Vec<KeyBinding>,    pub(crate) cancel_operator: Vec<KeyBinding>,}/// 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, Default)]pub(crate) struct VimOperatorKeymap {    pub(crate) delete_line: Vec<KeyBinding>,    pub(crate) yank_line: Vec<KeyBinding>,    pub(crate) motion_left: Vec<KeyBinding>,    pub(crate) motion_right: Vec<KeyBinding>,    pub(crate) motion_up: Vec<KeyBinding>,    pub(crate) motion_down: Vec<KeyBinding>,    pub(crate) motion_word_forward: Vec<KeyBinding>,    pub(crate) motion_word_backward: Vec<KeyBinding>,    pub(crate) motion_word_end: Vec<KeyBinding>,    pub(crate) motion_line_start: Vec<KeyBinding>,    pub(crate) motion_line_end: Vec<KeyBinding>,    pub(crate) select_inner_text_object: Vec<KeyBinding>,    pub(crate) select_around_text_object: Vec<KeyBinding>,    pub(crate) cancel: Vec<KeyBinding>,}/// Vim text-object keybindings active after an operator plus inner/around prefix.#[derive(Clone, Debug, Default)]pub(crate) struct VimTextObjectKeymap {    pub(crate) word: Vec<KeyBinding>,    pub(crate) big_word: Vec<KeyBinding>,    pub(crate) parentheses: Vec<KeyBinding>,    pub(crate) brackets: Vec<KeyBinding>,    pub(crate) braces: Vec<KeyBinding>,    pub(crate) double_quote: Vec<KeyBinding>,    pub(crate) single_quote: Vec<KeyBinding>,    pub(crate) backtick: Vec<KeyBinding>,    pub(crate) cancel: Vec<KeyBinding>,}/// Pager/overlay keybindings for transcript and static help views.#[derive(Clone, Debug)]pub(crate) struct PagerKeymap {    pub(crate) scroll_up: Vec<KeyBinding>,    pub(crate) scroll_down: Vec<KeyBinding>,    pub(crate) page_up: Vec<KeyBinding>,    pub(crate) page_down: Vec<KeyBinding>,    pub(crate) half_page_up: Vec<KeyBinding>,    pub(crate) half_page_down: Vec<KeyBinding>,    pub(crate) jump_top: Vec<KeyBinding>,    pub(crate) jump_bottom: Vec<KeyBinding>,    pub(crate) close: Vec<KeyBinding>,    pub(crate) close_transcript: Vec<KeyBinding>,}/// Generic list picker keybindings shared across popup list views.////// These actions describe list intent rather than a specific widget layout./// Vertical actions move the highlighted row, page and jump actions move within/// the current filtered row set, and horizontal actions are available to views/// that expose adjacent choices such as tabs, toolbar values, or ordered item/// movement. Views that also accept search text are responsible for checking/// `is_plain_text_key_event` before dispatching plain-character bindings so a/// configured `j`, `k`, `h`, or `l` does not steal query input.#[derive(Clone, Debug)]pub(crate) struct ListKeymap {    pub(crate) move_up: Vec<KeyBinding>,    pub(crate) move_down: Vec<KeyBinding>,    pub(crate) move_left: Vec<KeyBinding>,    pub(crate) move_right: Vec<KeyBinding>,    pub(crate) page_up: Vec<KeyBinding>,    pub(crate) page_down: Vec<KeyBinding>,    pub(crate) jump_top: Vec<KeyBinding>,    pub(crate) jump_bottom: Vec<KeyBinding>,    pub(crate) accept: Vec<KeyBinding>,    pub(crate) cancel: Vec<KeyBinding>,}/// Approval modal keybindings.////// This covers both selection actions and the "open details fullscreen" escape/// hatch for large approval payloads.#[derive(Clone, Debug)]pub(crate) struct ApprovalKeymap {    pub(crate) open_fullscreen: Vec<KeyBinding>,    pub(crate) open_thread: Vec<KeyBinding>,    pub(crate) approve: Vec<KeyBinding>,    pub(crate) approve_for_session: Vec<KeyBinding>,    pub(crate) approve_for_prefix: Vec<KeyBinding>,    pub(crate) deny: Vec<KeyBinding>,    pub(crate) decline: Vec<KeyBinding>,    pub(crate) cancel: Vec<KeyBinding>,}/// Returns the first binding, used as the primary UI hint for an action.////// Rendering code should prefer this for concise hints while preserving all/// bindings for actual input matching.pub(crate) fn primary_binding(bindings: &[KeyBinding]) -> Option<KeyBinding> {    bindings.first().copied()}/// Resolve one context-local action binding from config.////// Expands to `resolve_bindings(...)` with:/// - configured source: `tui.keymap.<context>.<action>`/// - fallback source: the same action from built-in defaults/// - error path: a stable string path for user-facing diagnostics////// This keeps the resolution table concise while guaranteeing path strings/// stay in sync with field names.macro_rules! resolve_local {    ($keymap:expr, $defaults:expr, $context:ident, $action:ident) => {        resolve_bindings(            ($keymap).$context.$action.as_ref(),            &($defaults).$context.$action,            concat!(                "tui.keymap.",                stringify!($context),                ".",                stringify!($action)            ),        )?    };}/// Resolve one action binding with global fallback.////// Expands to `resolve_bindings_with_global_fallback(...)` with precedence:/// 1. `tui.keymap.<context>.<action>`/// 2. `tui.keymap.global.<action>`/// 3. built-in defaults for `<context>.<action>`////// Used only for actions that intentionally support global reuse./// Context-local empty lists still count as configured values, so they unbind/// the action instead of falling back to `global`.macro_rules! resolve_with_global {    ($keymap:expr, $defaults:expr, $context:ident, $action:ident) => {        resolve_bindings_with_global_fallback(            ($keymap).$context.$action.as_ref(),            ($keymap).global.$action.as_ref(),            &($defaults).$context.$action,            concat!(                "tui.keymap.",                stringify!($context),                ".",                stringify!($action)            ),        )?    };}/// Expand one default-table binding entry into a [`KeyBinding`].////// This is a small declarative layer over `key_hint::{plain, ctrl, alt, shift}`/// used by `default_bindings!` so `built_in_defaults` stays readable.////// Supported forms:/// - `plain(<KeyCode>)`/// - `ctrl(<KeyCode>)`/// - `alt(<KeyCode>)`/// - `shift(<KeyCode>)`/// - `raw(<KeyBinding expression>)` for bindings that do not match the helpers///   (for example combined modifiers like Ctrl+Shift).macro_rules! default_binding {    (plain($key:expr)) => {        key_hint::plain($key)    };    (ctrl($key:expr)) => {        key_hint::ctrl($key)    };    (alt($key:expr)) => {        key_hint::alt($key)    };    (shift($key:expr)) => {        key_hint::shift($key)    };    (raw($binding:expr)) => {        $binding    };}/// Build a `Vec<KeyBinding>` for built-in defaults.////// This macro is intentionally scoped to built-in keymaps. Runtime/// config parsing still goes through `parse_bindings(...)` so user errors can/// be reported with config-path-aware diagnostics.macro_rules! default_bindings {    ($($kind:ident($($arg:tt)*)),* $(,)?) => {        vec![$(default_binding!($kind($($arg)*))),*]    };}impl RuntimeKeymap {    /// Return built-in defaults.    ///    /// This is a convenience for tests and bootstrapping UI state before user    /// config has been loaded. It should not be used as a fallback after    /// parsing `TuiKeymap`, because doing so would ignore explicit user    /// unbindings and conflict diagnostics.    pub(crate) fn defaults() -> Self {        Self::built_in_defaults()    }    /// Resolve a runtime keymap from config, applying precedence and validation.    ///    /// Returns an error when:    ///    /// 1. A keybinding spec cannot be parsed.    /// 2. A context has ambiguous bindings (same key assigned to multiple actions).    ///    /// The error text includes the relevant config path and a concrete next step.    /// Calling code should not merge bindings across unrelated contexts before    /// dispatch, or conflict guarantees from this resolver no longer hold.    pub(crate) fn from_config(keymap: &TuiKeymap) -> Result<Self, String> {        let defaults = Self::built_in_defaults();        let app = AppKeymap {            open_transcript: resolve_bindings(                keymap.global.open_transcript.as_ref(),                &defaults.app.open_transcript,                "tui.keymap.global.open_transcript",            )?,            open_external_editor: resolve_bindings(                keymap.global.open_external_editor.as_ref(),                &defaults.app.open_external_editor,                "tui.keymap.global.open_external_editor",            )?,            copy: resolve_bindings(                keymap.global.copy.as_ref(),                &defaults.app.copy,                "tui.keymap.global.copy",            )?,            clear_terminal: resolve_bindings(                keymap.global.clear_terminal.as_ref(),                &defaults.app.clear_terminal,                "tui.keymap.global.clear_terminal",            )?,            toggle_vim_mode: resolve_bindings(                keymap.global.toggle_vim_mode.as_ref(),                &defaults.app.toggle_vim_mode,                "tui.keymap.global.toggle_vim_mode",            )?,            toggle_fast_mode: resolve_bindings(                keymap.global.toggle_fast_mode.as_ref(),                &defaults.app.toggle_fast_mode,                "tui.keymap.global.toggle_fast_mode",            )?,            toggle_raw_output: resolve_bindings(                keymap.global.toggle_raw_output.as_ref(),                &defaults.app.toggle_raw_output,                "tui.keymap.global.toggle_raw_output",            )?,        };        let mut chat = ChatKeymap {            interrupt_turn: resolve_bindings(                keymap.chat.interrupt_turn.as_ref(),                &defaults.chat.interrupt_turn,                "tui.keymap.chat.interrupt_turn",            )?,            decrease_reasoning_effort: resolve_bindings(                keymap.chat.decrease_reasoning_effort.as_ref(),                &defaults.chat.decrease_reasoning_effort,                "tui.keymap.chat.decrease_reasoning_effort",            )?,            increase_reasoning_effort: resolve_bindings(                keymap.chat.increase_reasoning_effort.as_ref(),                &defaults.chat.increase_reasoning_effort,                "tui.keymap.chat.increase_reasoning_effort",            )?,            edit_queued_message: resolve_bindings(                keymap.chat.edit_queued_message.as_ref(),                &defaults.chat.edit_queued_message,                "tui.keymap.chat.edit_queued_message",            )?,        };        let composer = ComposerKeymap {            submit: resolve_with_global!(keymap, defaults, composer, submit),            queue: resolve_with_global!(keymap, defaults, composer, queue),            toggle_shortcuts: resolve_with_global!(keymap, defaults, composer, toggle_shortcuts),            history_search_previous: resolve_local!(                keymap,                defaults,                composer,                history_search_previous            ),            history_search_next: resolve_local!(keymap, defaults, composer, history_search_next),        };        let editor = EditorKeymap {            insert_newline: resolve_local!(keymap, defaults, editor, insert_newline),            move_left: resolve_local!(keymap, defaults, editor, move_left),            move_right: resolve_local!(keymap, defaults, editor, move_right),            move_up: resolve_local!(keymap, defaults, editor, move_up),            move_down: resolve_local!(keymap, defaults, editor, move_down),            move_word_left: resolve_local!(keymap, defaults, editor, move_word_left),            move_word_right: resolve_local!(keymap, defaults, editor, move_word_right),            move_line_start: resolve_local!(keymap, defaults, editor, move_line_start),            move_line_end: resolve_local!(keymap, defaults, editor, move_line_end),            delete_backward: resolve_local!(keymap, defaults, editor, delete_backward),            delete_forward: resolve_local!(keymap, defaults, editor, delete_forward),            delete_backward_word: resolve_local!(keymap, defaults, editor, delete_backward_word),            delete_forward_word: resolve_local!(keymap, defaults, editor, delete_forward_word),            kill_line_start: resolve_local!(keymap, defaults, editor, kill_line_start),            kill_whole_line: resolve_local!(keymap, defaults, editor, kill_whole_line),            kill_line_end: resolve_local!(keymap, defaults, editor, kill_line_end),            yank: resolve_local!(keymap, defaults, editor, yank),        };        let mut vim_normal = VimNormalKeymap {            enter_insert: resolve_local!(keymap, defaults, vim_normal, enter_insert),            append_after_cursor: resolve_local!(keymap, defaults, vim_normal, append_after_cursor),            append_line_end: resolve_local!(keymap, defaults, vim_normal, append_line_end),            insert_line_start: resolve_local!(keymap, defaults, vim_normal, insert_line_start),            open_line_below: resolve_local!(keymap, defaults, vim_normal, open_line_below),            open_line_above: resolve_local!(keymap, defaults, vim_normal, open_line_above),            move_left: resolve_local!(keymap, defaults, vim_normal, move_left),            move_right: resolve_local!(keymap, defaults, vim_normal, move_right),            move_up: resolve_local!(keymap, defaults, vim_normal, move_up),            move_down: resolve_local!(keymap, defaults, vim_normal, move_down),            move_word_forward: resolve_local!(keymap, defaults, vim_normal, move_word_forward),            move_word_backward: resolve_local!(keymap, defaults, vim_normal, move_word_backward),            move_word_end: resolve_local!(keymap, defaults, vim_normal, move_word_end),            move_line_start: resolve_local!(keymap, defaults, vim_normal, move_line_start),            move_line_end: resolve_local!(keymap, defaults, vim_normal, move_line_end),            delete_char: resolve_local!(keymap, defaults, vim_normal, delete_char),            substitute_char: resolve_local!(keymap, defaults, vim_normal, substitute_char),            delete_to_line_end: resolve_local!(keymap, defaults, vim_normal, delete_to_line_end),            change_to_line_end: resolve_local!(keymap, defaults, vim_normal, change_to_line_end),            yank_line: resolve_local!(keymap, defaults, vim_normal, yank_line),            paste_after: resolve_local!(keymap, defaults, vim_normal, paste_after),            start_delete_operator: resolve_local!(                keymap,                defaults,                vim_normal,                start_delete_operator            ),            start_yank_operator: resolve_local!(keymap, defaults, vim_normal, start_yank_operator),            start_change_operator: resolve_local!(                keymap,                defaults,                vim_normal,                start_change_operator            ),            cancel_operator: resolve_local!(keymap, defaults, vim_normal, cancel_operator),        };        let configured_vim_normal_bindings_to_preserve = configured_bindings_to_preserve([            (                keymap.vim_normal.enter_insert.as_ref(),                vim_normal.enter_insert.as_slice(),            ),            (                keymap.vim_normal.append_after_cursor.as_ref(),                vim_normal.append_after_cursor.as_slice(),            ),            (                keymap.vim_normal.append_line_end.as_ref(),                vim_normal.append_line_end.as_slice(),            ),            (                keymap.vim_normal.insert_line_start.as_ref(),                vim_normal.insert_line_start.as_slice(),            ),            (                keymap.vim_normal.open_line_below.as_ref(),                vim_normal.open_line_below.as_slice(),            ),            (                keymap.vim_normal.open_line_above.as_ref(),                vim_normal.open_line_above.as_slice(),            ),            (                keymap.vim_normal.move_left.as_ref(),                vim_normal.move_left.as_slice(),            ),            (                keymap.vim_normal.move_right.as_ref(),                vim_normal.move_right.as_slice(),            ),            (                keymap.vim_normal.move_up.as_ref(),                vim_normal.move_up.as_slice(),            ),            (                keymap.vim_normal.move_down.as_ref(),                vim_normal.move_down.as_slice(),            ),            (                keymap.vim_normal.move_word_forward.as_ref(),                vim_normal.move_word_forward.as_slice(),            ),            (                keymap.vim_normal.move_word_backward.as_ref(),                vim_normal.move_word_backward.as_slice(),            ),            (                keymap.vim_normal.move_word_end.as_ref(),                vim_normal.move_word_end.as_slice(),            ),            (                keymap.vim_normal.move_line_start.as_ref(),                vim_normal.move_line_start.as_slice(),            ),            (                keymap.vim_normal.move_line_end.as_ref(),                vim_normal.move_line_end.as_slice(),            ),            (                keymap.vim_normal.delete_char.as_ref(),                vim_normal.delete_char.as_slice(),            ),            (                keymap.vim_normal.change_to_line_end.as_ref(),                vim_normal.change_to_line_end.as_slice(),            ),            (                keymap.vim_normal.delete_to_line_end.as_ref(),                vim_normal.delete_to_line_end.as_slice(),            ),            (                keymap.vim_normal.yank_line.as_ref(),                vim_normal.yank_line.as_slice(),            ),            (                keymap.vim_normal.paste_after.as_ref(),                vim_normal.paste_after.as_slice(),            ),            (                keymap.vim_normal.start_delete_operator.as_ref(),                vim_normal.start_delete_operator.as_slice(),            ),            (                keymap.vim_normal.start_yank_operator.as_ref(),                vim_normal.start_yank_operator.as_slice(),            ),            (                keymap.vim_normal.start_change_operator.as_ref(),                vim_normal.start_change_operator.as_slice(),            ),            (                keymap.vim_normal.cancel_operator.as_ref(),                vim_normal.cancel_operator.as_slice(),            ),        ]);        if keymap.vim_normal.start_change_operator.is_none() {            vim_normal                .start_change_operator                .retain(|binding| !configured_vim_normal_bindings_to_preserve.contains(binding));        }        if keymap.vim_normal.substitute_char.is_none() {            vim_normal                .substitute_char                .retain(|binding| !configured_vim_normal_bindings_to_preserve.contains(binding));        }        let mut vim_operator = VimOperatorKeymap {            delete_line: resolve_local!(keymap, defaults, vim_operator, delete_line),            yank_line: resolve_local!(keymap, defaults, vim_operator, yank_line),            motion_left: resolve_local!(keymap, defaults, vim_operator, motion_left),            motion_right: resolve_local!(keymap, defaults, vim_operator, motion_right),            motion_up: resolve_local!(keymap, defaults, vim_operator, motion_up),            motion_down: resolve_local!(keymap, defaults, vim_operator, motion_down),            motion_word_forward: resolve_local!(                keymap,                defaults,                vim_operator,                motion_word_forward            ),            motion_word_backward: resolve_local!(                keymap,                defaults,                vim_operator,                motion_word_backward            ),            motion_word_end: resolve_local!(keymap, defaults, vim_operator, motion_word_end),            motion_line_start: resolve_local!(keymap, defaults, vim_operator, motion_line_start),            motion_line_end: resolve_local!(keymap, defaults, vim_operator, motion_line_end),            select_inner_text_object: resolve_local!(                keymap,                defaults,                vim_operator,                select_inner_text_object            ),            select_around_text_object: resolve_local!(                keymap,                defaults,                vim_operator,                select_around_text_object            ),            cancel: resolve_local!(keymap, defaults, vim_operator, cancel),        };        let configured_vim_operator_bindings_to_preserve = configured_bindings_to_preserve([            (                keymap.vim_operator.delete_line.as_ref(),                vim_operator.delete_line.as_slice(),            ),            (                keymap.vim_operator.yank_line.as_ref(),                vim_operator.yank_line.as_slice(),            ),            (                keymap.vim_operator.motion_left.as_ref(),                vim_operator.motion_left.as_slice(),            ),            (                keymap.vim_operator.motion_right.as_ref(),                vim_operator.motion_right.as_slice(),            ),            (                keymap.vim_operator.motion_up.as_ref(),                vim_operator.motion_up.as_slice(),            ),            (                keymap.vim_operator.motion_down.as_ref(),                vim_operator.motion_down.as_slice(),            ),            (                keymap.vim_operator.motion_word_forward.as_ref(),                vim_operator.motion_word_forward.as_slice(),            ),            (                keymap.vim_operator.motion_word_backward.as_ref(),                vim_operator.motion_word_backward.as_slice(),            ),            (                keymap.vim_operator.motion_word_end.as_ref(),                vim_operator.motion_word_end.as_slice(),            ),            (                keymap.vim_operator.motion_line_start.as_ref(),                vim_operator.motion_line_start.as_slice(),            ),            (                keymap.vim_operator.motion_line_end.as_ref(),                vim_operator.motion_line_end.as_slice(),            ),            (                keymap.vim_operator.cancel.as_ref(),                vim_operator.cancel.as_slice(),            ),        ]);        if keymap.vim_operator.select_inner_text_object.is_none() {            vim_operator                .select_inner_text_object                .retain(|binding| !configured_vim_operator_bindings_to_preserve.contains(binding));        }        if keymap.vim_operator.select_around_text_object.is_none() {            vim_operator                .select_around_text_object                .retain(|binding| !configured_vim_operator_bindings_to_preserve.contains(binding));        }        let vim_text_object = VimTextObjectKeymap {            word: resolve_local!(keymap, defaults, vim_text_object, word),            big_word: resolve_local!(keymap, defaults, vim_text_object, big_word),            parentheses: resolve_local!(keymap, defaults, vim_text_object, parentheses),            brackets: resolve_local!(keymap, defaults, vim_text_object, brackets),            braces: resolve_local!(keymap, defaults, vim_text_object, braces),            double_quote: resolve_local!(keymap, defaults, vim_text_object, double_quote),            single_quote: resolve_local!(keymap, defaults, vim_text_object, single_quote),            backtick: resolve_local!(keymap, defaults, vim_text_object, backtick),            cancel: resolve_local!(keymap, defaults, vim_text_object, cancel),        };        // Reasoning arrow aliases are fallback defaults: existing explicit        // bindings on the same input path keep the keys, while explicit        // reasoning bindings remain authoritative.        if keymap.chat.decrease_reasoning_effort.is_none()            && configured_main_surface_alias_is_used(keymap, "shift-down")        {            chat.decrease_reasoning_effort                .retain(|binding| *binding != key_hint::shift(KeyCode::Down));        }        if keymap.chat.increase_reasoning_effort.is_none()            && configured_main_surface_alias_is_used(keymap, "shift-up")        {            chat.increase_reasoning_effort                .retain(|binding| *binding != key_hint::shift(KeyCode::Up));        }        let pager = PagerKeymap {            scroll_up: resolve_local!(keymap, defaults, pager, scroll_up),            scroll_down: resolve_local!(keymap, defaults, pager, scroll_down),            page_up: resolve_local!(keymap, defaults, pager, page_up),            page_down: resolve_local!(keymap, defaults, pager, page_down),            half_page_up: resolve_local!(keymap, defaults, pager, half_page_up),            half_page_down: resolve_local!(keymap, defaults, pager, half_page_down),            jump_top: resolve_local!(keymap, defaults, pager, jump_top),            jump_bottom: resolve_local!(keymap, defaults, pager, jump_bottom),            close: resolve_local!(keymap, defaults, pager, close),            close_transcript: resolve_local!(keymap, defaults, pager, close_transcript),        };        let approval = ApprovalKeymap {            open_fullscreen: resolve_local!(keymap, defaults, approval, open_fullscreen),            open_thread: resolve_local!(keymap, defaults, approval, open_thread),            approve: resolve_local!(keymap, defaults, approval, approve),            approve_for_session: resolve_local!(keymap, defaults, approval, approve_for_session),            approve_for_prefix: resolve_local!(keymap, defaults, approval, approve_for_prefix),            deny: resolve_local!(keymap, defaults, approval, deny),            decline: resolve_local!(keymap, defaults, approval, decline),            cancel: resolve_local!(keymap, defaults, approval, cancel),        };        let list_move_up = resolve_local!(keymap, defaults, list, move_up);        let list_move_down = resolve_local!(keymap, defaults, list, move_down);        let list_accept = resolve_local!(keymap, defaults, list, accept);        let list_cancel = resolve_local!(keymap, defaults, list, cancel);        let configured_bindings_to_preserve = configured_bindings_to_preserve([            (                keymap.global.open_transcript.as_ref(),                app.open_transcript.as_slice(),            ),            (                keymap.global.open_external_editor.as_ref(),                app.open_external_editor.as_slice(),            ),            (keymap.global.copy.as_ref(), app.copy.as_slice()),            (                keymap.global.clear_terminal.as_ref(),                app.clear_terminal.as_slice(),            ),            (                keymap.global.toggle_vim_mode.as_ref(),                app.toggle_vim_mode.as_slice(),            ),            (                keymap.global.toggle_fast_mode.as_ref(),                app.toggle_fast_mode.as_slice(),            ),            (                keymap.global.toggle_raw_output.as_ref(),                app.toggle_raw_output.as_slice(),            ),            (keymap.list.move_up.as_ref(), list_move_up.as_slice()),            (keymap.list.move_down.as_ref(), list_move_down.as_slice()),            (keymap.list.accept.as_ref(), list_accept.as_slice()),            (keymap.list.cancel.as_ref(), list_cancel.as_slice()),            (                keymap.approval.open_fullscreen.as_ref(),                approval.open_fullscreen.as_slice(),            ),            (                keymap.approval.open_thread.as_ref(),                approval.open_thread.as_slice(),            ),            (                keymap.approval.approve.as_ref(),                approval.approve.as_slice(),            ),            (                keymap.approval.approve_for_session.as_ref(),                approval.approve_for_session.as_slice(),            ),            (                keymap.approval.approve_for_prefix.as_ref(),                approval.approve_for_prefix.as_slice(),            ),            (keymap.approval.deny.as_ref(), approval.deny.as_slice()),            (                keymap.approval.decline.as_ref(),                approval.decline.as_slice(),            ),            (keymap.approval.cancel.as_ref(), approval.cancel.as_slice()),        ]);        let list = ListKeymap {            move_up: list_move_up,            move_down: list_move_down,            move_left: resolve_new_default_bindings(                keymap.list.move_left.as_ref(),                &defaults.list.move_left,                &configured_bindings_to_preserve,                "tui.keymap.list.move_left",            )?,            move_right: resolve_new_default_bindings(                keymap.list.move_right.as_ref(),                &defaults.list.move_right,                &configured_bindings_to_preserve,                "tui.keymap.list.move_right",            )?,            page_up: resolve_new_default_bindings(                keymap.list.page_up.as_ref(),                &defaults.list.page_up,                &configured_bindings_to_preserve,                "tui.keymap.list.page_up",            )?,            page_down: resolve_new_default_bindings(                keymap.list.page_down.as_ref(),                &defaults.list.page_down,                &configured_bindings_to_preserve,                "tui.keymap.list.page_down",            )?,            jump_top: resolve_new_default_bindings(                keymap.list.jump_top.as_ref(),                &defaults.list.jump_top,                &configured_bindings_to_preserve,                "tui.keymap.list.jump_top",            )?,            jump_bottom: resolve_new_default_bindings(                keymap.list.jump_bottom.as_ref(),                &defaults.list.jump_bottom,                &configured_bindings_to_preserve,                "tui.keymap.list.jump_bottom",            )?,            accept: list_accept,            cancel: list_cancel,        };        let resolved = Self {            app,            chat,            composer,            editor,            vim_normal,            vim_operator,            vim_text_object,            pager,            list,            approval,        };        resolved.validate_conflicts()?;        Ok(resolved)    }    /// Built-in keymap defaults.    ///    /// Some actions intentionally include compatibility variants (for example    /// both `?` and `shift-?`) because terminals disagree on whether SHIFT is    /// preserved for certain printable/control chords.    fn built_in_defaults() -> Self {        Self {            app: AppKeymap {                open_transcript: default_bindings![ctrl(KeyCode::Char('t'))],                open_external_editor: default_bindings![ctrl(KeyCode::Char('g'))],                copy: default_bindings![ctrl(KeyCode::Char('o'))],                clear_terminal: default_bindings![ctrl(KeyCode::Char('l'))],                toggle_vim_mode: default_bindings![],                toggle_fast_mode: default_bindings![],                toggle_raw_output: default_bindings![alt(KeyCode::Char('r'))],            },            chat: ChatKeymap {                interrupt_turn: default_bindings![plain(KeyCode::Esc)],                decrease_reasoning_effort: default_bindings![                    alt(KeyCode::Char(',')),                    shift(KeyCode::Down)                ],                increase_reasoning_effort: default_bindings![                    alt(KeyCode::Char('.')),                    shift(KeyCode::Up)                ],                edit_queued_message: default_bindings![alt(KeyCode::Up), shift(KeyCode::Left)],            },            composer: ComposerKeymap {                submit: default_bindings![plain(KeyCode::Enter)],                queue: default_bindings![plain(KeyCode::Tab)],                toggle_shortcuts: default_bindings![                    plain(KeyCode::Char('?')),                    shift(KeyCode::Char('?'))                ],                history_search_previous: default_bindings![ctrl(KeyCode::Char('r'))],                history_search_next: default_bindings![ctrl(KeyCode::Char('s'))],            },            editor: EditorKeymap {                insert_newline: default_bindings![                    ctrl(KeyCode::Char('j')),                    ctrl(KeyCode::Char('m')),                    plain(KeyCode::Enter),                    shift(KeyCode::Enter),                    alt(KeyCode::Enter)                ],                move_left: default_bindings![plain(KeyCode::Left), ctrl(KeyCode::Char('b'))],                move_right: default_bindings![plain(KeyCode::Right), ctrl(KeyCode::Char('f'))],                move_up: default_bindings![plain(KeyCode::Up), ctrl(KeyCode::Char('p'))],                move_down: default_bindings![plain(KeyCode::Down), ctrl(KeyCode::Char('n'))],                move_word_left: default_bindings![                    alt(KeyCode::Char('b')),                    raw(KeyBinding::new(KeyCode::Left, KeyModifiers::ALT)),                    raw(KeyBinding::new(KeyCode::Left, KeyModifiers::CONTROL))                ],                move_word_right: default_bindings![                    alt(KeyCode::Char('f')),                    raw(KeyBinding::new(KeyCode::Right, KeyModifiers::ALT)),                    raw(KeyBinding::new(KeyCode::Right, KeyModifiers::CONTROL))                ],                move_line_start: default_bindings![plain(KeyCode::Home), ctrl(KeyCode::Char('a'))],                move_line_end: default_bindings![plain(KeyCode::End), ctrl(KeyCode::Char('e'))],                delete_backward: default_bindings![                    plain(KeyCode::Backspace),                    shift(KeyCode::Backspace),                    ctrl(KeyCode::Char('h'))                ],                delete_forward: default_bindings![                    plain(KeyCode::Delete),                    shift(KeyCode::Delete),                    ctrl(KeyCode::Char('d'))                ],                delete_backward_word: default_bindings![                    alt(KeyCode::Backspace),                    ctrl(KeyCode::Backspace),                    raw(KeyBinding::new(                        KeyCode::Backspace,                        KeyModifiers::CONTROL | KeyModifiers::SHIFT,                    )),                    ctrl(KeyCode::Char('w')),                    raw(KeyBinding::new(                        KeyCode::Char('h'),                        KeyModifiers::CONTROL | KeyModifiers::ALT,                    ))                ],                delete_forward_word: default_bindings![                    alt(KeyCode::Delete),                    ctrl(KeyCode::Delete),                    raw(KeyBinding::new(                        KeyCode::Delete,                        KeyModifiers::CONTROL | KeyModifiers::SHIFT,                    )),                    alt(KeyCode::Char('d'))                ],                kill_line_start: default_bindings![ctrl(KeyCode::Char('u'))],                kill_whole_line: default_bindings![],                kill_line_end: default_bindings![ctrl(KeyCode::Char('k'))],                yank: default_bindings![ctrl(KeyCode::Char('y'))],            },            vim_normal: VimNormalKeymap {                enter_insert: default_bindings![plain(KeyCode::Char('i')), plain(KeyCode::Insert)],                append_after_cursor: default_bindings![plain(KeyCode::Char('a'))],                append_line_end: default_bindings![                    shift(KeyCode::Char('a')),                    plain(KeyCode::Char('A'))                ],                insert_line_start: default_bindings![                    shift(KeyCode::Char('i')),                    plain(KeyCode::Char('I'))                ],                open_line_below: default_bindings![plain(KeyCode::Char('o'))],                open_line_above: default_bindings![                    shift(KeyCode::Char('o')),                    plain(KeyCode::Char('O'))                ],                move_left: default_bindings![plain(KeyCode::Char('h')), plain(KeyCode::Left)],                move_right: default_bindings![plain(KeyCode::Char('l')), plain(KeyCode::Right)],                move_up: default_bindings![plain(KeyCode::Char('k')), plain(KeyCode::Up)],                move_down: default_bindings![plain(KeyCode::Char('j')), plain(KeyCode::Down)],                move_word_forward: default_bindings![plain(KeyCode::Char('w'))],                move_word_backward: default_bindings![plain(KeyCode::Char('b'))],                move_word_end: default_bindings![plain(KeyCode::Char('e'))],                move_line_start: default_bindings![plain(KeyCode::Char('0'))],                move_line_end: default_bindings![                    plain(KeyCode::Char('$')),                    shift(KeyCode::Char('$'))                ],                delete_char: default_bindings![plain(KeyCode::Char('x'))],                substitute_char: default_bindings![plain(KeyCode::Char('s'))],                delete_to_line_end: default_bindings![                    shift(KeyCode::Char('d')),                    plain(KeyCode::Char('D'))                ],                change_to_line_end: default_bindings![                    shift(KeyCode::Char('c')),                    plain(KeyCode::Char('C'))                ],                yank_line: default_bindings![shift(KeyCode::Char('y')), plain(KeyCode::Char('Y'))],                paste_after: default_bindings![plain(KeyCode::Char('p'))],                start_delete_operator: default_bindings![plain(KeyCode::Char('d'))],                start_yank_operator: default_bindings![plain(KeyCode::Char('y'))],                start_change_operator: default_bindings![plain(KeyCode::Char('c'))],                cancel_operator: default_bindings![plain(KeyCode::Esc)],            },            vim_operator: VimOperatorKeymap {                delete_line: default_bindings![plain(KeyCode::Char('d'))],                yank_line: default_bindings![plain(KeyCode::Char('y'))],                motion_left: default_bindings![plain(KeyCode::Char('h'))],                motion_right: default_bindings![plain(KeyCode::Char('l'))],                motion_up: default_bindings![plain(KeyCode::Char('k'))],                motion_down: default_bindings![plain(KeyCode::Char('j'))],                motion_word_forward: default_bindings![plain(KeyCode::Char('w'))],                motion_word_backward: default_bindings![plain(KeyCode::Char('b'))],                motion_word_end: default_bindings![plain(KeyCode::Char('e'))],                motion_line_start: default_bindings![plain(KeyCode::Char('0'))],                motion_line_end: default_bindings![                    plain(KeyCode::Char('$')),                    shift(KeyCode::Char('$'))                ],                select_inner_text_object: default_bindings![plain(KeyCode::Char('i'))],                select_around_text_object: default_bindings![plain(KeyCode::Char('a'))],                cancel: default_bindings![plain(KeyCode::Esc)],            },            vim_text_object: VimTextObjectKeymap {                word: default_bindings![plain(KeyCode::Char('w'))],                big_word: default_bindings![shift(KeyCode::Char('w')), plain(KeyCode::Char('W'))],                parentheses: default_bindings![                    plain(KeyCode::Char('(')),                    shift(KeyCode::Char('(')),                    plain(KeyCode::Char(')')),                    shift(KeyCode::Char(')')),                    plain(KeyCode::Char('b'))                ],                brackets: default_bindings![plain(KeyCode::Char('[')), plain(KeyCode::Char(']'))],                braces: default_bindings![                    plain(KeyCode::Char('{')),                    shift(KeyCode::Char('{')),                    plain(KeyCode::Char('}')),                    shift(KeyCode::Char('}')),                    shift(KeyCode::Char('b')),                    plain(KeyCode::Char('B'))                ],                double_quote: default_bindings![                    plain(KeyCode::Char('"')),                    shift(KeyCode::Char('"'))                ],                single_quote: default_bindings![plain(KeyCode::Char('\''))],                backtick: default_bindings![plain(KeyCode::Char('`'))],                cancel: default_bindings![plain(KeyCode::Esc)],            },            pager: PagerKeymap {                scroll_up: default_bindings![plain(KeyCode::Up), plain(KeyCode::Char('k'))],                scroll_down: default_bindings![plain(KeyCode::Down), plain(KeyCode::Char('j'))],                page_up: default_bindings![                    plain(KeyCode::PageUp),                    shift(KeyCode::Char(' ')),                    ctrl(KeyCode::Char('b'))                ],                page_down: default_bindings![                    plain(KeyCode::PageDown),                    plain(KeyCode::Char(' ')),                    ctrl(KeyCode::Char('f'))                ],                half_page_up: default_bindings![ctrl(KeyCode::Char('u'))],                half_page_down: default_bindings![ctrl(KeyCode::Char('d'))],                jump_top: default_bindings![plain(KeyCode::Home)],                jump_bottom: default_bindings![plain(KeyCode::End)],                close: default_bindings![plain(KeyCode::Char('q')), ctrl(KeyCode::Char('c'))],                close_transcript: default_bindings![ctrl(KeyCode::Char('t'))],            },            list: ListKeymap {                move_up: default_bindings![                    plain(KeyCode::Up),                    ctrl(KeyCode::Char('p')),                    ctrl(KeyCode::Char('k')),                    plain(KeyCode::Char('k'))                ],                move_down: default_bindings![                    plain(KeyCode::Down),                    ctrl(KeyCode::Char('n')),                    ctrl(KeyCode::Char('j')),                    plain(KeyCode::Char('j'))                ],                move_left: default_bindings![plain(KeyCode::Left), ctrl(KeyCode::Char('h'))],                move_right: default_bindings![plain(KeyCode::Right), ctrl(KeyCode::Char('l'))],                page_up: default_bindings![plain(KeyCode::PageUp), ctrl(KeyCode::Char('b'))],                page_down: default_bindings![plain(KeyCode::PageDown), ctrl(KeyCode::Char('f'))],                jump_top: default_bindings![plain(KeyCode::Home)],                jump_bottom: default_bindings![plain(KeyCode::End)],                accept: default_bindings![plain(KeyCode::Enter)],                cancel: default_bindings![plain(KeyCode::Esc)],            },            approval: ApprovalKeymap {                open_fullscreen: default_bindings![                    ctrl(KeyCode::Char('a')),                    raw(KeyBinding::new(                        KeyCode::Char('a'),                        KeyModifiers::CONTROL | KeyModifiers::SHIFT,                    ))                ],                open_thread: default_bindings![plain(KeyCode::Char('o'))],                approve: default_bindings![plain(KeyCode::Char('y'))],                approve_for_session: default_bindings![plain(KeyCode::Char('a'))],                approve_for_prefix: default_bindings![plain(KeyCode::Char('p'))],                deny: default_bindings![plain(KeyCode::Char('d'))],                decline: default_bindings![plain(KeyCode::Esc), plain(KeyCode::Char('n'))],                cancel: default_bindings![plain(KeyCode::Char('c'))],            },        }    }    /// Reject ambiguous bindings in scopes that are evaluated together.    ///    /// We validate in multiple passes because runtime handling has mixed    /// precedence:    ///    /// 1. `app` actions can shadow composer actions because app checks run    ///    before forwarding to the composer.    /// 2. Contexts with hard-coded sequence behavior, such as edit-previous    ///    backtracking, intentionally stay outside this configurable keymap.    fn validate_conflicts(&self) -> Result<(), String> {        validate_unique(            "app",            [                ("open_transcript", self.app.open_transcript.as_slice()),                (                    "open_external_editor",                    self.app.open_external_editor.as_slice(),                ),                ("copy", self.app.copy.as_slice()),                ("clear_terminal", self.app.clear_terminal.as_slice()),                ("toggle_vim_mode", self.app.toggle_vim_mode.as_slice()),                ("toggle_fast_mode", self.app.toggle_fast_mode.as_slice()),                ("toggle_raw_output", self.app.toggle_raw_output.as_slice()),                ("chat.interrupt_turn", self.chat.interrupt_turn.as_slice()),                (                    "chat.decrease_reasoning_effort",                    self.chat.decrease_reasoning_effort.as_slice(),                ),                (                    "chat.increase_reasoning_effort",                    self.chat.increase_reasoning_effort.as_slice(),                ),                (                    "chat.edit_queued_message",                    self.chat.edit_queued_message.as_slice(),                ),                ("composer.submit", self.composer.submit.as_slice()),                ("composer.queue", self.composer.queue.as_slice()),                (                    "composer.toggle_shortcuts",                    self.composer.toggle_shortcuts.as_slice(),                ),                (                    "composer.history_search_previous",                    self.composer.history_search_previous.as_slice(),                ),                (                    "composer.history_search_next",                    self.composer.history_search_next.as_slice(),                ),            ],        )?;        validate_no_reserved(            "main",            [                ("open_transcript", self.app.open_transcript.as_slice()),                (                    "open_external_editor",                    self.app.open_external_editor.as_slice(),                ),                ("copy", self.app.copy.as_slice()),                ("clear_terminal", self.app.clear_terminal.as_slice()),                ("toggle_vim_mode", self.app.toggle_vim_mode.as_slice()),                ("toggle_fast_mode", self.app.toggle_fast_mode.as_slice()),                ("toggle_raw_output", self.app.toggle_raw_output.as_slice()),                ("chat.interrupt_turn", self.chat.interrupt_turn.as_slice()),                (                    "chat.decrease_reasoning_effort",                    self.chat.decrease_reasoning_effort.as_slice(),                ),                (                    "chat.increase_reasoning_effort",                    self.chat.increase_reasoning_effort.as_slice(),                ),                (                    "chat.edit_queued_message",                    self.chat.edit_queued_message.as_slice(),                ),                ("composer.submit", self.composer.submit.as_slice()),                ("composer.queue", self.composer.queue.as_slice()),                (                    "composer.toggle_shortcuts",                    self.composer.toggle_shortcuts.as_slice(),                ),                (                    "composer.history_search_previous",                    self.composer.history_search_previous.as_slice(),                ),                (                    "composer.history_search_next",                    self.composer.history_search_next.as_slice(),                ),            ],            MAIN_RESERVED_BINDINGS,            [(                "chat.interrupt_turn",                "fixed.backtrack",                key_hint::plain(KeyCode::Esc),            )],        )?;        validate_no_shadow_with_allowed_overlaps(            "app",            [                ("open_transcript", self.app.open_transcript.as_slice()),                (                    "open_external_editor",                    self.app.open_external_editor.as_slice(),                ),                ("copy", self.app.copy.as_slice()),                ("clear_terminal", self.app.clear_terminal.as_slice()),                ("toggle_vim_mode", self.app.toggle_vim_mode.as_slice()),                ("toggle_fast_mode", self.app.toggle_fast_mode.as_slice()),                ("toggle_raw_output", self.app.toggle_raw_output.as_slice()),            ],            [                ("list.move_up", self.list.move_up.as_slice()),                ("list.move_down", self.list.move_down.as_slice()),                ("list.move_left", self.list.move_left.as_slice()),                ("list.move_right", self.list.move_right.as_slice()),                ("list.page_up", self.list.page_up.as_slice()),                ("list.page_down", self.list.page_down.as_slice()),                ("list.jump_top", self.list.jump_top.as_slice()),                ("list.jump_bottom", self.list.jump_bottom.as_slice()),                ("list.accept", self.list.accept.as_slice()),                ("list.cancel", self.list.cancel.as_slice()),                (                    "approval.open_fullscreen",                    self.approval.open_fullscreen.as_slice(),                ),                ("approval.open_thread", self.approval.open_thread.as_slice()),                ("approval.approve", self.approval.approve.as_slice()),                (                    "approval.approve_for_session",                    self.approval.approve_for_session.as_slice(),                ),                (                    "approval.approve_for_prefix",                    self.approval.approve_for_prefix.as_slice(),                ),                ("approval.deny", self.approval.deny.as_slice()),                ("approval.decline", self.approval.decline.as_slice()),                ("approval.cancel", self.approval.cancel.as_slice()),            ],            [(                "clear_terminal",                "list.move_right",                key_hint::ctrl(KeyCode::Char('l')),            )],        )?;        // The request-user-input overlay consumes turn interruption before        // configurable question navigation reaches its list handler.        validate_no_shadow_with_allowed_overlaps(            "request_user_input",            [("chat.interrupt_turn", self.chat.interrupt_turn.as_slice())],            [                ("list.move_left", self.list.move_left.as_slice()),                ("list.move_right", self.list.move_right.as_slice()),            ],            [],        )?;        // While the composer is focused, these main-surface handlers always        // consume matching keys before the event reaches the textarea editor.        validate_no_shadow_with_allowed_overlaps(            "main",            [                ("open_transcript", self.app.open_transcript.as_slice()),                (                    "open_external_editor",                    self.app.open_external_editor.as_slice(),                ),                ("copy", self.app.copy.as_slice()),                ("clear_terminal", self.app.clear_terminal.as_slice()),                ("chat.interrupt_turn", self.chat.interrupt_turn.as_slice()),                (                    "chat.decrease_reasoning_effort",                    self.chat.decrease_reasoning_effort.as_slice(),                ),                (                    "chat.increase_reasoning_effort",                    self.chat.increase_reasoning_effort.as_slice(),                ),                ("composer.submit", self.composer.submit.as_slice()),                ("toggle_vim_mode", self.app.toggle_vim_mode.as_slice()),                ("toggle_fast_mode", self.app.toggle_fast_mode.as_slice()),                ("toggle_raw_output", self.app.toggle_raw_output.as_slice()),                (                    "composer.history_search_previous",                    self.composer.history_search_previous.as_slice(),                ),            ],            [                (                    "editor.insert_newline",                    self.editor.insert_newline.as_slice(),                ),                ("editor.move_left", self.editor.move_left.as_slice()),                ("editor.move_right", self.editor.move_right.as_slice()),                ("editor.move_up", self.editor.move_up.as_slice()),                ("editor.move_down", self.editor.move_down.as_slice()),                (                    "editor.move_word_left",                    self.editor.move_word_left.as_slice(),                ),                (                    "editor.move_word_right",                    self.editor.move_word_right.as_slice(),                ),                (                    "editor.move_line_start",                    self.editor.move_line_start.as_slice(),                ),                ("editor.move_line_end", self.editor.move_line_end.as_slice()),                (                    "editor.delete_backward",                    self.editor.delete_backward.as_slice(),                ),                (                    "editor.delete_forward",                    self.editor.delete_forward.as_slice(),                ),                (                    "editor.delete_backward_word",                    self.editor.delete_backward_word.as_slice(),                ),                (                    "editor.delete_forward_word",                    self.editor.delete_forward_word.as_slice(),                ),                (                    "editor.kill_line_start",                    self.editor.kill_line_start.as_slice(),                ),                (                    "editor.kill_whole_line",                    self.editor.kill_whole_line.as_slice(),                ),                ("editor.kill_line_end", self.editor.kill_line_end.as_slice()),                ("editor.yank", self.editor.yank.as_slice()),            ],            [(                "composer.submit",                "editor.insert_newline",                key_hint::plain(KeyCode::Enter),            )],        )?;        validate_unique(            "editor",            [                ("insert_newline", self.editor.insert_newline.as_slice()),                ("move_left", self.editor.move_left.as_slice()),                ("move_right", self.editor.move_right.as_slice()),                ("move_up", self.editor.move_up.as_slice()),                ("move_down", self.editor.move_down.as_slice()),                ("move_word_left", self.editor.move_word_left.as_slice()),                ("move_word_right", self.editor.move_word_right.as_slice()),                ("move_line_start", self.editor.move_line_start.as_slice()),                ("move_line_end", self.editor.move_line_end.as_slice()),                ("delete_backward", self.editor.delete_backward.as_slice()),                ("delete_forward", self.editor.delete_forward.as_slice()),                (                    "delete_backward_word",                    self.editor.delete_backward_word.as_slice(),                ),                (                    "delete_forward_word",                    self.editor.delete_forward_word.as_slice(),                ),                ("kill_line_start", self.editor.kill_line_start.as_slice()),                ("kill_whole_line", self.editor.kill_whole_line.as_slice()),                ("kill_line_end", self.editor.kill_line_end.as_slice()),                ("yank", self.editor.yank.as_slice()),            ],        )?;        validate_unique(            "vim_normal",            [                ("enter_insert", self.vim_normal.enter_insert.as_slice()),                (                    "append_after_cursor",                    self.vim_normal.append_after_cursor.as_slice(),                ),                (                    "append_line_end",                    self.vim_normal.append_line_end.as_slice(),                ),                (                    "insert_line_start",                    self.vim_normal.insert_line_start.as_slice(),                ),                (                    "open_line_below",                    self.vim_normal.open_line_below.as_slice(),                ),                (                    "open_line_above",                    self.vim_normal.open_line_above.as_slice(),                ),                ("move_left", self.vim_normal.move_left.as_slice()),                ("move_right", self.vim_normal.move_right.as_slice()),                ("move_up", self.vim_normal.move_up.as_slice()),                ("move_down", self.vim_normal.move_down.as_slice()),                (                    "move_word_forward",                    self.vim_normal.move_word_forward.as_slice(),                ),                (                    "move_word_backward",                    self.vim_normal.move_word_backward.as_slice(),                ),                ("move_word_end", self.vim_normal.move_word_end.as_slice()),                (                    "move_line_start",                    self.vim_normal.move_line_start.as_slice(),                ),                ("move_line_end", self.vim_normal.move_line_end.as_slice()),                ("delete_char", self.vim_normal.delete_char.as_slice()),                (                    "substitute_char",                    self.vim_normal.substitute_char.as_slice(),                ),                (                    "delete_to_line_end",                    self.vim_normal.delete_to_line_end.as_slice(),                ),                (                    "change_to_line_end",                    self.vim_normal.change_to_line_end.as_slice(),                ),                ("yank_line", self.vim_normal.yank_line.as_slice()),                ("paste_after", self.vim_normal.paste_after.as_slice()),                (                    "start_delete_operator",                    self.vim_normal.start_delete_operator.as_slice(),                ),                (                    "start_yank_operator",                    self.vim_normal.start_yank_operator.as_slice(),                ),                (                    "start_change_operator",                    self.vim_normal.start_change_operator.as_slice(),                ),                (                    "cancel_operator",                    self.vim_normal.cancel_operator.as_slice(),                ),            ],        )?;        validate_unique(            "vim_operator",            [                ("delete_line", self.vim_operator.delete_line.as_slice()),                ("yank_line", self.vim_operator.yank_line.as_slice()),                ("motion_left", self.vim_operator.motion_left.as_slice()),                ("motion_right", self.vim_operator.motion_right.as_slice()),                ("motion_up", self.vim_operator.motion_up.as_slice()),                ("motion_down", self.vim_operator.motion_down.as_slice()),                (                    "motion_word_forward",                    self.vim_operator.motion_word_forward.as_slice(),                ),                (                    "motion_word_backward",                    self.vim_operator.motion_word_backward.as_slice(),                ),                (                    "motion_word_end",                    self.vim_operator.motion_word_end.as_slice(),                ),                (                    "motion_line_start",                    self.vim_operator.motion_line_start.as_slice(),                ),                (                    "motion_line_end",                    self.vim_operator.motion_line_end.as_slice(),                ),                (                    "select_inner_text_object",                    self.vim_operator.select_inner_text_object.as_slice(),                ),                (                    "select_around_text_object",                    self.vim_operator.select_around_text_object.as_slice(),                ),                ("cancel", self.vim_operator.cancel.as_slice()),            ],        )?;        validate_unique(            "vim_text_object",            [                ("word", self.vim_text_object.word.as_slice()),                ("big_word", self.vim_text_object.big_word.as_slice()),                ("parentheses", self.vim_text_object.parentheses.as_slice()),                ("brackets", self.vim_text_object.brackets.as_slice()),                ("braces", self.vim_text_object.braces.as_slice()),                ("double_quote", self.vim_text_object.double_quote.as_slice()),                ("single_quote", self.vim_text_object.single_quote.as_slice()),                ("backtick", self.vim_text_object.backtick.as_slice()),                ("cancel", self.vim_text_object.cancel.as_slice()),            ],        )?;        validate_unique(            "pager",            [                ("scroll_up", self.pager.scroll_up.as_slice()),                ("scroll_down", self.pager.scroll_down.as_slice()),                ("page_up", self.pager.page_up.as_slice()),                ("page_down", self.pager.page_down.as_slice()),                ("half_page_up", self.pager.half_page_up.as_slice()),                ("half_page_down", self.pager.half_page_down.as_slice()),                ("jump_top", self.pager.jump_top.as_slice()),                ("jump_bottom", self.pager.jump_bottom.as_slice()),                ("close", self.pager.close.as_slice()),                ("close_transcript", self.pager.close_transcript.as_slice()),            ],        )?;        validate_no_reserved(            "pager",            [                ("scroll_up", self.pager.scroll_up.as_slice()),                ("scroll_down", self.pager.scroll_down.as_slice()),                ("page_up", self.pager.page_up.as_slice()),                ("page_down", self.pager.page_down.as_slice()),                ("half_page_up", self.pager.half_page_up.as_slice()),                ("half_page_down", self.pager.half_page_down.as_slice()),                ("jump_top", self.pager.jump_top.as_slice()),                ("jump_bottom", self.pager.jump_bottom.as_slice()),                ("close", self.pager.close.as_slice()),                ("close_transcript", self.pager.close_transcript.as_slice()),            ],            TRANSCRIPT_BACKTRACK_RESERVED_BINDINGS,            [],        )?;        validate_unique(            "list",            [                ("move_up", self.list.move_up.as_slice()),                ("move_down", self.list.move_down.as_slice()),                ("move_left", self.list.move_left.as_slice()),                ("move_right", self.list.move_right.as_slice()),                ("page_up", self.list.page_up.as_slice()),                ("page_down", self.list.page_down.as_slice()),                ("jump_top", self.list.jump_top.as_slice()),                ("jump_bottom", self.list.jump_bottom.as_slice()),                ("accept", self.list.accept.as_slice()),                ("cancel", self.list.cancel.as_slice()),            ],        )?;        validate_unique(            "approval",            [                ("open_fullscreen", self.approval.open_fullscreen.as_slice()),                ("open_thread", self.approval.open_thread.as_slice()),                ("approve", self.approval.approve.as_slice()),                (                    "approve_for_session",                    self.approval.approve_for_session.as_slice(),                ),                (                    "approve_for_prefix",                    self.approval.approve_for_prefix.as_slice(),                ),                ("deny", self.approval.deny.as_slice()),                ("decline", self.approval.decline.as_slice()),                ("cancel", self.approval.cancel.as_slice()),            ],        )?;        let mut seen: HashMap<(KeyCode, KeyModifiers), &'static str> = HashMap::new();        for (action, bindings) in [            ("list.move_up", self.list.move_up.as_slice()),            ("list.move_down", self.list.move_down.as_slice()),            ("list.move_left", self.list.move_left.as_slice()),            ("list.move_right", self.list.move_right.as_slice()),            ("list.page_up", self.list.page_up.as_slice()),            ("list.page_down", self.list.page_down.as_slice()),            ("list.jump_top", self.list.jump_top.as_slice()),            ("list.jump_bottom", self.list.jump_bottom.as_slice()),            ("list.accept", self.list.accept.as_slice()),            ("list.cancel", self.list.cancel.as_slice()),            (                "approval.open_fullscreen",                self.approval.open_fullscreen.as_slice(),            ),            ("approval.open_thread", self.approval.open_thread.as_slice()),            ("approval.approve", self.approval.approve.as_slice()),            (                "approval.approve_for_session",                self.approval.approve_for_session.as_slice(),            ),            (                "approval.approve_for_prefix",                self.approval.approve_for_prefix.as_slice(),            ),            ("approval.deny", self.approval.deny.as_slice()),            ("approval.decline", self.approval.decline.as_slice()),            ("approval.cancel", self.approval.cancel.as_slice()),        ] {            for binding in bindings {                let key = binding.parts();                if let Some(previous) = seen.insert(key, action) {                    // Approval overlays intentionally reserve Esc as a stable                    // cancellation path even though decline options may also                    // display it in contexts where that is safe.                    if previous == "list.cancel"                        && action == "approval.decline"                        && key == (KeyCode::Esc, KeyModifiers::NONE)                    {                        continue;                    }                    return Err(format!(                        "Ambiguous approval overlay keymap bindings: `{previous}` and `{action}` use the same key. \Set unique keys in `~/.codex/config.toml` and retry. \See the Codex keymap documentation for supported actions and examples."                    ));                }            }        }        Ok(())    }}/// Reject duplicate keys inside one effective context map.////// This intentionally allows the same key across different contexts; handlers/// only evaluate one context at a time.fn validate_unique<const N: usize>(    context: &str,    pairs: [(&'static str, &[KeyBinding]); N],) -> Result<(), String> {    let mut seen: HashMap<(KeyCode, KeyModifiers), &'static str> = HashMap::new();    for (action, bindings) in pairs {        for binding in bindings {            let key = binding.parts();            if let Some(previous) = seen.insert(key, action) {                return Err(format!(                    "Ambiguous `tui.keymap.{context}` bindings: `{previous}` and `{action}` use the same key. \Set unique keys in `~/.codex/config.toml` and retry. \See the Codex keymap documentation for supported actions and examples."                ));            }        }    }    Ok(())}fn validate_no_shadow_with_allowed_overlaps<const N: usize, const M: usize, const A: usize>(    context: &str,    primary: [(&'static str, &[KeyBinding]); N],    shadowed: [(&'static str, &[KeyBinding]); M],    allowed_overlaps: [(&'static str, &'static str, KeyBinding); A],) -> Result<(), String> {    let mut seen: HashMap<(KeyCode, KeyModifiers), &'static str> = HashMap::new();    for (action, bindings) in primary {        for binding in bindings {            seen.insert(binding.parts(), action);        }    }    for (action, bindings) in shadowed {        for binding in bindings {            let key = binding.parts();            if let Some(previous) = seen.get(&key) {                if allowed_overlaps.iter().any(                    |(allowed_primary, allowed_shadowed, allowed_binding)| {                        *allowed_primary == *previous                            && *allowed_shadowed == action                            && allowed_binding.parts() == key                    },                ) {                    continue;                }                return Err(format!(                    "Ambiguous `tui.keymap.{context}` bindings: `{previous}` shadows `{action}` with the same key. \Set unique keys in `~/.codex/config.toml` and retry. \See the Codex keymap documentation for supported actions and examples."                ));            }        }    }    Ok(())}fn validate_no_reserved<const N: usize, const A: usize>(    context: &str,    pairs: [(&'static str, &[KeyBinding]); N],    reserved: &[(&'static str, KeyBinding)],    allowed_overlaps: [(&'static str, &'static str, KeyBinding); A],) -> Result<(), String> {    for (action, bindings) in pairs {        for binding in bindings {            let key = binding.parts();            if let Some((reserved_action, _)) = reserved                .iter()                .find(|(_, reserved_binding)| reserved_binding.parts() == key)            {                if allowed_overlaps.iter().any(                    |(allowed_action, allowed_reserved_action, allowed_binding)| {                        *allowed_action == action                            && *allowed_reserved_action == *reserved_action                            && allowed_binding.parts() == key                    },                ) {                    continue;                }                return Err(format!(                    "Ambiguous `tui.keymap.{context}` bindings: `{action}` uses a key reserved by `{reserved_action}`. \Set a different key in `~/.codex/config.toml` and retry. \See the Codex keymap documentation for supported actions and examples."                ));            }        }    }    Ok(())}const MAIN_RESERVED_BINDINGS: &[(&str, KeyBinding)] = &[    (        "fixed.interrupt_or_quit",        key_hint::ctrl(KeyCode::Char('c')),    ),    ("fixed.quit", key_hint::ctrl(KeyCode::Char('d'))),    ("fixed.paste_image", key_hint::ctrl(KeyCode::Char('v'))),    ("fixed.paste_image", key_hint::ctrl_alt(KeyCode::Char('v'))),    (        "fixed.cycle_collaboration_mode",        key_hint::shift(KeyCode::Tab),    ),    ("fixed.backtrack", key_hint::plain(KeyCode::Esc)),    ("fixed.previous_agent", key_hint::alt(KeyCode::Left)),    ("fixed.next_agent", key_hint::alt(KeyCode::Right)),    ("fixed.slash_command", key_hint::plain(KeyCode::Char('/'))),    ("fixed.shell_command", key_hint::plain(KeyCode::Char('!'))),    ("fixed.file_paths", key_hint::plain(KeyCode::Char('@'))),    (        "fixed.connector_mentions",        key_hint::plain(KeyCode::Char('$')),    ),];const TRANSCRIPT_BACKTRACK_RESERVED_BINDINGS: &[(&str, KeyBinding)] = &[    (        "fixed.transcript_edit_previous",        key_hint::plain(KeyCode::Esc),    ),    (        "fixed.transcript_edit_previous",        key_hint::plain(KeyCode::Left),    ),    (        "fixed.transcript_edit_next",        key_hint::plain(KeyCode::Right),    ),    (        "fixed.transcript_confirm_edit",        key_hint::plain(KeyCode::Enter),    ),];/// Resolve one action with context -> global -> default precedence.////// `path` should be the context-specific config path so parser errors point/// users at the override they attempted to set.////// A configured empty list is authoritative: it returns an empty binding set/// and does not continue to the global or built-in fallback. This is what makes/// explicit unbinding work for globally reusable actions like composer submit.fn resolve_bindings_with_global_fallback(    configured: Option<&KeybindingsSpec>,    global: Option<&KeybindingsSpec>,    fallback: &[KeyBinding],    path: &str,) -> Result<Vec<KeyBinding>, String> {    if let Some(configured) = configured {        return parse_bindings(configured, path);    }    if let Some(global) = global {        return parse_bindings(global, path);    }    Ok(fallback.to_vec())}/// Resolve one action binding in a context without global fallback.////// Missing values inherit from the built-in fallback; configured values, including/// empty lists, replace that fallback for the action.fn resolve_bindings(    configured: Option<&KeybindingsSpec>,    fallback: &[KeyBinding],    path: &str,) -> Result<Vec<KeyBinding>, String> {    let Some(spec) = configured else {        return Ok(fallback.to_vec());    };    parse_bindings(spec, path)}fn configured_bindings_to_preserve<const N: usize>(    pairs: [(Option<&KeybindingsSpec>, &[KeyBinding]); N],) -> Vec<KeyBinding> {    let mut configured_bindings = Vec::new();    for (configured, resolved) in pairs {        if configured.is_none() {            continue;        }        for binding in resolved {            if !configured_bindings.contains(binding) {                configured_bindings.push(*binding);            }        }    }    configured_bindings}fn configured_main_surface_alias_is_used(keymap: &TuiKeymap, alias: &str) -> bool {    let mut global = keymap.global.clone();    if keymap.composer.submit.is_some() {        global.submit = None;    }    if keymap.composer.queue.is_some() {        global.queue = None;    }    if keymap.composer.toggle_shortcuts.is_some() {        global.toggle_shortcuts = None;    }    // Reasoning shortcuts run before composer/editor key handling, so fallback    // aliases must yield to any explicit binding on the same main-surface input    // path.    configured_context_alias_is_used(&global, alias)        || configured_context_alias_is_used(&keymap.chat, alias)        || configured_context_alias_is_used(&keymap.composer, alias)        || configured_context_alias_is_used(&keymap.editor, alias)        || configured_context_alias_is_used(&keymap.vim_normal, alias)        || configured_context_alias_is_used(&keymap.vim_operator, alias)        || configured_context_alias_is_used(&keymap.vim_text_object, alias)}fn configured_context_alias_is_used(context: &impl Serialize, alias: &str) -> bool {    let Ok(value) = serde_json::to_value(context) else {        return false;    };    keymap_value_contains_alias(&value, alias)}fn keymap_value_contains_alias(value: &serde_json::Value, alias: &str) -> bool {    match value {        serde_json::Value::String(value) => value == alias,        serde_json::Value::Array(values) => values            .iter()            .any(|value| keymap_value_contains_alias(value, alias)),        serde_json::Value::Object(values) => values            .values()            .any(|value| keymap_value_contains_alias(value, alias)),        serde_json::Value::Bool(_) | serde_json::Value::Number(_) | serde_json::Value::Null => {            false        }    }}fn resolve_new_default_bindings(    configured: Option<&KeybindingsSpec>,    fallback: &[KeyBinding],    configured_bindings_to_preserve: &[KeyBinding],    path: &str,) -> Result<Vec<KeyBinding>, String> {    let Some(spec) = configured else {        return Ok(fallback            .iter()            .copied()            .filter(|binding| !configured_bindings_to_preserve.contains(binding))            .collect());    };    parse_bindings(spec, path)}/// Parse one keybinding value (`string` or `list[string]`) into concrete bindings.////// Duplicate entries are de-duplicated while preserving first-seen order so the/// first key can remain the primary UI hint.fn parse_bindings(spec: &KeybindingsSpec, path: &str) -> Result<Vec<KeyBinding>, String> {    let mut parsed = Vec::new();    for raw in spec.specs() {        let binding = parse_keybinding(raw.as_str()).ok_or_else(|| {            format!(                "Invalid `{path}` = `{}`. Use values like `ctrl-a`, `shift-enter`, or `page-down`. \See the Codex keymap documentation for supported actions and examples.",                raw.as_str()            )        })?;        if !parsed.contains(&binding) {            parsed.push(binding);        }    }    Ok(parsed)}/// Parse one normalized keybinding spec such as `ctrl-a` or `shift-enter`.////// Specs are expected to be normalized by config deserialization, but this/// parser remains strict to keep runtime error messages precise.fn parse_keybinding(spec: &str) -> Option<KeyBinding> {    let mut parts = spec.split('-');    let mut modifiers = KeyModifiers::NONE;    let mut key_name = None;    for part in parts.by_ref() {        match part {            "ctrl" => modifiers |= KeyModifiers::CONTROL,            "alt" => modifiers |= KeyModifiers::ALT,            "shift" => modifiers |= KeyModifiers::SHIFT,            other => {                key_name = Some(other.to_string());                break;            }        }    }    let mut key_name = key_name?;    for trailing in parts {        key_name.push('-');        key_name.push_str(trailing);    }    let key = match key_name.as_str() {        "enter" => KeyCode::Enter,        "tab" => KeyCode::Tab,        "backspace" => KeyCode::Backspace,        "esc" => KeyCode::Esc,        "delete" => KeyCode::Delete,        "up" => KeyCode::Up,        "down" => KeyCode::Down,        "left" => KeyCode::Left,        "right" => KeyCode::Right,        "home" => KeyCode::Home,        "end" => KeyCode::End,        "page-up" => KeyCode::PageUp,        "page-down" => KeyCode::PageDown,        "space" => KeyCode::Char(' '),        "minus" => KeyCode::Char('-'),        other if other.len() == 1 => KeyCode::Char(char::from(other.as_bytes()[0])),        other if other.starts_with('f') => {            let number = other[1..].parse::<u8>().ok()?;            if (1..=MAX_FUNCTION_KEY).contains(&number) {                KeyCode::F(number)            } else {                return None;            }        }        _ => return None,    };    Some(KeyBinding::new(key, modifiers))}#[cfg(test)]mod tests {    use super::*;    use codex_config::types::KeybindingSpec;    fn one(spec: &str) -> KeybindingsSpec {        KeybindingsSpec::One(KeybindingSpec(spec.to_string()))    }    fn expect_conflict(keymap: &TuiKeymap, first: &str, second: &str) {        let err = RuntimeKeymap::from_config(keymap).expect_err("expected conflict");        assert!(err.contains(first));        assert!(err.contains(second));    }    #[test]    fn parses_canonical_binding() {        let binding = parse_keybinding("ctrl-alt-shift-a").expect("binding should parse");        assert_eq!(binding.parts().0, KeyCode::Char('a'));        assert_eq!(            binding.parts().1,            KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT        );    }    #[test]    fn rejects_shadowing_composer_binding_in_app_scope() {        let mut keymap = TuiKeymap::default();        keymap.global.open_transcript = Some(one("ctrl-t"));        keymap.composer.submit = Some(one("ctrl-t"));        let err = RuntimeKeymap::from_config(&keymap).expect_err("expected shadowing conflict");        assert!(err.contains("composer.submit"));        assert!(err.contains("open_transcript"));    }    #[test]    fn rejects_shadowing_composer_queue_in_app_scope() {        let mut keymap = TuiKeymap::default();        keymap.global.open_external_editor = Some(one("ctrl-g"));        keymap.composer.queue = Some(one("ctrl-g"));        let err = RuntimeKeymap::from_config(&keymap).expect_err("expected shadowing conflict");        assert!(err.contains("composer.queue"));        assert!(err.contains("open_external_editor"));    }    #[test]    fn rejects_shadowing_composer_toggle_shortcuts_in_app_scope() {        let mut keymap = TuiKeymap::default();        keymap.global.open_transcript = Some(one("ctrl-k"));        keymap.composer.toggle_shortcuts = Some(one("ctrl-k"));        let err = RuntimeKeymap::from_config(&keymap).expect_err("expected shadowing conflict");        assert!(err.contains("composer.toggle_shortcuts"));        assert!(err.contains("open_transcript"));    }    #[test]    fn rejects_shadowing_editor_binding_in_main_scope() {        let mut keymap = TuiKeymap::default();        keymap.composer.submit = Some(one("ctrl-j"));        keymap.editor.insert_newline = Some(one("ctrl-j"));        let err = RuntimeKeymap::from_config(&keymap).expect_err("expected shadowing conflict");        assert!(err.contains("composer.submit"));        assert!(err.contains("editor.insert_newline"));    }    #[test]    fn rejects_shadowing_editor_binding_from_outer_main_handler() {        let mut keymap = TuiKeymap::default();        keymap.global.copy = Some(one("ctrl-y"));        keymap.editor.yank = Some(one("ctrl-y"));        let err = RuntimeKeymap::from_config(&keymap).expect_err("expected shadowing conflict");        assert!(err.contains("copy"));        assert!(err.contains("editor.yank"));    }    #[test]    fn rejects_shadowing_approval_binding_in_app_scope() {        let mut keymap = TuiKeymap::default();        keymap.global.open_transcript = Some(one("y"));        let err = RuntimeKeymap::from_config(&keymap).expect_err("expected shadowing conflict");        assert!(err.contains("approval.approve"));        assert!(err.contains("open_transcript"));    }    #[test]    fn rejects_shadowing_list_binding_in_app_scope() {        let mut keymap = TuiKeymap::default();        keymap.global.copy = Some(one("down"));        let err = RuntimeKeymap::from_config(&keymap).expect_err("expected shadowing conflict");        assert!(err.contains("list.move_down"));        assert!(err.contains("copy"));    }    #[test]    fn supports_string_or_array_bindings() {        let mut keymap = TuiKeymap::default();        keymap.composer.submit = Some(KeybindingsSpec::Many(vec![            KeybindingSpec("ctrl-enter".to_string()),            KeybindingSpec("meta-enter".to_string()),        ]));        let err = RuntimeKeymap::from_config(&keymap).expect_err("meta is not a valid modifier");        assert!(err.contains("tui.keymap.composer.submit"));        keymap.composer.submit = Some(KeybindingsSpec::Many(vec![            KeybindingSpec("ctrl-enter".to_string()),            KeybindingSpec("ctrl-shift-enter".to_string()),        ]));        let runtime = RuntimeKeymap::from_config(&keymap).expect("valid multi-binding");        assert_eq!(runtime.composer.submit.len(), 2);    }    #[test]    fn deduplicates_repeated_bindings_while_preserving_first_seen_order() {        let mut keymap = TuiKeymap::default();        keymap.composer.submit = Some(KeybindingsSpec::Many(vec![            KeybindingSpec("ctrl-enter".to_string()),            KeybindingSpec("ctrl-enter".to_string()),            KeybindingSpec("ctrl-shift-enter".to_string()),        ]));        let runtime = RuntimeKeymap::from_config(&keymap).expect("valid multi-binding");        assert_eq!(            runtime.composer.submit,            vec![                key_hint::ctrl(KeyCode::Enter),                KeyBinding::new(KeyCode::Enter, KeyModifiers::CONTROL | KeyModifiers::SHIFT)            ]        );    }    #[test]    fn falls_back_to_global_binding_when_context_override_is_not_set() {        let mut keymap = TuiKeymap::default();        keymap.global.queue = Some(one("ctrl-q"));        let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");        assert_eq!(            runtime.composer.queue,            vec![key_hint::ctrl(KeyCode::Char('q'))]        );    }    #[test]    fn invalid_global_open_transcript_binding_reports_global_path() {        let mut keymap = TuiKeymap::default();        keymap.global.open_transcript = Some(one("meta-t"));        let err = RuntimeKeymap::from_config(&keymap).expect_err("expected parse error");        assert!(err.contains("tui.keymap.global.open_transcript"));    }    #[test]    fn invalid_global_open_external_editor_binding_reports_global_path() {        let mut keymap = TuiKeymap::default();        keymap.global.open_external_editor = Some(one("meta-g"));        let err = RuntimeKeymap::from_config(&keymap).expect_err("expected parse error");        assert!(err.contains("tui.keymap.global.open_external_editor"));    }    #[test]    fn default_copy_binding_is_ctrl_o() {        let runtime = RuntimeKeymap::defaults();        assert_eq!(runtime.app.copy, vec![key_hint::ctrl(KeyCode::Char('o'))]);    }    #[test]    fn defaults_include_reassignable_main_surface_actions() {        let runtime = RuntimeKeymap::defaults();        assert_eq!(            runtime.app.clear_terminal,            vec![key_hint::ctrl(KeyCode::Char('l'))]        );        assert_eq!(runtime.app.toggle_fast_mode, Vec::new());        assert_eq!(            runtime.chat.interrupt_turn,            vec![key_hint::plain(KeyCode::Esc)]        );        assert_eq!(            runtime.chat.decrease_reasoning_effort,            vec![                key_hint::alt(KeyCode::Char(',')),                key_hint::shift(KeyCode::Down),            ]        );        assert_eq!(            runtime.chat.increase_reasoning_effort,            vec![                key_hint::alt(KeyCode::Char('.')),                key_hint::shift(KeyCode::Up),            ]        );        assert_eq!(            runtime.chat.edit_queued_message,            vec![key_hint::alt(KeyCode::Up), key_hint::shift(KeyCode::Left)]        );        assert_eq!(            runtime.composer.history_search_previous,            vec![key_hint::ctrl(KeyCode::Char('r'))]        );        assert_eq!(            runtime.composer.history_search_next,            vec![key_hint::ctrl(KeyCode::Char('s'))]        );        assert_eq!(runtime.editor.kill_whole_line, Vec::new());    }    #[test]    fn defaults_include_list_page_and_jump_actions() {        let runtime = RuntimeKeymap::defaults();        assert_eq!(            runtime.list.move_up,            vec![                key_hint::plain(KeyCode::Up),                key_hint::ctrl(KeyCode::Char('p')),                key_hint::ctrl(KeyCode::Char('k')),                key_hint::plain(KeyCode::Char('k')),            ]        );        assert_eq!(            runtime.list.move_down,            vec![                key_hint::plain(KeyCode::Down),                key_hint::ctrl(KeyCode::Char('n')),                key_hint::ctrl(KeyCode::Char('j')),                key_hint::plain(KeyCode::Char('j')),            ]        );        assert_eq!(            runtime.list.move_left,            vec![                key_hint::plain(KeyCode::Left),                key_hint::ctrl(KeyCode::Char('h')),            ]        );        assert_eq!(            runtime.list.move_right,            vec![                key_hint::plain(KeyCode::Right),                key_hint::ctrl(KeyCode::Char('l')),            ]        );        assert_eq!(            runtime.list.page_up,            vec![                key_hint::plain(KeyCode::PageUp),                key_hint::ctrl(KeyCode::Char('b')),            ]        );        assert_eq!(            runtime.list.page_down,            vec![                key_hint::plain(KeyCode::PageDown),                key_hint::ctrl(KeyCode::Char('f')),            ]        );        assert_eq!(runtime.list.jump_top, vec![key_hint::plain(KeyCode::Home)]);        assert_eq!(            runtime.list.jump_bottom,            vec![key_hint::plain(KeyCode::End)]        );    }    #[test]    fn configured_main_surface_bindings_prune_reasoning_fallback_aliases() {        let mut keymap = TuiKeymap::default();        keymap.editor.move_up = Some(one("shift-up"));        keymap.vim_text_object.word = Some(one("shift-down"));        let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");        assert_eq!(runtime.editor.move_up, vec![key_hint::shift(KeyCode::Up)]);        assert_eq!(            runtime.vim_text_object.word,            vec![key_hint::shift(KeyCode::Down)]        );        assert_eq!(            runtime.chat.decrease_reasoning_effort,            vec![key_hint::alt(KeyCode::Char(','))]        );        assert_eq!(            runtime.chat.increase_reasoning_effort,            vec![key_hint::alt(KeyCode::Char('.'))]        );    }    #[test]    fn explicit_reasoning_binding_still_conflicts_with_editor_binding() {        let mut keymap = TuiKeymap::default();        keymap.editor.move_up = Some(one("shift-up"));        keymap.chat.increase_reasoning_effort = Some(one("shift-up"));        expect_conflict(&keymap, "chat.increase_reasoning_effort", "editor.move_up");    }    #[test]    fn configured_legacy_list_bindings_prune_new_default_overlaps() {        let mut keymap = TuiKeymap::default();        keymap.list.move_up = Some(one("page-up"));        keymap.list.move_down = Some(one("page-down"));        let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");        assert_eq!(runtime.list.move_up, vec![key_hint::plain(KeyCode::PageUp)]);        assert_eq!(            runtime.list.move_down,            vec![key_hint::plain(KeyCode::PageDown)]        );        assert_eq!(            runtime.list.page_up,            vec![key_hint::ctrl(KeyCode::Char('b'))]        );        assert_eq!(            runtime.list.page_down,            vec![key_hint::ctrl(KeyCode::Char('f'))]        );    }    #[test]    fn configured_legacy_list_bindings_can_prune_all_new_default_keys() {        let mut keymap = TuiKeymap::default();        keymap.list.move_up = Some(KeybindingsSpec::Many(vec![            KeybindingSpec("page-up".to_string()),            KeybindingSpec("ctrl-b".to_string()),        ]));        let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");        assert_eq!(            runtime.list.move_up,            vec![                key_hint::plain(KeyCode::PageUp),                key_hint::ctrl(KeyCode::Char('b')),            ]        );        assert_eq!(runtime.list.page_up, Vec::new());    }    #[test]    fn explicit_new_list_bindings_still_conflict_with_legacy_bindings() {        let mut keymap = TuiKeymap::default();        keymap.list.move_up = Some(one("page-up"));        keymap.list.page_up = Some(one("page-up"));        expect_conflict(&keymap, "move_up", "page_up");    }    #[test]    fn configured_app_bindings_prune_new_list_default_overlaps() {        let mut keymap = TuiKeymap::default();        keymap.global.copy = Some(one("page-down"));        let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");        assert_eq!(runtime.app.copy, vec![key_hint::plain(KeyCode::PageDown)]);        assert_eq!(            runtime.list.page_down,            vec![key_hint::ctrl(KeyCode::Char('f'))]        );    }    #[test]    fn configured_approval_bindings_prune_new_list_default_overlaps() {        let mut keymap = TuiKeymap::default();        keymap.approval.approve = Some(one("home"));        let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");        assert_eq!(            runtime.approval.approve,            vec![key_hint::plain(KeyCode::Home)]        );        assert_eq!(runtime.list.jump_top, Vec::new());    }    #[test]    fn explicit_new_list_bindings_still_conflict_with_configured_approval_bindings() {        let mut keymap = TuiKeymap::default();        keymap.approval.approve = Some(one("home"));        keymap.list.jump_top = Some(one("home"));        expect_conflict(&keymap, "list.jump_top", "approval.approve");    }    #[test]    fn configured_legacy_vim_normal_bindings_prune_new_change_operator_default() {        let mut keymap = TuiKeymap::default();        keymap.vim_normal.move_left = Some(one("c"));        let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");        assert_eq!(            runtime.vim_normal.move_left,            vec![key_hint::plain(KeyCode::Char('c'))]        );        assert_eq!(runtime.vim_normal.start_change_operator, Vec::new());    }    #[test]    fn explicit_new_vim_normal_binding_still_conflicts_with_legacy_binding() {        let mut keymap = TuiKeymap::default();        keymap.vim_normal.move_left = Some(one("c"));        keymap.vim_normal.start_change_operator = Some(one("c"));        expect_conflict(&keymap, "move_left", "start_change_operator");    }    #[test]    fn configured_legacy_vim_normal_bindings_prune_new_substitute_default() {        let mut keymap = TuiKeymap::default();        keymap.vim_normal.move_left = Some(one("s"));        let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");        assert_eq!(            runtime.vim_normal.move_left,            vec![key_hint::plain(KeyCode::Char('s'))]        );        assert_eq!(runtime.vim_normal.substitute_char, Vec::new());    }    #[test]    fn explicit_new_vim_normal_substitute_binding_still_conflicts_with_legacy_binding() {        let mut keymap = TuiKeymap::default();        keymap.vim_normal.move_left = Some(one("s"));        keymap.vim_normal.substitute_char = Some(one("s"));        expect_conflict(&keymap, "move_left", "substitute_char");    }    #[test]    fn configured_legacy_vim_operator_bindings_prune_new_text_object_defaults() {        let mut keymap = TuiKeymap::default();        keymap.vim_operator.motion_left = Some(one("i"));        keymap.vim_operator.motion_right = Some(one("a"));        let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");        assert_eq!(            runtime.vim_operator.motion_left,            vec![key_hint::plain(KeyCode::Char('i'))]        );        assert_eq!(            runtime.vim_operator.motion_right,            vec![key_hint::plain(KeyCode::Char('a'))]        );        assert_eq!(runtime.vim_operator.select_inner_text_object, Vec::new());        assert_eq!(runtime.vim_operator.select_around_text_object, Vec::new());    }    #[test]    fn explicit_new_vim_operator_binding_still_conflicts_with_legacy_binding() {        let mut keymap = TuiKeymap::default();        keymap.vim_operator.motion_left = Some(one("i"));        keymap.vim_operator.select_inner_text_object = Some(one("i"));        expect_conflict(&keymap, "motion_left", "select_inner_text_object");    }    #[test]    fn vim_normal_defaults_include_insert_and_arrow_aliases() {        let runtime = RuntimeKeymap::defaults();        assert_eq!(            runtime.vim_normal.enter_insert,            vec![                key_hint::plain(KeyCode::Char('i')),                key_hint::plain(KeyCode::Insert)            ]        );        assert_eq!(            runtime.vim_normal.move_left,            vec![                key_hint::plain(KeyCode::Char('h')),                key_hint::plain(KeyCode::Left)            ]        );        assert_eq!(            runtime.vim_normal.move_right,            vec![                key_hint::plain(KeyCode::Char('l')),                key_hint::plain(KeyCode::Right)            ]        );        assert_eq!(            runtime.vim_normal.move_up,            vec![                key_hint::plain(KeyCode::Char('k')),                key_hint::plain(KeyCode::Up)            ]        );        assert_eq!(            runtime.vim_normal.move_down,            vec![                key_hint::plain(KeyCode::Char('j')),                key_hint::plain(KeyCode::Down)            ]        );    }    #[test]    fn invalid_global_copy_binding_reports_global_path() {        let mut keymap = TuiKeymap::default();        keymap.global.copy = Some(one("meta-o"));        let err = RuntimeKeymap::from_config(&keymap).expect_err("expected parse error");        assert!(err.contains("tui.keymap.global.copy"));    }    #[test]    fn rejects_conflicting_editor_bindings() {        let mut keymap = TuiKeymap::default();        keymap.editor.move_left = Some(one("ctrl-h"));        keymap.editor.move_right = Some(one("ctrl-h"));        expect_conflict(&keymap, "move_left", "move_right");    }    #[test]    fn rejects_conflicting_pager_bindings() {        let mut keymap = TuiKeymap::default();        keymap.pager.scroll_up = Some(one("ctrl-u"));        keymap.pager.scroll_down = Some(one("ctrl-u"));        expect_conflict(&keymap, "scroll_up", "scroll_down");    }    #[test]    fn rejects_conflicting_list_bindings() {        let mut keymap = TuiKeymap::default();        keymap.list.move_up = Some(one("up"));        keymap.list.move_down = Some(one("up"));        expect_conflict(&keymap, "move_up", "move_down");        let mut keymap = TuiKeymap::default();        keymap.list.move_left = Some(one("left"));        keymap.list.move_right = Some(one("left"));        expect_conflict(&keymap, "move_left", "move_right");    }    #[test]    fn rejects_conflicting_list_page_and_jump_bindings() {        let mut keymap = TuiKeymap::default();        keymap.list.page_up = Some(one("home"));        keymap.list.jump_top = Some(one("home"));        expect_conflict(&keymap, "page_up", "jump_top");    }    #[test]    fn rejects_conflicting_approval_bindings() {        let mut keymap = TuiKeymap::default();        keymap.approval.approve = Some(one("y"));        keymap.approval.decline = Some(one("y"));        expect_conflict(&keymap, "approve", "decline");    }    #[test]    fn rejects_conflicting_approval_deny_binding() {        let mut keymap = TuiKeymap::default();        keymap.approval.approve = Some(one("y"));        keymap.approval.deny = Some(one("y"));        expect_conflict(&keymap, "approve", "deny");    }    #[test]    fn rejects_conflicting_approval_overlay_accept_binding() {        let mut keymap = TuiKeymap::default();        keymap.list.accept = Some(one("y"));        expect_conflict(&keymap, "list.accept", "approval.approve");    }    #[test]    fn rejects_conflicting_approval_overlay_cancel_binding() {        let mut keymap = TuiKeymap::default();        keymap.list.cancel = Some(one("c"));        expect_conflict(&keymap, "list.cancel", "approval.cancel");    }    #[test]    fn reassignable_fixed_shortcuts_conflict_until_original_action_is_unbound() {        let mut keymap = TuiKeymap::default();        keymap.global.copy = Some(one("alt-."));        expect_conflict(&keymap, "copy", "chat.increase_reasoning_effort");        keymap.chat.increase_reasoning_effort = Some(KeybindingsSpec::Many(vec![]));        let runtime = RuntimeKeymap::from_config(&keymap).expect("remapped key should be free");        assert_eq!(runtime.app.copy, vec![key_hint::alt(KeyCode::Char('.'))]);    }    #[test]    fn kill_whole_line_can_be_assigned_without_default_binding() {        let mut keymap = TuiKeymap::default();        keymap.editor.kill_whole_line = Some(one("ctrl-shift-u"));        let runtime = RuntimeKeymap::from_config(&keymap).expect("runtime keymap");        assert_eq!(            runtime.editor.kill_whole_line,            vec![KeyBinding::new(                KeyCode::Char('u'),                KeyModifiers::CONTROL | KeyModifiers::SHIFT,            )]        );    }    #[test]    fn kill_whole_line_conflicts_until_kill_line_start_is_unbound() {        let mut keymap = TuiKeymap::default();        keymap.editor.kill_whole_line = Some(one("ctrl-u"));        expect_conflict(&keymap, "kill_line_start", "kill_whole_line");        keymap.editor.kill_line_start = Some(KeybindingsSpec::Many(vec![]));        let runtime = RuntimeKeymap::from_config(&keymap).expect("remapped key should be free");        assert_eq!(            runtime.editor.kill_whole_line,            vec![key_hint::ctrl(KeyCode::Char('u'))]        );    }    #[test]    fn toggle_fast_mode_can_be_assigned_without_default_binding() {        let mut keymap = TuiKeymap::default();        keymap.global.toggle_fast_mode = Some(one("ctrl-shift-f"));        let runtime = RuntimeKeymap::from_config(&keymap).expect("runtime keymap");        assert_eq!(            runtime.app.toggle_fast_mode,            vec![KeyBinding::new(                KeyCode::Char('f'),                KeyModifiers::CONTROL | KeyModifiers::SHIFT,            )]        );    }    #[test]    fn toggle_fast_mode_conflicts_with_existing_main_surface_bindings() {        let mut keymap = TuiKeymap::default();        keymap.global.toggle_fast_mode = Some(one("ctrl-l"));        expect_conflict(&keymap, "clear_terminal", "toggle_fast_mode");    }    #[test]    fn rejects_main_bindings_that_collide_with_remaining_fixed_shortcuts() {        let mut keymap = TuiKeymap::default();        keymap.composer.submit = Some(one("ctrl-v"));        expect_conflict(&keymap, "composer.submit", "fixed.paste_image");    }    #[test]    fn interrupt_turn_allows_backtrack_escape_and_can_be_remapped_or_unbound() {        let mut keymap = TuiKeymap::default();        let runtime = RuntimeKeymap::from_config(&keymap).expect("default keymap should parse");        assert_eq!(            runtime.chat.interrupt_turn,            vec![key_hint::plain(KeyCode::Esc)]        );        keymap.chat.interrupt_turn = Some(one("f12"));        let runtime = RuntimeKeymap::from_config(&keymap).expect("remapped keymap should parse");        assert_eq!(            runtime.chat.interrupt_turn,            vec![key_hint::plain(KeyCode::F(12))]        );        keymap.chat.interrupt_turn = Some(KeybindingsSpec::Many(vec![]));        let runtime = RuntimeKeymap::from_config(&keymap).expect("unbound keymap should parse");        assert!(runtime.chat.interrupt_turn.is_empty());    }    #[test]    fn interrupt_turn_rejects_other_fixed_shortcuts() {        let mut keymap = TuiKeymap::default();        keymap.chat.interrupt_turn = Some(one("ctrl-v"));        expect_conflict(&keymap, "chat.interrupt_turn", "fixed.paste_image");    }    #[test]    fn interrupt_turn_rejects_request_user_input_question_navigation_bindings() {        let mut keymap = TuiKeymap::default();        keymap.chat.interrupt_turn = Some(one("f12"));        keymap.list.move_right = Some(one("f12"));        expect_conflict(&keymap, "chat.interrupt_turn", "list.move_right");    }    #[test]    fn rejects_pager_bindings_that_collide_with_transcript_backtrack_keys() {        let mut keymap = TuiKeymap::default();        keymap.pager.close = Some(one("left"));        expect_conflict(&keymap, "close", "fixed.transcript_edit_previous");    }    #[test]    fn parses_function_keys_and_rejects_out_of_range_function_keys() {        assert_eq!(            parse_keybinding("f1").map(|binding| binding.parts()),            Some((KeyCode::F(1), KeyModifiers::NONE))        );        assert_eq!(            parse_keybinding("f24").map(|binding| binding.parts()),            Some((KeyCode::F(24), KeyModifiers::NONE))        );        assert_eq!(parse_keybinding("f25"), None);    }    #[test]    fn parses_all_named_non_character_keys() {        let cases = [            ("tab", KeyCode::Tab),            ("backspace", KeyCode::Backspace),            ("esc", KeyCode::Esc),            ("delete", KeyCode::Delete),            ("up", KeyCode::Up),            ("down", KeyCode::Down),            ("left", KeyCode::Left),            ("right", KeyCode::Right),            ("home", KeyCode::Home),            ("end", KeyCode::End),            ("page-up", KeyCode::PageUp),            ("page-down", KeyCode::PageDown),            ("space", KeyCode::Char(' ')),            ("minus", KeyCode::Char('-')),        ];        for (spec, expected_key) in cases {            assert_eq!(                parse_keybinding(spec).map(|binding| binding.parts()),                Some((expected_key, KeyModifiers::NONE)),                "failed to parse {spec}"            );        }    }    #[test]    fn rejects_modifier_only_and_nonnumeric_function_key_specs() {        assert_eq!(parse_keybinding("ctrl"), None);        assert_eq!(parse_keybinding("ff"), None);    }    #[test]    fn parses_minus_alias_and_legacy_literal_minus() {        assert_eq!(            parse_keybinding("alt-minus").map(|binding| binding.parts()),            Some((KeyCode::Char('-'), KeyModifiers::ALT))        );        assert_eq!(            parse_keybinding("alt--").map(|binding| binding.parts()),            Some((KeyCode::Char('-'), KeyModifiers::ALT))        );        assert_eq!(            parse_keybinding("-").map(|binding| binding.parts()),            Some((KeyCode::Char('-'), KeyModifiers::NONE))        );    }    #[test]    fn explicit_empty_array_unbinds_action() {        let mut keymap = TuiKeymap::default();        keymap.composer.toggle_shortcuts = Some(KeybindingsSpec::Many(vec![]));        let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");        assert!(runtime.composer.toggle_shortcuts.is_empty());    }    #[test]    fn raw_output_toggle_defaults_to_alt_r() {        let runtime = RuntimeKeymap::defaults();        assert_eq!(            runtime.app.toggle_raw_output,            vec![key_hint::alt(KeyCode::Char('r'))]        );    }    #[test]    fn raw_output_toggle_can_be_remapped() {        let mut keymap = TuiKeymap::default();        keymap.global.toggle_raw_output = Some(one("f12"));        let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");        assert_eq!(            runtime.app.toggle_raw_output,            vec![key_hint::plain(KeyCode::F(12))]        );    }    #[test]    fn default_editor_insert_newline_includes_current_aliases() {        let runtime = RuntimeKeymap::defaults();        assert_eq!(            runtime.editor.insert_newline,            vec![                key_hint::ctrl(KeyCode::Char('j')),                key_hint::ctrl(KeyCode::Char('m')),                key_hint::plain(KeyCode::Enter),                key_hint::shift(KeyCode::Enter),                key_hint::alt(KeyCode::Enter),            ]        );    }    #[test]    fn default_editor_delete_forward_word_includes_alt_d() {        let runtime = RuntimeKeymap::defaults();        assert!(            runtime                .editor                .delete_forward_word                .contains(&key_hint::alt(KeyCode::Char('d')))        );    }    #[test]    fn default_editor_deletion_includes_modified_backspace_delete_aliases() {        let runtime = RuntimeKeymap::defaults();        assert!(            runtime                .editor                .delete_backward                .contains(&key_hint::shift(KeyCode::Backspace))        );        assert!(            runtime                .editor                .delete_forward                .contains(&key_hint::shift(KeyCode::Delete))        );        assert!(            runtime                .editor                .delete_backward_word                .contains(&key_hint::ctrl(KeyCode::Backspace))        );        assert!(            runtime                .editor                .delete_backward_word                .contains(&KeyBinding::new(                    KeyCode::Backspace,                    KeyModifiers::CONTROL | KeyModifiers::SHIFT                ))        );        assert!(            runtime                .editor                .delete_forward_word                .contains(&key_hint::ctrl(KeyCode::Delete))        );        assert!(            runtime                .editor                .delete_forward_word                .contains(&KeyBinding::new(                    KeyCode::Delete,                    KeyModifiers::CONTROL | KeyModifiers::SHIFT                ))        );    }    #[test]    fn default_composer_toggle_shortcuts_includes_shift_question_mark() {        let runtime = RuntimeKeymap::defaults();        assert!(            runtime                .composer                .toggle_shortcuts                .contains(&key_hint::shift(KeyCode::Char('?')))        );    }    #[test]    fn default_approval_open_fullscreen_includes_ctrl_shift_a() {        let runtime = RuntimeKeymap::defaults();        assert!(runtime.approval.open_fullscreen.contains(&KeyBinding::new(            KeyCode::Char('a'),            KeyModifiers::CONTROL | KeyModifiers::SHIFT        )));    }    #[test]    fn primary_binding_returns_first_or_none() {        let bindings = vec![            key_hint::ctrl(KeyCode::Char('a')),            key_hint::shift(KeyCode::Char('b')),        ];        assert_eq!(            primary_binding(&bindings),            Some(key_hint::ctrl(KeyCode::Char('a')))        );        assert_eq!(primary_binding(&[]), None);    }    #[test]    fn defaults_pass_conflict_validation() {        RuntimeKeymap::defaults()            .validate_conflicts()            .expect("default keymap should be conflict free");    }}