From cd33ecd95481da9671bb4f759dbaa9e23ebe6976 Mon Sep 17 00:00:00 2001 From: Chris Guidry Date: Sat, 1 Aug 2026 00:54:48 -0400 Subject: [PATCH] Wrap a narration row the screen is too narrow to hold A round renders at the width it started on, so a terminal that narrowed under it hands rows wider than the screen, and a table too wide for any screen renders that way at every width. Buffer::set_line dropped everything past the last column, and a row in the scrollback never repaints, so a round pinned at eighty columns flushing onto twenty wrote "india juliett kilo l" and lost the rest for good. A narration row wider than the screen now wraps again at the width the terminal has, and every row of that goes out. Only the write changes. The stream still renders and counts rows at the round's own width, so block layout and the spill accounting above this are untouched. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01CUXjWo1zGFhdJUig1hcQGf --- src/markdown/table.rs | 4 ++-- src/play/transcript.rs | 40 +++++++++++++++++++++++++++++++++--- src/play/transcript_tests.rs | 26 +++++++++++++++++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/src/markdown/table.rs b/src/markdown/table.rs index fa42ca2..00fa7db 100644 --- a/src/markdown/table.rs +++ b/src/markdown/table.rs @@ -19,8 +19,8 @@ use super::{Events, inline, style}; use crate::wrap::wrap_spans; /// The narrowest a column's content shrinks to. A table that still does -/// not fit at this width renders too wide and lets the terminal cut it, -/// which loses the right of a row but never a row itself. +/// not fit at this width renders too wide rather than dropping a row of +/// itself, and what becomes of the overflow is the caller's business. const MINIMUM: usize = 3; /// One row of the table: its cells, each still unwrapped. diff --git a/src/play/transcript.rs b/src/play/transcript.rs index 4d4d74f..f51120a 100644 --- a/src/play/transcript.rs +++ b/src/play/transcript.rs @@ -143,14 +143,14 @@ impl Transcript { ) -> Result<(), B::Error> { let width = self.round_width(terminal)?; for row in self.narration.committable(width) { - self.emit(terminal, row, Kind::Narration)?; + self.narrate(terminal, row)?; } let forming = self.narration.forming(width).len(); for row in self .narration .spill(width, forming.saturating_sub(tail_rows as usize)) { - self.emit(terminal, row, Kind::Narration)?; + self.narrate(terminal, row)?; } Ok(()) } @@ -168,12 +168,46 @@ impl Transcript { pub fn flush(&mut self, terminal: &mut Terminal) -> Result<(), B::Error> { let width = self.round_width(terminal)?; for row in self.narration.finish(width) { - self.emit(terminal, row, Kind::Narration)?; + self.narrate(terminal, row)?; } self.width = None; Ok(()) } + /// Puts one row of the DM's narration in the transcript, cut to what + /// the terminal can hold now. + /// + /// A row wider than the screen is what a round rendered before the + /// terminal narrowed under it, and a table too wide for any screen + /// renders that way at every width. [`Buffer::set_line`] would drop + /// everything past the last column, and a row in the scrollback never + /// repaints, so those words would be gone for good. Wrapping the row + /// again at the width the terminal has now keeps all of them, at the + /// cost of the row's own spacing: a wrap collapses runs of whitespace, + /// so an indent or a table's padding does not survive the second + /// wrap. + /// + /// The extra rows this makes are invisible to + /// [`crate::markdown::MarkdownStream`], which counts the rows it + /// rendered at the round's own width, not the rows that reach the + /// screen. + /// + /// [`Buffer::set_line`]: ratatui::buffer::Buffer::set_line + fn narrate( + &mut self, + terminal: &mut Terminal, + row: Line<'static>, + ) -> Result<(), B::Error> { + let width = terminal.size()?.width as usize; + if row.width() <= width { + return self.emit(terminal, row, Kind::Narration); + } + for row in wrap_spans(Text::from(row), width) { + self.emit(terminal, row, Kind::Narration)?; + } + Ok(()) + } + /// The width this round renders at: the terminal's own width the /// first time the round asks for it, and that same width every time /// after, until [`Transcript::flush`] ends the round. diff --git a/src/play/transcript_tests.rs b/src/play/transcript_tests.rs index 61c6bfd..e9c5e70 100644 --- a/src/play/transcript_tests.rs +++ b/src/play/transcript_tests.rs @@ -420,3 +420,29 @@ fn a_round_that_narrows_mid_stream_says_nothing_a_second_time() { assert_eq!(scrollback(&wide), ["", PARAGRAPH]); assert_eq!(scrollback(&narrow), ["", AFTER]); } + +#[test] +fn a_row_too_wide_for_the_terminal_it_lands_on_keeps_every_word() { + let mut wide = terminal(WIDE); + let mut narrow = terminal(NARROW); + let mut transcript = Transcript::default(); + + transcript.delta(&format!("{AFTER}\n\n{PARAGRAPH}")); + transcript.stream(&mut wide, DEEP_TAIL).unwrap(); + transcript.flush(&mut narrow).unwrap(); + + // The paragraph rendered as one row at eighty columns. All of it is + // here, over the four rows twenty columns takes, and the row that + // separates it from the block above is one blank row of spacing. + assert_eq!(scrollback(&wide), ["", AFTER]); + assert_eq!( + scrollback(&narrow), + [ + "", + "one two three four", + "five six seven eight", + "nine ten eleven", + "twelve", + ] + ); +} -- 2.51.2