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