diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index ac69bef2..397c0dc9 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>, + /// Stored parameters needed to instantiate the ChatWidget later, e.g., /// after dismissing the Git-repo warning. chat_args: Option, @@ -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 fd6b2479..3aaa7897 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 e4ea1d38..2a91655c 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 860439ff..7c825acd 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 f9b71a23..dda61d0b 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); } }); }