Skip to main content

rmux_core/screen/
writer.rs

1use crate::grid::{Grid, GridCell, GridCellFlags, GridLineFlags};
2use crate::input::mode;
3use crate::input::{CellState, InputEndType, ScreenWriter, COLOUR_DEFAULT};
4use crate::{TerminalPaletteIndex, TerminalPassthrough};
5
6use super::{SavedGrid, Screen, TITLE_STACK_MAX};
7
8fn cursor_backward_tab_steps(n: u32, current: u32) -> u32 {
9    n.max(1).min(current.saturating_add(1))
10}
11
12impl ScreenWriter for Screen {
13    fn collect_add(&mut self, ch: char, cell: &CellState) {
14        self.write_char(ch, cell, false);
15    }
16
17    fn collect_add_with_charset(&mut self, ch: char, cell: &CellState, acs: bool) {
18        self.write_char(ch, cell, acs);
19    }
20
21    fn collect_add_ascii_run(&mut self, bytes: &[u8], cell: &CellState, acs: bool) {
22        if !self.write_plain_ascii_run(bytes, cell, acs) {
23            for &byte in bytes {
24                self.write_char(char::from(byte), cell, acs);
25            }
26        }
27    }
28
29    fn cursor_up(&mut self, n: u32) {
30        self.clear_pending_wrap();
31        // Positions above the scroll region keep the screen edge; all others
32        // stop at the region's top margin.
33        let minimum_y = if self.cursor_y >= self.rupper {
34            self.rupper
35        } else {
36            0
37        };
38        self.cursor_y = self.cursor_y.saturating_sub(n).max(minimum_y);
39    }
40
41    fn cursor_down(&mut self, n: u32) {
42        self.clear_pending_wrap();
43        // Positions below the scroll region keep the screen edge; all others
44        // stop at the region's bottom margin.
45        let maximum_y = if self.cursor_y <= self.rlower {
46            self.rlower
47        } else {
48            self.grid.sy().saturating_sub(1)
49        };
50        self.cursor_y = self.cursor_y.saturating_add(n).min(maximum_y);
51    }
52
53    fn cursor_left(&mut self, n: u32) {
54        self.clear_pending_wrap();
55        self.cursor_x = self.cursor_column().saturating_sub(n);
56    }
57
58    fn cursor_right(&mut self, n: u32) {
59        self.clear_pending_wrap();
60        self.cursor_x = self
61            .cursor_column()
62            .saturating_add(n)
63            .min(self.max_cursor_x());
64    }
65
66    fn cursor_move(&mut self, col: i32, row: i32, origin_mode: bool) {
67        self.clear_pending_wrap();
68        let max_x = self.grid.sx().saturating_sub(1);
69        let (min_y, max_y) = if origin_mode && (self.mode & mode::MODE_ORIGIN) != 0 {
70            (self.rupper, self.rlower)
71        } else {
72            (0, self.grid.sy().saturating_sub(1))
73        };
74
75        if col >= 0 {
76            self.cursor_x = (col as u32).min(max_x);
77        }
78        if row >= 0 {
79            self.cursor_y = min_y.saturating_add(row as u32).min(max_y);
80        }
81    }
82
83    fn insert_line(&mut self, n: u32, bg: i32) {
84        self.clear_pending_wrap();
85        if self.cursor_y < self.rupper || self.cursor_y > self.rlower {
86            return;
87        }
88        self.clear_selected_cells();
89
90        let upper = self.cursor_y;
91        let lower = self.rlower;
92        let lines = n.max(1).min(lower.saturating_sub(upper).saturating_add(1));
93        self.grid.insert_lines(upper, lower, lines, bg);
94    }
95
96    fn delete_line(&mut self, n: u32, bg: i32) {
97        self.clear_pending_wrap();
98        if self.cursor_y < self.rupper || self.cursor_y > self.rlower {
99            return;
100        }
101        self.clear_selected_cells();
102
103        let upper = self.cursor_y;
104        let lower = self.rlower;
105        let lines = n.max(1).min(lower.saturating_sub(upper).saturating_add(1));
106        for _ in 0..lines {
107            self.grid.scroll_region_up(upper, lower, bg, false);
108        }
109    }
110
111    fn scroll_up(&mut self, n: u32, bg: i32) {
112        self.clear_pending_wrap();
113        self.clear_selected_cells();
114        let lines = n
115            .max(1)
116            .min(self.rlower.saturating_sub(self.rupper).saturating_add(1));
117        for _ in 0..lines {
118            self.grid
119                .scroll_region_up(self.rupper, self.rlower, bg, self.rupper == 0);
120        }
121    }
122
123    fn scroll_down(&mut self, n: u32, bg: i32) {
124        self.clear_pending_wrap();
125        self.clear_selected_cells();
126        let lines = n
127            .max(1)
128            .min(self.rlower.saturating_sub(self.rupper).saturating_add(1));
129        for _ in 0..lines {
130            self.grid.scroll_region_down(self.rupper, self.rlower, bg);
131        }
132    }
133
134    fn linefeed(&mut self, wrapped: bool, bg: i32) {
135        self.pending_wrap = false;
136        if wrapped {
137            if let Some(line) = self.current_line_mut() {
138                line.set_wrapped(true);
139            }
140        }
141
142        if self.cursor_y == self.rlower {
143            self.clear_selected_cells();
144            self.grid
145                .scroll_region_up(self.rupper, self.rlower, bg, self.rupper == 0);
146        } else if self.cursor_y < self.grid.sy().saturating_sub(1) {
147            self.cursor_y += 1;
148        }
149    }
150
151    fn reverse_index(&mut self, bg: i32) {
152        self.clear_pending_wrap();
153        if self.cursor_y == self.rupper {
154            self.clear_selected_cells();
155            self.grid.scroll_region_down(self.rupper, self.rlower, bg);
156        } else if self.cursor_y > 0 {
157            self.cursor_y -= 1;
158        }
159    }
160
161    fn carriage_return(&mut self) {
162        self.pending_wrap = false;
163        self.cursor_x = 0;
164    }
165
166    fn backspace(&mut self) {
167        self.clear_pending_wrap();
168        let cx = self.cursor_column();
169        if cx > 0 {
170            self.cursor_x = cx - 1;
171            return;
172        }
173        if self.cursor_y == 0 {
174            return;
175        }
176        if self
177            .grid
178            .visible_line(self.cursor_y - 1)
179            .is_some_and(|line| line.flags().contains(GridLineFlags::WRAPPED))
180        {
181            self.cursor_y -= 1;
182            self.cursor_x = self.max_cursor_x();
183        }
184    }
185
186    fn insert_character(&mut self, n: u32, bg: i32) {
187        self.clear_pending_wrap();
188        self.clear_selected_cells();
189        let sx = self.grid.sx();
190        let x = self.logical_insert_column();
191        if x >= sx {
192            self.pending_wrap = (self.mode & crate::input::mode::MODE_WRAP) != 0;
193            return;
194        }
195        self.cursor_x = x;
196        let count = n.max(1).min(sx.saturating_sub(x));
197        let blank = self.blank_cell(bg);
198        if let Some(line) = self.current_line_mut() {
199            line.insert_cells(x, count, &blank);
200            Self::repair_wide_cells_on_line(line, sx, bg);
201            line.touch();
202        }
203    }
204
205    fn delete_character(&mut self, n: u32, bg: i32) {
206        self.clear_pending_wrap();
207        self.clear_selected_cells();
208        let x = self.cursor_column();
209        let sx = self.grid.sx();
210        let count = n.max(1).min(sx.saturating_sub(x));
211        let blank = self.blank_cell(bg);
212        let clears_whole_line = x == 0 && count == sx;
213        if clears_whole_line {
214            self.grid.break_wrap_before_visible_line(self.cursor_y);
215        }
216        if let Some(line) = self.current_line_mut() {
217            line.delete_cells(x, count, &blank);
218            Self::repair_wide_cells_on_line(line, sx, bg);
219            if clears_whole_line {
220                line.set_wrapped(false);
221            }
222            line.touch();
223        }
224    }
225
226    fn clear_character(&mut self, n: u32, bg: i32) {
227        self.clear_pending_wrap();
228        let x = self.cursor_column();
229        let end = x
230            .saturating_add(n.max(1))
231            .saturating_sub(1)
232            .min(self.grid.sx().saturating_sub(1));
233        self.clear_line_range(self.cursor_y, x, end, bg);
234    }
235
236    fn clear_end_of_screen(&mut self, bg: i32) {
237        if self.cursor_y == 0 && self.cursor_column() == 0 {
238            self.clear_selected_cells();
239            self.grid.clear_visible_to_history(COLOUR_DEFAULT);
240            return;
241        }
242        let x = self.cursor_column();
243        if self.cursor_y < self.grid.sy() {
244            self.clear_line_range(self.cursor_y, x, self.grid.sx().saturating_sub(1), bg);
245        }
246        if self.cursor_y + 1 < self.grid.sy() {
247            self.clear_screen_region(
248                self.cursor_y + 1,
249                self.grid.sy().saturating_sub(1),
250                COLOUR_DEFAULT,
251            );
252        }
253    }
254
255    fn clear_start_of_screen(&mut self, bg: i32) {
256        if self.cursor_y > 0 {
257            self.clear_screen_region(0, self.cursor_y - 1, COLOUR_DEFAULT);
258        }
259        self.clear_line_range(self.cursor_y, 0, self.cursor_column(), bg);
260    }
261
262    fn clear_screen(&mut self, _bg: i32) {
263        self.clear_selected_cells();
264        self.grid.clear_visible_to_history(COLOUR_DEFAULT);
265    }
266
267    fn clear_history(&mut self) {
268        self.clear_selected_cells();
269        self.grid.clear_history();
270    }
271
272    fn clear_end_of_line(&mut self, bg: i32) {
273        self.clear_line_range(
274            self.cursor_y,
275            self.cursor_column(),
276            self.grid.sx().saturating_sub(1),
277            bg,
278        );
279    }
280
281    fn clear_start_of_line(&mut self, bg: i32) {
282        self.clear_line_range(self.cursor_y, 0, self.cursor_column(), bg);
283    }
284
285    fn clear_line(&mut self, bg: i32) {
286        self.clear_line_range(self.cursor_y, 0, self.grid.sx().saturating_sub(1), bg);
287    }
288
289    fn mode_set(&mut self, mode_bits: u32) {
290        self.mode |= mode_bits;
291    }
292
293    fn mode_clear(&mut self, mode_bits: u32) {
294        self.mode &= !mode_bits;
295        if (self.mode & mode::MODE_WRAP) == 0 {
296            self.clear_pending_wrap();
297        }
298    }
299
300    fn set_scroll_region(&mut self, top: u32, bottom: u32) {
301        let max_y = self.grid.sy().saturating_sub(1);
302        let top = top.min(max_y);
303        let bottom = bottom.min(max_y);
304        if top >= bottom {
305            return;
306        }
307        self.pending_wrap = false;
308        self.cursor_x = 0;
309        self.cursor_y = 0;
310        self.rupper = top;
311        self.rlower = bottom;
312    }
313
314    fn alternate_on(&mut self, bg: i32, save_cursor: bool) {
315        if !self.alternate_screen_enabled {
316            return;
317        }
318        if self.is_alternate() {
319            return;
320        }
321        self.clear_selected_cells();
322
323        let mut saved_grid = Grid::new(self.grid.size(), 0);
324        saved_grid.replace_visible(self.grid.visible_lines());
325        self.saved_grid = Some(SavedGrid {
326            grid: saved_grid,
327            history_enabled: self.grid.history_enabled(),
328        });
329        if save_cursor {
330            self.saved_cursor_x = Some(self.cursor_x);
331            self.saved_cursor_y = Some(self.cursor_y);
332            self.saved_cursor_pending_wrap = self.pending_wrap;
333            self.saved_state.cx = self.cursor_column();
334            self.saved_state.cy = self.cursor_y;
335        }
336
337        self.grid.clear_visible(bg);
338        self.grid.set_history_enabled(false);
339        self.pending_wrap = false;
340        if !save_cursor || !self.preserve_alternate_screen_cursor {
341            self.cursor_x = 0;
342            self.cursor_y = 0;
343        }
344    }
345
346    fn alternate_off(&mut self, _bg: i32, restore_cursor: bool) {
347        self.clear_selected_cells();
348        let saved_cursor = if restore_cursor {
349            self.saved_cursor_x
350                .zip(self.saved_cursor_y)
351                .map(|(x, y)| (x, y, self.saved_cursor_pending_wrap))
352        } else {
353            None
354        };
355
356        let Some(saved) = self.saved_grid.take() else {
357            if let Some((x, y, pending_wrap)) = saved_cursor {
358                self.restore_cursor_position(x, y, pending_wrap);
359            }
360            return;
361        };
362
363        let current_size = self.grid.size();
364        let saved_size = saved.grid.size();
365        let alternate_cursor = (self.cursor_x, self.cursor_y, self.pending_wrap);
366        self.grid
367            .restore_visible_at_size(saved_size, saved.grid.visible_lines(), COLOUR_DEFAULT);
368        self.grid.set_history_enabled(saved.history_enabled);
369        if let Some((x, y, pending_wrap)) = saved_cursor {
370            self.restore_cursor_position(x, y, pending_wrap);
371        } else {
372            self.restore_cursor_position(
373                alternate_cursor.0,
374                alternate_cursor.1,
375                alternate_cursor.2,
376            );
377        }
378        self.resize(current_size);
379    }
380
381    fn tab(&mut self) {
382        self.clear_pending_wrap();
383        let start = self.cursor_column();
384        let next = ((start + 1) as usize..self.tabs.len())
385            .find(|index| self.tabs[*index])
386            .map(|index| index as u32)
387            .unwrap_or_else(|| self.grid.sx().saturating_sub(1));
388        self.cursor_x = next;
389    }
390
391    fn cursor_backward_tab(&mut self, n: u32) {
392        self.clear_pending_wrap();
393        let mut current = self.cursor_column();
394        for _ in 0..cursor_backward_tab_steps(n, current) {
395            if current == 0 {
396                break;
397            }
398            let previous = (0..current as usize)
399                .rev()
400                .find(|index| self.tabs[*index])
401                .map(|index| index as u32)
402                .unwrap_or(0);
403            current = previous;
404        }
405        self.cursor_x = current;
406    }
407
408    fn set_tab_stop(&mut self) {
409        let column = self.cursor_column() as usize;
410        if let Some(tab) = self.tabs.get_mut(column) {
411            *tab = true;
412        }
413    }
414
415    fn clear_tab_stop(&mut self) {
416        let column = self.cursor_column() as usize;
417        if let Some(tab) = self.tabs.get_mut(column) {
418            *tab = false;
419        }
420    }
421
422    fn clear_all_tab_stops(&mut self) {
423        self.tabs.fill(false);
424    }
425
426    fn set_title(&mut self, title: &str) {
427        if self.title_rename_enabled {
428            Screen::set_title(self, title);
429        }
430    }
431
432    fn set_window_name(&mut self, name: &str) {
433        if self.title_rename_enabled && self.window_name != name {
434            self.window_name = name.to_owned();
435            self.bump_metadata_revision();
436        }
437    }
438
439    fn set_path(&mut self, path: &str) {
440        if self.path != path {
441            self.path = path.to_owned();
442            self.bump_metadata_revision();
443        }
444    }
445
446    fn save_cursor(&mut self) {
447        self.saved_cursor_x = Some(self.cursor_x);
448        self.saved_cursor_y = Some(self.cursor_y);
449        self.saved_cursor_pending_wrap = self.pending_wrap;
450    }
451
452    fn restore_cursor(&mut self) {
453        if let (Some(x), Some(y)) = (self.saved_cursor_x, self.saved_cursor_y) {
454            self.restore_cursor_position(x, y, self.saved_cursor_pending_wrap);
455        }
456    }
457
458    fn alignment_test(&mut self) {
459        self.clear_selected_cells();
460        self.rupper = 0;
461        self.rlower = self.grid.sy().saturating_sub(1);
462        let sx = self.grid.sx();
463        for y in 0..self.grid.sy() {
464            if let Some(line) = self.grid.visible_line_mut(y) {
465                for x in 0..sx {
466                    if let Some(cell) = line.cell_mut(x) {
467                        *cell = GridCell::from_state(
468                            'E',
469                            1,
470                            &CellState::default(),
471                            GridCellFlags::default(),
472                        );
473                    }
474                }
475                line.set_wrapped(false);
476                line.touch();
477            }
478        }
479    }
480
481    fn full_reset(&mut self) {
482        self.clear_selected_cells();
483        if self.is_alternate() {
484            self.alternate_off(COLOUR_DEFAULT, false);
485        }
486        self.cursor_x = 0;
487        self.cursor_y = 0;
488        self.pending_wrap = false;
489        self.rupper = 0;
490        self.rlower = self.grid.sy().saturating_sub(1);
491        self.mode = mode::MODE_CURSOR | mode::MODE_WRAP | (self.mode & mode::MODE_CRLF);
492        self.grid.clear_visible(COLOUR_DEFAULT);
493        self.reset_tabs();
494        self.title_stack.clear();
495        self.active_hyperlink = 0;
496        self.hyperlinks.reset();
497        self.bump_metadata_revision();
498    }
499
500    fn start_sync(&mut self) {
501        self.mode |= mode::MODE_SYNC;
502    }
503
504    fn stop_sync(&mut self) {
505        self.mode &= !mode::MODE_SYNC;
506    }
507
508    fn set_cursor_style(&mut self, n: u32) {
509        self.cursor_style = n;
510    }
511
512    fn osc_hyperlink(&mut self, data: &str) {
513        let (internal_id, uri) = Self::parse_hyperlink(data);
514        if uri.is_empty() {
515            self.active_hyperlink = 0;
516            self.bump_metadata_revision();
517            return;
518        }
519        self.active_hyperlink = self.hyperlinks.put(&uri, internal_id.as_deref());
520        self.bump_metadata_revision();
521    }
522
523    fn current_hyperlink_id(&self) -> u32 {
524        self.active_hyperlink
525    }
526
527    fn bell(&mut self) {
528        self.bell_count = self.bell_count.saturating_add(1);
529    }
530
531    fn apc_passthrough(&mut self, data: &[u8]) {
532        self.push_terminal_passthrough(TerminalPassthrough::kitty_graphics(
533            self.cursor_x,
534            self.cursor_y,
535            data.to_vec(),
536        ));
537    }
538
539    fn dcs_passthrough(&mut self, data: &[u8]) {
540        self.push_terminal_passthrough(TerminalPassthrough::raw(
541            self.cursor_x,
542            self.cursor_y,
543            data.to_vec(),
544        ));
545    }
546
547    fn sixel_passthrough(&mut self, data: &[u8]) {
548        self.push_terminal_passthrough(TerminalPassthrough::sixel(
549            self.cursor_x,
550            self.cursor_y,
551            data.to_vec(),
552        ));
553    }
554
555    fn screen_size_x(&self) -> u32 {
556        self.grid.sx()
557    }
558
559    fn screen_size_y(&self) -> u32 {
560        self.grid.sy()
561    }
562
563    fn cursor_x(&self) -> u32 {
564        self.cursor_x
565    }
566
567    fn cursor_y(&self) -> u32 {
568        self.cursor_y
569    }
570
571    fn current_mode(&self) -> u32 {
572        self.mode
573    }
574
575    fn push_title(&mut self) {
576        if !self.title_rename_enabled {
577            return;
578        }
579        if self.title_stack.len() >= TITLE_STACK_MAX {
580            let excess = self.title_stack.len() + 1 - TITLE_STACK_MAX;
581            self.title_stack.drain(0..excess);
582        }
583        self.title_stack.push(self.title.clone());
584        self.bump_metadata_revision();
585    }
586
587    fn pop_title(&mut self) {
588        if !self.title_rename_enabled {
589            return;
590        }
591        if let Some(title) = self.title_stack.pop() {
592            self.title = title;
593            self.bump_metadata_revision();
594        }
595    }
596
597    fn osc_palette(&mut self, data: &str, _end: InputEndType) {
598        // OSC 4 is a list of index/value pairs. tmux 3.7b forwards each valid
599        // query as its own canonical ST-terminated OSC while handling palette
600        // sets internally. RMUX does not maintain an outer-terminal palette,
601        // so keep set behaviour unchanged and relay only strict, bounded
602        // queries. The dedicated event kind bypasses generic raw-passthrough
603        // policy without reflecting arbitrary OSC payloads.
604        let mut fields = data.split(';');
605        while let Some(index) = fields.next() {
606            let Some(value) = fields.next() else {
607                break;
608            };
609            if value != "?" {
610                continue;
611            }
612            let Some(index) = TerminalPaletteIndex::parse(index) else {
613                continue;
614            };
615            self.push_terminal_passthrough(TerminalPassthrough::palette_query(index));
616        }
617    }
618    fn osc_notification(&mut self, _data: &str) {}
619    fn osc_fg_colour(&mut self, _data: &str, _end: InputEndType) {}
620    fn osc_bg_colour(&mut self, _data: &str, _end: InputEndType) {}
621    fn osc_cursor_colour(&mut self, _data: &str, _end: InputEndType) {}
622    fn osc_clipboard(&mut self, data: &str, end: InputEndType) {
623        let mut sequence = Vec::with_capacity(data.len() + 7);
624        sequence.extend_from_slice(b"\x1b]52;");
625        sequence.extend_from_slice(data.as_bytes());
626        match end {
627            InputEndType::Bel => sequence.push(b'\x07'),
628            InputEndType::St => sequence.extend_from_slice(b"\x1b\\"),
629        }
630        if let Some((selection, payload)) = data.split_once(';') {
631            if payload == "?" {
632                self.push_terminal_passthrough(TerminalPassthrough::clipboard_query(
633                    crate::TerminalClipboardQuery::new(selection, end),
634                    sequence,
635                ));
636                return;
637            }
638        }
639        self.push_terminal_passthrough(TerminalPassthrough::clipboard(sequence));
640    }
641    fn osc_reset_palette(&mut self, _data: &str) {}
642    fn osc_reset_fg(&mut self) {}
643    fn osc_reset_bg(&mut self) {}
644    fn osc_reset_cursor(&mut self) {}
645    fn osc_shell_integration(&mut self, _data: &str) {}
646}
647
648#[cfg(test)]
649mod tests {
650    #[test]
651    fn cursor_backward_tab_steps_are_bounded_by_cursor_column() {
652        assert_eq!(super::cursor_backward_tab_steps(0, 0), 1);
653        assert_eq!(super::cursor_backward_tab_steps(1, 7), 1);
654        assert_eq!(super::cursor_backward_tab_steps(u32::MAX, 7), 8);
655    }
656}