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(crate) use cell::{GridCell, GridCellFlags, GridLine, GridLineFlags};
18use render::{append_cell_text, append_grid_string_code, append_hyperlink};
19
20const HISTORY_STAMP_REFRESH_LINES: u16 = 256;
21
22#[derive(Debug, Clone, PartialEq, Eq, Default)]
24#[cfg_attr(not(test), allow(dead_code))]
25pub(crate) struct GridCapture {
26 pub lines: Vec<String>,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct GridRenderOptions {
33 pub join_wrapped: bool,
35 pub with_sequences: bool,
37 pub escape_sequences: bool,
39 pub include_empty_cells: bool,
41 pub use_tmux_cell_capacity: bool,
43 pub trim_spaces: bool,
45}
46
47impl Default for GridRenderOptions {
48 fn default() -> Self {
49 Self {
50 join_wrapped: false,
51 with_sequences: false,
52 escape_sequences: false,
53 include_empty_cells: true,
54 use_tmux_cell_capacity: false,
55 trim_spaces: true,
56 }
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct GridStringState {
63 last_cell: GridCell,
64}
65
66impl Default for GridStringState {
67 fn default() -> Self {
68 Self {
69 last_cell: GridCell::blank_with_bg(COLOUR_DEFAULT),
70 }
71 }
72}
73
74impl GridStringState {
75 pub(crate) fn reset_to_default_line_style(
76 &mut self,
77 options: GridRenderOptions,
78 hyperlinks: Option<&Hyperlinks>,
79 output: &mut Vec<u8>,
80 ) {
81 if !options.with_sequences {
82 return;
83 }
84
85 let default_cell = GridCell::blank_with_bg(COLOUR_DEFAULT);
86 let mut rendered = String::new();
87 let mut has_link = false;
88 append_grid_string_code(
89 &self.last_cell,
90 &default_cell,
91 &mut rendered,
92 options.escape_sequences,
93 hyperlinks,
94 &mut has_link,
95 );
96 if has_link {
97 append_hyperlink(&mut rendered, "", "", options.escape_sequences);
98 }
99 output.extend_from_slice(rendered.as_bytes());
100 self.last_cell = default_cell;
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
106pub(crate) struct Grid {
107 sx: u32,
108 sy: u32,
109 hlimit: usize,
110 hscrolled: usize,
111 history_enabled: bool,
112 history_stamp: i64,
113 history_stamp_remaining: u16,
114 history: VecDeque<GridLine>,
115 visible: VecDeque<GridLine>,
116}
117
118impl Grid {
119 #[must_use]
121 pub fn new(size: TerminalSize, hlimit: usize) -> Self {
122 let sx = u32::from(size.cols.max(1));
123 let sy = u32::from(size.rows.max(1));
124 Self {
125 sx,
126 sy,
127 hlimit,
128 hscrolled: 0,
129 history_enabled: true,
130 history_stamp: 0,
131 history_stamp_remaining: 0,
132 history: VecDeque::new(),
133 visible: (0..sy).map(|_| GridLine::new(sx)).collect(),
134 }
135 }
136
137 #[must_use]
139 pub fn size(&self) -> TerminalSize {
140 TerminalSize {
141 cols: u16::try_from(self.sx).unwrap_or(u16::MAX),
142 rows: u16::try_from(self.sy).unwrap_or(u16::MAX),
143 }
144 }
145
146 #[must_use]
148 pub const fn sx(&self) -> u32 {
149 self.sx
150 }
151
152 #[must_use]
154 pub const fn sy(&self) -> u32 {
155 self.sy
156 }
157
158 #[must_use]
160 pub fn hsize(&self) -> usize {
161 self.history.len()
162 }
163
164 #[must_use]
166 pub const fn hlimit(&self) -> usize {
167 self.hlimit
168 }
169
170 #[must_use]
172 pub const fn history_enabled(&self) -> bool {
173 self.history_enabled
174 }
175
176 pub fn set_hlimit(&mut self, hlimit: usize) {
178 self.hlimit = hlimit;
179 while self.history.len() > self.hlimit {
180 let _ = self.history.pop_front();
181 }
182 self.hscrolled = self.hscrolled.min(self.history.len());
183 }
184
185 pub fn set_history_enabled(&mut self, enabled: bool) {
187 self.history_enabled = enabled;
188 }
189
190 #[allow(dead_code)]
192 #[must_use]
193 pub const fn hscrolled(&self) -> usize {
194 self.hscrolled
195 }
196
197 #[must_use]
199 pub fn visible_line(&self, y: u32) -> Option<&GridLine> {
200 self.visible.get(y as usize)
201 }
202
203 pub(crate) fn visible_line_mut(&mut self, y: u32) -> Option<&mut GridLine> {
204 self.visible.get_mut(y as usize)
205 }
206
207 #[allow(dead_code)]
210 #[must_use]
211 pub fn absolute_line(&self, absolute_y: usize) -> Option<&GridLine> {
212 if absolute_y < self.history.len() {
213 self.history.get(absolute_y)
214 } else {
215 self.visible.get(absolute_y - self.history.len())
216 }
217 }
218
219 pub fn remove_absolute_line(&mut self, absolute_y: usize) -> bool {
224 if absolute_y < self.history.len() {
225 let _ = self.history.remove(absolute_y);
226 self.hscrolled = self.hscrolled.min(self.history.len());
227 return true;
228 }
229
230 let visible_index = absolute_y.saturating_sub(self.history.len());
231 if visible_index >= self.visible.len() {
232 return false;
233 }
234
235 let _ = self.visible.remove(visible_index);
236 self.visible.push_back(GridLine::new(self.sx));
237 true
238 }
239
240 pub(crate) fn truncate_after_absolute_line(&mut self, absolute_y: usize) -> bool {
242 let total = self.history.len() + self.visible.len();
243 if absolute_y >= total {
244 return false;
245 }
246
247 let keep = absolute_y.saturating_add(1);
248 let mut lines = self
249 .history
250 .iter()
251 .chain(self.visible.iter())
252 .take(keep)
253 .cloned()
254 .collect::<Vec<_>>();
255 let visible_rows = self.sy as usize;
256 while lines.len() < visible_rows {
257 lines.push(GridLine::new(self.sx));
258 }
259
260 let visible_start = lines.len().saturating_sub(visible_rows);
261 let mut visible = lines.split_off(visible_start);
262 for line in &mut visible {
263 line.resize_width_preserving_wrap(self.sx, COLOUR_DEFAULT);
264 }
265 self.history = compacted_history(lines);
266 while self.history.len() > self.hlimit {
267 let _ = self.history.pop_front();
268 }
269 self.visible = visible.into();
270 self.hscrolled = self.history.len();
271 true
272 }
273
274 #[must_use]
276 pub fn absolute_line_wrapped(&self, absolute_y: usize) -> Option<bool> {
277 self.absolute_line(absolute_y)
278 .map(|line| line.flags.contains(GridLineFlags::WRAPPED))
279 }
280
281 pub fn clear_history(&mut self) {
283 self.history.clear();
284 self.hscrolled = 0;
285 }
286
287 pub fn clear_visible(&mut self, bg: Colour) {
289 for line in &mut self.visible {
290 line.clear(bg);
291 }
292 }
293
294 pub fn clear_visible_to_history(&mut self, bg: Colour) {
296 if self.history_enabled {
297 let last_used = self.visible.iter().rposition(|line| line.used_end() > 0);
298 if let Some(last_used) = last_used {
299 for index in 0..=last_used {
300 let line = self.visible[index].clone();
301 self.push_history(line);
302 }
303 }
304 }
305 self.clear_visible(bg);
306 }
307
308 pub fn replace_visible(&mut self, lines: Vec<GridLine>) {
310 self.sy = lines.len() as u32;
311 self.visible = lines.into();
312 for line in &mut self.visible {
313 line.resize_width_preserving_wrap(self.sx, COLOUR_DEFAULT);
314 }
315 }
316
317 pub(crate) fn replace_visible_resized_width_only(
318 &mut self,
319 source_size: TerminalSize,
320 lines: Vec<GridLine>,
321 bg: Colour,
322 ) {
323 debug_assert_eq!(
324 u32::from(source_size.rows.max(1)),
325 self.sy,
326 "width-only visible restore must not change row policy"
327 );
328 let target_width = self.sx;
329 let mut viewport = Grid::new(source_size, 0);
330 viewport.replace_visible(lines);
331 viewport.resize_width(target_width, bg);
332 self.visible = viewport.visible;
333 }
334
335 #[cfg_attr(not(test), allow(dead_code))]
337 #[must_use]
338 pub fn capture(&self, join_wrapped: bool) -> GridCapture {
339 let mut lines = Vec::new();
340 let mut pending = String::new();
341
342 for line in self.history.iter().chain(self.visible.iter()) {
343 let rendered = line.render_text();
344 if join_wrapped {
345 pending.push_str(&rendered);
346 if !line.flags.contains(GridLineFlags::WRAPPED) {
347 lines.push(std::mem::take(&mut pending));
348 }
349 continue;
350 }
351
352 lines.push(rendered);
353 }
354
355 if join_wrapped && !pending.is_empty() {
356 lines.push(pending);
357 }
358
359 GridCapture { lines }
360 }
361
362 #[must_use]
364 pub fn render_absolute_line(
365 &self,
366 absolute_y: usize,
367 options: GridRenderOptions,
368 state: &mut GridStringState,
369 hyperlinks: Option<&Hyperlinks>,
370 ) -> Option<String> {
371 self.absolute_line(absolute_y)
372 .map(|line| line.render_with_options(self.sx as usize, options, state, hyperlinks))
373 }
374
375 pub fn append_rendered_absolute_line(
376 &self,
377 absolute_y: usize,
378 options: GridRenderOptions,
379 state: &mut GridStringState,
380 hyperlinks: Option<&Hyperlinks>,
381 output: &mut Vec<u8>,
382 ) -> Option<()> {
383 let line = self.absolute_line(absolute_y)?;
384 if line.render_bytes_with_options(self.sx as usize, options, output) {
385 return Some(());
386 }
387 let rendered = line.render_with_options(self.sx as usize, options, state, hyperlinks);
388 output.extend_from_slice(rendered.as_bytes());
389 Some(())
390 }
391
392 #[must_use]
396 pub fn render_visible_line_with_default_style(
397 &self,
398 row: usize,
399 options: GridRenderOptions,
400 state: &mut GridStringState,
401 hyperlinks: Option<&Hyperlinks>,
402 style: &Style,
403 ) -> Option<String> {
404 self.visible_line(u32::try_from(row).ok()?).map(|line| {
405 line.render_with_default_style(self.sx as usize, options, state, hyperlinks, style)
406 })
407 }
408
409 #[must_use]
411 pub fn history_byte_size(&self) -> usize {
412 self.history
413 .iter()
414 .map(|line| line.render_text().len() + 1)
415 .sum()
416 }
417
418 #[must_use]
420 pub fn visible_lines(&self) -> Vec<GridLine> {
421 self.visible.iter().cloned().collect()
422 }
423
424 pub(crate) fn scroll_region_up(
425 &mut self,
426 upper: u32,
427 lower: u32,
428 bg: Colour,
429 to_history: bool,
430 ) {
431 if !self.valid_region(upper, lower) {
432 return;
433 }
434
435 let upper = upper as usize;
436 let lower = lower as usize;
437 if upper == 0 && lower + 1 == self.visible.len() {
438 let Some(mut removed) = self.visible.pop_front() else {
439 return;
440 };
441 if to_history && self.history_enabled {
442 self.push_history(removed);
443 self.visible.push_back(GridLine::blank_with_bg(self.sx, bg));
444 } else {
445 removed.clear(bg);
446 self.visible.push_back(removed);
447 }
448 return;
449 }
450
451 let removed_for_history = if to_history && self.history_enabled {
452 let blank = GridLine::blank_with_bg(self.sx, bg);
453 let visible = self.visible.make_contiguous();
454 let removed = std::mem::replace(&mut visible[upper], blank);
455 Some(removed)
456 } else {
457 None
458 };
459 if let Some(removed) = removed_for_history {
460 self.push_history(removed);
461 }
462 let visible = self.visible.make_contiguous();
463 visible[upper..=lower].rotate_left(1);
464 let removed = &mut visible[lower];
465 removed.clear(bg);
466 }
467
468 pub(crate) fn scroll_region_down(&mut self, upper: u32, lower: u32, bg: Colour) {
469 if !self.valid_region(upper, lower) {
470 return;
471 }
472
473 let upper = upper as usize;
474 let lower = lower as usize;
475 if upper == 0 && lower + 1 == self.visible.len() {
476 let Some(mut removed) = self.visible.pop_back() else {
477 return;
478 };
479 removed.clear(bg);
480 self.visible.push_front(removed);
481 return;
482 }
483
484 let visible = self.visible.make_contiguous();
485 visible[upper..=lower].rotate_right(1);
486 visible[upper].clear(bg);
487 }
488
489 pub(crate) fn resize_width(&mut self, sx: u32, bg: Colour) {
490 let sx = sx.max(1);
491 if sx == self.sx {
492 return;
493 }
494
495 if self.can_resize_width_without_reflow(sx) {
496 for line in &mut self.history {
497 line.resize_width_preserving_wrap(sx, bg);
498 }
499 for line in &mut self.visible {
500 line.resize_width_preserving_wrap(sx, bg);
501 }
502 self.sx = sx;
503 return;
504 }
505
506 let visible_rows = self.sy as usize;
507 let lines = self
508 .history
509 .iter()
510 .chain(self.visible.iter())
511 .cloned()
512 .collect::<Vec<_>>();
513 let mut reflowed = reflow_wrapped_lines(lines, sx, bg);
514 while reflowed.len() < visible_rows {
515 reflowed.push(GridLine::blank_with_bg(sx, bg));
516 }
517
518 let history_rows = reflowed.len().saturating_sub(visible_rows);
519 let mut visible = reflowed.split_off(history_rows);
520 for line in &mut visible {
521 line.resize_width_preserving_wrap(sx, bg);
522 }
523 self.history = compacted_history(reflowed);
524 while self.history.len() > self.hlimit {
525 let _ = self.history.pop_front();
526 }
527 self.visible = visible.into();
528 self.hscrolled = self.history.len();
529 self.sx = sx;
530 }
531
532 fn can_resize_width_without_reflow(&self, sx: u32) -> bool {
533 self.history.iter().chain(self.visible.iter()).all(|line| {
534 !line.flags.contains(GridLineFlags::WRAPPED) && line.used_end() <= sx as usize
535 })
536 }
537
538 pub(crate) fn resize_height(&mut self, sy: u32, cursor_y: &mut u32, bg: Colour) {
539 let sy = sy.max(1);
540 let oldy = self.sy;
541
542 if sy < oldy {
543 let mut needed = oldy - sy;
544
545 let available_bottom = oldy.saturating_sub(1).saturating_sub(*cursor_y);
546 let remove_bottom = available_bottom.min(needed);
547 for _ in 0..remove_bottom {
548 let _ = self.visible.pop_back();
549 }
550 needed -= remove_bottom;
551
552 if self.history_enabled {
553 for _ in 0..needed {
554 let Some(line) = self.visible.pop_front() else {
555 break;
556 };
557 self.push_history(line);
558 }
559 } else {
560 let remove_top = (*cursor_y).min(needed);
561 for _ in 0..remove_top {
562 let _ = self.visible.pop_front();
563 }
564 *cursor_y = cursor_y.saturating_sub(remove_top);
565 }
566 } else if sy > oldy {
567 let mut needed = sy - oldy;
568 let pull = self.hscrolled.min(needed as usize).min(self.history.len()) as u32;
569 if self.history_enabled && pull > 0 {
570 let mut restored = Vec::with_capacity(pull as usize);
571 for _ in 0..pull {
572 if let Some(line) = self.history.pop_back() {
573 restored.push(line);
574 }
575 }
576 restored.reverse();
577 for mut line in restored.into_iter().rev() {
578 line.resize_width_preserving_wrap(self.sx, bg);
579 self.visible.push_front(line);
580 }
581 *cursor_y = cursor_y.saturating_add(pull).min(sy.saturating_sub(1));
582 self.hscrolled -= pull as usize;
583 needed -= pull;
584 }
585
586 for _ in 0..needed {
587 self.visible.push_back(GridLine::blank_with_bg(self.sx, bg));
588 }
589 }
590
591 self.sy = sy;
592 while self.visible.len() > self.sy as usize {
593 let _ = self.visible.pop_back();
594 }
595 while self.visible.len() < self.sy as usize {
596 self.visible.push_back(GridLine::blank_with_bg(self.sx, bg));
597 }
598 for line in &mut self.visible {
599 line.resize_width_preserving_wrap(self.sx, bg);
600 }
601 *cursor_y = (*cursor_y).min(self.sy.saturating_sub(1));
602 }
603
604 fn valid_region(&self, upper: u32, lower: u32) -> bool {
605 upper < self.sy && lower < self.sy && upper <= lower
606 }
607
608 fn push_history(&mut self, mut line: GridLine) {
609 if self.hlimit == 0 {
610 return;
611 }
612
613 line.stamp_for_history_at(self.next_history_stamp());
614 line.compact_for_history();
615 if self.history.len() == self.hlimit {
616 let _ = self.history.pop_front();
617 }
618 self.history.push_back(line);
619 self.hscrolled = (self.hscrolled + 1).min(self.history.len());
620 }
621
622 fn next_history_stamp(&mut self) -> i64 {
623 if self.history_stamp_remaining == 0 {
624 self.history_stamp = cell::current_unix_timestamp();
625 self.history_stamp_remaining = HISTORY_STAMP_REFRESH_LINES;
626 }
627 self.history_stamp_remaining = self.history_stamp_remaining.saturating_sub(1);
628 self.history_stamp
629 }
630}
631
632fn compacted_history(lines: Vec<GridLine>) -> VecDeque<GridLine> {
633 lines
634 .into_iter()
635 .map(|mut line| {
636 line.compact_for_history();
637 line
638 })
639 .collect()
640}
641
642fn reflow_wrapped_lines(lines: Vec<GridLine>, width: u32, bg: Colour) -> Vec<GridLine> {
643 let mut output = Vec::new();
644 let mut logical_cells = Vec::new();
645 let mut logical_plain_text: Option<String> = None;
646 let mut logical_flags = None;
647
648 for line in lines {
649 let wrapped = line.flags.contains(GridLineFlags::WRAPPED);
650 if logical_flags.is_none() {
651 let mut flags = line.flags;
652 flags.remove(GridLineFlags::WRAPPED);
653 logical_flags = Some(flags);
654 logical_plain_text = (bg == COLOUR_DEFAULT).then(String::new);
655 }
656
657 let end = if wrapped {
658 self::line_width(&line)
659 } else {
660 line.used_end()
661 };
662 if let (Some(logical_text), Some(text)) = (logical_plain_text.as_mut(), line.plain_text()) {
663 logical_text.extend(
664 text.bytes()
665 .chain(std::iter::repeat(b' '))
666 .take(end)
667 .map(char::from),
668 );
669 } else {
670 if let Some(text) = logical_plain_text.take() {
671 extend_plain_ascii_cells(&mut logical_cells, text.bytes());
672 }
673 if let Some(text) = line.plain_text() {
674 extend_plain_ascii_cells(
675 &mut logical_cells,
676 text.bytes().chain(std::iter::repeat(b' ')).take(end),
677 );
678 } else {
679 logical_cells.extend(
680 line.cells
681 .iter()
682 .take(end)
683 .filter(|cell| !cell.is_padding())
684 .cloned(),
685 );
686 }
687 }
688
689 if !wrapped {
690 let flags = logical_flags.take().unwrap_or_default();
691 if let Some(text) = logical_plain_text.take() {
692 output.extend(reflow_plain_ascii_line(&text, flags, width, bg));
693 } else {
694 output.extend(reflow_logical_line(&logical_cells, flags, width, bg));
695 }
696 logical_cells.clear();
697 }
698 }
699
700 if logical_flags.is_some() || !logical_cells.is_empty() || logical_plain_text.is_some() {
701 let flags = logical_flags.unwrap_or_default();
702 if let Some(text) = logical_plain_text {
703 output.extend(reflow_plain_ascii_line(&text, flags, width, bg));
704 } else {
705 output.extend(reflow_logical_line(&logical_cells, flags, width, bg));
706 }
707 }
708
709 output
710}
711
712fn extend_plain_ascii_cells(cells: &mut Vec<GridCell>, bytes: impl IntoIterator<Item = u8>) {
713 cells.extend(bytes.into_iter().map(GridCell::from_plain_ascii));
714}
715
716fn reflow_plain_ascii_line(
717 text: &str,
718 first_flags: GridLineFlags,
719 width: u32,
720 bg: Colour,
721) -> Vec<GridLine> {
722 if text.is_empty() || bg != COLOUR_DEFAULT {
723 let mut line = GridLine::blank_with_bg(width, bg);
724 line.flags = first_flags;
725 return vec![line];
726 }
727
728 let width = width.max(1);
729 let width_usize = width as usize;
730 let mut output = Vec::with_capacity(text.len().div_ceil(width_usize));
731 let mut start = 0;
732 let mut flags = first_flags;
733 while start < text.len() {
734 let end = (start + width_usize).min(text.len());
735 let mut line = GridLine::from_plain_ascii_text(width, flags, text[start..end].to_owned());
736 if end < text.len() {
737 line.set_wrapped(true);
738 }
739 output.push(line);
740 flags = GridLineFlags::default();
741 start = end;
742 }
743 output
744}
745
746fn reflow_logical_line(
747 cells: &[GridCell],
748 first_flags: GridLineFlags,
749 width: u32,
750 bg: Colour,
751) -> Vec<GridLine> {
752 if cells.is_empty() {
753 let mut line = GridLine::blank_with_bg(width, bg);
754 line.flags = first_flags;
755 return vec![line];
756 }
757
758 let mut output = Vec::new();
759 let mut current = GridLine::blank_with_bg(width, bg);
760 current.flags = first_flags;
761 let mut x: u32 = 0;
762
763 for cell in cells {
764 let mut cell = cell.clone();
765 let mut cell_width = u32::from(cell.width().max(1));
766 if cell_width > width {
767 cell_width = 1;
768 cell.set_width(1);
769 }
770 if x > 0 && x.saturating_add(cell_width) > width {
771 current.set_wrapped(true);
772 output.push(current);
773 current = GridLine::blank_with_bg(width, bg);
774 x = 0;
775 }
776
777 if let Some(target) = current.cell_mut(x) {
778 *target = cell.clone();
779 }
780 for offset in 1..cell_width {
781 if let Some(padding_cell) = current.cell_mut(x + offset) {
782 let mut padding = cell.clone();
783 padding.set_text(" ".to_owned());
784 padding.set_width(0);
785 padding.set_flags(GridCellFlags::PADDING);
786 *padding_cell = padding;
787 }
788 }
789 current.touch();
790 x += cell_width;
791 }
792
793 output.push(current);
794 output
795}
796
797fn line_width(line: &GridLine) -> usize {
798 line.width() as usize
799}
800
801#[cfg(test)]
802#[path = "grid/tests.rs"]
803mod tests;