mirror of
https://github.com/openai/codex.git
synced 2026-04-28 18:32:04 +03:00
9.1 KiB
9.1 KiB
PR #1599: Implement redraw debounce
- URL: https://github.com/openai/codex/pull/1599
- Author: aibrahim-oai
- Created: 2025-07-17 17:18:15 UTC
- Updated: 2025-07-17 19:55:03 UTC
- Changes: +49/-7, Files changed: 5, Commits: 6
Description
Summary
- debouce redraw events so repeated requests don't overwhelm the terminal
- add
RequestRedrawevent and schedule redraws after 100ms
Testing
cargo clippy --testscargo test(fails: Sandbox Denied errors in landlock tests)
https://chatgpt.com/codex/tasks/task_i_68792a65b8b483218ec90a8f68746cd8
Full Diff
diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs
index ac69bef2e9..397c0dc9a4 100644
--- a/codex-rs/tui/src/app.rs
+++ b/codex-rs/tui/src/app.rs
@@ -18,8 +18,15 @@ use crossterm::event::KeyEvent;
use crossterm::event::MouseEvent;
use crossterm::event::MouseEventKind;
use std::path::PathBuf;
+use std::sync::Arc;
+use std::sync::Mutex;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::channel;
+use std::thread;
+use std::time::Duration;
+
+/// Time window for debouncing redraw requests.
+const REDRAW_DEBOUNCE: Duration = Duration::from_millis(100);
/// Top-level application state: which full-screen view is currently active.
#[allow(clippy::large_enum_variant)]
@@ -46,6 +53,9 @@ pub(crate) struct App<'a> {
file_search: FileSearchManager,
+ /// True when a redraw has been scheduled but not yet executed.
+ pending_redraw: Arc<Mutex<bool>>,
+
/// Stored parameters needed to instantiate the ChatWidget later, e.g.,
/// after dismissing the Git-repo warning.
chat_args: Option<ChatWidgetArgs>,
@@ -70,6 +80,7 @@ impl App<'_> {
) -> Self {
let (app_event_tx, app_event_rx) = channel();
let app_event_tx = AppEventSender::new(app_event_tx);
+ let pending_redraw = Arc::new(Mutex::new(false));
let scroll_event_helper = ScrollEventHelper::new(app_event_tx.clone());
// Spawn a dedicated thread for reading the crossterm event loop and
@@ -83,7 +94,7 @@ impl App<'_> {
app_event_tx.send(AppEvent::KeyEvent(key_event));
}
crossterm::event::Event::Resize(_, _) => {
- app_event_tx.send(AppEvent::Redraw);
+ app_event_tx.send(AppEvent::RequestRedraw);
}
crossterm::event::Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollUp,
@@ -152,6 +163,7 @@ impl App<'_> {
app_state,
config,
file_search,
+ pending_redraw,
chat_args,
}
}
@@ -162,6 +174,29 @@ impl App<'_> {
self.app_event_tx.clone()
}
+ /// Schedule a redraw if one is not already pending.
+ #[allow(clippy::unwrap_used)]
+ fn schedule_redraw(&self) {
+ {
+ #[allow(clippy::unwrap_used)]
+ let mut flag = self.pending_redraw.lock().unwrap();
+ if *flag {
+ return;
+ }
+ *flag = true;
+ }
+
+ let tx = self.app_event_tx.clone();
+ let pending_redraw = self.pending_redraw.clone();
+ thread::spawn(move || {
+ thread::sleep(REDRAW_DEBOUNCE);
+ tx.send(AppEvent::Redraw);
+ #[allow(clippy::unwrap_used)]
+ let mut f = pending_redraw.lock().unwrap();
+ *f = false;
+ });
+ }
+
pub(crate) fn run(
&mut self,
terminal: &mut tui::Tui,
@@ -169,10 +204,13 @@ impl App<'_> {
) -> Result<()> {
// Insert an event to trigger the first render.
let app_event_tx = self.app_event_tx.clone();
- app_event_tx.send(AppEvent::Redraw);
+ app_event_tx.send(AppEvent::RequestRedraw);
while let Ok(event) = self.app_event_rx.recv() {
match event {
+ AppEvent::RequestRedraw => {
+ self.schedule_redraw();
+ }
AppEvent::Redraw => {
self.draw_next_frame(terminal)?;
}
@@ -249,7 +287,7 @@ impl App<'_> {
Vec::new(),
));
self.app_state = AppState::Chat { widget: new_widget };
- self.app_event_tx.send(AppEvent::Redraw);
+ self.app_event_tx.send(AppEvent::RequestRedraw);
}
SlashCommand::ToggleMouseMode => {
if let Err(e) = mouse_capture.toggle() {
@@ -336,7 +374,7 @@ impl App<'_> {
args.initial_images,
));
self.app_state = AppState::Chat { widget };
- self.app_event_tx.send(AppEvent::Redraw);
+ self.app_event_tx.send(AppEvent::RequestRedraw);
}
GitWarningOutcome::Quit => {
self.app_event_tx.send(AppEvent::ExitRequest);
diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs
index fd6b2479ee..3aaa789760 100644
--- a/codex-rs/tui/src/app_event.rs
+++ b/codex-rs/tui/src/app_event.rs
@@ -8,6 +8,10 @@ use crate::slash_command::SlashCommand;
pub(crate) enum AppEvent {
CodexEvent(Event),
+ /// Request a redraw which will be debounced by the [`App`].
+ RequestRedraw,
+
+ /// Actually draw the next frame.
Redraw,
KeyEvent(KeyEvent),
diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs
index e4ea1d3823..2a91655cc5 100644
--- a/codex-rs/tui/src/bottom_pane/mod.rs
+++ b/codex-rs/tui/src/bottom_pane/mod.rs
@@ -212,7 +212,7 @@ impl BottomPane<'_> {
}
pub(crate) fn request_redraw(&self) {
- self.app_event_tx.send(AppEvent::Redraw)
+ self.app_event_tx.send(AppEvent::RequestRedraw)
}
/// Returns true when a popup inside the composer is visible.
diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs
index 860439ffb6..7c825acd41 100644
--- a/codex-rs/tui/src/chatwidget.rs
+++ b/codex-rs/tui/src/chatwidget.rs
@@ -431,7 +431,7 @@ impl ChatWidget<'_> {
}
fn request_redraw(&mut self) {
- self.app_event_tx.send(AppEvent::Redraw);
+ self.app_event_tx.send(AppEvent::RequestRedraw);
}
pub(crate) fn add_diff_output(&mut self, diff_output: String) {
diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs
index f9b71a23cb..dda61d0bd0 100644
--- a/codex-rs/tui/src/status_indicator_widget.rs
+++ b/codex-rs/tui/src/status_indicator_widget.rs
@@ -65,7 +65,7 @@ impl StatusIndicatorWidget {
std::thread::sleep(Duration::from_millis(200));
counter = counter.wrapping_add(1);
frame_idx_clone.store(counter, Ordering::Relaxed);
- app_event_tx_clone.send(AppEvent::Redraw);
+ app_event_tx_clone.send(AppEvent::RequestRedraw);
}
});
}
Review Comments
codex-rs/tui/src/app.rs
- Created: 2025-07-17 18:27:52 UTC | Link: https://github.com/openai/codex/pull/1599#discussion_r2214016700
@@ -162,17 +174,39 @@ impl<'a> App<'a> {
self.app_event_tx.clone()
}
+ /// Schedule a redraw if one is not already pending.
+ #[allow(clippy::unwrap_used)]
Can you move this to just above where you use it like on 180?
- Created: 2025-07-17 18:31:36 UTC | Link: https://github.com/openai/codex/pull/1599#discussion_r2214023745
@@ -162,17 +174,39 @@ impl<'a> App<'a> {
self.app_event_tx.clone()
}
+ /// Schedule a redraw if one is not already pending.
+ #[allow(clippy::unwrap_used)]
+ fn schedule_redraw(&self) {
+ let mut flag = self.pending_redraw.lock().unwrap();
+ if *flag {
+ return;
+ }
+ *flag = true;
+ let tx = self.app_event_tx.clone();
+ let pending = Arc::clone(&self.pending_redraw);
+ thread::spawn(move || {
+ thread::sleep(REDRAW_DEBOUNCE);
+ tx.send(AppEvent::Redraw);
+ #[allow(clippy::unwrap_used)]
+ let mut f = pending.lock().unwrap();
+ *f = false;
+ });
This is slightly better because it results in holding the lock for a shorter amount of time.
That extra level of scoping around the use of
self.pending_redraw.lock()andflagensures that after*flag = true, the lock is dropped.Also,
Arc::clone()is less canonical than just invoking.clone(), in my experience.{ #[allow(clippy::unwrap_used)] let mut flag = self.pending_redraw.lock().unwrap(); if *flag { return; } *flag = true; } let tx = self.app_event_tx.clone(); let pending_redraw = &self.pending_redraw.clone(); thread::spawn(move || { thread::sleep(REDRAW_DEBOUNCE); tx.send(AppEvent::Redraw); #[allow(clippy::unwrap_used)] let mut f = pending.lock().unwrap(); *f = false; });