diff options
| author | salmonik <salmonik@tuta.io> | 2026-07-31 17:28:31 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-07-31 17:28:31 +0200 |
| commit | 994a2196745637fd0805132711b1ed9354b75426 (patch) | |
| tree | c666940f060aa8db028c75fd3aef799fabbd69b4 /rsnix/src/vt.rs | |
| parent | tty + vt terminal split, rsdosedit editor (1:1 UNIX console) (diff) | |
| download | rs-dos-994a2196745637fd0805132711b1ed9354b75426.tar.gz rs-dos-994a2196745637fd0805132711b1ed9354b75426.zip | |
vt: decode UTF-8, render box-drawing/arrow glyphs procedurally
The editor's gutter '│' (U+2502) appeared as '???' — write_bytes fed
each UTF-8 byte to the state machine, which mapped every byte >= 0x80
to '?', and the Latin-1 font has no box-drawing glyphs anyway.
- vt.write_bytes now decodes UTF-8 sequences; printable non-ASCII
dispatches to the glyph path instead of the byte parser.
- gop: unicode_glyph_bits() procedurally rasterises box-drawing
(U+2500-257F light/heavy/double), arrows (U+2190-2193) and the em
dash (U+2014) — pure geometry, no font data needed; blit_glyph falls
back to it for ch >= 256.
Verified via framebuffer dumps: editor gutter │ renders as a vertical
line, help screen ═ separators and ←→↑↓ all render as lines (no '?').
Shell banner em dash renders too.
Diffstat (limited to 'rsnix/src/vt.rs')
| -rw-r--r-- | rsnix/src/vt.rs | 87 |
1 files changed, 70 insertions, 17 deletions
diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs index eb60e11..3b81d68 100644 --- a/rsnix/src/vt.rs +++ b/rsnix/src/vt.rs | |||
| @@ -48,7 +48,11 @@ struct Cell { | |||
| 48 | bg: u8, | 48 | bg: u8, |
| 49 | } | 49 | } |
| 50 | 50 | ||
| 51 | const BLANK: Cell = Cell { ch: 32, fg: 7, bg: 0 }; | 51 | const BLANK: Cell = Cell { |
| 52 | ch: 32, | ||
| 53 | fg: 7, | ||
| 54 | bg: 0, | ||
| 55 | }; | ||
| 52 | 56 | ||
| 53 | // ---- screen state ---------------------------------------------------------- | 57 | // ---- screen state ---------------------------------------------------------- |
| 54 | 58 | ||
| @@ -115,7 +119,9 @@ pub fn size() -> (usize, usize) { | |||
| 115 | } | 119 | } |
| 116 | 120 | ||
| 117 | pub fn set_cursor_visible(v: bool) { | 121 | pub fn set_cursor_visible(v: bool) { |
| 118 | unsafe { CURSOR_VISIBLE = v; } | 122 | unsafe { |
| 123 | CURSOR_VISIBLE = v; | ||
| 124 | } | ||
| 119 | flush(); | 125 | flush(); |
| 120 | } | 126 | } |
| 121 | 127 | ||
| @@ -180,15 +186,7 @@ fn put_byte(b: u8) { | |||
| 180 | CURSOR_C = (CURSOR_C + n).min(COLS - 1); | 186 | CURSOR_C = (CURSOR_C + n).min(COLS - 1); |
| 181 | } | 187 | } |
| 182 | 0x00..=0x1f => {} // other control chars: ignored | 188 | 0x00..=0x1f => {} // other control chars: ignored |
| 183 | _ => { | 189 | _ => put_printable(b as u16), |
| 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 | }, | 190 | }, |
| 193 | Parser::Escape => match b { | 191 | Parser::Escape => match b { |
| 194 | b'[' => { | 192 | b'[' => { |
| @@ -234,6 +232,34 @@ fn param(i: usize, default: usize) -> usize { | |||
| 234 | } | 232 | } |
| 235 | } | 233 | } |
| 236 | 234 | ||
| 235 | /// Place a printable glyph at the cursor and advance (wrapping/scrolling). | ||
| 236 | /// `ch` is the character code: ASCII or a Unicode codepoint that the | ||
| 237 | /// rasteriser can draw (box-drawing / arrows are rendered procedurally). | ||
| 238 | fn put_printable(ch: u16) { | ||
| 239 | unsafe { | ||
| 240 | put_cell(CURSOR_R, CURSOR_C, ch); | ||
| 241 | CURSOR_C += 1; | ||
| 242 | if CURSOR_C >= COLS { | ||
| 243 | CURSOR_C = 0; | ||
| 244 | newline(); | ||
| 245 | } | ||
| 246 | } | ||
| 247 | } | ||
| 248 | |||
| 249 | /// Dispatch one character to the terminal: ASCII goes through the byte | ||
| 250 | /// state machine (control/escape handling), printable non-ASCII goes | ||
| 251 | /// straight to the glyph path (replaced by '?' when not drawable). | ||
| 252 | fn write_char(c: char) { | ||
| 253 | let u = c as u32; | ||
| 254 | if u < 0x80 { | ||
| 255 | put_byte(u as u8); | ||
| 256 | } else if (0x2500..=0x257f).contains(&u) || (0x2190..=0x2193).contains(&u) || u == 0x2014 { | ||
| 257 | put_printable(c as u16); | ||
| 258 | } else { | ||
| 259 | put_byte(b'?'); | ||
| 260 | } | ||
| 261 | } | ||
| 262 | |||
| 237 | fn csi_final(b: u8) { | 263 | fn csi_final(b: u8) { |
| 238 | match b { | 264 | match b { |
| 239 | // Cursor position (1-based; 0 means 1) | 265 | // Cursor position (1-based; 0 means 1) |
| @@ -392,19 +418,46 @@ fn scroll_up() { | |||
| 392 | // public writing API | 418 | // public writing API |
| 393 | // --------------------------------------------------------------------------- | 419 | // --------------------------------------------------------------------------- |
| 394 | 420 | ||
| 395 | /// Write raw bytes (ANSI-capable). Used by full-screen apps. | 421 | /// Write raw bytes (ANSI-capable). Used by full-screen apps. UTF-8 |
| 422 | /// sequences are decoded so Unicode glyphs (box-drawing, arrows) render; | ||
| 423 | /// undecodable bytes become '?'. | ||
| 396 | pub fn write_bytes(b: &[u8]) { | 424 | pub fn write_bytes(b: &[u8]) { |
| 397 | for &x in b { | 425 | let mut i = 0; |
| 398 | put_byte(x); | 426 | while i < b.len() { |
| 427 | let byte = b[i]; | ||
| 428 | if byte < 0x80 { | ||
| 429 | put_byte(byte); | ||
| 430 | i += 1; | ||
| 431 | continue; | ||
| 432 | } | ||
| 433 | // Decode the next UTF-8 character starting at i. | ||
| 434 | match core::str::from_utf8(&b[i..]) { | ||
| 435 | Ok(s) => { | ||
| 436 | let c = s.chars().next().unwrap_or('?'); | ||
| 437 | write_char(c); | ||
| 438 | i += c.len_utf8(); | ||
| 439 | } | ||
| 440 | Err(e) => { | ||
| 441 | let valid = e.valid_up_to(); | ||
| 442 | if valid > 0 { | ||
| 443 | let s = unsafe { core::str::from_utf8_unchecked(&b[i..i + valid]) }; | ||
| 444 | let c = s.chars().next().unwrap_or('?'); | ||
| 445 | write_char(c); | ||
| 446 | i += c.len_utf8(); | ||
| 447 | } else { | ||
| 448 | put_byte(b'?'); | ||
| 449 | i += 1; | ||
| 450 | } | ||
| 451 | } | ||
| 452 | } | ||
| 399 | } | 453 | } |
| 400 | flush(); | 454 | flush(); |
| 401 | } | 455 | } |
| 402 | 456 | ||
| 403 | /// Write a UTF-8 string (plain text; non-ASCII becomes '?'). | 457 | /// Write a UTF-8 string (plain text; Unicode glyphs render, others '?'). |
| 404 | pub fn write_str(s: &str) { | 458 | pub fn write_str(s: &str) { |
| 405 | for c in s.chars() { | 459 | for c in s.chars() { |
| 406 | let b = if (c as u32) < 0x80 { c as u8 } else { b'?' }; | 460 | write_char(c); |
| 407 | put_byte(b); | ||
| 408 | } | 461 | } |
| 409 | flush(); | 462 | flush(); |
| 410 | } | 463 | } |
