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

4.3 KiB

PR #2113: fix: update ctrl-z to suspend tui

Description

  • Lean on ctrl-c and esc to interrupt.
  • (Only on unix.)

https://github.com/user-attachments/assets/7ce6c57f-6ee2-40c2-8cd2-b31265f16c1c

Full Diff

diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock
index 4eddf7bd7b..85fae48997 100644
--- a/codex-rs/Cargo.lock
+++ b/codex-rs/Cargo.lock
@@ -881,6 +881,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 719c631149..31f198f543 100644
--- a/codex-rs/tui/Cargo.toml
+++ b/codex-rs/tui/Cargo.toml
@@ -72,6 +72,9 @@ unicode-segmentation = "1.12.0"
 unicode-width = "0.1"
 uuid = "1"
 
+[target.'cfg(unix)'.dependencies]
+libc = "0.2"
+
 
 [dev-dependencies]
 chrono = { version = "0.4", features = ["serde"] }
diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs
index a97948f3ea..51e2542792 100644
--- a/codex-rs/tui/src/app.rs
+++ b/codex-rs/tui/src/app.rs
@@ -256,9 +256,11 @@ impl App<'_> {
                             kind: KeyEventKind::Press,
                             ..
                         } => {
-                            if let AppState::Chat { widget } = &mut self.app_state {
-                                widget.on_ctrl_z();
+                            #[cfg(unix)]
+                            {
+                                self.suspend(terminal)?;
                             }
+                            // No-op on non-Unix platforms.
                         }
                         KeyEvent {
                             code: KeyCode::Char('d'),
@@ -454,6 +456,23 @@ impl App<'_> {
         Ok(())
     }
 
+    #[cfg(unix)]
+    fn suspend(&mut self, terminal: &mut tui::Tui) -> Result<()> {
+        tui::restore()?;
+        // SAFETY: Unix-only code path. We intentionally send SIGTSTP to the
+        // current process group (pid 0) to trigger standard job-control
+        // suspension semantics. This FFI does not involve any raw pointers,
+        // is not called from a signal handler, and uses a constant signal.
+        // Errors from kill are acceptable (e.g., if already stopped) — the
+        // subsequent re-init path will still leave the terminal in a good state.
+        // We considered `nix`, but didn't think it was worth pulling in for this one call.
+        unsafe { libc::kill(0, libc::SIGTSTP) };
+        *terminal = tui::init(&self.config)?;
+        terminal.clear()?;
+        self.app_event_tx.send(AppEvent::RequestRedraw);
+        Ok(())
+    }
+
     pub(crate) fn token_usage(&self) -> codex_core::protocol::TokenUsage {
         match &self.app_state {
             AppState::Chat { widget } => widget.token_usage().clone(),
diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs
index 344f025842..fd28263124 100644
--- a/codex-rs/tui/src/chatwidget.rs
+++ b/codex-rs/tui/src/chatwidget.rs
@@ -599,10 +599,6 @@ impl ChatWidget<'_> {
         }
     }
 
-    pub(crate) fn on_ctrl_z(&mut self) {
-        self.interrupt_running_task();
-    }
-
     pub(crate) fn composer_is_empty(&self) -> bool {
         self.bottom_pane.composer_is_empty()
     }

Review Comments

codex-rs/tui/src/app.rs

@@ -454,6 +456,22 @@ impl App<'_> {
         Ok(())
     }
 
+    #[cfg(unix)]
+    fn suspend(&mut self, terminal: &mut tui::Tui) -> Result<()> {
+        tui::restore()?;
+        // SAFETY: Unix-only code path. We intentionally send SIGTSTP to the
+        // current process group (pid 0) to trigger standard job-control
+        // suspension semantics. This FFI does not involve any raw pointers,
+        // is not called from a signal handler, and uses a constant signal.
+        // Errors from kill are acceptable (e.g., if already stopped) — the
+        // subsequent re-init path will still leave the terminal in a good state.
+        unsafe { libc::kill(0, libc::SIGTSTP) };

@ae-openai from the automated review:

unsafe { libc::kill(...) }: consider switching to nix::sys::signal::kill to avoid manual unsafe.