From 2d2df891bb2c1087a68fff6b9a219421097c189f Mon Sep 17 00:00:00 2001 From: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:19:03 -0700 Subject: [PATCH] fix: long lines incorrectly wrapped (#1710) fix to #1685. --- codex-rs/tui/src/insert_history.rs | 143 +++++++++++++++++++++++------ 1 file changed, 113 insertions(+), 30 deletions(-) diff --git a/codex-rs/tui/src/insert_history.rs b/codex-rs/tui/src/insert_history.rs index 7948436cd..1e8b1f539 100644 --- a/codex-rs/tui/src/insert_history.rs +++ b/codex-rs/tui/src/insert_history.rs @@ -1,7 +1,9 @@ +use std::fmt; use std::io; use std::io::Write; use crate::tui; +use crossterm::Command; use crossterm::queue; use crossterm::style::Color as CColor; use crossterm::style::Colors; @@ -11,46 +13,127 @@ use crossterm::style::SetBackgroundColor; use crossterm::style::SetColors; use crossterm::style::SetForegroundColor; use ratatui::layout::Position; +use ratatui::layout::Size; use ratatui::prelude::Backend; use ratatui::style::Color; use ratatui::style::Modifier; use ratatui::text::Line; use ratatui::text::Span; +/// Insert `lines` above the viewport. pub(crate) fn insert_history_lines(terminal: &mut tui::Tui, lines: Vec>) { - let screen_height = terminal - .backend() - .size() - .map(|s| s.height) - .unwrap_or(0xffffu16); + let screen_size = terminal.backend().size().unwrap_or(Size::new(0, 0)); + let mut area = terminal.get_frame().area(); - // We scroll up one line at a time because we can't position the cursor - // above the top of the screen. i.e. if - // lines.len() > screen_height - area.top() - // we would need to print the first line above the top of the screen, which - // can't be done. - for line in lines.into_iter() { - // 1. Scroll everything above the viewport up by one line - if area.bottom() >= screen_height { - let top = area.top(); - terminal.backend_mut().scroll_region_up(0..top, 1).ok(); - // 2. Move the cursor to the blank line - terminal.set_cursor_position(Position::new(0, top - 1)).ok(); - } else { - // If the viewport isn't at the bottom of the screen, scroll down instead - terminal - .backend_mut() - .scroll_region_down(area.top()..area.bottom() + 1, 1) - .ok(); - terminal - .set_cursor_position(Position::new(0, area.top())) - .ok(); - area.y += 1; - } - // 3. Write the line + + let wrapped_lines = wrapped_line_count(&lines, area.width); + let cursor_top = if area.bottom() < screen_size.height { + // If the viewport is not at the bottom of the screen, scroll it down to make room. + // Don't scroll it past the bottom of the screen. + let scroll_amount = wrapped_lines.min(screen_size.height - area.bottom()); + terminal + .backend_mut() + .scroll_region_down(area.top()..screen_size.height, scroll_amount) + .ok(); + let cursor_top = area.top() - 1; + area.y += scroll_amount; + terminal.set_viewport_area(area); + cursor_top + } else { + area.top() - 1 + }; + + // Limit the scroll region to the lines from the top of the screen to the + // top of the viewport. With this in place, when we add lines inside this + // area, only the lines in this area will be scrolled. We place the cursor + // at the end of the scroll region, and add lines starting there. + // + // ┌─Screen───────────────────────┐ + // │┌╌Scroll region╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐│ + // │┆ ┆│ + // │┆ ┆│ + // │┆ ┆│ + // │█╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘│ + // │╭─Viewport───────────────────╮│ + // ││ ││ + // │╰────────────────────────────╯│ + // └──────────────────────────────┘ + queue!(std::io::stdout(), SetScrollRegion(1..area.top())).ok(); + + terminal + .set_cursor_position(Position::new(0, cursor_top)) + .ok(); + + for line in lines { + queue!(std::io::stdout(), Print("\r\n")).ok(); write_spans(&mut std::io::stdout(), line.iter()).ok(); } - terminal.set_viewport_area(area); + + queue!(std::io::stdout(), ResetScrollRegion).ok(); +} + +fn wrapped_line_count(lines: &[Line], width: u16) -> u16 { + let mut count = 0; + for line in lines { + count += line_height(line, width); + } + count +} + +fn line_height(line: &Line, width: u16) -> u16 { + use unicode_width::UnicodeWidthStr; + // get the total display width of the line, accounting for double-width chars + let total_width = line + .spans + .iter() + .map(|span| span.content.width()) + .sum::(); + // divide by width to get the number of lines, rounding up + if width == 0 { + 1 + } else { + (total_width as u16).div_ceil(width).max(1) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SetScrollRegion(pub std::ops::Range); + +impl Command for SetScrollRegion { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + write!(f, "\x1b[{};{}r", self.0.start, self.0.end) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + panic!("tried to execute SetScrollRegion command using WinAPI, use ANSI instead"); + } + + #[cfg(windows)] + fn is_ansi_code_supported(&self) -> bool { + // TODO(nornagon): is this supported on Windows? + true + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ResetScrollRegion; + +impl Command for ResetScrollRegion { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + write!(f, "\x1b[r") + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + panic!("tried to execute ResetScrollRegion command using WinAPI, use ANSI instead"); + } + + #[cfg(windows)] + fn is_ansi_code_supported(&self) -> bool { + // TODO(nornagon): is this supported on Windows? + true + } } struct ModifierDiff {