From db98d2ce25a5c46097a0602d3df38056f45cfebf Mon Sep 17 00:00:00 2001 From: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:47:00 -0700 Subject: [PATCH] enable alternate scroll in transcript mode (#2686) this allows the mouse wheel to scroll the transcript / diff views. --- codex-rs/tui/src/transcript_app.rs | 15 ++++++++- codex-rs/tui/src/tui.rs | 51 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/codex-rs/tui/src/transcript_app.rs b/codex-rs/tui/src/transcript_app.rs index bc559d15..c2b73fe3 100644 --- a/codex-rs/tui/src/transcript_app.rs +++ b/codex-rs/tui/src/transcript_app.rs @@ -1,4 +1,5 @@ use std::io::Result; +use std::time::Duration; use crate::insert_history; use crate::tui; @@ -227,6 +228,7 @@ impl TranscriptApp { } fn handle_key_event(&mut self, tui: &mut tui::Tui, key_event: KeyEvent) { + let mut defer_draw_ms: Option = None; match key_event { // Ctrl+Z is handled at the App level when transcript overlay is active KeyEvent { @@ -254,6 +256,7 @@ impl TranscriptApp { .. } => { self.scroll_offset = self.scroll_offset.saturating_sub(1); + defer_draw_ms = Some(16); } KeyEvent { code: KeyCode::Down, @@ -261,6 +264,7 @@ impl TranscriptApp { .. } => { self.scroll_offset = self.scroll_offset.saturating_add(1); + defer_draw_ms = Some(16); } KeyEvent { code: KeyCode::PageUp, @@ -269,6 +273,7 @@ impl TranscriptApp { } => { let area = self.scroll_area(tui.terminal.viewport_area); self.scroll_offset = self.scroll_offset.saturating_sub(area.height as usize); + defer_draw_ms = Some(16); } KeyEvent { code: KeyCode::PageDown | KeyCode::Char(' '), @@ -277,6 +282,7 @@ impl TranscriptApp { } => { let area = self.scroll_area(tui.terminal.viewport_area); self.scroll_offset = self.scroll_offset.saturating_add(area.height as usize); + defer_draw_ms = Some(16); } KeyEvent { code: KeyCode::Home, @@ -284,6 +290,7 @@ impl TranscriptApp { .. } => { self.scroll_offset = 0; + defer_draw_ms = Some(16); } KeyEvent { code: KeyCode::End, @@ -291,12 +298,18 @@ impl TranscriptApp { .. } => { self.scroll_offset = usize::MAX; + defer_draw_ms = Some(16); } _ => { return; } } - tui.frame_requester().schedule_frame(); + if let Some(ms) = defer_draw_ms { + tui.frame_requester() + .schedule_frame_in(Duration::from_millis(ms)); + } else { + tui.frame_requester().schedule_frame(); + } } fn scroll_area(&self, area: Rect) -> Rect { diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index d30e9a6e..f9d74989 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -11,6 +11,7 @@ use std::sync::atomic::Ordering; use std::time::Duration; use std::time::Instant; +use crossterm::Command; use crossterm::SynchronizedUpdate; use crossterm::cursor; use crossterm::cursor::MoveTo; @@ -65,6 +66,48 @@ pub fn set_modes() -> Result<()> { Ok(()) } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct EnableAlternateScroll; + +impl Command for EnableAlternateScroll { + fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result { + write!(f, "\x1b[?1007h") + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + Err(std::io::Error::other( + "tried to execute EnableAlternateScroll using WinAPI; use ANSI instead", + )) + } + + #[cfg(windows)] + fn is_ansi_code_supported(&self) -> bool { + true + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct DisableAlternateScroll; + +impl Command for DisableAlternateScroll { + fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result { + write!(f, "\x1b[?1007l") + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + Err(std::io::Error::other( + "tried to execute DisableAlternateScroll using WinAPI; use ANSI instead", + )) + } + + #[cfg(windows)] + fn is_ansi_code_supported(&self) -> bool { + true + } +} + /// Restore the terminal to its original state. /// Inverse of `set_modes`. pub fn restore() -> Result<()> { @@ -290,6 +333,8 @@ impl Tui { ) { if alt_screen_active.load(Ordering::Relaxed) { + // Disable alternate scroll when suspending from alt-screen + let _ = execute!(stdout(), DisableAlternateScroll); let _ = execute!(stdout(), LeaveAlternateScreen); resume_pending.store(ResumeAction::RestoreAlt as u8, Ordering::Relaxed); } else { @@ -370,6 +415,8 @@ impl Tui { } PreparedResumeAction::RestoreAltScreen => { execute!(self.terminal.backend_mut(), EnterAlternateScreen)?; + // Enable "alternate scroll" so terminals may translate wheel to arrows + execute!(self.terminal.backend_mut(), EnableAlternateScroll)?; if let Ok(size) = self.terminal.size() { self.terminal.set_viewport_area(ratatui::layout::Rect::new( 0, @@ -388,6 +435,8 @@ impl Tui { /// inline viewport for restoration when leaving. pub fn enter_alt_screen(&mut self) -> Result<()> { let _ = execute!(self.terminal.backend_mut(), EnterAlternateScreen); + // Enable "alternate scroll" so terminals may translate wheel to arrows + let _ = execute!(self.terminal.backend_mut(), EnableAlternateScroll); if let Ok(size) = self.terminal.size() { self.alt_saved_viewport = Some(self.terminal.viewport_area); self.terminal.set_viewport_area(ratatui::layout::Rect::new( @@ -404,6 +453,8 @@ impl Tui { /// Leave alternate screen and restore the previously saved inline viewport, if any. pub fn leave_alt_screen(&mut self) -> Result<()> { + // Disable alternate scroll when leaving alt-screen + let _ = execute!(self.terminal.backend_mut(), DisableAlternateScroll); let _ = execute!(self.terminal.backend_mut(), LeaveAlternateScreen); if let Some(saved) = self.alt_saved_viewport.take() { self.terminal.set_viewport_area(saved);