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