ratatui_toolkit/vt100_term/
screen.rs

1//! Screen state management
2//!
3//! Handles terminal state including cursor position, attributes,
4//! and processing of termwiz escape sequence actions.
5
6use super::cell::{Attrs, Cell};
7use super::grid::{Grid, Size};
8use termwiz::escape::Action;
9
10/// Cursor position
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12struct Cursor {
13    row: usize,
14    col: usize,
15}
16
17impl Cursor {
18    fn new() -> Self {
19        Self { row: 0, col: 0 }
20    }
21}
22
23/// Terminal screen state
24#[derive(Debug, Clone)]
25pub struct Screen {
26    /// The grid of cells
27    grid: Grid,
28
29    /// Current cursor position
30    cursor: Cursor,
31
32    /// Current text attributes
33    attrs: Attrs,
34
35    /// Saved cursor position (for save/restore)
36    saved_cursor: Option<Cursor>,
37
38    /// Whether cursor is visible
39    #[allow(dead_code)]
40    cursor_visible: bool,
41}
42
43impl Screen {
44    /// Create a new screen
45    pub fn new(rows: usize, cols: usize, scrollback_len: usize) -> Self {
46        Self {
47            grid: Grid::new(rows, cols, scrollback_len),
48            cursor: Cursor::new(),
49            attrs: Attrs::default(),
50            saved_cursor: None,
51            cursor_visible: true,
52        }
53    }
54
55    /// Get screen size
56    pub fn size(&self) -> Size {
57        self.grid.size()
58    }
59
60    /// Get current scrollback offset
61    pub fn scrollback(&self) -> usize {
62        self.grid.scrollback()
63    }
64
65    /// Get maximum scrollback length
66    pub fn scrollback_len(&self) -> usize {
67        self.grid.scrollback_len()
68    }
69
70    /// Set scrollback offset
71    pub fn set_scrollback(&mut self, offset: usize) {
72        self.grid.set_scrollback(offset);
73    }
74
75    /// Scroll screen up (view older content)
76    pub fn scroll_screen_up(&mut self, n: usize) {
77        self.grid.scroll_screen_up(n);
78    }
79
80    /// Scroll screen down (view newer content)
81    pub fn scroll_screen_down(&mut self, n: usize) {
82        self.grid.scroll_screen_down(n);
83    }
84
85    /// Get cell at position (relative to visible area)
86    pub fn cell(&self, row: usize, col: usize) -> Option<&Cell> {
87        self.grid.cell(row, col)
88    }
89
90    /// Resize the screen
91    pub fn resize(&mut self, rows: usize, cols: usize) {
92        self.grid.resize(rows, cols);
93        // Clamp cursor to new bounds
94        self.cursor.row = self.cursor.row.min(rows.saturating_sub(1));
95        self.cursor.col = self.cursor.col.min(cols.saturating_sub(1));
96    }
97
98    /// Get selected text
99    pub fn get_selected_text(&self, low_x: i32, low_y: i32, high_x: i32, high_y: i32) -> String {
100        self.grid.get_selected_text(low_x, low_y, high_x, high_y)
101    }
102
103    /// Handle a termwiz action
104    pub fn handle_action(&mut self, action: &Action) {
105        match action {
106            Action::Print(c) => self.print_char(*c),
107            Action::PrintString(s) => {
108                for c in s.chars() {
109                    self.print_char(c);
110                }
111            }
112            Action::Control(code) => {
113                // ControlCode is an enum, convert to u8
114                let byte = match code {
115                    termwiz::escape::ControlCode::Null => 0x00,
116                    termwiz::escape::ControlCode::Bell => 0x07,
117                    termwiz::escape::ControlCode::Backspace => 0x08,
118                    termwiz::escape::ControlCode::HorizontalTab => 0x09,
119                    termwiz::escape::ControlCode::LineFeed => 0x0A,
120                    termwiz::escape::ControlCode::VerticalTab => 0x0B,
121                    termwiz::escape::ControlCode::FormFeed => 0x0C,
122                    termwiz::escape::ControlCode::CarriageReturn => 0x0D,
123                    _ => return, // Ignore other control codes
124                };
125                self.handle_control(byte);
126            }
127            Action::CSI(csi) => self.handle_csi(csi),
128            Action::Esc(esc) => self.handle_esc(esc),
129            Action::OperatingSystemCommand(osc) => self.handle_osc(osc),
130            _ => {
131                // Ignore other action types for now
132            }
133        }
134    }
135
136    /// Print a character at cursor position
137    fn print_char(&mut self, c: char) {
138        let size = self.grid.size();
139
140        // Handle newline
141        if c == '\n' {
142            self.cursor.col = 0;
143            self.cursor.row += 1;
144            if self.cursor.row >= size.rows {
145                self.grid.scroll_up(1);
146                self.cursor.row = size.rows - 1;
147            }
148            return;
149        }
150
151        // Handle carriage return
152        if c == '\r' {
153            self.cursor.col = 0;
154            return;
155        }
156
157        // Handle tab
158        if c == '\t' {
159            self.cursor.col = ((self.cursor.col + 8) / 8) * 8;
160            if self.cursor.col >= size.cols {
161                self.cursor.col = 0;
162                self.cursor.row += 1;
163                if self.cursor.row >= size.rows {
164                    self.grid.scroll_up(1);
165                    self.cursor.row = size.rows - 1;
166                }
167            }
168            return;
169        }
170
171        // Print normal character
172        if let Some(cell) = self.grid.cell_mut(self.cursor.row, self.cursor.col) {
173            cell.text = c.to_string();
174            cell.attrs = self.attrs;
175        }
176
177        // Advance cursor
178        self.cursor.col += 1;
179        if self.cursor.col >= size.cols {
180            self.cursor.col = 0;
181            self.cursor.row += 1;
182            if self.cursor.row >= size.rows {
183                self.grid.scroll_up(1);
184                self.cursor.row = size.rows - 1;
185            }
186        }
187    }
188
189    /// Handle control codes
190    fn handle_control(&mut self, code: u8) {
191        match code {
192            0x08 => {
193                // Backspace
194                if self.cursor.col > 0 {
195                    self.cursor.col -= 1;
196                }
197            }
198            0x0A => {
199                // Line feed
200                self.cursor.row += 1;
201                if self.cursor.row >= self.grid.size().rows {
202                    self.grid.scroll_up(1);
203                    self.cursor.row = self.grid.size().rows - 1;
204                }
205            }
206            0x0D => {
207                // Carriage return
208                self.cursor.col = 0;
209            }
210            _ => {}
211        }
212    }
213
214    /// Handle CSI (Control Sequence Introducer) sequences
215    fn handle_csi(&mut self, csi: &termwiz::escape::CSI) {
216        use termwiz::escape::CSI;
217
218        match csi {
219            CSI::Cursor(cursor) => self.handle_cursor(cursor),
220            CSI::Sgr(sgr) => self.handle_sgr(sgr),
221            CSI::Edit(edit) => self.handle_edit(edit),
222            CSI::Mode(mode) => self.handle_mode(mode),
223            _ => {}
224        }
225    }
226
227    /// Handle cursor movement
228    fn handle_cursor(&mut self, cursor: &termwiz::escape::csi::Cursor) {
229        use termwiz::escape::csi::Cursor;
230
231        let size = self.grid.size();
232
233        match cursor {
234            Cursor::Position { line, col } => {
235                self.cursor.row = (line.as_zero_based() as usize).min(size.rows - 1);
236                self.cursor.col = (col.as_zero_based() as usize).min(size.cols - 1);
237            }
238            Cursor::Up(n) => {
239                self.cursor.row = self.cursor.row.saturating_sub(*n as usize);
240            }
241            Cursor::Down(n) => {
242                self.cursor.row = (self.cursor.row + *n as usize).min(size.rows - 1);
243            }
244            Cursor::Right(n) => {
245                self.cursor.col = (self.cursor.col + *n as usize).min(size.cols - 1);
246            }
247            Cursor::Left(n) => {
248                self.cursor.col = self.cursor.col.saturating_sub(*n as usize);
249            }
250            Cursor::CharacterAbsolute(col) => {
251                self.cursor.col = (col.as_zero_based() as usize).min(size.cols - 1);
252            }
253            Cursor::LineTabulation(n) => {
254                self.cursor.row = (self.cursor.row + *n as usize).min(size.rows - 1);
255            }
256            Cursor::SaveCursor => {
257                self.saved_cursor = Some(self.cursor);
258            }
259            Cursor::RestoreCursor => {
260                if let Some(saved) = self.saved_cursor {
261                    self.cursor = saved;
262                }
263            }
264            _ => {}
265        }
266    }
267
268    /// Handle SGR (Select Graphic Rendition) - text attributes
269    fn handle_sgr(&mut self, sgr: &termwiz::escape::csi::Sgr) {
270        use termwiz::escape::csi::Sgr;
271
272        match sgr {
273            Sgr::Reset => {
274                self.attrs = Attrs::default();
275            }
276            Sgr::Intensity(intensity) => {
277                self.attrs.intensity = *intensity;
278            }
279            Sgr::Underline(underline) => {
280                self.attrs.underline = *underline != termwiz::cell::Underline::None;
281            }
282            Sgr::Blink(blink) => {
283                self.attrs.blink = *blink != termwiz::cell::Blink::None;
284            }
285            Sgr::Inverse(inverse) => {
286                self.attrs.reverse = *inverse;
287            }
288            Sgr::Italic(italic) => {
289                self.attrs.italic = *italic;
290            }
291            Sgr::StrikeThrough(strike) => {
292                self.attrs.strikethrough = *strike;
293            }
294            Sgr::Foreground(color) => {
295                // Convert ColorSpec to ColorAttribute
296                self.attrs.fgcolor = match color {
297                    termwiz::color::ColorSpec::Default => termwiz::color::ColorAttribute::Default,
298                    termwiz::color::ColorSpec::PaletteIndex(idx) => {
299                        termwiz::color::ColorAttribute::PaletteIndex(*idx)
300                    }
301                    termwiz::color::ColorSpec::TrueColor(rgb) => {
302                        termwiz::color::ColorAttribute::TrueColorWithDefaultFallback(*rgb)
303                    }
304                };
305            }
306            Sgr::Background(color) => {
307                // Convert ColorSpec to ColorAttribute
308                self.attrs.bgcolor = match color {
309                    termwiz::color::ColorSpec::Default => termwiz::color::ColorAttribute::Default,
310                    termwiz::color::ColorSpec::PaletteIndex(idx) => {
311                        termwiz::color::ColorAttribute::PaletteIndex(*idx)
312                    }
313                    termwiz::color::ColorSpec::TrueColor(rgb) => {
314                        termwiz::color::ColorAttribute::TrueColorWithDefaultFallback(*rgb)
315                    }
316                };
317            }
318            _ => {}
319        }
320    }
321
322    /// Handle edit operations
323    fn handle_edit(&mut self, edit: &termwiz::escape::csi::Edit) {
324        use termwiz::escape::csi::Edit;
325
326        match edit {
327            Edit::EraseInLine(erase) => {
328                use termwiz::escape::csi::EraseInLine;
329                let size = self.grid.size();
330                let row = self.cursor.row;
331
332                match erase {
333                    EraseInLine::EraseToEndOfLine => {
334                        for col in self.cursor.col..size.cols {
335                            if let Some(cell) = self.grid.cell_mut(row, col) {
336                                *cell = Cell::default();
337                            }
338                        }
339                    }
340                    EraseInLine::EraseToStartOfLine => {
341                        for col in 0..=self.cursor.col {
342                            if let Some(cell) = self.grid.cell_mut(row, col) {
343                                *cell = Cell::default();
344                            }
345                        }
346                    }
347                    EraseInLine::EraseLine => {
348                        for col in 0..size.cols {
349                            if let Some(cell) = self.grid.cell_mut(row, col) {
350                                *cell = Cell::default();
351                            }
352                        }
353                    }
354                }
355            }
356            Edit::EraseInDisplay(erase) => {
357                use termwiz::escape::csi::EraseInDisplay;
358
359                match erase {
360                    EraseInDisplay::EraseToEndOfDisplay => {
361                        // Clear from cursor to end of screen
362                        let size = self.grid.size();
363
364                        // Clear rest of current line
365                        for col in self.cursor.col..size.cols {
366                            if let Some(cell) = self.grid.cell_mut(self.cursor.row, col) {
367                                *cell = Cell::default();
368                            }
369                        }
370
371                        // Clear all lines below
372                        for row in (self.cursor.row + 1)..size.rows {
373                            for col in 0..size.cols {
374                                if let Some(cell) = self.grid.cell_mut(row, col) {
375                                    *cell = Cell::default();
376                                }
377                            }
378                        }
379                    }
380                    EraseInDisplay::EraseToStartOfDisplay => {
381                        let size = self.grid.size();
382
383                        // Clear all lines above
384                        for row in 0..self.cursor.row {
385                            for col in 0..size.cols {
386                                if let Some(cell) = self.grid.cell_mut(row, col) {
387                                    *cell = Cell::default();
388                                }
389                            }
390                        }
391
392                        // Clear start of current line
393                        for col in 0..=self.cursor.col {
394                            if let Some(cell) = self.grid.cell_mut(self.cursor.row, col) {
395                                *cell = Cell::default();
396                            }
397                        }
398                    }
399                    EraseInDisplay::EraseDisplay => {
400                        self.grid.clear();
401                    }
402                    _ => {}
403                }
404            }
405            _ => {}
406        }
407    }
408
409    /// Handle mode changes
410    fn handle_mode(&mut self, _mode: &termwiz::escape::csi::Mode) {
411        // Mode changes like show/hide cursor, alternate screen, etc.
412        // For now, we'll keep it simple and ignore mode changes
413        // TODO: Handle DECTCEM (cursor visibility) and other modes
414    }
415
416    /// Handle ESC sequences
417    fn handle_esc(&mut self, _esc: &termwiz::escape::Esc) {
418        // Most ESC sequences are handled by CSI
419    }
420
421    /// Handle OSC (Operating System Command) sequences
422    fn handle_osc(&mut self, _osc: &termwiz::escape::OperatingSystemCommand) {
423        // OSC sequences like clipboard (OSC 52) could be handled here
424        // For now, we'll handle clipboard in the VT100Term widget
425    }
426}