diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index 76fb9b9c00..33509874fa 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -19,6 +19,7 @@ use crossterm::cursor::SetCursorStyle; use crossterm::event::DisableBracketedPaste; use crossterm::event::DisableFocusChange; use crossterm::event::EnableBracketedPaste; +#[cfg(not(windows))] use crossterm::event::EnableFocusChange; use crossterm::event::KeyEvent; use crossterm::terminal::EnterAlternateScreen; @@ -62,6 +63,8 @@ mod keyboard_modes; mod terminal_stderr; #[cfg(test)] pub(crate) mod test_support; +#[cfg(any(windows, test))] +mod windows_console; /// Target frame interval for UI redraw scheduling. pub(crate) const TARGET_FRAME_INTERVAL: Duration = frame_rate_limiter::MIN_FRAME_INTERVAL; @@ -123,6 +126,26 @@ mod tests { )); } + #[test] + fn windows_console_input_modes_preserve_original_vt_input_state() { + let input_record_mode = super::windows_console::input_record_mode(/*mode*/ 0x398); + assert_eq!(input_record_mode, 0x198); + assert_eq!( + super::windows_console::restored_input_mode( + input_record_mode, + super::windows_console::VirtualTerminalInput::Enabled, + ), + 0x398 + ); + assert_eq!( + super::windows_console::restored_input_mode( + /*mode*/ 0x198, + super::windows_console::VirtualTerminalInput::Disabled, + ), + 0x198 + ); + } + #[test] fn unfocused_notification_condition_emits_when_unfocused() { assert!(should_emit_notification( @@ -193,6 +216,8 @@ pub fn set_modes() -> Result<()> { execute!(stdout(), EnableBracketedPaste)?; enable_raw_mode()?; + #[cfg(windows)] + windows_console::set_input_record_mode()?; // Enable keyboard enhancement flags so modifiers for keys like Enter are disambiguated. // chat_composer.rs is using a keyboard event listener to enter for any modified keys // to create a new line that require this. @@ -201,7 +226,10 @@ pub fn set_modes() -> Result<()> { // gracefully if unsupported. keyboard_modes::enable_keyboard_enhancement(); + #[cfg(not(windows))] let _ = execute!(stdout(), EnableFocusChange); + #[cfg(windows)] + let _ = execute!(stdout(), DisableFocusChange); Ok(()) } @@ -279,6 +307,10 @@ fn restore_common( { first_error.get_or_insert(err); } + #[cfg(windows)] + if let Err(err) = windows_console::restore_input_mode() { + first_error.get_or_insert(err); + } if let Err(err) = execute!( stdout(), SetCursorStyle::DefaultUserShape, diff --git a/codex-rs/tui/src/tui/event_stream.rs b/codex-rs/tui/src/tui/event_stream.rs index 9a5e416fd6..b805176756 100644 --- a/codex-rs/tui/src/tui/event_stream.rs +++ b/codex-rs/tui/src/tui/event_stream.rs @@ -125,7 +125,21 @@ impl Default for CrosstermEventSource { impl EventSource for CrosstermEventSource { fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(&mut self.get_mut().0).poll_next(cx) + // Crossterm's Windows backend expects Win32 input records. If VT input is inherited or + // restored by another console client, navigation keys arrive as literal escape bytes. + #[cfg(windows)] + let _ = super::windows_console::ensure_input_record_mode(); + + let result = Pin::new(&mut self.get_mut().0).poll_next(cx); + + // EventStream starts its blocking reader before returning Pending, so reassert the mode + // after that transition as well. + #[cfg(windows)] + if result.is_pending() { + let _ = super::windows_console::ensure_input_record_mode(); + } + + result } } diff --git a/codex-rs/tui/src/tui/windows_console.rs b/codex-rs/tui/src/tui/windows_console.rs new file mode 100644 index 0000000000..e328f4ef50 --- /dev/null +++ b/codex-rs/tui/src/tui/windows_console.rs @@ -0,0 +1,104 @@ +const ENABLE_VIRTUAL_TERMINAL_INPUT: u32 = 0x0200; + +#[derive(Clone, Copy)] +pub(super) enum VirtualTerminalInput { + Enabled, + Disabled, +} + +pub(super) fn input_record_mode(mode: u32) -> u32 { + mode & !ENABLE_VIRTUAL_TERMINAL_INPUT +} + +pub(super) fn restored_input_mode(mode: u32, original: VirtualTerminalInput) -> u32 { + match original { + VirtualTerminalInput::Enabled => mode | ENABLE_VIRTUAL_TERMINAL_INPUT, + VirtualTerminalInput::Disabled => input_record_mode(mode), + } +} + +#[cfg(windows)] +static ORIGINAL_VT_INPUT: std::sync::Mutex> = + std::sync::Mutex::new(Vec::new()); + +#[cfg(windows)] +fn current_input_mode() -> Option<(windows_sys::Win32::Foundation::HANDLE, u32)> { + use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; + use windows_sys::Win32::System::Console::GetConsoleMode; + use windows_sys::Win32::System::Console::GetStdHandle; + use windows_sys::Win32::System::Console::STD_INPUT_HANDLE; + + let handle = unsafe { GetStdHandle(STD_INPUT_HANDLE) }; + if handle == INVALID_HANDLE_VALUE || handle == 0 { + return None; + } + + let mut mode = 0; + if unsafe { GetConsoleMode(handle, &mut mode) } == 0 { + return None; + } + + Some((handle, mode)) +} + +#[cfg(windows)] +pub(super) fn set_input_record_mode() -> std::io::Result<()> { + use windows_sys::Win32::System::Console::SetConsoleMode; + + let Some((handle, mode)) = current_input_mode() else { + return Ok(()); + }; + let requested_mode = input_record_mode(mode); + if requested_mode != mode && unsafe { SetConsoleMode(handle, requested_mode) } == 0 { + return Err(std::io::Error::last_os_error()); + } + + let original = if mode & ENABLE_VIRTUAL_TERMINAL_INPUT != 0 { + VirtualTerminalInput::Enabled + } else { + VirtualTerminalInput::Disabled + }; + ORIGINAL_VT_INPUT + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .push(original); + Ok(()) +} + +#[cfg(windows)] +pub(super) fn ensure_input_record_mode() -> std::io::Result<()> { + use windows_sys::Win32::System::Console::SetConsoleMode; + + let Some((handle, mode)) = current_input_mode() else { + return Ok(()); + }; + let requested_mode = input_record_mode(mode); + if requested_mode != mode && unsafe { SetConsoleMode(handle, requested_mode) } == 0 { + return Err(std::io::Error::last_os_error()); + } + + Ok(()) +} + +#[cfg(windows)] +pub(super) fn restore_input_mode() -> std::io::Result<()> { + use windows_sys::Win32::System::Console::SetConsoleMode; + + let mut original_modes = ORIGINAL_VT_INPUT + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let Some(original) = original_modes.last().copied() else { + return Ok(()); + }; + let Some((handle, mode)) = current_input_mode() else { + original_modes.pop(); + return Ok(()); + }; + let requested_mode = restored_input_mode(mode, original); + if requested_mode != mode && unsafe { SetConsoleMode(handle, requested_mode) } == 0 { + return Err(std::io::Error::last_os_error()); + } + + original_modes.pop(); + Ok(()) +}