ratatui_toolkit/primitives/termtui/
screen.rs

1//! Terminal screen state manager
2
3use crate::primitives::termtui::attrs::{Attrs, Color};
4use crate::primitives::termtui::grid::{Grid, Pos};
5use crate::primitives::termtui::size::Size;
6use termwiz::escape::csi::{Cursor, Edit, EraseInDisplay, EraseInLine, Mode, Sgr};
7use termwiz::escape::{Action, ControlCode, Esc, EscCode, OperatingSystemCommand};
8use unicode_width::UnicodeWidthChar;
9
10// Terminal mode flags
11const MODE_CURSOR_VISIBLE: u8 = 1 << 0;
12const MODE_ALTERNATE_SCREEN: u8 = 1 << 1;
13#[allow(dead_code)]
14const MODE_APPLICATION_CURSOR: u8 = 1 << 2;
15#[allow(dead_code)]
16const MODE_BRACKETED_PASTE: u8 = 1 << 3;
17const MODE_AUTO_WRAP: u8 = 1 << 4;
18#[allow(dead_code)]
19const MODE_ORIGIN: u8 = 1 << 5;
20
21/// Terminal screen state
22#[derive(Clone)]
23pub struct Screen {
24    /// Primary grid
25    grid: Grid,
26    /// Alternate grid (for full-screen apps)
27    alternate_grid: Grid,
28    /// Current text attributes
29    attrs: Attrs,
30    /// Terminal modes
31    modes: u8,
32    /// Window title
33    title: String,
34    /// Icon name
35    icon_name: String,
36    /// Pending wrap (cursor at end of line)
37    pending_wrap: bool,
38}
39
40impl Screen {
41    /// Create a new screen
42    pub fn new(rows: usize, cols: usize, scrollback: usize) -> Self {
43        let size = Size::new(cols as u16, rows as u16);
44
45        Self {
46            grid: Grid::new(size, scrollback),
47            alternate_grid: Grid::new(size, 0), // No scrollback for alternate
48            attrs: Attrs::default(),
49            modes: MODE_CURSOR_VISIBLE | MODE_AUTO_WRAP,
50            title: String::new(),
51            icon_name: String::new(),
52            pending_wrap: false,
53        }
54    }
55
56    /// Get the current grid (primary or alternate)
57    fn grid(&self) -> &Grid {
58        if self.mode(MODE_ALTERNATE_SCREEN) {
59            &self.alternate_grid
60        } else {
61            &self.grid
62        }
63    }
64
65    /// Get mutable current grid
66    fn grid_mut(&mut self) -> &mut Grid {
67        if self.mode(MODE_ALTERNATE_SCREEN) {
68            &mut self.alternate_grid
69        } else {
70            &mut self.grid
71        }
72    }
73
74    /// Get the primary grid (for rendering)
75    pub fn primary_grid(&self) -> &Grid {
76        &self.grid
77    }
78
79    /// Get screen size
80    pub fn size(&self) -> Size {
81        self.grid().size()
82    }
83
84    /// Get cursor position
85    pub fn cursor_pos(&self) -> Pos {
86        self.grid().pos()
87    }
88
89    /// Check if cursor is visible
90    pub fn cursor_visible(&self) -> bool {
91        self.mode(MODE_CURSOR_VISIBLE)
92    }
93
94    /// Get window title
95    pub fn title(&self) -> &str {
96        &self.title
97    }
98
99    /// Get current scrollback offset
100    pub fn scrollback(&self) -> usize {
101        self.grid().scrollback()
102    }
103
104    /// Set scrollback offset
105    pub fn set_scrollback(&mut self, offset: usize) {
106        self.grid_mut().set_scrollback(offset);
107    }
108
109    /// Scroll screen up (for user interaction)
110    pub fn scroll_screen_up(&mut self, n: usize) {
111        let current = self.grid().scrollback();
112        self.grid_mut().set_scrollback(current + n);
113    }
114
115    /// Scroll screen down (for user interaction)
116    pub fn scroll_screen_down(&mut self, n: usize) {
117        let current = self.grid().scrollback();
118        self.grid_mut().set_scrollback(current.saturating_sub(n));
119    }
120
121    /// Get selected text
122    pub fn get_selected_text(&self, low_x: i32, low_y: i32, high_x: i32, high_y: i32) -> String {
123        self.grid().get_selected_text(low_x, low_y, high_x, high_y)
124    }
125
126    /// Check a mode flag
127    fn mode(&self, mode: u8) -> bool {
128        self.modes & mode != 0
129    }
130
131    /// Set a mode flag
132    #[allow(dead_code)]
133    fn set_mode(&mut self, mode: u8) {
134        self.modes |= mode;
135    }
136
137    /// Clear a mode flag
138    #[allow(dead_code)]
139    fn clear_mode(&mut self, mode: u8) {
140        self.modes &= !mode;
141    }
142
143    /// Resize the screen
144    pub fn resize(&mut self, rows: usize, cols: usize) {
145        let size = Size::new(cols as u16, rows as u16);
146        self.grid.resize(size);
147        self.alternate_grid.resize(size);
148    }
149
150    /// Handle a termwiz action
151    pub fn handle_action(&mut self, action: Action) {
152        match action {
153            Action::Print(c) => self.text(c),
154            Action::PrintString(s) => {
155                for c in s.chars() {
156                    self.text(c);
157                }
158            }
159            Action::Control(code) => self.handle_control(code),
160            Action::Esc(esc) => self.handle_esc(esc),
161            Action::CSI(csi) => self.handle_csi(csi),
162            Action::OperatingSystemCommand(osc) => self.handle_osc(*osc),
163            Action::DeviceControl(_) => {} // DCS sequences not implemented
164            Action::Sixel(_) => {}         // Sixel graphics not implemented
165            Action::XtGetTcap(_) => {}     // Terminal cap queries not implemented
166            Action::KittyImage(_) => {}    // Kitty images not implemented
167        }
168    }
169
170    /// Handle a printable character
171    fn text(&mut self, c: char) {
172        let char_width = c.width().unwrap_or(0);
173        if char_width == 0 {
174            // Combining character - append to previous cell
175            // (simplified: skip for now)
176            return;
177        }
178
179        let size = self.grid().size();
180
181        // Handle pending wrap
182        if self.pending_wrap {
183            self.pending_wrap = false;
184
185            // Mark current row as wrapped
186            if let Some(row) = self.grid_mut().current_row_mut() {
187                row.set_wrapped(true);
188            }
189
190            // Move to next line
191            let pos = self.grid().pos();
192            if pos.row + 1 >= size.rows {
193                self.grid_mut().scroll_up(1);
194            } else {
195                self.grid_mut().set_row(pos.row + 1);
196            }
197            self.grid_mut().set_col(0);
198        }
199
200        let pos = self.grid().pos();
201        let attrs = self.attrs; // Copy attrs to avoid borrow conflict
202
203        // Write character to current cell
204        if let Some(row) = self.grid_mut().drawing_row_mut(pos.row) {
205            if let Some(cell) = row.get_mut(pos.col) {
206                cell.set_text(c.to_string());
207                cell.set_attrs(attrs);
208            }
209
210            // Handle wide characters
211            if char_width == 2 && pos.col + 1 < size.cols {
212                if let Some(next_cell) = row.get_mut(pos.col + 1) {
213                    next_cell.set_wide_continuation();
214                    next_cell.set_attrs(attrs);
215                }
216            }
217        }
218
219        // Move cursor
220        let new_col = pos.col + char_width as u16;
221        if new_col >= size.cols {
222            if self.mode(MODE_AUTO_WRAP) {
223                self.pending_wrap = true;
224                self.grid_mut().set_col(size.cols - 1);
225            }
226        } else {
227            self.grid_mut().set_col(new_col);
228        }
229    }
230
231    /// Handle control codes
232    fn handle_control(&mut self, code: ControlCode) {
233        match code {
234            ControlCode::Bell => {} // Could trigger bell callback
235            ControlCode::Backspace => {
236                let pos = self.grid().pos();
237                if pos.col > 0 {
238                    self.grid_mut().set_col(pos.col - 1);
239                }
240                self.pending_wrap = false;
241            }
242            ControlCode::HorizontalTab => {
243                let pos = self.grid().pos();
244                let next_tab = ((pos.col / 8) + 1) * 8;
245                let size = self.grid().size();
246                self.grid_mut().set_col(next_tab.min(size.cols - 1));
247                self.pending_wrap = false;
248            }
249            ControlCode::LineFeed | ControlCode::VerticalTab | ControlCode::FormFeed => {
250                let pos = self.grid().pos();
251                let size = self.grid().size();
252                if pos.row + 1 >= size.rows {
253                    self.grid_mut().scroll_up(1);
254                } else {
255                    self.grid_mut().set_row(pos.row + 1);
256                }
257                self.pending_wrap = false;
258            }
259            ControlCode::CarriageReturn => {
260                self.grid_mut().set_col(0);
261                self.pending_wrap = false;
262            }
263            _ => {}
264        }
265    }
266
267    /// Handle escape sequences
268    fn handle_esc(&mut self, esc: Esc) {
269        match esc {
270            Esc::Code(EscCode::DecSaveCursorPosition) => {
271                self.grid_mut().save_pos();
272            }
273            Esc::Code(EscCode::DecRestoreCursorPosition) => {
274                self.grid_mut().restore_pos();
275            }
276            Esc::Code(EscCode::ReverseIndex) => {
277                // Move cursor up, scrolling if needed
278                let pos = self.grid().pos();
279                if pos.row == 0 {
280                    self.grid_mut().scroll_down(1);
281                } else {
282                    self.grid_mut().set_row(pos.row - 1);
283                }
284            }
285            Esc::Code(EscCode::Index) => {
286                // Move cursor down, scrolling if needed
287                let pos = self.grid().pos();
288                let size = self.grid().size();
289                if pos.row + 1 >= size.rows {
290                    self.grid_mut().scroll_up(1);
291                } else {
292                    self.grid_mut().set_row(pos.row + 1);
293                }
294            }
295            Esc::Code(EscCode::NextLine) => {
296                // Like Index but also carriage return
297                let pos = self.grid().pos();
298                let size = self.grid().size();
299                if pos.row + 1 >= size.rows {
300                    self.grid_mut().scroll_up(1);
301                } else {
302                    self.grid_mut().set_row(pos.row + 1);
303                }
304                self.grid_mut().set_col(0);
305            }
306            Esc::Code(EscCode::FullReset) => {
307                self.grid_mut().clear();
308                self.grid_mut().set_pos(Pos::new(0, 0));
309                self.attrs = Attrs::default();
310                self.modes = MODE_CURSOR_VISIBLE | MODE_AUTO_WRAP;
311            }
312            _ => {}
313        }
314    }
315
316    /// Handle CSI sequences
317    fn handle_csi(&mut self, csi: termwiz::escape::csi::CSI) {
318        use termwiz::escape::csi::CSI;
319
320        match csi {
321            CSI::Cursor(cursor) => self.handle_cursor(cursor),
322            CSI::Edit(edit) => self.handle_edit(edit),
323            CSI::Sgr(sgr) => self.handle_sgr(sgr),
324            CSI::Mode(mode) => self.handle_mode(mode),
325            CSI::Window(_) => {}   // Window manipulation not implemented
326            CSI::Keyboard(_) => {} // Keyboard modes not implemented
327            CSI::Mouse(_) => {}    // Mouse reporting not implemented
328            CSI::Device(_) => {}   // Device queries not implemented
329            _ => {}
330        }
331    }
332
333    /// Handle cursor movement
334    fn handle_cursor(&mut self, cursor: Cursor) {
335        let size = self.grid().size();
336        let pos = self.grid().pos();
337
338        match cursor {
339            Cursor::Position { line, col } => {
340                let row = line.as_zero_based().min(size.rows.saturating_sub(1) as u32) as u16;
341                let col = col.as_zero_based().min(size.cols.saturating_sub(1) as u32) as u16;
342                self.grid_mut().set_pos(Pos::new(col, row));
343            }
344            Cursor::Up(n) => {
345                let new_row = pos.row.saturating_sub(n as u16);
346                self.grid_mut().set_row(new_row);
347            }
348            Cursor::Down(n) => {
349                let new_row = (pos.row + n as u16).min(size.rows - 1);
350                self.grid_mut().set_row(new_row);
351            }
352            Cursor::Left(n) => {
353                let new_col = pos.col.saturating_sub(n as u16);
354                self.grid_mut().set_col(new_col);
355            }
356            Cursor::Right(n) => {
357                let new_col = (pos.col + n as u16).min(size.cols - 1);
358                self.grid_mut().set_col(new_col);
359            }
360            Cursor::CharacterAbsolute(col) => {
361                let col = col.as_zero_based().min(size.cols.saturating_sub(1) as u32) as u16;
362                self.grid_mut().set_col(col);
363            }
364            Cursor::NextLine(n) => {
365                let new_row = (pos.row + n as u16).min(size.rows - 1);
366                self.grid_mut().set_pos(Pos::new(0, new_row));
367            }
368            Cursor::PrecedingLine(n) => {
369                let new_row = pos.row.saturating_sub(n as u16);
370                self.grid_mut().set_pos(Pos::new(0, new_row));
371            }
372            Cursor::SaveCursor => {
373                self.grid_mut().save_pos();
374            }
375            Cursor::RestoreCursor => {
376                self.grid_mut().restore_pos();
377            }
378            _ => {}
379        }
380        self.pending_wrap = false;
381    }
382
383    /// Handle edit operations
384    fn handle_edit(&mut self, edit: Edit) {
385        let size = self.grid().size();
386        let pos = self.grid().pos();
387
388        match edit {
389            Edit::EraseInDisplay(mode) => match mode {
390                EraseInDisplay::EraseToEndOfDisplay => {
391                    self.grid_mut().clear_below();
392                }
393                EraseInDisplay::EraseToStartOfDisplay => {
394                    self.grid_mut().clear_above();
395                }
396                EraseInDisplay::EraseDisplay => {
397                    self.grid_mut().clear();
398                }
399                EraseInDisplay::EraseScrollback => {
400                    // Clear scrollback - not implemented yet
401                }
402            },
403            Edit::EraseInLine(mode) => {
404                if let Some(row) = self.grid_mut().drawing_row_mut(pos.row) {
405                    match mode {
406                        EraseInLine::EraseToEndOfLine => {
407                            row.erase(pos.col, size.cols);
408                        }
409                        EraseInLine::EraseToStartOfLine => {
410                            row.erase(0, pos.col + 1);
411                        }
412                        EraseInLine::EraseLine => {
413                            row.clear();
414                        }
415                    }
416                }
417            }
418            Edit::InsertCharacter(n) => {
419                if let Some(row) = self.grid_mut().drawing_row_mut(pos.row) {
420                    for _ in 0..n {
421                        row.insert(pos.col, Default::default());
422                    }
423                }
424            }
425            Edit::DeleteCharacter(n) => {
426                if let Some(row) = self.grid_mut().drawing_row_mut(pos.row) {
427                    for _ in 0..n {
428                        row.remove(pos.col);
429                    }
430                }
431            }
432            Edit::EraseCharacter(n) => {
433                if let Some(row) = self.grid_mut().drawing_row_mut(pos.row) {
434                    row.erase(pos.col, pos.col + n as u16);
435                }
436            }
437            Edit::InsertLine(n) => {
438                for _ in 0..n {
439                    self.grid_mut().scroll_down(1);
440                }
441            }
442            Edit::DeleteLine(n) => {
443                for _ in 0..n {
444                    self.grid_mut().scroll_up(1);
445                }
446            }
447            Edit::ScrollDown(n) => {
448                self.grid_mut().scroll_down(n as usize);
449            }
450            Edit::ScrollUp(n) => {
451                self.grid_mut().scroll_up(n as usize);
452            }
453            _ => {}
454        }
455    }
456
457    /// Handle SGR (Select Graphic Rendition) - text styling
458    fn handle_sgr(&mut self, sgr: Sgr) {
459        match sgr {
460            Sgr::Reset => {
461                self.attrs.reset();
462            }
463            Sgr::Intensity(intensity) => match intensity {
464                termwiz::cell::Intensity::Bold => {
465                    self.attrs.set_bold(true);
466                }
467                termwiz::cell::Intensity::Normal => {
468                    self.attrs.set_bold(false);
469                }
470                termwiz::cell::Intensity::Half => {
471                    self.attrs.set_bold(false);
472                }
473            },
474            Sgr::Italic(on) => {
475                self.attrs.set_italic(on);
476            }
477            Sgr::Underline(underline) => {
478                self.attrs
479                    .set_underline(underline != termwiz::cell::Underline::None);
480            }
481            Sgr::Inverse(on) => {
482                self.attrs.set_inverse(on);
483            }
484            Sgr::StrikeThrough(on) => {
485                self.attrs.set_strikethrough(on);
486            }
487            Sgr::Foreground(color) => {
488                self.attrs.fg = Color::from(color);
489            }
490            Sgr::Background(color) => {
491                self.attrs.bg = Color::from(color);
492            }
493            _ => {}
494        }
495    }
496
497    /// Handle mode changes
498    fn handle_mode(&mut self, mode: Mode) {
499        // Standard ANSI modes - not commonly used
500        let _ = mode;
501    }
502
503    /// Handle OSC (Operating System Command)
504    fn handle_osc(&mut self, osc: OperatingSystemCommand) {
505        match osc {
506            OperatingSystemCommand::SetWindowTitle(title)
507            | OperatingSystemCommand::SetWindowTitleSun(title) => {
508                self.title = title;
509            }
510            OperatingSystemCommand::SetIconName(name)
511            | OperatingSystemCommand::SetIconNameSun(name) => {
512                self.icon_name = name;
513            }
514            OperatingSystemCommand::SetIconNameAndWindowTitle(title) => {
515                self.title = title.clone();
516                self.icon_name = title;
517            }
518            _ => {}
519        }
520    }
521
522    /// Get visible rows iterator (for rendering)
523    pub fn visible_rows(&self) -> impl Iterator<Item = &crate::primitives::termtui::row::Row> {
524        self.grid().visible_rows()
525    }
526
527    /// Check if in alternate screen mode
528    pub fn is_alternate_screen(&self) -> bool {
529        self.mode(MODE_ALTERNATE_SCREEN)
530    }
531}
532
533#[cfg(test)]
534mod tests {
535    use super::*;
536
537    #[test]
538    fn test_screen_new() {
539        let screen = Screen::new(24, 80, 1000);
540        assert_eq!(screen.size().rows, 24);
541        assert_eq!(screen.size().cols, 80);
542    }
543
544    #[test]
545    fn test_screen_text() {
546        let mut screen = Screen::new(24, 80, 1000);
547
548        screen.text('H');
549        screen.text('i');
550
551        assert_eq!(screen.cursor_pos().col, 2);
552    }
553
554    #[test]
555    fn test_screen_newline() {
556        let mut screen = Screen::new(24, 80, 1000);
557
558        screen.text('A');
559        screen.handle_control(ControlCode::LineFeed);
560        screen.handle_control(ControlCode::CarriageReturn);
561        screen.text('B');
562
563        assert_eq!(screen.cursor_pos().row, 1);
564        assert_eq!(screen.cursor_pos().col, 1);
565    }
566
567    #[test]
568    fn test_screen_scroll() {
569        let mut screen = Screen::new(24, 80, 100);
570
571        // Fill screen and trigger scroll
572        for _ in 0..30 {
573            screen.handle_control(ControlCode::LineFeed);
574        }
575
576        assert!(screen.grid().scrollback_available() > 0);
577    }
578}