Files
codex/prs/bolinfest/PR-1766.md
2025-09-02 15:17:45 -07:00

8.4 KiB

PR #1766: Supporting both shift+enter and ctrl+j and adding kpp check + unit test

Description

Apple terminal shows ctrl+j

image

iTerm that supports KPP shows shift+enter

image

Full 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<CrosstermBackend<Stdout>>;
 
+// 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<bool> {
+    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<bool> {
+    Ok(false)
+}
+
 /// Initialize the terminal (inline viewport; history stays in normal scrollback)
 pub fn init(_config: &Config) -> Result<Tui> {
     execute!(stdout(), EnableBracketedPaste)?;
@@ -34,6 +90,11 @@ pub fn init(_config: &Config) -> Result<Tui> {
                 | 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

@@ -18,6 +20,60 @@ use crate::custom_terminal::Terminal;
 /// A type alias for the terminal type used in this application
 pub type Tui = Terminal<CrosstermBackend<Stdout>>;
 
+// 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?