Codex Handbook
config/src/tui_keymap.rs 687 lines
//! TUI keymap config schema and canonical key-spec normalization.//!//! This module defines the on-disk `[tui.keymap]` contract used by//! `~/.codex/config.toml` and normalizes user-entered key specs into canonical//! forms consumed by runtime keymap resolution in `codex-rs/tui/src/keymap.rs`.//!//! Responsibilities://!//! 1. Define strongly typed config contexts/actions with unknown-field//!    rejection.//! 2. Normalize accepted key aliases into canonical names.//! 3. Reject malformed bindings early with user-facing diagnostics.//!//! Non-responsibilities://!//! 1. Dispatch precedence and conflict validation.//! 2. Input event matching at runtime.use schemars::JsonSchema;use serde::Deserialize;use serde::Deserializer;use serde::Serialize;use serde::de::Error as SerdeError;use std::collections::BTreeMap;/// Highest function key supported by portable TUI keymap configuration.pub const MAX_FUNCTION_KEY: u8 = 24;/// Normalized string representation of a single key event (for example `ctrl-a`).////// The parser accepts a small alias set (for example `escape` -> `esc`,/// `pageup` -> `page-up`) and stores the canonical form.////// This deliberately represents one terminal key event, not a sequence of/// events. A value like `ctrl-x ctrl-s` is not a chord in this schema; adding/// multi-step chords would require a separate runtime state machine.#[derive(Serialize, Debug, Clone, PartialEq, Eq, JsonSchema)]#[serde(transparent)]pub struct KeybindingSpec(#[schemars(with = "String")] pub String);impl KeybindingSpec {    /// Returns the canonical key-spec string (for example `ctrl-a`).    pub fn as_str(&self) -> &str {        self.0.as_str()    }}impl<'de> Deserialize<'de> for KeybindingSpec {    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>    where        D: Deserializer<'de>,    {        let raw = String::deserialize(deserializer)?;        let normalized = normalize_keybinding_spec(&raw).map_err(SerdeError::custom)?;        Ok(Self(normalized))    }}/// One action binding value in config.////// This accepts either:////// 1. A single key spec string (`"ctrl-a"`)./// 2. A list of key spec strings (`["ctrl-a", "alt-a"]`).////// An empty list explicitly unbinds the action in that scope. Because an/// explicit empty list is still a configured value, runtime resolution must not/// fall through to global or built-in defaults for that action.#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]#[serde(untagged)]pub enum KeybindingsSpec {    One(KeybindingSpec),    Many(Vec<KeybindingSpec>),}impl KeybindingsSpec {    /// Returns all configured key specs for one action in declaration order.    ///    /// Callers should preserve this ordering when deriving UI hints so the    /// first binding remains the primary affordance shown to users.    pub fn specs(&self) -> Vec<&KeybindingSpec> {        match self {            Self::One(spec) => vec![spec],            Self::Many(specs) => specs.iter().collect(),        }    }}/// Global keybindings. These are used when a context does not define an override.#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]#[serde(deny_unknown_fields)]#[schemars(deny_unknown_fields)]pub struct TuiGlobalKeymap {    /// Open the transcript overlay.    pub open_transcript: Option<KeybindingsSpec>,    /// Open the external editor for the current draft.    pub open_external_editor: Option<KeybindingsSpec>,    /// Copy the last agent response to the clipboard.    pub copy: Option<KeybindingsSpec>,    /// Clear the terminal UI.    pub clear_terminal: Option<KeybindingsSpec>,    /// Submit the current composer draft.    pub submit: Option<KeybindingsSpec>,    /// Queue the current composer draft while a task is running.    pub queue: Option<KeybindingsSpec>,    /// Toggle the composer shortcut overlay.    pub toggle_shortcuts: Option<KeybindingsSpec>,    /// Toggle Vim mode for the composer input.    pub toggle_vim_mode: Option<KeybindingsSpec>,    /// Toggle Fast mode.    pub toggle_fast_mode: Option<KeybindingsSpec>,    /// Toggle raw scrollback mode for copy-friendly transcript selection.    pub toggle_raw_output: Option<KeybindingsSpec>,}/// Chat context keybindings.#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]#[serde(deny_unknown_fields)]#[schemars(deny_unknown_fields)]pub struct TuiChatKeymap {    /// Interrupt the active turn.    pub interrupt_turn: Option<KeybindingsSpec>,    /// Decrease the active reasoning effort.    pub decrease_reasoning_effort: Option<KeybindingsSpec>,    /// Increase the active reasoning effort.    pub increase_reasoning_effort: Option<KeybindingsSpec>,    /// Edit the most recently queued message.    pub edit_queued_message: Option<KeybindingsSpec>,}/// Composer context keybindings. These override corresponding `global` actions.#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]#[serde(deny_unknown_fields)]#[schemars(deny_unknown_fields)]pub struct TuiComposerKeymap {    /// Submit the current composer draft.    pub submit: Option<KeybindingsSpec>,    /// Queue the current composer draft while a task is running.    pub queue: Option<KeybindingsSpec>,    /// Toggle the composer shortcut overlay.    pub toggle_shortcuts: Option<KeybindingsSpec>,    /// Open reverse history search or move to the previous match.    pub history_search_previous: Option<KeybindingsSpec>,    /// Move to the next match in reverse history search.    pub history_search_next: Option<KeybindingsSpec>,}/// Editor context keybindings for text editing inside text areas.#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]#[serde(deny_unknown_fields)]#[schemars(deny_unknown_fields)]pub struct TuiEditorKeymap {    /// Insert a newline in the editor.    pub insert_newline: Option<KeybindingsSpec>,    /// Move cursor left by one grapheme.    pub move_left: Option<KeybindingsSpec>,    /// Move cursor right by one grapheme.    pub move_right: Option<KeybindingsSpec>,    /// Move cursor up one visual line.    pub move_up: Option<KeybindingsSpec>,    /// Move cursor down one visual line.    pub move_down: Option<KeybindingsSpec>,    /// Move cursor to beginning of previous word.    pub move_word_left: Option<KeybindingsSpec>,    /// Move cursor to end of next word.    pub move_word_right: Option<KeybindingsSpec>,    /// Move cursor to beginning of line.    pub move_line_start: Option<KeybindingsSpec>,    /// Move cursor to end of line.    pub move_line_end: Option<KeybindingsSpec>,    /// Delete one grapheme to the left.    pub delete_backward: Option<KeybindingsSpec>,    /// Delete one grapheme to the right.    pub delete_forward: Option<KeybindingsSpec>,    /// Delete the previous word.    pub delete_backward_word: Option<KeybindingsSpec>,    /// Delete the next word.    pub delete_forward_word: Option<KeybindingsSpec>,    /// Kill text from cursor to line start.    pub kill_line_start: Option<KeybindingsSpec>,    /// Kill the current line.    pub kill_whole_line: Option<KeybindingsSpec>,    /// Kill text from cursor to line end.    pub kill_line_end: Option<KeybindingsSpec>,    /// Yank the kill buffer.    pub yank: Option<KeybindingsSpec>,}/// Vim normal-mode keybindings for modal editing inside text areas.////// Actions that use uppercase letters (like `A` for append-line-end) should/// be specified as `shift-a` in config; the runtime matcher handles/// cross-terminal shift-reporting differences automatically.#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]#[schemars(deny_unknown_fields)]pub struct TuiVimNormalKeymap {    /// Enter insert mode at cursor (`i`).    pub enter_insert: Option<KeybindingsSpec>,    /// Enter insert mode after cursor (`a`).    pub append_after_cursor: Option<KeybindingsSpec>,    /// Enter insert mode at end of line (`A`).    pub append_line_end: Option<KeybindingsSpec>,    /// Enter insert mode at first non-blank of line (`I`).    pub insert_line_start: Option<KeybindingsSpec>,    /// Open a new line below and enter insert mode (`o`).    pub open_line_below: Option<KeybindingsSpec>,    /// Open a new line above and enter insert mode (`O`).    pub open_line_above: Option<KeybindingsSpec>,    /// Move cursor left (`h`).    pub move_left: Option<KeybindingsSpec>,    /// Move cursor right (`l`).    pub move_right: Option<KeybindingsSpec>,    /// Move cursor up (`k`), or recall older composer history at history boundaries.    pub move_up: Option<KeybindingsSpec>,    /// Move cursor down (`j`), or recall newer composer history at history boundaries.    pub move_down: Option<KeybindingsSpec>,    /// Move cursor to start of next word (`w`).    pub move_word_forward: Option<KeybindingsSpec>,    /// Move cursor to start of previous word (`b`).    pub move_word_backward: Option<KeybindingsSpec>,    /// Move cursor to end of current/next word (`e`).    pub move_word_end: Option<KeybindingsSpec>,    /// Move cursor to start of line (`0`).    pub move_line_start: Option<KeybindingsSpec>,    /// Move cursor to end of line (`$`).    pub move_line_end: Option<KeybindingsSpec>,    /// Delete character under cursor (`x`).    pub delete_char: Option<KeybindingsSpec>,    /// Delete character under cursor and enter insert mode (`s`).    pub substitute_char: Option<KeybindingsSpec>,    /// Delete from cursor to end of line (`D`).    pub delete_to_line_end: Option<KeybindingsSpec>,    /// Change from cursor to end of line and enter insert mode (`C`).    pub change_to_line_end: Option<KeybindingsSpec>,    /// Yank the entire line (`Y`).    pub yank_line: Option<KeybindingsSpec>,    /// Paste after cursor (`p`).    pub paste_after: Option<KeybindingsSpec>,    /// Begin delete operator; next key selects motion (`d`).    pub start_delete_operator: Option<KeybindingsSpec>,    /// Begin yank operator; next key selects motion (`y`).    pub start_yank_operator: Option<KeybindingsSpec>,    /// Begin change operator; next keys select a text object.    pub start_change_operator: Option<KeybindingsSpec>,    /// Cancel a pending operator and return to normal mode.    pub cancel_operator: Option<KeybindingsSpec>,}/// Vim operator-pending keybindings for modal editing inside text areas.////// This context is active only while waiting for a motion after `d` or `y`./// Repeating the operator key (`dd`, `yy`) targets the entire line. Pressing/// `Esc` cancels the pending operator and returns to normal mode without/// modifying text.#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]#[schemars(deny_unknown_fields)]pub struct TuiVimOperatorKeymap {    /// Repeat delete operator to delete the whole line (`dd`).    pub delete_line: Option<KeybindingsSpec>,    /// Repeat yank operator to yank the whole line (`yy`).    pub yank_line: Option<KeybindingsSpec>,    /// Motion: left (`h`).    pub motion_left: Option<KeybindingsSpec>,    /// Motion: right (`l`).    pub motion_right: Option<KeybindingsSpec>,    /// Motion: up one line (`k`).    pub motion_up: Option<KeybindingsSpec>,    /// Motion: down one line (`j`).    pub motion_down: Option<KeybindingsSpec>,    /// Motion: to start of next word (`w`).    pub motion_word_forward: Option<KeybindingsSpec>,    /// Motion: to start of previous word (`b`).    pub motion_word_backward: Option<KeybindingsSpec>,    /// Motion: to end of current/next word (`e`).    pub motion_word_end: Option<KeybindingsSpec>,    /// Motion: to start of line (`0`).    pub motion_line_start: Option<KeybindingsSpec>,    /// Motion: to end of line (`$`).    pub motion_line_end: Option<KeybindingsSpec>,    /// Select an inner text object after an operator.    pub select_inner_text_object: Option<KeybindingsSpec>,    /// Select an around text object after an operator.    pub select_around_text_object: Option<KeybindingsSpec>,    /// Cancel the pending operator and return to normal mode.    pub cancel: Option<KeybindingsSpec>,}/// Vim text-object keybindings for modal editing inside text areas.#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]#[serde(deny_unknown_fields)]#[schemars(deny_unknown_fields)]pub struct TuiVimTextObjectKeymap {    /// Text object: word.    pub word: Option<KeybindingsSpec>,    /// Text object: whitespace-delimited WORD.    pub big_word: Option<KeybindingsSpec>,    /// Text object: parentheses.    pub parentheses: Option<KeybindingsSpec>,    /// Text object: brackets.    pub brackets: Option<KeybindingsSpec>,    /// Text object: braces.    pub braces: Option<KeybindingsSpec>,    /// Text object: double quotes.    pub double_quote: Option<KeybindingsSpec>,    /// Text object: single quotes.    pub single_quote: Option<KeybindingsSpec>,    /// Text object: backticks.    pub backtick: Option<KeybindingsSpec>,    /// Cancel the pending text-object command.    pub cancel: Option<KeybindingsSpec>,}/// Pager context keybindings for transcript and static overlays.#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]#[serde(deny_unknown_fields)]#[schemars(deny_unknown_fields)]pub struct TuiPagerKeymap {    /// Scroll up by one row.    pub scroll_up: Option<KeybindingsSpec>,    /// Scroll down by one row.    pub scroll_down: Option<KeybindingsSpec>,    /// Scroll up by one page.    pub page_up: Option<KeybindingsSpec>,    /// Scroll down by one page.    pub page_down: Option<KeybindingsSpec>,    /// Scroll up by half a page.    pub half_page_up: Option<KeybindingsSpec>,    /// Scroll down by half a page.    pub half_page_down: Option<KeybindingsSpec>,    /// Jump to the beginning.    pub jump_top: Option<KeybindingsSpec>,    /// Jump to the end.    pub jump_bottom: Option<KeybindingsSpec>,    /// Close the pager overlay.    pub close: Option<KeybindingsSpec>,    /// Close the transcript overlay via its dedicated toggle key.    pub close_transcript: Option<KeybindingsSpec>,}/// List selection context keybindings for popup-style selectable lists.#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]#[serde(deny_unknown_fields)]#[schemars(deny_unknown_fields)]pub struct TuiListKeymap {    /// Move list selection up.    pub move_up: Option<KeybindingsSpec>,    /// Move list selection down.    pub move_down: Option<KeybindingsSpec>,    /// Move horizontally left in list pickers that support horizontal actions.    pub move_left: Option<KeybindingsSpec>,    /// Move horizontally right in list pickers that support horizontal actions.    pub move_right: Option<KeybindingsSpec>,    /// Move list selection up by one page.    pub page_up: Option<KeybindingsSpec>,    /// Move list selection down by one page.    pub page_down: Option<KeybindingsSpec>,    /// Jump to the first list item.    pub jump_top: Option<KeybindingsSpec>,    /// Jump to the last list item.    pub jump_bottom: Option<KeybindingsSpec>,    /// Accept current selection.    pub accept: Option<KeybindingsSpec>,    /// Cancel and close selection view.    pub cancel: Option<KeybindingsSpec>,}/// Approval overlay keybindings.#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]#[serde(deny_unknown_fields)]#[schemars(deny_unknown_fields)]pub struct TuiApprovalKeymap {    /// Open the full-screen approval details view.    pub open_fullscreen: Option<KeybindingsSpec>,    /// Open the thread that requested approval when shown from another thread.    pub open_thread: Option<KeybindingsSpec>,    /// Approve the primary option.    pub approve: Option<KeybindingsSpec>,    /// Approve for session when that option exists.    pub approve_for_session: Option<KeybindingsSpec>,    /// Approve with exec-policy prefix when that option exists.    pub approve_for_prefix: Option<KeybindingsSpec>,    /// Deny without providing follow-up guidance.    pub deny: Option<KeybindingsSpec>,    /// Decline and provide corrective guidance.    pub decline: Option<KeybindingsSpec>,    /// Cancel an elicitation request.    pub cancel: Option<KeybindingsSpec>,}/// Raw keymap configuration from `[tui.keymap]`.////// Each context contains action-level overrides. Missing actions inherit from/// built-in defaults, and selected chat/composer actions can fall back/// through `global` during runtime resolution.////// This type is intentionally a persistence shape, not the structure used by/// input handlers. Runtime consumers should resolve it into/// `RuntimeKeymap` first so precedence, empty-list unbinding, and duplicate-key/// validation are applied consistently.#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]#[serde(deny_unknown_fields)]#[schemars(deny_unknown_fields)]pub struct TuiKeymap {    #[serde(default)]    pub global: TuiGlobalKeymap,    #[serde(default)]    pub chat: TuiChatKeymap,    #[serde(default)]    pub composer: TuiComposerKeymap,    #[serde(default)]    pub editor: TuiEditorKeymap,    #[serde(default)]    pub vim_normal: TuiVimNormalKeymap,    #[serde(default)]    pub vim_operator: TuiVimOperatorKeymap,    #[serde(default)]    pub vim_text_object: TuiVimTextObjectKeymap,    #[serde(default)]    pub pager: TuiPagerKeymap,    #[serde(default)]    pub list: TuiListKeymap,    #[serde(default)]    pub approval: TuiApprovalKeymap,}/// Normalize one user-entered key spec into canonical storage format.////// The output always orders modifiers as `ctrl-alt-shift-<key>` when present/// and applies accepted aliases (`escape` -> `esc`, `pageup` -> `page-up`)./// Inputs that cannot be represented unambiguously are rejected.////// Normalization happens at config-deserialization time so downstream runtime/// code only has to parse one spelling for each key. Callers should not bypass/// this function when accepting user-authored key specs, or otherwise equivalent/// keys can fail to compare equal in tests, UI hints, and duplicate detection.fn normalize_keybinding_spec(raw: &str) -> Result<String, String> {    let lower = raw.trim().to_ascii_lowercase();    if lower.is_empty() {        return Err(            "keybinding cannot be empty. Use values like `ctrl-a` or `shift-enter`.\n\See the Codex keymap documentation for supported actions and examples."                .to_string(),        );    }    let segments: Vec<&str> = lower        .split('-')        .filter(|segment| !segment.is_empty())        .collect();    if segments.is_empty() {        return Err(format!(            "invalid keybinding `{raw}`. Use values like `ctrl-a`, `shift-enter`, or `page-down`."        ));    }    let mut modifiers =        BTreeMap::<&str, bool>::from([("ctrl", false), ("alt", false), ("shift", false)]);    let mut key_segments = Vec::new();    let mut saw_key = false;    for segment in segments {        let canonical_mod = match segment {            "ctrl" | "control" => Some("ctrl"),            "alt" | "option" => Some("alt"),            "shift" => Some("shift"),            _ => None,        };        if !saw_key && let Some(modifier) = canonical_mod {            if modifiers.get(modifier).copied().unwrap_or(false) {                return Err(format!(                    "duplicate modifier in keybinding `{raw}`. Use each modifier at most once."                ));            }            modifiers.insert(modifier, true);            continue;        }        saw_key = true;        key_segments.push(segment);    }    if key_segments.is_empty() {        return Err(format!(            "missing key in keybinding `{raw}`. Add a key name like `a`, `enter`, or `page-down`."        ));    }    if key_segments        .iter()        .any(|segment| matches!(*segment, "ctrl" | "control" | "alt" | "option" | "shift"))    {        return Err(format!(            "invalid keybinding `{raw}`: modifiers must come before the key (for example `ctrl-a`)."        ));    }    let key = normalize_key_name(&key_segments.join("-"), raw)?;    let mut normalized = Vec::new();    if modifiers.get("ctrl").copied().unwrap_or(false) {        normalized.push("ctrl".to_string());    }    if modifiers.get("alt").copied().unwrap_or(false) {        normalized.push("alt".to_string());    }    if modifiers.get("shift").copied().unwrap_or(false) {        normalized.push("shift".to_string());    }    normalized.push(key);    Ok(normalized.join("-"))}/// Normalize and validate one key name segment.////// This accepts a constrained key vocabulary to keep runtime parser behavior/// deterministic across platforms.fn normalize_key_name(key: &str, original: &str) -> Result<String, String> {    let alias = match key {        "escape" => "esc",        "return" => "enter",        "spacebar" => "space",        "pgup" | "pageup" => "page-up",        "pgdn" | "pagedown" => "page-down",        "del" => "delete",        other => other,    };    if alias.len() == 1 {        let ch = alias.chars().next().unwrap_or_default();        if ch.is_ascii() && !ch.is_ascii_control() && ch != '-' {            return Ok(alias.to_string());        }    }    if matches!(        alias,        "enter"            | "tab"            | "backspace"            | "esc"            | "delete"            | "up"            | "down"            | "left"            | "right"            | "home"            | "end"            | "page-up"            | "page-down"            | "space"            | "minus"    ) {        return Ok(alias.to_string());    }    if let Some(number) = alias.strip_prefix('f')        && let Ok(number) = number.parse::<u8>()        && (1..=MAX_FUNCTION_KEY).contains(&number)    {        return Ok(alias.to_string());    }    Err(format!(        "unknown key `{key}` in keybinding `{original}`. \Use a printable character (for example `a`), function keys (`f1`-`f{MAX_FUNCTION_KEY}`), \or one of: enter, tab, backspace, esc, delete, arrows, home/end, page-up/page-down, space, minus.\n\See the Codex keymap documentation for supported actions and examples."    ))}#[cfg(test)]mod tests {    use super::*;    use pretty_assertions::assert_eq;    #[test]    fn misplaced_action_at_keymap_root_is_rejected() {        // Actions placed directly under [tui.keymap] instead of a context        // sub-table (e.g. [tui.keymap.global]) must produce a parse error,        // not be silently ignored.        let toml_input = r#"            open_transcript = "ctrl-s"        "#;        let result = toml::from_str::<TuiKeymap>(toml_input);        assert!(            result.is_err(),            "expected error for action at keymap root, got: {result:?}"        );    }    #[test]    fn misspelled_action_under_context_is_rejected() {        let toml_input = r#"            [global]            open_transcrip = "ctrl-x"        "#;        let err = toml::from_str::<TuiKeymap>(toml_input)            .expect_err("expected unknown action under context");        assert!(            err.to_string().contains("open_transcrip"),            "expected error to mention misspelled field, got: {err}"        );    }    #[test]    fn misspelled_vim_text_object_action_is_rejected() {        let toml_input = r#"            [vim_text_object]            double_quotes = "shift-quote"        "#;        let err = toml::from_str::<TuiKeymap>(toml_input)            .expect_err("expected unknown vim text object action");        assert!(            err.to_string().contains("double_quotes"),            "expected error to mention misspelled field, got: {err}"        );    }    #[test]    fn removed_backtrack_actions_are_rejected() {        for (context, action) in [            ("global", "edit_previous_message"),            ("global", "confirm_edit_previous_message"),            ("chat", "edit_previous_message"),            ("chat", "confirm_edit_previous_message"),            ("pager", "edit_previous_message"),            ("pager", "edit_next_message"),            ("pager", "confirm_edit_message"),        ] {            let toml_input = format!(                r#"                [{context}]                {action} = "ctrl-x"                "#            );            let err = toml::from_str::<TuiKeymap>(&toml_input)                .expect_err("expected removed backtrack action to be rejected");            assert!(                err.to_string().contains(action),                "expected error to mention removed field {action}, got: {err}"            );        }    }    #[test]    fn action_under_global_context_is_accepted() {        let toml_input = r#"            [global]            open_transcript = "ctrl-s"        "#;        let keymap: TuiKeymap = toml::from_str(toml_input).expect("valid config");        assert!(keymap.global.open_transcript.is_some());    }    #[test]    fn minus_bindings_under_global_context_are_accepted() {        for (spec, expected) in [            (                "minus",                KeybindingsSpec::One(KeybindingSpec("minus".to_string())),            ),            (                "alt-minus",                KeybindingsSpec::One(KeybindingSpec("alt-minus".to_string())),            ),        ] {            let toml_input = format!(                r#"                [global]                open_transcript = "{spec}"                "#            );            let keymap: TuiKeymap = toml::from_str(&toml_input).expect("valid config");            let mut expected_keymap = TuiKeymap::default();            expected_keymap.global.open_transcript = Some(expected);            assert_eq!(keymap, expected_keymap);        }    }    #[test]    fn function_keys_through_f24_are_accepted() {        assert_eq!(normalize_keybinding_spec("F13"), Ok("f13".to_string()));        assert_eq!(normalize_keybinding_spec("f24"), Ok("f24".to_string()));        assert!(normalize_keybinding_spec("f25").is_err());    }}