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. --- CONTEXT.md | 4 +- rsdos/src/main.rs | 18 +++--- rsnix/src/gop.rs | 161 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- rsnix/src/ps2.rs | 24 ++++++-- rsnix/src/vt.rs | 87 +++++++++++++++++++++++------ shell/src/lib.rs | 1 - 6 files changed, 261 insertions(+), 34 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index b557192..20c60f7 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -104,7 +104,9 @@ Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown: - `gop.rs` — framebuffer rasteriser (glyph blits, pixel shifts); the text surface itself lives in `vt` - `vt.rs` — terminal emulator: cell grid, cursor, ANSI CSI parser (H/J/K, - cursor moves, SGR 16-colour, `?25l/h`), dirty-cell blitting + cursor moves, SGR 16-colour, `?25l/h`), dirty-cell blitting, UTF-8 + decoding (box-drawing `│`/`═`, arrows `←↑→↓`, em dash `—` render + procedurally instead of `?`) - `tty.rs` — line discipline: canonical `read_line` (echo, line buffer, ^C/^D) and raw `read_byte` (terminal byte protocol: arrows → `ESC [ A–D`, ^X → `0x18`, …) diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs index dc59e8e..c36660e 100644 --- a/rsdos/src/main.rs +++ b/rsdos/src/main.rs @@ -83,13 +83,17 @@ pub extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut SystemT rsnix::serial_println!("gop: not found (0x{:x})", gop_result); } rsnix::vt::init(); - rsnix::serial_println!("vt: terminal ready ({}x{})", { - let (rows, cols) = rsnix::vt::size(); - rows - }, { - let (rows, cols) = rsnix::vt::size(); - cols - }); + rsnix::serial_println!( + "vt: terminal ready ({}x{})", + { + let (rows, cols) = rsnix::vt::size(); + rows + }, + { + let (rows, cols) = rsnix::vt::size(); + cols + } + ); // ExitBootServices — we now own the machine if unsafe { (bs.exit_boot_services)(image_handle, map_key) } != EFI_SUCCESS { diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs index 81b8945..9cb0d7d 100644 --- a/rsnix/src/gop.rs +++ b/rsnix/src/gop.rs @@ -135,13 +135,166 @@ impl Framebuffer { } /// Blit one glyph cell at (col, row) with explicit fg/bg colours. +/// Procedurally-rendered bitmaps for Unicode glyphs missing from the +/// 256-glyph Latin-1 font: box-drawing (U+2500–U+257F), arrows +/// (U+2190–U+2193) and the em dash (U+2014). Box drawing is pure +/// geometry, so these are computed, not stored. Same format as the font: +/// one byte per row, bit 7 = leftmost column. +fn unicode_glyph_bits(ch: u16) -> Option<[u8; 16]> { + let mut g = [0u8; 16]; + match ch { + // ─ light horizontal, ━ heavy horizontal + 0x2500 => { + g[7] = 0xFF; + g[8] = 0xFF; + } + 0x2501 => { + g[7] = 0xFF; + g[8] = 0xFF; + g[9] = 0xFF; + } + // │ light vertical, ┃ heavy vertical + 0x2502 => { + for y in 0..16 { + g[y] = 0x18; + } + } + 0x2503 => { + for y in 0..16 { + g[y] = 0x3C; + } + } + // corners ┌ ┐ └ ┘ + 0x250C => { + g[7] = 0xF0; + g[8] = 0xF8; + for y in 9..16 { + g[y] = 0x18; + } + } + 0x2510 => { + g[7] = 0x0F; + g[8] = 0x1F; + for y in 9..16 { + g[y] = 0x18; + } + } + 0x2514 => { + for y in 0..7 { + g[y] = 0x18; + } + g[7] = 0xF8; + g[8] = 0xF0; + } + 0x2518 => { + for y in 0..7 { + g[y] = 0x18; + } + g[7] = 0x1F; + g[8] = 0x0F; + } + // T junctions ├ ┤ ┬ ┴ and cross ┼ + 0x251C => { + for y in 0..16 { + g[y] = 0x18; + } + g[7] = 0xF8; + g[8] = 0xF8; + } + 0x2524 => { + for y in 0..16 { + g[y] = 0x18; + } + g[7] = 0x1F; + g[8] = 0x1F; + } + 0x252C => { + g[7] = 0xFF; + g[8] = 0xFF; + for y in 9..16 { + g[y] = 0x18; + } + } + 0x2534 => { + for y in 0..7 { + g[y] = 0x18; + } + g[7] = 0xFF; + g[8] = 0xFF; + } + 0x253C => { + for y in 0..16 { + g[y] = 0x18; + } + g[7] = 0xFF; + g[8] = 0xFF; + } + // ═ double horizontal, ║ double vertical + 0x2550 => { + g[6] = 0xFF; + g[9] = 0xFF; + } + 0x2551 => { + for y in 0..16 { + g[y] = 0x24; + } + } + // — em dash + 0x2014 => { + g[7] = 0xFF; + g[8] = 0xFF; + } + // ← → ↑ ↓ + 0x2190 => { + g[8] = 0xFF; + g[6] = 0xC0; + g[7] = 0xC0; + g[9] = 0xC0; + g[10] = 0xC0; + } + 0x2192 => { + g[8] = 0xFF; + g[6] = 0x03; + g[7] = 0x03; + g[9] = 0x03; + g[10] = 0x03; + } + 0x2191 => { + g[0] = 0x18; + g[1] = 0x3C; + g[2] = 0x7E; + for y in 3..16 { + g[y] = 0x18; + } + } + 0x2193 => { + for y in 0..13 { + g[y] = 0x18; + } + g[13] = 0x7E; + g[14] = 0x3C; + g[15] = 0x18; + } + _ => return None, + } + Some(g) +} + pub(crate) fn blit_glyph(col: usize, row: usize, ch: u16, fg: (u8, u8, u8), bg: (u8, u8, u8)) { if let Some(fb) = unsafe { FB.as_ref() } { - let glyph_idx = if ch < 256 { ch as usize } else { 32 }; let glyph = font::GLYPH_8X16; - let data = match glyph.get(glyph_idx) { - Some(d) => *d, - None => return, + let data = if ch < 256 { + match glyph.get(ch as usize) { + Some(d) => *d, + None => return, + } + } else if let Some(bits) = unicode_glyph_bits(ch) { + bits + } else { + match glyph.get(32) { + Some(d) => *d, + None => return, + } }; let fg = rgb(fg.0, fg.1, fg.2); diff --git a/rsnix/src/ps2.rs b/rsnix/src/ps2.rs index 8dd58a0..4747813 100644 --- a/rsnix/src/ps2.rs +++ b/rsnix/src/ps2.rs @@ -337,10 +337,26 @@ fn process_e0(byte: u8) -> Option { update_modifiers(byte, true, true); match byte { - 0x48 => Some(if ctrl_held() { KeyEvent::CtrlArrow(0) } else { KeyEvent::ArrowUp }), - 0x50 => Some(if ctrl_held() { KeyEvent::CtrlArrow(1) } else { KeyEvent::ArrowDown }), - 0x4B => Some(if ctrl_held() { KeyEvent::CtrlArrow(2) } else { KeyEvent::ArrowLeft }), - 0x4D => Some(if ctrl_held() { KeyEvent::CtrlArrow(3) } else { KeyEvent::ArrowRight }), + 0x48 => Some(if ctrl_held() { + KeyEvent::CtrlArrow(0) + } else { + KeyEvent::ArrowUp + }), + 0x50 => Some(if ctrl_held() { + KeyEvent::CtrlArrow(1) + } else { + KeyEvent::ArrowDown + }), + 0x4B => Some(if ctrl_held() { + KeyEvent::CtrlArrow(2) + } else { + KeyEvent::ArrowLeft + }), + 0x4D => Some(if ctrl_held() { + KeyEvent::CtrlArrow(3) + } else { + KeyEvent::ArrowRight + }), 0x47 => Some(KeyEvent::Home), 0x4F => Some(KeyEvent::End), 0x49 => Some(KeyEvent::PageUp), 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(); } diff --git a/shell/src/lib.rs b/shell/src/lib.rs index ec2474d..4601b98 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs @@ -279,7 +279,6 @@ fn cmd_hexdump(args: &str) { } } - // --------------------------------------------------------------------------- // edit — run the built-in editor (rsdosedit) on a file // --------------------------------------------------------------------------- -- cgit v1.2.3