Rust · 31346 bytes Raw Blame History
1 //! Terminal screen buffer
2 //!
3 //! Manages the grid of cells that make up the terminal display.
4 //! Uses VTE for parsing escape sequences.
5
6 use vte::{Params, Parser, Perform};
7
8 /// A single cell in the terminal grid
9 #[derive(Clone, Debug)]
10 pub struct Cell {
11 pub c: char,
12 pub fg: Color,
13 pub bg: Color,
14 pub bold: bool,
15 pub underline: bool,
16 pub inverse: bool,
17 }
18
19 impl Default for Cell {
20 fn default() -> Self {
21 Self {
22 c: ' ',
23 fg: Color::Default,
24 bg: Color::Default,
25 bold: false,
26 underline: false,
27 inverse: false,
28 }
29 }
30 }
31
32 /// Terminal colors
33 #[derive(Clone, Copy, Debug, PartialEq)]
34 pub enum Color {
35 Default,
36 Black,
37 Red,
38 Green,
39 Yellow,
40 Blue,
41 Magenta,
42 Cyan,
43 White,
44 BrightBlack,
45 BrightRed,
46 BrightGreen,
47 BrightYellow,
48 BrightBlue,
49 BrightMagenta,
50 BrightCyan,
51 BrightWhite,
52 Indexed(u8),
53 Rgb(u8, u8, u8),
54 }
55
56 /// Terminal screen state
57 pub struct TerminalScreen {
58 /// Grid of cells (row-major)
59 cells: Vec<Vec<Cell>>,
60 /// Number of columns
61 pub cols: u16,
62 /// Number of rows
63 pub rows: u16,
64 /// Cursor position (0-indexed)
65 pub cursor_row: u16,
66 pub cursor_col: u16,
67 /// Current text attributes
68 current_fg: Color,
69 current_bg: Color,
70 current_bold: bool,
71 current_underline: bool,
72 current_inverse: bool,
73 /// VTE parser
74 parser: Parser,
75 /// Scrollback buffer
76 scrollback: Vec<Vec<Cell>>,
77 /// Max scrollback lines
78 max_scrollback: usize,
79 /// Scroll offset (0 = at bottom)
80 pub scroll_offset: usize,
81 /// DEC private modes
82 pub cursor_visible: bool,
83 autowrap: bool,
84 application_cursor_keys: bool,
85 bracketed_paste: bool,
86 /// Alternate screen buffer
87 alt_cells: Option<Vec<Vec<Cell>>>,
88 alt_cursor_row: u16,
89 alt_cursor_col: u16,
90 using_alt_screen: bool,
91 /// Saved cursor position (for ESC 7/8 and CSI s/u)
92 saved_cursor_row: u16,
93 saved_cursor_col: u16,
94 /// Scroll region (top, bottom) - 0-indexed, inclusive
95 scroll_top: u16,
96 scroll_bottom: u16,
97 /// Response queue for device status reports
98 response_queue: Vec<Vec<u8>>,
99 /// Current working directory (from OSC 7)
100 pub cwd: Option<String>,
101 }
102
103 impl TerminalScreen {
104 pub fn new(cols: u16, rows: u16) -> Self {
105 let cells = vec![vec![Cell::default(); cols as usize]; rows as usize];
106 Self {
107 cells,
108 cols,
109 rows,
110 cursor_row: 0,
111 cursor_col: 0,
112 current_fg: Color::Default,
113 current_bg: Color::Default,
114 current_bold: false,
115 current_underline: false,
116 current_inverse: false,
117 parser: Parser::new(),
118 scrollback: Vec::new(),
119 max_scrollback: 10000,
120 scroll_offset: 0,
121 // DEC private modes
122 cursor_visible: true,
123 autowrap: true,
124 application_cursor_keys: false,
125 bracketed_paste: false,
126 // Alternate screen buffer
127 alt_cells: None,
128 alt_cursor_row: 0,
129 alt_cursor_col: 0,
130 using_alt_screen: false,
131 // Saved cursor
132 saved_cursor_row: 0,
133 saved_cursor_col: 0,
134 // Scroll region (full screen by default)
135 scroll_top: 0,
136 scroll_bottom: rows.saturating_sub(1),
137 // Response queue
138 response_queue: Vec::new(),
139 // Current working directory
140 cwd: None,
141 }
142 }
143
144 /// Process raw bytes from the PTY
145 pub fn process(&mut self, data: &[u8]) {
146 // Take parser out temporarily to avoid borrow conflict
147 let mut parser = std::mem::take(&mut self.parser);
148 for byte in data {
149 parser.advance(self, *byte);
150 }
151 self.parser = parser;
152 }
153
154 /// Get a reference to the cell grid
155 pub fn cells(&self) -> &Vec<Vec<Cell>> {
156 &self.cells
157 }
158
159 /// Get a row from scrollback or current screen
160 pub fn get_row(&self, row: usize) -> Option<&Vec<Cell>> {
161 if self.scroll_offset > 0 {
162 let scrollback_row = self.scrollback.len().saturating_sub(self.scroll_offset) + row;
163 if scrollback_row < self.scrollback.len() {
164 self.scrollback.get(scrollback_row)
165 } else {
166 let screen_row = scrollback_row - self.scrollback.len();
167 self.cells.get(screen_row)
168 }
169 } else {
170 self.cells.get(row)
171 }
172 }
173
174 /// Resize the terminal
175 pub fn resize(&mut self, cols: u16, rows: u16) {
176 // Create new cell grid
177 let mut new_cells = vec![vec![Cell::default(); cols as usize]; rows as usize];
178
179 // Copy existing content
180 for (r, row) in self.cells.iter().enumerate() {
181 if r >= rows as usize {
182 break;
183 }
184 for (c, cell) in row.iter().enumerate() {
185 if c >= cols as usize {
186 break;
187 }
188 new_cells[r][c] = cell.clone();
189 }
190 }
191
192 self.cells = new_cells;
193 self.cols = cols;
194 self.rows = rows;
195
196 // Ensure cursor is within bounds
197 self.cursor_row = self.cursor_row.min(rows.saturating_sub(1));
198 self.cursor_col = self.cursor_col.min(cols.saturating_sub(1));
199
200 // Update scroll region to new size
201 self.scroll_bottom = rows.saturating_sub(1);
202 if self.scroll_top > self.scroll_bottom {
203 self.scroll_top = 0;
204 }
205 }
206
207 /// Drain response queue (for device status reports)
208 pub fn drain_responses(&mut self) -> Vec<Vec<u8>> {
209 std::mem::take(&mut self.response_queue)
210 }
211
212 /// Whether the application running in the terminal has enabled bracketed paste.
213 pub fn bracketed_paste_enabled(&self) -> bool {
214 self.bracketed_paste
215 }
216
217 /// Enter alternate screen buffer
218 fn enter_alt_screen(&mut self) {
219 if !self.using_alt_screen {
220 // Save primary screen and cursor
221 self.alt_cells = Some(std::mem::take(&mut self.cells));
222 self.alt_cursor_row = self.cursor_row;
223 self.alt_cursor_col = self.cursor_col;
224 // Create fresh alt screen
225 self.cells = vec![vec![Cell::default(); self.cols as usize]; self.rows as usize];
226 self.cursor_row = 0;
227 self.cursor_col = 0;
228 self.using_alt_screen = true;
229 }
230 }
231
232 /// Leave alternate screen buffer
233 fn leave_alt_screen(&mut self) {
234 if self.using_alt_screen {
235 if let Some(primary) = self.alt_cells.take() {
236 self.cells = primary;
237 self.cursor_row = self.alt_cursor_row;
238 self.cursor_col = self.alt_cursor_col;
239 }
240 self.using_alt_screen = false;
241 }
242 }
243
244 /// Save cursor position
245 fn save_cursor(&mut self) {
246 self.saved_cursor_row = self.cursor_row;
247 self.saved_cursor_col = self.cursor_col;
248 }
249
250 /// Restore cursor position
251 fn restore_cursor(&mut self) {
252 self.cursor_row = self.saved_cursor_row.min(self.rows.saturating_sub(1));
253 self.cursor_col = self.saved_cursor_col.min(self.cols.saturating_sub(1));
254 }
255
256 /// Handle DEC private mode set/reset
257 fn handle_dec_private_mode(&mut self, params: &[u16], set: bool) {
258 for &param in params {
259 match param {
260 1 => self.application_cursor_keys = set, // DECCKM
261 7 => self.autowrap = set, // DECAWM
262 25 => self.cursor_visible = set, // DECTCEM
263 1049 => {
264 // Alternate screen buffer
265 if set {
266 self.enter_alt_screen();
267 } else {
268 self.leave_alt_screen();
269 }
270 }
271 2004 => self.bracketed_paste = set, // Bracketed paste
272 _ => {} // Ignore unknown modes
273 }
274 }
275 }
276
277 /// Reverse index - move cursor up, scroll down if at top
278 fn reverse_index(&mut self) {
279 if self.cursor_row == self.scroll_top {
280 self.scroll_down_region(1);
281 } else {
282 self.cursor_row = self.cursor_row.saturating_sub(1);
283 }
284 }
285
286 /// Index - move cursor down, scroll up if at bottom
287 fn index(&mut self) {
288 if self.cursor_row == self.scroll_bottom {
289 self.scroll_up_region(1);
290 } else if self.cursor_row < self.rows - 1 {
291 self.cursor_row += 1;
292 }
293 }
294
295 /// Next line - move to start of next line, scroll if needed
296 fn next_line(&mut self) {
297 self.cursor_col = 0;
298 self.index();
299 }
300
301 /// Scroll up within scroll region
302 fn scroll_up_region(&mut self, n: u16) {
303 let top = self.scroll_top as usize;
304 let bottom = self.scroll_bottom as usize;
305
306 for _ in 0..n {
307 if top < self.cells.len() && bottom < self.cells.len() && top <= bottom {
308 // Move top row to scrollback (only if scroll region is full screen)
309 if self.scroll_top == 0 && self.scroll_bottom == self.rows - 1 {
310 let top_row = self.cells.remove(top);
311 self.scrollback.push(top_row);
312 if self.scrollback.len() > self.max_scrollback {
313 self.scrollback.remove(0);
314 }
315 } else {
316 self.cells.remove(top);
317 }
318 // Insert new row at bottom of scroll region
319 self.cells.insert(bottom, vec![Cell::default(); self.cols as usize]);
320 }
321 }
322 }
323
324 /// Scroll down within scroll region
325 fn scroll_down_region(&mut self, n: u16) {
326 let top = self.scroll_top as usize;
327 let bottom = self.scroll_bottom as usize;
328
329 for _ in 0..n {
330 if top < self.cells.len() && bottom < self.cells.len() && top <= bottom {
331 // Remove bottom row
332 self.cells.remove(bottom);
333 // Insert new row at top of scroll region
334 self.cells.insert(top, vec![Cell::default(); self.cols as usize]);
335 }
336 }
337 }
338
339 /// Insert n lines at cursor position
340 fn insert_lines(&mut self, n: u16) {
341 let row = self.cursor_row as usize;
342 let bottom = self.scroll_bottom as usize;
343
344 for _ in 0..n {
345 if row <= bottom && bottom < self.cells.len() {
346 self.cells.remove(bottom);
347 self.cells.insert(row, vec![Cell::default(); self.cols as usize]);
348 }
349 }
350 }
351
352 /// Delete n lines at cursor position
353 fn delete_lines(&mut self, n: u16) {
354 let row = self.cursor_row as usize;
355 let bottom = self.scroll_bottom as usize;
356
357 for _ in 0..n {
358 if row <= bottom && row < self.cells.len() {
359 self.cells.remove(row);
360 self.cells.insert(bottom, vec![Cell::default(); self.cols as usize]);
361 }
362 }
363 }
364
365 /// Insert n blank characters at cursor position
366 fn insert_chars(&mut self, n: u16) {
367 if let Some(row) = self.cells.get_mut(self.cursor_row as usize) {
368 let col = self.cursor_col as usize;
369 for _ in 0..n {
370 if col < row.len() {
371 row.pop(); // Remove from end
372 row.insert(col, Cell::default()); // Insert at cursor
373 }
374 }
375 }
376 }
377
378 /// Delete n characters at cursor position
379 fn delete_chars(&mut self, n: u16) {
380 if let Some(row) = self.cells.get_mut(self.cursor_row as usize) {
381 let col = self.cursor_col as usize;
382 for _ in 0..n {
383 if col < row.len() {
384 row.remove(col);
385 row.push(Cell::default()); // Add blank at end
386 }
387 }
388 }
389 }
390
391 /// Clear from start of screen to cursor
392 fn clear_from_start(&mut self) {
393 // Clear all rows before cursor row
394 for row in self.cells.iter_mut().take(self.cursor_row as usize) {
395 for cell in row.iter_mut() {
396 *cell = Cell::default();
397 }
398 }
399 // Clear current row from start to cursor
400 if let Some(row) = self.cells.get_mut(self.cursor_row as usize) {
401 for cell in row.iter_mut().take(self.cursor_col as usize + 1) {
402 *cell = Cell::default();
403 }
404 }
405 }
406
407 /// Clear from start of line to cursor
408 fn clear_line_from_start(&mut self) {
409 if let Some(row) = self.cells.get_mut(self.cursor_row as usize) {
410 for cell in row.iter_mut().take(self.cursor_col as usize + 1) {
411 *cell = Cell::default();
412 }
413 }
414 }
415
416 /// Scroll the screen up by one line
417 fn scroll_up(&mut self) {
418 if !self.cells.is_empty() {
419 // Move top row to scrollback
420 let top_row = self.cells.remove(0);
421 self.scrollback.push(top_row);
422
423 // Trim scrollback if too large
424 if self.scrollback.len() > self.max_scrollback {
425 self.scrollback.remove(0);
426 }
427
428 // Add new empty row at bottom
429 self.cells.push(vec![Cell::default(); self.cols as usize]);
430 }
431 }
432
433 /// Clear the screen
434 fn clear_screen(&mut self) {
435 for row in &mut self.cells {
436 for cell in row {
437 *cell = Cell::default();
438 }
439 }
440 }
441
442 /// Clear from cursor to end of line
443 fn clear_to_eol(&mut self) {
444 if let Some(row) = self.cells.get_mut(self.cursor_row as usize) {
445 for cell in row.iter_mut().skip(self.cursor_col as usize) {
446 *cell = Cell::default();
447 }
448 }
449 }
450
451 /// Clear from cursor to end of screen
452 fn clear_to_eos(&mut self) {
453 self.clear_to_eol();
454 for row in self.cells.iter_mut().skip(self.cursor_row as usize + 1) {
455 for cell in row {
456 *cell = Cell::default();
457 }
458 }
459 }
460
461 /// Put a character at the cursor position
462 fn put_char(&mut self, c: char) {
463 if self.cursor_row < self.rows && self.cursor_col < self.cols {
464 if let Some(row) = self.cells.get_mut(self.cursor_row as usize) {
465 if let Some(cell) = row.get_mut(self.cursor_col as usize) {
466 cell.c = c;
467 cell.fg = self.current_fg;
468 cell.bg = self.current_bg;
469 cell.bold = self.current_bold;
470 cell.underline = self.current_underline;
471 cell.inverse = self.current_inverse;
472 }
473 }
474 }
475 }
476 }
477
478 /// VTE Perform implementation for processing escape sequences
479 impl Perform for TerminalScreen {
480 fn print(&mut self, c: char) {
481 self.put_char(c);
482 self.cursor_col += 1;
483
484 // Handle line wrap
485 if self.cursor_col >= self.cols {
486 self.cursor_col = 0;
487 self.cursor_row += 1;
488 if self.cursor_row >= self.rows {
489 self.scroll_up();
490 self.cursor_row = self.rows - 1;
491 }
492 }
493 }
494
495 fn execute(&mut self, byte: u8) {
496 match byte {
497 // Backspace
498 0x08 => {
499 self.cursor_col = self.cursor_col.saturating_sub(1);
500 }
501 // Tab
502 0x09 => {
503 self.cursor_col = ((self.cursor_col / 8) + 1) * 8;
504 if self.cursor_col >= self.cols {
505 self.cursor_col = self.cols - 1;
506 }
507 }
508 // Line feed
509 0x0A => {
510 self.cursor_row += 1;
511 if self.cursor_row >= self.rows {
512 self.scroll_up();
513 self.cursor_row = self.rows - 1;
514 }
515 }
516 // Carriage return
517 0x0D => {
518 self.cursor_col = 0;
519 }
520 _ => {}
521 }
522 }
523
524 fn hook(&mut self, _params: &Params, _intermediates: &[u8], _ignore: bool, _action: char) {}
525
526 fn put(&mut self, _byte: u8) {}
527
528 fn unhook(&mut self) {}
529
530 fn osc_dispatch(&mut self, params: &[&[u8]], _bell_terminated: bool) {
531 // OSC 7: Set working directory
532 // Format: OSC 7 ; file://hostname/path ST
533 if !params.is_empty() {
534 if let Ok(cmd) = std::str::from_utf8(params[0]) {
535 if cmd == "7" && params.len() >= 2 {
536 if let Ok(url) = std::str::from_utf8(params[1]) {
537 // Parse file://hostname/path format
538 if let Some(path) = url.strip_prefix("file://") {
539 // Find the first slash after hostname
540 if let Some(slash_idx) = path.find('/') {
541 let dir = &path[slash_idx..];
542 self.cwd = Some(dir.to_string());
543 }
544 }
545 }
546 }
547 }
548 }
549 }
550
551 fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, action: char) {
552 let params: Vec<u16> = params.iter().map(|p| p.first().copied().unwrap_or(0) as u16).collect();
553
554 // Check for DEC private mode sequences (CSI ? ...)
555 let is_private = intermediates.contains(&b'?');
556
557 if is_private {
558 match action {
559 'h' => self.handle_dec_private_mode(&params, true), // Set mode
560 'l' => self.handle_dec_private_mode(&params, false), // Reset mode
561 _ => {}
562 }
563 return;
564 }
565
566 match action {
567 // Cursor Up
568 'A' => {
569 let n = params.first().copied().unwrap_or(1).max(1);
570 self.cursor_row = self.cursor_row.saturating_sub(n);
571 }
572 // Cursor Down
573 'B' => {
574 let n = params.first().copied().unwrap_or(1).max(1);
575 self.cursor_row = (self.cursor_row + n).min(self.rows - 1);
576 }
577 // Cursor Forward
578 'C' => {
579 let n = params.first().copied().unwrap_or(1).max(1);
580 self.cursor_col = (self.cursor_col + n).min(self.cols - 1);
581 }
582 // Cursor Back
583 'D' => {
584 let n = params.first().copied().unwrap_or(1).max(1);
585 self.cursor_col = self.cursor_col.saturating_sub(n);
586 }
587 // Cursor Next Line
588 'E' => {
589 let n = params.first().copied().unwrap_or(1).max(1);
590 self.cursor_col = 0;
591 self.cursor_row = (self.cursor_row + n).min(self.rows - 1);
592 }
593 // Cursor Previous Line
594 'F' => {
595 let n = params.first().copied().unwrap_or(1).max(1);
596 self.cursor_col = 0;
597 self.cursor_row = self.cursor_row.saturating_sub(n);
598 }
599 // Cursor Horizontal Absolute
600 'G' => {
601 let col = params.first().copied().unwrap_or(1).max(1) - 1;
602 self.cursor_col = col.min(self.cols - 1);
603 }
604 // Cursor Position (CUP)
605 'H' | 'f' => {
606 let row = params.first().copied().unwrap_or(1).max(1) - 1;
607 let col = params.get(1).copied().unwrap_or(1).max(1) - 1;
608 self.cursor_row = row.min(self.rows - 1);
609 self.cursor_col = col.min(self.cols - 1);
610 }
611 // Erase in Display
612 'J' => {
613 let mode = params.first().copied().unwrap_or(0);
614 match mode {
615 0 => self.clear_to_eos(),
616 1 => self.clear_from_start(),
617 2 | 3 => self.clear_screen(),
618 _ => {}
619 }
620 }
621 // Erase in Line
622 'K' => {
623 let mode = params.first().copied().unwrap_or(0);
624 match mode {
625 0 => self.clear_to_eol(),
626 1 => self.clear_line_from_start(),
627 2 => {
628 // Clear entire line
629 if let Some(row) = self.cells.get_mut(self.cursor_row as usize) {
630 for cell in row {
631 *cell = Cell::default();
632 }
633 }
634 }
635 _ => {}
636 }
637 }
638 // Insert Lines
639 'L' => {
640 let n = params.first().copied().unwrap_or(1).max(1);
641 self.insert_lines(n);
642 }
643 // Delete Lines
644 'M' => {
645 let n = params.first().copied().unwrap_or(1).max(1);
646 self.delete_lines(n);
647 }
648 // Delete Characters
649 'P' => {
650 let n = params.first().copied().unwrap_or(1).max(1);
651 self.delete_chars(n);
652 }
653 // Scroll Up
654 'S' => {
655 let n = params.first().copied().unwrap_or(1).max(1);
656 self.scroll_up_region(n);
657 }
658 // Scroll Down
659 'T' => {
660 let n = params.first().copied().unwrap_or(1).max(1);
661 self.scroll_down_region(n);
662 }
663 // Erase Characters
664 'X' => {
665 let n = params.first().copied().unwrap_or(1).max(1) as usize;
666 if let Some(row) = self.cells.get_mut(self.cursor_row as usize) {
667 for i in 0..n {
668 let col = self.cursor_col as usize + i;
669 if col < row.len() {
670 row[col] = Cell::default();
671 }
672 }
673 }
674 }
675 // Insert Characters
676 '@' => {
677 let n = params.first().copied().unwrap_or(1).max(1);
678 self.insert_chars(n);
679 }
680 // Cursor Vertical Absolute
681 'd' => {
682 let row = params.first().copied().unwrap_or(1).max(1) - 1;
683 self.cursor_row = row.min(self.rows - 1);
684 }
685 // Device Status Report
686 'n' => {
687 let mode = params.first().copied().unwrap_or(0);
688 match mode {
689 5 => {
690 // Status report - respond "OK"
691 self.response_queue.push(b"\x1b[0n".to_vec());
692 }
693 6 => {
694 // Cursor position report
695 let response = format!("\x1b[{};{}R", self.cursor_row + 1, self.cursor_col + 1);
696 self.response_queue.push(response.into_bytes());
697 }
698 _ => {}
699 }
700 }
701 // Set Scroll Region (DECSTBM)
702 'r' => {
703 let top = params.first().copied().unwrap_or(1).max(1) - 1;
704 let bottom = params.get(1).copied().unwrap_or(self.rows).max(1) - 1;
705 if top < bottom && bottom < self.rows {
706 self.scroll_top = top;
707 self.scroll_bottom = bottom;
708 // Move cursor to home position
709 self.cursor_row = 0;
710 self.cursor_col = 0;
711 }
712 }
713 // Save Cursor Position
714 's' => {
715 self.save_cursor();
716 }
717 // Restore Cursor Position
718 'u' => {
719 self.restore_cursor();
720 }
721 // Select Graphic Rendition (SGR) - colors and attributes
722 'm' => {
723 if params.is_empty() {
724 // Reset all attributes
725 self.current_fg = Color::Default;
726 self.current_bg = Color::Default;
727 self.current_bold = false;
728 self.current_underline = false;
729 self.current_inverse = false;
730 return;
731 }
732
733 let mut iter = params.iter().peekable();
734 while let Some(&param) = iter.next() {
735 match param {
736 0 => {
737 self.current_fg = Color::Default;
738 self.current_bg = Color::Default;
739 self.current_bold = false;
740 self.current_underline = false;
741 self.current_inverse = false;
742 }
743 1 => self.current_bold = true,
744 4 => self.current_underline = true,
745 7 => self.current_inverse = true,
746 22 => self.current_bold = false,
747 24 => self.current_underline = false,
748 27 => self.current_inverse = false,
749 // Foreground colors
750 30 => self.current_fg = Color::Black,
751 31 => self.current_fg = Color::Red,
752 32 => self.current_fg = Color::Green,
753 33 => self.current_fg = Color::Yellow,
754 34 => self.current_fg = Color::Blue,
755 35 => self.current_fg = Color::Magenta,
756 36 => self.current_fg = Color::Cyan,
757 37 => self.current_fg = Color::White,
758 38 => {
759 // Extended foreground color
760 if let Some(&mode) = iter.next() {
761 match mode {
762 5 => {
763 // 256-color mode
764 if let Some(&idx) = iter.next() {
765 self.current_fg = Color::Indexed(idx as u8);
766 }
767 }
768 2 => {
769 // RGB mode
770 let r = iter.next().copied().unwrap_or(0) as u8;
771 let g = iter.next().copied().unwrap_or(0) as u8;
772 let b = iter.next().copied().unwrap_or(0) as u8;
773 self.current_fg = Color::Rgb(r, g, b);
774 }
775 _ => {}
776 }
777 }
778 }
779 39 => self.current_fg = Color::Default,
780 // Background colors
781 40 => self.current_bg = Color::Black,
782 41 => self.current_bg = Color::Red,
783 42 => self.current_bg = Color::Green,
784 43 => self.current_bg = Color::Yellow,
785 44 => self.current_bg = Color::Blue,
786 45 => self.current_bg = Color::Magenta,
787 46 => self.current_bg = Color::Cyan,
788 47 => self.current_bg = Color::White,
789 48 => {
790 // Extended background color
791 if let Some(&mode) = iter.next() {
792 match mode {
793 5 => {
794 if let Some(&idx) = iter.next() {
795 self.current_bg = Color::Indexed(idx as u8);
796 }
797 }
798 2 => {
799 let r = iter.next().copied().unwrap_or(0) as u8;
800 let g = iter.next().copied().unwrap_or(0) as u8;
801 let b = iter.next().copied().unwrap_or(0) as u8;
802 self.current_bg = Color::Rgb(r, g, b);
803 }
804 _ => {}
805 }
806 }
807 }
808 49 => self.current_bg = Color::Default,
809 // Bright foreground colors
810 90 => self.current_fg = Color::BrightBlack,
811 91 => self.current_fg = Color::BrightRed,
812 92 => self.current_fg = Color::BrightGreen,
813 93 => self.current_fg = Color::BrightYellow,
814 94 => self.current_fg = Color::BrightBlue,
815 95 => self.current_fg = Color::BrightMagenta,
816 96 => self.current_fg = Color::BrightCyan,
817 97 => self.current_fg = Color::BrightWhite,
818 // Bright background colors
819 100 => self.current_bg = Color::BrightBlack,
820 101 => self.current_bg = Color::BrightRed,
821 102 => self.current_bg = Color::BrightGreen,
822 103 => self.current_bg = Color::BrightYellow,
823 104 => self.current_bg = Color::BrightBlue,
824 105 => self.current_bg = Color::BrightMagenta,
825 106 => self.current_bg = Color::BrightCyan,
826 107 => self.current_bg = Color::BrightWhite,
827 _ => {}
828 }
829 }
830 }
831 _ => {}
832 }
833 }
834
835 fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {
836 match (intermediates, byte) {
837 // Save cursor position (DECSC)
838 ([], b'7') => self.save_cursor(),
839 // Restore cursor position (DECRC)
840 ([], b'8') => self.restore_cursor(),
841 // Reverse Index - move cursor up, scroll down if at top
842 ([], b'M') => self.reverse_index(),
843 // Index - move cursor down, scroll up if at bottom
844 ([], b'D') => self.index(),
845 // Next Line - move to start of next line
846 ([], b'E') => self.next_line(),
847 // Reset to Initial State (RIS)
848 ([], b'c') => {
849 // Full reset
850 self.clear_screen();
851 self.cursor_row = 0;
852 self.cursor_col = 0;
853 self.current_fg = Color::Default;
854 self.current_bg = Color::Default;
855 self.current_bold = false;
856 self.current_underline = false;
857 self.current_inverse = false;
858 self.scroll_top = 0;
859 self.scroll_bottom = self.rows.saturating_sub(1);
860 }
861 _ => {}
862 }
863 }
864 }
865