From 8bebe86a47aebb262643208b8cf242caa275831d Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Mon, 17 Nov 2025 17:18:16 -0600 Subject: [PATCH] Fix TUI issues with Alt-Gr on Windows (#6799) This PR fixes keyboard handling for the Right Alt (aka "Alt-Gr") key on Windows. This key appears on keyboards in Central and Eastern Europe. Codex has effectively never worked for Windows users in these regions because the code didn't properly handle this key, which is used for typing common symbols like `\` and `@`. A few days ago, I merged a [community-authored PR](https://github.com/openai/codex/pull/6720) that supplied a partial fix for this issue. Upon closer inspect, that PR was 1) too broad (not scoped to Windows only) and 2) incomplete (didn't fix all relevant code paths, so paste was still broken). This improvement is based on another [community-provided PR](https://github.com/openai/codex/pull/3241) by @marektomas-cz. He submitted it back in September and later closed it because it didn't receive any attention. This fix addresses the following bugs: #5922, #3046, #3092, #3519, #5684, #5843. --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 7 +++---- codex-rs/tui/src/bottom_pane/textarea.rs | 11 +++++------ codex-rs/tui/src/key_hint.rs | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 446563a19..f64a824f4 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -1,3 +1,4 @@ +use crate::key_hint::has_ctrl_or_alt; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; @@ -1103,8 +1104,7 @@ impl ChatComposer { .. } = input { - let has_ctrl_or_alt = - modifiers.contains(KeyModifiers::CONTROL) || modifiers.contains(KeyModifiers::ALT); + let has_ctrl_or_alt = has_ctrl_or_alt(modifiers); if !has_ctrl_or_alt { // Non-ASCII characters (e.g., from IMEs) can arrive in quick bursts and be // misclassified by paste heuristics. Flush any active burst buffer and insert @@ -1174,8 +1174,7 @@ impl ChatComposer { } = input; match code { KeyCode::Char(_) => { - let has_ctrl_or_alt = modifiers.contains(KeyModifiers::CONTROL) - || modifiers.contains(KeyModifiers::ALT); + let has_ctrl_or_alt = has_ctrl_or_alt(modifiers); if has_ctrl_or_alt { self.paste_burst.clear_window_after_non_char(); } diff --git a/codex-rs/tui/src/bottom_pane/textarea.rs b/codex-rs/tui/src/bottom_pane/textarea.rs index 0a7faa4fc..03de33681 100644 --- a/codex-rs/tui/src/bottom_pane/textarea.rs +++ b/codex-rs/tui/src/bottom_pane/textarea.rs @@ -1,3 +1,4 @@ +use crate::key_hint::is_altgr; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyModifiers; @@ -247,16 +248,13 @@ impl TextArea { } if modifiers == (KeyModifiers::CONTROL | KeyModifiers::ALT) => { self.delete_backward_word() }, + // Windows AltGr generates ALT|CONTROL; treat as a plain character input unless + // we match a specific Control+Alt binding above. KeyEvent { code: KeyCode::Char(c), modifiers, .. - } if modifiers.contains(KeyModifiers::ALT) - && modifiers.contains(KeyModifiers::CONTROL) => - { - // AltGr on many keyboards reports as Ctrl+Alt; treat it as a literal char. - self.insert_str(&c.to_string()); - }, + } if is_altgr(modifiers) => self.insert_str(&c.to_string()), KeyEvent { code: KeyCode::Backspace, modifiers: KeyModifiers::ALT, @@ -1464,6 +1462,7 @@ mod tests { assert_eq!(t.cursor(), 3); } + #[cfg_attr(not(windows), ignore = "AltGr modifier only applies on Windows")] #[test] fn altgr_ctrl_alt_char_inserts_literal() { let mut t = ta_with(""); diff --git a/codex-rs/tui/src/key_hint.rs b/codex-rs/tui/src/key_hint.rs index 1126b8023..6272ab0df 100644 --- a/codex-rs/tui/src/key_hint.rs +++ b/codex-rs/tui/src/key_hint.rs @@ -89,3 +89,19 @@ impl From<&KeyBinding> for Span<'static> { fn key_hint_style() -> Style { Style::default().dim() } + +pub(crate) fn has_ctrl_or_alt(mods: KeyModifiers) -> bool { + (mods.contains(KeyModifiers::CONTROL) || mods.contains(KeyModifiers::ALT)) && !is_altgr(mods) +} + +#[cfg(windows)] +#[inline] +pub(crate) fn is_altgr(mods: KeyModifiers) -> bool { + mods.contains(KeyModifiers::ALT) && mods.contains(KeyModifiers::CONTROL) +} + +#[cfg(not(windows))] +#[inline] +pub(crate) fn is_altgr(_mods: KeyModifiers) -> bool { + false +}