Skip to main content

rmux_core/grid/
cell.rs

1use compact_str::CompactString;
2use std::sync::atomic::{AtomicU64, Ordering};
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use crate::hyperlinks::Hyperlinks;
6use crate::input::{CellState, Colour, GridAttr, COLOUR_DEFAULT, COLOUR_NONE, COLOUR_TERMINAL};
7use crate::style::Style;
8
9use super::{
10    append_cell_text, append_grid_string_code, append_hyperlink, GridRenderOptions, GridStringState,
11};
12
13/// Per-cell flags matching tmux `GRID_FLAG_*`, plus internal-only render metadata.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub(crate) struct GridCellFlags(u8);
16
17#[allow(dead_code)]
18impl GridCellFlags {
19    /// This cell is a padding cell belonging to a wide glyph.
20    pub const PADDING: Self = Self(0x1);
21    /// This cell was produced by a clear operation.
22    pub const CLEARED: Self = Self(0x2);
23    /// This cell represents tab-expanded whitespace.
24    pub const TAB: Self = Self(0x4);
25    /// This cell is part of an alternate representation.
26    pub const EXTENDED: Self = Self(0x8);
27    /// This cell is selected.
28    pub const SELECTED: Self = Self(0x10);
29    /// This cell should not inherit the palette.
30    pub const NOPALETTE: Self = Self(0x20);
31    /// Internal marker for an unused physical cell left before a wrapped wide glyph.
32    pub const REFLOW_GAP: Self = Self(0x40);
33
34    /// Returns the raw bit value.
35    #[must_use]
36    pub const fn bits(self) -> u8 {
37        self.0
38    }
39
40    /// Returns whether all bits from `other` are present.
41    #[must_use]
42    pub const fn contains(self, other: Self) -> bool {
43        self.0 & other.0 == other.0
44    }
45
46    /// Adds the bits from `other`.
47    pub fn insert(&mut self, other: Self) {
48        self.0 |= other.0;
49    }
50
51    /// Removes the bits from `other`.
52    pub fn remove(&mut self, other: Self) {
53        self.0 &= !other.0;
54    }
55}
56
57/// Columns a rendered grid line paints when it is replayed into a terminal.
58///
59/// A replay places the following line either by autowrap or by an explicit
60/// line break, and that choice depends on the columns the line occupies, not
61/// on the byte length of its escape-laden rendering.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
63pub struct RenderedLineSpan {
64    columns: u32,
65    leading_columns: u32,
66}
67
68impl RenderedLineSpan {
69    /// Returns the columns painted by the whole line.
70    #[must_use]
71    pub const fn columns(self) -> u32 {
72        self.columns
73    }
74
75    /// Returns the columns painted by the first cell, zero when the line
76    /// paints nothing.
77    #[must_use]
78    pub const fn leading_columns(self) -> u32 {
79        self.leading_columns
80    }
81
82    /// Builds a span for a line of single-column cells.
83    const fn leading_one(columns: u32) -> Self {
84        Self {
85            columns,
86            leading_columns: if columns == 0 { 0 } else { 1 },
87        }
88    }
89
90    fn push(&mut self, width: u32) {
91        if self.columns == 0 {
92            self.leading_columns = width;
93        }
94        self.columns = self.columns.saturating_add(width);
95    }
96
97    fn pop_column(&mut self) {
98        self.columns = self.columns.saturating_sub(1);
99        if self.columns == 0 {
100            self.leading_columns = 0;
101        }
102    }
103}
104
105/// Per-line flags matching tmux `GRID_LINE_*`.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
107pub(crate) struct GridLineFlags(u8);
108
109#[allow(dead_code)]
110impl GridLineFlags {
111    /// The logical line continues on the following row.
112    pub const WRAPPED: Self = Self(0x1);
113    /// The line uses extended cell storage.
114    pub const EXTENDED: Self = Self(0x2);
115    /// The line is dead.
116    pub const DEAD: Self = Self(0x4);
117    /// The line starts a shell prompt block.
118    pub const START_PROMPT: Self = Self(0x8);
119    /// The line starts a shell output block.
120    pub const START_OUTPUT: Self = Self(0x10);
121
122    /// Returns the raw bit value.
123    #[must_use]
124    pub const fn bits(self) -> u8 {
125        self.0
126    }
127
128    /// Returns whether all bits from `other` are present.
129    #[must_use]
130    pub const fn contains(self, other: Self) -> bool {
131        self.0 & other.0 == other.0
132    }
133
134    /// Adds the bits from `other`.
135    pub fn insert(&mut self, other: Self) {
136        self.0 |= other.0;
137    }
138
139    /// Removes the bits from `other`.
140    pub fn remove(&mut self, other: Self) {
141        self.0 &= !other.0;
142    }
143}
144
145/// One stored grid cell, including text and style.
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub(crate) struct GridCell {
148    pub(super) text: CellText,
149    width: u8,
150    pub(super) flags: GridCellFlags,
151    attr: u16,
152    fg: Colour,
153    bg: Colour,
154    us: Colour,
155    link: u32,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub(super) struct CellText {
160    inline: [u8; 4],
161    inline_len: u8,
162    extended: Option<Box<CompactString>>,
163}
164
165impl CellText {
166    fn from_char(ch: char) -> Self {
167        let mut inline = [0_u8; 4];
168        let inline_len = ch.encode_utf8(&mut inline).len() as u8;
169        Self {
170            inline,
171            inline_len,
172            extended: None,
173        }
174    }
175
176    fn from_ascii_byte(byte: u8) -> Self {
177        debug_assert!(byte.is_ascii());
178        Self {
179            inline: [byte, 0, 0, 0],
180            inline_len: 1,
181            extended: None,
182        }
183    }
184
185    fn new(text: String) -> Self {
186        let mut chars = text.chars();
187        if let Some(ch) = chars.next() {
188            if chars.next().is_none() {
189                return Self::from_char(ch);
190            }
191        }
192        Self {
193            inline: [0_u8; 4],
194            inline_len: 0,
195            extended: Some(Box::new(CompactString::new(text))),
196        }
197    }
198
199    pub(super) fn as_str(&self) -> &str {
200        if let Some(text) = &self.extended {
201            return text.as_str();
202        }
203        std::str::from_utf8(&self.inline[..usize::from(self.inline_len)])
204            .expect("inline cell text must be valid utf-8")
205    }
206
207    fn is_single_space(&self) -> bool {
208        self.extended.is_none() && self.inline_len == 1 && self.inline[0] == b' '
209    }
210}
211
212impl Default for GridCell {
213    fn default() -> Self {
214        Self::blank_with_bg(COLOUR_DEFAULT)
215    }
216}
217
218#[allow(dead_code)]
219impl GridCell {
220    /// Creates a blank cell with the given background colour.
221    #[must_use]
222    pub fn blank_with_bg(bg: Colour) -> Self {
223        Self {
224            text: CellText::from_char(' '),
225            width: 1,
226            flags: GridCellFlags::CLEARED,
227            attr: 0,
228            fg: COLOUR_DEFAULT,
229            bg,
230            us: COLOUR_DEFAULT,
231            link: 0,
232        }
233    }
234
235    /// Creates a printable cell from the parser cell state.
236    #[must_use]
237    pub fn from_state(ch: char, width: u8, state: &CellState, flags: GridCellFlags) -> Self {
238        let mut resolved_flags = flags;
239        resolved_flags.remove(GridCellFlags::CLEARED);
240        Self {
241            text: CellText::from_char(ch),
242            width,
243            flags: resolved_flags,
244            attr: state.attr(),
245            fg: state.fg(),
246            bg: state.bg(),
247            us: state.us(),
248            link: state.link(),
249        }
250    }
251
252    pub(super) fn from_plain_ascii(byte: u8) -> Self {
253        Self {
254            text: CellText::from_ascii_byte(byte),
255            width: 1,
256            flags: GridCellFlags::default(),
257            attr: 0,
258            fg: COLOUR_DEFAULT,
259            bg: COLOUR_DEFAULT,
260            us: COLOUR_DEFAULT,
261            link: 0,
262        }
263    }
264
265    fn set_plain_ascii(&mut self, byte: u8) {
266        self.text = CellText::from_ascii_byte(byte);
267        self.width = 1;
268        self.flags = GridCellFlags::default();
269        self.attr = 0;
270        self.fg = COLOUR_DEFAULT;
271        self.bg = COLOUR_DEFAULT;
272        self.us = COLOUR_DEFAULT;
273        self.link = 0;
274    }
275
276    /// Returns the stored text payload.
277    #[must_use]
278    pub fn text(&self) -> &str {
279        self.text.as_str()
280    }
281
282    /// Returns the display width of the cell.
283    #[must_use]
284    pub const fn width(&self) -> u8 {
285        self.width
286    }
287
288    /// Returns the cell flags.
289    #[must_use]
290    pub const fn flags(&self) -> GridCellFlags {
291        self.flags
292    }
293
294    /// Returns whether this cell is a padding cell.
295    #[must_use]
296    pub const fn is_padding(&self) -> bool {
297        self.flags.contains(GridCellFlags::PADDING)
298    }
299
300    /// Returns whether this blank cell exists only to place a following wide glyph.
301    #[must_use]
302    pub const fn is_reflow_gap(&self) -> bool {
303        self.flags.contains(GridCellFlags::REFLOW_GAP)
304    }
305
306    /// Returns the cell attributes.
307    #[must_use]
308    pub const fn attr(&self) -> u16 {
309        self.attr
310    }
311
312    /// Returns the foreground colour.
313    #[must_use]
314    pub const fn fg(&self) -> Colour {
315        self.fg
316    }
317
318    /// Returns the background colour.
319    #[must_use]
320    pub const fn bg(&self) -> Colour {
321        self.bg
322    }
323
324    /// Returns the underline colour.
325    #[must_use]
326    pub const fn us(&self) -> Colour {
327        self.us
328    }
329
330    /// Returns the hyperlink inner ID.
331    #[must_use]
332    pub const fn link(&self) -> u32 {
333        self.link
334    }
335
336    /// Returns whether the cell is visually blank.
337    #[must_use]
338    pub fn is_blank(&self) -> bool {
339        self.flags.contains(GridCellFlags::CLEARED)
340            && !self.flags.contains(GridCellFlags::PADDING)
341            && self.width == 1
342            && self.text.is_single_space()
343            && self.attr == 0
344            && self.fg == COLOUR_DEFAULT
345            && self.bg == COLOUR_DEFAULT
346            && self.us == COLOUR_DEFAULT
347            && self.link == 0
348    }
349
350    fn is_explicit_default_space(&self) -> bool {
351        !self.flags.contains(GridCellFlags::CLEARED)
352            && !self.flags.contains(GridCellFlags::PADDING)
353            && self.width == 1
354            && self.text.is_single_space()
355            && self.has_default_style()
356    }
357
358    fn has_default_style(&self) -> bool {
359        self.attr == 0
360            && self.fg == COLOUR_DEFAULT
361            && self.bg == COLOUR_DEFAULT
362            && self.us == COLOUR_DEFAULT
363            && self.link == 0
364    }
365
366    fn has_non_default_style(&self) -> bool {
367        !self.has_default_style()
368    }
369
370    pub(crate) fn set_text(&mut self, text: String) {
371        self.text = CellText::new(text);
372    }
373
374    pub(crate) fn set_width(&mut self, width: u8) {
375        self.width = width;
376    }
377
378    pub(crate) fn set_flags(&mut self, flags: GridCellFlags) {
379        self.flags = flags;
380    }
381
382    pub(crate) fn set_attr(&mut self, attr: u16) {
383        self.attr = attr;
384    }
385
386    pub(crate) fn set_fg(&mut self, fg: Colour) {
387        self.fg = fg;
388    }
389
390    pub(crate) fn set_bg(&mut self, bg: Colour) {
391        self.bg = bg;
392    }
393
394    pub(crate) fn set_us(&mut self, us: Colour) {
395        self.us = us;
396    }
397
398    fn is_plain_default_ascii(&self) -> bool {
399        self.width == 1
400            && self.flags == GridCellFlags::default()
401            && self.attr == 0
402            && self.fg == COLOUR_DEFAULT
403            && self.bg == COLOUR_DEFAULT
404            && self.us == COLOUR_DEFAULT
405            && self.link == 0
406            && self.text.as_str().is_ascii()
407            && self.text.as_str().len() == 1
408    }
409}
410
411/// One absolute grid line.
412#[derive(Debug, Clone, PartialEq, Eq)]
413pub(crate) struct GridLine {
414    pub(super) cells: Vec<GridCell>,
415    plain_text: Option<CompactString>,
416    width: u32,
417    /// Exclusive end of compact ASCII data, including trailing spaces omitted
418    /// from `plain_text`. The remaining physical suffix is still cleared.
419    plain_text_data_end: u16,
420    pub(super) flags: GridLineFlags,
421    time: i64,
422    revision: u64,
423}
424
425impl GridLine {
426    /// Creates a blank line with `width` cells.
427    #[must_use]
428    pub fn new(width: u32) -> Self {
429        Self {
430            cells: Vec::new(),
431            plain_text: Some(CompactString::new("")),
432            width,
433            plain_text_data_end: 0,
434            flags: GridLineFlags::default(),
435            time: 0,
436            revision: next_line_revision(),
437        }
438    }
439
440    /// Creates a blank line with a specific background colour.
441    #[must_use]
442    pub fn blank_with_bg(width: u32, bg: Colour) -> Self {
443        if bg == COLOUR_DEFAULT {
444            return Self::new(width);
445        }
446        Self {
447            cells: vec![GridCell::blank_with_bg(bg); width as usize],
448            plain_text: None,
449            width,
450            plain_text_data_end: 0,
451            flags: GridLineFlags::default(),
452            time: 0,
453            revision: next_line_revision(),
454        }
455    }
456
457    pub(super) fn from_plain_ascii_text(width: u32, flags: GridLineFlags, text: String) -> Self {
458        debug_assert!(text.is_ascii());
459        let plain_text_data_end = u16::try_from(text.len()).unwrap_or(u16::MAX);
460        Self {
461            cells: Vec::new(),
462            plain_text: Some(CompactString::from(text)),
463            width,
464            plain_text_data_end,
465            flags,
466            time: 0,
467            revision: next_line_revision(),
468        }
469    }
470
471    /// Returns all cells in the line.
472    #[must_use]
473    pub fn cells(&self) -> &[GridCell] {
474        &self.cells
475    }
476
477    #[must_use]
478    pub(super) const fn width(&self) -> u32 {
479        self.width
480    }
481
482    pub(crate) fn plain_text(&self) -> Option<&str> {
483        self.plain_text.as_ref().map(CompactString::as_str)
484    }
485
486    /// Returns a mutable cell by column.
487    pub(crate) fn cell_mut(&mut self, x: u32) -> Option<&mut GridCell> {
488        self.materialize_for_cell_mutation();
489        self.cells.get_mut(x as usize)
490    }
491
492    pub(crate) fn materialize_for_cell_mutation(&mut self) {
493        if self.plain_text.is_some() {
494            self.materialize_plain_text(self.width as usize);
495        }
496    }
497
498    pub(crate) fn insert_cells(&mut self, start: u32, count: u32, blank: &GridCell) {
499        self.materialize_for_cell_mutation();
500        let reflow_gap_count = self
501            .cells
502            .iter()
503            .filter(|cell| cell.is_reflow_gap())
504            .count();
505        let start = start as usize;
506        if start >= self.cells.len() {
507            return;
508        }
509        let count = (count as usize).min(self.cells.len() - start);
510        if count == 0 {
511            return;
512        }
513        self.cells[start..].rotate_right(count);
514        for cell in &mut self.cells[start..start + count] {
515            *cell = blank.clone();
516        }
517        // tmux 3.7b raises cellused to the row width when ICH moves cells,
518        // but a clear at the final column leaves the existing gap untouched.
519        if start + 1 == self.cells.len() {
520            self.restore_trailing_reflow_gaps(reflow_gap_count);
521        } else {
522            self.materialize_reflow_gaps();
523        }
524    }
525
526    pub(crate) fn delete_cells(&mut self, start: u32, count: u32, blank: &GridCell) {
527        self.materialize_for_cell_mutation();
528        let reflow_gap_count = self
529            .cells
530            .iter()
531            .filter(|cell| cell.is_reflow_gap())
532            .count();
533        let start = start as usize;
534        if start >= self.cells.len() {
535            return;
536        }
537        let count = (count as usize).min(self.cells.len() - start);
538        if count == 0 {
539            return;
540        }
541        self.cells[start..].rotate_left(count);
542        let fill_start = self.cells.len() - count;
543        for cell in &mut self.cells[fill_start..] {
544            *cell = blank.clone();
545        }
546        // DCH keeps cellused at max(old cellused, width - count), so only the
547        // overlapping suffix remains a structural placement gap.
548        self.restore_trailing_reflow_gaps(reflow_gap_count.min(count));
549    }
550
551    pub(crate) fn write_plain_ascii_run(&mut self, start: u32, bytes: &[u8]) -> bool {
552        if bytes.is_empty() {
553            return true;
554        }
555        let start = start as usize;
556        let Some(end) = start.checked_add(bytes.len()) else {
557            return false;
558        };
559        if end > self.width as usize {
560            return false;
561        }
562        if self.plain_text.is_some() && self.cells.is_empty() {
563            self.update_plain_text_cache(start, bytes);
564            self.touch();
565            return true;
566        }
567        if end > self.cells.len() {
568            return false;
569        }
570        if self.cells[start..end]
571            .iter()
572            .any(|cell| cell.width() != 1 || cell.is_padding())
573        {
574            return false;
575        }
576        if self.cells.get(end).is_some_and(GridCell::is_padding) {
577            return false;
578        }
579
580        let can_cache_plain_text = self.can_cache_plain_ascii_run(start);
581        for (cell, byte) in self.cells[start..end].iter_mut().zip(bytes) {
582            cell.set_plain_ascii(*byte);
583        }
584        if can_cache_plain_text {
585            self.update_plain_text_cache(start, bytes);
586        } else {
587            self.plain_text = None;
588            self.plain_text_data_end = 0;
589        }
590        self.touch();
591        true
592    }
593
594    /// Returns an immutable cell by column.
595    #[must_use]
596    pub fn cell(&self, x: u32) -> Option<GridCell> {
597        if x >= self.width {
598            return None;
599        }
600        if let Some(text) = &self.plain_text {
601            if let Some(byte) = text.as_bytes().get(x as usize).copied() {
602                return Some(GridCell::from_plain_ascii(byte));
603            }
604            return Some(if x < u32::from(self.plain_text_data_end) {
605                GridCell::from_plain_ascii(b' ')
606            } else {
607                GridCell::default()
608            });
609        }
610        self.cells.get(x as usize).cloned()
611    }
612
613    /// Returns whether the cell at the given column is padding.
614    #[must_use]
615    pub fn is_padding_cell(&self, x: u32) -> bool {
616        if self.plain_text.is_some() {
617            return false;
618        }
619        self.cells.get(x as usize).is_some_and(GridCell::is_padding)
620    }
621
622    /// Returns the owning non-padding cell for the column, when present.
623    #[must_use]
624    pub fn owning_cell_x(&self, x: u32) -> Option<u32> {
625        if self.plain_text.is_some() {
626            return (x < self.width).then_some(x);
627        }
628        let cell = self.cell(x)?;
629        if !cell.is_padding() {
630            return Some(x);
631        }
632
633        let mut owner = x;
634        while owner > 0 {
635            owner -= 1;
636            let cell = self.cell(owner)?;
637            if !cell.is_padding() {
638                let width = u32::from(cell.width().max(1));
639                if owner.saturating_add(width) > x {
640                    return Some(owner);
641                }
642                return None;
643            }
644        }
645        None
646    }
647
648    /// Returns the line flags.
649    #[must_use]
650    pub const fn flags(&self) -> GridLineFlags {
651        self.flags
652    }
653
654    /// Returns the last mutation timestamp.
655    #[allow(dead_code)]
656    #[must_use]
657    pub const fn time(&self) -> i64 {
658        self.time
659    }
660
661    #[must_use]
662    pub(crate) const fn revision(&self) -> u64 {
663        self.revision
664    }
665
666    pub(crate) fn touch(&mut self) {
667        let line_id = self.revision & LINE_REVISION_ID_MASK;
668        let generation = (self.revision & LINE_REVISION_GENERATION_MASK).saturating_add(1);
669        self.revision = line_id | generation.min(LINE_REVISION_GENERATION_MASK);
670    }
671
672    pub(crate) fn stamp_for_history_at(&mut self, timestamp: i64) {
673        if self.time != 0 {
674            return;
675        }
676        self.time = timestamp;
677    }
678
679    pub(crate) fn set_wrapped(&mut self, wrapped: bool) {
680        let was_wrapped = self.flags.contains(GridLineFlags::WRAPPED);
681        if wrapped {
682            self.flags.insert(GridLineFlags::WRAPPED);
683        } else {
684            self.flags.remove(GridLineFlags::WRAPPED);
685        }
686        if was_wrapped != wrapped {
687            self.touch();
688        }
689    }
690
691    pub(crate) fn clear(&mut self, bg: Colour) {
692        if bg == COLOUR_DEFAULT {
693            self.cells.clear();
694            self.plain_text = Some(CompactString::new(""));
695            self.plain_text_data_end = 0;
696        } else {
697            self.plain_text = None;
698            self.plain_text_data_end = 0;
699            self.cells
700                .resize(self.width as usize, GridCell::blank_with_bg(bg));
701            self.cells.fill(GridCell::blank_with_bg(bg));
702        }
703        self.flags = GridLineFlags::default();
704        self.touch();
705    }
706
707    pub(crate) fn resize_width_preserving_wrap(&mut self, width: u32, bg: Colour) {
708        self.resize_width_internal(width, bg, true);
709    }
710
711    pub(crate) fn compact_for_history(&mut self) {
712        if self.try_compact_plain_text() {
713            return;
714        }
715        if self.flags.contains(GridLineFlags::WRAPPED) {
716            return;
717        }
718
719        let used_end = self.used_end();
720        if used_end < self.cells.len() {
721            self.cells.truncate(used_end);
722            self.cells.shrink_to_fit();
723        }
724    }
725
726    fn resize_width_internal(&mut self, width: u32, bg: Colour, preserve_wrap: bool) {
727        let width = width as usize;
728        let old_width = self.width as usize;
729        if self.plain_text.is_some() && bg == COLOUR_DEFAULT {
730            if let Some(text) = &mut self.plain_text {
731                if text.len() > width {
732                    text.truncate(width);
733                }
734            }
735            self.plain_text_data_end = self
736                .plain_text_data_end
737                .min(u16::try_from(width).unwrap_or(u16::MAX));
738            self.width = u32::try_from(width).unwrap_or(u32::MAX);
739            let wrapped_before = self.flags.contains(GridLineFlags::WRAPPED);
740            if !preserve_wrap {
741                self.flags.remove(GridLineFlags::WRAPPED);
742            }
743            if old_width != width || (wrapped_before && !preserve_wrap) {
744                self.touch();
745            }
746            return;
747        }
748        if self.plain_text.is_some() {
749            self.materialize_plain_text(width);
750        }
751        let resized = self.cells.len() != width;
752        if resized {
753            self.cells.resize(width, GridCell::blank_with_bg(bg));
754            self.width = u32::try_from(width).unwrap_or(u32::MAX);
755        }
756        let wrapped_before = self.flags.contains(GridLineFlags::WRAPPED);
757        if !preserve_wrap {
758            self.flags.remove(GridLineFlags::WRAPPED);
759        }
760        if resized || (wrapped_before && !preserve_wrap) {
761            self.touch();
762        }
763    }
764
765    pub(super) fn render_text(&self) -> String {
766        if let Some(text) = &self.plain_text {
767            return text.trim_end_matches(' ').to_owned();
768        }
769        let mut rendered = String::new();
770        for cell in &self.cells {
771            if cell.flags.contains(GridCellFlags::PADDING) {
772                continue;
773            }
774            rendered.push_str(cell.text.as_str());
775        }
776        while rendered.ends_with(' ') {
777            rendered.pop();
778        }
779        rendered
780    }
781
782    pub(super) fn rendered_text_len(&self) -> usize {
783        if let Some(text) = &self.plain_text {
784            return text.trim_end_matches(' ').len();
785        }
786
787        let mut total = 0_usize;
788        let mut trimmed = 0_usize;
789        for cell in &self.cells {
790            if cell.flags.contains(GridCellFlags::PADDING) {
791                continue;
792            }
793            let text = cell.text.as_str();
794            total = total.saturating_add(text.len());
795            let without_trailing_spaces = text.trim_end_matches(' ').len();
796            if without_trailing_spaces > 0 {
797                trimmed = total.saturating_sub(text.len() - without_trailing_spaces);
798            }
799        }
800        trimmed
801    }
802
803    pub(super) fn recovery_clone_bytes(&self) -> usize {
804        let cell_bytes = self
805            .cells
806            .len()
807            .saturating_mul(std::mem::size_of::<GridCell>());
808        let extended_cell_text_bytes = self
809            .cells
810            .iter()
811            .filter(|cell| cell.text.extended.is_some())
812            .map(|cell| {
813                std::mem::size_of::<CompactString>().saturating_add(cell.text.as_str().len())
814            })
815            .sum::<usize>();
816        let plain_text_bytes = self.plain_text.as_ref().map_or(0, CompactString::len);
817        std::mem::size_of::<Self>()
818            .saturating_add(cell_bytes)
819            .saturating_add(extended_cell_text_bytes)
820            .saturating_add(plain_text_bytes)
821    }
822
823    pub(super) fn used_end(&self) -> usize {
824        if let Some(text) = &self.plain_text {
825            return text.len();
826        }
827        self.cells
828            .iter()
829            .rposition(|cell| !cell.is_blank())
830            .map_or(0, |index| index + 1)
831    }
832
833    pub(super) fn mark_reflow_gap(&mut self, start: u32) {
834        self.materialize_for_cell_mutation();
835        for cell in self.cells.iter_mut().skip(start as usize) {
836            debug_assert!(cell.is_blank());
837            let mut flags = cell.flags();
838            flags.insert(GridCellFlags::REFLOW_GAP);
839            cell.set_flags(flags);
840        }
841    }
842
843    /// Marks only the unused physical suffix skipped before a wide pre-wrap.
844    pub(crate) fn mark_unused_suffix_as_reflow_gap(&mut self, start: u32) {
845        if self.used_end() <= start as usize {
846            self.mark_reflow_gap(start);
847        }
848    }
849
850    fn restore_trailing_reflow_gaps(&mut self, count: usize) {
851        self.materialize_reflow_gaps();
852        for cell in self.cells.iter_mut().rev().take(count) {
853            debug_assert!(cell.is_blank());
854            let mut flags = cell.flags();
855            flags.insert(GridCellFlags::REFLOW_GAP);
856            cell.set_flags(flags);
857        }
858    }
859
860    fn materialize_reflow_gaps(&mut self) {
861        for cell in &mut self.cells {
862            let mut flags = cell.flags();
863            if flags.contains(GridCellFlags::REFLOW_GAP) {
864                flags.remove(GridCellFlags::REFLOW_GAP);
865                cell.set_flags(flags);
866            }
867        }
868    }
869
870    /// Returns the unused columns a wide glyph left at the end of the line
871    /// when it did not fit and pre-wrapped onto the following row.
872    pub(super) fn trailing_reflow_gap(&self) -> u32 {
873        let width = self.width as usize;
874        let gap = self
875            .cells
876            .iter()
877            .take(width)
878            .rev()
879            .take_while(|cell| cell.is_reflow_gap())
880            .count();
881        u32::try_from(gap).unwrap_or(u32::MAX)
882    }
883
884    pub(super) fn reflow_logical_width(&self) -> usize {
885        let width = self.width as usize;
886        width.saturating_sub(
887            self.cells
888                .iter()
889                .take(width)
890                .filter(|cell| cell.is_reflow_gap())
891                .count(),
892        )
893    }
894
895    pub(super) fn reflow_logical_column(&self, physical_column: usize) -> usize {
896        let physical_column = physical_column.min(self.width as usize);
897        physical_column.saturating_sub(
898            self.cells
899                .iter()
900                .take(physical_column)
901                .filter(|cell| cell.is_reflow_gap())
902                .count(),
903        )
904    }
905
906    pub(super) fn tmux_cell_capacity(&self, line_width: usize) -> usize {
907        let used_end = self.used_end();
908        if used_end == 0 {
909            return 0;
910        }
911
912        let bucket_used = self.extended_cell_count().max(1);
913        let quarter = (line_width / 4).max(1);
914        let half = (line_width / 2).max(quarter);
915        if bucket_used < quarter {
916            quarter
917        } else if bucket_used < half {
918            half
919        } else {
920            line_width
921        }
922    }
923
924    fn tmux_capture_cell_end(&self, line_width: usize) -> usize {
925        let used_end = self.used_end();
926        if used_end == 0 {
927            return 0;
928        }
929        if self.ends_with_default_spaces_after_styled_cells(used_end) {
930            return used_end;
931        }
932        self.tmux_cell_capacity(line_width)
933    }
934
935    fn render_cell_end(&self, line_width: usize, options: GridRenderOptions) -> usize {
936        let used_end = self.used_end();
937        if options.trim_spaces {
938            return if options.include_empty_cells {
939                used_end.min(line_width)
940            } else {
941                used_end
942            };
943        }
944        if options.include_empty_cells && options.use_tmux_cell_capacity {
945            self.tmux_capture_cell_end(line_width)
946        } else if options.include_empty_cells {
947            line_width
948        } else {
949            used_end
950        }
951    }
952
953    fn ends_with_default_spaces_after_styled_cells(&self, used_end: usize) -> bool {
954        let mut first_trailing_space = used_end;
955        while first_trailing_space > 0
956            && self.cells[first_trailing_space - 1].is_explicit_default_space()
957        {
958            first_trailing_space -= 1;
959        }
960
961        first_trailing_space < used_end
962            && self.cells[..first_trailing_space]
963                .iter()
964                .any(GridCell::has_non_default_style)
965    }
966
967    pub(super) fn extended_cell_count(&self) -> usize {
968        if let Some(text) = &self.plain_text {
969            return text.len();
970        }
971        self.cells[..self.used_end()]
972            .iter()
973            .filter(|cell| !cell.is_blank() && !cell.is_padding())
974            .count()
975    }
976
977    pub(super) fn render_with_options(
978        &self,
979        line_width: usize,
980        options: GridRenderOptions,
981        state: &mut GridStringState,
982        hyperlinks: Option<&Hyperlinks>,
983    ) -> String {
984        self.render_with_options_measured(line_width, options, state, hyperlinks)
985            .0
986    }
987
988    /// Renders the line and reports the columns the rendered text paints.
989    ///
990    /// Replaying a rendered line moves the cursor by the reported span, so
991    /// callers that place the following line by autowrap need the span rather
992    /// than the byte length.
993    pub(super) fn render_with_options_measured(
994        &self,
995        line_width: usize,
996        options: GridRenderOptions,
997        state: &mut GridStringState,
998        hyperlinks: Option<&Hyperlinks>,
999    ) -> (String, RenderedLineSpan) {
1000        if let Some(text) = &self.plain_text {
1001            let mut rendered = String::new();
1002            append_plain_text_state_prefix(options, state, hyperlinks, &mut rendered);
1003            // A compacted line holds default single-column ASCII, so every
1004            // rendered byte paints exactly one column.
1005            let used_end = self.plain_text_render_end(text.len(), options);
1006            let columns =
1007                append_plain_text_with_options(text, used_end, line_width, options, &mut rendered);
1008            return (rendered, RenderedLineSpan::leading_one(columns));
1009        }
1010        let mut rendered = String::new();
1011        let mut has_link = false;
1012        let end = self.render_cell_end(line_width, options);
1013        let mut span = RenderedLineSpan::default();
1014
1015        for cell in self.cells.iter().take(end) {
1016            if cell.flags.contains(GridCellFlags::PADDING) {
1017                continue;
1018            }
1019            if options.with_sequences {
1020                append_grid_string_code(
1021                    &state.last_cell,
1022                    cell,
1023                    &mut rendered,
1024                    options.escape_sequences,
1025                    hyperlinks,
1026                    &mut has_link,
1027                );
1028                state.last_cell = cell.clone();
1029            }
1030            append_cell_text(cell, &mut rendered, options.escape_sequences);
1031            span.push(u32::from(cell.width.max(1)));
1032        }
1033        if options.include_empty_cells && end > self.cells.len() {
1034            let filled = end - self.cells.len();
1035            rendered.extend(std::iter::repeat_n(' ', filled));
1036            for _ in 0..filled {
1037                span.push(1);
1038            }
1039        }
1040
1041        if has_link {
1042            append_hyperlink(&mut rendered, "", "", options.escape_sequences);
1043        }
1044        if options.trim_spaces {
1045            while rendered.ends_with(' ') {
1046                rendered.pop();
1047                span.pop_column();
1048            }
1049        }
1050        (rendered, span)
1051    }
1052
1053    pub(super) fn render_bytes_with_options(
1054        &self,
1055        line_width: usize,
1056        options: GridRenderOptions,
1057        output: &mut Vec<u8>,
1058    ) -> bool {
1059        if options.with_sequences || options.escape_sequences {
1060            return false;
1061        }
1062        if let Some(text) = &self.plain_text {
1063            let used_end = self.plain_text_render_end(text.len(), options);
1064            append_plain_text_bytes_with_options(text, used_end, line_width, options, output);
1065            return true;
1066        }
1067
1068        let start = output.len();
1069        let end = self.render_cell_end(line_width, options);
1070
1071        for cell in self.cells.iter().take(end) {
1072            if cell.flags.contains(GridCellFlags::PADDING) {
1073                continue;
1074            }
1075            if cell.flags.contains(GridCellFlags::TAB) {
1076                output.push(b'\t');
1077                continue;
1078            }
1079            output.extend_from_slice(cell.text().as_bytes());
1080        }
1081        if options.include_empty_cells && end > self.cells.len() {
1082            output.extend(std::iter::repeat_n(b' ', end - self.cells.len()));
1083        }
1084        if options.trim_spaces {
1085            trim_trailing_ascii_spaces(output, start);
1086        }
1087        true
1088    }
1089
1090    pub(super) fn render_with_default_style(
1091        &self,
1092        line_width: usize,
1093        options: GridRenderOptions,
1094        state: &mut GridStringState,
1095        hyperlinks: Option<&Hyperlinks>,
1096        style: &Style,
1097    ) -> String {
1098        let mut line = self.clone();
1099        line.overlay_default_style(style);
1100        line.render_with_options(line_width, options, state, hyperlinks)
1101    }
1102
1103    fn overlay_default_style(&mut self, style: &Style) {
1104        if self.plain_text.is_some() {
1105            self.materialize_plain_text(self.width as usize);
1106        }
1107        let background = effective_background(style);
1108        for cell in &mut self.cells {
1109            if cell.is_padding() {
1110                continue;
1111            }
1112            if style_colour_is_set(style.cell.fg) && style_colour_is_unset(cell.fg()) {
1113                cell.set_fg(style.cell.fg);
1114            }
1115            if style_colour_is_set(background) && style_colour_is_unset(cell.bg()) {
1116                cell.set_bg(background);
1117            }
1118            if style_colour_is_set(style.cell.us) && style_colour_is_unset(cell.us()) {
1119                cell.set_us(style.cell.us);
1120            }
1121            if style.cell.attr != 0 && cell.attr() == 0 {
1122                cell.set_attr(style.cell.attr & !GridAttr::NOATTR);
1123            }
1124        }
1125    }
1126
1127    fn try_compact_plain_text(&mut self) -> bool {
1128        if self.plain_text.is_some() {
1129            self.cells.clear();
1130            self.cells.shrink_to_fit();
1131            return true;
1132        }
1133        let Some(compacted) = self.compacted_plain_text_clone() else {
1134            return false;
1135        };
1136        *self = compacted;
1137        true
1138    }
1139
1140    fn compacted_plain_text_clone(&self) -> Option<Self> {
1141        if let Some(text) = &self.plain_text {
1142            return Some(Self {
1143                cells: Vec::new(),
1144                plain_text: Some(text.clone()),
1145                width: self.width,
1146                plain_text_data_end: self.plain_text_data_end,
1147                flags: self.flags,
1148                time: self.time,
1149                revision: self.revision,
1150            });
1151        }
1152        if self.cells.iter().any(GridCell::is_reflow_gap) {
1153            return None;
1154        }
1155        let used_end = self.used_end();
1156        if used_end == 0 {
1157            return Some(Self {
1158                cells: Vec::new(),
1159                plain_text: Some(CompactString::new("")),
1160                width: self.width,
1161                plain_text_data_end: 0,
1162                flags: self.flags,
1163                time: self.time,
1164                revision: self.revision,
1165            });
1166        }
1167
1168        let cells = self.cells.get(..used_end)?;
1169        if !cells.iter().all(GridCell::is_plain_default_ascii) {
1170            return None;
1171        }
1172
1173        let mut text = String::with_capacity(used_end);
1174        for cell in cells {
1175            text.push_str(cell.text.as_str());
1176        }
1177        Some(Self {
1178            cells: Vec::new(),
1179            plain_text: Some(CompactString::from(text)),
1180            width: self.width,
1181            plain_text_data_end: u16::try_from(used_end).unwrap_or(u16::MAX),
1182            flags: self.flags,
1183            time: self.time,
1184            revision: self.revision,
1185        })
1186    }
1187
1188    fn can_cache_plain_ascii_run(&self, start: usize) -> bool {
1189        if self.plain_text.is_some() {
1190            return true;
1191        }
1192        start == 0 && self.cells.iter().all(GridCell::is_blank)
1193    }
1194
1195    fn update_plain_text_cache(&mut self, start: usize, bytes: &[u8]) {
1196        let text = self.plain_text.get_or_insert_with(CompactString::default);
1197        if text.len() < start {
1198            for _ in text.len()..start {
1199                text.push(' ');
1200            }
1201        }
1202        let end = start + bytes.len();
1203        let replacement = std::str::from_utf8(bytes).expect("plain ascii run must be utf-8");
1204        if text.len() < end {
1205            text.truncate(start);
1206            text.push_str(replacement);
1207        } else {
1208            text.replace_range(start..end, replacement);
1209        }
1210        while text.ends_with(' ') {
1211            text.pop();
1212        }
1213        let data_end = usize::from(self.plain_text_data_end)
1214            .max(end)
1215            .max(text.len());
1216        self.plain_text_data_end = u16::try_from(data_end).unwrap_or(u16::MAX);
1217    }
1218
1219    fn materialize_plain_text(&mut self, width: usize) {
1220        let Some(text) = self.plain_text.take() else {
1221            return;
1222        };
1223        self.cells = text.bytes().map(GridCell::from_plain_ascii).collect();
1224        let data_end = usize::from(self.plain_text_data_end).min(width);
1225        self.cells
1226            .resize(data_end, GridCell::from_plain_ascii(b' '));
1227        if self.cells.len() > width {
1228            self.cells.truncate(width);
1229        }
1230        if self.cells.len() < width {
1231            self.cells.resize(width, GridCell::default());
1232        }
1233        self.plain_text_data_end = 0;
1234        self.width = u32::try_from(width).unwrap_or(u32::MAX);
1235    }
1236
1237    fn plain_text_render_end(&self, text_len: usize, options: GridRenderOptions) -> usize {
1238        if options.join_wrapped && !options.trim_spaces {
1239            usize::from(self.plain_text_data_end).max(text_len)
1240        } else {
1241            text_len
1242        }
1243    }
1244}
1245
1246fn effective_background(style: &Style) -> Colour {
1247    if style_colour_is_set(style.cell.bg) {
1248        style.cell.bg
1249    } else {
1250        style.fill
1251    }
1252}
1253
1254fn style_colour_is_set(colour: Colour) -> bool {
1255    !style_colour_is_unset(colour)
1256}
1257
1258fn style_colour_is_unset(colour: Colour) -> bool {
1259    matches!(colour, COLOUR_DEFAULT | COLOUR_TERMINAL | COLOUR_NONE)
1260}
1261
1262fn append_plain_text_state_prefix(
1263    options: GridRenderOptions,
1264    state: &mut GridStringState,
1265    hyperlinks: Option<&Hyperlinks>,
1266    output: &mut String,
1267) {
1268    if !options.with_sequences {
1269        return;
1270    }
1271    let default_cell = GridCell::blank_with_bg(COLOUR_DEFAULT);
1272    let mut has_link = false;
1273    append_grid_string_code(
1274        &state.last_cell,
1275        &default_cell,
1276        output,
1277        options.escape_sequences,
1278        hyperlinks,
1279        &mut has_link,
1280    );
1281    state.last_cell = default_cell;
1282    if has_link {
1283        append_hyperlink(output, "", "", options.escape_sequences);
1284    }
1285}
1286
1287/// Appends the compacted plain-ASCII text and returns the columns it paints.
1288fn append_plain_text_with_options(
1289    text: &str,
1290    used_end: usize,
1291    line_width: usize,
1292    options: GridRenderOptions,
1293    output: &mut String,
1294) -> u32 {
1295    let start = output.len();
1296    let end = plain_text_capture_end(used_end, line_width, options);
1297    let copied_end = text.len().min(end);
1298    output.push_str(&text[..copied_end]);
1299    if text.len() < end {
1300        output.extend(std::iter::repeat_n(' ', end - text.len()));
1301    }
1302    if options.trim_spaces {
1303        trim_trailing_spaces(output, start);
1304    }
1305    u32::try_from(output.len().saturating_sub(start)).unwrap_or(u32::MAX)
1306}
1307
1308fn append_plain_text_bytes_with_options(
1309    text: &str,
1310    used_end: usize,
1311    line_width: usize,
1312    options: GridRenderOptions,
1313    output: &mut Vec<u8>,
1314) {
1315    let start = output.len();
1316    let end = plain_text_capture_end(used_end, line_width, options);
1317    let bytes = text.as_bytes();
1318    output.extend_from_slice(&bytes[..bytes.len().min(end)]);
1319    if bytes.len() < end {
1320        output.extend(std::iter::repeat_n(b' ', end - bytes.len()));
1321    }
1322    if options.trim_spaces {
1323        trim_trailing_ascii_spaces(output, start);
1324    }
1325}
1326
1327fn plain_text_capture_end(used_end: usize, line_width: usize, options: GridRenderOptions) -> usize {
1328    if options.trim_spaces {
1329        return if options.include_empty_cells {
1330            used_end.min(line_width)
1331        } else {
1332            used_end
1333        };
1334    }
1335    if options.include_empty_cells && options.use_tmux_cell_capacity {
1336        if used_end == 0 {
1337            return 0;
1338        }
1339        let bucket_used = used_end.max(1);
1340        let quarter = (line_width / 4).max(1);
1341        let half = (line_width / 2).max(quarter);
1342        if bucket_used < quarter {
1343            quarter
1344        } else if bucket_used < half {
1345            half
1346        } else {
1347            line_width
1348        }
1349    } else if options.include_empty_cells {
1350        line_width
1351    } else {
1352        used_end
1353    }
1354}
1355
1356fn trim_trailing_ascii_spaces(output: &mut Vec<u8>, floor: usize) {
1357    while output.len() > floor && output.last() == Some(&b' ') {
1358        output.pop();
1359    }
1360}
1361
1362fn trim_trailing_spaces(output: &mut String, floor: usize) {
1363    while output.len() > floor && output.ends_with(' ') {
1364        output.pop();
1365    }
1366}
1367
1368fn next_line_revision() -> u64 {
1369    static NEXT_LINE_ID: AtomicU64 = AtomicU64::new(1);
1370    NEXT_LINE_ID.fetch_add(1, Ordering::Relaxed) << LINE_REVISION_GENERATION_BITS
1371}
1372
1373pub(super) fn current_unix_timestamp() -> i64 {
1374    SystemTime::now()
1375        .duration_since(UNIX_EPOCH)
1376        .map(|duration| i64::try_from(duration.as_secs()).unwrap_or(i64::MAX))
1377        .unwrap_or(0)
1378}
1379
1380const LINE_REVISION_GENERATION_BITS: u32 = 32;
1381const LINE_REVISION_GENERATION_MASK: u64 = (1_u64 << LINE_REVISION_GENERATION_BITS) - 1;
1382const LINE_REVISION_ID_MASK: u64 = !LINE_REVISION_GENERATION_MASK;
1383
1384#[cfg(test)]
1385mod tests {
1386    use super::{plain_text_capture_end, CellText, GridCell, GridLine};
1387    use crate::grid::GridRenderOptions;
1388
1389    #[test]
1390    fn grid_cell_layout_stays_compact() {
1391        assert!(
1392            std::mem::size_of::<CellText>() <= 16,
1393            "cell text regressed to {} bytes",
1394            std::mem::size_of::<CellText>()
1395        );
1396        assert!(
1397            std::mem::size_of::<GridCell>() <= 40,
1398            "grid cell regressed to {} bytes",
1399            std::mem::size_of::<GridCell>()
1400        );
1401        assert!(
1402            std::mem::size_of::<GridLine>() <= 72,
1403            "grid line header regressed to {} bytes",
1404            std::mem::size_of::<GridLine>()
1405        );
1406    }
1407
1408    #[test]
1409    fn trimmed_plain_text_capture_skips_padding_work() {
1410        let options = GridRenderOptions {
1411            include_empty_cells: true,
1412            trim_spaces: true,
1413            ..GridRenderOptions::default()
1414        };
1415
1416        assert_eq!(plain_text_capture_end(4, 120, options), 4);
1417    }
1418
1419    #[test]
1420    fn untrimmed_plain_text_capture_keeps_padding_width() {
1421        let options = GridRenderOptions {
1422            include_empty_cells: true,
1423            trim_spaces: false,
1424            ..GridRenderOptions::default()
1425        };
1426
1427        assert_eq!(plain_text_capture_end(4, 120, options), 120);
1428    }
1429
1430    #[test]
1431    fn history_compaction_keeps_existing_plain_text_without_cells() {
1432        let mut line = GridLine::new(10);
1433        line.materialize_for_cell_mutation();
1434        assert!(!line.cells().is_empty());
1435
1436        assert!(line.write_plain_ascii_run(0, b"abc"));
1437        assert_eq!(line.plain_text(), Some("abc"));
1438        assert!(!line.cells().is_empty());
1439
1440        line.compact_for_history();
1441
1442        assert_eq!(line.plain_text(), Some("abc"));
1443        assert!(line.cells().is_empty());
1444    }
1445}