Skip to main content

rmux_core/
grid.rs

1//! Safe grid and scrollback storage for pane screen contents.
2
3use rmux_proto::TerminalSize;
4use std::collections::VecDeque;
5
6use crate::hyperlinks::Hyperlinks;
7use crate::input::{Colour, COLOUR_DEFAULT};
8use crate::style::Style;
9
10#[path = "grid/cell.rs"]
11mod cell;
12#[path = "grid/history_bytes.rs"]
13mod history_bytes;
14#[path = "grid/render.rs"]
15mod render;
16
17pub use cell::RenderedLineSpan;
18pub(crate) use cell::{GridCell, GridCellFlags, GridLine, GridLineFlags};
19use render::{append_cell_text, append_grid_string_code, append_hyperlink};
20
21pub(crate) fn render_cell_state_ansi(
22    state: &crate::input::CellState,
23    hyperlinks: &Hyperlinks,
24) -> Vec<u8> {
25    let previous = GridCell::blank_with_bg(COLOUR_DEFAULT);
26    let current = GridCell::from_state(' ', 1, state, GridCellFlags::default());
27    let mut rendered = String::new();
28    let mut has_link = false;
29    append_grid_string_code(
30        &previous,
31        &current,
32        &mut rendered,
33        false,
34        Some(hyperlinks),
35        &mut has_link,
36    );
37    rendered.into_bytes()
38}
39
40const HISTORY_STAMP_REFRESH_LINES: u16 = 256;
41
42/// Captured grid content rendered as logical lines.
43#[derive(Debug, Clone, PartialEq, Eq, Default)]
44#[cfg_attr(not(test), allow(dead_code))]
45pub(crate) struct GridCapture {
46    /// Captured lines ordered from oldest to newest.
47    pub lines: Vec<String>,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub(crate) struct GridLogicalCursor {
52    logical_start_y: usize,
53    offset: usize,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub(crate) struct GridPhysicalCursor {
58    pub absolute_y: usize,
59    pub x: u32,
60    pub pending_wrap: bool,
61}
62
63/// Rendering flags for tmux-style grid capture.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct GridRenderOptions {
66    /// Whether wrapped rows should omit separating newlines.
67    pub join_wrapped: bool,
68    /// Whether to emit ANSI SGR and OSC sequences inline.
69    pub with_sequences: bool,
70    /// Whether control sequences should be octal-escaped.
71    pub escape_sequences: bool,
72    /// Whether trailing empty cells should be included.
73    pub include_empty_cells: bool,
74    /// Whether included empty cells should stop at tmux's allocation bucket.
75    pub use_tmux_cell_capacity: bool,
76    /// Whether trailing spaces should be trimmed from the rendered line.
77    pub trim_spaces: bool,
78}
79
80impl Default for GridRenderOptions {
81    fn default() -> Self {
82        Self {
83            join_wrapped: false,
84            with_sequences: false,
85            escape_sequences: false,
86            include_empty_cells: true,
87            use_tmux_cell_capacity: false,
88            trim_spaces: true,
89        }
90    }
91}
92
93/// Per-capture ANSI state matching tmux's carried `lastgc`.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct GridStringState {
96    last_cell: GridCell,
97}
98
99impl Default for GridStringState {
100    fn default() -> Self {
101        Self {
102            last_cell: GridCell::blank_with_bg(COLOUR_DEFAULT),
103        }
104    }
105}
106
107impl GridStringState {
108    pub(crate) fn reset_to_default_line_style(
109        &mut self,
110        options: GridRenderOptions,
111        hyperlinks: Option<&Hyperlinks>,
112        output: &mut Vec<u8>,
113    ) {
114        if !options.with_sequences {
115            return;
116        }
117
118        let default_cell = GridCell::blank_with_bg(COLOUR_DEFAULT);
119        let mut rendered = String::new();
120        let mut has_link = false;
121        append_grid_string_code(
122            &self.last_cell,
123            &default_cell,
124            &mut rendered,
125            options.escape_sequences,
126            hyperlinks,
127            &mut has_link,
128        );
129        if has_link {
130            append_hyperlink(&mut rendered, "", "", options.escape_sequences);
131        }
132        output.extend_from_slice(rendered.as_bytes());
133        self.last_cell = default_cell;
134    }
135}
136
137/// Absolute grid storage split into history and visible rows.
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub(crate) struct Grid {
140    sx: u32,
141    sy: u32,
142    hlimit: usize,
143    reflow_history_capacity: usize,
144    hscrolled: usize,
145    history_enabled: bool,
146    history_stamp: i64,
147    history_stamp_remaining: u16,
148    history: VecDeque<GridLine>,
149    history_content_bytes: usize,
150    visible: VecDeque<GridLine>,
151}
152
153impl Grid {
154    /// Creates a new grid with the given geometry and history limit.
155    #[must_use]
156    pub fn new(size: TerminalSize, hlimit: usize) -> Self {
157        let sx = u32::from(size.cols.max(1));
158        let sy = u32::from(size.rows.max(1));
159        Self {
160            sx,
161            sy,
162            hlimit,
163            reflow_history_capacity: 0,
164            hscrolled: 0,
165            history_enabled: true,
166            history_stamp: 0,
167            history_stamp_remaining: 0,
168            history: VecDeque::new(),
169            history_content_bytes: 0,
170            visible: (0..sy).map(|_| GridLine::new(sx)).collect(),
171        }
172    }
173
174    /// Returns the grid size.
175    #[must_use]
176    pub fn size(&self) -> TerminalSize {
177        TerminalSize {
178            cols: u16::try_from(self.sx).unwrap_or(u16::MAX),
179            rows: u16::try_from(self.sy).unwrap_or(u16::MAX),
180        }
181    }
182
183    /// Returns the visible width in columns.
184    #[must_use]
185    pub const fn sx(&self) -> u32 {
186        self.sx
187    }
188
189    /// Returns the visible height in rows.
190    #[must_use]
191    pub const fn sy(&self) -> u32 {
192        self.sy
193    }
194
195    /// Returns the history size in rows.
196    #[must_use]
197    pub fn hsize(&self) -> usize {
198        self.history.len()
199    }
200
201    /// Returns the configured history limit.
202    #[must_use]
203    pub const fn hlimit(&self) -> usize {
204        self.hlimit
205    }
206
207    /// Returns whether history collection is enabled.
208    #[must_use]
209    pub const fn history_enabled(&self) -> bool {
210        self.history_enabled
211    }
212
213    /// Updates the history limit and evicts old rows if needed.
214    pub fn set_hlimit(&mut self, hlimit: usize) {
215        self.hlimit = hlimit;
216        self.reflow_history_capacity = 0;
217        while self.history.len() > self.hlimit {
218            let _ = self.pop_history_front();
219        }
220        self.hscrolled = self.hscrolled.min(self.history.len());
221    }
222
223    /// Enables or disables scrollback collection.
224    pub fn set_history_enabled(&mut self, enabled: bool) {
225        self.history_enabled = enabled;
226    }
227
228    /// Returns the number of history rows that can be pulled back by growth.
229    #[allow(dead_code)]
230    #[must_use]
231    pub const fn hscrolled(&self) -> usize {
232        self.hscrolled
233    }
234
235    /// Returns one visible line by row.
236    #[must_use]
237    pub fn visible_line(&self, y: u32) -> Option<&GridLine> {
238        self.visible.get(y as usize)
239    }
240
241    pub(crate) fn visible_line_mut(&mut self, y: u32) -> Option<&mut GridLine> {
242        self.visible.get_mut(y as usize)
243    }
244
245    /// Breaks the soft-wrap boundary immediately before one visible row.
246    ///
247    /// The predecessor may be the final history row when the first viewport
248    /// row is a continuation. Alternate-screen rows never continue main-screen
249    /// history, which is retained in the same grid with history disabled.
250    pub(crate) fn break_wrap_before_visible_line(&mut self, y: u32) {
251        let previous = if y == 0 {
252            if self.history_enabled {
253                self.history.back_mut()
254            } else {
255                None
256            }
257        } else {
258            self.visible.get_mut(y.saturating_sub(1) as usize)
259        };
260        if let Some(previous) = previous {
261            previous.set_wrapped(false);
262        }
263    }
264
265    /// Returns one absolute line where rows `0..hsize` are history and
266    /// `hsize..hsize+sy` are the visible screen.
267    #[allow(dead_code)]
268    #[must_use]
269    pub fn absolute_line(&self, absolute_y: usize) -> Option<&GridLine> {
270        if absolute_y < self.history.len() {
271            self.history.get(absolute_y)
272        } else {
273            self.visible.get(absolute_y - self.history.len())
274        }
275    }
276
277    /// Removes one absolute line from history or the visible viewport.
278    ///
279    /// Visible removals keep the viewport height stable by pushing a blank row
280    /// at the bottom.
281    pub fn remove_absolute_line(&mut self, absolute_y: usize) -> bool {
282        if absolute_y < self.history.len() {
283            if let Some(line) = self.history.remove(absolute_y) {
284                self.history_content_bytes = self
285                    .history_content_bytes
286                    .saturating_sub(history_line_content_bytes(&line));
287            }
288            self.reflow_history_capacity = self.reflow_history_capacity.saturating_sub(1);
289            self.hscrolled = self.hscrolled.min(self.history.len());
290            return true;
291        }
292
293        let visible_index = absolute_y.saturating_sub(self.history.len());
294        if visible_index >= self.visible.len() {
295            return false;
296        }
297
298        let _ = self.visible.remove(visible_index);
299        self.visible.push_back(GridLine::new(self.sx));
300        true
301    }
302
303    /// Drops all lines after the addressed absolute row and recomposes the viewport.
304    pub(crate) fn truncate_after_absolute_line(&mut self, absolute_y: usize) -> bool {
305        let total = self.history.len() + self.visible.len();
306        if absolute_y >= total {
307            return false;
308        }
309
310        let keep = absolute_y.saturating_add(1);
311        let mut lines = self
312            .history
313            .iter()
314            .chain(self.visible.iter())
315            .take(keep)
316            .cloned()
317            .collect::<Vec<_>>();
318        let visible_rows = self.sy as usize;
319        while lines.len() < visible_rows {
320            lines.push(GridLine::new(self.sx));
321        }
322
323        let visible_start = lines.len().saturating_sub(visible_rows);
324        let mut visible = lines.split_off(visible_start);
325        for line in &mut visible {
326            line.resize_width_preserving_wrap(self.sx, COLOUR_DEFAULT);
327        }
328        self.history = compacted_history(lines);
329        while self.history.len() > self.effective_history_capacity() {
330            let _ = self.history.pop_front();
331        }
332        self.recompute_history_content_bytes();
333        self.visible = visible.into();
334        self.hscrolled = self.history.len();
335        true
336    }
337
338    /// Returns whether the absolute line is marked as wrapped.
339    #[must_use]
340    pub fn absolute_line_wrapped(&self, absolute_y: usize) -> Option<bool> {
341        self.absolute_line(absolute_y)
342            .map(|line| line.flags.contains(GridLineFlags::WRAPPED))
343    }
344
345    /// Returns the columns a pre-wrapped wide glyph left unused at the end of
346    /// the absolute line.
347    #[must_use]
348    pub fn absolute_line_trailing_reflow_gap(&self, absolute_y: usize) -> Option<u32> {
349        self.absolute_line(absolute_y)
350            .map(GridLine::trailing_reflow_gap)
351    }
352
353    /// Clears every history row.
354    pub fn clear_history(&mut self) {
355        self.history.clear();
356        self.history_content_bytes = 0;
357        self.reflow_history_capacity = 0;
358        self.hscrolled = 0;
359    }
360
361    /// Clears the visible grid.
362    pub fn clear_visible(&mut self, bg: Colour) {
363        for line in &mut self.visible {
364            line.clear(bg);
365        }
366    }
367
368    /// Moves used visible rows to scrollback before clearing the viewport.
369    pub fn clear_visible_to_history(&mut self, bg: Colour) {
370        if self.history_enabled {
371            let last_used = self.visible.iter().rposition(|line| line.used_end() > 0);
372            if let Some(last_used) = last_used {
373                for index in 0..=last_used {
374                    let line = self.visible[index].clone();
375                    self.push_history(line);
376                }
377            }
378        }
379        self.clear_visible(bg);
380    }
381
382    /// Replaces the visible rows with a saved copy.
383    pub fn replace_visible(&mut self, lines: Vec<GridLine>) {
384        self.sy = lines.len() as u32;
385        self.visible = lines.into();
386        for line in &mut self.visible {
387            line.resize_width_preserving_wrap(self.sx, COLOUR_DEFAULT);
388        }
389    }
390
391    pub(crate) fn resize_visible_width_preserving_cursor(
392        &mut self,
393        sx: u32,
394        bg: Colour,
395        visible_y: u32,
396        cursor_x: u32,
397        pending_wrap: bool,
398    ) -> GridPhysicalCursor {
399        let source_width = self.sx.max(1);
400        let target_width = sx.max(1);
401        let visible = reflow_alternate_visible_lines(
402            self.visible_lines(),
403            target_width,
404            bg,
405            self.sy as usize,
406        );
407        let current_history = self.history.len();
408        self.sx = target_width;
409        self.visible = visible.into();
410
411        // tmux keeps the alternate-screen cursor at its physical coordinate
412        // across a width resize. Its grid may retain an x beyond the new edge;
413        // RMUX models the same next-write behavior with a bounded edge cursor
414        // and pending wrap.
415        let physical_x = if pending_wrap { source_width } else { cursor_x };
416        let (x, pending_wrap) = if physical_x >= target_width {
417            (target_width.saturating_sub(1), true)
418        } else {
419            (physical_x, false)
420        };
421        GridPhysicalCursor {
422            absolute_y: current_history.saturating_add(
423                usize::try_from(visible_y.min(self.sy.saturating_sub(1))).unwrap_or(usize::MAX),
424            ),
425            x,
426            pending_wrap,
427        }
428    }
429
430    pub(crate) fn restore_visible_at_size(
431        &mut self,
432        source_size: TerminalSize,
433        lines: Vec<GridLine>,
434        bg: Colour,
435    ) {
436        self.sx = u32::from(source_size.cols.max(1));
437        self.sy = u32::from(source_size.rows.max(1));
438        self.visible = lines.into();
439        while self.visible.len() > self.sy as usize {
440            let _ = self.visible.pop_back();
441        }
442        while self.visible.len() < self.sy as usize {
443            self.visible.push_back(GridLine::blank_with_bg(self.sx, bg));
444        }
445        for line in &mut self.visible {
446            line.resize_width_preserving_wrap(self.sx, bg);
447        }
448    }
449
450    /// Captures the grid as rendered lines. Wrapped rows are optionally joined.
451    #[cfg_attr(not(test), allow(dead_code))]
452    #[must_use]
453    pub fn capture(&self, join_wrapped: bool) -> GridCapture {
454        let mut lines = Vec::new();
455        let mut pending = String::new();
456
457        for line in self.history.iter().chain(self.visible.iter()) {
458            let wrapped = line.flags.contains(GridLineFlags::WRAPPED);
459            let rendered = if join_wrapped {
460                line.render_with_options(
461                    self.sx as usize,
462                    GridRenderOptions {
463                        join_wrapped: true,
464                        include_empty_cells: false,
465                        trim_spaces: false,
466                        ..GridRenderOptions::default()
467                    },
468                    &mut GridStringState::default(),
469                    None,
470                )
471            } else {
472                line.render_text()
473            };
474            if join_wrapped {
475                pending.push_str(&rendered);
476                if !wrapped {
477                    lines.push(std::mem::take(&mut pending));
478                }
479                continue;
480            }
481
482            lines.push(rendered);
483        }
484
485        if join_wrapped && !pending.is_empty() {
486            lines.push(pending);
487        }
488
489        GridCapture { lines }
490    }
491
492    /// Renders one absolute line using tmux-style capture options.
493    #[must_use]
494    pub fn render_absolute_line(
495        &self,
496        absolute_y: usize,
497        options: GridRenderOptions,
498        state: &mut GridStringState,
499        hyperlinks: Option<&Hyperlinks>,
500    ) -> Option<String> {
501        self.absolute_line(absolute_y)
502            .map(|line| line.render_with_options(self.sx as usize, options, state, hyperlinks))
503    }
504
505    /// Renders one absolute line and reports the columns it paints.
506    #[must_use]
507    pub fn render_absolute_line_measured(
508        &self,
509        absolute_y: usize,
510        options: GridRenderOptions,
511        state: &mut GridStringState,
512        hyperlinks: Option<&Hyperlinks>,
513    ) -> Option<(String, RenderedLineSpan)> {
514        self.absolute_line(absolute_y).map(|line| {
515            line.render_with_options_measured(self.sx as usize, options, state, hyperlinks)
516        })
517    }
518
519    pub fn append_rendered_absolute_line(
520        &self,
521        absolute_y: usize,
522        options: GridRenderOptions,
523        state: &mut GridStringState,
524        hyperlinks: Option<&Hyperlinks>,
525        output: &mut Vec<u8>,
526    ) -> Option<()> {
527        let line = self.absolute_line(absolute_y)?;
528        if line.render_bytes_with_options(self.sx as usize, options, output) {
529            return Some(());
530        }
531        let rendered = line.render_with_options(self.sx as usize, options, state, hyperlinks);
532        output.extend_from_slice(rendered.as_bytes());
533        Some(())
534    }
535
536    /// Renders one visible line after applying a pane default-style overlay to
537    /// default cells only. This is used by live renderers to avoid cloning the
538    /// full screen and scrollback when only the viewport is needed.
539    #[must_use]
540    pub fn render_visible_line_with_default_style(
541        &self,
542        row: usize,
543        options: GridRenderOptions,
544        state: &mut GridStringState,
545        hyperlinks: Option<&Hyperlinks>,
546        style: &Style,
547    ) -> Option<String> {
548        self.visible_line(u32::try_from(row).ok()?).map(|line| {
549            line.render_with_default_style(self.sx as usize, options, state, hyperlinks, style)
550        })
551    }
552
553    /// Returns the retained history size in bytes including newlines.
554    #[must_use]
555    pub const fn history_byte_size(&self) -> usize {
556        self.history_content_bytes
557    }
558
559    /// Captures only the visible rows.
560    #[must_use]
561    pub fn visible_lines(&self) -> Vec<GridLine> {
562        self.visible.iter().cloned().collect()
563    }
564
565    pub(crate) fn clone_recovery_projection(&self, max_history_bytes: usize) -> Self {
566        let mut remaining = max_history_bytes;
567        let mut start = self.history.len();
568        while start > 0 {
569            let group_start = self.logical_start_y(start - 1);
570            let group_bytes = self
571                .history
572                .range(group_start..start)
573                .map(GridLine::recovery_clone_bytes)
574                .fold(0_usize, usize::saturating_add);
575            if group_bytes > remaining {
576                break;
577            }
578            remaining -= group_bytes;
579            start = group_start;
580        }
581
582        let retained = self
583            .history
584            .range(start..)
585            .cloned()
586            .collect::<VecDeque<_>>();
587        let history_content_bytes = retained.iter().map(history_line_content_bytes).sum();
588        Self {
589            sx: self.sx,
590            sy: self.sy,
591            hlimit: retained.len(),
592            reflow_history_capacity: 0,
593            hscrolled: retained.len(),
594            history_enabled: self.history_enabled,
595            history_stamp: self.history_stamp,
596            history_stamp_remaining: self.history_stamp_remaining,
597            history: retained,
598            history_content_bytes,
599            visible: self.visible.clone(),
600        }
601    }
602
603    pub(crate) fn scroll_region_up(
604        &mut self,
605        upper: u32,
606        lower: u32,
607        bg: Colour,
608        to_history: bool,
609    ) {
610        if !self.valid_region(upper, lower) {
611            return;
612        }
613
614        let moved_lines = lower.saturating_sub(upper);
615        if !(to_history && self.history_enabled) {
616            self.prepare_wrapped_line_move(upper, upper.saturating_add(1), moved_lines);
617        }
618        self.scroll_region_up_one(upper, lower, bg, to_history);
619    }
620
621    fn scroll_region_up_one(&mut self, upper: u32, lower: u32, bg: Colour, to_history: bool) {
622        let upper = upper as usize;
623        let lower = lower as usize;
624        if upper == 0 && lower + 1 == self.visible.len() {
625            let Some(mut removed) = self.visible.pop_front() else {
626                return;
627            };
628            if to_history && self.history_enabled {
629                self.push_history(removed);
630                self.visible.push_back(GridLine::blank_with_bg(self.sx, bg));
631            } else {
632                removed.clear(bg);
633                self.visible.push_back(removed);
634            }
635            return;
636        }
637
638        let removed_for_history = if to_history && self.history_enabled {
639            let blank = GridLine::blank_with_bg(self.sx, bg);
640            let visible = self.visible.make_contiguous();
641            let removed = std::mem::replace(&mut visible[upper], blank);
642            Some(removed)
643        } else {
644            None
645        };
646        if let Some(removed) = removed_for_history {
647            self.push_history(removed);
648        }
649        let visible = self.visible.make_contiguous();
650        visible[upper..=lower].rotate_left(1);
651        let removed = &mut visible[lower];
652        removed.clear(bg);
653    }
654
655    pub(crate) fn scroll_region_down(&mut self, upper: u32, lower: u32, bg: Colour) {
656        if !self.valid_region(upper, lower) {
657            return;
658        }
659
660        let moved_lines = lower.saturating_sub(upper);
661        self.prepare_wrapped_line_move(upper.saturating_add(1), upper, moved_lines);
662        self.scroll_region_down_one(upper, lower, bg);
663    }
664
665    fn scroll_region_down_one(&mut self, upper: u32, lower: u32, bg: Colour) {
666        let upper = upper as usize;
667        let lower = lower as usize;
668        if upper == 0 && lower + 1 == self.visible.len() {
669            let Some(mut removed) = self.visible.pop_back() else {
670                return;
671            };
672            removed.clear(bg);
673            self.visible.push_front(removed);
674            return;
675        }
676
677        let visible = self.visible.make_contiguous();
678        visible[upper..=lower].rotate_right(1);
679        visible[upper].clear(bg);
680    }
681
682    pub(crate) fn insert_lines(&mut self, upper: u32, lower: u32, count: u32, bg: Colour) {
683        if !self.valid_region(upper, lower) {
684            return;
685        }
686        let region_lines = lower.saturating_sub(upper).saturating_add(1);
687        let count = count.max(1).min(region_lines);
688        let moved_lines = region_lines.saturating_sub(count);
689        if moved_lines == 0 {
690            self.break_wrap_before_visible_line(upper);
691        } else {
692            self.prepare_wrapped_line_move(upper.saturating_add(count), upper, moved_lines);
693        }
694        for _ in 0..count {
695            self.scroll_region_down_one(upper, lower, bg);
696        }
697        // tmux follows the move with a full-line clear starting after the
698        // moved range. That clear also breaks WRAPPED immediately before its
699        // start, even when its unsigned row count describes an empty range.
700        if count != moved_lines {
701            self.break_wrap_before_visible_line(upper.saturating_add(moved_lines));
702        }
703    }
704
705    /// Mirrors tmux's `grid_move_lines` wrap-boundary maintenance without
706    /// coupling raw row storage to tmux's allocation strategy.
707    fn prepare_wrapped_line_move(&mut self, destination: u32, source: u32, count: u32) {
708        if count == 0 || source == destination {
709            return;
710        }
711        self.break_wrap_before_visible_line(destination);
712        let destination_end = destination.saturating_add(count);
713        if source < destination || source >= destination_end {
714            self.break_wrap_before_visible_line(source);
715        }
716    }
717
718    pub(crate) fn logical_cursor(
719        &self,
720        visible_y: u32,
721        cursor_x: u32,
722        pending_wrap: bool,
723    ) -> GridLogicalCursor {
724        let total_lines = self.total_line_count();
725        if total_lines == 0 {
726            return GridLogicalCursor {
727                logical_start_y: 0,
728                offset: 0,
729            };
730        }
731
732        let absolute_y = self
733            .history
734            .len()
735            .saturating_add(visible_y as usize)
736            .min(total_lines.saturating_sub(1));
737        let logical_start_y = self.logical_start_y(absolute_y);
738        let mut offset = 0_usize;
739        for line_y in logical_start_y..absolute_y {
740            if let Some(line) = self.absolute_line(line_y) {
741                offset = offset.saturating_add(line.reflow_logical_width());
742            }
743        }
744        let physical_cursor_column = if pending_wrap {
745            self.sx as usize
746        } else {
747            cursor_x.min(self.sx.saturating_sub(1)) as usize
748        };
749        let cursor_column = self
750            .absolute_line(absolute_y)
751            .map_or(physical_cursor_column, |line| {
752                line.reflow_logical_column(physical_cursor_column)
753            });
754        offset = offset.saturating_add(cursor_column);
755
756        GridLogicalCursor {
757            logical_start_y,
758            offset,
759        }
760    }
761
762    pub(crate) fn resize_width_remapping_cursor(
763        &mut self,
764        sx: u32,
765        bg: Colour,
766        cursor: GridLogicalCursor,
767    ) -> GridPhysicalCursor {
768        let sx = sx.max(1);
769        if sx == self.sx {
770            return self.locate_cursor_from_logical(cursor);
771        }
772
773        if self.can_resize_width_without_reflow(sx) {
774            for line in &mut self.history {
775                line.resize_width_preserving_wrap(sx, bg);
776            }
777            for line in &mut self.visible {
778                line.resize_width_preserving_wrap(sx, bg);
779            }
780            self.sx = sx;
781            self.recompute_history_content_bytes();
782            return self.locate_cursor_from_logical(cursor);
783        }
784
785        let visible_rows = self.sy as usize;
786        let lines = self
787            .history
788            .iter()
789            .chain(self.visible.iter())
790            .cloned()
791            .collect::<Vec<_>>();
792        let (mut reflowed, reflow_cursor) =
793            reflow_wrapped_lines_remapping_cursor(lines, sx, bg, cursor);
794        while reflowed.len() < visible_rows {
795            reflowed.push(GridLine::blank_with_bg(sx, bg));
796        }
797
798        let default_history_rows = reflowed.len().saturating_sub(visible_rows);
799        let mut mapped_cursor = reflow_cursor.unwrap_or(GridPhysicalCursor {
800            absolute_y: reflowed.len().saturating_sub(1),
801            x: sx.saturating_sub(1),
802            pending_wrap: false,
803        });
804        let trailing_empty_rows = reflowed
805            .iter()
806            .rev()
807            .take_while(|line| line.used_end() == 0 && !line.flags.contains(GridLineFlags::WRAPPED))
808            .count();
809        let cursor_shift = default_history_rows.saturating_sub(mapped_cursor.absolute_y);
810        let history_rows =
811            default_history_rows.saturating_sub(cursor_shift.min(trailing_empty_rows));
812        if mapped_cursor.absolute_y < history_rows {
813            mapped_cursor.absolute_y = history_rows;
814            mapped_cursor.x = 0;
815            mapped_cursor.pending_wrap = false;
816        }
817        let mut remaining = reflowed.split_off(history_rows);
818        remaining.truncate(visible_rows);
819        while remaining.len() < visible_rows {
820            remaining.push(GridLine::blank_with_bg(sx, bg));
821        }
822        let mut visible = remaining;
823        for line in &mut visible {
824            line.resize_width_preserving_wrap(sx, bg);
825        }
826        self.history = compacted_history(reflowed);
827        self.recompute_history_content_bytes();
828        self.reflow_history_capacity = if self.history.len() > self.hlimit {
829            self.history.len()
830        } else {
831            0
832        };
833        self.visible = visible.into();
834        self.hscrolled = if self.hlimit == 0 {
835            0
836        } else {
837            self.history.len()
838        };
839        self.sx = sx;
840        mapped_cursor
841    }
842
843    fn can_resize_width_without_reflow(&self, sx: u32) -> bool {
844        self.history.iter().chain(self.visible.iter()).all(|line| {
845            !line.flags.contains(GridLineFlags::WRAPPED) && line.used_end() <= sx as usize
846        })
847    }
848
849    fn effective_history_capacity(&self) -> usize {
850        self.hlimit.max(self.reflow_history_capacity)
851    }
852
853    pub(crate) fn resize_height(&mut self, sy: u32, cursor_y: &mut u32, bg: Colour) {
854        let sy = sy.max(1);
855        let oldy = self.sy;
856
857        if sy < oldy {
858            let mut needed = oldy - sy;
859
860            let available_bottom = oldy.saturating_sub(1).saturating_sub(*cursor_y);
861            let remove_bottom = available_bottom.min(needed);
862            for _ in 0..remove_bottom {
863                let _ = self.visible.pop_back();
864            }
865            needed -= remove_bottom;
866
867            if self.history_enabled {
868                for _ in 0..needed {
869                    let Some(line) = self.visible.pop_front() else {
870                        break;
871                    };
872                    self.push_history_preserving_reflow_capacity(line);
873                }
874            } else {
875                let remove_top = (*cursor_y).min(needed);
876                for _ in 0..remove_top {
877                    let _ = self.visible.pop_front();
878                }
879                *cursor_y = cursor_y.saturating_sub(remove_top);
880            }
881        } else if sy > oldy {
882            let mut needed = sy - oldy;
883            let pull = self.hscrolled.min(needed as usize).min(self.history.len()) as u32;
884            if self.history_enabled && pull > 0 {
885                let mut restored = Vec::with_capacity(pull as usize);
886                for _ in 0..pull {
887                    if let Some(line) = self.pop_history_back() {
888                        restored.push(line);
889                    }
890                }
891                restored.reverse();
892                for mut line in restored.into_iter().rev() {
893                    line.resize_width_preserving_wrap(self.sx, bg);
894                    self.visible.push_front(line);
895                }
896                *cursor_y = cursor_y.saturating_add(pull).min(sy.saturating_sub(1));
897                self.hscrolled -= pull as usize;
898                needed -= pull;
899            }
900
901            for _ in 0..needed {
902                self.visible.push_back(GridLine::blank_with_bg(self.sx, bg));
903            }
904        }
905
906        self.sy = sy;
907        while self.visible.len() > self.sy as usize {
908            let _ = self.visible.pop_back();
909        }
910        while self.visible.len() < self.sy as usize {
911            self.visible.push_back(GridLine::blank_with_bg(self.sx, bg));
912        }
913        for line in &mut self.visible {
914            line.resize_width_preserving_wrap(self.sx, bg);
915        }
916        *cursor_y = (*cursor_y).min(self.sy.saturating_sub(1));
917    }
918
919    fn valid_region(&self, upper: u32, lower: u32) -> bool {
920        upper < self.sy && lower < self.sy && upper <= lower
921    }
922
923    fn total_line_count(&self) -> usize {
924        self.history.len().saturating_add(self.visible.len())
925    }
926
927    fn logical_start_y(&self, absolute_y: usize) -> usize {
928        let mut start = absolute_y.min(self.total_line_count().saturating_sub(1));
929        while start > 0
930            && self
931                .absolute_line(start - 1)
932                .is_some_and(|line| line.flags.contains(GridLineFlags::WRAPPED))
933        {
934            start -= 1;
935        }
936        start
937    }
938
939    fn locate_cursor_from_logical(&self, cursor: GridLogicalCursor) -> GridPhysicalCursor {
940        let total_lines = self.total_line_count();
941        if total_lines == 0 {
942            return GridPhysicalCursor {
943                absolute_y: 0,
944                x: 0,
945                pending_wrap: false,
946            };
947        }
948
949        let start = cursor.logical_start_y.min(total_lines.saturating_sub(1));
950        let mut lines = Vec::new();
951        for absolute_y in start..total_lines {
952            let Some(line) = self.absolute_line(absolute_y) else {
953                break;
954            };
955            lines.push(line.clone());
956            if !line.flags.contains(GridLineFlags::WRAPPED) {
957                break;
958            }
959        }
960
961        if lines.len() == 1 {
962            let used_end = lines[0].used_end();
963            let at_new_edge =
964                used_end > 0 && cursor.offset == used_end && used_end == self.sx as usize;
965            return GridPhysicalCursor {
966                absolute_y: start,
967                x: if at_new_edge {
968                    self.sx.saturating_sub(1)
969                } else {
970                    u32::try_from(cursor.offset)
971                        .unwrap_or(u32::MAX)
972                        .min(self.sx.saturating_sub(1))
973                },
974                pending_wrap: at_new_edge,
975            };
976        }
977
978        let (_, relative) = reflow_wrapped_lines_remapping_cursor(
979            lines,
980            self.sx,
981            COLOUR_DEFAULT,
982            GridLogicalCursor {
983                logical_start_y: 0,
984                offset: cursor.offset,
985            },
986        );
987        let relative = relative.unwrap_or(GridPhysicalCursor {
988            absolute_y: 0,
989            x: 0,
990            pending_wrap: false,
991        });
992        GridPhysicalCursor {
993            absolute_y: start.saturating_add(relative.absolute_y),
994            x: relative.x,
995            pending_wrap: relative.pending_wrap,
996        }
997    }
998
999    fn push_history(&mut self, line: GridLine) {
1000        if self.hlimit == 0 {
1001            return;
1002        }
1003        // Normal terminal output consumes any unused resize-restoration
1004        // budget. Keep only the overflow rows that are still in history before
1005        // inserting the new row.
1006        if self.reflow_history_capacity > 0 {
1007            self.reflow_history_capacity = if self.history.len() > self.hlimit {
1008                self.history.len()
1009            } else {
1010                0
1011            };
1012        }
1013        self.push_history_with_effective_capacity(line);
1014    }
1015
1016    fn push_history_preserving_reflow_capacity(&mut self, line: GridLine) {
1017        if self.hlimit == 0 {
1018            return;
1019        }
1020        // A height shrink may immediately return rows pulled by a preceding
1021        // growth, so it must retain the current reflow restoration budget.
1022        self.push_history_with_effective_capacity(line);
1023    }
1024
1025    fn push_history_with_effective_capacity(&mut self, mut line: GridLine) {
1026        let history_capacity = self.effective_history_capacity();
1027        if history_capacity == 0 {
1028            return;
1029        }
1030
1031        line.stamp_for_history_at(self.next_history_stamp());
1032        line.compact_for_history();
1033        if self.history.len() >= history_capacity {
1034            let _ = self.pop_history_front();
1035        }
1036        self.history_content_bytes = self
1037            .history_content_bytes
1038            .saturating_add(history_line_content_bytes(&line));
1039        self.history.push_back(line);
1040        self.hscrolled = (self.hscrolled + 1).min(self.history.len());
1041    }
1042
1043    fn pop_history_front(&mut self) -> Option<GridLine> {
1044        let line = self.history.pop_front()?;
1045        self.history_content_bytes = self
1046            .history_content_bytes
1047            .saturating_sub(history_line_content_bytes(&line));
1048        Some(line)
1049    }
1050
1051    fn pop_history_back(&mut self) -> Option<GridLine> {
1052        let line = self.history.pop_back()?;
1053        self.history_content_bytes = self
1054            .history_content_bytes
1055            .saturating_sub(history_line_content_bytes(&line));
1056        Some(line)
1057    }
1058
1059    fn recompute_history_content_bytes(&mut self) {
1060        self.history_content_bytes = self.history.iter().map(history_line_content_bytes).sum();
1061    }
1062
1063    fn next_history_stamp(&mut self) -> i64 {
1064        if self.history_stamp_remaining == 0 {
1065            self.history_stamp = cell::current_unix_timestamp();
1066            self.history_stamp_remaining = HISTORY_STAMP_REFRESH_LINES;
1067        }
1068        self.history_stamp_remaining = self.history_stamp_remaining.saturating_sub(1);
1069        self.history_stamp
1070    }
1071}
1072
1073fn history_line_content_bytes(line: &GridLine) -> usize {
1074    line.rendered_text_len().saturating_add(1)
1075}
1076
1077fn compacted_history(lines: Vec<GridLine>) -> VecDeque<GridLine> {
1078    lines
1079        .into_iter()
1080        .map(|mut line| {
1081            line.compact_for_history();
1082            line
1083        })
1084        .collect()
1085}
1086
1087struct AlternateReflowGroup {
1088    source_rows: usize,
1089    rows: Vec<GridLine>,
1090}
1091
1092fn reflow_alternate_visible_lines(
1093    lines: Vec<GridLine>,
1094    width: u32,
1095    bg: Colour,
1096    visible_rows: usize,
1097) -> Vec<GridLine> {
1098    let mut source_groups = Vec::new();
1099    let mut current_group = Vec::new();
1100    for line in lines {
1101        let wrapped = line.flags.contains(GridLineFlags::WRAPPED);
1102        current_group.push(line);
1103        if !wrapped {
1104            source_groups.push(std::mem::take(&mut current_group));
1105        }
1106    }
1107    if !current_group.is_empty() {
1108        source_groups.push(current_group);
1109    }
1110
1111    let trailing_blank_groups = source_groups
1112        .iter()
1113        .rev()
1114        .take_while(|group| {
1115            group
1116                .iter()
1117                .all(|line| line.used_end() == 0 && line.flags == GridLineFlags::default())
1118        })
1119        .count();
1120    source_groups.truncate(source_groups.len().saturating_sub(trailing_blank_groups));
1121
1122    let mut groups = source_groups
1123        .into_iter()
1124        .map(|group| {
1125            let source_rows = group.len();
1126            let (rows, _) = reflow_wrapped_lines_remapping_cursor(
1127                group,
1128                width,
1129                bg,
1130                GridLogicalCursor {
1131                    logical_start_y: usize::MAX,
1132                    offset: 0,
1133                },
1134            );
1135            AlternateReflowGroup { source_rows, rows }
1136        })
1137        .collect::<Vec<_>>();
1138
1139    let mut allocations = groups
1140        .iter()
1141        .map(|group| group.source_rows.min(group.rows.len()))
1142        .collect::<Vec<_>>();
1143    let allocated = allocations.iter().copied().sum::<usize>();
1144    let mut remaining = visible_rows.saturating_sub(allocated);
1145    for (group, allocation) in groups.iter().zip(&mut allocations) {
1146        let extra = group.rows.len().saturating_sub(*allocation).min(remaining);
1147        *allocation = allocation.saturating_add(extra);
1148        remaining -= extra;
1149    }
1150
1151    let mut visible = Vec::with_capacity(visible_rows);
1152    for (mut group, allocation) in groups.drain(..).zip(allocations) {
1153        let truncated = allocation < group.rows.len();
1154        group.rows.truncate(allocation);
1155        if truncated {
1156            if let Some(last) = group.rows.last_mut() {
1157                last.set_wrapped(false);
1158            }
1159        }
1160        visible.extend(group.rows);
1161    }
1162    visible.truncate(visible_rows);
1163    while visible.len() < visible_rows {
1164        visible.push(GridLine::blank_with_bg(width, bg));
1165    }
1166    visible
1167}
1168
1169fn reflow_wrapped_lines_remapping_cursor(
1170    lines: Vec<GridLine>,
1171    width: u32,
1172    bg: Colour,
1173    cursor: GridLogicalCursor,
1174) -> (Vec<GridLine>, Option<GridPhysicalCursor>) {
1175    let mut output = Vec::new();
1176    let mut logical_cells = Vec::new();
1177    let mut logical_plain_text: Option<String> = None;
1178    let mut logical_flags = None;
1179    let mut logical_start_y = 0_usize;
1180    let mut mapped_cursor = None;
1181
1182    for (absolute_y, line) in lines.into_iter().enumerate() {
1183        let wrapped = line.flags.contains(GridLineFlags::WRAPPED);
1184        if logical_flags.is_none() {
1185            logical_start_y = absolute_y;
1186            let mut flags = line.flags;
1187            flags.remove(GridLineFlags::WRAPPED);
1188            logical_flags = Some(flags);
1189            logical_plain_text = (bg == COLOUR_DEFAULT).then(String::new);
1190        }
1191
1192        let end = if wrapped {
1193            self::line_width(&line)
1194        } else {
1195            line.used_end()
1196        };
1197        if let (Some(logical_text), Some(text)) = (logical_plain_text.as_mut(), line.plain_text()) {
1198            logical_text.extend(
1199                text.bytes()
1200                    .chain(std::iter::repeat(b' '))
1201                    .take(end)
1202                    .map(char::from),
1203            );
1204        } else {
1205            if let Some(text) = logical_plain_text.take() {
1206                extend_plain_ascii_cells(&mut logical_cells, text.bytes());
1207            }
1208            if let Some(text) = line.plain_text() {
1209                extend_plain_ascii_cells(
1210                    &mut logical_cells,
1211                    text.bytes().chain(std::iter::repeat(b' ')).take(end),
1212                );
1213            } else {
1214                logical_cells.extend(
1215                    line.cells
1216                        .iter()
1217                        .take(end)
1218                        .filter(|cell| !cell.is_padding() && !cell.is_reflow_gap())
1219                        .cloned(),
1220                );
1221            }
1222        }
1223
1224        if !wrapped {
1225            let flags = logical_flags.take().unwrap_or_default();
1226            let cursor_offset =
1227                (logical_start_y == cursor.logical_start_y).then_some(cursor.offset);
1228            let (reflowed, relative_cursor) = if let Some(text) = logical_plain_text.take() {
1229                reflow_plain_ascii_line_remapping_cursor(&text, flags, width, bg, cursor_offset)
1230            } else {
1231                reflow_logical_line_remapping_cursor(
1232                    &logical_cells,
1233                    flags,
1234                    width,
1235                    bg,
1236                    cursor_offset,
1237                )
1238            };
1239            if let Some(mut physical) = relative_cursor {
1240                physical.absolute_y = physical.absolute_y.saturating_add(output.len());
1241                mapped_cursor = Some(physical);
1242            }
1243            output.extend(reflowed);
1244            logical_cells.clear();
1245        }
1246    }
1247
1248    if logical_flags.is_some() || !logical_cells.is_empty() || logical_plain_text.is_some() {
1249        let flags = logical_flags.unwrap_or_default();
1250        let cursor_offset = (logical_start_y == cursor.logical_start_y).then_some(cursor.offset);
1251        let (reflowed, relative_cursor) = if let Some(text) = logical_plain_text {
1252            reflow_plain_ascii_line_remapping_cursor(&text, flags, width, bg, cursor_offset)
1253        } else {
1254            reflow_logical_line_remapping_cursor(&logical_cells, flags, width, bg, cursor_offset)
1255        };
1256        if let Some(mut physical) = relative_cursor {
1257            physical.absolute_y = physical.absolute_y.saturating_add(output.len());
1258            mapped_cursor = Some(physical);
1259        }
1260        output.extend(reflowed);
1261    }
1262
1263    (output, mapped_cursor)
1264}
1265
1266fn extend_plain_ascii_cells(cells: &mut Vec<GridCell>, bytes: impl IntoIterator<Item = u8>) {
1267    cells.extend(bytes.into_iter().map(GridCell::from_plain_ascii));
1268}
1269
1270fn reflow_plain_ascii_line_remapping_cursor(
1271    text: &str,
1272    first_flags: GridLineFlags,
1273    width: u32,
1274    bg: Colour,
1275    cursor_offset: Option<usize>,
1276) -> (Vec<GridLine>, Option<GridPhysicalCursor>) {
1277    let width = width.max(1);
1278    if text.is_empty() || bg != COLOUR_DEFAULT {
1279        let mut line = GridLine::blank_with_bg(width, bg);
1280        line.flags = first_flags;
1281        let cursor = cursor_offset.map(|offset| GridPhysicalCursor {
1282            absolute_y: 0,
1283            x: u32::try_from(offset)
1284                .unwrap_or(u32::MAX)
1285                .min(width.saturating_sub(1)),
1286            pending_wrap: false,
1287        });
1288        return (vec![line], cursor);
1289    }
1290
1291    let width_usize = width as usize;
1292    let mut output = Vec::with_capacity(text.len().div_ceil(width_usize));
1293    let mut start = 0;
1294    let mut flags = first_flags;
1295    while start < text.len() {
1296        let end = (start + width_usize).min(text.len());
1297        let mut line = GridLine::from_plain_ascii_text(width, flags, text[start..end].to_owned());
1298        if end < text.len() {
1299            line.set_wrapped(true);
1300        }
1301        output.push(line);
1302        flags = GridLineFlags::default();
1303        start = end;
1304    }
1305
1306    let cursor = cursor_offset.map(|offset| {
1307        let content_len = text.len();
1308        if offset == content_len && content_len > 0 && content_len.is_multiple_of(width_usize) {
1309            return GridPhysicalCursor {
1310                absolute_y: content_len.div_ceil(width_usize).saturating_sub(1),
1311                x: width.saturating_sub(1),
1312                pending_wrap: true,
1313            };
1314        }
1315        if offset <= content_len {
1316            return GridPhysicalCursor {
1317                absolute_y: offset / width_usize,
1318                x: u32::try_from(offset % width_usize).unwrap_or(u32::MAX),
1319                pending_wrap: false,
1320            };
1321        }
1322        GridPhysicalCursor {
1323            absolute_y: output.len().saturating_sub(1),
1324            x: u32::try_from(offset)
1325                .unwrap_or(u32::MAX)
1326                .min(width.saturating_sub(1)),
1327            pending_wrap: false,
1328        }
1329    });
1330    (output, cursor)
1331}
1332
1333fn reflow_logical_line_remapping_cursor(
1334    cells: &[GridCell],
1335    first_flags: GridLineFlags,
1336    width: u32,
1337    bg: Colour,
1338    cursor_offset: Option<usize>,
1339) -> (Vec<GridLine>, Option<GridPhysicalCursor>) {
1340    let width = width.max(1);
1341    if cells.is_empty() {
1342        let mut line = GridLine::blank_with_bg(width, bg);
1343        line.flags = first_flags;
1344        let cursor = cursor_offset.map(|offset| GridPhysicalCursor {
1345            absolute_y: 0,
1346            x: u32::try_from(offset)
1347                .unwrap_or(u32::MAX)
1348                .min(width.saturating_sub(1)),
1349            pending_wrap: false,
1350        });
1351        return (vec![line], cursor);
1352    }
1353
1354    let mut output = Vec::new();
1355    let mut current = GridLine::blank_with_bg(width, bg);
1356    current.flags = first_flags;
1357    let mut x: u32 = 0;
1358    let mut logical_offset = 0_usize;
1359    let mut mapped_cursor = None;
1360
1361    for cell in cells {
1362        let mut cell = cell.clone();
1363        let source_cell_width = u32::from(cell.width().max(1));
1364        let mut cell_width = source_cell_width;
1365        if cell_width > width {
1366            cell_width = 1;
1367            cell.set_width(1);
1368        }
1369        if x > 0 && x.saturating_add(cell_width) > width {
1370            current.mark_reflow_gap(x);
1371            current.set_wrapped(true);
1372            output.push(current);
1373            current = GridLine::blank_with_bg(width, bg);
1374            x = 0;
1375        }
1376
1377        if mapped_cursor.is_none() {
1378            if let Some(cursor_offset) = cursor_offset {
1379                let cell_end = logical_offset.saturating_add(source_cell_width as usize);
1380                if cursor_offset >= logical_offset && cursor_offset < cell_end {
1381                    let relative = cursor_offset.saturating_sub(logical_offset);
1382                    let physical_offset =
1383                        u32::try_from(relative).unwrap_or(u32::MAX).min(cell_width);
1384                    if physical_offset == cell_width && x.saturating_add(cell_width) == width {
1385                        mapped_cursor = Some(GridPhysicalCursor {
1386                            absolute_y: output.len(),
1387                            x: width.saturating_sub(1),
1388                            pending_wrap: true,
1389                        });
1390                    } else {
1391                        mapped_cursor = Some(GridPhysicalCursor {
1392                            absolute_y: output.len(),
1393                            x: x.saturating_add(physical_offset)
1394                                .min(width.saturating_sub(1)),
1395                            pending_wrap: false,
1396                        });
1397                    }
1398                }
1399            }
1400        }
1401
1402        if let Some(target) = current.cell_mut(x) {
1403            *target = cell.clone();
1404        }
1405        for offset in 1..cell_width {
1406            if let Some(padding_cell) = current.cell_mut(x + offset) {
1407                let mut padding = cell.clone();
1408                padding.set_text(" ".to_owned());
1409                padding.set_width(0);
1410                padding.set_flags(GridCellFlags::PADDING);
1411                *padding_cell = padding;
1412            }
1413        }
1414        current.touch();
1415        x += cell_width;
1416        logical_offset = logical_offset.saturating_add(source_cell_width as usize);
1417    }
1418
1419    if mapped_cursor.is_none() {
1420        if let Some(cursor_offset) = cursor_offset {
1421            if cursor_offset == logical_offset {
1422                mapped_cursor = Some(if x == width {
1423                    GridPhysicalCursor {
1424                        absolute_y: output.len(),
1425                        x: width.saturating_sub(1),
1426                        pending_wrap: true,
1427                    }
1428                } else {
1429                    GridPhysicalCursor {
1430                        absolute_y: output.len(),
1431                        x,
1432                        pending_wrap: false,
1433                    }
1434                });
1435            } else if cursor_offset > logical_offset {
1436                mapped_cursor = Some(GridPhysicalCursor {
1437                    absolute_y: output.len(),
1438                    x: u32::try_from(cursor_offset)
1439                        .unwrap_or(u32::MAX)
1440                        .min(width.saturating_sub(1)),
1441                    pending_wrap: false,
1442                });
1443            }
1444        }
1445    }
1446    output.push(current);
1447    (output, mapped_cursor)
1448}
1449
1450fn line_width(line: &GridLine) -> usize {
1451    line.width() as usize
1452}
1453
1454#[cfg(test)]
1455#[path = "grid/tests.rs"]
1456mod tests;