mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
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:
1 parent
ee0247f95a
commit
7dc1856685
21 files changed
+398
-97
No files matched your search
+37
-26
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user