//! 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(); if rows == 0 || cols == 0 { // No usable framebuffer geometry: GOP is missing entirely or the // active mode reports 0x0. Without a display there is nothing // the shell can do, so stop with a visible explanation instead // of building an empty grid and panicking at the first cell // write (a blank screen on real hardware, which has no serial // console). 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 { ROWS = rows; COLS = cols; CELLS = Box::into_raw(Box::new(vec![BLANK; rows * cols])); RENDERED = Box::into_raw(Box::new(vec![BLANK; rows * cols])); // Home below row 0: row 0 is the boot-marker status line, so the // terminal (shell + klog screen echo) must never write to it. CURSOR_R = 1; CURSOR_C = 0; LAST_CURSOR = (1, 0); CURSOR_VISIBLE = true; SGR_FG = 7; SGR_BG = 0; PARSER = Parser::Ground; NPARAMS = 0; PRIVATE = false; } } /// True once the terminal is initialised (grids live). The klog screen /// echo checks this so serial lines printed before vt::init (very early /// boot) are skipped instead of hitting a null grid. pub fn ready() -> bool { unsafe { ROWS != 0 && !CELLS.is_null() } } /// (rows, cols) — matches the editor's expected (rows, cols) ordering. pub fn size() -> (usize, usize) { unsafe { (ROWS, COLS) } } /// Re-render the console's cells into the shadow for a pixel region, so the /// window manager can restore the console (layer 0) beneath a window that /// moved, closed, or was minimized. Does not push — the caller pushes once. pub fn repaint_region(x: usize, y: usize, w: usize, h: usize) { unsafe { if CELLS.is_null() { return; } let (cols, rows) = (COLS, ROWS); let c0 = x / 8; let c1 = (x + w).div_ceil(8).min(cols); let r0 = y / 16; let r1 = (y + h).div_ceil(16).min(rows); let cells = &*CELLS; for r in r0..r1 { for c in c0..c1 { let cell = cells[r * cols + c]; let fg = PALETTE[cell.fg as usize]; let bg = PALETTE[cell.bg as usize]; crate::gop::shadow_glyph_px(c * 8, r * 16, cell.ch, fg, bg); } } } } /// DEBUG (crash-family): read one cell's raw word (ch | fg<<16 | bg<<24) /// from the live cells grid and the rendered grid, for the given text /// row/col. Returns (raw_cell, raw_rendered). pub fn cell_raw(r: usize, c: usize) -> (u32, u32) { unsafe { if CELLS.is_null() || RENDERED.is_null() { return (0xFFFF_FFFF, 0xFFFF_FFFF); } let raw = |v: &Vec| -> u32 { if r < ROWS && c < COLS { let cell = v[r * COLS + c]; cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24) } else { 0xFFFF_FFFE } }; ( raw(&*(CELLS as *const Vec)), raw(&*(RENDERED as *const Vec)), ) } } /// Shell-entry self-check: dump the vt grid's heap structs so a real- /// hardware boot shows whether the cells VecData / data arrays are sane /// at shell entry (the OptiPlex's first banner write #GPs when the /// cells buffer holds firmware 0xAA fill instead of real pointers). pub fn debug_verify() { unsafe { let total = ROWS * COLS; let msg = |m: &str| { crate::serial_println!("vt: {}", m); }; if CELLS.is_null() || RENDERED.is_null() { msg("verify: cells/renderd NULL — vt::init did not run"); return; } let c = &*(CELLS as *const Vec); let r = &*(RENDERED as *const Vec); msg(&alloc::format!( "verify: cells ptr={:#x} cap={:#x} len={:#x} rendered ptr={:#x} cap={:#x} len={:#x} want={:#x}", c.as_ptr() as usize, c.capacity(), c.len(), r.as_ptr() as usize, r.capacity(), r.len(), total )); // The cells array must cover rows*cols and be inside the heap. let heap_ok = crate::memory::heap_range() .map(|(hs, he)| { let p = c.as_ptr() as usize; let bytes = c.capacity().saturating_mul(core::mem::size_of::()); p >= hs && p + bytes <= he }) .unwrap_or(false); // Content check: at shell entry nothing has been printed yet, so // every cell must still hold the grid's init fill. NOTE: that // fill is BLANK (space + fg 7 + bg 0 = raw 0x70020), NOT zero — // `vec![BLANK; n]` fills the grid with BLANK, and all boot // output to this point is serial-only (boot markers are a // direct-FB blit, never the vt grid). The v0.5.7 check // compared against 0 and therefore tripped on every healthy // boot (the "0x70020 at grid(0,0)" mystery — the grid's own // init value, with the probe loop's early `break` leaving the // other three probe slots as zero-initialized). A stale cells // data pointer that happens to be in-heap still passes the // range checks (len/cap look fine) but its "cells" read back as // the underlying memory's leftover fill. NOTE: the OptiPlex // dump value 0x00AAAAAA is the VGA GRAY text colour // (rgb(170,170,170) = PALETTE[7] = fg 7), i.e. rendered shadow // pixels — the memory read as heap held display text, not a // firmware scrub. BLANK (0x70020) vs 0x00AAAAAA is cleanly // distinguishable. let blank_raw = BLANK.ch as u32 | ((BLANK.fg as u32) << 16) | ((BLANK.bg as u32) << 24); let mut content_ok = true; let mut rcontent_ok = true; let mut c_probes_raw: [u32; 4] = [0; 4]; let mut r_probes_raw: [u32; 4] = [0; 4]; if heap_ok && c.len() == total && total > 0 { // Sample the first cell, the last cell, and two interior // cells — reading the whole grid here would be ~60 KiB of // checks at every boot for no real benefit. All four // probes are recorded unconditionally (no early break) so // a serial log shows every value. let probes = [0usize, total / 3, (2 * total) / 3, total - 1]; for (pi, &p) in probes.iter().enumerate() { let cell = *c.as_ptr().add(p); let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); c_probes_raw[pi] = raw; if raw != blank_raw { content_ok = false; } } // Same probe on the rendered grid: it is a *separate* // allocation, so its content discriminates "only the cells // VecData's ptr field is stale" (rendered ok) from "both // grids' memory was clobbered / both VecData ptrs are // stale" (rendered garbage too). for (pi, &p) in probes.iter().enumerate() { let cell = *r.as_ptr().add(p); let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); r_probes_raw[pi] = raw; if raw != blank_raw { rcontent_ok = false; } } } // Full-grid summary (cheap: a few thousand reads, serial-only). // Definitive health signal on QEMU: healthy = every cell is // BLANK (nz == blanks == total); the probes above are just the // tripwire, this catches a partial clobber that the four // samples happen to miss. let (mut c_nz, mut c_blank) = (0usize, 0usize); let (mut r_nz, mut r_blank) = (0usize, 0usize); if heap_ok && c.len() == total && r.len() == total && total > 0 { for i in 0..total { let cell = *c.as_ptr().add(i); let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); if raw != 0 { c_nz += 1; } if raw == blank_raw { c_blank += 1; } let cell = *r.as_ptr().add(i); let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); if raw != 0 { r_nz += 1; } if raw == blank_raw { r_blank += 1; } } } msg(&alloc::format!( "verify: grids c nz={} blank={} r nz={} blank={} (of {})", c_nz, c_blank, r_nz, r_blank, total )); if c.len() != total || r.len() != total || !heap_ok || !content_ok || !rcontent_ok { msg("verify: FAILED — cells/rendered not sane at shell entry"); msg(&alloc::format!( "verify: probes c=[{:#x},{:#x},{:#x},{:#x}] r=[{:#x},{:#x},{:#x},{:#x}]", c_probes_raw[0], c_probes_raw[1], c_probes_raw[2], c_probes_raw[3], r_probes_raw[0], r_probes_raw[1], r_probes_raw[2], r_probes_raw[3] )); // NOTE: debug markers are serial-only now; the crash family // was resolved (EBS-retry free-list rebuild) and the screen // must stay clean for users. crate::serial_println!( "vt: c {:#x} cap={:#x} {} | r {:#x} {}", c.as_ptr() as usize, c.capacity(), if content_ok { "ok" } else { "GARBAGE" }, r.as_ptr() as usize, if rcontent_ok { "ok" } else { "GARBAGE" } ); } else { msg("verify: ok"); crate::serial_println!( "vt: c {:#x} cap={:#x} ok | r {:#x} ok", c.as_ptr() as usize, c.capacity(), r.as_ptr() as usize ); } } } pub fn set_cursor_visible(v: bool) { sync_scroll(); 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 params() -> &'static mut [usize; 4] { unsafe { &mut *core::ptr::addr_of_mut!(PARAMS) } } 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) { // A CSI sequence means a full-screen app is actively rendering — // push any deferred scrolls so it draws over a current framebuffer // (streaming shell output never emits CSI, so it stays batched). if unsafe { PENDING_SCROLLS } > 0 { sync_scroll(); } 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(); } } } /// Deferred scrolls: the logical grid scrolls instantly (RAM), but the /// framebuffer shift+push is batched. N pending scrolls cost one push, /// so a burst of streaming output no longer writes the whole framebuffer /// once per line. `flush` renders nothing while scrolls are pending; /// `sync_scroll` pushes the whole batch and lets the normal diff re-blit /// only the newly-exposed rows. const SCROLL_BATCH: usize = 16; static mut PENDING_SCROLLS: usize = 0; static mut LAST_SYNC_TICK: u64 = 0; /// Shift the logical grid up one row; defer the framebuffer shift. fn scroll_up() { let n = unsafe { ROWS * COLS }; let cols = unsafe { COLS }; cells().copy_within(cols..n, 0); for c in 0..cols { cells()[n - cols + c] = BLANK; } unsafe { PENDING_SCROLLS += 1; // Batch: sync when enough scrolls piled up, or when the stream // pauses (~40 ms at the 100 Hz timer) so interactive output still // appears promptly. const TICK_LIMIT: u64 = 4; let now = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed); if PENDING_SCROLLS >= SCROLL_BATCH || now.wrapping_sub(LAST_SYNC_TICK) > TICK_LIMIT { sync_scroll(); } } } /// Push deferred scrolls to the display in one batch: shift the shadow /// (and the rendered grid, which mirrors it) up by the pending count and /// do a single write-only push, then let the normal flush diff re-blit /// the newly-exposed rows and redraw the cursor. pub fn sync_scroll() { unsafe { let n = PENDING_SCROLLS; if n == 0 { return; } let rows = ROWS; let cols = COLS; gop::shift_up_n(n); // Mirror the pushed content in RENDERED (it describes the fb). let total = rows * cols; rendered_cells().copy_within(n * cols..total, 0); for i in (total - n * cols)..total { rendered_cells()[i] = BLANK; } PENDING_SCROLLS = 0; LAST_SYNC_TICK = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed); flush(); } } // --------------------------------------------------------------------------- // public writing API // --------------------------------------------------------------------------- /// Disable interrupts unless they are already off (nested-safe). Returns /// whether they were on, so `irq_restore` re-enables exactly when needed. /// The terminal write path uses this so a klog screen-echo (which can run /// in ISR context) can never tear a shell write, and a task-context write /// can never be split by a tick. 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)); } } } /// 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 on = irq_off(); 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(); irq_restore(on); } /// Write a UTF-8 string (plain text; Unicode glyphs render, others '?'). /// Atomic with respect to interrupts: a klog screen-echo in ISR context /// cannot interleave with a shell write, and a tick cannot split a write. pub fn write_str(s: &str) { let on = irq_off(); for c in s.chars() { write_char(c); } flush(); irq_restore(on); } /// Write a single ASCII byte / char. pub fn put_char(b: u8) { let on = irq_off(); put_byte(b); flush(); irq_restore(on); } /// Clear the whole screen, reset colours, home the cursor. pub fn clear_all() { unsafe { SGR_FG = 7; SGR_BG = 0; sync_scroll(); 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() { unsafe { // Deferred scrolls pending: the framebuffer lags the logical grid. // Render nothing until `sync_scroll` pushes the batch — the diff // would otherwise see every cell as changed and re-rasterise the // whole screen once per line. if PENDING_SCROLLS > 0 { return; } } 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, }; } } }