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