1use std::collections::VecDeque;
4
5use rmux_proto::TerminalSize;
6
7use crate::hyperlinks::Hyperlinks;
8use crate::input::{Colour, COLOUR_DEFAULT};
9
10#[path = "grid/cell.rs"]
11mod cell;
12#[path = "grid/render.rs"]
13mod render;
14
15pub(crate) use cell::{GridCell, GridCellFlags, GridLine, GridLineFlags};
16use render::{append_cell_text, append_grid_string_code, append_hyperlink};
17
18#[derive(Debug, Clone, PartialEq, Eq, Default)]
20#[cfg_attr(not(test), allow(dead_code))]
21pub(crate) struct GridCapture {
22 pub lines: Vec<String>,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct GridRenderOptions {
29 pub join_wrapped: bool,
31 pub with_sequences: bool,
33 pub escape_sequences: bool,
35 pub include_empty_cells: bool,
37 pub trim_spaces: bool,
39}
40
41impl Default for GridRenderOptions {
42 fn default() -> Self {
43 Self {
44 join_wrapped: false,
45 with_sequences: false,
46 escape_sequences: false,
47 include_empty_cells: true,
48 trim_spaces: true,
49 }
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct GridStringState {
56 last_cell: GridCell,
57}
58
59impl Default for GridStringState {
60 fn default() -> Self {
61 Self {
62 last_cell: GridCell::blank_with_bg(COLOUR_DEFAULT),
63 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
69pub(crate) struct Grid {
70 sx: u32,
71 sy: u32,
72 hlimit: usize,
73 hscrolled: usize,
74 history_enabled: bool,
75 history: VecDeque<GridLine>,
76 visible: Vec<GridLine>,
77}
78
79impl Grid {
80 #[must_use]
82 pub fn new(size: TerminalSize, hlimit: usize) -> Self {
83 let sx = u32::from(size.cols.max(1));
84 let sy = u32::from(size.rows.max(1));
85 Self {
86 sx,
87 sy,
88 hlimit,
89 hscrolled: 0,
90 history_enabled: true,
91 history: VecDeque::new(),
92 visible: (0..sy).map(|_| GridLine::new(sx)).collect(),
93 }
94 }
95
96 #[must_use]
98 pub fn size(&self) -> TerminalSize {
99 TerminalSize {
100 cols: u16::try_from(self.sx).unwrap_or(u16::MAX),
101 rows: u16::try_from(self.sy).unwrap_or(u16::MAX),
102 }
103 }
104
105 #[must_use]
107 pub const fn sx(&self) -> u32 {
108 self.sx
109 }
110
111 #[must_use]
113 pub const fn sy(&self) -> u32 {
114 self.sy
115 }
116
117 #[must_use]
119 pub fn hsize(&self) -> usize {
120 self.history.len()
121 }
122
123 #[must_use]
125 pub const fn hlimit(&self) -> usize {
126 self.hlimit
127 }
128
129 #[must_use]
131 pub const fn history_enabled(&self) -> bool {
132 self.history_enabled
133 }
134
135 pub fn set_hlimit(&mut self, hlimit: usize) {
137 self.hlimit = hlimit;
138 while self.history.len() > self.hlimit {
139 let _ = self.history.pop_front();
140 }
141 self.hscrolled = self.hscrolled.min(self.history.len());
142 }
143
144 pub fn set_history_enabled(&mut self, enabled: bool) {
146 self.history_enabled = enabled;
147 }
148
149 #[allow(dead_code)]
151 #[must_use]
152 pub const fn hscrolled(&self) -> usize {
153 self.hscrolled
154 }
155
156 #[must_use]
158 pub fn visible_line(&self, y: u32) -> Option<&GridLine> {
159 self.visible.get(y as usize)
160 }
161
162 pub(crate) fn visible_line_mut(&mut self, y: u32) -> Option<&mut GridLine> {
163 self.visible.get_mut(y as usize)
164 }
165
166 #[allow(dead_code)]
169 #[must_use]
170 pub fn absolute_line(&self, absolute_y: usize) -> Option<&GridLine> {
171 if absolute_y < self.history.len() {
172 self.history.get(absolute_y)
173 } else {
174 self.visible.get(absolute_y - self.history.len())
175 }
176 }
177
178 pub fn remove_absolute_line(&mut self, absolute_y: usize) -> bool {
183 if absolute_y < self.history.len() {
184 let _ = self.history.remove(absolute_y);
185 self.hscrolled = self.hscrolled.min(self.history.len());
186 return true;
187 }
188
189 let visible_index = absolute_y.saturating_sub(self.history.len());
190 if visible_index >= self.visible.len() {
191 return false;
192 }
193
194 let _ = self.visible.remove(visible_index);
195 self.visible.push(GridLine::new(self.sx));
196 true
197 }
198
199 #[must_use]
201 pub fn absolute_line_wrapped(&self, absolute_y: usize) -> Option<bool> {
202 self.absolute_line(absolute_y)
203 .map(|line| line.flags.contains(GridLineFlags::WRAPPED))
204 }
205
206 pub fn clear_history(&mut self) {
208 self.history.clear();
209 self.hscrolled = 0;
210 }
211
212 pub fn clear_visible(&mut self, bg: Colour) {
214 for line in &mut self.visible {
215 line.clear(bg);
216 }
217 }
218
219 pub fn replace_visible(&mut self, lines: Vec<GridLine>) {
221 self.sy = lines.len() as u32;
222 self.visible = lines;
223 for line in &mut self.visible {
224 line.resize_width_preserving_wrap(self.sx, COLOUR_DEFAULT);
225 }
226 }
227
228 #[cfg_attr(not(test), allow(dead_code))]
230 #[must_use]
231 pub fn capture(&self, join_wrapped: bool) -> GridCapture {
232 let mut lines = Vec::new();
233 let mut pending = String::new();
234
235 for line in self.history.iter().chain(self.visible.iter()) {
236 let rendered = line.render_text();
237 if join_wrapped {
238 pending.push_str(&rendered);
239 if !line.flags.contains(GridLineFlags::WRAPPED) {
240 lines.push(std::mem::take(&mut pending));
241 }
242 continue;
243 }
244
245 lines.push(rendered);
246 }
247
248 if join_wrapped && !pending.is_empty() {
249 lines.push(pending);
250 }
251
252 GridCapture { lines }
253 }
254
255 #[must_use]
257 pub fn render_absolute_line(
258 &self,
259 absolute_y: usize,
260 options: GridRenderOptions,
261 state: &mut GridStringState,
262 hyperlinks: Option<&Hyperlinks>,
263 ) -> Option<String> {
264 self.absolute_line(absolute_y)
265 .map(|line| line.render_with_options(options, state, hyperlinks))
266 }
267
268 #[must_use]
270 pub fn history_byte_size(&self) -> usize {
271 self.history
272 .iter()
273 .map(|line| line.render_text().len() + 1)
274 .sum()
275 }
276
277 #[must_use]
279 pub fn visible_lines(&self) -> Vec<GridLine> {
280 self.visible.clone()
281 }
282
283 pub(crate) fn scroll_region_up(
284 &mut self,
285 upper: u32,
286 lower: u32,
287 bg: Colour,
288 to_history: bool,
289 ) {
290 if !self.valid_region(upper, lower) {
291 return;
292 }
293
294 let removed = self.visible.remove(upper as usize);
295 if to_history && self.history_enabled {
296 self.push_history(removed);
297 }
298 self.visible
299 .insert(lower as usize, GridLine::blank_with_bg(self.sx, bg));
300 }
301
302 pub(crate) fn scroll_region_down(&mut self, upper: u32, lower: u32, bg: Colour) {
303 if !self.valid_region(upper, lower) {
304 return;
305 }
306
307 let _ = self.visible.remove(lower as usize);
308 self.visible
309 .insert(upper as usize, GridLine::blank_with_bg(self.sx, bg));
310 }
311
312 pub(crate) fn resize_width(&mut self, sx: u32, bg: Colour) {
313 let sx = sx.max(1);
314 if sx == self.sx {
315 return;
316 }
317
318 let visible_rows = self.sy as usize;
319 let lines = self
320 .history
321 .iter()
322 .chain(self.visible.iter())
323 .cloned()
324 .collect::<Vec<_>>();
325 let mut reflowed = reflow_wrapped_lines(lines, sx, bg);
326 while reflowed.len() < visible_rows {
327 reflowed.push(GridLine::blank_with_bg(sx, bg));
328 }
329
330 let history_rows = reflowed.len().saturating_sub(visible_rows);
331 let visible = reflowed.split_off(history_rows);
332 self.history = reflowed.into();
333 while self.history.len() > self.hlimit {
334 let _ = self.history.pop_front();
335 }
336 self.visible = visible;
337 self.hscrolled = self.history.len();
338 self.sx = sx;
339 }
340
341 pub(crate) fn resize_height(&mut self, sy: u32, cursor_y: &mut u32, bg: Colour) {
342 let sy = sy.max(1);
343 let oldy = self.sy;
344
345 if sy < oldy {
346 let mut needed = oldy - sy;
347
348 let available_bottom = oldy.saturating_sub(1).saturating_sub(*cursor_y);
349 let remove_bottom = available_bottom.min(needed);
350 for _ in 0..remove_bottom {
351 let _ = self.visible.pop();
352 }
353 needed -= remove_bottom;
354
355 if self.history_enabled {
356 for _ in 0..needed {
357 let Some(line) = self.visible.first().cloned() else {
358 break;
359 };
360 let _ = self.visible.remove(0);
361 self.push_history(line);
362 }
363 } else {
364 let remove_top = (*cursor_y).min(needed);
365 for _ in 0..remove_top {
366 if !self.visible.is_empty() {
367 let _ = self.visible.remove(0);
368 }
369 }
370 *cursor_y = cursor_y.saturating_sub(remove_top);
371 }
372 } else if sy > oldy {
373 let mut needed = sy - oldy;
374 let pull = self.hscrolled.min(needed as usize).min(self.history.len()) as u32;
375 if self.history_enabled && pull > 0 {
376 let mut restored = Vec::with_capacity(pull as usize);
377 for _ in 0..pull {
378 if let Some(line) = self.history.pop_back() {
379 restored.push(line);
380 }
381 }
382 restored.reverse();
383 for line in restored.into_iter().rev() {
384 self.visible.insert(0, line);
385 }
386 *cursor_y = cursor_y.saturating_add(pull).min(sy.saturating_sub(1));
387 self.hscrolled -= pull as usize;
388 needed -= pull;
389 }
390
391 for _ in 0..needed {
392 self.visible.push(GridLine::blank_with_bg(self.sx, bg));
393 }
394 }
395
396 self.sy = sy;
397 self.visible
398 .resize_with(self.sy as usize, || GridLine::blank_with_bg(self.sx, bg));
399 *cursor_y = (*cursor_y).min(self.sy.saturating_sub(1));
400 }
401
402 fn valid_region(&self, upper: u32, lower: u32) -> bool {
403 upper < self.sy && lower < self.sy && upper <= lower
404 }
405
406 fn push_history(&mut self, mut line: GridLine) {
407 if self.hlimit == 0 {
408 return;
409 }
410
411 line.touch();
412 if self.history.len() == self.hlimit {
413 let _ = self.history.pop_front();
414 }
415 self.history.push_back(line);
416 self.hscrolled = (self.hscrolled + 1).min(self.history.len());
417 }
418}
419
420fn reflow_wrapped_lines(lines: Vec<GridLine>, width: u32, bg: Colour) -> Vec<GridLine> {
421 let mut output = Vec::new();
422 let mut logical_cells = Vec::new();
423 let mut logical_flags = None;
424
425 for line in lines {
426 let wrapped = line.flags.contains(GridLineFlags::WRAPPED);
427 if logical_flags.is_none() {
428 let mut flags = line.flags;
429 flags.remove(GridLineFlags::WRAPPED);
430 logical_flags = Some(flags);
431 }
432
433 let end = if wrapped {
434 line.cells.len()
435 } else {
436 line.used_end()
437 };
438 logical_cells.extend(
439 line.cells
440 .iter()
441 .take(end)
442 .filter(|cell| !cell.is_padding())
443 .cloned(),
444 );
445
446 if !wrapped {
447 output.extend(reflow_logical_line(
448 &logical_cells,
449 logical_flags.take().unwrap_or_default(),
450 width,
451 bg,
452 ));
453 logical_cells.clear();
454 }
455 }
456
457 if logical_flags.is_some() || !logical_cells.is_empty() {
458 output.extend(reflow_logical_line(
459 &logical_cells,
460 logical_flags.unwrap_or_default(),
461 width,
462 bg,
463 ));
464 }
465
466 output
467}
468
469fn reflow_logical_line(
470 cells: &[GridCell],
471 first_flags: GridLineFlags,
472 width: u32,
473 bg: Colour,
474) -> Vec<GridLine> {
475 if cells.is_empty() {
476 let mut line = GridLine::blank_with_bg(width, bg);
477 line.flags = first_flags;
478 return vec![line];
479 }
480
481 let mut output = Vec::new();
482 let mut current = GridLine::blank_with_bg(width, bg);
483 current.flags = first_flags;
484 let mut x: u32 = 0;
485
486 for cell in cells {
487 let mut cell = cell.clone();
488 let mut cell_width = u32::from(cell.width().max(1));
489 if cell_width > width {
490 cell_width = 1;
491 cell.set_width(1);
492 }
493 if x > 0 && x.saturating_add(cell_width) > width {
494 current.set_wrapped(true);
495 output.push(current);
496 current = GridLine::blank_with_bg(width, bg);
497 x = 0;
498 }
499
500 if let Some(target) = current.cells.get_mut(x as usize) {
501 *target = cell.clone();
502 }
503 for offset in 1..cell_width {
504 if let Some(padding_cell) = current.cells.get_mut((x + offset) as usize) {
505 let mut padding = cell.clone();
506 padding.set_text(" ".to_owned());
507 padding.set_width(0);
508 padding.set_flags(GridCellFlags::PADDING);
509 *padding_cell = padding;
510 }
511 }
512 current.touch();
513 x += cell_width;
514 }
515
516 output.push(current);
517 output
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523 use crate::input::CellState;
524
525 #[test]
526 fn render_without_trimming_preserves_explicit_trailing_spaces_but_not_cleared_cells() {
527 let mut line = GridLine::new(6);
528 let state = CellState::default();
529 for (x, ch) in "A ".chars().enumerate() {
530 *line.cell_mut(x as u32).expect("cell exists") =
531 GridCell::from_state(ch, 1, &state, GridCellFlags::default());
532 }
533
534 let mut render_state = GridStringState::default();
535 let rendered = line.render_with_options(
536 GridRenderOptions {
537 trim_spaces: false,
538 include_empty_cells: false,
539 ..GridRenderOptions::default()
540 },
541 &mut render_state,
542 None,
543 );
544 assert_eq!(rendered, "A ");
545
546 let mut render_state = GridStringState::default();
547 let trimmed = line.render_with_options(
548 GridRenderOptions {
549 trim_spaces: true,
550 include_empty_cells: false,
551 ..GridRenderOptions::default()
552 },
553 &mut render_state,
554 None,
555 );
556 assert_eq!(trimmed, "A");
557 }
558
559 #[test]
560 fn capture_join_wrapped_keeps_spaces_at_wrapped_boundaries() {
561 let mut grid = Grid::new(TerminalSize { cols: 6, rows: 2 }, 0);
562 let state = CellState::default();
563 let first = grid.visible_line_mut(0).expect("line exists");
564 for (x, ch) in "user ".chars().enumerate() {
565 *first.cell_mut(x as u32).expect("cell exists") =
566 GridCell::from_state(ch, 1, &state, GridCellFlags::default());
567 }
568 first.set_wrapped(true);
569 let second = grid.visible_line_mut(1).expect("line exists");
570 for (x, ch) in "root".chars().enumerate() {
571 *second.cell_mut(x as u32).expect("cell exists") =
572 GridCell::from_state(ch, 1, &state, GridCellFlags::default());
573 }
574
575 let mut render_state = GridStringState::default();
576 let mut output = Vec::new();
577 for absolute_y in 0..2 {
578 let line = grid
579 .render_absolute_line(
580 absolute_y,
581 GridRenderOptions {
582 join_wrapped: true,
583 trim_spaces: false,
584 include_empty_cells: false,
585 ..GridRenderOptions::default()
586 },
587 &mut render_state,
588 None,
589 )
590 .expect("line renders");
591 output.extend_from_slice(line.as_bytes());
592 if !grid.absolute_line_wrapped(absolute_y).unwrap_or(false) {
593 output.push(b'\n');
594 }
595 }
596
597 assert_eq!(String::from_utf8(output).expect("utf8"), "user root\n");
598 }
599}