mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +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:
+17
-15
@@ -174,6 +174,7 @@ use crossterm::event::KeyEventKind;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use ratatui::backend::Backend;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::layout::Size;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::Paragraph;
|
||||
@@ -1135,7 +1136,7 @@ See the Codex keymap documentation for supported actions and examples."
|
||||
let tui_events = tui.event_stream();
|
||||
tokio::pin!(tui_events);
|
||||
|
||||
tui.frame_requester().schedule_frame();
|
||||
tui.schedule_screen_size_recheck(Duration::ZERO);
|
||||
tracing::info!(
|
||||
duration_ms = %(startup_elapsed_before_app + startup_started_at.elapsed()).as_millis(),
|
||||
bootstrap_ms = %bootstrap_ms,
|
||||
@@ -1289,8 +1290,9 @@ See the Codex keymap documentation for supported actions and examples."
|
||||
app_server: &mut AppServerSession,
|
||||
event: TuiEvent,
|
||||
) -> Result<AppRunControl> {
|
||||
if matches!(event, TuiEvent::Draw | TuiEvent::Resize) {
|
||||
self.handle_draw_pre_render(tui)?;
|
||||
let screen_size = tui.screen_size_for_event(&event)?;
|
||||
if !matches!(&event, TuiEvent::Key(_) | TuiEvent::Paste(_)) {
|
||||
self.handle_draw_pre_render(tui, screen_size)?;
|
||||
}
|
||||
|
||||
if self.overlay.is_some() {
|
||||
@@ -1308,9 +1310,9 @@ See the Codex keymap documentation for supported actions and examples."
|
||||
let pasted = pasted.replace("\r", "\n");
|
||||
self.chat_widget.handle_paste(pasted);
|
||||
}
|
||||
TuiEvent::Draw | TuiEvent::Resize => {
|
||||
TuiEvent::Draw | TuiEvent::Resume | TuiEvent::Resize(_) => {
|
||||
if self.backtrack_render_pending {
|
||||
self.rebuild_transcript_after_backtrack(tui)?;
|
||||
self.rebuild_transcript_after_backtrack(tui, screen_size.into())?;
|
||||
self.backtrack_render_pending = false;
|
||||
}
|
||||
self.chat_widget.maybe_post_pending_notification(tui);
|
||||
@@ -1318,18 +1320,18 @@ See the Codex keymap documentation for supported actions and examples."
|
||||
.chat_widget
|
||||
.handle_paste_burst_tick(tui.frame_requester())
|
||||
{
|
||||
tui.defer_screen_size(screen_size);
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
// Allow widgets to process any pending timers before rendering.
|
||||
self.chat_widget.pre_draw_tick();
|
||||
let rendered_area = self.render_chat_widget_frame(tui)?;
|
||||
let rendered_area = self.render_chat_widget_frame(tui, screen_size)?;
|
||||
if self.chat_widget.ambient_pet_image_enabled() {
|
||||
let terminal_size = tui.terminal.size()?;
|
||||
let ambient_pet_area = Rect::new(
|
||||
/*x*/ 0,
|
||||
/*y*/ 0,
|
||||
terminal_size.width,
|
||||
terminal_size.height,
|
||||
screen_size.width,
|
||||
screen_size.height,
|
||||
);
|
||||
if let Err(err) = tui.draw_ambient_pet_image(
|
||||
self.chat_widget
|
||||
@@ -1361,17 +1363,17 @@ See the Codex keymap documentation for supported actions and examples."
|
||||
pub(super) fn show_shutdown_feedback(&mut self, tui: &mut tui::Tui) -> Result<()> {
|
||||
self.disable_ambient_pet_before_shutdown(tui)?;
|
||||
self.chat_widget.show_shutdown_in_progress();
|
||||
self.handle_draw_pre_render(tui)?;
|
||||
let screen_size = tui.terminal.last_known_screen_size;
|
||||
self.handle_draw_pre_render(tui, screen_size)?;
|
||||
self.chat_widget.pre_draw_tick();
|
||||
self.render_chat_widget_frame(tui)?;
|
||||
self.render_chat_widget_frame(tui, screen_size)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_chat_widget_frame(&mut self, tui: &mut tui::Tui) -> Result<Rect> {
|
||||
let width = tui.terminal.size()?.width;
|
||||
self.with_chat_widget_frame(width, |desired_height, chat_widget| {
|
||||
fn render_chat_widget_frame(&mut self, tui: &mut tui::Tui, screen_size: Size) -> Result<Rect> {
|
||||
self.with_chat_widget_frame(screen_size.width, |desired_height, chat_widget| {
|
||||
let mut rendered_area = Rect::default();
|
||||
tui.draw_with_resize_reflow(desired_height, |frame| {
|
||||
tui.draw_with_resize_reflow(desired_height, screen_size, |frame| {
|
||||
let area = frame.area();
|
||||
rendered_area = area;
|
||||
chat_widget.render(area, frame.buffer);
|
||||
|
||||
@@ -437,13 +437,14 @@ impl App {
|
||||
AppEvent::CodexOp(op) => {
|
||||
let is_user_turn = matches!(&op, AppCommand::UserTurn { .. });
|
||||
if is_user_turn {
|
||||
self.handle_draw_pre_render(tui)?;
|
||||
let screen_size = tui.terminal.last_known_screen_size;
|
||||
self.handle_draw_pre_render(tui, screen_size)?;
|
||||
if self.transcript_reflow.has_pending_reflow() {
|
||||
self.transcript_reflow.schedule_immediate();
|
||||
self.maybe_run_resize_reflow(tui)?;
|
||||
self.maybe_run_resize_reflow(tui, screen_size)?;
|
||||
}
|
||||
self.chat_widget.pre_draw_tick();
|
||||
self.render_chat_widget_frame(tui)?;
|
||||
self.render_chat_widget_frame(tui, screen_size)?;
|
||||
}
|
||||
self.chat_widget.prepare_local_op_submission(&op);
|
||||
if let Err(err) = self.submit_active_thread_op(app_server, op).await {
|
||||
|
||||
@@ -79,7 +79,8 @@ impl App {
|
||||
} else {
|
||||
self.chat_widget.set_raw_output_mode(enabled);
|
||||
}
|
||||
if let Err(err) = self.reflow_transcript_now(tui) {
|
||||
let terminal_width = tui.terminal.last_known_screen_size.into();
|
||||
if let Err(err) = self.reflow_transcript_now(tui, terminal_width) {
|
||||
tracing::warn!(error = %err, "failed to reflow transcript after raw output mode toggle");
|
||||
self.chat_widget
|
||||
.add_error_message(format!("Failed to redraw transcript: {err}"));
|
||||
|
||||
@@ -30,6 +30,16 @@ use crate::terminal_hyperlinks::HyperlinkLine;
|
||||
use crate::transcript_reflow::TRANSCRIPT_REFLOW_DEBOUNCE;
|
||||
use crate::tui;
|
||||
|
||||
/// Full terminal width before transcript-specific layout reservations.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(super) struct TerminalWidth(u16);
|
||||
|
||||
impl From<ratatui::layout::Size> for TerminalWidth {
|
||||
fn from(size: ratatui::layout::Size) -> Self {
|
||||
Self(size.width)
|
||||
}
|
||||
}
|
||||
|
||||
struct ReflowCellDisplay {
|
||||
lines: Vec<HyperlinkLine>,
|
||||
is_stream_continuation: bool,
|
||||
@@ -253,7 +263,8 @@ impl App {
|
||||
pub(super) fn maybe_finish_stream_reflow(&mut self, tui: &mut tui::Tui) -> Result<()> {
|
||||
if self.transcript_reflow.take_stream_finish_reflow_needed() {
|
||||
self.schedule_immediate_resize_reflow(tui);
|
||||
self.maybe_run_resize_reflow(tui)?;
|
||||
let screen_size = tui.terminal.last_known_screen_size;
|
||||
self.maybe_run_resize_reflow(tui, screen_size)?;
|
||||
} else if self.transcript_reflow.pending_is_due(Instant::now()) {
|
||||
tui.frame_requester().schedule_frame();
|
||||
}
|
||||
@@ -285,7 +296,8 @@ impl App {
|
||||
}
|
||||
|
||||
self.schedule_immediate_resize_reflow(tui);
|
||||
self.maybe_run_resize_reflow(tui)?;
|
||||
let screen_size = tui.terminal.last_known_screen_size;
|
||||
self.maybe_run_resize_reflow(tui, screen_size)?;
|
||||
if !self.transcript_reflow.has_pending_reflow() {
|
||||
self.transcript_reflow.clear_stream_flags();
|
||||
}
|
||||
@@ -342,8 +354,11 @@ impl App {
|
||||
self.reset_history_emission_state();
|
||||
}
|
||||
|
||||
pub(super) fn handle_draw_pre_render(&mut self, tui: &mut tui::Tui) -> Result<()> {
|
||||
let size = tui.terminal.size()?;
|
||||
pub(super) fn handle_draw_pre_render(
|
||||
&mut self,
|
||||
tui: &mut tui::Tui,
|
||||
size: ratatui::layout::Size,
|
||||
) -> Result<()> {
|
||||
let should_rebuild_transcript = self.handle_draw_size_change(
|
||||
size,
|
||||
tui.terminal.last_known_screen_size,
|
||||
@@ -355,7 +370,7 @@ impl App {
|
||||
// rebuild from transcript cells.
|
||||
tui.clear_pending_history_lines();
|
||||
}
|
||||
self.maybe_run_resize_reflow(tui)?;
|
||||
self.maybe_run_resize_reflow(tui, size)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -365,7 +380,11 @@ impl App {
|
||||
/// surface. Callers must keep using `HistoryCell` source as the rebuild input; attempting to
|
||||
/// reuse terminal-wrapped output here would preserve exactly the stale wrapping this feature is
|
||||
/// meant to remove.
|
||||
pub(super) fn maybe_run_resize_reflow(&mut self, tui: &mut tui::Tui) -> Result<()> {
|
||||
pub(super) fn maybe_run_resize_reflow(
|
||||
&mut self,
|
||||
tui: &mut tui::Tui,
|
||||
screen_size: ratatui::layout::Size,
|
||||
) -> Result<()> {
|
||||
let Some(deadline) = self.transcript_reflow.pending_until() else {
|
||||
return Ok(());
|
||||
};
|
||||
@@ -390,8 +409,8 @@ impl App {
|
||||
let reflow_ran_during_stream =
|
||||
!self.transcript_cells.is_empty() && self.should_mark_reflow_as_stream_time();
|
||||
|
||||
let width = self.reflow_transcript_now(tui)?;
|
||||
self.transcript_reflow.mark_reflowed_width(width);
|
||||
let width = self.reflow_transcript_now(tui, screen_size.into())?;
|
||||
self.transcript_reflow.mark_reflowed_width(width.0);
|
||||
|
||||
if reflow_ran_during_stream {
|
||||
self.transcript_reflow.mark_ran_during_stream();
|
||||
@@ -399,15 +418,17 @@ impl App {
|
||||
// Some terminals settle their final reported width after the repaint that handled the
|
||||
// last resize event. Request one cheap follow-up draw so `handle_draw_pre_render` can
|
||||
// sample that width and schedule a final reflow if needed.
|
||||
tui.frame_requester()
|
||||
.schedule_frame_in(TRANSCRIPT_REFLOW_DEBOUNCE);
|
||||
tui.schedule_screen_size_recheck(TRANSCRIPT_REFLOW_DEBOUNCE);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn reflow_transcript_now(&mut self, tui: &mut tui::Tui) -> Result<u16> {
|
||||
let terminal_width = tui.terminal.size()?.width;
|
||||
let width = self.chat_widget.history_wrap_width(terminal_width);
|
||||
pub(super) fn reflow_transcript_now(
|
||||
&mut self,
|
||||
tui: &mut tui::Tui,
|
||||
terminal_width: TerminalWidth,
|
||||
) -> Result<TerminalWidth> {
|
||||
let width = self.chat_widget.history_wrap_width(terminal_width.0);
|
||||
if self.transcript_cells.is_empty() {
|
||||
// Drop any queued pre-resize/pre-consolidation inserts before rebuilding from cells.
|
||||
tui.clear_pending_history_lines();
|
||||
@@ -438,9 +459,12 @@ impl App {
|
||||
/// Unlike resize reflow, rollback must clear the terminal even when no cells remain. Otherwise
|
||||
/// the cancelled user prompt stays visible in scrollback despite being removed from the source
|
||||
/// transcript.
|
||||
pub(super) fn rebuild_transcript_after_backtrack(&mut self, tui: &mut tui::Tui) -> Result<()> {
|
||||
let terminal_width = tui.terminal.size()?.width;
|
||||
let width = self.chat_widget.history_wrap_width(terminal_width);
|
||||
pub(super) fn rebuild_transcript_after_backtrack(
|
||||
&mut self,
|
||||
tui: &mut tui::Tui,
|
||||
terminal_width: TerminalWidth,
|
||||
) -> Result<()> {
|
||||
let width = self.chat_widget.history_wrap_width(terminal_width.0);
|
||||
let reflowed_lines = if self.transcript_cells.is_empty() {
|
||||
self.reset_history_emission_state();
|
||||
Vec::new()
|
||||
|
||||
@@ -5357,7 +5357,8 @@ async fn required_stream_reflow_during_capped_initial_replay_survives_transcript
|
||||
assert!(app.initial_history_replay_buffer.is_none());
|
||||
assert!(app.transcript_reflow.has_pending_reflow());
|
||||
|
||||
app.maybe_run_resize_reflow(&mut tui)?;
|
||||
let screen_size = tui.terminal.last_known_screen_size;
|
||||
app.maybe_run_resize_reflow(&mut tui, screen_size)?;
|
||||
assert!(app.transcript_reflow.has_pending_reflow());
|
||||
|
||||
app.close_transcript_overlay(&mut tui);
|
||||
@@ -5422,6 +5423,27 @@ async fn height_shrink_schedules_resize_reflow() {
|
||||
assert!(app.transcript_reflow.has_pending_reflow());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resizing_empty_transcript_schedules_settled_size_recheck() {
|
||||
let (mut app, _rx, _op_rx) = make_test_app_with_channels().await;
|
||||
let mut tui = crate::tui::test_support::make_test_tui().expect("test tui");
|
||||
let frame_requester = crate::tui::FrameRequester::test_dummy();
|
||||
let initial_size = ratatui::layout::Size::new(/*width*/ 80, /*height*/ 24);
|
||||
let resized_size = ratatui::layout::Size::new(/*width*/ 100, /*height*/ 24);
|
||||
|
||||
assert!(!app.handle_draw_size_change(initial_size, initial_size, &frame_requester));
|
||||
tui.screen_size_for_event(&TuiEvent::Resize(resized_size))
|
||||
.expect("resolve resize event");
|
||||
tui.terminal.resize(resized_size).expect("apply event size");
|
||||
assert!(app.handle_draw_size_change(resized_size, initial_size, &frame_requester));
|
||||
tokio::time::sleep(crate::transcript_reflow::TRANSCRIPT_REFLOW_DEBOUNCE).await;
|
||||
assert_eq!(
|
||||
tui.screen_size_for_event(&TuiEvent::Draw)
|
||||
.expect("resolve settled size"),
|
||||
initial_size
|
||||
);
|
||||
}
|
||||
|
||||
fn test_turn(turn_id: &str, status: TurnStatus, items: Vec<ThreadItem>) -> Turn {
|
||||
Turn {
|
||||
id: turn_id.to_string(),
|
||||
|
||||
@@ -341,8 +341,10 @@ impl App {
|
||||
/// source of truth for the active cell and its cache invalidation key, and because `App` owns
|
||||
/// overlay lifecycle and frame scheduling for animations.
|
||||
fn overlay_forward_event(&mut self, tui: &mut tui::Tui, event: TuiEvent) -> Result<()> {
|
||||
if matches!(&event, TuiEvent::Draw | TuiEvent::Resize)
|
||||
&& let Some(Overlay::Transcript(t)) = &mut self.overlay
|
||||
if matches!(
|
||||
&event,
|
||||
TuiEvent::Draw | TuiEvent::Resume | TuiEvent::Resize(_)
|
||||
) && let Some(Overlay::Transcript(t)) = &mut self.overlay
|
||||
{
|
||||
let active_key = self.chat_widget.active_cell_transcript_key();
|
||||
let chat_widget = &self.chat_widget;
|
||||
|
||||
@@ -345,8 +345,17 @@ where
|
||||
where
|
||||
F: FnOnce(&mut Frame),
|
||||
{
|
||||
self.try_draw(|frame| {
|
||||
render_callback(frame);
|
||||
let screen_size = self.size()?;
|
||||
self.draw_with_size(screen_size, render_callback)
|
||||
}
|
||||
|
||||
/// Draws a single frame using a screen size already obtained by the caller.
|
||||
pub(crate) fn draw_with_size<F>(&mut self, screen_size: Size, render: F) -> io::Result<()>
|
||||
where
|
||||
F: FnOnce(&mut Frame),
|
||||
{
|
||||
self.try_draw_with_size(screen_size, |frame| {
|
||||
render(frame);
|
||||
io::Result::Ok(())
|
||||
})
|
||||
}
|
||||
@@ -391,10 +400,18 @@ where
|
||||
F: FnOnce(&mut Frame) -> Result<(), E>,
|
||||
E: Into<io::Error>,
|
||||
{
|
||||
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
|
||||
// and the terminal (if growing), which may OOB.
|
||||
self.autoresize()?;
|
||||
let screen_size = self.size()?;
|
||||
self.try_draw_with_size(screen_size, render_callback)
|
||||
}
|
||||
|
||||
fn try_draw_with_size<F, E>(&mut self, screen_size: Size, render_callback: F) -> io::Result<()>
|
||||
where
|
||||
F: FnOnce(&mut Frame) -> Result<(), E>,
|
||||
E: Into<io::Error>,
|
||||
{
|
||||
if screen_size != self.last_known_screen_size {
|
||||
self.resize(screen_size)?;
|
||||
}
|
||||
let mut frame = self.get_frame();
|
||||
|
||||
render_callback(&mut frame).map_err(Into::into)?;
|
||||
@@ -792,6 +809,7 @@ mod tests {
|
||||
use super::*;
|
||||
use std::num::NonZeroU16;
|
||||
|
||||
use insta::assert_snapshot;
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::backend::WindowSize;
|
||||
use ratatui::layout::Rect;
|
||||
@@ -806,6 +824,7 @@ mod tests {
|
||||
output: Vec<u8>,
|
||||
size: Size,
|
||||
cursor: Position,
|
||||
size_call_count: std::cell::Cell<usize>,
|
||||
}
|
||||
|
||||
impl CaptureBackend {
|
||||
@@ -814,6 +833,7 @@ mod tests {
|
||||
output: Vec::new(),
|
||||
size: Size { width, height },
|
||||
cursor: Position { x: 0, y: 0 },
|
||||
size_call_count: std::cell::Cell::new(/*value*/ 0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -889,6 +909,8 @@ mod tests {
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Size> {
|
||||
self.size_call_count
|
||||
.set(self.size_call_count.get().saturating_add(/*rhs*/ 1));
|
||||
Ok(self.size)
|
||||
}
|
||||
|
||||
@@ -904,6 +926,65 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_redraws_with_known_size_do_not_query_backend_size() {
|
||||
let mut terminal =
|
||||
Terminal::with_options(CaptureBackend::new(/*width*/ 80, /*height*/ 24))
|
||||
.expect("terminal");
|
||||
let screen_size = terminal.last_known_screen_size;
|
||||
|
||||
for _ in 0..3 {
|
||||
terminal.draw_with_size(screen_size, |_| {}).expect("draw");
|
||||
}
|
||||
|
||||
assert_eq!(terminal.backend().size_call_count.get(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_draw_applies_event_dimensions_without_querying_backend_size() {
|
||||
let mut terminal =
|
||||
Terminal::with_options(CaptureBackend::new(/*width*/ 12, /*height*/ 4))
|
||||
.expect("terminal");
|
||||
let mut snapshots = Vec::new();
|
||||
|
||||
for width in [12, 8] {
|
||||
let size = Size::new(width, /*height*/ 4);
|
||||
let area = Rect::new(/*x*/ 0, /*y*/ 0, size.width, size.height);
|
||||
terminal.set_viewport_area(area);
|
||||
terminal
|
||||
.draw_with_size(size, |frame| {
|
||||
Paragraph::new("alpha beta")
|
||||
.wrap(Wrap { trim: false })
|
||||
.render(area, frame.buffer_mut());
|
||||
})
|
||||
.expect("draw resized frame");
|
||||
|
||||
let rendered = (0..size.height)
|
||||
.map(|y| {
|
||||
(0..size.width)
|
||||
.map(|x| terminal.previous_buffer()[(x, y)].symbol())
|
||||
.collect::<String>()
|
||||
.trim_end()
|
||||
.to_string()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
snapshots.push(rendered.trim_end().to_string());
|
||||
}
|
||||
|
||||
assert_eq!(terminal.backend().size_call_count.get(), 1);
|
||||
assert_eq!(
|
||||
terminal.last_known_screen_size,
|
||||
Size::new(/*width*/ 8, /*height*/ 4)
|
||||
);
|
||||
assert_snapshot!(snapshots.join("\n\n"), @r"
|
||||
alpha beta
|
||||
|
||||
alpha
|
||||
beta
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_buffers_does_not_emit_clear_to_end_for_full_width_row() {
|
||||
let area = Rect::new(0, 0, 3, 2);
|
||||
|
||||
@@ -129,10 +129,11 @@ pub(crate) async fn run_cwd_selection_prompt(
|
||||
|
||||
while !screen.is_done() {
|
||||
if let Some(event) = events.next().await {
|
||||
tui.screen_size_for_event(&event)?;
|
||||
match event {
|
||||
TuiEvent::Key(key_event) => screen.handle_key(key_event),
|
||||
TuiEvent::Paste(_) => {}
|
||||
TuiEvent::Draw | TuiEvent::Resize => {
|
||||
TuiEvent::Draw | TuiEvent::Resume | TuiEvent::Resize(_) => {
|
||||
tui.draw(u16::MAX, |frame| {
|
||||
frame.render_widget_ref(&screen, frame.area());
|
||||
})?;
|
||||
|
||||
@@ -101,10 +101,11 @@ pub(crate) async fn run_external_agent_config_migration_prompt(
|
||||
|
||||
while !screen.is_done() {
|
||||
if let Some(event) = events.next().await {
|
||||
let _ = tui.screen_size_for_event(&event);
|
||||
match event {
|
||||
TuiEvent::Key(key_event) => screen.handle_key(key_event),
|
||||
TuiEvent::Paste(_) => {}
|
||||
TuiEvent::Draw | TuiEvent::Resize => {
|
||||
TuiEvent::Draw | TuiEvent::Resume | TuiEvent::Resize(_) => {
|
||||
let _ = tui.draw(u16::MAX, |frame| {
|
||||
frame.render_widget_ref(&screen, frame.area());
|
||||
});
|
||||
|
||||
@@ -58,10 +58,11 @@ pub(crate) async fn run_external_agent_config_source_prompt(
|
||||
|
||||
while !screen.is_done() {
|
||||
if let Some(event) = events.next().await {
|
||||
let _ = tui.screen_size_for_event(&event);
|
||||
match event {
|
||||
TuiEvent::Key(key_event) => screen.handle_key(key_event),
|
||||
TuiEvent::Paste(_) => {}
|
||||
TuiEvent::Draw | TuiEvent::Resize => {
|
||||
TuiEvent::Draw | TuiEvent::Resume | TuiEvent::Resize(_) => {
|
||||
let _ = tui.draw(u16::MAX, |frame| {
|
||||
frame.render_widget_ref(&screen, frame.area());
|
||||
});
|
||||
|
||||
@@ -150,10 +150,11 @@ pub(crate) async fn run_model_migration_prompt(
|
||||
|
||||
while !screen.is_done() {
|
||||
if let Some(event) = events.next().await {
|
||||
let _ = alt.tui.screen_size_for_event(&event);
|
||||
match event {
|
||||
TuiEvent::Key(key_event) => screen.handle_key(key_event),
|
||||
TuiEvent::Paste(_) => {}
|
||||
TuiEvent::Draw | TuiEvent::Resize => {
|
||||
TuiEvent::Draw | TuiEvent::Resume | TuiEvent::Resize(_) => {
|
||||
let _ = alt.tui.draw(u16::MAX, |frame| {
|
||||
frame.render_widget_ref(&screen, frame.area());
|
||||
});
|
||||
|
||||
@@ -495,6 +495,7 @@ pub(crate) async fn run_onboarding_app(
|
||||
tokio::select! {
|
||||
event = tui_events.next() => {
|
||||
if let Some(event) = event {
|
||||
tui.screen_size_for_event(&event)?;
|
||||
match event {
|
||||
TuiEvent::Key(key_event) => {
|
||||
onboarding_screen.handle_key_event(key_event);
|
||||
@@ -509,7 +510,7 @@ pub(crate) async fn run_onboarding_app(
|
||||
TuiEvent::Paste(text) => {
|
||||
onboarding_screen.handle_paste(text);
|
||||
}
|
||||
TuiEvent::Draw | TuiEvent::Resize => {
|
||||
TuiEvent::Draw | TuiEvent::Resume | TuiEvent::Resize(_) => {
|
||||
if !did_full_clear_after_success
|
||||
&& onboarding_screen.steps.iter().any(|step| {
|
||||
if let Step::Auth(w) = step {
|
||||
|
||||
@@ -799,7 +799,7 @@ impl TranscriptOverlay {
|
||||
}
|
||||
other => self.view.handle_key_event(tui, other),
|
||||
},
|
||||
TuiEvent::Draw | TuiEvent::Resize => {
|
||||
TuiEvent::Draw | TuiEvent::Resume | TuiEvent::Resize(_) => {
|
||||
tui.draw(u16::MAX, |frame| {
|
||||
self.render(frame.area(), frame.buffer);
|
||||
})?;
|
||||
@@ -897,7 +897,7 @@ impl StaticOverlay {
|
||||
}
|
||||
other => self.view.handle_key_event(tui, other),
|
||||
},
|
||||
TuiEvent::Draw | TuiEvent::Resize => {
|
||||
TuiEvent::Draw | TuiEvent::Resume | TuiEvent::Resize(_) => {
|
||||
tui.draw(u16::MAX, |frame| {
|
||||
self.render(frame.area(), frame.buffer);
|
||||
})?;
|
||||
|
||||
@@ -50,6 +50,7 @@ use crossterm::event::KeyModifiers;
|
||||
use ratatui::layout::Constraint;
|
||||
use ratatui::layout::Layout;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::layout::Size;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::style::Styled as _;
|
||||
@@ -480,6 +481,7 @@ async fn run_session_picker_with_loader(
|
||||
loop {
|
||||
tokio::select! {
|
||||
Some(ev) = tui_events.next() => {
|
||||
let screen_size = alt.tui.screen_size_for_event(&ev)?;
|
||||
if state.overlay.is_some() {
|
||||
state.handle_overlay_event(alt.tui, ev)?;
|
||||
continue;
|
||||
@@ -496,14 +498,13 @@ async fn run_session_picker_with_loader(
|
||||
TuiEvent::Paste(pasted) => {
|
||||
state.handle_paste(pasted);
|
||||
}
|
||||
TuiEvent::Draw | TuiEvent::Resize => {
|
||||
if let Ok(size) = alt.tui.terminal.size() {
|
||||
let list_height =
|
||||
size.height.saturating_sub(PICKER_CHROME_HEIGHT) as usize;
|
||||
state.update_viewport(list_height, list_viewport_width(size.width));
|
||||
state.ensure_minimum_rows_for_view(list_height);
|
||||
}
|
||||
draw_picker(alt.tui, &state)?;
|
||||
TuiEvent::Draw | TuiEvent::Resume | TuiEvent::Resize(_) => {
|
||||
let list_width = list_viewport_width(screen_size.width);
|
||||
let list_height =
|
||||
usize::from(screen_size.height.saturating_sub(PICKER_CHROME_HEIGHT));
|
||||
state.update_viewport(list_height, list_width);
|
||||
state.ensure_minimum_rows_for_view(list_height);
|
||||
draw_picker(alt.tui, &state, screen_size)?;
|
||||
if state.note_transcript_loading_frame_drawn() {
|
||||
state.open_pending_transcript_if_ready();
|
||||
}
|
||||
@@ -1876,10 +1877,9 @@ fn parse_timestamp_str(ts: &str) -> Option<DateTime<Utc>> {
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn draw_picker(tui: &mut Tui, state: &PickerState) -> std::io::Result<()> {
|
||||
fn draw_picker(tui: &mut Tui, state: &PickerState, screen_size: Size) -> std::io::Result<()> {
|
||||
// Render full-screen overlay
|
||||
let height = tui.terminal.size()?.height;
|
||||
tui.draw(height, |frame| {
|
||||
tui.draw(screen_size.height, |frame| {
|
||||
let area = frame.area();
|
||||
let [header, _header_gap, search, _search_gap, list, footer] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
|
||||
@@ -107,6 +107,7 @@ async fn run_startup_hooks_review_app(
|
||||
let Some(event) = tui_events.next().await else {
|
||||
return Ok(StartupHooksReviewOutcome::Continue);
|
||||
};
|
||||
tui.screen_size_for_event(&event)?;
|
||||
match event {
|
||||
TuiEvent::Key(key_event) => {
|
||||
if matches!(key_event.kind, KeyEventKind::Press | KeyEventKind::Repeat) {
|
||||
@@ -167,7 +168,7 @@ async fn run_startup_hooks_review_app(
|
||||
}
|
||||
}
|
||||
TuiEvent::Paste(_) => {}
|
||||
TuiEvent::Draw | TuiEvent::Resize => draw_view(tui, &view)?,
|
||||
TuiEvent::Draw | TuiEvent::Resume | TuiEvent::Resize(_) => draw_view(tui, &view)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+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()
|
||||
|
||||
@@ -263,11 +263,13 @@ impl<S: EventSource + Default + Unpin> TuiEventStream<S> {
|
||||
"failed to suspend TUI process"
|
||||
);
|
||||
}
|
||||
return Some(TuiEvent::Draw);
|
||||
return Some(TuiEvent::Resume);
|
||||
}
|
||||
Some(TuiEvent::Key(key_event))
|
||||
}
|
||||
Event::Resize(_, _) => Some(TuiEvent::Resize),
|
||||
Event::Resize(width, height) => {
|
||||
Some(TuiEvent::Resize(ratatui::layout::Size { width, height }))
|
||||
}
|
||||
Event::Paste(pasted) => Some(TuiEvent::Paste(pasted)),
|
||||
Event::FocusGained => {
|
||||
self.terminal_focused.store(true, Ordering::Relaxed);
|
||||
@@ -513,7 +515,13 @@ mod tests {
|
||||
handle.send(Ok(Event::Resize(80, 24)));
|
||||
|
||||
let next = stream.next().await;
|
||||
assert!(matches!(next, Some(TuiEvent::Resize)));
|
||||
assert!(matches!(
|
||||
next,
|
||||
Some(TuiEvent::Resize(ratatui::layout::Size {
|
||||
width: 80,
|
||||
height: 24
|
||||
}))
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
|
||||
@@ -14,6 +14,7 @@ use crossterm::terminal::EnterAlternateScreen;
|
||||
use crossterm::terminal::LeaveAlternateScreen;
|
||||
use ratatui::crossterm::execute;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::layout::Size;
|
||||
|
||||
use crate::key_hint;
|
||||
|
||||
@@ -178,7 +179,7 @@ pub(crate) enum PreparedResumeAction {
|
||||
}
|
||||
|
||||
impl PreparedResumeAction {
|
||||
pub(crate) fn apply(self, terminal: &mut Terminal) -> Result<()> {
|
||||
pub(crate) fn apply(self, terminal: &mut Terminal, screen_size: Size) -> Result<()> {
|
||||
match self {
|
||||
PreparedResumeAction::RealignViewport(area) => {
|
||||
terminal.set_viewport_area(area);
|
||||
@@ -187,10 +188,8 @@ impl PreparedResumeAction {
|
||||
execute!(terminal.backend_mut(), EnterAlternateScreen)?;
|
||||
// Enable "alternate scroll" so terminals may translate wheel to arrows
|
||||
execute!(terminal.backend_mut(), EnableAlternateScroll)?;
|
||||
if let Ok(size) = terminal.size() {
|
||||
terminal.set_viewport_area(Rect::new(0, 0, size.width, size.height));
|
||||
terminal.clear()?;
|
||||
}
|
||||
terminal.set_viewport_area(Rect::from(screen_size));
|
||||
terminal.clear()?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
//! Terminal geometry policy shared by the main app and standalone screens.
|
||||
|
||||
use std::io;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use ratatui::layout::Size;
|
||||
|
||||
use super::Tui;
|
||||
use super::TuiEvent;
|
||||
use crate::transcript_reflow::TRANSCRIPT_REFLOW_DEBOUNCE;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct ScreenSizePolicy {
|
||||
pending_recheck_at: Option<Instant>,
|
||||
pending_draw_size: Option<Size>,
|
||||
deferred_size: Option<Size>,
|
||||
}
|
||||
|
||||
impl Tui {
|
||||
/// Resolve event geometry while avoiding backend queries on ordinary repaint frames.
|
||||
pub(crate) fn screen_size_for_event(&mut self, event: &TuiEvent) -> io::Result<Size> {
|
||||
if matches!(event, TuiEvent::Resize(_)) {
|
||||
self.schedule_screen_size_recheck(TRANSCRIPT_REFLOW_DEBOUNCE);
|
||||
} else if matches!(event, TuiEvent::Draw)
|
||||
&& let Some(deadline) = self.screen_size.pending_recheck_at
|
||||
{
|
||||
let now = Instant::now();
|
||||
if now < deadline {
|
||||
self.frame_requester.schedule_frame_in(deadline - now);
|
||||
} else {
|
||||
return self.screen_size_for_event(&TuiEvent::Resume);
|
||||
}
|
||||
}
|
||||
|
||||
let cached = self.terminal.last_known_screen_size;
|
||||
let size = match event {
|
||||
TuiEvent::Resize(size) => {
|
||||
self.screen_size.deferred_size = None;
|
||||
*size
|
||||
}
|
||||
TuiEvent::Resume => {
|
||||
self.screen_size.pending_recheck_at = None;
|
||||
self.screen_size.deferred_size = None;
|
||||
self.terminal.size()?
|
||||
}
|
||||
TuiEvent::Draw => self.screen_size.deferred_size.take().unwrap_or(cached),
|
||||
TuiEvent::Key(_) | TuiEvent::Paste(_) => cached,
|
||||
};
|
||||
self.screen_size.pending_draw_size =
|
||||
(!matches!(event, TuiEvent::Key(_) | TuiEvent::Paste(_))).then_some(size);
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
/// Schedule one backend sample after a resize, reflow, or external program.
|
||||
pub(crate) fn schedule_screen_size_recheck(&mut self, delay: Duration) {
|
||||
self.screen_size.pending_recheck_at = Some(Instant::now() + delay);
|
||||
self.frame_requester.schedule_frame_in(delay);
|
||||
}
|
||||
|
||||
pub(crate) fn defer_screen_size(&mut self, size: Size) {
|
||||
self.screen_size.deferred_size = Some(size);
|
||||
}
|
||||
|
||||
pub(super) fn take_event_screen_size(&mut self) -> io::Result<Size> {
|
||||
if let Some(size) = self.screen_size.pending_draw_size.take() {
|
||||
return Ok(size);
|
||||
}
|
||||
let size = self.terminal.size()?;
|
||||
self.screen_size.pending_recheck_at = None;
|
||||
self.screen_size.deferred_size = None;
|
||||
Ok(size)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "screen_size_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,65 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::layout::Size;
|
||||
|
||||
use crate::tui::TuiEvent;
|
||||
|
||||
#[tokio::test]
|
||||
async fn draw_size_policy_refreshes_only_after_resume() {
|
||||
let mut tui = crate::tui::test_support::make_test_tui().expect("test tui");
|
||||
let cached = Size::new(/*width*/ 120, /*height*/ 40);
|
||||
let resumed = tui.terminal.size().expect("backend size");
|
||||
tui.terminal.last_known_screen_size = cached;
|
||||
let resized = Size::new(/*width*/ 100, /*height*/ 30);
|
||||
for (event, expected) in [
|
||||
(TuiEvent::Draw, cached),
|
||||
(TuiEvent::Resume, resumed),
|
||||
(TuiEvent::Resize(resized), resized),
|
||||
(TuiEvent::Paste(String::new()), cached),
|
||||
(TuiEvent::Draw, resized),
|
||||
(TuiEvent::Paste(String::new()), cached),
|
||||
] {
|
||||
assert_eq!(tui.screen_size_for_event(&event).expect("size"), expected);
|
||||
if matches!(event, TuiEvent::Resize(_)) {
|
||||
tui.defer_screen_size(resized);
|
||||
}
|
||||
}
|
||||
assert_eq!(tui.take_event_screen_size().expect("size"), resumed);
|
||||
assert!(tui.screen_size.pending_recheck_at.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn standalone_resize_draw_rechecks_settled_screen_size_once() {
|
||||
let mut tui = crate::tui::test_support::make_test_tui().expect("test tui");
|
||||
let resize_size = Size::new(/*width*/ 120, /*height*/ 40);
|
||||
|
||||
tui.screen_size_for_event(&TuiEvent::Resize(resize_size))
|
||||
.expect("resolve resize");
|
||||
tui.terminal.resize(resize_size).expect("apply resize");
|
||||
assert_eq!(
|
||||
tui.screen_size_for_event(&TuiEvent::Draw)
|
||||
.expect("resolve early draw"),
|
||||
resize_size
|
||||
);
|
||||
|
||||
tui.schedule_screen_size_recheck(Duration::ZERO);
|
||||
assert_eq!(
|
||||
tui.screen_size_for_event(&TuiEvent::Draw)
|
||||
.expect("resolve settled draw"),
|
||||
tui.terminal.size().expect("terminal size")
|
||||
);
|
||||
assert!(tui.screen_size.pending_recheck_at.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn entering_alternate_screen_updates_cached_screen_size() {
|
||||
let mut tui = crate::tui::test_support::make_test_tui().expect("test tui");
|
||||
let screen_size = tui.terminal.size().expect("terminal size");
|
||||
tui.terminal.last_known_screen_size = Size::new(/*width*/ 120, /*height*/ 40);
|
||||
|
||||
tui.enter_alt_screen().expect("enter alternate screen");
|
||||
|
||||
assert_eq!(tui.terminal.last_known_screen_size, screen_size);
|
||||
tui.leave_alt_screen().expect("leave alternate screen");
|
||||
}
|
||||
@@ -55,10 +55,11 @@ pub(crate) async fn run_update_prompt_if_needed(
|
||||
|
||||
while !screen.is_done() {
|
||||
if let Some(event) = events.next().await {
|
||||
tui.screen_size_for_event(&event)?;
|
||||
match event {
|
||||
TuiEvent::Key(key_event) => screen.handle_key(key_event),
|
||||
TuiEvent::Paste(_) => {}
|
||||
TuiEvent::Draw | TuiEvent::Resize => {
|
||||
TuiEvent::Draw | TuiEvent::Resume | TuiEvent::Resize(_) => {
|
||||
tui.draw(u16::MAX, |frame| {
|
||||
frame.render_widget_ref(&screen, frame.area());
|
||||
})?;
|
||||
|
||||
Reference in New Issue
Block a user