From 994a2196745637fd0805132711b1ed9354b75426 Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 31 Jul 2026 17:28:31 +0200 Subject: vt: decode UTF-8, render box-drawing/arrow glyphs procedurally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- rsnix/src/vt.rs | 87 ++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 70 insertions(+), 17 deletions(-) (limited to 'rsnix/src/vt.rs') 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 { bg: u8, } -const BLANK: Cell = Cell { ch: 32, fg: 7, bg: 0 }; +const BLANK: Cell = Cell { + ch: 32, + fg: 7, + bg: 0, +}; // ---- screen state ---------------------------------------------------------- @@ -115,7 +119,9 @@ pub fn size() -> (usize, usize) { } pub fn set_cursor_visible(v: bool) { - unsafe { CURSOR_VISIBLE = v; } + unsafe { + CURSOR_VISIBLE = v; + } flush(); } @@ -180,15 +186,7 @@ fn put_byte(b: u8) { CURSOR_C = (CURSOR_C + n).min(COLS - 1); } 0x00..=0x1f => {} // other control chars: ignored - _ => { - let ch = if b < 0x80 { b as u16 } else { b'?' as u16 }; - put_cell(CURSOR_R, CURSOR_C, ch); - CURSOR_C += 1; - if CURSOR_C >= COLS { - CURSOR_C = 0; - newline(); - } - } + _ => put_printable(b as u16), }, Parser::Escape => match b { b'[' => { @@ -234,6 +232,34 @@ fn param(i: usize, default: usize) -> usize { } } +/// 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) { match b { // Cursor position (1-based; 0 means 1) @@ -392,19 +418,46 @@ fn scroll_up() { // public writing API // --------------------------------------------------------------------------- -/// Write raw bytes (ANSI-capable). Used by full-screen apps. +/// 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]) { - for &x in b { - put_byte(x); + 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(); } -/// Write a UTF-8 string (plain text; non-ASCII becomes '?'). +/// Write a UTF-8 string (plain text; Unicode glyphs render, others '?'). pub fn write_str(s: &str) { for c in s.chars() { - let b = if (c as u32) < 0x80 { c as u8 } else { b'?' }; - put_byte(b); + write_char(c); } flush(); } -- cgit v1.2.3