diff options
| author | salmonik <salmonik@tuta.io> | 2026-07-31 17:19:09 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-07-31 17:19:09 +0200 |
| commit | fa06681a8418b7e31af96e010bb424e178402f4d (patch) | |
| tree | 3b2fa8674384078f73b62274c71e8279ca6d0644 /rsnix/src/vt.rs | |
| parent | shell: ^C cancels the line / aborts streams, ^D = EOF → poweroff (diff) | |
| download | rs-dos-fa06681a8418b7e31af96e010bb424e178402f4d.tar.gz rs-dos-fa06681a8418b7e31af96e010bb424e178402f4d.zip | |
tty + vt terminal split, rsdosedit editor (1:1 UNIX console)
Kernel now has the classic UNIX console split, so full-screen ANSI apps
can run in ring 0:
- vt.rs: terminal emulator owning the text screen — cell grid (char +
fg/bg), logical cursor (inverse-video), scrolling, and a small ANSI
CSI parser (cursor position H/f, erase J/K, moves A-D, SGR 16-colour
with correct ANSI→VGA mapping, cursor show/hide ?25). Blits only
changed cells; scroll shifts cell grids + pixels and re-rasterises
only the exposed bottom row. gop_println!/clear_screen now render
through the vt; gop.rs is the raw rasteriser (colored glyph blits,
pixel shift).
- tty.rs: line discipline — canonical read_line (echo, line buffer,
^C cancel, ^D EOF) moved out of the shell, and raw read_byte that
translates KeyEvents to the terminal byte protocol (arrows → ESC[A-D,
Backspace → 0x7f, ^X → 0x18, Ctrl+arrows → ESC[1;5A-D, Home/End/PgUp/
PgDn/Delete). interrupt_pressed() stays for streaming commands.
- ps2: generic Ctrl(char) + CtrlArrow events (was: only CtrlC/CtrlD).
- rsdosedit: no_std port of the RNG rsedit editor as the first real
app — undo, syntax highlighting (verified green/gray/yellow on the
framebuffer), search, word nav, smart indent, ^S save / ^X exit /
^X again to discard. termios → tty raw, stdout → vt, std::fs → VFS
(ramfs), SIGWINCH → fixed geometry, force-exit window → key counter.
- shell: 'edit <path>' command; read_line/strbuf moved to rsnix.
- CONTEXT.md: layout + milestones + known-issue perf notes.
Verified in QEMU: full edit → type → arrow-move → insert → save → exit
round trip with byte-exact file content; ANSI colors via screendump.
Diffstat (limited to 'rsnix/src/vt.rs')
| -rw-r--r-- | rsnix/src/vt.rs | 476 |
1 files changed, 476 insertions, 0 deletions
diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs new file mode 100644 index 0000000..eb60e11 --- /dev/null +++ b/rsnix/src/vt.rs | |||
| @@ -0,0 +1,476 @@ | |||
| 1 | //! Minimal ANSI terminal emulator over the GOP framebuffer. | ||
| 2 | //! | ||
| 3 | //! `vt` owns the text screen: a cell grid (char + fg/bg colour), a logical | ||
| 4 | //! cursor, scrolling, and a small CSI parser. It renders to the framebuffer | ||
| 5 | //! through `gop`, blitting only cells whose content changed since the last | ||
| 6 | //! flush. | ||
| 7 | //! | ||
| 8 | //! This is the kernel's "terminal". Plain text (shell output, typed echo) | ||
| 9 | //! flows through here in *ground* state; full-screen apps (rsdosedit) write | ||
| 10 | //! ANSI escape sequences that the CSI parser interprets — cursor addressing, | ||
| 11 | //! erase, 16-colour SGR, cursor show/hide. | ||
| 12 | //! | ||
| 13 | //! Known perf note: scrolling shifts the whole framebuffer one row up (a | ||
| 14 | //! ~3 MB copy at 1024×768), which dominates under QEMU without KVM. A ring | ||
| 15 | //! buffer / deferred-pan approach is the future optimisation; the cell grid | ||
| 16 | //! itself never re-rasterises on scroll — only the exposed bottom row is | ||
| 17 | //! blitted. | ||
| 18 | |||
| 19 | use crate::gop; | ||
| 20 | use alloc::boxed::Box; | ||
| 21 | use alloc::vec; | ||
| 22 | use alloc::vec::Vec; | ||
| 23 | |||
| 24 | /// Standard VGA 16-colour palette (r, g, b). | ||
| 25 | pub const PALETTE: [(u8, u8, u8); 16] = [ | ||
| 26 | (0, 0, 0), | ||
| 27 | (0, 0, 170), | ||
| 28 | (0, 170, 0), | ||
| 29 | (0, 170, 170), | ||
| 30 | (170, 0, 0), | ||
| 31 | (170, 0, 170), | ||
| 32 | (170, 85, 0), | ||
| 33 | (170, 170, 170), | ||
| 34 | (85, 85, 85), | ||
| 35 | (85, 85, 255), | ||
| 36 | (85, 255, 85), | ||
| 37 | (85, 255, 255), | ||
| 38 | (255, 85, 85), | ||
| 39 | (255, 85, 255), | ||
| 40 | (255, 255, 85), | ||
| 41 | (255, 255, 255), | ||
| 42 | ]; | ||
| 43 | |||
| 44 | #[derive(Clone, Copy, PartialEq, Eq)] | ||
| 45 | struct Cell { | ||
| 46 | ch: u16, | ||
| 47 | fg: u8, | ||
| 48 | bg: u8, | ||
| 49 | } | ||
| 50 | |||
| 51 | const BLANK: Cell = Cell { ch: 32, fg: 7, bg: 0 }; | ||
| 52 | |||
| 53 | // ---- screen state ---------------------------------------------------------- | ||
| 54 | |||
| 55 | static mut CELLS: *mut Vec<Cell> = core::ptr::null_mut(); | ||
| 56 | /// What is currently on the physical framebuffer (used to diff blits). | ||
| 57 | static mut RENDERED: *mut Vec<Cell> = core::ptr::null_mut(); | ||
| 58 | static mut ROWS: usize = 0; | ||
| 59 | static mut COLS: usize = 0; | ||
| 60 | |||
| 61 | // ---- cursor ---------------------------------------------------------------- | ||
| 62 | |||
| 63 | static mut CURSOR_R: usize = 0; | ||
| 64 | static mut CURSOR_C: usize = 0; | ||
| 65 | static mut LAST_CURSOR: (usize, usize) = (0, 0); | ||
| 66 | static mut CURSOR_VISIBLE: bool = true; | ||
| 67 | |||
| 68 | // ---- SGR state ------------------------------------------------------------- | ||
| 69 | |||
| 70 | static mut SGR_FG: u8 = 7; | ||
| 71 | static mut SGR_BG: u8 = 0; | ||
| 72 | |||
| 73 | // ---- ANSI parser state ----------------------------------------------------- | ||
| 74 | |||
| 75 | #[derive(Clone, Copy, PartialEq, Eq)] | ||
| 76 | enum Parser { | ||
| 77 | Ground, | ||
| 78 | Escape, | ||
| 79 | Csi, | ||
| 80 | } | ||
| 81 | |||
| 82 | static mut PARSER: Parser = Parser::Ground; | ||
| 83 | static mut PRIVATE: bool = false; | ||
| 84 | static mut PARAMS: [usize; 4] = [0; 4]; | ||
| 85 | static mut NPARAMS: usize = 0; | ||
| 86 | static mut CUR: usize = 0; | ||
| 87 | |||
| 88 | // --------------------------------------------------------------------------- | ||
| 89 | // init / dimensions | ||
| 90 | // --------------------------------------------------------------------------- | ||
| 91 | |||
| 92 | /// Allocate the cell grid. Call once after `gop::init()`. | ||
| 93 | pub fn init() { | ||
| 94 | let (cols, rows) = gop::dims(); | ||
| 95 | unsafe { | ||
| 96 | ROWS = rows; | ||
| 97 | COLS = cols; | ||
| 98 | CELLS = Box::into_raw(Box::new(vec![BLANK; rows * cols])); | ||
| 99 | RENDERED = Box::into_raw(Box::new(vec![BLANK; rows * cols])); | ||
| 100 | CURSOR_R = 0; | ||
| 101 | CURSOR_C = 0; | ||
| 102 | LAST_CURSOR = (0, 0); | ||
| 103 | CURSOR_VISIBLE = true; | ||
| 104 | SGR_FG = 7; | ||
| 105 | SGR_BG = 0; | ||
| 106 | PARSER = Parser::Ground; | ||
| 107 | NPARAMS = 0; | ||
| 108 | PRIVATE = false; | ||
| 109 | } | ||
| 110 | } | ||
| 111 | |||
| 112 | /// (rows, cols) — matches the editor's expected (rows, cols) ordering. | ||
| 113 | pub fn size() -> (usize, usize) { | ||
| 114 | unsafe { (ROWS, COLS) } | ||
| 115 | } | ||
| 116 | |||
| 117 | pub fn set_cursor_visible(v: bool) { | ||
| 118 | unsafe { CURSOR_VISIBLE = v; } | ||
| 119 | flush(); | ||
| 120 | } | ||
| 121 | |||
| 122 | // --------------------------------------------------------------------------- | ||
| 123 | // cell access | ||
| 124 | // --------------------------------------------------------------------------- | ||
| 125 | |||
| 126 | fn idx(r: usize, c: usize) -> usize { | ||
| 127 | r * unsafe { COLS } + c | ||
| 128 | } | ||
| 129 | |||
| 130 | fn cells() -> &'static mut Vec<Cell> { | ||
| 131 | unsafe { &mut *CELLS } | ||
| 132 | } | ||
| 133 | |||
| 134 | fn rendered_cells() -> &'static mut Vec<Cell> { | ||
| 135 | unsafe { &mut *RENDERED } | ||
| 136 | } | ||
| 137 | |||
| 138 | fn cell(r: usize, c: usize) -> &'static mut Cell { | ||
| 139 | &mut cells()[idx(r, c)] | ||
| 140 | } | ||
| 141 | |||
| 142 | fn rendered(r: usize, c: usize) -> &'static mut Cell { | ||
| 143 | &mut rendered_cells()[idx(r, c)] | ||
| 144 | } | ||
| 145 | |||
| 146 | fn put_cell(r: usize, c: usize, ch: u16) { | ||
| 147 | let cell = cell(r, c); | ||
| 148 | cell.ch = ch; | ||
| 149 | cell.fg = unsafe { SGR_FG }; | ||
| 150 | cell.bg = unsafe { SGR_BG }; | ||
| 151 | } | ||
| 152 | |||
| 153 | // --------------------------------------------------------------------------- | ||
| 154 | // writing — the ANSI state machine | ||
| 155 | // --------------------------------------------------------------------------- | ||
| 156 | |||
| 157 | /// Feed one byte through the terminal state machine. | ||
| 158 | fn put_byte(b: u8) { | ||
| 159 | unsafe { | ||
| 160 | match PARSER { | ||
| 161 | Parser::Ground => match b { | ||
| 162 | 0x1b => PARSER = Parser::Escape, | ||
| 163 | 0x0d => CURSOR_C = 0, | ||
| 164 | 0x0a => newline(), | ||
| 165 | 0x08 => { | ||
| 166 | // Backspace: move cursor back and erase. | ||
| 167 | if CURSOR_C > 0 { | ||
| 168 | CURSOR_C -= 1; | ||
| 169 | } else if CURSOR_R > 0 { | ||
| 170 | CURSOR_R -= 1; | ||
| 171 | CURSOR_C = COLS - 1; | ||
| 172 | } else { | ||
| 173 | return; | ||
| 174 | } | ||
| 175 | let c = cell(CURSOR_R, CURSOR_C); | ||
| 176 | *c = BLANK; | ||
| 177 | } | ||
| 178 | 0x09 => { | ||
| 179 | let n = 8 - (CURSOR_C % 8); | ||
| 180 | CURSOR_C = (CURSOR_C + n).min(COLS - 1); | ||
| 181 | } | ||
| 182 | 0x00..=0x1f => {} // other control chars: ignored | ||
| 183 | _ => { | ||
| 184 | let ch = if b < 0x80 { b as u16 } else { b'?' as u16 }; | ||
| 185 | put_cell(CURSOR_R, CURSOR_C, ch); | ||
| 186 | CURSOR_C += 1; | ||
| 187 | if CURSOR_C >= COLS { | ||
| 188 | CURSOR_C = 0; | ||
| 189 | newline(); | ||
| 190 | } | ||
| 191 | } | ||
| 192 | }, | ||
| 193 | Parser::Escape => match b { | ||
| 194 | b'[' => { | ||
| 195 | PRIVATE = false; | ||
| 196 | NPARAMS = 0; | ||
| 197 | CUR = 0; | ||
| 198 | PARSER = Parser::Csi; | ||
| 199 | } | ||
| 200 | _ => PARSER = Parser::Ground, // unsupported escape — drop | ||
| 201 | }, | ||
| 202 | Parser::Csi => match b { | ||
| 203 | b'?' => PRIVATE = true, | ||
| 204 | b'0'..=b'9' => { | ||
| 205 | CUR = CUR.saturating_mul(10).saturating_add((b - b'0') as usize); | ||
| 206 | } | ||
| 207 | b';' => { | ||
| 208 | if NPARAMS < PARAMS.len() { | ||
| 209 | PARAMS[NPARAMS] = CUR; | ||
| 210 | NPARAMS += 1; | ||
| 211 | } | ||
| 212 | CUR = 0; | ||
| 213 | } | ||
| 214 | _ => { | ||
| 215 | if NPARAMS < PARAMS.len() { | ||
| 216 | PARAMS[NPARAMS] = CUR; | ||
| 217 | NPARAMS += 1; | ||
| 218 | } | ||
| 219 | PARSER = Parser::Ground; | ||
| 220 | csi_final(b); | ||
| 221 | } | ||
| 222 | }, | ||
| 223 | } | ||
| 224 | } | ||
| 225 | } | ||
| 226 | |||
| 227 | fn param(i: usize, default: usize) -> usize { | ||
| 228 | unsafe { | ||
| 229 | if i < NPARAMS { | ||
| 230 | PARAMS[i] | ||
| 231 | } else { | ||
| 232 | default | ||
| 233 | } | ||
| 234 | } | ||
| 235 | } | ||
| 236 | |||
| 237 | fn csi_final(b: u8) { | ||
| 238 | match b { | ||
| 239 | // Cursor position (1-based; 0 means 1) | ||
| 240 | b'H' | b'f' => { | ||
| 241 | let r = param(0, 1).max(1).saturating_sub(1); | ||
| 242 | let c = param(1, 1).max(1).saturating_sub(1); | ||
| 243 | unsafe { | ||
| 244 | CURSOR_R = r.min(ROWS - 1); | ||
| 245 | CURSOR_C = c.min(COLS - 1); | ||
| 246 | } | ||
| 247 | } | ||
| 248 | // Cursor up / down / forward / back | ||
| 249 | b'A' => unsafe { CURSOR_R = CURSOR_R.saturating_sub(param(0, 1).max(1)) }, | ||
| 250 | b'B' => unsafe { CURSOR_R = (CURSOR_R + param(0, 1).max(1)).min(ROWS - 1) }, | ||
| 251 | b'C' => unsafe { CURSOR_C = (CURSOR_C + param(0, 1).max(1)).min(COLS - 1) }, | ||
| 252 | b'D' => unsafe { CURSOR_C = CURSOR_C.saturating_sub(param(0, 1).max(1)) }, | ||
| 253 | // Erase in display: 0 below, 1 above, 2 all | ||
| 254 | b'J' => match param(0, 0) { | ||
| 255 | 0 => { | ||
| 256 | erase_eol(); | ||
| 257 | unsafe { | ||
| 258 | for r in (CURSOR_R + 1)..ROWS { | ||
| 259 | erase_row(r); | ||
| 260 | } | ||
| 261 | } | ||
| 262 | } | ||
| 263 | 1 => { | ||
| 264 | unsafe { | ||
| 265 | for r in 0..CURSOR_R { | ||
| 266 | erase_row(r); | ||
| 267 | } | ||
| 268 | } | ||
| 269 | erase_bol(); | ||
| 270 | } | ||
| 271 | _ => erase_all(), | ||
| 272 | }, | ||
| 273 | // Erase in line: 0 to EOL, 1 from BOL, 2 whole line | ||
| 274 | b'K' => match param(0, 0) { | ||
| 275 | 1 => erase_bol(), | ||
| 276 | 2 => { | ||
| 277 | let r = unsafe { CURSOR_R }; | ||
| 278 | erase_row(r); | ||
| 279 | } | ||
| 280 | _ => erase_eol(), | ||
| 281 | }, | ||
| 282 | // SGR — colours / attributes | ||
| 283 | b'm' => { | ||
| 284 | let n = unsafe { NPARAMS }; | ||
| 285 | if n == 0 { | ||
| 286 | apply_sgr(0); | ||
| 287 | } else { | ||
| 288 | for i in 0..n { | ||
| 289 | apply_sgr(unsafe { PARAMS[i] }); | ||
| 290 | } | ||
| 291 | } | ||
| 292 | } | ||
| 293 | // Private mode: 25 = cursor show/hide (h = show, l = hide) | ||
| 294 | b'l' | b'h' if unsafe { PRIVATE } => { | ||
| 295 | for i in 0..unsafe { NPARAMS } { | ||
| 296 | if unsafe { PARAMS[i] } == 25 { | ||
| 297 | unsafe { CURSOR_VISIBLE = b == b'h' }; | ||
| 298 | } | ||
| 299 | } | ||
| 300 | } | ||
| 301 | _ => {} // unknown / ignored | ||
| 302 | } | ||
| 303 | } | ||
| 304 | |||
| 305 | /// ANSI SGR colour index (0–7) → VGA palette index. The two orderings | ||
| 306 | /// differ: ANSI 1=red, 3=yellow, 4=blue, 6=cyan; VGA 1=blue, 3=cyan, | ||
| 307 | /// 4=red, 6=brown. Terminals translate ANSI→VGA. | ||
| 308 | const ANSI_TO_VGA: [u8; 8] = [0, 4, 2, 6, 1, 5, 3, 7]; | ||
| 309 | |||
| 310 | fn apply_sgr(p: usize) { | ||
| 311 | unsafe { | ||
| 312 | match p { | ||
| 313 | 0 => { | ||
| 314 | SGR_FG = 7; | ||
| 315 | SGR_BG = 0; | ||
| 316 | } | ||
| 317 | 1 => { | ||
| 318 | if SGR_FG < 8 { | ||
| 319 | SGR_FG += 8; // bold ≈ bright | ||
| 320 | } | ||
| 321 | } | ||
| 322 | 30..=37 => SGR_FG = ANSI_TO_VGA[p - 30], | ||
| 323 | 40..=47 => SGR_BG = ANSI_TO_VGA[p - 40], | ||
| 324 | 90..=97 => SGR_FG = ANSI_TO_VGA[p - 90] + 8, | ||
| 325 | 100..=107 => SGR_BG = ANSI_TO_VGA[p - 100] + 8, | ||
| 326 | 39 => SGR_FG = 7, | ||
| 327 | 49 => SGR_BG = 0, | ||
| 328 | _ => {} | ||
| 329 | } | ||
| 330 | } | ||
| 331 | } | ||
| 332 | |||
| 333 | // --------------------------------------------------------------------------- | ||
| 334 | // erase / scroll helpers | ||
| 335 | // --------------------------------------------------------------------------- | ||
| 336 | |||
| 337 | fn erase_eol() { | ||
| 338 | let r = unsafe { CURSOR_R }; | ||
| 339 | let c = unsafe { CURSOR_C }; | ||
| 340 | for col in c..unsafe { COLS } { | ||
| 341 | *cell(r, col) = BLANK; | ||
| 342 | } | ||
| 343 | } | ||
| 344 | |||
| 345 | fn erase_bol() { | ||
| 346 | let r = unsafe { CURSOR_R }; | ||
| 347 | let c = unsafe { CURSOR_C }; | ||
| 348 | for col in 0..=c { | ||
| 349 | *cell(r, col) = BLANK; | ||
| 350 | } | ||
| 351 | } | ||
| 352 | |||
| 353 | fn erase_row(r: usize) { | ||
| 354 | for col in 0..unsafe { COLS } { | ||
| 355 | *cell(r, col) = BLANK; | ||
| 356 | } | ||
| 357 | } | ||
| 358 | |||
| 359 | fn erase_all() { | ||
| 360 | for r in 0..unsafe { ROWS } { | ||
| 361 | erase_row(r); | ||
| 362 | } | ||
| 363 | } | ||
| 364 | |||
| 365 | /// Advance cursor to the next line (col 0), scrolling if at the bottom. | ||
| 366 | fn newline() { | ||
| 367 | unsafe { | ||
| 368 | CURSOR_C = 0; | ||
| 369 | if CURSOR_R + 1 < ROWS { | ||
| 370 | CURSOR_R += 1; | ||
| 371 | } else { | ||
| 372 | scroll_up(); | ||
| 373 | } | ||
| 374 | } | ||
| 375 | } | ||
| 376 | |||
| 377 | /// Shift the whole screen up one row: cell grids in memory, pixels on the | ||
| 378 | /// framebuffer. Only the exposed bottom row gets re-rasterised by `flush`. | ||
| 379 | fn scroll_up() { | ||
| 380 | let n = unsafe { ROWS * COLS }; | ||
| 381 | let cols = unsafe { COLS }; | ||
| 382 | cells().copy_within(cols..n, 0); | ||
| 383 | rendered_cells().copy_within(cols..n, 0); | ||
| 384 | for c in 0..cols { | ||
| 385 | cells()[n - cols + c] = BLANK; | ||
| 386 | rendered_cells()[n - cols + c] = BLANK; | ||
| 387 | } | ||
| 388 | gop::shift_up(); | ||
| 389 | } | ||
| 390 | |||
| 391 | // --------------------------------------------------------------------------- | ||
| 392 | // public writing API | ||
| 393 | // --------------------------------------------------------------------------- | ||
| 394 | |||
| 395 | /// Write raw bytes (ANSI-capable). Used by full-screen apps. | ||
| 396 | pub fn write_bytes(b: &[u8]) { | ||
| 397 | for &x in b { | ||
| 398 | put_byte(x); | ||
| 399 | } | ||
| 400 | flush(); | ||
| 401 | } | ||
| 402 | |||
| 403 | /// Write a UTF-8 string (plain text; non-ASCII becomes '?'). | ||
| 404 | pub fn write_str(s: &str) { | ||
| 405 | for c in s.chars() { | ||
| 406 | let b = if (c as u32) < 0x80 { c as u8 } else { b'?' }; | ||
| 407 | put_byte(b); | ||
| 408 | } | ||
| 409 | flush(); | ||
| 410 | } | ||
| 411 | |||
| 412 | /// Write a single ASCII byte / char. | ||
| 413 | pub fn put_char(b: u8) { | ||
| 414 | put_byte(b); | ||
| 415 | flush(); | ||
| 416 | } | ||
| 417 | |||
| 418 | /// Clear the whole screen, reset colours, home the cursor. | ||
| 419 | pub fn clear_all() { | ||
| 420 | unsafe { | ||
| 421 | SGR_FG = 7; | ||
| 422 | SGR_BG = 0; | ||
| 423 | erase_all(); | ||
| 424 | CURSOR_R = 0; | ||
| 425 | CURSOR_C = 0; | ||
| 426 | } | ||
| 427 | flush(); | ||
| 428 | } | ||
| 429 | |||
| 430 | // --------------------------------------------------------------------------- | ||
| 431 | // rendering | ||
| 432 | // --------------------------------------------------------------------------- | ||
| 433 | |||
| 434 | /// Blit every cell whose content differs from what the framebuffer shows, | ||
| 435 | /// then draw the logical cursor (inverse video) on top. | ||
| 436 | fn flush() { | ||
| 437 | let rows = unsafe { ROWS }; | ||
| 438 | let cols = unsafe { COLS }; | ||
| 439 | let n = rows * cols; | ||
| 440 | { | ||
| 441 | let cells = cells(); | ||
| 442 | let rendered = rendered_cells(); | ||
| 443 | for i in 0..n { | ||
| 444 | let c = cells[i]; | ||
| 445 | let r = rendered[i]; | ||
| 446 | if c != r { | ||
| 447 | let row = i / cols; | ||
| 448 | let col = i % cols; | ||
| 449 | let fg = PALETTE[c.fg as usize]; | ||
| 450 | let bg = PALETTE[c.bg as usize]; | ||
| 451 | gop::blit_glyph(col, row, c.ch, fg, bg); | ||
| 452 | rendered[i] = c; | ||
| 453 | } | ||
| 454 | } | ||
| 455 | } | ||
| 456 | |||
| 457 | unsafe { | ||
| 458 | let (lr, lc) = LAST_CURSOR; | ||
| 459 | let moved = (lr, lc) != (CURSOR_R, CURSOR_C); | ||
| 460 | // Erase the previous cursor position (restore its normal colours). | ||
| 461 | if (moved || !CURSOR_VISIBLE) && lr < rows && lc < cols { | ||
| 462 | let c = cells()[idx(lr, lc)]; | ||
| 463 | let fg = PALETTE[c.fg as usize]; | ||
| 464 | let bg = PALETTE[c.bg as usize]; | ||
| 465 | gop::blit_glyph(lc, lr, c.ch, fg, bg); | ||
| 466 | } | ||
| 467 | LAST_CURSOR = (CURSOR_R, CURSOR_C); | ||
| 468 | // Draw the cursor as an inverse-video cell. | ||
| 469 | if CURSOR_VISIBLE && CURSOR_R < rows && CURSOR_C < cols { | ||
| 470 | let c = cells()[idx(CURSOR_R, CURSOR_C)]; | ||
| 471 | let fg = PALETTE[c.fg as usize]; | ||
| 472 | let bg = PALETTE[c.bg as usize]; | ||
| 473 | gop::blit_glyph(CURSOR_C, CURSOR_R, c.ch, bg, fg); | ||
| 474 | } | ||
| 475 | } | ||
| 476 | } | ||
