# PR #1766: Supporting both shift+enter and ctrl+j and adding kpp check + unit test - URL: https://github.com/openai/codex/pull/1766 - Author: pap-openai - Created: 2025-07-31 21:05:22 UTC - Updated: 2025-08-01 00:32:14 UTC - Changes: +98/-1, Files changed: 5, Commits: 8 ## Description Apple terminal shows ctrl+j image iTerm that supports KPP shows shift+enter image ## Full Diff ```diff diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 87d59b21be..e6b5c11bd9 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -854,6 +854,7 @@ dependencies = [ "image", "insta", "lazy_static", + "libc", "mcp-types", "path-clean", "pretty_assertions", diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 63d287ca11..e5739ec505 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -32,6 +32,7 @@ color-eyre = "0.6.3" crossterm = { version = "0.28.1", features = ["bracketed-paste"] } image = { version = "^0.25.6", default-features = false, features = ["jpeg"] } lazy_static = "1" +libc = "0.2" mcp-types = { path = "../mcp-types" } path-clean = "1.0.1" ratatui = { version = "0.29.0", features = [ diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 3bc573a003..6b7017f6f0 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -712,11 +712,16 @@ impl WidgetRef for &ChatComposer<'_> { Span::from(" to quit"), ] } else { + let newline_hint = if crate::tui::is_kkp_enabled() { + "Shift+⏎" + } else { + "Ctrl+J" + }; vec![ Span::from(" "), "⏎".set_style(key_hint_style), Span::from(" send "), - "Shift+⏎".set_style(key_hint_style), + newline_hint.set_style(key_hint_style), Span::from(" newline "), "Ctrl+C".set_style(key_hint_style), Span::from(" quit"), @@ -961,6 +966,9 @@ mod tests { use ratatui::Terminal; use ratatui::backend::TestBackend; + // First, run snapshots with KKP enabled so hints show Shift+⏎. + crate::tui::set_kkp_for_tests(true); + let (tx, _rx) = std::sync::mpsc::channel(); let sender = AppEventSender::new(tx); let mut terminal = match Terminal::new(TestBackend::new(100, 10)) { @@ -1005,6 +1013,18 @@ mod tests { assert_snapshot!(name, terminal.backend()); } + + // Also add one snapshot with KKP disabled so we still see Ctrl+J. + crate::tui::set_kkp_for_tests(false); + let mut terminal_ctrlj = match Terminal::new(TestBackend::new(100, 10)) { + Ok(t) => t, + Err(e) => panic!("Failed to create terminal: {e}"), + }; + let composer = ChatComposer::new(true, sender.clone()); + terminal_ctrlj + .draw(|f| f.render_widget_ref(&composer, f.area())) + .unwrap_or_else(|e| panic!("Failed to draw empty_ctrlj composer: {e}")); + assert_snapshot!("empty_ctrlj", terminal_ctrlj.backend()); } #[test] diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty_ctrlj.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty_ctrlj.snap new file mode 100644 index 0000000000..f798f1af16 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty_ctrlj.snap @@ -0,0 +1,14 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal_ctrlj.backend() +--- +"▌ ... " +"▌ " +"▌ " +"▌ " +"▌ " +"▌ " +"▌ " +"▌ " +"▌ " +" ⏎ send Ctrl+J newline Ctrl+C quit " diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index 268483cbcf..81ec9aebdc 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -1,6 +1,8 @@ use std::io::Result; use std::io::Stdout; use std::io::stdout; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; use codex_core::config::Config; use crossterm::event::DisableBracketedPaste; @@ -18,6 +20,60 @@ use crate::custom_terminal::Terminal; /// A type alias for the terminal type used in this application pub type Tui = Terminal>; +// Global flag indicating whether Kitty Keyboard Protocol (KKP) appears enabled. +static KKP_ENABLED: AtomicBool = AtomicBool::new(false); + +/// Return whether KKP (alternate key reporting) appears enabled. +pub(crate) fn is_kkp_enabled() -> bool { + KKP_ENABLED.load(Ordering::Relaxed) +} + +#[cfg(test)] +pub(crate) fn set_kkp_for_tests(value: bool) { + KKP_ENABLED.store(value, Ordering::Relaxed); +} + +/// Try to detect Kitty Keyboard Protocol support by issuing a progressive +/// enhancement query and waiting briefly for a response. +#[cfg(unix)] +fn detect_kitty_protocol() -> std::io::Result { + use std::io::Read; + use std::io::Write; + use std::io::{self}; + use std::os::unix::io::AsRawFd; + + let mut stdout = io::stdout(); + let mut stdin = io::stdin(); + + // Send query for progressive enhancement + DA1 + write!(stdout, "\x1b[?u\x1b[c")?; + stdout.flush()?; + + // Wait up to ~200ms for a response + let fd = stdin.as_raw_fd(); + let mut pfd = libc::pollfd { + fd, + events: libc::POLLIN, + revents: 0, + }; + let rc = unsafe { libc::poll(&mut pfd as *mut libc::pollfd, 1, 200) }; + if rc > 0 && (pfd.revents & libc::POLLIN) != 0 { + let mut buf = [0u8; 256]; + if let Ok(n) = stdin.read(&mut buf) { + let response = String::from_utf8_lossy(&buf[..n]); + if response.contains("[?") && response.contains('u') { + return Ok(true); + } + } + } + Ok(false) +} + +#[cfg(not(unix))] +fn detect_kitty_protocol() -> std::io::Result { + Ok(false) +} + /// Initialize the terminal (inline viewport; history stays in normal scrollback) pub fn init(_config: &Config) -> Result { execute!(stdout(), EnableBracketedPaste)?; @@ -34,6 +90,11 @@ pub fn init(_config: &Config) -> Result { | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS ) )?; + + // Detect KKP availability; used to adjust UI hints in the composer. + let kkp = detect_kitty_protocol().unwrap_or(false); + KKP_ENABLED.store(kkp, Ordering::Relaxed); + set_panic_hook(); let backend = CrosstermBackend::new(stdout()); ``` ## Review Comments ### codex-rs/tui/src/tui.rs - Created: 2025-08-01 00:17:33 UTC | Link: https://github.com/openai/codex/pull/1766#discussion_r2246611560 ```diff @@ -18,6 +20,60 @@ use crate::custom_terminal::Terminal; /// A type alias for the terminal type used in this application pub type Tui = Terminal>; +// Global flag indicating whether Kitty Keyboard Protocol (KKP) appears enabled. +static KKP_ENABLED: AtomicBool = AtomicBool::new(false); ``` > This feels like we are going to end up with flaky tests due to race conditions?