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