use std::collections::VecDeque; use crate::attr::{CellAttributes, Color}; #[derive(Clone, Copy, Debug)] pub struct GridCell { pub char: char, pub attrs: CellAttributes, } #[derive(Clone)] pub struct GridRow { cols: Vec, dirty: bool, } pub struct Buffer { scrollback: VecDeque, rows: Vec, width: usize, height: usize, scrollback_limit: usize, } #[derive(Clone, Copy, PartialEq, Eq)] pub struct Cursor { pub row: usize, pub col: usize, } enum EscapeState { Normal, Escape, Csi, } pub struct State { pub buffer: Buffer, esc_state: EscapeState, esc_args: Vec, scroll: usize, pub cursor: Cursor, #[allow(unused)] saved_cursor: Option, pub default_attributes: CellAttributes, pub attributes: CellAttributes, } impl GridCell { pub fn new(char: char, attrs: CellAttributes) -> Self { Self { char, attrs } } pub fn empty(bg: Color) -> Self { Self { char: '\0', attrs: CellAttributes { fg: Color::Black, bg, bright: false, }, } } } impl GridRow { pub fn new(width: usize, bg: Color) -> Self { Self { cols: vec![GridCell::empty(bg); width], dirty: true, } } pub fn set_cell(&mut self, col: usize, char: char, attrs: CellAttributes) { self.cols[col] = GridCell::new(char, attrs); self.dirty = true; } pub fn is_dirty(&self) -> bool { self.dirty } pub fn clear_dirty(&mut self) { self.dirty = false; } pub fn cells(&self) -> impl Iterator { self.cols.iter() } fn clear(&mut self, bg: Color) { self.cols.fill(GridCell::empty(bg)); self.dirty = true; } fn erase_to_right(&mut self, start: usize, bg: Color) { self.cols[start..].fill(GridCell::empty(bg)); self.dirty = true; } fn resize(&mut self, width: usize, bg: Color) { self.cols.resize(width, GridCell::empty(bg)); self.dirty = true; } } impl Buffer { pub fn new(width: usize, height: usize, bg: Color) -> Self { Self { rows: vec![GridRow::new(width, bg); height], scrollback: VecDeque::new(), scrollback_limit: 1024, width, height, } } pub fn clear(&mut self, bg: Color) { self.rows.fill(GridRow::new(self.width, bg)); } pub fn iter_rows_mut(&mut self, scroll: usize, mut handler: F) { let scroll = scroll.min(self.scrollback.len()); let non_scroll = self.height.saturating_sub(scroll); let scroll_end = self.height - non_scroll; for y in 0..scroll_end { let i = scroll - 1 - y; let row = &mut self.scrollback[i]; handler(y, row); } for i in 0..non_scroll { let y = scroll + i; let row = &mut self.rows[i]; handler(y, row); } } pub fn visible_rows_mut(&mut self, scroll: usize, mut handler: F) { self.iter_rows_mut(scroll, |y, row| { if row.dirty { row.dirty = false; handler(y, row); } }); } pub fn resize(&mut self, width: usize, height: usize, bg: Color) { self.rows.resize(height, GridRow::new(width, bg)); for row in self.rows.iter_mut() { row.resize(width, bg); } self.width = width; self.height = height; } pub fn set_cell(&mut self, cur: Cursor, cell: GridCell) { self.rows[cur.row].cols[cur.col] = cell; self.rows[cur.row].dirty = true; } pub fn scroll_once(&mut self, bg: Color) { self.scrollback.push_front(self.rows[0].clone()); if self.scrollback.len() >= self.scrollback_limit { self.scrollback.pop_back(); } for i in 1..self.height { self.rows[i - 1] = self.rows[i].clone(); self.rows[i - 1].dirty = true; } self.rows[self.height - 1] = GridRow::new(self.width, bg); } pub fn erase_row(&mut self, row: usize, bg: Color) { self.rows[row].clear(bg); } pub fn set_row_dirty(&mut self, row: usize) { if row >= self.rows.len() { return; } self.rows[row].dirty = true; } pub fn invalidate_rows(&mut self, scroll: usize) { self.iter_rows_mut(scroll, |_, row| row.dirty = true); } } impl State { pub fn new(width: usize, height: usize) -> Self { let default_attributes = CellAttributes { fg: Color::White, bg: Color::Black, bright: false, }; Self { buffer: Buffer::new(width, height, default_attributes.bg), esc_args: Vec::new(), esc_state: EscapeState::Normal, cursor: Cursor { row: 0, col: 0 }, scroll: 0, saved_cursor: None, default_attributes, attributes: default_attributes, } } pub fn resize(&mut self, width: usize, height: usize) { self.buffer .resize(width, height, self.default_attributes.bg); self.scroll = 0; if self.cursor.row >= height { self.cursor.row = height - 1; } if self.cursor.col >= width { self.cursor.col = width - 1; } } fn putc_normal(&mut self, ch: u8) -> bool { let mut redraw = match ch { c if c >= 127 => { let attr = CellAttributes { fg: Color::Black, bg: Color::Red, bright: false, }; self.buffer.set_cell(self.cursor, GridCell::new('?', attr)); self.cursor.col += 1; true } b'\x1B' => { self.esc_state = EscapeState::Escape; self.esc_args.clear(); self.esc_args.push(0); return false; } b'\r' => { self.buffer.rows[self.cursor.row].dirty = true; self.cursor.col = 0; true } b'\n' => { self.buffer.rows[self.cursor.row].dirty = true; self.cursor.row += 1; self.cursor.col = 0; true } _ => { self.buffer .set_cell(self.cursor, GridCell::new(ch as char, self.attributes)); self.cursor.col += 1; true } }; if self.cursor.col >= self.buffer.width { if self.cursor.row < self.buffer.height { self.buffer.rows[self.cursor.row].dirty = true; } self.cursor.row += 1; self.cursor.col = 0; redraw = true; } while self.cursor.row >= self.buffer.height { self.buffer.scroll_once(self.default_attributes.bg); self.cursor.row -= 1; redraw = true; } redraw } fn handle_ctlseq(&mut self, c: u8) -> bool { let redraw = match c { // Move back one character b'D' => { if self.cursor.col > 0 { self.buffer.set_row_dirty(self.cursor.row); self.cursor.col -= 1; true } else { false } } // Character attributes b'm' => match self.esc_args[0] { 0 => { self.attributes = self.default_attributes; false } 1 => { self.attributes.bright = true; false } 30..=39 => { let vt_color = self.esc_args[0] % 10; if vt_color == 9 { self.attributes.fg = Color::Black; } else { self.attributes.fg = Color::from_esc(vt_color); } false } 40..=49 => { let vt_color = self.esc_args[0] % 10; if vt_color == 9 { self.attributes.bg = Color::Black; } else { self.attributes.bg = Color::from_esc(vt_color); } false } _ => false, }, // Move cursor to position b'f' => { let row = self.esc_args[0].clamp(1, self.buffer.height as u32) - 1; let col = self.esc_args[1].clamp(1, self.buffer.width as u32) - 1; self.buffer.set_row_dirty(self.cursor.row); self.cursor = Cursor { row: row as _, col: col as _, }; true } // Clear rows/columns/screen b'J' => match self.esc_args[0] { // Erase lines down 0 => false, // Erase lines up 1 => false, // Erase all 2 => { self.buffer.clear(self.attributes.bg); true } _ => false, }, b'K' => match self.esc_args[0] { // Erase to right 0 => { self.buffer.rows[self.cursor.row] .erase_to_right(self.cursor.col, self.attributes.bg); true } // Erase All 2 => { self.buffer.erase_row(self.cursor.row, self.attributes.bg); true } _ => false, }, _ => false, }; self.esc_state = EscapeState::Normal; redraw } fn handle_ctlseq_byte(&mut self, c: u8) -> bool { match c { b'0'..=b'9' => { let arg = self.esc_args.last_mut().unwrap(); *arg *= 10; *arg += (c - b'0') as u32; false } b';' => { self.esc_args.push(0); false } _ => self.handle_ctlseq(c), } } pub fn handle_shell_output(&mut self, ch: u8) -> bool { match self.esc_state { EscapeState::Normal => self.putc_normal(ch), EscapeState::Escape => match ch { b'[' => { self.esc_state = EscapeState::Csi; false } _ => { self.esc_state = EscapeState::Normal; false } }, EscapeState::Csi => self.handle_ctlseq_byte(ch), } } pub fn invalidate_current_viewport(&mut self) { self.buffer.invalidate_rows(self.scroll); } pub fn adjust_scroll(&mut self) -> usize { if self.scroll > self.buffer.scrollback.len() { self.scroll = self.buffer.scrollback.len(); } self.scroll } pub fn scroll_up(&mut self) -> bool { let max = self.buffer.scrollback.len(); if max == 0 { self.scroll = 0; return true; } let amount = max.min(8); if amount != 0 { self.scroll += amount; self.invalidate_current_viewport(); true } else { false } } pub fn scroll_down(&mut self) -> bool { let amount = self.scroll.min(8); if amount != 0 { self.scroll -= amount; self.invalidate_current_viewport(); true } else { false } } pub fn scroll_home(&mut self) -> bool { if self.scroll != self.buffer.scrollback.len() { self.scroll = self.buffer.scrollback.len(); self.invalidate_current_viewport(); true } else { false } } pub fn scroll_end(&mut self) -> bool { if self.scroll != 0 { self.scroll = 0; self.invalidate_current_viewport(); true } else { false } } }