summaryrefslogtreecommitdiffstats
path: root/rsnix/src/vt.rs
diff options
context:
space:
mode:
Diffstat (limited to 'rsnix/src/vt.rs')
-rw-r--r--rsnix/src/vt.rs476
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
19use crate::gop;
20use alloc::boxed::Box;
21use alloc::vec;
22use alloc::vec::Vec;
23
24/// Standard VGA 16-colour palette (r, g, b).
25pub 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)]
45struct Cell {
46 ch: u16,
47 fg: u8,
48 bg: u8,
49}
50
51const BLANK: Cell = Cell { ch: 32, fg: 7, bg: 0 };
52
53// ---- screen state ----------------------------------------------------------
54
55static mut CELLS: *mut Vec<Cell> = core::ptr::null_mut();
56/// What is currently on the physical framebuffer (used to diff blits).
57static mut RENDERED: *mut Vec<Cell> = core::ptr::null_mut();
58static mut ROWS: usize = 0;
59static mut COLS: usize = 0;
60
61// ---- cursor ----------------------------------------------------------------
62
63static mut CURSOR_R: usize = 0;
64static mut CURSOR_C: usize = 0;
65static mut LAST_CURSOR: (usize, usize) = (0, 0);
66static mut CURSOR_VISIBLE: bool = true;
67
68// ---- SGR state -------------------------------------------------------------
69
70static mut SGR_FG: u8 = 7;
71static mut SGR_BG: u8 = 0;
72
73// ---- ANSI parser state -----------------------------------------------------
74
75#[derive(Clone, Copy, PartialEq, Eq)]
76enum Parser {
77 Ground,
78 Escape,
79 Csi,
80}
81
82static mut PARSER: Parser = Parser::Ground;
83static mut PRIVATE: bool = false;
84static mut PARAMS: [usize; 4] = [0; 4];
85static mut NPARAMS: usize = 0;
86static mut CUR: usize = 0;
87
88// ---------------------------------------------------------------------------
89// init / dimensions
90// ---------------------------------------------------------------------------
91
92/// Allocate the cell grid. Call once after `gop::init()`.
93pub 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.
113pub fn size() -> (usize, usize) {
114 unsafe { (ROWS, COLS) }
115}
116
117pub fn set_cursor_visible(v: bool) {
118 unsafe { CURSOR_VISIBLE = v; }
119 flush();
120}
121
122// ---------------------------------------------------------------------------
123// cell access
124// ---------------------------------------------------------------------------
125
126fn idx(r: usize, c: usize) -> usize {
127 r * unsafe { COLS } + c
128}
129
130fn cells() -> &'static mut Vec<Cell> {
131 unsafe { &mut *CELLS }
132}
133
134fn rendered_cells() -> &'static mut Vec<Cell> {
135 unsafe { &mut *RENDERED }
136}
137
138fn cell(r: usize, c: usize) -> &'static mut Cell {
139 &mut cells()[idx(r, c)]
140}
141
142fn rendered(r: usize, c: usize) -> &'static mut Cell {
143 &mut rendered_cells()[idx(r, c)]
144}
145
146fn 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.
158fn 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
227fn param(i: usize, default: usize) -> usize {
228 unsafe {
229 if i < NPARAMS {
230 PARAMS[i]
231 } else {
232 default
233 }
234 }
235}
236
237fn 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.
308const ANSI_TO_VGA: [u8; 8] = [0, 4, 2, 6, 1, 5, 3, 7];
309
310fn 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
337fn 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
345fn 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
353fn erase_row(r: usize) {
354 for col in 0..unsafe { COLS } {
355 *cell(r, col) = BLANK;
356 }
357}
358
359fn 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.
366fn 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`.
379fn 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.
396pub 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 '?').
404pub 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.
413pub fn put_char(b: u8) {
414 put_byte(b);
415 flush();
416}
417
418/// Clear the whole screen, reset colours, home the cursor.
419pub 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.
436fn 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}