From 2f0726ad6d5939e5e79b4df6fcb71f79a5fdf48f Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Sun, 31 May 2026 15:42:39 -0300 Subject: [PATCH] feat(tui): allow function keys through f24 in keymaps (#25329) ## Why Closes #25006. `tui.keymap` currently rejects `F13` even though Codex's terminal event layer can report higher function keys. This prevents users from using common remappings such as Caps Lock to `F13`. ## What Changed - Define a shared portable upper bound of `F24` for stored TUI keybindings. - Accept `f13` through `f24` in config normalization and runtime parsing. - Allow `/keymap` capture to persist `F13` through `F24`. - Update the unsupported-function-key error and add boundary tests for `F13`, `F24`, and `F25`. ## How to Test 1. Add a binding such as: ```toml [tui.keymap.global] open_transcript = "f13" ``` 2. Start Codex and press the remapped `F13` key. 3. Confirm Codex loads the config without the previous `F1 through F12` error and the action runs. 4. Open `/keymap`, capture `F13` for an action, and confirm the saved binding is `f13`. 5. As a regression check, try to capture `F25` and confirm Codex reports that only `F1` through `F24` can be stored. Targeted tests: - `just test -p codex-config` - `just test -p codex-tui function_keys` Full `just test -p codex-tui` completed with 2,752 passing tests, 4 skipped tests, and two unrelated guardian feature-flag failures: - `app::tests::update_feature_flags_disabling_guardian_clears_review_policy_and_restores_default` - `app::tests::update_feature_flags_disabling_guardian_clears_manual_review_policy_without_history` --- codex-rs/config/src/tui_keymap.rs | 14 ++++++++++++-- codex-rs/config/src/types.rs | 1 + codex-rs/tui/src/keymap.rs | 9 +++++++-- codex-rs/tui/src/keymap_setup.rs | 25 +++++++++++++++++++++---- 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/codex-rs/config/src/tui_keymap.rs b/codex-rs/config/src/tui_keymap.rs index 99cb3e50f..93392f1f7 100644 --- a/codex-rs/config/src/tui_keymap.rs +++ b/codex-rs/config/src/tui_keymap.rs @@ -23,6 +23,9 @@ 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`, @@ -552,14 +555,14 @@ fn normalize_key_name(key: &str, original: &str) -> Result { if let Some(number) = alias.strip_prefix('f') && let Ok(number) = number.parse::() - && (1..=12).contains(&number) + && (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`-`f12`), \ +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." )) @@ -674,4 +677,11 @@ mod tests { 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()); + } } diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index 30d297030..598b7fdee 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -31,6 +31,7 @@ use serde::Serialize; pub use crate::tui_keymap::KeybindingSpec; pub use crate::tui_keymap::KeybindingsSpec; +pub use crate::tui_keymap::MAX_FUNCTION_KEY; pub use crate::tui_keymap::TuiApprovalKeymap; pub use crate::tui_keymap::TuiChatKeymap; pub use crate::tui_keymap::TuiComposerKeymap; diff --git a/codex-rs/tui/src/keymap.rs b/codex-rs/tui/src/keymap.rs index 9a656d846..ac7463277 100644 --- a/codex-rs/tui/src/keymap.rs +++ b/codex-rs/tui/src/keymap.rs @@ -21,6 +21,7 @@ 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; @@ -1939,7 +1940,7 @@ fn parse_keybinding(spec: &str) -> Option { other if other.len() == 1 => KeyCode::Char(char::from(other.as_bytes()[0])), other if other.starts_with('f') => { let number = other[1..].parse::().ok()?; - if (1..=12).contains(&number) { + if (1..=MAX_FUNCTION_KEY).contains(&number) { KeyCode::F(number) } else { return None; @@ -2636,7 +2637,11 @@ mod tests { parse_keybinding("f1").map(|binding| binding.parts()), Some((KeyCode::F(1), KeyModifiers::NONE)) ); - assert_eq!(parse_keybinding("f13"), None); + assert_eq!( + parse_keybinding("f24").map(|binding| binding.parts()), + Some((KeyCode::F(24), KeyModifiers::NONE)) + ); + assert_eq!(parse_keybinding("f25"), None); } #[test] diff --git a/codex-rs/tui/src/keymap_setup.rs b/codex-rs/tui/src/keymap_setup.rs index 8c1292aeb..759938fd3 100644 --- a/codex-rs/tui/src/keymap_setup.rs +++ b/codex-rs/tui/src/keymap_setup.rs @@ -33,6 +33,7 @@ pub(crate) use picker::build_keymap_picker_params_with_filter; use codex_config::types::KeybindingSpec; use codex_config::types::KeybindingsSpec; +use codex_config::types::MAX_FUNCTION_KEY; use codex_config::types::TuiKeymap; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; @@ -737,11 +738,11 @@ fn key_parts_to_config_key_spec( KeyCode::End => "end".to_string(), KeyCode::PageUp => "page-up".to_string(), KeyCode::PageDown => "page-down".to_string(), - KeyCode::F(number) if (1..=12).contains(&number) => format!("f{number}"), + KeyCode::F(number) if (1..=MAX_FUNCTION_KEY).contains(&number) => format!("f{number}"), KeyCode::F(_) => { - return Err( - "Only function keys F1 through F12 can be stored in `tui.keymap`.".to_string(), - ); + return Err(format!( + "Only function keys F1 through F{MAX_FUNCTION_KEY} can be stored in `tui.keymap`." + )); } KeyCode::Char(' ') => "space".to_string(), KeyCode::Char(mut ch) => { @@ -1610,6 +1611,22 @@ mod tests { ); } + #[test] + fn key_capture_serializes_function_keys_through_f24() { + assert_eq!( + key_event_to_config_key_spec(KeyEvent::from(KeyCode::F(13))), + Ok("f13".to_string()) + ); + assert_eq!( + key_event_to_config_key_spec(KeyEvent::from(KeyCode::F(24))), + Ok("f24".to_string()) + ); + assert_eq!( + key_event_to_config_key_spec(KeyEvent::from(KeyCode::F(25))), + Err("Only function keys F1 through F24 can be stored in `tui.keymap`.".to_string()) + ); + } + #[test] fn key_capture_serializes_c0_control_chars_as_ctrl_bindings() { assert_eq!(