Implement redraw debounce (#1599)

## Summary
- debouce redraw events so repeated requests don't overwhelm the
terminal
- add `RequestRedraw` event and schedule redraws after 100ms

## Testing
- `cargo clippy --tests`
- `cargo test` *(fails: Sandbox Denied errors in landlock tests)*

------
https://chatgpt.com/codex/tasks/task_i_68792a65b8b483218ec90a8f68746cd8

---------

Co-authored-by: Michael Bolin <mbolin@openai.com>
This commit is contained in:
aibrahim-oai
2025-07-17 12:54:55 -07:00
committed by GitHub
parent 6949329a7f
commit bb30ab9e96
5 changed files with 49 additions and 7 deletions

View File

@@ -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);

View File

@@ -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),

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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);
}
});
}