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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub(crate) struct GridCellFlags(u8);
16
17#[allow(dead_code)]
18impl GridCellFlags {
19 pub const PADDING: Self = Self(0x1);
21 pub const CLEARED: Self = Self(0x2);
23 pub const TAB: Self = Self(0x4);
25 pub const EXTENDED: Self = Self(0x8);
27 pub const SELECTED: Self = Self(0x10);
29 pub const NOPALETTE: Self = Self(0x20);
31 pub const REFLOW_GAP: Self = Self(0x40);
33
34 #[must_use]
36 pub const fn bits(self) -> u8 {
37 self.0
38 }
39
40 #[must_use]
42 pub const fn contains(self, other: Self) -> bool {
43 self.0 & other.0 == other.0
44 }
45
46 pub fn insert(&mut self, other: Self) {
48 self.0 |= other.0;
49 }
50
51 pub fn remove(&mut self, other: Self) {
53 self.0 &= !other.0;
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
63pub struct RenderedLineSpan {
64 columns: u32,
65 leading_columns: u32,
66}
67
68impl RenderedLineSpan {
69 #[must_use]
71 pub const fn columns(self) -> u32 {
72 self.columns
73 }
74
75 #[must_use]
78 pub const fn leading_columns(self) -> u32 {
79 self.leading_columns
80 }
81
82 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
107pub(crate) struct GridLineFlags(u8);
108
109#[allow(dead_code)]
110impl GridLineFlags {
111 pub const WRAPPED: Self = Self(0x1);
113 pub const EXTENDED: Self = Self(0x2);
115 pub const DEAD: Self = Self(0x4);
117 pub const START_PROMPT: Self = Self(0x8);
119 pub const START_OUTPUT: Self = Self(0x10);
121
122 #[must_use]
124 pub const fn bits(self) -> u8 {
125 self.0
126 }
127
128 #[must_use]
130 pub const fn contains(self, other: Self) -> bool {
131 self.0 & other.0 == other.0
132 }
133
134 pub fn insert(&mut self, other: Self) {
136 self.0 |= other.0;
137 }
138
139 pub fn remove(&mut self, other: Self) {
141 self.0 &= !other.0;
142 }
143}
144
145#[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 #[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 #[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 #[must_use]
278 pub fn text(&self) -> &str {
279 self.text.as_str()
280 }
281
282 #[must_use]
284 pub const fn width(&self) -> u8 {
285 self.width
286 }
287
288 #[must_use]
290 pub const fn flags(&self) -> GridCellFlags {
291 self.flags
292 }
293
294 #[must_use]
296 pub const fn is_padding(&self) -> bool {
297 self.flags.contains(GridCellFlags::PADDING)
298 }
299
300 #[must_use]
302 pub const fn is_reflow_gap(&self) -> bool {
303 self.flags.contains(GridCellFlags::REFLOW_GAP)
304 }
305
306 #[must_use]
308 pub const fn attr(&self) -> u16 {
309 self.attr
310 }
311
312 #[must_use]
314 pub const fn fg(&self) -> Colour {
315 self.fg
316 }
317
318 #[must_use]
320 pub const fn bg(&self) -> Colour {
321 self.bg
322 }
323
324 #[must_use]
326 pub const fn us(&self) -> Colour {
327 self.us
328 }
329
330 #[must_use]
332 pub const fn link(&self) -> u32 {
333 self.link
334 }
335
336 #[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#[derive(Debug, Clone, PartialEq, Eq)]
413pub(crate) struct GridLine {
414 pub(super) cells: Vec<GridCell>,
415 plain_text: Option<CompactString>,
416 width: u32,
417 plain_text_data_end: u16,
420 pub(super) flags: GridLineFlags,
421 time: i64,
422 revision: u64,
423}
424
425impl GridLine {
426 #[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 #[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 #[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 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 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 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 #[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 #[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 #[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 #[must_use]
650 pub const fn flags(&self) -> GridLineFlags {
651 self.flags
652 }
653
654 #[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 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 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 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 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
1287fn 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}