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 #[cfg_attr(not(test), allow(dead_code))]
319 #[must_use]
320 pub fn capture(&self, join_wrapped: bool) -> GridCapture {
321 let mut lines = Vec::new();
322 let mut pending = String::new();
323
324 for line in self.history.iter().chain(self.visible.iter()) {
325 let rendered = line.render_text();
326 if join_wrapped {
327 pending.push_str(&rendered);
328 if !line.flags.contains(GridLineFlags::WRAPPED) {
329 lines.push(std::mem::take(&mut pending));
330 }
331 continue;
332 }
333
334 lines.push(rendered);
335 }
336
337 if join_wrapped && !pending.is_empty() {
338 lines.push(pending);
339 }
340
341 GridCapture { lines }
342 }
343
344 #[must_use]
346 pub fn render_absolute_line(
347 &self,
348 absolute_y: usize,
349 options: GridRenderOptions,
350 state: &mut GridStringState,
351 hyperlinks: Option<&Hyperlinks>,
352 ) -> Option<String> {
353 self.absolute_line(absolute_y)
354 .map(|line| line.render_with_options(self.sx as usize, options, state, hyperlinks))
355 }
356
357 pub fn append_rendered_absolute_line(
358 &self,
359 absolute_y: usize,
360 options: GridRenderOptions,
361 state: &mut GridStringState,
362 hyperlinks: Option<&Hyperlinks>,
363 output: &mut Vec<u8>,
364 ) -> Option<()> {
365 let line = self.absolute_line(absolute_y)?;
366 if line.render_bytes_with_options(self.sx as usize, options, output) {
367 return Some(());
368 }
369 let rendered = line.render_with_options(self.sx as usize, options, state, hyperlinks);
370 output.extend_from_slice(rendered.as_bytes());
371 Some(())
372 }
373
374 #[must_use]
378 pub fn render_visible_line_with_default_style(
379 &self,
380 row: usize,
381 options: GridRenderOptions,
382 state: &mut GridStringState,
383 hyperlinks: Option<&Hyperlinks>,
384 style: &Style,
385 ) -> Option<String> {
386 self.visible_line(u32::try_from(row).ok()?).map(|line| {
387 line.render_with_default_style(self.sx as usize, options, state, hyperlinks, style)
388 })
389 }
390
391 #[must_use]
393 pub fn history_byte_size(&self) -> usize {
394 self.history
395 .iter()
396 .map(|line| line.render_text().len() + 1)
397 .sum()
398 }
399
400 #[must_use]
402 pub fn visible_lines(&self) -> Vec<GridLine> {
403 self.visible.iter().cloned().collect()
404 }
405
406 pub(crate) fn scroll_region_up(
407 &mut self,
408 upper: u32,
409 lower: u32,
410 bg: Colour,
411 to_history: bool,
412 ) {
413 if !self.valid_region(upper, lower) {
414 return;
415 }
416
417 let upper = upper as usize;
418 let lower = lower as usize;
419 if upper == 0 && lower + 1 == self.visible.len() {
420 let Some(mut removed) = self.visible.pop_front() else {
421 return;
422 };
423 if to_history && self.history_enabled {
424 self.push_history(removed);
425 self.visible.push_back(GridLine::blank_with_bg(self.sx, bg));
426 } else {
427 removed.clear(bg);
428 self.visible.push_back(removed);
429 }
430 return;
431 }
432
433 let removed_for_history = if to_history && self.history_enabled {
434 let blank = GridLine::blank_with_bg(self.sx, bg);
435 let visible = self.visible.make_contiguous();
436 let removed = std::mem::replace(&mut visible[upper], blank);
437 Some(removed)
438 } else {
439 None
440 };
441 if let Some(removed) = removed_for_history {
442 self.push_history(removed);
443 }
444 let visible = self.visible.make_contiguous();
445 visible[upper..=lower].rotate_left(1);
446 let removed = &mut visible[lower];
447 removed.clear(bg);
448 }
449
450 pub(crate) fn scroll_region_down(&mut self, upper: u32, lower: u32, bg: Colour) {
451 if !self.valid_region(upper, lower) {
452 return;
453 }
454
455 let upper = upper as usize;
456 let lower = lower as usize;
457 if upper == 0 && lower + 1 == self.visible.len() {
458 let Some(mut removed) = self.visible.pop_back() else {
459 return;
460 };
461 removed.clear(bg);
462 self.visible.push_front(removed);
463 return;
464 }
465
466 let visible = self.visible.make_contiguous();
467 visible[upper..=lower].rotate_right(1);
468 visible[upper].clear(bg);
469 }
470
471 pub(crate) fn resize_width(&mut self, sx: u32, bg: Colour) {
472 let sx = sx.max(1);
473 if sx == self.sx {
474 return;
475 }
476
477 let visible_rows = self.sy as usize;
478 let lines = self
479 .history
480 .iter()
481 .chain(self.visible.iter())
482 .cloned()
483 .collect::<Vec<_>>();
484 let mut reflowed = reflow_wrapped_lines(lines, sx, bg);
485 while reflowed.len() < visible_rows {
486 reflowed.push(GridLine::blank_with_bg(sx, bg));
487 }
488
489 let history_rows = reflowed.len().saturating_sub(visible_rows);
490 let mut visible = reflowed.split_off(history_rows);
491 for line in &mut visible {
492 line.resize_width_preserving_wrap(sx, bg);
493 }
494 self.history = compacted_history(reflowed);
495 while self.history.len() > self.hlimit {
496 let _ = self.history.pop_front();
497 }
498 self.visible = visible.into();
499 self.hscrolled = self.history.len();
500 self.sx = sx;
501 }
502
503 pub(crate) fn resize_height(&mut self, sy: u32, cursor_y: &mut u32, bg: Colour) {
504 let sy = sy.max(1);
505 let oldy = self.sy;
506
507 if sy < oldy {
508 let mut needed = oldy - sy;
509
510 let available_bottom = oldy.saturating_sub(1).saturating_sub(*cursor_y);
511 let remove_bottom = available_bottom.min(needed);
512 for _ in 0..remove_bottom {
513 let _ = self.visible.pop_back();
514 }
515 needed -= remove_bottom;
516
517 if self.history_enabled {
518 for _ in 0..needed {
519 let Some(line) = self.visible.pop_front() else {
520 break;
521 };
522 self.push_history(line);
523 }
524 } else {
525 let remove_top = (*cursor_y).min(needed);
526 for _ in 0..remove_top {
527 let _ = self.visible.pop_front();
528 }
529 *cursor_y = cursor_y.saturating_sub(remove_top);
530 }
531 } else if sy > oldy {
532 let mut needed = sy - oldy;
533 let pull = self.hscrolled.min(needed as usize).min(self.history.len()) as u32;
534 if self.history_enabled && pull > 0 {
535 let mut restored = Vec::with_capacity(pull as usize);
536 for _ in 0..pull {
537 if let Some(line) = self.history.pop_back() {
538 restored.push(line);
539 }
540 }
541 restored.reverse();
542 for mut line in restored.into_iter().rev() {
543 line.resize_width_preserving_wrap(self.sx, bg);
544 self.visible.push_front(line);
545 }
546 *cursor_y = cursor_y.saturating_add(pull).min(sy.saturating_sub(1));
547 self.hscrolled -= pull as usize;
548 needed -= pull;
549 }
550
551 for _ in 0..needed {
552 self.visible.push_back(GridLine::blank_with_bg(self.sx, bg));
553 }
554 }
555
556 self.sy = sy;
557 while self.visible.len() > self.sy as usize {
558 let _ = self.visible.pop_back();
559 }
560 while self.visible.len() < self.sy as usize {
561 self.visible.push_back(GridLine::blank_with_bg(self.sx, bg));
562 }
563 for line in &mut self.visible {
564 line.resize_width_preserving_wrap(self.sx, bg);
565 }
566 *cursor_y = (*cursor_y).min(self.sy.saturating_sub(1));
567 }
568
569 fn valid_region(&self, upper: u32, lower: u32) -> bool {
570 upper < self.sy && lower < self.sy && upper <= lower
571 }
572
573 fn push_history(&mut self, mut line: GridLine) {
574 if self.hlimit == 0 {
575 return;
576 }
577
578 line.stamp_for_history_at(self.next_history_stamp());
579 line.compact_for_history();
580 if self.history.len() == self.hlimit {
581 let _ = self.history.pop_front();
582 }
583 self.history.push_back(line);
584 self.hscrolled = (self.hscrolled + 1).min(self.history.len());
585 }
586
587 fn next_history_stamp(&mut self) -> i64 {
588 if self.history_stamp_remaining == 0 {
589 self.history_stamp = cell::current_unix_timestamp();
590 self.history_stamp_remaining = HISTORY_STAMP_REFRESH_LINES;
591 }
592 self.history_stamp_remaining = self.history_stamp_remaining.saturating_sub(1);
593 self.history_stamp
594 }
595}
596
597fn compacted_history(lines: Vec<GridLine>) -> VecDeque<GridLine> {
598 lines
599 .into_iter()
600 .map(|mut line| {
601 line.compact_for_history();
602 line
603 })
604 .collect()
605}
606
607fn reflow_wrapped_lines(lines: Vec<GridLine>, width: u32, bg: Colour) -> Vec<GridLine> {
608 let mut output = Vec::new();
609 let mut logical_cells = Vec::new();
610 let mut logical_flags = None;
611
612 for line in lines {
613 let wrapped = line.flags.contains(GridLineFlags::WRAPPED);
614 if logical_flags.is_none() {
615 let mut flags = line.flags;
616 flags.remove(GridLineFlags::WRAPPED);
617 logical_flags = Some(flags);
618 }
619
620 let end = if wrapped {
621 self::line_width(&line)
622 } else {
623 line.used_end()
624 };
625 if let Some(text) = line.plain_text() {
626 logical_cells.extend(
627 text.bytes()
628 .chain(std::iter::repeat(b' '))
629 .take(end)
630 .map(GridCell::from_plain_ascii),
631 );
632 } else {
633 logical_cells.extend(
634 line.cells
635 .iter()
636 .take(end)
637 .filter(|cell| !cell.is_padding())
638 .cloned(),
639 );
640 }
641
642 if !wrapped {
643 output.extend(reflow_logical_line(
644 &logical_cells,
645 logical_flags.take().unwrap_or_default(),
646 width,
647 bg,
648 ));
649 logical_cells.clear();
650 }
651 }
652
653 if logical_flags.is_some() || !logical_cells.is_empty() {
654 output.extend(reflow_logical_line(
655 &logical_cells,
656 logical_flags.unwrap_or_default(),
657 width,
658 bg,
659 ));
660 }
661
662 output
663}
664
665fn reflow_logical_line(
666 cells: &[GridCell],
667 first_flags: GridLineFlags,
668 width: u32,
669 bg: Colour,
670) -> Vec<GridLine> {
671 if cells.is_empty() {
672 let mut line = GridLine::blank_with_bg(width, bg);
673 line.flags = first_flags;
674 return vec![line];
675 }
676
677 let mut output = Vec::new();
678 let mut current = GridLine::blank_with_bg(width, bg);
679 current.flags = first_flags;
680 let mut x: u32 = 0;
681
682 for cell in cells {
683 let mut cell = cell.clone();
684 let mut cell_width = u32::from(cell.width().max(1));
685 if cell_width > width {
686 cell_width = 1;
687 cell.set_width(1);
688 }
689 if x > 0 && x.saturating_add(cell_width) > width {
690 current.set_wrapped(true);
691 output.push(current);
692 current = GridLine::blank_with_bg(width, bg);
693 x = 0;
694 }
695
696 if let Some(target) = current.cell_mut(x) {
697 *target = cell.clone();
698 }
699 for offset in 1..cell_width {
700 if let Some(padding_cell) = current.cell_mut(x + offset) {
701 let mut padding = cell.clone();
702 padding.set_text(" ".to_owned());
703 padding.set_width(0);
704 padding.set_flags(GridCellFlags::PADDING);
705 *padding_cell = padding;
706 }
707 }
708 current.touch();
709 x += cell_width;
710 }
711
712 output.push(current);
713 output
714}
715
716fn line_width(line: &GridLine) -> usize {
717 line.width() as usize
718}
719
720#[cfg(test)]
721#[path = "grid/tests.rs"]
722mod tests;