From 3ccd483824a351f6d092b8ff4132c4c029acad3e Mon Sep 17 00:00:00 2001 From: James Peter Date: Fri, 31 Oct 2025 02:14:21 +1000 Subject: [PATCH 1/2] fix(tui): allow AltGr printable keys on Windows (#5922) --- COMMIT_MESSAGE_ISSUE_5922.txt | 1 + PR_BODY.md | 15 +++---- PR_BODY_ISSUE_5922.md | 10 +++++ code-rs/tui/src/bottom_pane/textarea.rs | 52 ++++++++++++++++++++++++- code-rs/tui/tests/windows_altgr.rs | 34 ++++++++++++++++ 5 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 COMMIT_MESSAGE_ISSUE_5922.txt create mode 100644 PR_BODY_ISSUE_5922.md create mode 100644 code-rs/tui/tests/windows_altgr.rs diff --git a/COMMIT_MESSAGE_ISSUE_5922.txt b/COMMIT_MESSAGE_ISSUE_5922.txt new file mode 100644 index 00000000000..92a39672cc9 --- /dev/null +++ b/COMMIT_MESSAGE_ISSUE_5922.txt @@ -0,0 +1 @@ +fix(tui): allow AltGr printable keys on Windows (#5922) diff --git a/PR_BODY.md b/PR_BODY.md index f9128ce12ac..33944682a6a 100644 --- a/PR_BODY.md +++ b/PR_BODY.md @@ -1,9 +1,10 @@ -## Overview -- AutoRunPhase now carries struct payloads; controller exposes helpers (`is_active`, `is_paused_manual`, `resume_after_submit`, `awaiting_coordinator_submit`, `awaiting_review`, `in_transient_recovery`). -- ChatWidget hot paths (manual pause, coordinator routing, ESC handling, review exit) rely on helpers/`matches!` instead of raw booleans. +## Summary +- allow Windows AltGr combos (Control+Alt) to insert printable characters without swallowing them, while keeping Ctrl+Alt shortcuts intact +- add Windows-only unit tests around `TextArea` and an integration-style ComposerInput test to cover `/`, `@`, and Ctrl+Alt+H +- document the fix via regression tests so future Windows keyboard regressions are caught early -## Tests -- `./build-fast.sh` +## Testing +- ./build-fast.sh +- cargo test -p code-tui --test windows_altgr -- --ignored *(fails: local cargo registry copy of `cc` 1.2.41 is missing generated modules; clear/update the registry and rerun on Windows)* -## Follow-ups -- See `docs/auto-drive-phase-migration-TODO.md` for remaining legacy-flag removals and snapshot coverage. +Closes #5922. diff --git a/PR_BODY_ISSUE_5922.md b/PR_BODY_ISSUE_5922.md new file mode 100644 index 00000000000..dbd1c4ab11b --- /dev/null +++ b/PR_BODY_ISSUE_5922.md @@ -0,0 +1,10 @@ +## Summary +- treat Windows AltGr (Control+Alt) key chords as printable characters so `/`, `@`, and other symbols insert correctly in the composer and terminal cells +- keep Ctrl+Alt shortcuts (e.g., Ctrl+Alt+H delete word) by excluding ASCII letters from the AltGr path and add Windows-only regression tests +- cover the change with new `TextArea` unit tests and a ComposerInput integration-style test to prevent future regressions + +## Testing +- ./build-fast.sh +- cargo test -p code-tui --test windows_altgr -- --ignored *(fails: local cargo registry copy of `cc` 1.2.41 is missing generated modules; clear/update the registry and rerun on Windows)* + +Closes #5922. diff --git a/code-rs/tui/src/bottom_pane/textarea.rs b/code-rs/tui/src/bottom_pane/textarea.rs index f5aecd736f3..66f9975d5c6 100644 --- a/code-rs/tui/src/bottom_pane/textarea.rs +++ b/code-rs/tui/src/bottom_pane/textarea.rs @@ -246,6 +246,20 @@ impl TextArea { code: KeyCode::Char(c), .. } if matches!(c, '\n' | '\r') => self.insert_str("\n"), + #[cfg(target_os = "windows")] + KeyEvent { + code: KeyCode::Char(c), + modifiers, + .. + } if (modifiers == (KeyModifiers::CONTROL | KeyModifiers::ALT) + || modifiers == (KeyModifiers::CONTROL + | KeyModifiers::ALT + | KeyModifiers::SHIFT)) + && !c.is_ascii_control() + && !c.is_ascii_alphabetic() => + { + self.insert_str(&c.to_string()); + } KeyEvent { code: KeyCode::Char(c), // Insert plain characters (and Shift-modified). Do NOT insert when ALT is held, @@ -909,6 +923,43 @@ impl TextArea { } } +#[cfg(all(test, target_os = "windows"))] +mod windows_tests { + use super::*; + use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + + #[test] + fn altgr_control_alt_characters_insert_printables() { + let mut textarea = TextArea::new(); + let cases = [ + ('/', KeyModifiers::CONTROL | KeyModifiers::ALT), + ('@', KeyModifiers::CONTROL | KeyModifiers::ALT), + ('{', KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT), + ]; + + for (ch, modifiers) in cases { + textarea.set_text(""); + textarea.set_cursor(0); + textarea.input(KeyEvent::new(KeyCode::Char(ch), modifiers)); + assert_eq!(textarea.text(), ch.to_string(), "expected AltGr combination to insert {ch}"); + } + } + + #[test] + fn ctrl_alt_letter_shortcut_preserved() { + let mut textarea = TextArea::new(); + textarea.set_text("word"); + textarea.set_cursor(textarea.text().len()); + + textarea.input(KeyEvent::new( + KeyCode::Char('h'), + KeyModifiers::CONTROL | KeyModifiers::ALT, + )); + + assert_eq!(textarea.text(), "", "Ctrl+Alt+H should still delete backward word"); + } +} + impl WidgetRef for &TextArea { fn render_ref(&self, area: Rect, buf: &mut Buffer) { let lines = self.wrapped_lines(area.width); @@ -955,4 +1006,3 @@ impl TextArea { } } } - diff --git a/code-rs/tui/tests/windows_altgr.rs b/code-rs/tui/tests/windows_altgr.rs new file mode 100644 index 00000000000..368d4e9f4ca --- /dev/null +++ b/code-rs/tui/tests/windows_altgr.rs @@ -0,0 +1,34 @@ +#![cfg(target_os = "windows")] + +use code_tui::public_widgets::ComposerInput; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + +#[test] +fn composer_input_altgr_characters_insert_text() { + let mut composer = ComposerInput::new(); + + let cases = [ + ('/', KeyModifiers::CONTROL | KeyModifiers::ALT), + ('@', KeyModifiers::CONTROL | KeyModifiers::ALT), + ('{', KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT), + ]; + + for (ch, modifiers) in cases { + composer.clear(); + let _ = composer.input(KeyEvent::new(KeyCode::Char(ch), modifiers)); + assert_eq!(composer.text(), ch.to_string(), "AltGr input should insert printable character"); + } +} + +#[test] +fn composer_input_ctrl_alt_letter_shortcut_still_deletes_word() { + let mut composer = ComposerInput::new(); + composer.handle_paste("word".to_string()); + + let _ = composer.input(KeyEvent::new( + KeyCode::Char('h'), + KeyModifiers::CONTROL | KeyModifiers::ALT, + )); + + assert!(composer.text().is_empty(), "Ctrl+Alt+H should delete the previous word"); +} From 648993a9bfdf56d94bca59b3894c1d1440a3b271 Mon Sep 17 00:00:00 2001 From: James Peter Date: Fri, 7 Nov 2025 06:17:13 +1000 Subject: [PATCH 2/2] fix(tui/input): avoid ctrl+alt text on non-Windows --- code-rs/tui/src/bottom_pane/textarea.rs | 23 ++++++++++++++-- code-rs/tui/tests/non_windows_shortcuts.rs | 32 ++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 code-rs/tui/tests/non_windows_shortcuts.rs diff --git a/code-rs/tui/src/bottom_pane/textarea.rs b/code-rs/tui/src/bottom_pane/textarea.rs index 66f9975d5c6..719de18360b 100644 --- a/code-rs/tui/src/bottom_pane/textarea.rs +++ b/code-rs/tui/src/bottom_pane/textarea.rs @@ -923,11 +923,12 @@ impl TextArea { } } -#[cfg(all(test, target_os = "windows"))] -mod windows_tests { +#[cfg(test)] +mod tests { use super::*; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + #[cfg(target_os = "windows")] #[test] fn altgr_control_alt_characters_insert_printables() { let mut textarea = TextArea::new(); @@ -958,6 +959,24 @@ mod windows_tests { assert_eq!(textarea.text(), "", "Ctrl+Alt+H should still delete backward word"); } + + #[cfg(not(target_os = "windows"))] + #[test] + fn ctrl_alt_symbol_shortcut_is_ignored_for_text_insertion() { + let mut textarea = TextArea::new(); + textarea.set_text(""); + textarea.set_cursor(0); + + textarea.input(KeyEvent::new( + KeyCode::Char('@'), + KeyModifiers::CONTROL | KeyModifiers::ALT, + )); + + assert!( + textarea.text().is_empty(), + "Ctrl+Alt symbol should not insert printable characters on non-Windows" + ); + } } impl WidgetRef for &TextArea { diff --git a/code-rs/tui/tests/non_windows_shortcuts.rs b/code-rs/tui/tests/non_windows_shortcuts.rs new file mode 100644 index 00000000000..4725384279c --- /dev/null +++ b/code-rs/tui/tests/non_windows_shortcuts.rs @@ -0,0 +1,32 @@ +#![cfg(not(target_os = "windows"))] + +use code_tui::ComposerInput; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + +#[test] +fn composer_ctrl_alt_letter_shortcut_deletes_word() { + let mut composer = ComposerInput::new(); + composer.handle_paste("word".to_string()); + + let _ = composer.input(KeyEvent::new( + KeyCode::Char('h'), + KeyModifiers::CONTROL | KeyModifiers::ALT, + )); + + assert!(composer.text().is_empty(), "Ctrl+Alt+H should delete the previous word"); +} + +#[test] +fn composer_ctrl_alt_symbol_does_not_insert_text() { + let mut composer = ComposerInput::new(); + + let _ = composer.input(KeyEvent::new( + KeyCode::Char('@'), + KeyModifiers::CONTROL | KeyModifiers::ALT, + )); + + assert!( + composer.text().is_empty(), + "Ctrl+Alt symbol should be treated as a shortcut, not inserted text" + ); +}