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 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 pub(crate) const fn plain_output_forwarding_safe(&self) -> bool {
129 let unsafe_modes = mode::MODE_INSERT | mode::MODE_CRLF | mode::MODE_SYNC;
130 !self.pending_wrap
131 && self.mode & mode::MODE_WRAP != 0
132 && self.mode & unsafe_modes == 0
133 && self.rupper == 0
134 && self.rlower == self.grid.sy().saturating_sub(1)
135 }
136
137 #[must_use]
139 pub fn size(&self) -> TerminalSize {
140 self.grid.size()
141 }
142
143 #[cfg_attr(not(test), allow(dead_code))]
144 #[must_use]
145 pub(crate) fn grid(&self) -> &Grid {
146 &self.grid
147 }
148
149 #[must_use]
151 pub fn title(&self) -> &str {
152 &self.title
153 }
154
155 pub fn set_title(&mut self, title: impl Into<String>) {
157 self.title = title.into();
158 }
159
160 pub fn set_title_rename_enabled(&mut self, enabled: bool) {
162 self.title_rename_enabled = enabled;
163 }
164
165 pub fn set_alternate_screen_enabled(&mut self, enabled: bool) {
170 self.alternate_screen_enabled = enabled;
171 }
172
173 pub(crate) fn set_preserve_alternate_screen_cursor(&mut self, enabled: bool) {
174 self.preserve_alternate_screen_cursor = enabled;
175 }
176
177 #[must_use]
179 pub fn path(&self) -> &str {
180 &self.path
181 }
182
183 #[must_use]
185 pub fn is_alternate(&self) -> bool {
186 self.saved_grid.is_some()
187 }
188
189 #[must_use]
191 pub fn history_limit(&self) -> usize {
192 self.grid.hlimit()
193 }
194
195 #[must_use]
197 pub fn history_size(&self) -> usize {
198 self.grid.hsize()
199 }
200
201 #[must_use]
203 pub const fn cursor_position(&self) -> (u32, u32) {
204 (self.cursor_x, self.cursor_y)
205 }
206
207 #[must_use]
209 pub fn cursor_absolute_y(&self) -> usize {
210 self.grid.hsize() + self.cursor_y as usize
211 }
212
213 #[must_use]
215 pub fn absolute_line_count(&self) -> usize {
216 self.grid.hsize() + self.grid.sy() as usize
217 }
218
219 pub fn delete_visible_line(&mut self, y: u32) -> bool {
224 if y >= self.grid.sy() {
225 return false;
226 }
227
228 let cursor_x = self.cursor_x;
229 let cursor_y = self.cursor_y;
230 let rupper = self.rupper;
231 let rlower = self.rlower;
232
233 self.cursor_x = 0;
234 self.cursor_y = y;
235 self.pending_wrap = false;
236 self.rupper = 0;
237 self.rlower = self.grid.sy().saturating_sub(1);
238 self.delete_line(1, COLOUR_DEFAULT);
239
240 self.cursor_y = if cursor_y > y {
241 cursor_y.saturating_sub(1)
242 } else {
243 cursor_y
244 }
245 .min(self.grid.sy().saturating_sub(1));
246 self.cursor_x = cursor_x.min(self.grid.sx().saturating_sub(1));
247 self.pending_wrap = false;
248 self.rupper = rupper;
249 self.rlower = rlower;
250 true
251 }
252
253 pub fn delete_absolute_line(&mut self, absolute_y: usize) -> bool {
255 let history_size = self.grid.hsize();
256 let visible_y = absolute_y.saturating_sub(history_size);
257 let removed = self.grid.remove_absolute_line(absolute_y);
258 if !removed {
259 return false;
260 }
261
262 if absolute_y >= history_size {
263 let visible_y = visible_y as u32;
264 if visible_y < self.cursor_y {
265 self.cursor_y = self.cursor_y.saturating_sub(1);
266 }
267 }
268 self.pending_wrap = false;
269 true
270 }
271
272 pub fn trim_below_cursor(&mut self) -> bool {
274 let cursor_absolute_y = self.cursor_absolute_y();
275 if !self.grid.truncate_after_absolute_line(cursor_absolute_y) {
276 return false;
277 }
278
279 let history_size = self.grid.hsize();
280 self.cursor_y = cursor_absolute_y
281 .saturating_sub(history_size)
282 .min(self.grid.sy().saturating_sub(1) as usize) as u32;
283 self.cursor_x = self.cursor_x.min(self.max_cursor_x());
284 self.pending_wrap = false;
285 true
286 }
287
288 #[must_use]
290 pub fn history_bytes(&self) -> usize {
291 self.grid.history_byte_size()
292 }
293
294 pub fn take_bell_count(&mut self) -> u64 {
296 let bell_count = self.bell_count;
297 self.bell_count = 0;
298 bell_count
299 }
300
301 pub fn take_terminal_passthrough(&mut self) -> Vec<TerminalPassthrough> {
303 std::mem::take(&mut self.terminal_passthrough)
304 }
305
306 pub fn take_terminal_passthrough_dropped_count(&mut self) -> u64 {
308 let dropped = self.dropped_terminal_passthrough_count;
309 self.dropped_terminal_passthrough_count = 0;
310 dropped
311 }
312
313 fn push_terminal_passthrough(&mut self, passthrough: TerminalPassthrough) {
314 if passthrough.payload().len() > MAX_TERMINAL_PASSTHROUGH_PAYLOAD_BYTES {
315 self.dropped_terminal_passthrough_count =
316 self.dropped_terminal_passthrough_count.saturating_add(1);
317 return;
318 }
319
320 let overflow = self
321 .terminal_passthrough
322 .len()
323 .saturating_add(1)
324 .saturating_sub(MAX_TERMINAL_PASSTHROUGH_EVENTS);
325 if overflow > 0 {
326 self.terminal_passthrough.drain(..overflow);
327 self.dropped_terminal_passthrough_count = self
328 .dropped_terminal_passthrough_count
329 .saturating_add(overflow as u64);
330 }
331
332 self.terminal_passthrough.push(passthrough);
333 }
334
335 #[must_use]
337 pub fn hyperlink_uri(&self, inner_id: u32) -> Option<&str> {
338 self.hyperlinks
339 .get(inner_id)
340 .map(|entry| entry.uri.as_str())
341 }
342
343 pub fn set_history_limit(&mut self, limit: usize) {
345 self.grid.set_hlimit(limit);
346 }
347
348 pub fn set_utf8_config(&mut self, utf8_config: Utf8Config) {
350 self.utf8_config = utf8_config;
351 }
352
353 pub fn resize(&mut self, size: TerminalSize) {
355 self.clear_selected_cells();
356 let cols = u32::from(size.cols.max(1));
357 let rows = u32::from(size.rows.max(1));
358 if cols != self.grid.sx() {
359 let remapped = if self.is_alternate() {
360 self.grid.resize_visible_width_preserving_cursor(
361 cols,
362 COLOUR_DEFAULT,
363 self.cursor_y,
364 self.cursor_x,
365 self.pending_wrap,
366 )
367 } else {
368 let cursor =
369 self.grid
370 .logical_cursor(self.cursor_y, self.cursor_x, self.pending_wrap);
371 self.grid
372 .resize_width_remapping_cursor(cols, COLOUR_DEFAULT, cursor)
373 };
374 self.apply_grid_cursor(remapped);
375 self.reset_tabs();
376 }
377 if rows != self.grid.sy() {
378 self.grid
379 .resize_height(rows, &mut self.cursor_y, COLOUR_DEFAULT);
380 }
381 self.rupper = 0;
382 self.rlower = rows.saturating_sub(1);
383 self.cursor_x = self.cursor_x.min(self.max_cursor_x());
384 self.pending_wrap = self.pending_wrap
385 && (self.mode & mode::MODE_WRAP) != 0
386 && self.cursor_x == self.max_cursor_x();
387 }
388
389 pub fn clear_history_and_hyperlinks(&mut self, reset_hyperlinks: bool) {
391 self.clear_selected_cells();
392 self.grid.clear_history();
393 if reset_hyperlinks {
394 self.hyperlinks.reset();
395 }
396 }
397
398 fn reset_tabs(&mut self) {
399 self.tabs = vec![false; self.grid.sx() as usize];
400 for column in (8..self.grid.sx()).step_by(8) {
401 self.tabs[column as usize] = true;
402 }
403 }
404
405 fn max_cursor_x(&self) -> u32 {
406 self.grid.sx().saturating_sub(1)
407 }
408
409 fn cursor_column(&self) -> u32 {
410 self.cursor_x.min(self.max_cursor_x())
411 }
412
413 fn logical_insert_column(&self) -> u32 {
414 let x = self.cursor_column();
415 let Some(line) = self.grid.visible_line(self.cursor_y) else {
416 return x;
417 };
418 let Some(owner_x) = line.owning_cell_x(x).filter(|owner_x| *owner_x != x) else {
419 return x;
420 };
421 let width = line
422 .cell(owner_x)
423 .map_or(1, |cell| u32::from(cell.width()).max(1));
424 owner_x.saturating_add(width)
425 }
426
427 fn current_line_mut(&mut self) -> Option<&mut GridLine> {
428 self.grid.visible_line_mut(self.cursor_y)
429 }
430
431 fn clear_pending_wrap(&mut self) {
432 self.pending_wrap = false;
433 }
434
435 fn restore_cursor_position(&mut self, x: u32, y: u32, pending_wrap: bool) {
436 self.cursor_x = x.min(self.max_cursor_x());
437 self.cursor_y = y.min(self.grid.sy().saturating_sub(1));
438 self.pending_wrap = pending_wrap
439 && (self.mode & mode::MODE_WRAP) != 0
440 && self.cursor_x == self.max_cursor_x();
441 }
442
443 fn apply_grid_cursor(&mut self, cursor: GridPhysicalCursor) {
444 let history_size = self.grid.hsize();
445 self.cursor_x = cursor.x.min(self.max_cursor_x());
446 self.cursor_y = cursor
447 .absolute_y
448 .saturating_sub(history_size)
449 .min(self.grid.sy().saturating_sub(1) as usize) as u32;
450 self.pending_wrap = cursor.pending_wrap
451 && (self.mode & mode::MODE_WRAP) != 0
452 && self.cursor_x == self.max_cursor_x();
453 }
454
455 fn apply_pending_wrap(&mut self) {
456 if !self.pending_wrap || (self.mode & mode::MODE_WRAP) == 0 {
457 self.pending_wrap = false;
458 return;
459 }
460
461 if let Some(line) = self.current_line_mut() {
462 line.set_wrapped(true);
463 }
464 self.pending_wrap = false;
465 self.linefeed(false, COLOUR_DEFAULT);
466 self.cursor_x = 0;
467 }
468
469 fn blank_cell(&self, bg: i32) -> GridCell {
470 GridCell::blank_with_bg(bg)
471 }
472
473 fn repair_wide_cells_on_line(line: &mut GridLine, sx: u32, bg: i32) {
474 let blank = GridCell::blank_with_bg(bg);
475 let mut changed = false;
476 let mut x = 0;
477
478 while x < sx {
479 let Some(cell) = line.cell(x) else {
480 x += 1;
481 continue;
482 };
483
484 if cell.is_padding() {
485 if line.owning_cell_x(x).is_none() {
486 if let Some(target) = line.cell_mut(x) {
487 *target = blank.clone();
488 changed = true;
489 }
490 }
491 x += 1;
492 continue;
493 }
494
495 let width = u32::from(cell.width());
496 if width <= 1 {
497 x += 1;
498 continue;
499 }
500
501 let mut valid = x.saturating_add(width) <= sx;
502 if valid {
503 for offset in 1..width {
504 let column = x + offset;
505 let valid_padding = line
506 .cell(column)
507 .is_some_and(|candidate| candidate.is_padding())
508 && line.owning_cell_x(column) == Some(x);
509 if !valid_padding {
510 valid = false;
511 break;
512 }
513 }
514 }
515
516 if valid {
517 x += width;
518 continue;
519 }
520
521 if let Some(target) = line.cell_mut(x) {
522 *target = blank.clone();
523 changed = true;
524 }
525 x += 1;
526 }
527
528 if changed {
529 line.touch();
530 }
531 }
532
533 fn overwrite_for_write(&mut self, x: u32, width: u32) {
534 let sx = self.grid.sx();
535 let blank = GridCell::blank_with_bg(COLOUR_DEFAULT);
536 let Some(line) = self.current_line_mut() else {
537 return;
538 };
539
540 let current_is_padding = line.is_padding_cell(x);
541 if current_is_padding {
542 if let Some(owner_x) = line.owning_cell_x(x).filter(|owner_x| *owner_x != x) {
543 if let Some(owner) = line.cell_mut(owner_x) {
544 *owner = blank.clone();
545 }
546 }
547 }
548
549 let clear_following_padding = width != 1
550 || line
551 .cell(x)
552 .is_some_and(|cell| cell.width() != 1 || cell.is_padding());
553 if clear_following_padding {
554 let mut clear_x = x.saturating_add(width);
555 while clear_x < sx && line.is_padding_cell(clear_x) {
556 if let Some(cell) = line.cell_mut(clear_x) {
557 *cell = blank.clone();
558 }
559 clear_x += 1;
560 }
561 }
562
563 line.touch();
564 }
565
566 fn clear_line_range(&mut self, y: u32, start: u32, end_inclusive: u32, bg: i32) {
567 self.clear_selected_cells();
568 let sx = self.grid.sx();
569 let end = end_inclusive.min(sx.saturating_sub(1));
570 let Some(line) = self.grid.visible_line_mut(y) else {
571 return;
572 };
573 for x in start.min(sx)..=end {
574 if let Some(cell) = line.cell_mut(x) {
575 *cell = GridCell::blank_with_bg(bg);
576 }
577 }
578 Self::repair_wide_cells_on_line(line, sx, bg);
579 line.set_wrapped(false);
580 line.touch();
581 }
582
583 fn clear_screen_region(&mut self, start_y: u32, end_y_inclusive: u32, bg: i32) {
584 self.clear_selected_cells();
585 for y in start_y..=end_y_inclusive.min(self.grid.sy().saturating_sub(1)) {
586 if let Some(line) = self.grid.visible_line_mut(y) {
587 line.clear(bg);
588 }
589 }
590 }
591
592 fn write_char(&mut self, ch: char, cell: &CellState, acs: bool) {
593 if self.grid.sx() == 0 || self.grid.sy() == 0 {
594 return;
595 }
596 self.clear_selected_cells();
597
598 let ch = if acs { acs::translate_acs(ch) } else { ch };
599 let requested_width = u32::from(self.utf8_config.width(ch));
600 if self.combine_char(ch) {
601 return;
602 }
603 let width = requested_width.clamp(1, self.grid.sx());
607
608 let wrap_enabled = (self.mode & mode::MODE_WRAP) != 0;
609 let mut automatic_wrap_continuation = self.pending_wrap && wrap_enabled;
610 self.apply_pending_wrap();
611
612 if (self.mode & mode::MODE_INSERT) != 0 {
613 let insert_x = self.logical_insert_column();
614 if insert_x >= self.grid.sx() {
615 if !wrap_enabled {
616 return;
617 }
618 if let Some(line) = self.current_line_mut() {
619 line.set_wrapped(true);
620 }
621 self.linefeed(false, COLOUR_DEFAULT);
622 self.cursor_x = 0;
623 automatic_wrap_continuation = true;
624 } else {
625 self.cursor_x = insert_x;
626 }
627 }
628
629 let write_would_cross_right_edge =
630 width > self.grid.sx() || self.cursor_x > self.grid.sx().saturating_sub(width);
631 if !wrap_enabled && width > 1 && write_would_cross_right_edge {
632 return;
633 }
634
635 if (self.mode & mode::MODE_INSERT) != 0 {
638 <Self as ScreenWriter>::insert_character(self, width, cell.bg());
639 }
640
641 if wrap_enabled && write_would_cross_right_edge {
642 let gap_start = self.cursor_column();
643 if let Some(line) = self.current_line_mut() {
644 line.mark_unused_suffix_as_reflow_gap(gap_start);
645 line.set_wrapped(true);
646 }
647 self.linefeed(false, COLOUR_DEFAULT);
648 self.cursor_x = 0;
649 automatic_wrap_continuation = true;
650 }
651
652 if self.cursor_y >= self.grid.sy()
653 || self.cursor_column() > self.grid.sx().saturating_sub(width)
654 {
655 return;
656 }
657
658 let x = self.cursor_column();
659 if x == 0 && !automatic_wrap_continuation {
660 self.break_previous_wrapped_line();
661 }
662 self.overwrite_for_write(x, width);
663 if let Some(line) = self.current_line_mut() {
664 if let Some(target) = line.cell_mut(x) {
665 *target = GridCell::from_state(
666 ch,
667 u8::try_from(width).unwrap_or(1),
668 cell,
669 GridCellFlags::default(),
670 );
671 }
672 for offset in 1..width {
673 if let Some(padding) = line.cell_mut(x + offset) {
674 *padding = GridCell::from_state(' ', 0, cell, GridCellFlags::PADDING);
675 }
676 }
677 line.touch();
678 }
679
680 if wrap_enabled && x + width >= self.grid.sx() {
681 self.cursor_x = self.max_cursor_x();
682 self.pending_wrap = true;
683 } else {
684 self.cursor_x = x.saturating_add(width).min(self.max_cursor_x());
685 self.pending_wrap = false;
686 }
687 }
688
689 fn write_plain_ascii_run(&mut self, mut bytes: &[u8], cell: &CellState, acs: bool) -> bool {
690 if bytes.is_empty() {
691 return true;
692 }
693 if acs
694 || (self.mode & mode::MODE_INSERT) != 0
695 || cell.attr() != 0
696 || cell.fg() != COLOUR_DEFAULT
697 || cell.bg() != COLOUR_DEFAULT
698 || cell.us() != COLOUR_DEFAULT
699 || cell.link() != 0
700 || self.grid.sx() == 0
701 || self.grid.sy() == 0
702 {
703 return false;
704 }
705 self.clear_selected_cells();
706
707 while !bytes.is_empty() {
708 let automatic_wrap_continuation =
709 self.pending_wrap && (self.mode & mode::MODE_WRAP) != 0;
710 self.apply_pending_wrap();
711 if self.cursor_y >= self.grid.sy() {
712 self.write_ascii_run_slow(bytes, cell, acs);
713 return true;
714 }
715
716 let sx = self.grid.sx();
717 let x = self.cursor_column();
718 if x == 0 && !automatic_wrap_continuation {
719 self.break_previous_wrapped_line();
720 }
721
722 if (self.mode & mode::MODE_WRAP) == 0 {
723 let available = sx.saturating_sub(x) as usize;
724 if bytes.len() > available {
725 self.write_ascii_run_slow(bytes, cell, acs);
726 return true;
727 }
728 }
729
730 let writable = sx.saturating_sub(x) as usize;
731 if writable == 0 {
732 self.write_ascii_run_slow(bytes, cell, acs);
733 return true;
734 }
735 let chunk_len = bytes.len().min(writable);
736 let (chunk, rest) = bytes.split_at(chunk_len);
737 let wrote_chunk = self
738 .current_line_mut()
739 .is_some_and(|line| line.write_plain_ascii_run(x, chunk));
740 if !wrote_chunk {
741 self.write_ascii_run_slow(bytes, cell, acs);
745 return true;
746 }
747
748 if (self.mode & mode::MODE_WRAP) != 0 && x + chunk_len as u32 >= sx {
749 self.cursor_x = self.max_cursor_x();
750 self.pending_wrap = true;
751 } else {
752 self.cursor_x = x.saturating_add(chunk_len as u32).min(self.max_cursor_x());
753 self.pending_wrap = false;
754 }
755 bytes = rest;
756 }
757 true
758 }
759
760 fn write_ascii_run_slow(&mut self, bytes: &[u8], cell: &CellState, acs: bool) {
761 for &byte in bytes {
762 self.write_char(char::from(byte), cell, acs);
763 }
764 }
765
766 fn break_previous_wrapped_line(&mut self) {
767 if self.cursor_y == 0 {
768 return;
769 }
770 if let Some(previous) = self.grid.visible_line_mut(self.cursor_y - 1) {
771 previous.set_wrapped(false);
772 }
773 }
774
775 fn combine_char(&mut self, ch: char) -> bool {
776 let mut x = self.cursor_column();
777 if self.pending_wrap {
778 x = self.max_cursor_x();
779 } else if x == 0 {
780 return matches!(
781 utf8_combine_char(None, ch, &self.utf8_config),
782 CombineResult::Discard
783 );
784 } else {
785 x -= 1;
786 }
787
788 let Some((target_x, previous)) = self.grid.visible_line(self.cursor_y).map(|line| {
789 let target_x = line.owning_cell_x(x).unwrap_or(x);
790 let previous = line
791 .cell(target_x)
792 .map(|cell| (cell.text().to_owned(), cell.width()));
793 (target_x, previous)
794 }) else {
795 return matches!(
796 utf8_combine_char(None, ch, &self.utf8_config),
797 CombineResult::Discard
798 );
799 };
800 let result = utf8_combine_char(
801 previous
802 .as_ref()
803 .map(|(text, width)| (text.as_str(), *width)),
804 ch,
805 &self.utf8_config,
806 );
807
808 match result {
809 CombineResult::Standalone { .. } => false,
810 CombineResult::Discard => true,
811 CombineResult::Combined { text, width } => {
812 let previous_width = previous.as_ref().map_or(0, |(_, width)| *width);
813 let available_width = self.grid.sx().saturating_sub(target_x).max(1);
814 let width = width.min(u8::try_from(available_width).unwrap_or(u8::MAX));
815 if width != previous_width {
816 self.overwrite_for_write(target_x, u32::from(width));
820 }
821 let Some(line) = self.grid.visible_line_mut(self.cursor_y) else {
822 return true;
823 };
824 if let Some(cell) = line.cell_mut(target_x) {
825 cell.set_text(text);
826 cell.set_width(width);
827 if width == 2 {
828 let mut padding = cell.clone();
829 padding.set_text(" ".to_owned());
830 padding.set_width(0);
831 padding.set_flags(GridCellFlags::PADDING);
832 if let Some(padding_cell) = line.cell_mut(target_x + 1) {
833 *padding_cell = padding;
834 }
835 }
836 line.touch();
837 }
838 if previous_width == 1 && width == 2 && !self.pending_wrap {
839 let next_cursor = target_x.saturating_add(2);
840 if next_cursor >= self.grid.sx() {
841 self.cursor_x = self.max_cursor_x();
842 self.pending_wrap = (self.mode & mode::MODE_WRAP) != 0;
843 } else {
844 self.cursor_x = next_cursor;
845 }
846 }
847 true
848 }
849 }
850 }
851
852 fn parse_hyperlink(data: &str) -> (Option<String>, String) {
853 let (params, uri) = data.split_once(';').unwrap_or((data, ""));
854 let mut internal_id = None;
855 for part in params.split(':') {
856 if let Some(value) = part.strip_prefix("id=") {
857 internal_id = Some(value.to_owned());
858 }
859 }
860 (internal_id, uri.to_owned())
861 }
862}
863
864#[cfg(test)]
865#[path = "screen/tests.rs"]
866mod tests;