//! Minimal ANSI terminal emulator over the GOP framebuffer. //! //! `vt` owns the text screen: a cell grid (char + fg/bg colour), a logical //! cursor, scrolling, and a small CSI parser. It renders to the framebuffer //! through `gop`, blitting only cells whose content changed since the last //! flush. //! //! This is the kernel's "terminal". Plain text (shell output, typed echo) //! flows through here in *ground* state; full-screen apps (rsdosedit) write //! ANSI escape sequences that the CSI parser interprets — cursor addressing, //! erase, 16-colour SGR, cursor show/hide. //! //! Known perf note: scrolling shifts the whole framebuffer one row up (a //! ~3 MB copy at 1024×768), which dominates under QEMU without KVM. A ring //! buffer / deferred-pan approach is the future optimisation; the cell grid //! itself never re-rasterises on scroll — only the exposed bottom row is //! blitted. use crate::gop; use alloc::boxed::Box; use alloc::vec; use alloc::vec::Vec; /// Standard VGA 16-colour palette (r, g, b). pub const PALETTE: [(u8, u8, u8); 16] = [ (0, 0, 0), (0, 0, 170), (0, 170, 0), (0, 170, 170), (170, 0, 0), (170, 0, 170), (170, 85, 0), (170, 170, 170), (85, 85, 85), (85, 85, 255), (85, 255, 85), (85, 255, 255), (255, 85, 85), (255, 85, 255), (255, 255, 85), (255, 255, 255), ]; #[derive(Clone, Copy, PartialEq, Eq)] struct Cell { ch: u16, fg: u8, bg: u8, } const BLANK: Cell = Cell { ch: 32, fg: 7, bg: 0, }; // ---- screen state ---------------------------------------------------------- static mut CELLS: *mut Vec = core::ptr::null_mut(); /// What is currently on the physical framebuffer (used to diff blits). static mut RENDERED: *mut Vec = core::ptr::null_mut(); static mut ROWS: usize = 0; static mut COLS: usize = 0; // ---- cursor ---------------------------------------------------------------- static mut CURSOR_R: usize = 0; static mut CURSOR_C: usize = 0; static mut LAST_CURSOR: (usize, usize) = (0, 0); static mut CURSOR_VISIBLE: bool = true; // ---- SGR state ------------------------------------------------------------- static mut SGR_FG: u8 = 7; static mut SGR_BG: u8 = 0; // ---- ANSI parser state ----------------------------------------------------- #[derive(Clone, Copy, PartialEq, Eq)] enum Parser { Ground, Escape, Csi, } static mut PARSER: Parser = Parser::Ground; static mut PRIVATE: bool = false; static mut PARAMS: [usize; 4] = [0; 4]; static mut NPARAMS: usize = 0; static mut CUR: usize = 0; // --------------------------------------------------------------------------- // init / dimensions // --------------------------------------------------------------------------- /// Allocate the cell grid. Call once after `gop::init()`. pub fn init() { let (cols, rows) = gop::dims(); unsafe { ROWS = rows; COLS = cols; CELLS = Box::into_raw(Box::new(vec![BLANK; rows * cols])); RENDERED = Box::into_raw(Box::new(vec![BLANK; rows * cols])); CURSOR_R = 0; CURSOR_C = 0; LAST_CURSOR = (0, 0); CURSOR_VISIBLE = true; SGR_FG = 7; SGR_BG = 0; PARSER = Parser::Ground; NPARAMS = 0; PRIVATE = false; } } /// (rows, cols) — matches the editor's expected (rows, cols) ordering. pub fn size() -> (usize, usize) { unsafe { (ROWS, COLS) } } pub fn set_cursor_visible(v: bool) { unsafe { CURSOR_VISIBLE = v; } flush(); } // --------------------------------------------------------------------------- // cell access // --------------------------------------------------------------------------- fn idx(r: usize, c: usize) -> usize { r * unsafe { COLS } + c } fn cells() -> &'static mut Vec { unsafe { &mut *CELLS } } fn rendered_cells() -> &'static mut Vec { unsafe { &mut *RENDERED } } fn cell(r: usize, c: usize) -> &'static mut Cell { &mut cells()[idx(r, c)] } fn rendered(r: usize, c: usize) -> &'static mut Cell { &mut rendered_cells()[idx(r, c)] } fn put_cell(r: usize, c: usize, ch: u16) { let cell = cell(r, c); cell.ch = ch; cell.fg = unsafe { SGR_FG }; cell.bg = unsafe { SGR_BG }; } // --------------------------------------------------------------------------- // writing — the ANSI state machine // --------------------------------------------------------------------------- /// Feed one byte through the terminal state machine. fn put_byte(b: u8) { unsafe { match PARSER { Parser::Ground => match b { 0x1b => PARSER = Parser::Escape, 0x0d => CURSOR_C = 0, 0x0a => newline(), 0x08 => { // Backspace: move cursor back and erase. if CURSOR_C > 0 { CURSOR_C -= 1; } else if CURSOR_R > 0 { CURSOR_R -= 1; CURSOR_C = COLS - 1; } else { return; } let c = cell(CURSOR_R, CURSOR_C); *c = BLANK; } 0x09 => { let n = 8 - (CURSOR_C % 8); CURSOR_C = (CURSOR_C + n).min(COLS - 1); } 0x00..=0x1f => {} // other control chars: ignored _ => put_printable(b as u16), }, Parser::Escape => match b { b'[' => { PRIVATE = false; NPARAMS = 0; CUR = 0; PARSER = Parser::Csi; } _ => PARSER = Parser::Ground, // unsupported escape — drop }, Parser::Csi => match b { b'?' => PRIVATE = true, b'0'..=b'9' => { CUR = CUR.saturating_mul(10).saturating_add((b - b'0') as usize); } b';' => { if NPARAMS < PARAMS.len() { PARAMS[NPARAMS] = CUR; NPARAMS += 1; } CUR = 0; } _ => { if NPARAMS < PARAMS.len() { PARAMS[NPARAMS] = CUR; NPARAMS += 1; } PARSER = Parser::Ground; csi_final(b); } }, } } } fn param(i: usize, default: usize) -> usize { unsafe { if i < NPARAMS { PARAMS[i] } else { default } } } /// Place a printable glyph at the cursor and advance (wrapping/scrolling). /// `ch` is the character code: ASCII or a Unicode codepoint that the /// rasteriser can draw (box-drawing / arrows are rendered procedurally). fn put_printable(ch: u16) { unsafe { put_cell(CURSOR_R, CURSOR_C, ch); CURSOR_C += 1; if CURSOR_C >= COLS { CURSOR_C = 0; newline(); } } } /// Dispatch one character to the terminal: ASCII goes through the byte /// state machine (control/escape handling), printable non-ASCII goes /// straight to the glyph path (replaced by '?' when not drawable). fn write_char(c: char) { let u = c as u32; if u < 0x80 { put_byte(u as u8); } else if (0x2500..=0x257f).contains(&u) || (0x2190..=0x2193).contains(&u) || u == 0x2014 { put_printable(c as u16); } else { put_byte(b'?'); } } fn csi_final(b: u8) { match b { // Cursor position (1-based; 0 means 1) b'H' | b'f' => { let r = param(0, 1).max(1).saturating_sub(1); let c = param(1, 1).max(1).saturating_sub(1); unsafe { CURSOR_R = r.min(ROWS - 1); CURSOR_C = c.min(COLS - 1); } } // Cursor up / down / forward / back b'A' => unsafe { CURSOR_R = CURSOR_R.saturating_sub(param(0, 1).max(1)) }, b'B' => unsafe { CURSOR_R = (CURSOR_R + param(0, 1).max(1)).min(ROWS - 1) }, b'C' => unsafe { CURSOR_C = (CURSOR_C + param(0, 1).max(1)).min(COLS - 1) }, b'D' => unsafe { CURSOR_C = CURSOR_C.saturating_sub(param(0, 1).max(1)) }, // Erase in display: 0 below, 1 above, 2 all b'J' => match param(0, 0) { 0 => { erase_eol(); unsafe { for r in (CURSOR_R + 1)..ROWS { erase_row(r); } } } 1 => { unsafe { for r in 0..CURSOR_R { erase_row(r); } } erase_bol(); } _ => erase_all(), }, // Erase in line: 0 to EOL, 1 from BOL, 2 whole line b'K' => match param(0, 0) { 1 => erase_bol(), 2 => { let r = unsafe { CURSOR_R }; erase_row(r); } _ => erase_eol(), }, // SGR — colours / attributes b'm' => { let n = unsafe { NPARAMS }; if n == 0 { apply_sgr(0); } else { for i in 0..n { apply_sgr(unsafe { PARAMS[i] }); } } } // Private mode: 25 = cursor show/hide (h = show, l = hide) b'l' | b'h' if unsafe { PRIVATE } => { for i in 0..unsafe { NPARAMS } { if unsafe { PARAMS[i] } == 25 { unsafe { CURSOR_VISIBLE = b == b'h' }; } } } _ => {} // unknown / ignored } } /// ANSI SGR colour index (0–7) → VGA palette index. The two orderings /// differ: ANSI 1=red, 3=yellow, 4=blue, 6=cyan; VGA 1=blue, 3=cyan, /// 4=red, 6=brown. Terminals translate ANSI→VGA. const ANSI_TO_VGA: [u8; 8] = [0, 4, 2, 6, 1, 5, 3, 7]; fn apply_sgr(p: usize) { unsafe { match p { 0 => { SGR_FG = 7; SGR_BG = 0; } 1 => { if SGR_FG < 8 { SGR_FG += 8; // bold ≈ bright } } 30..=37 => SGR_FG = ANSI_TO_VGA[p - 30], 40..=47 => SGR_BG = ANSI_TO_VGA[p - 40], 90..=97 => SGR_FG = ANSI_TO_VGA[p - 90] + 8, 100..=107 => SGR_BG = ANSI_TO_VGA[p - 100] + 8, 39 => SGR_FG = 7, 49 => SGR_BG = 0, _ => {} } } } // --------------------------------------------------------------------------- // erase / scroll helpers // --------------------------------------------------------------------------- fn erase_eol() { let r = unsafe { CURSOR_R }; let c = unsafe { CURSOR_C }; for col in c..unsafe { COLS } { *cell(r, col) = BLANK; } } fn erase_bol() { let r = unsafe { CURSOR_R }; let c = unsafe { CURSOR_C }; for col in 0..=c { *cell(r, col) = BLANK; } } fn erase_row(r: usize) { for col in 0..unsafe { COLS } { *cell(r, col) = BLANK; } } fn erase_all() { for r in 0..unsafe { ROWS } { erase_row(r); } } /// Advance cursor to the next line (col 0), scrolling if at the bottom. fn newline() { unsafe { CURSOR_C = 0; if CURSOR_R + 1 < ROWS { CURSOR_R += 1; } else { scroll_up(); } } } /// Shift the whole screen up one row: cell grids in memory, pixels on the /// framebuffer. Only the exposed bottom row gets re-rasterised by `flush`. fn scroll_up() { let n = unsafe { ROWS * COLS }; let cols = unsafe { COLS }; cells().copy_within(cols..n, 0); rendered_cells().copy_within(cols..n, 0); for c in 0..cols { cells()[n - cols + c] = BLANK; rendered_cells()[n - cols + c] = BLANK; } gop::shift_up(); } // --------------------------------------------------------------------------- // public writing API // --------------------------------------------------------------------------- /// Write raw bytes (ANSI-capable). Used by full-screen apps. UTF-8 /// sequences are decoded so Unicode glyphs (box-drawing, arrows) render; /// undecodable bytes become '?'. pub fn write_bytes(b: &[u8]) { let mut i = 0; while i < b.len() { let byte = b[i]; if byte < 0x80 { put_byte(byte); i += 1; continue; } // Decode the next UTF-8 character starting at i. match core::str::from_utf8(&b[i..]) { Ok(s) => { let c = s.chars().next().unwrap_or('?'); write_char(c); i += c.len_utf8(); } Err(e) => { let valid = e.valid_up_to(); if valid > 0 { let s = unsafe { core::str::from_utf8_unchecked(&b[i..i + valid]) }; let c = s.chars().next().unwrap_or('?'); write_char(c); i += c.len_utf8(); } else { put_byte(b'?'); i += 1; } } } } flush(); } /// Write a UTF-8 string (plain text; Unicode glyphs render, others '?'). pub fn write_str(s: &str) { for c in s.chars() { write_char(c); } flush(); } /// Write a single ASCII byte / char. pub fn put_char(b: u8) { put_byte(b); flush(); } /// Clear the whole screen, reset colours, home the cursor. pub fn clear_all() { unsafe { SGR_FG = 7; SGR_BG = 0; erase_all(); CURSOR_R = 0; CURSOR_C = 0; } flush(); } // --------------------------------------------------------------------------- // rendering // --------------------------------------------------------------------------- /// Blit every cell whose content differs from what the framebuffer shows, /// then draw the logical cursor (inverse video) on top. The cursor's /// inverse colours are mirrored into RENDERED so that pixel shifts /// (scrolling) or full clears can never leave a stale inverse block that /// the diff can't see (blank-vs-blank). fn flush() { let rows = unsafe { ROWS }; let cols = unsafe { COLS }; let n = rows * cols; let cursor_visible = unsafe { CURSOR_VISIBLE }; let (cr, cc) = unsafe { (CURSOR_R, CURSOR_C) }; { let cells = cells(); let rendered = rendered_cells(); for i in 0..n { let row = i / cols; let col = i % cols; // The visible cursor cell is owned by the cursor pass below. if cursor_visible && row == cr && col == cc { continue; } let c = cells[i]; let r = rendered[i]; if c != r { let fg = PALETTE[c.fg as usize]; let bg = PALETTE[c.bg as usize]; gop::blit_glyph(col, row, c.ch, fg, bg); rendered[i] = c; } } } unsafe { let (lr, lc) = LAST_CURSOR; let moved = (lr, lc) != (CURSOR_R, CURSOR_C); // Erase the previous cursor position (restore its normal colours) // and record it in RENDERED so the diff stays in sync. if (moved || !CURSOR_VISIBLE) && lr < rows && lc < cols { let i = idx(lr, lc); let c = cells()[i]; let fg = PALETTE[c.fg as usize]; let bg = PALETTE[c.bg as usize]; gop::blit_glyph(lc, lr, c.ch, fg, bg); rendered_cells()[i] = c; } LAST_CURSOR = (CURSOR_R, CURSOR_C); // Draw the cursor as an inverse-video cell, mirroring the inverse // colours into RENDERED (scrolling/clearing then repair it). if CURSOR_VISIBLE && CURSOR_R < rows && CURSOR_C < cols { let i = idx(CURSOR_R, CURSOR_C); let c = cells()[i]; let fg = PALETTE[c.fg as usize]; let bg = PALETTE[c.bg as usize]; gop::blit_glyph(CURSOR_C, CURSOR_R, c.ch, bg, fg); rendered_cells()[i] = Cell { ch: c.ch, fg: c.bg, bg: c.fg, }; } } }