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