//! Minimal ANSI terminal emulator, now per-instance. //! //! A `Terminal` owns a cell grid (char + fg/bg colour), a logical cursor, //! scrolling, and a small CSI parser, and renders into a caller-owned pixel //! buffer (the terminal window's framebuffer). Only cells whose content //! changed since the last flush are re-blitted. //! //! 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. use crate::gop; 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, }; #[derive(Clone, Copy, PartialEq, Eq)] enum Parser { Ground, Escape, Csi, } /// ANSI SGR colour index (0–7) → VGA palette index. const ANSI_TO_VGA: [u8; 8] = [0, 4, 2, 6, 1, 5, 3, 7]; const SCROLL_BATCH: usize = 16; const TICK_LIMIT: u64 = 4; /// One text terminal: a cell grid + cursor + ANSI parser, rendered into a /// pixel buffer (`w`×`h`, row-major `u32`). pub struct Terminal { cells: Vec, rendered: Vec, rows: usize, cols: usize, cursor_r: usize, cursor_c: usize, last_cursor: (usize, usize), cursor_visible: bool, sgr_fg: u8, sgr_bg: u8, parser: Parser, private: bool, params: [usize; 4], nparams: usize, cur: usize, pending_scrolls: usize, last_sync_tick: u64, dirty_all: bool, buffer: Vec, ext_buf: Option<*mut u32>, w: usize, h: usize, window_id: Option, } fn union_rect( d: &mut Option<(usize, usize, usize, usize)>, x: usize, y: usize, w: usize, h: usize, ) { *d = match *d { Some((dx, dy, dw, dh)) => { let nx = dx.min(x); let ny = dy.min(y); let nx2 = (dx + dw).max(x + w); let ny2 = (dy + dh).max(y + h); Some((nx, ny, nx2 - nx, ny2 - ny)) } None => Some((x, y, w, h)), }; } impl Terminal { /// Build a terminal with a fresh `w`×`h` pixel buffer. pub fn new(w: usize, h: usize) -> Terminal { let cols = w / 8; let rows = h / 16; Terminal { cells: vec![BLANK; rows * cols], rendered: vec![BLANK; rows * cols], rows, cols, cursor_r: 1, cursor_c: 0, last_cursor: (1, 0), cursor_visible: true, sgr_fg: 7, sgr_bg: 0, parser: Parser::Ground, private: false, params: [0; 4], nparams: 0, cur: 0, pending_scrolls: 0, last_sync_tick: 0, dirty_all: true, buffer: vec![0; w * h], ext_buf: None, w, h, window_id: None, } } /// Build a terminal that renders into an externally-owned pixel buffer /// (e.g. one allocated by the window manager). The caller must free /// the buffer when the window is destroyed. pub fn from_buffer(buf: *mut u32, w: usize, h: usize) -> Terminal { let cols = w / 8; let rows = h / 16; Terminal { cells: vec![BLANK; rows * cols], rendered: vec![BLANK; rows * cols], rows, cols, cursor_r: 1, cursor_c: 0, last_cursor: (1, 0), cursor_visible: true, sgr_fg: 7, sgr_bg: 0, parser: Parser::Ground, private: false, params: [0; 4], nparams: 0, cur: 0, pending_scrolls: 0, last_sync_tick: 0, dirty_all: true, buffer: Vec::new(), ext_buf: Some(buf), w, h, window_id: None, } } /// Raw pointer to the pixel buffer (owned or external). fn buf_ptr(&mut self) -> *mut u32 { if let Some(p) = self.ext_buf { p } else { self.buffer.as_mut_ptr() } } /// Attach this terminal to a WM window (so `flush` composites it). pub fn set_window(&mut self, id: usize) { self.window_id = Some(id); } /// Buffer pointer and pixel dimensions (for the WM to composite). pub fn window_params(&mut self) -> (*const u32, usize, usize) { (self.buf_ptr() as *const u32, self.w, self.h) } pub fn ready(&self) -> bool { self.rows != 0 } pub fn size(&self) -> (usize, usize) { (self.rows, self.cols) } pub fn rows(&self) -> usize { self.rows } pub fn cols(&self) -> usize { self.cols } /// Raw (ch|fg<<16|bg<<24) values of a cell and its rendered mirror, for /// the `heapdbg` diagnostic. pub fn cell_raw(&self, r: usize, c: usize) -> (u32, u32) { if r >= self.rows || c >= self.cols { return (0xFFFF_FFFF, 0xFFFF_FFFF); } let raw = |cell: &Cell| -> u32 { cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24) }; let i = self.idx(r, c); (raw(&self.cells[i]), raw(&self.rendered[i])) } pub fn set_cursor_visible(&mut self, v: bool) { self.sync_scroll(); self.cursor_visible = v; self.flush(); } /// Push deferred scrolls: shift the pixel buffer, mirror the rendered /// grid, and re-blit the exposed rows. pub fn sync_scroll(&mut self) { let n = self.pending_scrolls; if n == 0 { return; } let rows = self.rows; let cols = self.cols; gop::buffer_shift_up(self.buf_ptr(), self.w, self.h, n * 16); // Mirror the pushed content in RENDERED (it describes the buffer). let total = rows * cols; self.rendered.copy_within(n * cols..total, 0); for i in (total - n * cols)..total { self.rendered[i] = BLANK; } self.pending_scrolls = 0; self.last_sync_tick = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed); self.dirty_all = true; self.flush(); } /// Re-blit a region of this terminal's buffer into the shadow (used by /// the window manager to restore the console layer beneath a window). pub fn repaint_region(&mut self, x: usize, y: usize, w: usize, h: usize) { let src = unsafe { self.buf_ptr().add(y * self.w + x) }; gop::shadow_blit(x, y, w, h, src, self.w); } /// Write raw bytes (ANSI-capable), decoding UTF-8 for drawable glyphs. pub fn write_bytes(&mut self, b: &[u8]) { let on = irq_off(); let mut i = 0; while i < b.len() { let byte = b[i]; if byte < 0x80 { self.put_byte(byte); i += 1; continue; } match core::str::from_utf8(&b[i..]) { Ok(s) => { let c = s.chars().next().unwrap_or('?'); self.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('?'); self.write_char(c); i += c.len_utf8(); } else { self.put_byte(b'?'); i += 1; } } } } self.flush(); irq_restore(on); } /// Write a UTF-8 string (plain text; Unicode glyphs render, others '?'). pub fn write_str(&mut self, s: &str) { let on = irq_off(); for c in s.chars() { self.write_char(c); } self.flush(); irq_restore(on); } /// Write a single ASCII byte / char. pub fn put_char(&mut self, b: u8) { let on = irq_off(); self.put_byte(b); self.flush(); irq_restore(on); } /// Clear the whole screen, reset colours, home the cursor. pub fn clear_all(&mut self) { self.sgr_fg = 7; self.sgr_bg = 0; self.sync_scroll(); self.erase_all(); self.cursor_r = 0; self.cursor_c = 0; self.flush(); } // -- internals ---------------------------------------------------------- fn idx(&self, r: usize, c: usize) -> usize { r * self.cols + c } fn put_cell(&mut self, r: usize, c: usize, ch: u16) { let i = self.idx(r, c); self.cells[i].ch = ch; self.cells[i].fg = self.sgr_fg; self.cells[i].bg = self.sgr_bg; } fn put_byte(&mut self, b: u8) { match self.parser { Parser::Ground => match b { 0x1b => self.parser = Parser::Escape, 0x0d => self.cursor_c = 0, 0x0a => self.newline(), 0x08 => { if self.cursor_c > 0 { self.cursor_c -= 1; } else if self.cursor_r > 0 { self.cursor_r -= 1; self.cursor_c = self.cols - 1; } else { return; } let i = self.idx(self.cursor_r, self.cursor_c); self.cells[i] = BLANK; } 0x09 => { let n = 8 - (self.cursor_c % 8); self.cursor_c = (self.cursor_c + n).min(self.cols - 1); } 0x00..=0x1f => {} _ => self.put_printable(b as u16), }, Parser::Escape => match b { b'[' => { self.private = false; self.nparams = 0; self.cur = 0; self.parser = Parser::Csi; } _ => self.parser = Parser::Ground, }, Parser::Csi => match b { b'?' => self.private = true, b'0'..=b'9' => { self.cur = self .cur .saturating_mul(10) .saturating_add((b - b'0') as usize); } b';' => { if self.nparams < self.params.len() { self.params[self.nparams] = self.cur; self.nparams += 1; } self.cur = 0; } _ => { if self.nparams < self.params.len() { self.params[self.nparams] = self.cur; self.nparams += 1; } self.parser = Parser::Ground; self.csi_final(b); } }, } } fn param(&self, i: usize, default: usize) -> usize { if i < self.nparams { self.params[i] } else { default } } fn put_printable(&mut self, ch: u16) { self.put_cell(self.cursor_r, self.cursor_c, ch); self.cursor_c += 1; if self.cursor_c >= self.cols { self.cursor_c = 0; self.newline(); } } fn write_char(&mut self, c: char) { let u = c as u32; if u < 0x80 { self.put_byte(u as u8); } else if (0x2500..=0x257f).contains(&u) || (0x2190..=0x2193).contains(&u) || u == 0x2014 { self.put_printable(c as u16); } else { self.put_byte(b'?'); } } fn csi_final(&mut self, b: u8) { if self.pending_scrolls > 0 { self.sync_scroll(); } match b { b'H' | b'f' => { let r = self.param(0, 1).max(1).saturating_sub(1); let c = self.param(1, 1).max(1).saturating_sub(1); self.cursor_r = r.min(self.rows - 1); self.cursor_c = c.min(self.cols - 1); } b'A' => self.cursor_r = self.cursor_r.saturating_sub(self.param(0, 1).max(1)), b'B' => self.cursor_r = (self.cursor_r + self.param(0, 1).max(1)).min(self.rows - 1), b'C' => self.cursor_c = (self.cursor_c + self.param(0, 1).max(1)).min(self.cols - 1), b'D' => self.cursor_c = self.cursor_c.saturating_sub(self.param(0, 1).max(1)), b'J' => match self.param(0, 0) { 0 => { self.erase_eol(); for r in (self.cursor_r + 1)..self.rows { self.erase_row(r); } } 1 => { for r in 0..self.cursor_r { self.erase_row(r); } self.erase_bol(); } _ => self.erase_all(), }, b'K' => match self.param(0, 0) { 0 => self.erase_eol(), 1 => self.erase_bol(), _ => self.erase_row(self.cursor_r), }, b'm' => { let n = self.nparams; if n == 0 { self.apply_sgr(0); } else { for i in 0..n { self.apply_sgr(self.params[i]); } } } b'l' | b'h' if self.private => { for i in 0..self.nparams { if self.params[i] == 25 { self.cursor_visible = b == b'h'; } } } _ => {} } } fn apply_sgr(&mut self, p: usize) { match p { 0 => { self.sgr_fg = 7; self.sgr_bg = 0; } 1 => { if self.sgr_fg < 8 { self.sgr_fg += 8; } } 30..=37 => self.sgr_fg = ANSI_TO_VGA[p - 30], 40..=47 => self.sgr_bg = ANSI_TO_VGA[p - 40], 90..=97 => self.sgr_fg = ANSI_TO_VGA[p - 90] + 8, 100..=107 => self.sgr_bg = ANSI_TO_VGA[p - 100] + 8, 39 => self.sgr_fg = 7, 49 => self.sgr_bg = 0, _ => {} } } fn erase_eol(&mut self) { let r = self.cursor_r; for col in self.cursor_c..self.cols { let i = self.idx(r, col); self.cells[i] = BLANK; } } fn erase_bol(&mut self) { let r = self.cursor_r; for col in 0..=self.cursor_c { let i = self.idx(r, col); self.cells[i] = BLANK; } } fn erase_row(&mut self, r: usize) { for col in 0..self.cols { let i = self.idx(r, col); self.cells[i] = BLANK; } } fn erase_all(&mut self) { for r in 0..self.rows { self.erase_row(r); } } fn newline(&mut self) { self.cursor_c = 0; if self.cursor_r + 1 < self.rows { self.cursor_r += 1; } else { self.scroll_up(); } } fn scroll_up(&mut self) { let n = self.rows * self.cols; let cols = self.cols; self.cells.copy_within(cols..n, 0); for c in 0..cols { self.cells[n - cols + c] = BLANK; } self.pending_scrolls += 1; let now = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed); if self.pending_scrolls >= SCROLL_BATCH || now.wrapping_sub(self.last_sync_tick) > TICK_LIMIT { self.sync_scroll(); } } fn flush(&mut self) { if self.pending_scrolls > 0 { return; } let rows = self.rows; let cols = self.cols; let n = rows * cols; let cursor_visible = self.cursor_visible; let (cr, cc) = (self.cursor_r, self.cursor_c); let mut dirty = if self.dirty_all { self.dirty_all = false; Some((0, 0, self.w, self.h)) } else { None }; // Diff cells vs rendered, re-blitting changed cells into the buffer. for i in 0..n { let row = i / cols; let col = i % cols; if cursor_visible && row == cr && col == cc { continue; } let c = self.cells[i]; let r = self.rendered[i]; if c != r { let fg = PALETTE[c.fg as usize]; let bg = PALETTE[c.bg as usize]; gop::buffer_glyph_px( self.buf_ptr(), self.w, self.h, col * 8, row * 16, c.ch, fg, bg, ); self.rendered[i] = c; union_rect(&mut dirty, col * 8, row * 16, 8, 16); } } // Cursor: erase the previous position, draw the new one (inverse). let (lr, lc) = self.last_cursor; let moved = (lr, lc) != (cr, cc); if (moved || !cursor_visible) && lr < rows && lc < cols { let i = self.idx(lr, lc); let c = self.cells[i]; let fg = PALETTE[c.fg as usize]; let bg = PALETTE[c.bg as usize]; gop::buffer_glyph_px( self.buf_ptr(), self.w, self.h, lc * 8, lr * 16, c.ch, fg, bg, ); self.rendered[i] = c; union_rect(&mut dirty, lc * 8, lr * 16, 8, 16); } if cursor_visible && cr < rows && cc < cols { let i = self.idx(cr, cc); let c = self.cells[i]; let fg = PALETTE[c.bg as usize]; let bg = PALETTE[c.fg as usize]; gop::buffer_glyph_px( self.buf_ptr(), self.w, self.h, cc * 8, cr * 16, c.ch, fg, bg, ); self.rendered[i] = Cell { ch: c.ch, fg: c.bg, bg: c.fg, }; union_rect(&mut dirty, cc * 8, cr * 16, 8, 16); } self.last_cursor = (cr, cc); // Display: composite the attached window, or push just the dirty // region of the buffer (not the whole screen). match self.window_id { Some(id) => crate::wm::flush(id), None => { if let Some((x, y, w, h)) = dirty { let src = unsafe { self.buf_ptr().add(y * self.w + x) }; gop::shadow_blit(x, y, w, h, src, self.w); gop::push_rect(x, y, w, h); } } } } } // --------------------------------------------------------------------------- // The home terminal + the legacy `vt::` API (thin wrappers). // --------------------------------------------------------------------------- static mut HOME: Option = None; /// When set, all vt output goes here instead of HOME. Used by windowed /// shells so that gop_print!/put_char/write_str write to the active /// terminal's pixel buffer. static mut ACTIVE: *mut Terminal = core::ptr::null_mut(); fn active() -> &'static mut Terminal { unsafe { if !ACTIVE.is_null() { &mut *ACTIVE } else { home() } } } fn home() -> &'static mut Terminal { unsafe { let slot = &mut *core::ptr::addr_of_mut!(HOME); slot.as_mut().expect("vt: not initialised") } } /// Set the active terminal for output (pass null to restore the home /// terminal). pub fn set_active(term: *mut Terminal) { unsafe { ACTIVE = term; } } /// Initialise the home terminal over a full-screen pixel buffer. Call once /// after `gop::init()`. pub fn init() { let (w, h) = gop::pixel_dims(); if w == 0 || h == 0 { let lines = [ "FATAL: no framebuffer geometry", "GOP missing or active mode is 0x0", "see the 'gop:' boot marker / serial log", ]; crate::gop::dump_lines(&lines); crate::serial_println!("FATAL: no framebuffer geometry (GOP missing or 0x0 mode)"); loop { unsafe { core::arch::asm!("cli; hlt"); } } } unsafe { HOME = Some(Terminal::new(w, h)); } } /// The home terminal instance (for the WM/boot to attach to a window). pub fn home_terminal() -> &'static mut Terminal { home() } pub fn ready() -> bool { unsafe { let slot = &*core::ptr::addr_of!(HOME); match slot { Some(t) => t.ready(), None => false, } } } pub fn size() -> (usize, usize) { home().size() } pub fn cell_raw(r: usize, c: usize) -> (u32, u32) { home().cell_raw(r, c) } pub fn set_cursor_visible(v: bool) { home().set_cursor_visible(v); } pub fn sync_scroll() { active().sync_scroll(); } pub fn repaint_region(x: usize, y: usize, w: usize, h: usize) { active().repaint_region(x, y, w, h); } pub fn write_bytes(b: &[u8]) { active().write_bytes(b); } pub fn write_str(s: &str) { active().write_str(s); } pub fn put_char(b: u8) { active().put_char(b); } pub fn clear_all() { active().clear_all(); } // --------------------------------------------------------------------------- // Interrupt-flag helpers (atomic terminal writes). // --------------------------------------------------------------------------- fn irq_off() -> bool { let rflags: u64; unsafe { core::arch::asm!( "pushfq; pop {}", out(reg) rflags, options(nostack, preserves_flags) ); if rflags & (1 << 9) != 0 { core::arch::asm!("cli", options(nostack, preserves_flags)); true } else { false } } } fn irq_restore(was_on: bool) { if was_on { unsafe { core::arch::asm!("sti", options(nostack, preserves_flags)); } } }