1use crate::grid::{Grid, GridLine, GridLineFlags};
2use crate::input::{Colour, COLOUR_DEFAULT};
3
4use super::{SavedGrid, Screen};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct ScreenCellRef<'a> {
9 text: &'a str,
10 width: u8,
11 padding: bool,
12 attr: u16,
13 fg: Colour,
14 bg: Colour,
15 us: Colour,
16 link: u32,
17}
18
19impl<'a> ScreenCellRef<'a> {
20 #[must_use]
22 pub const fn text(&self) -> &'a str {
23 self.text
24 }
25
26 #[must_use]
28 pub const fn width(&self) -> u8 {
29 self.width
30 }
31
32 #[must_use]
34 pub const fn is_padding(&self) -> bool {
35 self.padding
36 }
37
38 #[must_use]
40 pub const fn attr(&self) -> u16 {
41 self.attr
42 }
43
44 #[must_use]
46 pub const fn fg(&self) -> Colour {
47 self.fg
48 }
49
50 #[must_use]
52 pub const fn bg(&self) -> Colour {
53 self.bg
54 }
55
56 #[must_use]
58 pub const fn us(&self) -> Colour {
59 self.us
60 }
61
62 #[must_use]
64 pub const fn link(&self) -> u32 {
65 self.link
66 }
67}
68
69fn blank_cell_ref() -> ScreenCellRef<'static> {
70 ScreenCellRef {
71 text: " ",
72 width: 1,
73 padding: false,
74 attr: 0,
75 fg: COLOUR_DEFAULT,
76 bg: COLOUR_DEFAULT,
77 us: COLOUR_DEFAULT,
78 link: 0,
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct ScreenCellView {
85 pub(super) text: String,
86 pub(super) width: u8,
87 pub(super) padding: bool,
88 pub(super) attr: u16,
89 pub(super) fg: crate::input::Colour,
90 pub(super) bg: crate::input::Colour,
91 pub(super) us: crate::input::Colour,
92 pub(super) link: u32,
93}
94
95impl ScreenCellView {
96 #[must_use]
98 pub fn text(&self) -> &str {
99 &self.text
100 }
101
102 #[must_use]
104 pub const fn width(&self) -> u8 {
105 self.width
106 }
107
108 #[must_use]
110 pub const fn is_padding(&self) -> bool {
111 self.padding
112 }
113
114 #[must_use]
116 pub const fn attr(&self) -> u16 {
117 self.attr
118 }
119
120 #[must_use]
122 pub const fn fg(&self) -> crate::input::Colour {
123 self.fg
124 }
125
126 #[must_use]
128 pub const fn bg(&self) -> crate::input::Colour {
129 self.bg
130 }
131
132 #[must_use]
134 pub const fn us(&self) -> crate::input::Colour {
135 self.us
136 }
137
138 #[must_use]
140 pub const fn link(&self) -> u32 {
141 self.link
142 }
143}
144
145#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct ScreenLineView {
148 pub(super) cells: Vec<ScreenCellView>,
149 width: u32,
150 pub(super) wrapped: bool,
151 pub(super) start_prompt: bool,
152 pub(super) start_output: bool,
153 pub(super) time: i64,
154}
155
156impl ScreenLineView {
157 #[must_use]
159 pub fn cells(&self) -> &[ScreenCellView] {
160 &self.cells
161 }
162
163 #[must_use]
165 pub const fn width(&self) -> u32 {
166 self.width
167 }
168
169 #[must_use]
171 pub fn cell(&self, x: u32) -> Option<&ScreenCellView> {
172 self.cells.get(x as usize)
173 }
174
175 #[must_use]
177 pub const fn wrapped(&self) -> bool {
178 self.wrapped
179 }
180
181 #[must_use]
183 pub const fn start_prompt(&self) -> bool {
184 self.start_prompt
185 }
186
187 #[must_use]
189 pub const fn start_output(&self) -> bool {
190 self.start_output
191 }
192
193 #[must_use]
195 pub const fn time(&self) -> i64 {
196 self.time
197 }
198
199 #[must_use]
201 pub fn owning_cell_x(&self, x: u32) -> Option<u32> {
202 if x >= self.width {
203 return None;
204 }
205 let Some(cell) = self.cell(x) else {
206 return Some(x);
207 };
208 if !cell.is_padding() {
209 return Some(x);
210 }
211
212 let mut owner = x;
213 while owner > 0 {
214 owner -= 1;
215 let candidate = self.cell(owner)?;
216 if !candidate.is_padding() {
217 let width = u32::from(candidate.width().max(1));
218 if owner.saturating_add(width) > x {
219 return Some(owner);
220 }
221 return None;
222 }
223 }
224 None
225 }
226}
227
228impl Screen {
229 #[must_use]
232 pub fn recovery_metadata_fits(
233 &self,
234 max_string_bytes: usize,
235 max_title_stack_bytes: usize,
236 max_hyperlink_entry_bytes: usize,
237 max_hyperlink_total_bytes: usize,
238 ) -> bool {
239 if self.title.len() > max_string_bytes
240 || self.window_name.len() > max_string_bytes
241 || self.path.len() > max_string_bytes
242 || self
243 .title_stack
244 .iter()
245 .any(|title| title.len() > max_string_bytes)
246 || self
247 .title_stack
248 .iter()
249 .try_fold(0_usize, |total, title| total.checked_add(title.len()))
250 .is_none_or(|total| total > max_title_stack_bytes)
251 {
252 return false;
253 }
254 let (hyperlinks, complete) = self
255 .hyperlinks
256 .clone_bounded(max_hyperlink_entry_bytes, max_hyperlink_total_bytes);
257 complete && (self.active_hyperlink == 0 || hyperlinks.get(self.active_hyperlink).is_some())
258 }
259
260 #[must_use]
266 pub fn clone_recovery_viewport_bounded(
267 &self,
268 max_string_bytes: usize,
269 max_title_stack_bytes: usize,
270 max_hyperlink_entry_bytes: usize,
271 max_hyperlink_total_bytes: usize,
272 ) -> (Self, bool) {
273 let (mut viewport, metadata_complete) = self.recovery_viewport_shell(
274 max_string_bytes,
275 max_title_stack_bytes,
276 max_hyperlink_entry_bytes,
277 max_hyperlink_total_bytes,
278 );
279
280 viewport.grid.replace_visible(self.grid.visible_lines());
281 viewport.cursor_x = self.cursor_x.min(viewport.max_cursor_x());
282 viewport.cursor_y = self.cursor_y.min(viewport.grid.sy().saturating_sub(1));
283 viewport.pending_wrap = self.pending_wrap;
284 viewport.saved_cursor_x = None;
285 viewport.saved_cursor_y = None;
286 viewport.saved_cursor_pending_wrap = false;
287 viewport.rupper = self.rupper.min(viewport.grid.sy().saturating_sub(1));
288 viewport.rlower = self.rlower.min(viewport.grid.sy().saturating_sub(1));
289 viewport.tabs = self.tabs.clone();
290 viewport.title_rename_enabled = self.title_rename_enabled;
291 viewport.alternate_screen_enabled = self.alternate_screen_enabled;
292 viewport.preserve_alternate_screen_cursor = self.preserve_alternate_screen_cursor;
293
294 (viewport, metadata_complete)
295 }
296
297 #[must_use]
303 pub fn clone_recovery_projection_bounded(
304 &self,
305 max_string_bytes: usize,
306 max_title_stack_bytes: usize,
307 max_hyperlink_entry_bytes: usize,
308 max_hyperlink_total_bytes: usize,
309 max_history_bytes: usize,
310 ) -> (Self, bool) {
311 let (mut projection, metadata_complete) = self.recovery_viewport_shell(
312 max_string_bytes,
313 max_title_stack_bytes,
314 max_hyperlink_entry_bytes,
315 max_hyperlink_total_bytes,
316 );
317
318 projection.grid = self.grid.clone_recovery_projection(max_history_bytes);
319 projection.cursor_x = self.cursor_x.min(projection.max_cursor_x());
320 projection.cursor_y = self.cursor_y.min(projection.grid.sy().saturating_sub(1));
321 projection.pending_wrap = self.pending_wrap;
322 projection.saved_cursor_x = self.saved_cursor_x;
323 projection.saved_cursor_y = self.saved_cursor_y;
324 projection.saved_cursor_pending_wrap = self.saved_cursor_pending_wrap;
325 projection.saved_grid = self.saved_grid.as_ref().map(|saved| SavedGrid {
326 grid: saved.grid.clone_recovery_projection(0),
327 history_enabled: saved.history_enabled,
328 });
329 projection.rupper = self.rupper.min(projection.grid.sy().saturating_sub(1));
330 projection.rlower = self.rlower.min(projection.grid.sy().saturating_sub(1));
331 projection.tabs = self.tabs.clone();
332 projection.title_rename_enabled = self.title_rename_enabled;
333 projection.alternate_screen_enabled = self.alternate_screen_enabled;
334 projection.preserve_alternate_screen_cursor = self.preserve_alternate_screen_cursor;
335
336 (projection, metadata_complete)
337 }
338
339 #[must_use]
342 #[allow(clippy::too_many_arguments)]
343 pub fn clone_recovery_viewport_at_bounded(
344 &self,
345 top_line: usize,
346 cursor_x: u32,
347 cursor_absolute_y: usize,
348 max_string_bytes: usize,
349 max_title_stack_bytes: usize,
350 max_hyperlink_entry_bytes: usize,
351 max_hyperlink_total_bytes: usize,
352 ) -> (Self, bool) {
353 let size = self.grid.size();
354 let rows = usize::from(size.rows.max(1));
355 let cols = u32::from(size.cols.max(1));
356 let total_lines = self.absolute_line_count();
357 let top_line = top_line.min(total_lines.saturating_sub(rows));
358 let (mut viewport, metadata_complete) = self.recovery_viewport_shell(
359 max_string_bytes,
360 max_title_stack_bytes,
361 max_hyperlink_entry_bytes,
362 max_hyperlink_total_bytes,
363 );
364 let lines = (0..rows)
365 .map(|offset| {
366 self.grid
367 .absolute_line(top_line + offset)
368 .cloned()
369 .unwrap_or_else(|| GridLine::new(cols))
370 })
371 .collect();
372 viewport.grid.replace_visible(lines);
373 viewport.cursor_x = cursor_x.min(viewport.max_cursor_x());
374 viewport.cursor_y = if (top_line..top_line + rows).contains(&cursor_absolute_y) {
375 (cursor_absolute_y - top_line) as u32
376 } else {
377 0
378 };
379 viewport.pending_wrap = false;
380 viewport.rupper = 0;
381 viewport.rlower = u32::from(size.rows.max(1)).saturating_sub(1);
382 viewport.reset_tabs();
383 (viewport, metadata_complete)
384 }
385
386 #[allow(clippy::too_many_arguments)]
387 fn recovery_viewport_shell(
388 &self,
389 max_string_bytes: usize,
390 max_title_stack_bytes: usize,
391 max_hyperlink_entry_bytes: usize,
392 max_hyperlink_total_bytes: usize,
393 ) -> (Self, bool) {
394 let mut viewport = Self::new(self.grid.size(), 0);
395 let mut metadata_complete = true;
396 viewport.mode = self.mode;
397 viewport.cursor_style = self.cursor_style;
398 viewport.title = bounded_utf8_string(&self.title, max_string_bytes, &mut metadata_complete);
399 viewport.window_name =
400 bounded_utf8_string(&self.window_name, max_string_bytes, &mut metadata_complete);
401 viewport.path = bounded_utf8_string(&self.path, max_string_bytes, &mut metadata_complete);
402 viewport.title_stack = bounded_title_stack(
403 &self.title_stack,
404 max_string_bytes,
405 max_title_stack_bytes,
406 &mut metadata_complete,
407 );
408 let (hyperlinks, hyperlinks_complete) = self
409 .hyperlinks
410 .clone_bounded(max_hyperlink_entry_bytes, max_hyperlink_total_bytes);
411 metadata_complete &= hyperlinks_complete;
412 viewport.active_hyperlink = if hyperlinks.get(self.active_hyperlink).is_some() {
413 self.active_hyperlink
414 } else {
415 metadata_complete &= self.active_hyperlink == 0;
416 0
417 };
418 viewport.hyperlinks = hyperlinks;
419 viewport.metadata_revision = self.metadata_revision;
420 viewport.utf8_config = self.utf8_config.clone();
421 viewport.saved_cursor_x = None;
422 viewport.saved_cursor_y = None;
423 viewport.saved_cursor_pending_wrap = false;
424 viewport.saved_grid = self.saved_grid.as_ref().map(|saved| SavedGrid {
425 grid: Grid::new(self.grid.size(), 0),
426 history_enabled: saved.history_enabled,
427 });
428 viewport.title_rename_enabled = self.title_rename_enabled;
429 viewport.alternate_screen_enabled = self.alternate_screen_enabled;
430 viewport.preserve_alternate_screen_cursor = self.preserve_alternate_screen_cursor;
431 (viewport, metadata_complete)
432 }
433
434 pub fn visit_visible_line_cells(
441 &self,
442 row: usize,
443 cols: usize,
444 mut visit: impl FnMut(ScreenCellRef<'_>),
445 ) -> bool {
446 let Some(line) = self
447 .grid
448 .visible_line(u32::try_from(row).unwrap_or(u32::MAX))
449 else {
450 return false;
451 };
452 if let Some(text) = line.plain_text() {
453 let text_cols = text.len().min(cols);
454 for col in 0..text_cols {
455 visit(ScreenCellRef {
456 text: &text[col..col + 1],
457 width: 1,
458 padding: false,
459 attr: 0,
460 fg: COLOUR_DEFAULT,
461 bg: COLOUR_DEFAULT,
462 us: COLOUR_DEFAULT,
463 link: 0,
464 });
465 }
466 for _ in text_cols..cols {
467 visit(blank_cell_ref());
468 }
469 return true;
470 }
471
472 let mut emitted = 0_usize;
473 for cell in line.cells().iter().take(cols) {
474 visit(ScreenCellRef {
475 text: cell.text(),
476 width: cell.width(),
477 padding: cell.is_padding(),
478 attr: cell.attr(),
479 fg: cell.fg(),
480 bg: cell.bg(),
481 us: cell.us(),
482 link: cell.link(),
483 });
484 emitted += 1;
485 }
486 for _ in emitted..cols {
487 visit(blank_cell_ref());
488 }
489 true
490 }
491
492 #[must_use]
494 pub fn absolute_line_view(&self, absolute_y: usize) -> Option<ScreenLineView> {
495 let line = self.grid.absolute_line(absolute_y)?;
496 let width = u32::from(self.grid.size().cols.max(1));
497 let cells = if let Some(text) = line.plain_text() {
498 let mut cells = text
499 .bytes()
500 .map(|byte| ScreenCellView {
501 text: char::from(byte).to_string(),
502 width: 1,
503 padding: false,
504 attr: 0,
505 fg: crate::input::COLOUR_DEFAULT,
506 bg: crate::input::COLOUR_DEFAULT,
507 us: crate::input::COLOUR_DEFAULT,
508 link: 0,
509 })
510 .collect::<Vec<_>>();
511 cells.resize_with(width as usize, || ScreenCellView {
512 text: " ".to_owned(),
513 width: 1,
514 padding: false,
515 attr: 0,
516 fg: crate::input::COLOUR_DEFAULT,
517 bg: crate::input::COLOUR_DEFAULT,
518 us: crate::input::COLOUR_DEFAULT,
519 link: 0,
520 });
521 cells
522 } else {
523 line.cells()
524 .iter()
525 .map(|cell| ScreenCellView {
526 text: cell.text().to_owned(),
527 width: cell.width(),
528 padding: cell.is_padding(),
529 attr: cell.attr(),
530 fg: cell.fg(),
531 bg: cell.bg(),
532 us: cell.us(),
533 link: cell.link(),
534 })
535 .collect()
536 };
537 Some(ScreenLineView {
538 cells,
539 width,
540 wrapped: line.flags().contains(GridLineFlags::WRAPPED),
541 start_prompt: line.flags().contains(GridLineFlags::START_PROMPT),
542 start_output: line.flags().contains(GridLineFlags::START_OUTPUT),
543 time: line.time(),
544 })
545 }
546
547 #[must_use]
549 pub fn clone_viewport(&self, top_line: usize, cursor_x: u32, cursor_absolute_y: usize) -> Self {
550 let size = self.grid.size();
551 let rows = usize::from(size.rows.max(1));
552 let cols = u32::from(size.cols.max(1));
553 let total_lines = self.absolute_line_count();
554 let top_line = top_line.min(total_lines.saturating_sub(rows));
555 let mut viewport = Self::new(size, 0);
556
557 viewport.mode = self.mode;
558 viewport.cursor_style = self.cursor_style;
559 viewport.title = self.title.clone();
560 viewport.window_name = self.window_name.clone();
561 viewport.path = self.path.clone();
562 viewport.title_stack = self.title_stack.clone();
563 viewport.hyperlinks = self.hyperlinks.clone();
564 viewport.active_hyperlink = self.active_hyperlink;
565 viewport.metadata_revision = self.metadata_revision;
566 viewport.bell_count = 0;
567 viewport.utf8_config = self.utf8_config.clone();
568
569 let lines = (0..rows)
570 .map(|offset| {
571 self.grid
572 .absolute_line(top_line + offset)
573 .cloned()
574 .unwrap_or_else(|| GridLine::new(cols))
575 })
576 .collect();
577 viewport.grid.replace_visible(lines);
578
579 viewport.cursor_x = cursor_x.min(viewport.max_cursor_x());
580 viewport.cursor_y = if (top_line..top_line + rows).contains(&cursor_absolute_y) {
581 (cursor_absolute_y - top_line) as u32
582 } else {
583 0
584 };
585 viewport.pending_wrap = false;
586 viewport.saved_cursor_x = None;
587 viewport.saved_cursor_y = None;
588 viewport.saved_cursor_pending_wrap = false;
589 viewport.saved_grid = None;
590 viewport.rupper = 0;
591 viewport.rlower = u32::from(size.rows.max(1)).saturating_sub(1);
592 viewport.reset_tabs();
593 viewport
594 }
595}
596
597fn bounded_utf8_string(value: &str, max_bytes: usize, complete: &mut bool) -> String {
598 if value.len() <= max_bytes {
599 return value.to_owned();
600 }
601 *complete = false;
602 let mut end = max_bytes.min(value.len());
603 while end > 0 && !value.is_char_boundary(end) {
604 end -= 1;
605 }
606 value[..end].to_owned()
607}
608
609fn bounded_title_stack(
610 titles: &[String],
611 max_entry_bytes: usize,
612 max_total_bytes: usize,
613 complete: &mut bool,
614) -> Vec<String> {
615 let mut retained_reversed = Vec::new();
616 let mut retained_bytes = 0_usize;
617 for title in titles.iter().rev() {
618 let title = bounded_utf8_string(title, max_entry_bytes, complete);
619 let next = retained_bytes.saturating_add(title.len());
620 if next > max_total_bytes {
621 *complete = false;
622 break;
623 }
624 retained_reversed.push(title);
625 retained_bytes = next;
626 }
627 if retained_reversed.len() != titles.len() {
628 *complete = false;
629 }
630 retained_reversed.reverse();
631 retained_reversed
632}