@@ -0,0 +1,450 @@ |
| | 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 | +} |
| | 82 | + |
| | 83 | +impl TerminalScreen { |
| | 84 | + pub fn new(cols: u16, rows: u16) -> Self { |
| | 85 | + let cells = vec![vec![Cell::default(); cols as usize]; rows as usize]; |
| | 86 | + Self { |
| | 87 | + cells, |
| | 88 | + cols, |
| | 89 | + rows, |
| | 90 | + cursor_row: 0, |
| | 91 | + cursor_col: 0, |
| | 92 | + current_fg: Color::Default, |
| | 93 | + current_bg: Color::Default, |
| | 94 | + current_bold: false, |
| | 95 | + current_underline: false, |
| | 96 | + current_inverse: false, |
| | 97 | + parser: Parser::new(), |
| | 98 | + scrollback: Vec::new(), |
| | 99 | + max_scrollback: 10000, |
| | 100 | + scroll_offset: 0, |
| | 101 | + } |
| | 102 | + } |
| | 103 | + |
| | 104 | + /// Process raw bytes from the PTY |
| | 105 | + pub fn process(&mut self, data: &[u8]) { |
| | 106 | + // Take parser out temporarily to avoid borrow conflict |
| | 107 | + let mut parser = std::mem::take(&mut self.parser); |
| | 108 | + for byte in data { |
| | 109 | + parser.advance(self, *byte); |
| | 110 | + } |
| | 111 | + self.parser = parser; |
| | 112 | + } |
| | 113 | + |
| | 114 | + /// Get a reference to the cell grid |
| | 115 | + pub fn cells(&self) -> &Vec<Vec<Cell>> { |
| | 116 | + &self.cells |
| | 117 | + } |
| | 118 | + |
| | 119 | + /// Get a row from scrollback or current screen |
| | 120 | + pub fn get_row(&self, row: usize) -> Option<&Vec<Cell>> { |
| | 121 | + if self.scroll_offset > 0 { |
| | 122 | + let scrollback_row = self.scrollback.len().saturating_sub(self.scroll_offset) + row; |
| | 123 | + if scrollback_row < self.scrollback.len() { |
| | 124 | + self.scrollback.get(scrollback_row) |
| | 125 | + } else { |
| | 126 | + let screen_row = scrollback_row - self.scrollback.len(); |
| | 127 | + self.cells.get(screen_row) |
| | 128 | + } |
| | 129 | + } else { |
| | 130 | + self.cells.get(row) |
| | 131 | + } |
| | 132 | + } |
| | 133 | + |
| | 134 | + /// Resize the terminal |
| | 135 | + pub fn resize(&mut self, cols: u16, rows: u16) { |
| | 136 | + // Create new cell grid |
| | 137 | + let mut new_cells = vec![vec![Cell::default(); cols as usize]; rows as usize]; |
| | 138 | + |
| | 139 | + // Copy existing content |
| | 140 | + for (r, row) in self.cells.iter().enumerate() { |
| | 141 | + if r >= rows as usize { |
| | 142 | + break; |
| | 143 | + } |
| | 144 | + for (c, cell) in row.iter().enumerate() { |
| | 145 | + if c >= cols as usize { |
| | 146 | + break; |
| | 147 | + } |
| | 148 | + new_cells[r][c] = cell.clone(); |
| | 149 | + } |
| | 150 | + } |
| | 151 | + |
| | 152 | + self.cells = new_cells; |
| | 153 | + self.cols = cols; |
| | 154 | + self.rows = rows; |
| | 155 | + |
| | 156 | + // Ensure cursor is within bounds |
| | 157 | + self.cursor_row = self.cursor_row.min(rows.saturating_sub(1)); |
| | 158 | + self.cursor_col = self.cursor_col.min(cols.saturating_sub(1)); |
| | 159 | + } |
| | 160 | + |
| | 161 | + /// Scroll the screen up by one line |
| | 162 | + fn scroll_up(&mut self) { |
| | 163 | + if !self.cells.is_empty() { |
| | 164 | + // Move top row to scrollback |
| | 165 | + let top_row = self.cells.remove(0); |
| | 166 | + self.scrollback.push(top_row); |
| | 167 | + |
| | 168 | + // Trim scrollback if too large |
| | 169 | + if self.scrollback.len() > self.max_scrollback { |
| | 170 | + self.scrollback.remove(0); |
| | 171 | + } |
| | 172 | + |
| | 173 | + // Add new empty row at bottom |
| | 174 | + self.cells.push(vec![Cell::default(); self.cols as usize]); |
| | 175 | + } |
| | 176 | + } |
| | 177 | + |
| | 178 | + /// Clear the screen |
| | 179 | + fn clear_screen(&mut self) { |
| | 180 | + for row in &mut self.cells { |
| | 181 | + for cell in row { |
| | 182 | + *cell = Cell::default(); |
| | 183 | + } |
| | 184 | + } |
| | 185 | + } |
| | 186 | + |
| | 187 | + /// Clear from cursor to end of line |
| | 188 | + fn clear_to_eol(&mut self) { |
| | 189 | + if let Some(row) = self.cells.get_mut(self.cursor_row as usize) { |
| | 190 | + for cell in row.iter_mut().skip(self.cursor_col as usize) { |
| | 191 | + *cell = Cell::default(); |
| | 192 | + } |
| | 193 | + } |
| | 194 | + } |
| | 195 | + |
| | 196 | + /// Clear from cursor to end of screen |
| | 197 | + fn clear_to_eos(&mut self) { |
| | 198 | + self.clear_to_eol(); |
| | 199 | + for row in self.cells.iter_mut().skip(self.cursor_row as usize + 1) { |
| | 200 | + for cell in row { |
| | 201 | + *cell = Cell::default(); |
| | 202 | + } |
| | 203 | + } |
| | 204 | + } |
| | 205 | + |
| | 206 | + /// Put a character at the cursor position |
| | 207 | + fn put_char(&mut self, c: char) { |
| | 208 | + if self.cursor_row < self.rows && self.cursor_col < self.cols { |
| | 209 | + if let Some(row) = self.cells.get_mut(self.cursor_row as usize) { |
| | 210 | + if let Some(cell) = row.get_mut(self.cursor_col as usize) { |
| | 211 | + cell.c = c; |
| | 212 | + cell.fg = self.current_fg; |
| | 213 | + cell.bg = self.current_bg; |
| | 214 | + cell.bold = self.current_bold; |
| | 215 | + cell.underline = self.current_underline; |
| | 216 | + cell.inverse = self.current_inverse; |
| | 217 | + } |
| | 218 | + } |
| | 219 | + } |
| | 220 | + } |
| | 221 | +} |
| | 222 | + |
| | 223 | +/// VTE Perform implementation for processing escape sequences |
| | 224 | +impl Perform for TerminalScreen { |
| | 225 | + fn print(&mut self, c: char) { |
| | 226 | + self.put_char(c); |
| | 227 | + self.cursor_col += 1; |
| | 228 | + |
| | 229 | + // Handle line wrap |
| | 230 | + if self.cursor_col >= self.cols { |
| | 231 | + self.cursor_col = 0; |
| | 232 | + self.cursor_row += 1; |
| | 233 | + if self.cursor_row >= self.rows { |
| | 234 | + self.scroll_up(); |
| | 235 | + self.cursor_row = self.rows - 1; |
| | 236 | + } |
| | 237 | + } |
| | 238 | + } |
| | 239 | + |
| | 240 | + fn execute(&mut self, byte: u8) { |
| | 241 | + match byte { |
| | 242 | + // Backspace |
| | 243 | + 0x08 => { |
| | 244 | + self.cursor_col = self.cursor_col.saturating_sub(1); |
| | 245 | + } |
| | 246 | + // Tab |
| | 247 | + 0x09 => { |
| | 248 | + self.cursor_col = ((self.cursor_col / 8) + 1) * 8; |
| | 249 | + if self.cursor_col >= self.cols { |
| | 250 | + self.cursor_col = self.cols - 1; |
| | 251 | + } |
| | 252 | + } |
| | 253 | + // Line feed |
| | 254 | + 0x0A => { |
| | 255 | + self.cursor_row += 1; |
| | 256 | + if self.cursor_row >= self.rows { |
| | 257 | + self.scroll_up(); |
| | 258 | + self.cursor_row = self.rows - 1; |
| | 259 | + } |
| | 260 | + } |
| | 261 | + // Carriage return |
| | 262 | + 0x0D => { |
| | 263 | + self.cursor_col = 0; |
| | 264 | + } |
| | 265 | + _ => {} |
| | 266 | + } |
| | 267 | + } |
| | 268 | + |
| | 269 | + fn hook(&mut self, _params: &Params, _intermediates: &[u8], _ignore: bool, _action: char) {} |
| | 270 | + |
| | 271 | + fn put(&mut self, _byte: u8) {} |
| | 272 | + |
| | 273 | + fn unhook(&mut self) {} |
| | 274 | + |
| | 275 | + fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) {} |
| | 276 | + |
| | 277 | + fn csi_dispatch(&mut self, params: &Params, _intermediates: &[u8], _ignore: bool, action: char) { |
| | 278 | + let params: Vec<u16> = params.iter().map(|p| p.first().copied().unwrap_or(0) as u16).collect(); |
| | 279 | + |
| | 280 | + match action { |
| | 281 | + // Cursor Up |
| | 282 | + 'A' => { |
| | 283 | + let n = params.first().copied().unwrap_or(1).max(1); |
| | 284 | + self.cursor_row = self.cursor_row.saturating_sub(n); |
| | 285 | + } |
| | 286 | + // Cursor Down |
| | 287 | + 'B' => { |
| | 288 | + let n = params.first().copied().unwrap_or(1).max(1); |
| | 289 | + self.cursor_row = (self.cursor_row + n).min(self.rows - 1); |
| | 290 | + } |
| | 291 | + // Cursor Forward |
| | 292 | + 'C' => { |
| | 293 | + let n = params.first().copied().unwrap_or(1).max(1); |
| | 294 | + self.cursor_col = (self.cursor_col + n).min(self.cols - 1); |
| | 295 | + } |
| | 296 | + // Cursor Back |
| | 297 | + 'D' => { |
| | 298 | + let n = params.first().copied().unwrap_or(1).max(1); |
| | 299 | + self.cursor_col = self.cursor_col.saturating_sub(n); |
| | 300 | + } |
| | 301 | + // Cursor Position (CUP) |
| | 302 | + 'H' | 'f' => { |
| | 303 | + let row = params.first().copied().unwrap_or(1).max(1) - 1; |
| | 304 | + let col = params.get(1).copied().unwrap_or(1).max(1) - 1; |
| | 305 | + self.cursor_row = row.min(self.rows - 1); |
| | 306 | + self.cursor_col = col.min(self.cols - 1); |
| | 307 | + } |
| | 308 | + // Erase in Display |
| | 309 | + 'J' => { |
| | 310 | + let mode = params.first().copied().unwrap_or(0); |
| | 311 | + match mode { |
| | 312 | + 0 => self.clear_to_eos(), |
| | 313 | + 1 => {} // Clear from start to cursor (TODO) |
| | 314 | + 2 | 3 => self.clear_screen(), |
| | 315 | + _ => {} |
| | 316 | + } |
| | 317 | + } |
| | 318 | + // Erase in Line |
| | 319 | + 'K' => { |
| | 320 | + let mode = params.first().copied().unwrap_or(0); |
| | 321 | + match mode { |
| | 322 | + 0 => self.clear_to_eol(), |
| | 323 | + 1 => {} // Clear from start of line to cursor (TODO) |
| | 324 | + 2 => { |
| | 325 | + // Clear entire line |
| | 326 | + if let Some(row) = self.cells.get_mut(self.cursor_row as usize) { |
| | 327 | + for cell in row { |
| | 328 | + *cell = Cell::default(); |
| | 329 | + } |
| | 330 | + } |
| | 331 | + } |
| | 332 | + _ => {} |
| | 333 | + } |
| | 334 | + } |
| | 335 | + // Select Graphic Rendition (SGR) - colors and attributes |
| | 336 | + 'm' => { |
| | 337 | + if params.is_empty() { |
| | 338 | + // Reset all attributes |
| | 339 | + self.current_fg = Color::Default; |
| | 340 | + self.current_bg = Color::Default; |
| | 341 | + self.current_bold = false; |
| | 342 | + self.current_underline = false; |
| | 343 | + self.current_inverse = false; |
| | 344 | + return; |
| | 345 | + } |
| | 346 | + |
| | 347 | + let mut iter = params.iter().peekable(); |
| | 348 | + while let Some(¶m) = iter.next() { |
| | 349 | + match param { |
| | 350 | + 0 => { |
| | 351 | + self.current_fg = Color::Default; |
| | 352 | + self.current_bg = Color::Default; |
| | 353 | + self.current_bold = false; |
| | 354 | + self.current_underline = false; |
| | 355 | + self.current_inverse = false; |
| | 356 | + } |
| | 357 | + 1 => self.current_bold = true, |
| | 358 | + 4 => self.current_underline = true, |
| | 359 | + 7 => self.current_inverse = true, |
| | 360 | + 22 => self.current_bold = false, |
| | 361 | + 24 => self.current_underline = false, |
| | 362 | + 27 => self.current_inverse = false, |
| | 363 | + // Foreground colors |
| | 364 | + 30 => self.current_fg = Color::Black, |
| | 365 | + 31 => self.current_fg = Color::Red, |
| | 366 | + 32 => self.current_fg = Color::Green, |
| | 367 | + 33 => self.current_fg = Color::Yellow, |
| | 368 | + 34 => self.current_fg = Color::Blue, |
| | 369 | + 35 => self.current_fg = Color::Magenta, |
| | 370 | + 36 => self.current_fg = Color::Cyan, |
| | 371 | + 37 => self.current_fg = Color::White, |
| | 372 | + 38 => { |
| | 373 | + // Extended foreground color |
| | 374 | + if let Some(&mode) = iter.next() { |
| | 375 | + match mode { |
| | 376 | + 5 => { |
| | 377 | + // 256-color mode |
| | 378 | + if let Some(&idx) = iter.next() { |
| | 379 | + self.current_fg = Color::Indexed(idx as u8); |
| | 380 | + } |
| | 381 | + } |
| | 382 | + 2 => { |
| | 383 | + // RGB mode |
| | 384 | + let r = iter.next().copied().unwrap_or(0) as u8; |
| | 385 | + let g = iter.next().copied().unwrap_or(0) as u8; |
| | 386 | + let b = iter.next().copied().unwrap_or(0) as u8; |
| | 387 | + self.current_fg = Color::Rgb(r, g, b); |
| | 388 | + } |
| | 389 | + _ => {} |
| | 390 | + } |
| | 391 | + } |
| | 392 | + } |
| | 393 | + 39 => self.current_fg = Color::Default, |
| | 394 | + // Background colors |
| | 395 | + 40 => self.current_bg = Color::Black, |
| | 396 | + 41 => self.current_bg = Color::Red, |
| | 397 | + 42 => self.current_bg = Color::Green, |
| | 398 | + 43 => self.current_bg = Color::Yellow, |
| | 399 | + 44 => self.current_bg = Color::Blue, |
| | 400 | + 45 => self.current_bg = Color::Magenta, |
| | 401 | + 46 => self.current_bg = Color::Cyan, |
| | 402 | + 47 => self.current_bg = Color::White, |
| | 403 | + 48 => { |
| | 404 | + // Extended background color |
| | 405 | + if let Some(&mode) = iter.next() { |
| | 406 | + match mode { |
| | 407 | + 5 => { |
| | 408 | + if let Some(&idx) = iter.next() { |
| | 409 | + self.current_bg = Color::Indexed(idx as u8); |
| | 410 | + } |
| | 411 | + } |
| | 412 | + 2 => { |
| | 413 | + let r = iter.next().copied().unwrap_or(0) as u8; |
| | 414 | + let g = iter.next().copied().unwrap_or(0) as u8; |
| | 415 | + let b = iter.next().copied().unwrap_or(0) as u8; |
| | 416 | + self.current_bg = Color::Rgb(r, g, b); |
| | 417 | + } |
| | 418 | + _ => {} |
| | 419 | + } |
| | 420 | + } |
| | 421 | + } |
| | 422 | + 49 => self.current_bg = Color::Default, |
| | 423 | + // Bright foreground colors |
| | 424 | + 90 => self.current_fg = Color::BrightBlack, |
| | 425 | + 91 => self.current_fg = Color::BrightRed, |
| | 426 | + 92 => self.current_fg = Color::BrightGreen, |
| | 427 | + 93 => self.current_fg = Color::BrightYellow, |
| | 428 | + 94 => self.current_fg = Color::BrightBlue, |
| | 429 | + 95 => self.current_fg = Color::BrightMagenta, |
| | 430 | + 96 => self.current_fg = Color::BrightCyan, |
| | 431 | + 97 => self.current_fg = Color::BrightWhite, |
| | 432 | + // Bright background colors |
| | 433 | + 100 => self.current_bg = Color::BrightBlack, |
| | 434 | + 101 => self.current_bg = Color::BrightRed, |
| | 435 | + 102 => self.current_bg = Color::BrightGreen, |
| | 436 | + 103 => self.current_bg = Color::BrightYellow, |
| | 437 | + 104 => self.current_bg = Color::BrightBlue, |
| | 438 | + 105 => self.current_bg = Color::BrightMagenta, |
| | 439 | + 106 => self.current_bg = Color::BrightCyan, |
| | 440 | + 107 => self.current_bg = Color::BrightWhite, |
| | 441 | + _ => {} |
| | 442 | + } |
| | 443 | + } |
| | 444 | + } |
| | 445 | + _ => {} |
| | 446 | + } |
| | 447 | + } |
| | 448 | + |
| | 449 | + fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) {} |
| | 450 | +} |