Skip to main content

rmux_core/
screen.rs

1//! Screen state and `ScreenWriter` implementation backed by [`Grid`].
2
3use 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/// One pane screen, including scrollback, alternate-screen state, cursor
37/// position, modes, tab stops, and hyperlink storage.
38#[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    /// Creates a new screen with the given geometry and history limit.
72    #[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    /// Returns the current terminal mode flags.
110    #[must_use]
111    pub const fn mode(&self) -> u32 {
112        self.mode
113    }
114
115    /// Returns the most recent DECSCUSR cursor style parameter.
116    #[must_use]
117    pub const fn cursor_style(&self) -> u32 {
118        self.cursor_style
119    }
120
121    /// Returns the DECSTBM scroll region as `(top, bottom)` rows, 0-based and
122    /// inclusive. A full-screen region is `(0, rows - 1)`.
123    #[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    /// Returns the screen size.
138    #[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    /// Returns the current screen title.
150    #[must_use]
151    pub fn title(&self) -> &str {
152        &self.title
153    }
154
155    /// Sets the current screen title.
156    pub fn set_title(&mut self, title: impl Into<String>) {
157        self.title = title.into();
158    }
159
160    /// Enables or disables title changes requested by pane output.
161    pub fn set_title_rename_enabled(&mut self, enabled: bool) {
162        self.title_rename_enabled = enabled;
163    }
164
165    /// Enables or disables DEC alternate-screen entry for this screen.
166    ///
167    /// Exit sequences are still honored by the writer so disabling the option
168    /// while a pane is already in alternate screen does not trap it there.
169    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    /// Returns the most recent OSC 7 path.
178    #[must_use]
179    pub fn path(&self) -> &str {
180        &self.path
181    }
182
183    /// Returns whether the alternate screen is active.
184    #[must_use]
185    pub fn is_alternate(&self) -> bool {
186        self.saved_grid.is_some()
187    }
188
189    /// Returns the configured history limit.
190    #[must_use]
191    pub fn history_limit(&self) -> usize {
192        self.grid.hlimit()
193    }
194
195    /// Returns the current history size in rows.
196    #[must_use]
197    pub fn history_size(&self) -> usize {
198        self.grid.hsize()
199    }
200
201    /// Returns the current cursor position within the visible viewport.
202    #[must_use]
203    pub const fn cursor_position(&self) -> (u32, u32) {
204        (self.cursor_x, self.cursor_y)
205    }
206
207    /// Returns the absolute cursor row including history.
208    #[must_use]
209    pub fn cursor_absolute_y(&self) -> usize {
210        self.grid.hsize() + self.cursor_y as usize
211    }
212
213    /// Returns the total number of absolute lines retained by the screen.
214    #[must_use]
215    pub fn absolute_line_count(&self) -> usize {
216        self.grid.hsize() + self.grid.sy() as usize
217    }
218
219    /// Deletes one visible line and scrolls the remaining viewport content up.
220    ///
221    /// This clears any pending wrap state because deleting a visible row
222    /// invalidates the previous cursor edge condition.
223    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    /// Deletes one absolute line from history or the visible viewport.
254    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    /// Trims all lines below the cursor and pulls history into the viewport.
273    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    /// Returns the current retained history size in bytes.
289    #[must_use]
290    pub fn history_bytes(&self) -> usize {
291        self.grid.history_byte_size()
292    }
293
294    /// Drains and returns the number of BEL notifications observed since the last drain.
295    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    /// Drains terminal passthrough events observed since the last drain.
302    pub fn take_terminal_passthrough(&mut self) -> Vec<TerminalPassthrough> {
303        std::mem::take(&mut self.terminal_passthrough)
304    }
305
306    /// Drains the count of terminal passthrough events dropped by safety limits.
307    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    /// Returns the stored OSC 8 URI for a hyperlink inner ID.
336    #[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    /// Updates the history limit.
344    pub fn set_history_limit(&mut self, limit: usize) {
345        self.grid.set_hlimit(limit);
346    }
347
348    /// Updates the tmux-style UTF-8 width and combining configuration.
349    pub fn set_utf8_config(&mut self, utf8_config: Utf8Config) {
350        self.utf8_config = utf8_config;
351    }
352
353    /// Resizes the screen and resets the scroll region.
354    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    /// Clears history and optionally resets stored hyperlinks.
390    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        // A cell wider than the entire viewport cannot be represented with
604        // its normal padding. Preserve the glyph as a one-column cell rather
605        // than creating an out-of-bounds wide owner.
606        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        // tmux shifts the current row before an auto-wrapped wide glyph moves
636        // to the next row; the no-wrap rejection above must leave it untouched.
637        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                // Earlier chunks in this run may already have changed prior
742                // rows. Consume only the untouched suffix through the general
743                // writer so the caller never replays bytes already written.
744                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                    // Width promotion may replace the owner of an adjacent
817                    // wide cell. Clear that owner's displaced padding before
818                    // installing the new owner/padding pair.
819                    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;