1use crate::grid::{Grid, GridCell, GridCellFlags, GridLine, GridPhysicalCursor};
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 capture::{RecoveryRow, RecoveryRowRenderer};
26pub use view::{ScreenCellRef, ScreenCellView, ScreenLineView};
27
28pub(crate) const MAX_TERMINAL_PASSTHROUGH_EVENTS: usize = 256;
29const TITLE_STACK_MAX: usize = 100;
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 title_rename_enabled: bool,
59 tabs: Vec<bool>,
60 hyperlinks: Hyperlinks,
61 active_hyperlink: u32,
62 metadata_revision: u64,
63 bell_count: u64,
64 terminal_passthrough: Vec<TerminalPassthrough>,
65 dropped_terminal_passthrough_count: u64,
66 has_selected_cells: bool,
67 utf8_config: Utf8Config,
68 alternate_screen_enabled: bool,
69 preserve_alternate_screen_cursor: bool,
70}
71
72impl Screen {
73 pub(crate) fn render_cell_state_ansi(&self, state: &CellState) -> Vec<u8> {
74 crate::grid::render_cell_state_ansi(state, &self.hyperlinks)
75 }
76
77 pub(crate) fn render_cell_state_ansi_bounded(
78 &self,
79 state: &CellState,
80 max_hyperlink_bytes: usize,
81 ) -> (Vec<u8>, bool) {
82 let mut bounded = state.clone();
83 let complete = self
84 .hyperlinks
85 .entry_fits(bounded.link(), max_hyperlink_bytes);
86 if !complete {
87 bounded.cell.link = 0;
88 }
89 (
90 crate::grid::render_cell_state_ansi(&bounded, &self.hyperlinks),
91 complete,
92 )
93 }
94
95 #[must_use]
97 pub fn new(size: TerminalSize, history_limit: usize) -> Self {
98 let grid = Grid::new(size, history_limit);
99 let mut screen = Self {
100 grid,
101 cursor_x: 0,
102 cursor_y: 0,
103 pending_wrap: false,
104 saved_cursor_x: None,
105 saved_cursor_y: None,
106 saved_cursor_pending_wrap: false,
107 saved_state: SavedState::default(),
108 saved_grid: None,
109 rupper: 0,
110 rlower: u32::from(size.rows.max(1)).saturating_sub(1),
111 mode: mode::MODE_CURSOR | mode::MODE_WRAP,
112 cursor_style: 0,
113 title: String::new(),
114 window_name: String::new(),
115 path: String::new(),
116 title_stack: Vec::new(),
117 title_rename_enabled: true,
118 tabs: Vec::new(),
119 hyperlinks: Hyperlinks::new(),
120 active_hyperlink: 0,
121 metadata_revision: 0,
122 bell_count: 0,
123 terminal_passthrough: Vec::new(),
124 dropped_terminal_passthrough_count: 0,
125 has_selected_cells: false,
126 utf8_config: Utf8Config::default(),
127 alternate_screen_enabled: true,
128 preserve_alternate_screen_cursor: false,
129 };
130 screen.reset_tabs();
131 screen
132 }
133
134 #[must_use]
136 pub const fn mode(&self) -> u32 {
137 self.mode
138 }
139
140 #[must_use]
142 pub const fn cursor_style(&self) -> u32 {
143 self.cursor_style
144 }
145
146 #[must_use]
149 pub const fn scroll_region(&self) -> (u32, u32) {
150 (self.rupper, self.rlower)
151 }
152
153 pub(crate) const fn plain_output_forwarding_safe(&self) -> bool {
154 let unsafe_modes = mode::MODE_INSERT | mode::MODE_CRLF | mode::MODE_SYNC;
155 !self.pending_wrap
156 && self.mode & mode::MODE_WRAP != 0
157 && self.mode & unsafe_modes == 0
158 && self.rupper == 0
159 && self.rlower == self.grid.sy().saturating_sub(1)
160 }
161
162 #[must_use]
164 pub fn size(&self) -> TerminalSize {
165 self.grid.size()
166 }
167
168 #[cfg_attr(not(test), allow(dead_code))]
169 #[must_use]
170 pub(crate) fn grid(&self) -> &Grid {
171 &self.grid
172 }
173
174 #[must_use]
176 pub fn title(&self) -> &str {
177 &self.title
178 }
179
180 #[must_use]
182 pub fn title_stack(&self) -> &[String] {
183 &self.title_stack
184 }
185
186 #[must_use]
188 pub const fn title_stack_limit() -> usize {
189 TITLE_STACK_MAX
190 }
191
192 pub fn set_title(&mut self, title: impl Into<String>) {
194 let title = title.into();
195 if self.title != title {
196 self.title = title;
197 self.bump_metadata_revision();
198 }
199 }
200
201 #[must_use]
203 pub const fn metadata_revision(&self) -> u64 {
204 self.metadata_revision
205 }
206
207 fn bump_metadata_revision(&mut self) {
208 self.metadata_revision = self.metadata_revision.saturating_add(1);
209 }
210
211 pub fn set_title_rename_enabled(&mut self, enabled: bool) {
213 self.title_rename_enabled = enabled;
214 }
215
216 pub fn set_alternate_screen_enabled(&mut self, enabled: bool) {
221 self.alternate_screen_enabled = enabled;
222 }
223
224 pub(crate) fn set_preserve_alternate_screen_cursor(&mut self, enabled: bool) {
225 self.preserve_alternate_screen_cursor = enabled;
226 }
227
228 #[must_use]
230 pub fn path(&self) -> &str {
231 &self.path
232 }
233
234 #[must_use]
236 pub fn is_alternate(&self) -> bool {
237 self.saved_grid.is_some()
238 }
239
240 #[must_use]
242 pub fn alternate_saved_cursor(&self) -> Option<(u32, u32, bool)> {
243 self.saved_cursor_x.zip(self.saved_cursor_y).map(|(x, y)| {
244 (
245 x,
246 y,
247 self.saved_cursor_pending_wrap && self.mode & mode::MODE_WRAP != 0,
248 )
249 })
250 }
251
252 #[must_use]
254 pub const fn pending_wrap(&self) -> bool {
255 self.pending_wrap
256 }
257
258 #[must_use]
260 pub fn tab_stops(&self) -> &[bool] {
261 &self.tabs
262 }
263
264 #[must_use]
271 pub(crate) fn clone_recovery_state(&self) -> Self {
272 let mut recovery = self.clone();
273 recovery.bell_count = 0;
274 recovery.terminal_passthrough.clear();
275 recovery.dropped_terminal_passthrough_count = 0;
276 recovery.has_selected_cells = false;
277 recovery
278 }
279
280 #[must_use]
282 pub fn history_limit(&self) -> usize {
283 self.grid.hlimit()
284 }
285
286 #[must_use]
288 pub fn history_size(&self) -> usize {
289 self.grid.hsize()
290 }
291
292 #[must_use]
294 pub const fn cursor_position(&self) -> (u32, u32) {
295 (self.cursor_x, self.cursor_y)
296 }
297
298 #[must_use]
300 pub fn cursor_absolute_y(&self) -> usize {
301 self.grid.hsize() + self.cursor_y as usize
302 }
303
304 #[must_use]
306 pub fn absolute_line_count(&self) -> usize {
307 self.grid.hsize() + self.grid.sy() as usize
308 }
309
310 pub fn delete_visible_line(&mut self, y: u32) -> bool {
315 if y >= self.grid.sy() {
316 return false;
317 }
318
319 let cursor_x = self.cursor_x;
320 let cursor_y = self.cursor_y;
321 let rupper = self.rupper;
322 let rlower = self.rlower;
323
324 self.cursor_x = 0;
325 self.cursor_y = y;
326 self.pending_wrap = false;
327 self.rupper = 0;
328 self.rlower = self.grid.sy().saturating_sub(1);
329 self.delete_line(1, COLOUR_DEFAULT);
330
331 self.cursor_y = if cursor_y > y {
332 cursor_y.saturating_sub(1)
333 } else {
334 cursor_y
335 }
336 .min(self.grid.sy().saturating_sub(1));
337 self.cursor_x = cursor_x.min(self.grid.sx().saturating_sub(1));
338 self.pending_wrap = false;
339 self.rupper = rupper;
340 self.rlower = rlower;
341 true
342 }
343
344 pub fn delete_absolute_line(&mut self, absolute_y: usize) -> bool {
346 let history_size = self.grid.hsize();
347 let visible_y = absolute_y.saturating_sub(history_size);
348 let removed = self.grid.remove_absolute_line(absolute_y);
349 if !removed {
350 return false;
351 }
352
353 if absolute_y >= history_size {
354 let visible_y = visible_y as u32;
355 if visible_y < self.cursor_y {
356 self.cursor_y = self.cursor_y.saturating_sub(1);
357 }
358 }
359 self.pending_wrap = false;
360 true
361 }
362
363 pub fn trim_below_cursor(&mut self) -> bool {
365 let cursor_absolute_y = self.cursor_absolute_y();
366 if !self.grid.truncate_after_absolute_line(cursor_absolute_y) {
367 return false;
368 }
369
370 let history_size = self.grid.hsize();
371 self.cursor_y = cursor_absolute_y
372 .saturating_sub(history_size)
373 .min(self.grid.sy().saturating_sub(1) as usize) as u32;
374 self.cursor_x = self.cursor_x.min(self.max_cursor_x());
375 self.pending_wrap = false;
376 true
377 }
378
379 #[must_use]
381 pub fn history_bytes(&self) -> usize {
382 self.grid.history_byte_size()
383 }
384
385 pub fn take_bell_count(&mut self) -> u64 {
387 let bell_count = self.bell_count;
388 self.bell_count = 0;
389 bell_count
390 }
391
392 pub fn take_terminal_passthrough(&mut self) -> Vec<TerminalPassthrough> {
394 std::mem::take(&mut self.terminal_passthrough)
395 }
396
397 pub fn take_terminal_passthrough_dropped_count(&mut self) -> u64 {
399 let dropped = self.dropped_terminal_passthrough_count;
400 self.dropped_terminal_passthrough_count = 0;
401 dropped
402 }
403
404 fn push_terminal_passthrough(&mut self, passthrough: TerminalPassthrough) {
405 if passthrough.payload().len() > MAX_TERMINAL_PASSTHROUGH_PAYLOAD_BYTES {
406 self.dropped_terminal_passthrough_count =
407 self.dropped_terminal_passthrough_count.saturating_add(1);
408 return;
409 }
410
411 let overflow = self
412 .terminal_passthrough
413 .len()
414 .saturating_add(1)
415 .saturating_sub(MAX_TERMINAL_PASSTHROUGH_EVENTS);
416 if overflow > 0 {
417 self.terminal_passthrough.drain(..overflow);
418 self.dropped_terminal_passthrough_count = self
419 .dropped_terminal_passthrough_count
420 .saturating_add(overflow as u64);
421 }
422
423 self.terminal_passthrough.push(passthrough);
424 }
425
426 #[must_use]
428 pub fn hyperlink_uri(&self, inner_id: u32) -> Option<&str> {
429 self.hyperlinks
430 .get(inner_id)
431 .map(|entry| entry.uri.as_str())
432 }
433
434 pub fn set_history_limit(&mut self, limit: usize) {
436 self.grid.set_hlimit(limit);
437 }
438
439 pub fn set_utf8_config(&mut self, utf8_config: Utf8Config) {
441 self.utf8_config = utf8_config;
442 }
443
444 pub fn resize(&mut self, size: TerminalSize) {
446 self.clear_selected_cells();
447 let cols = u32::from(size.cols.max(1));
448 let rows = u32::from(size.rows.max(1));
449 if cols != self.grid.sx() {
450 let remapped = if self.is_alternate() {
451 self.grid.resize_visible_width_preserving_cursor(
452 cols,
453 COLOUR_DEFAULT,
454 self.cursor_y,
455 self.cursor_x,
456 self.pending_wrap,
457 )
458 } else {
459 let cursor =
460 self.grid
461 .logical_cursor(self.cursor_y, self.cursor_x, self.pending_wrap);
462 self.grid
463 .resize_width_remapping_cursor(cols, COLOUR_DEFAULT, cursor)
464 };
465 self.apply_grid_cursor(remapped);
466 self.reset_tabs();
467 }
468 if rows != self.grid.sy() {
469 self.grid
470 .resize_height(rows, &mut self.cursor_y, COLOUR_DEFAULT);
471 }
472 self.rupper = 0;
473 self.rlower = rows.saturating_sub(1);
474 self.cursor_x = self.cursor_x.min(self.max_cursor_x());
475 self.pending_wrap = self.pending_wrap
476 && (self.mode & mode::MODE_WRAP) != 0
477 && self.cursor_x == self.max_cursor_x();
478 }
479
480 pub fn clear_history_and_hyperlinks(&mut self, reset_hyperlinks: bool) {
482 self.clear_selected_cells();
483 self.grid.clear_history();
484 if reset_hyperlinks {
485 self.hyperlinks.reset();
486 self.bump_metadata_revision();
487 }
488 }
489
490 fn reset_tabs(&mut self) {
491 self.tabs = vec![false; self.grid.sx() as usize];
492 for column in (8..self.grid.sx()).step_by(8) {
493 self.tabs[column as usize] = true;
494 }
495 }
496
497 fn max_cursor_x(&self) -> u32 {
498 self.grid.sx().saturating_sub(1)
499 }
500
501 fn cursor_column(&self) -> u32 {
502 self.cursor_x.min(self.max_cursor_x())
503 }
504
505 fn logical_insert_column(&self) -> u32 {
506 let x = self.cursor_column();
507 let Some(line) = self.grid.visible_line(self.cursor_y) else {
508 return x;
509 };
510 let Some(owner_x) = line.owning_cell_x(x).filter(|owner_x| *owner_x != x) else {
511 return x;
512 };
513 let width = line
514 .cell(owner_x)
515 .map_or(1, |cell| u32::from(cell.width()).max(1));
516 owner_x.saturating_add(width)
517 }
518
519 fn current_line_mut(&mut self) -> Option<&mut GridLine> {
520 self.grid.visible_line_mut(self.cursor_y)
521 }
522
523 fn clear_pending_wrap(&mut self) {
524 self.pending_wrap = false;
525 }
526
527 fn restore_cursor_position(&mut self, x: u32, y: u32, pending_wrap: bool) {
528 self.cursor_x = x.min(self.max_cursor_x());
529 self.cursor_y = y.min(self.grid.sy().saturating_sub(1));
530 self.pending_wrap = pending_wrap
531 && (self.mode & mode::MODE_WRAP) != 0
532 && self.cursor_x == self.max_cursor_x();
533 }
534
535 fn apply_grid_cursor(&mut self, cursor: GridPhysicalCursor) {
536 let history_size = self.grid.hsize();
537 self.cursor_x = cursor.x.min(self.max_cursor_x());
538 self.cursor_y = cursor
539 .absolute_y
540 .saturating_sub(history_size)
541 .min(self.grid.sy().saturating_sub(1) as usize) as u32;
542 self.pending_wrap = cursor.pending_wrap
543 && (self.mode & mode::MODE_WRAP) != 0
544 && self.cursor_x == self.max_cursor_x();
545 }
546
547 fn apply_pending_wrap(&mut self) {
548 if !self.pending_wrap || (self.mode & mode::MODE_WRAP) == 0 {
549 self.pending_wrap = false;
550 return;
551 }
552
553 if let Some(line) = self.current_line_mut() {
554 line.set_wrapped(true);
555 }
556 self.pending_wrap = false;
557 self.linefeed(false, COLOUR_DEFAULT);
558 self.cursor_x = 0;
559 }
560
561 fn blank_cell(&self, bg: i32) -> GridCell {
562 GridCell::blank_with_bg(bg)
563 }
564
565 fn repair_wide_cells_on_line(line: &mut GridLine, sx: u32, bg: i32) {
566 let blank = GridCell::blank_with_bg(bg);
567 let mut changed = false;
568 let mut x = 0;
569
570 while x < sx {
571 let Some(cell) = line.cell(x) else {
572 x += 1;
573 continue;
574 };
575
576 if cell.is_padding() {
577 if line.owning_cell_x(x).is_none() {
578 if let Some(target) = line.cell_mut(x) {
579 *target = blank.clone();
580 changed = true;
581 }
582 }
583 x += 1;
584 continue;
585 }
586
587 let width = u32::from(cell.width());
588 if width <= 1 {
589 x += 1;
590 continue;
591 }
592
593 let mut valid = x.saturating_add(width) <= sx;
594 if valid {
595 for offset in 1..width {
596 let column = x + offset;
597 let valid_padding = line
598 .cell(column)
599 .is_some_and(|candidate| candidate.is_padding())
600 && line.owning_cell_x(column) == Some(x);
601 if !valid_padding {
602 valid = false;
603 break;
604 }
605 }
606 }
607
608 if valid {
609 x += width;
610 continue;
611 }
612
613 if let Some(target) = line.cell_mut(x) {
614 *target = blank.clone();
615 changed = true;
616 }
617 x += 1;
618 }
619
620 if changed {
621 line.touch();
622 }
623 }
624
625 fn overwrite_for_write(&mut self, x: u32, width: u32) {
626 let sx = self.grid.sx();
627 let blank = GridCell::blank_with_bg(COLOUR_DEFAULT);
628 let Some(line) = self.current_line_mut() else {
629 return;
630 };
631
632 let current_is_padding = line.is_padding_cell(x);
633 if current_is_padding {
634 if let Some(owner_x) = line.owning_cell_x(x).filter(|owner_x| *owner_x != x) {
635 if let Some(owner) = line.cell_mut(owner_x) {
636 *owner = blank.clone();
637 }
638 }
639 }
640
641 let clear_following_padding = width != 1
642 || line
643 .cell(x)
644 .is_some_and(|cell| cell.width() != 1 || cell.is_padding());
645 if clear_following_padding {
646 let mut clear_x = x.saturating_add(width);
647 while clear_x < sx && line.is_padding_cell(clear_x) {
648 if let Some(cell) = line.cell_mut(clear_x) {
649 *cell = blank.clone();
650 }
651 clear_x += 1;
652 }
653 }
654
655 line.touch();
656 }
657
658 fn clear_line_range(&mut self, y: u32, start: u32, end_inclusive: u32, bg: i32) {
659 self.clear_selected_cells();
660 let sx = self.grid.sx();
661 let end = end_inclusive.min(sx.saturating_sub(1));
662 let clears_whole_line = start == 0 && end == sx.saturating_sub(1);
663 if clears_whole_line {
664 self.grid.break_wrap_before_visible_line(y);
665 }
666 let Some(line) = self.grid.visible_line_mut(y) else {
667 return;
668 };
669 for x in start.min(sx)..=end {
670 if let Some(cell) = line.cell_mut(x) {
671 *cell = GridCell::blank_with_bg(bg);
672 }
673 }
674 Self::repair_wide_cells_on_line(line, sx, bg);
675 if clears_whole_line {
676 line.set_wrapped(false);
677 }
678 line.touch();
679 }
680
681 fn clear_screen_region(&mut self, start_y: u32, end_y_inclusive: u32, bg: i32) {
682 self.clear_selected_cells();
683 for y in start_y..=end_y_inclusive.min(self.grid.sy().saturating_sub(1)) {
684 self.grid.break_wrap_before_visible_line(y);
685 if let Some(line) = self.grid.visible_line_mut(y) {
686 line.clear(bg);
687 }
688 }
689 }
690
691 fn write_char(&mut self, ch: char, cell: &CellState, acs: bool) {
692 if self.grid.sx() == 0 || self.grid.sy() == 0 {
693 return;
694 }
695 self.clear_selected_cells();
696
697 let ch = if acs { acs::translate_acs(ch) } else { ch };
698 let requested_width = u32::from(self.utf8_config.width(ch));
699 if self.combine_char(ch) {
700 return;
701 }
702 let width = requested_width.clamp(1, self.grid.sx());
706
707 let wrap_enabled = (self.mode & mode::MODE_WRAP) != 0;
708 let mut automatic_wrap_continuation = self.pending_wrap && wrap_enabled;
709 self.apply_pending_wrap();
710
711 if (self.mode & mode::MODE_INSERT) != 0 {
712 let insert_x = self.logical_insert_column();
713 if insert_x >= self.grid.sx() {
714 if !wrap_enabled {
715 return;
716 }
717 if let Some(line) = self.current_line_mut() {
718 line.set_wrapped(true);
719 }
720 self.linefeed(false, COLOUR_DEFAULT);
721 self.cursor_x = 0;
722 automatic_wrap_continuation = true;
723 } else {
724 self.cursor_x = insert_x;
725 }
726 }
727
728 let write_would_cross_right_edge =
729 width > self.grid.sx() || self.cursor_x > self.grid.sx().saturating_sub(width);
730 if !wrap_enabled && width > 1 && write_would_cross_right_edge {
731 return;
732 }
733
734 if (self.mode & mode::MODE_INSERT) != 0 {
737 <Self as ScreenWriter>::insert_character(self, width, cell.bg());
738 }
739
740 if wrap_enabled && write_would_cross_right_edge {
741 let gap_start = self.cursor_column();
742 if let Some(line) = self.current_line_mut() {
743 line.mark_unused_suffix_as_reflow_gap(gap_start);
744 line.set_wrapped(true);
745 }
746 self.linefeed(false, COLOUR_DEFAULT);
747 self.cursor_x = 0;
748 automatic_wrap_continuation = true;
749 }
750
751 if self.cursor_y >= self.grid.sy()
752 || self.cursor_column() > self.grid.sx().saturating_sub(width)
753 {
754 return;
755 }
756
757 let x = self.cursor_column();
758 if x == 0 && !automatic_wrap_continuation {
759 self.break_previous_wrapped_line();
760 }
761 self.overwrite_for_write(x, width);
762 if let Some(line) = self.current_line_mut() {
763 if let Some(target) = line.cell_mut(x) {
764 *target = GridCell::from_state(
765 ch,
766 u8::try_from(width).unwrap_or(1),
767 cell,
768 GridCellFlags::default(),
769 );
770 }
771 for offset in 1..width {
772 if let Some(padding) = line.cell_mut(x + offset) {
773 *padding = GridCell::from_state(' ', 0, cell, GridCellFlags::PADDING);
774 }
775 }
776 line.touch();
777 }
778
779 if wrap_enabled && x + width >= self.grid.sx() {
780 self.cursor_x = self.max_cursor_x();
781 self.pending_wrap = true;
782 } else {
783 self.cursor_x = x.saturating_add(width).min(self.max_cursor_x());
784 self.pending_wrap = false;
785 }
786 }
787
788 fn write_plain_ascii_run(&mut self, mut bytes: &[u8], cell: &CellState, acs: bool) -> bool {
789 if bytes.is_empty() {
790 return true;
791 }
792 if acs
793 || (self.mode & mode::MODE_INSERT) != 0
794 || cell.attr() != 0
795 || cell.fg() != COLOUR_DEFAULT
796 || cell.bg() != COLOUR_DEFAULT
797 || cell.us() != COLOUR_DEFAULT
798 || cell.link() != 0
799 || self.grid.sx() == 0
800 || self.grid.sy() == 0
801 {
802 return false;
803 }
804 self.clear_selected_cells();
805
806 while !bytes.is_empty() {
807 let automatic_wrap_continuation =
808 self.pending_wrap && (self.mode & mode::MODE_WRAP) != 0;
809 self.apply_pending_wrap();
810 if self.cursor_y >= self.grid.sy() {
811 self.write_ascii_run_slow(bytes, cell, acs);
812 return true;
813 }
814
815 let sx = self.grid.sx();
816 let x = self.cursor_column();
817 if x == 0 && !automatic_wrap_continuation {
818 self.break_previous_wrapped_line();
819 }
820
821 if (self.mode & mode::MODE_WRAP) == 0 {
822 let available = sx.saturating_sub(x) as usize;
823 if bytes.len() > available {
824 self.write_ascii_run_slow(bytes, cell, acs);
825 return true;
826 }
827 }
828
829 let writable = sx.saturating_sub(x) as usize;
830 if writable == 0 {
831 self.write_ascii_run_slow(bytes, cell, acs);
832 return true;
833 }
834 let chunk_len = bytes.len().min(writable);
835 let (chunk, rest) = bytes.split_at(chunk_len);
836 let wrote_chunk = self
837 .current_line_mut()
838 .is_some_and(|line| line.write_plain_ascii_run(x, chunk));
839 if !wrote_chunk {
840 self.write_ascii_run_slow(bytes, cell, acs);
844 return true;
845 }
846
847 if (self.mode & mode::MODE_WRAP) != 0 && x + chunk_len as u32 >= sx {
848 self.cursor_x = self.max_cursor_x();
849 self.pending_wrap = true;
850 } else {
851 self.cursor_x = x.saturating_add(chunk_len as u32).min(self.max_cursor_x());
852 self.pending_wrap = false;
853 }
854 bytes = rest;
855 }
856 true
857 }
858
859 fn write_ascii_run_slow(&mut self, bytes: &[u8], cell: &CellState, acs: bool) {
860 for &byte in bytes {
861 self.write_char(char::from(byte), cell, acs);
862 }
863 }
864
865 fn break_previous_wrapped_line(&mut self) {
866 if self.cursor_y == 0 {
867 return;
868 }
869 if let Some(previous) = self.grid.visible_line_mut(self.cursor_y - 1) {
870 previous.set_wrapped(false);
871 }
872 }
873
874 fn combine_char(&mut self, ch: char) -> bool {
875 let mut x = self.cursor_column();
876 if self.pending_wrap {
877 x = self.max_cursor_x();
878 } else if x == 0 {
879 return matches!(
880 utf8_combine_char(None, ch, &self.utf8_config),
881 CombineResult::Discard
882 );
883 } else {
884 x -= 1;
885 }
886
887 let Some((target_x, previous)) = self.grid.visible_line(self.cursor_y).map(|line| {
888 let target_x = line.owning_cell_x(x).unwrap_or(x);
889 let previous = line
890 .cell(target_x)
891 .map(|cell| (cell.text().to_owned(), cell.width()));
892 (target_x, previous)
893 }) else {
894 return matches!(
895 utf8_combine_char(None, ch, &self.utf8_config),
896 CombineResult::Discard
897 );
898 };
899 let result = utf8_combine_char(
900 previous
901 .as_ref()
902 .map(|(text, width)| (text.as_str(), *width)),
903 ch,
904 &self.utf8_config,
905 );
906
907 match result {
908 CombineResult::Standalone { .. } => false,
909 CombineResult::Discard => true,
910 CombineResult::Combined { text, width } => {
911 let previous_width = previous.as_ref().map_or(0, |(_, width)| *width);
912 let available_width = self.grid.sx().saturating_sub(target_x).max(1);
913 let width = width.min(u8::try_from(available_width).unwrap_or(u8::MAX));
914 if width != previous_width {
915 self.overwrite_for_write(target_x, u32::from(width));
919 }
920 let Some(line) = self.grid.visible_line_mut(self.cursor_y) else {
921 return true;
922 };
923 if let Some(cell) = line.cell_mut(target_x) {
924 cell.set_text(text);
925 cell.set_width(width);
926 if width == 2 {
927 let mut padding = cell.clone();
928 padding.set_text(" ".to_owned());
929 padding.set_width(0);
930 padding.set_flags(GridCellFlags::PADDING);
931 if let Some(padding_cell) = line.cell_mut(target_x + 1) {
932 *padding_cell = padding;
933 }
934 }
935 line.touch();
936 }
937 if previous_width == 1 && width == 2 && !self.pending_wrap {
938 let next_cursor = target_x.saturating_add(2);
939 if next_cursor >= self.grid.sx() {
940 self.cursor_x = self.max_cursor_x();
941 self.pending_wrap = (self.mode & mode::MODE_WRAP) != 0;
942 } else {
943 self.cursor_x = next_cursor;
944 }
945 }
946 true
947 }
948 }
949 }
950
951 fn parse_hyperlink(data: &str) -> (Option<String>, String) {
952 let (params, uri) = data.split_once(';').unwrap_or((data, ""));
953 let mut internal_id = None;
954 for part in params.split(':') {
955 if let Some(value) = part.strip_prefix("id=") {
956 internal_id = Some(value.to_owned());
957 }
958 }
959 (internal_id, uri.to_owned())
960 }
961}
962
963#[cfg(test)]
964#[path = "screen/capture_ascii_space_tests.rs"]
965mod capture_ascii_space_tests;
966#[cfg(test)]
967#[path = "screen/capture_mutation_tests.rs"]
968mod capture_mutation_tests;
969#[cfg(test)]
970#[path = "screen/erase_tests.rs"]
971mod erase_tests;
972#[cfg(test)]
973#[path = "screen/tests.rs"]
974mod tests;