Avoid querying terminal size on every TUI redraw (#36482)

## What changed

- Carry dimensions on resize events and reuse the cached screen size for ordinary draws.
- Refresh terminal geometry after resize settling, process resume, and external program execution.
- Pass the resolved size through transcript reflow, viewport updates, and standalone TUI screens so each frame uses consistent dimensions.

## Testing

- Cover cached draws, resize-driven rendering, settled-size rechecks, resume refreshes, and alternate-screen size updates.

GitOrigin-RevId: 9d83dbab50d7a038bd59b4c66c13abcac69ddc79
This commit is contained in:
Felipe Coury authored and copyberry committed 2026-08-01 14:33:12 +00:00
1 parent ee0247f95a
commit 7dc1856685
21 files changed
+398 -97

No files matched your search

+37 -26
View File
@@ -35,6 +35,7 @@ use ratatui::crossterm::terminal::enable_raw_mode;
use ratatui::layout::Offset;
use ratatui::layout::Position;
use ratatui::layout::Rect;
use ratatui::layout::Size;
use ratatui::text::Line;
use tokio::sync::broadcast;
use tokio_stream::Stream;
@@ -52,6 +53,7 @@ use crate::tui::event_stream::EventBroker;
use crate::tui::event_stream::TuiEventStream;
#[cfg(unix)]
use crate::tui::job_control::SuspendContext;
use crate::tui::screen_size::ScreenSizePolicy;
use codex_config::types::NotificationCondition;
use codex_config::types::NotificationMethod;
@@ -61,6 +63,7 @@ mod frame_requester;
#[cfg(unix)]
mod job_control;
mod keyboard_modes;
mod screen_size;
mod terminal_stderr;
#[cfg(test)]
pub(crate) mod test_support;
@@ -548,13 +551,18 @@ pub enum TuiEvent {
Key(KeyEvent),
/// A bracketed paste payload normalized by the app layer before it reaches the composer.
Paste(String),
/// A terminal size notification that should be handled as resize-sensitive draw work.
/// A terminal size notification and its reported dimensions.
///
/// Resize is separate from `Draw` so the app can run feature-gated pre-render logic without
/// changing the default draw path for scheduled frames.
Resize,
Resize(Size),
/// A scheduled repaint that does not necessarily correspond to a terminal size change.
Draw,
/// The first repaint after returning from process suspension.
///
/// The app refreshes terminal geometry for this draw because resize events are not delivered
/// while the process is suspended.
Resume,
}
pub struct Tui {
@@ -563,6 +571,7 @@ pub struct Tui {
event_broker: Arc<EventBroker>,
pub(crate) terminal: Terminal,
pending_history_lines: Vec<PendingHistoryLines>,
screen_size: ScreenSizePolicy,
ambient_pet_image_state: crate::pets::PetImageRenderState,
pet_picker_preview_image_state: crate::pets::PetImageRenderState,
alt_saved_viewport: Option<ratatui::layout::Rect>,
@@ -620,6 +629,7 @@ impl Tui {
event_broker: Arc::new(EventBroker::new()),
terminal,
pending_history_lines: vec![],
screen_size: ScreenSizePolicy::default(),
ambient_pet_image_state: crate::pets::PetImageRenderState::default(),
pet_picker_preview_image_state: crate::pets::PetImageRenderState::default(),
alt_saved_viewport: None,
@@ -715,6 +725,7 @@ impl Tui {
}
self.resume_events();
self.schedule_screen_size_recheck(Duration::ZERO);
output
}
@@ -775,6 +786,7 @@ impl Tui {
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.resize(size)?;
self.terminal.set_viewport_area(ratatui::layout::Rect::new(
0,
0,
@@ -848,29 +860,29 @@ impl Tui {
fn update_inline_viewport_for_resize_reflow(
terminal: &mut Terminal,
height: u16,
screen_size: Size,
) -> Result<bool> {
let size = terminal.size()?;
let terminal_height_shrank = size.height < terminal.last_known_screen_size.height;
let terminal_height_grew = size.height > terminal.last_known_screen_size.height;
let terminal_height_shrank = screen_size.height < terminal.last_known_screen_size.height;
let terminal_height_grew = screen_size.height > terminal.last_known_screen_size.height;
let viewport_was_bottom_aligned =
terminal.viewport_area.bottom() == terminal.last_known_screen_size.height;
let previous_area = terminal.viewport_area;
let mut area = terminal.viewport_area;
area.height = height.min(size.height);
area.width = size.width;
area.height = height.min(screen_size.height);
area.width = screen_size.width;
let mut needs_full_repaint = false;
if area.bottom() > size.height {
let scroll_by = area.bottom() - size.height;
if area.bottom() > screen_size.height {
let scroll_by = area.bottom() - screen_size.height;
if !terminal_height_shrank {
terminal
.backend_mut()
.scroll_region_up(0..area.top(), scroll_by)?;
}
area.y = size.height - area.height;
area.y = screen_size.height - area.height;
} else if terminal_height_grew && viewport_was_bottom_aligned {
area.y = size.height - area.height;
area.y = screen_size.height - area.height;
}
if area != terminal.viewport_area {
@@ -915,6 +927,7 @@ impl Tui {
height: u16,
draw_fn: impl FnOnce(&mut custom_terminal::Frame),
) -> Result<()> {
let screen_size = self.take_event_screen_size()?;
// If we are resuming from ^Z, we need to prepare the resume action now so we can apply it
// in the synchronized update.
#[cfg(unix)]
@@ -924,14 +937,14 @@ impl Tui {
// Precompute any viewport updates that need a cursor-position query before entering
// the synchronized update, to avoid racing with the event reader.
let mut pending_viewport_area = self.pending_viewport_area()?;
let mut pending_viewport_area = self.pending_viewport_area(screen_size)?;
ensure_virtual_terminal_processing()?;
stdout().sync_update(|_| {
#[cfg(unix)]
if let Some(prepared) = prepared_resume.take() {
prepared.apply(&mut self.terminal)?;
prepared.apply(&mut self.terminal, screen_size)?;
}
let terminal = &mut self.terminal;
@@ -940,17 +953,15 @@ impl Tui {
terminal.clear()?;
}
let size = terminal.size()?;
let mut area = terminal.viewport_area;
area.height = height.min(size.height);
area.width = size.width;
area.height = height.min(screen_size.height);
area.width = screen_size.width;
// If the viewport has expanded, scroll everything else up to make room.
if area.bottom() > size.height {
if area.bottom() > screen_size.height {
terminal
.backend_mut()
.scroll_region_up(0..area.top(), area.bottom() - size.height)?;
area.y = size.height - area.height;
.scroll_region_up(0..area.top(), area.bottom() - screen_size.height)?;
area.y = screen_size.height - area.height;
}
if area != terminal.viewport_area {
// On startup, the old viewport can still be empty. Clear from the
@@ -979,7 +990,7 @@ impl Tui {
self.suspend_context.set_cursor_y(inline_area_bottom);
}
terminal.draw(|frame| {
terminal.draw_with_size(screen_size, |frame| {
draw_fn(frame);
})
})?
@@ -1049,6 +1060,7 @@ impl Tui {
pub fn draw_with_resize_reflow(
&mut self,
height: u16,
screen_size: Size,
draw_fn: impl FnOnce(&mut custom_terminal::Frame),
) -> Result<()> {
// If we are resuming from ^Z, we need to prepare the resume action now so we can apply it
@@ -1063,12 +1075,12 @@ impl Tui {
stdout().sync_update(|_| {
#[cfg(unix)]
if let Some(prepared) = prepared_resume.take() {
prepared.apply(&mut self.terminal)?;
prepared.apply(&mut self.terminal, screen_size)?;
}
let terminal = &mut self.terminal;
let needs_full_repaint =
Self::update_inline_viewport_for_resize_reflow(terminal, height)?;
Self::update_inline_viewport_for_resize_reflow(terminal, height, screen_size)?;
Self::flush_pending_history_lines(
terminal,
&mut self.pending_history_lines,
@@ -1093,15 +1105,14 @@ impl Tui {
self.suspend_context.set_cursor_y(inline_area_bottom);
}
terminal.draw(|frame| {
terminal.draw_with_size(screen_size, |frame| {
draw_fn(frame);
})
})?
}
fn pending_viewport_area(&mut self) -> Result<Option<Rect>> {
fn pending_viewport_area(&mut self, screen_size: Size) -> Result<Option<Rect>> {
let terminal = &mut self.terminal;
let screen_size = terminal.size()?;
let last_known_screen_size = terminal.last_known_screen_size;
if screen_size != last_known_screen_size
&& let Ok(cursor_pos) = terminal.get_cursor_position()