1use crate::grid::{Grid, GridCell, GridCellFlags, GridLine};
4use crate::hyperlinks::Hyperlinks;
5use crate::input::{mode, CellState, SavedState, ScreenWriter, COLOUR_DEFAULT};
6use crate::terminal_passthrough::{TerminalPassthrough, MAX_TERMINAL_PASSTHROUGH_PAYLOAD_BYTES};
7use crate::utf8::{combine_char as utf8_combine_char, CombineResult, Utf8Config};
8use rmux_proto::TerminalSize;
9
10#[path = "screen/acs.rs"]
11mod acs;
12#[path = "screen/capture.rs"]
13mod capture;
14#[path = "screen/history_bytes.rs"]
15mod history_bytes;
16#[path = "screen/selection.rs"]
17mod selection;
18#[path = "screen/style_overlay.rs"]
19mod style_overlay;
20#[path = "screen/view.rs"]
21mod view;
22#[path = "screen/writer.rs"]
23mod writer;
24
25pub use view::{ScreenCellRef, ScreenCellView, ScreenLineView};
26
27pub(crate) const MAX_TERMINAL_PASSTHROUGH_EVENTS: usize = 256;
28const TITLE_STACK_MAX: usize = 100;
29
30#[derive(Debug, Clone, PartialEq, Eq)]
31struct SavedGrid {
32 grid: Grid,
33 history_enabled: bool,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct Screen {
40 grid: Grid,
41 cursor_x: u32,
42 cursor_y: u32,
43 pending_wrap: bool,
44 saved_cursor_x: Option<u32>,
45 saved_cursor_y: Option<u32>,
46 saved_cursor_pending_wrap: bool,
47 saved_state: SavedState,
48 saved_grid: Option<SavedGrid>,
49 rupper: u32,
50 rlower: u32,
51 mode: u32,
52 cursor_style: u32,
53 title: String,
54 window_name: String,
55 path: String,
56 title_stack: Vec<String>,
57 title_rename_enabled: bool,
58 tabs: Vec<bool>,
59 hyperlinks: Hyperlinks,
60 active_hyperlink: u32,
61 bell_count: u64,
62 terminal_passthrough: Vec<TerminalPassthrough>,
63 dropped_terminal_passthrough_count: u64,
64 has_selected_cells: bool,
65 utf8_config: Utf8Config,
66 alternate_screen_enabled: bool,
67 preserve_alternate_screen_cursor: bool,
68}
69
70impl Screen {
71 #[must_use]
73 pub fn new(size: TerminalSize, history_limit: usize) -> Self {
74 let grid = Grid::new(size, history_limit);
75 let mut screen = Self {
76 grid,
77 cursor_x: 0,
78 cursor_y: 0,
79 pending_wrap: false,
80 saved_cursor_x: None,
81 saved_cursor_y: None,
82 saved_cursor_pending_wrap: false,
83 saved_state: SavedState::default(),
84 saved_grid: None,
85 rupper: 0,
86 rlower: u32::from(size.rows.max(1)).saturating_sub(1),
87 mode: mode::MODE_CURSOR | mode::MODE_WRAP,
88 cursor_style: 0,
89 title: String::new(),
90 window_name: String::new(),
91 path: String::new(),
92 title_stack: Vec::new(),
93 title_rename_enabled: true,
94 tabs: Vec::new(),
95 hyperlinks: Hyperlinks::new(),
96 active_hyperlink: 0,
97 bell_count: 0,
98 terminal_passthrough: Vec::new(),
99 dropped_terminal_passthrough_count: 0,
100 has_selected_cells: false,
101 utf8_config: Utf8Config::default(),
102 alternate_screen_enabled: true,
103 preserve_alternate_screen_cursor: false,
104 };
105 screen.reset_tabs();
106 screen
107 }
108
109 #[must_use]
111 pub const fn mode(&self) -> u32 {
112 self.mode
113 }
114
115 #[must_use]
117 pub const fn cursor_style(&self) -> u32 {
118 self.cursor_style
119 }
120
121 #[must_use]
124 pub const fn scroll_region(&self) -> (u32, u32) {
125 (self.rupper, self.rlower)
126 }
127
128 #[must_use]
130 pub fn size(&self) -> TerminalSize {
131 self.grid.size()
132 }
133
134 #[cfg_attr(not(test), allow(dead_code))]
135 #[must_use]
136 pub(crate) fn grid(&self) -> &Grid {
137 &self.grid
138 }
139
140 #[must_use]
142 pub fn title(&self) -> &str {
143 &self.title
144 }
145
146 pub fn set_title(&mut self, title: impl Into<String>) {
148 self.title = title.into();
149 }
150
151 pub fn set_title_rename_enabled(&mut self, enabled: bool) {
153 self.title_rename_enabled = enabled;
154 }
155
156 pub fn set_alternate_screen_enabled(&mut self, enabled: bool) {
161 self.alternate_screen_enabled = enabled;
162 }
163
164 pub(crate) fn set_preserve_alternate_screen_cursor(&mut self, enabled: bool) {
165 self.preserve_alternate_screen_cursor = enabled;
166 }
167
168 #[must_use]
170 pub fn path(&self) -> &str {
171 &self.path
172 }
173
174 #[must_use]
176 pub fn is_alternate(&self) -> bool {
177 self.saved_grid.is_some()
178 }
179
180 #[must_use]
182 pub fn history_limit(&self) -> usize {
183 self.grid.hlimit()
184 }
185
186 #[must_use]
188 pub fn history_size(&self) -> usize {
189 self.grid.hsize()
190 }
191
192 #[must_use]
194 pub const fn cursor_position(&self) -> (u32, u32) {
195 (self.cursor_x, self.cursor_y)
196 }
197
198 #[must_use]
200 pub fn cursor_absolute_y(&self) -> usize {
201 self.grid.hsize() + self.cursor_y as usize
202 }
203
204 #[must_use]
206 pub fn absolute_line_count(&self) -> usize {
207 self.grid.hsize() + self.grid.sy() as usize
208 }
209
210 pub fn delete_visible_line(&mut self, y: u32) -> bool {
215 if y >= self.grid.sy() {
216 return false;
217 }
218
219 let cursor_x = self.cursor_x;
220 let cursor_y = self.cursor_y;
221 let rupper = self.rupper;
222 let rlower = self.rlower;
223
224 self.cursor_x = 0;
225 self.cursor_y = y;
226 self.pending_wrap = false;
227 self.rupper = 0;
228 self.rlower = self.grid.sy().saturating_sub(1);
229 self.delete_line(1, COLOUR_DEFAULT);
230
231 self.cursor_y = if cursor_y > y {
232 cursor_y.saturating_sub(1)
233 } else {
234 cursor_y
235 }
236 .min(self.grid.sy().saturating_sub(1));
237 self.cursor_x = cursor_x.min(self.grid.sx().saturating_sub(1));
238 self.pending_wrap = false;
239 self.rupper = rupper;
240 self.rlower = rlower;
241 true
242 }
243
244 pub fn delete_absolute_line(&mut self, absolute_y: usize) -> bool {
246 let history_size = self.grid.hsize();
247 let visible_y = absolute_y.saturating_sub(history_size);
248 let removed = self.grid.remove_absolute_line(absolute_y);
249 if !removed {
250 return false;
251 }
252
253 if absolute_y >= history_size {
254 let visible_y = visible_y as u32;
255 if visible_y < self.cursor_y {
256 self.cursor_y = self.cursor_y.saturating_sub(1);
257 }
258 }
259 self.pending_wrap = false;
260 true
261 }
262
263 pub fn trim_below_cursor(&mut self) -> bool {
265 let cursor_absolute_y = self.cursor_absolute_y();
266 if !self.grid.truncate_after_absolute_line(cursor_absolute_y) {
267 return false;
268 }
269
270 let history_size = self.grid.hsize();
271 self.cursor_y = cursor_absolute_y
272 .saturating_sub(history_size)
273 .min(self.grid.sy().saturating_sub(1) as usize) as u32;
274 self.cursor_x = self.cursor_x.min(self.max_cursor_x());
275 self.pending_wrap = false;
276 true
277 }
278
279 #[must_use]
281 pub fn history_bytes(&self) -> usize {
282 self.grid.history_byte_size()
283 }
284
285 pub fn take_bell_count(&mut self) -> u64 {
287 let bell_count = self.bell_count;
288 self.bell_count = 0;
289 bell_count
290 }
291
292 pub fn take_terminal_passthrough(&mut self) -> Vec<TerminalPassthrough> {
294 std::mem::take(&mut self.terminal_passthrough)
295 }
296
297 pub fn take_terminal_passthrough_dropped_count(&mut self) -> u64 {
299 let dropped = self.dropped_terminal_passthrough_count;
300 self.dropped_terminal_passthrough_count = 0;
301 dropped
302 }
303
304 fn push_terminal_passthrough(&mut self, passthrough: TerminalPassthrough) {
305 if passthrough.payload().len() > MAX_TERMINAL_PASSTHROUGH_PAYLOAD_BYTES {
306 self.dropped_terminal_passthrough_count =
307 self.dropped_terminal_passthrough_count.saturating_add(1);
308 return;
309 }
310
311 let overflow = self
312 .terminal_passthrough
313 .len()
314 .saturating_add(1)
315 .saturating_sub(MAX_TERMINAL_PASSTHROUGH_EVENTS);
316 if overflow > 0 {
317 self.terminal_passthrough.drain(..overflow);
318 self.dropped_terminal_passthrough_count = self
319 .dropped_terminal_passthrough_count
320 .saturating_add(overflow as u64);
321 }
322
323 self.terminal_passthrough.push(passthrough);
324 }
325
326 #[must_use]
328 pub fn hyperlink_uri(&self, inner_id: u32) -> Option<&str> {
329 self.hyperlinks
330 .get(inner_id)
331 .map(|entry| entry.uri.as_str())
332 }
333
334 pub fn set_history_limit(&mut self, limit: usize) {
336 self.grid.set_hlimit(limit);
337 }
338
339 pub fn set_utf8_config(&mut self, utf8_config: Utf8Config) {
341 self.utf8_config = utf8_config;
342 }
343
344 pub fn resize(&mut self, size: TerminalSize) {
346 self.clear_selected_cells();
347 let cols = u32::from(size.cols.max(1));
348 let rows = u32::from(size.rows.max(1));
349 if cols != self.grid.sx() {
350 self.grid.resize_width(cols, COLOUR_DEFAULT);
351 self.reset_tabs();
352 }
353 if rows != self.grid.sy() {
354 self.grid
355 .resize_height(rows, &mut self.cursor_y, COLOUR_DEFAULT);
356 }
357 self.rupper = 0;
358 self.rlower = rows.saturating_sub(1);
359 self.cursor_x = self.cursor_x.min(self.max_cursor_x());
360 self.pending_wrap &= self.cursor_x == self.max_cursor_x();
361 }
362
363 pub fn clear_history_and_hyperlinks(&mut self, reset_hyperlinks: bool) {
365 self.clear_selected_cells();
366 self.grid.clear_history();
367 if reset_hyperlinks {
368 self.hyperlinks.reset();
369 }
370 }
371
372 fn reset_tabs(&mut self) {
373 self.tabs = vec![false; self.grid.sx() as usize];
374 for column in (8..self.grid.sx()).step_by(8) {
375 self.tabs[column as usize] = true;
376 }
377 }
378
379 fn max_cursor_x(&self) -> u32 {
380 self.grid.sx().saturating_sub(1)
381 }
382
383 fn cursor_column(&self) -> u32 {
384 self.cursor_x.min(self.max_cursor_x())
385 }
386
387 fn current_line_mut(&mut self) -> Option<&mut GridLine> {
388 self.grid.visible_line_mut(self.cursor_y)
389 }
390
391 fn clear_pending_wrap(&mut self) {
392 self.pending_wrap = false;
393 }
394
395 fn restore_cursor_position(&mut self, x: u32, y: u32, pending_wrap: bool) {
396 self.cursor_x = x.min(self.max_cursor_x());
397 self.cursor_y = y.min(self.grid.sy().saturating_sub(1));
398 self.pending_wrap = pending_wrap
399 && (self.mode & mode::MODE_WRAP) != 0
400 && self.cursor_x == self.max_cursor_x();
401 }
402
403 fn apply_pending_wrap(&mut self) {
404 if !self.pending_wrap || (self.mode & mode::MODE_WRAP) == 0 {
405 self.pending_wrap = false;
406 return;
407 }
408
409 if let Some(line) = self.current_line_mut() {
410 line.set_wrapped(true);
411 }
412 self.pending_wrap = false;
413 self.linefeed(false, COLOUR_DEFAULT);
414 self.cursor_x = 0;
415 }
416
417 fn blank_cell(&self, bg: i32) -> GridCell {
418 GridCell::blank_with_bg(bg)
419 }
420
421 fn repair_wide_cells_on_line(line: &mut GridLine, sx: u32, bg: i32) {
422 let blank = GridCell::blank_with_bg(bg);
423 let mut changed = false;
424 let mut x = 0;
425
426 while x < sx {
427 let Some(cell) = line.cell(x) else {
428 x += 1;
429 continue;
430 };
431
432 if cell.is_padding() {
433 if line.owning_cell_x(x).is_none() {
434 if let Some(target) = line.cell_mut(x) {
435 *target = blank.clone();
436 changed = true;
437 }
438 }
439 x += 1;
440 continue;
441 }
442
443 let width = u32::from(cell.width());
444 if width <= 1 {
445 x += 1;
446 continue;
447 }
448
449 let mut valid = x.saturating_add(width) <= sx;
450 if valid {
451 for offset in 1..width {
452 let column = x + offset;
453 let valid_padding = line
454 .cell(column)
455 .is_some_and(|candidate| candidate.is_padding())
456 && line.owning_cell_x(column) == Some(x);
457 if !valid_padding {
458 valid = false;
459 break;
460 }
461 }
462 }
463
464 if valid {
465 x += width;
466 continue;
467 }
468
469 if let Some(target) = line.cell_mut(x) {
470 *target = blank.clone();
471 changed = true;
472 }
473 x += 1;
474 }
475
476 if changed {
477 line.touch();
478 }
479 }
480
481 fn overwrite_for_write(&mut self, x: u32, width: u32) {
482 let sx = self.grid.sx();
483 let blank = GridCell::blank_with_bg(COLOUR_DEFAULT);
484 let Some(line) = self.current_line_mut() else {
485 return;
486 };
487
488 let current_is_padding = line.is_padding_cell(x);
489 if current_is_padding {
490 if let Some(owner_x) = line.owning_cell_x(x).filter(|owner_x| *owner_x != x) {
491 if let Some(owner) = line.cell_mut(owner_x) {
492 *owner = blank.clone();
493 }
494 }
495 }
496
497 let clear_following_padding = width != 1
498 || line
499 .cell(x)
500 .is_some_and(|cell| cell.width() != 1 || cell.is_padding());
501 if clear_following_padding {
502 let mut clear_x = x.saturating_add(width);
503 while clear_x < sx && line.is_padding_cell(clear_x) {
504 if let Some(cell) = line.cell_mut(clear_x) {
505 *cell = blank.clone();
506 }
507 clear_x += 1;
508 }
509 }
510
511 line.touch();
512 }
513
514 fn clear_line_range(&mut self, y: u32, start: u32, end_inclusive: u32, bg: i32) {
515 self.clear_selected_cells();
516 let sx = self.grid.sx();
517 let end = end_inclusive.min(sx.saturating_sub(1));
518 let Some(line) = self.grid.visible_line_mut(y) else {
519 return;
520 };
521 for x in start.min(sx)..=end {
522 if let Some(cell) = line.cell_mut(x) {
523 *cell = GridCell::blank_with_bg(bg);
524 }
525 }
526 Self::repair_wide_cells_on_line(line, sx, bg);
527 line.set_wrapped(false);
528 line.touch();
529 }
530
531 fn clear_screen_region(&mut self, start_y: u32, end_y_inclusive: u32, bg: i32) {
532 self.clear_selected_cells();
533 for y in start_y..=end_y_inclusive.min(self.grid.sy().saturating_sub(1)) {
534 if let Some(line) = self.grid.visible_line_mut(y) {
535 line.clear(bg);
536 }
537 }
538 }
539
540 fn write_char(&mut self, ch: char, cell: &CellState, acs: bool) {
541 if self.grid.sx() == 0 || self.grid.sy() == 0 {
542 return;
543 }
544 self.clear_selected_cells();
545
546 let ch = if acs { acs::translate_acs(ch) } else { ch };
547 let width = u32::from(self.utf8_config.width(ch));
548 if self.combine_char(ch) {
549 return;
550 }
551
552 let automatic_wrap_continuation = self.pending_wrap && (self.mode & mode::MODE_WRAP) != 0;
553 self.apply_pending_wrap();
554
555 if (self.mode & mode::MODE_WRAP) != 0
556 && self.cursor_x > self.grid.sx().saturating_sub(width)
557 {
558 if let Some(line) = self.current_line_mut() {
559 line.set_wrapped(true);
560 }
561 self.linefeed(false, COLOUR_DEFAULT);
562 self.cursor_x = 0;
563 }
564
565 if (self.mode & mode::MODE_WRAP) == 0
566 && width > 1
567 && (width > self.grid.sx() || self.cursor_x > self.grid.sx().saturating_sub(width))
568 {
569 return;
570 }
571
572 if self.cursor_y >= self.grid.sy()
573 || self.cursor_column() > self.grid.sx().saturating_sub(width)
574 {
575 return;
576 }
577
578 let x = self.cursor_column();
579 if x == 0 && !automatic_wrap_continuation {
580 self.break_previous_wrapped_line();
581 }
582 self.overwrite_for_write(x, width);
583 if let Some(line) = self.current_line_mut() {
584 if let Some(target) = line.cell_mut(x) {
585 *target = GridCell::from_state(
586 ch,
587 u8::try_from(width).unwrap_or(1),
588 cell,
589 GridCellFlags::default(),
590 );
591 }
592 for offset in 1..width {
593 if let Some(padding) = line.cell_mut(x + offset) {
594 *padding = GridCell::from_state(' ', 0, cell, GridCellFlags::PADDING);
595 }
596 }
597 line.touch();
598 }
599
600 if (self.mode & mode::MODE_WRAP) != 0 && x + width >= self.grid.sx() {
601 self.cursor_x = self.max_cursor_x();
602 self.pending_wrap = true;
603 } else {
604 self.cursor_x = x.saturating_add(width).min(self.max_cursor_x());
605 self.pending_wrap = false;
606 }
607 }
608
609 fn write_plain_ascii_run(&mut self, mut bytes: &[u8], cell: &CellState, acs: bool) -> bool {
610 if bytes.is_empty() {
611 return true;
612 }
613 if acs
614 || cell.attr() != 0
615 || cell.fg() != COLOUR_DEFAULT
616 || cell.bg() != COLOUR_DEFAULT
617 || cell.us() != COLOUR_DEFAULT
618 || cell.link() != 0
619 || self.grid.sx() == 0
620 || self.grid.sy() == 0
621 {
622 return false;
623 }
624 self.clear_selected_cells();
625
626 while !bytes.is_empty() {
627 let automatic_wrap_continuation =
628 self.pending_wrap && (self.mode & mode::MODE_WRAP) != 0;
629 self.apply_pending_wrap();
630 if self.cursor_y >= self.grid.sy() {
631 return false;
632 }
633
634 let sx = self.grid.sx();
635 let x = self.cursor_column();
636 if x == 0 && !automatic_wrap_continuation {
637 self.break_previous_wrapped_line();
638 }
639
640 if (self.mode & mode::MODE_WRAP) == 0 {
641 let available = sx.saturating_sub(x) as usize;
642 if bytes.len() > available {
643 return false;
644 }
645 }
646
647 let writable = sx.saturating_sub(x) as usize;
648 if writable == 0 {
649 return false;
650 }
651 let chunk_len = bytes.len().min(writable);
652 let (chunk, rest) = bytes.split_at(chunk_len);
653 let Some(line) = self.current_line_mut() else {
654 return false;
655 };
656 if !line.write_plain_ascii_run(x, chunk) {
657 return false;
658 }
659
660 if (self.mode & mode::MODE_WRAP) != 0 && x + chunk_len as u32 >= sx {
661 self.cursor_x = self.max_cursor_x();
662 self.pending_wrap = true;
663 } else {
664 self.cursor_x = x.saturating_add(chunk_len as u32).min(self.max_cursor_x());
665 self.pending_wrap = false;
666 }
667 bytes = rest;
668 }
669 true
670 }
671
672 fn break_previous_wrapped_line(&mut self) {
673 if self.cursor_y == 0 {
674 return;
675 }
676 if let Some(previous) = self.grid.visible_line_mut(self.cursor_y - 1) {
677 previous.set_wrapped(false);
678 }
679 }
680
681 fn combine_char(&mut self, ch: char) -> bool {
682 let mut x = self.cursor_column();
683 if self.pending_wrap {
684 x = self.max_cursor_x();
685 } else if x == 0 {
686 return matches!(
687 utf8_combine_char(None, ch, &self.utf8_config),
688 CombineResult::Discard
689 );
690 } else {
691 x -= 1;
692 }
693
694 let Some(line) = self.grid.visible_line_mut(self.cursor_y) else {
695 return matches!(
696 utf8_combine_char(None, ch, &self.utf8_config),
697 CombineResult::Discard
698 );
699 };
700 let target_x = line.owning_cell_x(x).unwrap_or(x);
701 let previous = line
702 .cell(target_x)
703 .map(|cell| (cell.text().to_owned(), cell.width()));
704 let result = utf8_combine_char(
705 previous
706 .as_ref()
707 .map(|(text, width)| (text.as_str(), *width)),
708 ch,
709 &self.utf8_config,
710 );
711
712 match result {
713 CombineResult::Standalone { .. } => false,
714 CombineResult::Discard => true,
715 CombineResult::Combined { text, width } => {
716 let previous_width = previous.as_ref().map_or(0, |(_, width)| *width);
717 if let Some(cell) = line.cell_mut(target_x) {
718 cell.set_text(text);
719 cell.set_width(width);
720 if width == 2 {
721 let mut padding = cell.clone();
722 padding.set_text(" ".to_owned());
723 padding.set_width(0);
724 padding.set_flags(GridCellFlags::PADDING);
725 if let Some(padding_cell) = line.cell_mut(target_x + 1) {
726 *padding_cell = padding;
727 }
728 }
729 line.touch();
730 }
731 if previous_width == 1 && width == 2 && !self.pending_wrap {
732 let next_cursor = target_x.saturating_add(2);
733 if next_cursor >= self.grid.sx() {
734 self.cursor_x = self.max_cursor_x();
735 self.pending_wrap = (self.mode & mode::MODE_WRAP) != 0;
736 } else {
737 self.cursor_x = next_cursor;
738 }
739 }
740 true
741 }
742 }
743 }
744
745 fn parse_hyperlink(data: &str) -> (Option<String>, String) {
746 let (params, uri) = data.split_once(';').unwrap_or((data, ""));
747 let mut internal_id = None;
748 for part in params.split(':') {
749 if let Some(value) = part.strip_prefix("id=") {
750 internal_id = Some(value.to_owned());
751 }
752 }
753 (internal_id, uri.to_owned())
754 }
755}
756
757#[cfg(test)]
758#[path = "screen/tests.rs"]
759mod tests;