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 | |
| 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')
| -rw-r--r-- | rsnix/src/gop.rs | 161 | ||||
| -rw-r--r-- | rsnix/src/ps2.rs | 24 | ||||
| -rw-r--r-- | rsnix/src/vt.rs | 87 |
3 files changed, 247 insertions, 25 deletions
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 { | |||
| 135 | } | 135 | } |
| 136 | 136 | ||
| 137 | /// Blit one glyph cell at (col, row) with explicit fg/bg colours. | 137 | /// Blit one glyph cell at (col, row) with explicit fg/bg colours. |
| 138 | /// Procedurally-rendered bitmaps for Unicode glyphs missing from the | ||
| 139 | /// 256-glyph Latin-1 font: box-drawing (U+2500–U+257F), arrows | ||
| 140 | /// (U+2190–U+2193) and the em dash (U+2014). Box drawing is pure | ||
| 141 | /// geometry, so these are computed, not stored. Same format as the font: | ||
| 142 | /// one byte per row, bit 7 = leftmost column. | ||
| 143 | fn unicode_glyph_bits(ch: u16) -> Option<[u8; 16]> { | ||
| 144 | let mut g = [0u8; 16]; | ||
| 145 | match ch { | ||
| 146 | // ─ light horizontal, ━ heavy horizontal | ||
| 147 | 0x2500 => { | ||
| 148 | g[7] = 0xFF; | ||
| 149 | g[8] = 0xFF; | ||
| 150 | } | ||
| 151 | 0x2501 => { | ||
| 152 | g[7] = 0xFF; | ||
| 153 | g[8] = 0xFF; | ||
| 154 | g[9] = 0xFF; | ||
| 155 | } | ||
| 156 | // │ light vertical, ┃ heavy vertical | ||
| 157 | 0x2502 => { | ||
| 158 | for y in 0..16 { | ||
| 159 | g[y] = 0x18; | ||
| 160 | } | ||
| 161 | } | ||
| 162 | 0x2503 => { | ||
| 163 | for y in 0..16 { | ||
| 164 | g[y] = 0x3C; | ||
| 165 | } | ||
| 166 | } | ||
| 167 | // corners ┌ ┐ └ ┘ | ||
| 168 | 0x250C => { | ||
| 169 | g[7] = 0xF0; | ||
| 170 | g[8] = 0xF8; | ||
| 171 | for y in 9..16 { | ||
| 172 | g[y] = 0x18; | ||
| 173 | } | ||
| 174 | } | ||
| 175 | 0x2510 => { | ||
| 176 | g[7] = 0x0F; | ||
| 177 | g[8] = 0x1F; | ||
| 178 | for y in 9..16 { | ||
| 179 | g[y] = 0x18; | ||
| 180 | } | ||
| 181 | } | ||
| 182 | 0x2514 => { | ||
| 183 | for y in 0..7 { | ||
| 184 | g[y] = 0x18; | ||
| 185 | } | ||
| 186 | g[7] = 0xF8; | ||
| 187 | g[8] = 0xF0; | ||
| 188 | } | ||
| 189 | 0x2518 => { | ||
| 190 | for y in 0..7 { | ||
| 191 | g[y] = 0x18; | ||
| 192 | } | ||
| 193 | g[7] = 0x1F; | ||
| 194 | g[8] = 0x0F; | ||
| 195 | } | ||
| 196 | // T junctions ├ ┤ ┬ ┴ and cross ┼ | ||
| 197 | 0x251C => { | ||
| 198 | for y in 0..16 { | ||
| 199 | g[y] = 0x18; | ||
| 200 | } | ||
| 201 | g[7] = 0xF8; | ||
| 202 | g[8] = 0xF8; | ||
| 203 | } | ||
| 204 | 0x2524 => { | ||
| 205 | for y in 0..16 { | ||
| 206 | g[y] = 0x18; | ||
| 207 | } | ||
| 208 | g[7] = 0x1F; | ||
| 209 | g[8] = 0x1F; | ||
| 210 | } | ||
| 211 | 0x252C => { | ||
| 212 | g[7] = 0xFF; | ||
| 213 | g[8] = 0xFF; | ||
| 214 | for y in 9..16 { | ||
| 215 | g[y] = 0x18; | ||
| 216 | } | ||
| 217 | } | ||
| 218 | 0x2534 => { | ||
| 219 | for y in 0..7 { | ||
| 220 | g[y] = 0x18; | ||
| 221 | } | ||
| 222 | g[7] = 0xFF; | ||
| 223 | g[8] = 0xFF; | ||
| 224 | } | ||
| 225 | 0x253C => { | ||
| 226 | for y in 0..16 { | ||
| 227 | g[y] = 0x18; | ||
| 228 | } | ||
| 229 | g[7] = 0xFF; | ||
| 230 | g[8] = 0xFF; | ||
| 231 | } | ||
| 232 | // ═ double horizontal, ║ double vertical | ||
| 233 | 0x2550 => { | ||
| 234 | g[6] = 0xFF; | ||
| 235 | g[9] = 0xFF; | ||
| 236 | } | ||
| 237 | 0x2551 => { | ||
| 238 | for y in 0..16 { | ||
| 239 | g[y] = 0x24; | ||
| 240 | } | ||
| 241 | } | ||
| 242 | // — em dash | ||
| 243 | 0x2014 => { | ||
| 244 | g[7] = 0xFF; | ||
| 245 | g[8] = 0xFF; | ||
| 246 | } | ||
| 247 | // ← → ↑ ↓ | ||
| 248 | 0x2190 => { | ||
| 249 | g[8] = 0xFF; | ||
| 250 | g[6] = 0xC0; | ||
| 251 | g[7] = 0xC0; | ||
| 252 | g[9] = 0xC0; | ||
| 253 | g[10] = 0xC0; | ||
| 254 | } | ||
| 255 | 0x2192 => { | ||
| 256 | g[8] = 0xFF; | ||
| 257 | g[6] = 0x03; | ||
| 258 | g[7] = 0x03; | ||
| 259 | g[9] = 0x03; | ||
| 260 | g[10] = 0x03; | ||
| 261 | } | ||
| 262 | 0x2191 => { | ||
| 263 | g[0] = 0x18; | ||
| 264 | g[1] = 0x3C; | ||
| 265 | g[2] = 0x7E; | ||
| 266 | for y in 3..16 { | ||
| 267 | g[y] = 0x18; | ||
| 268 | } | ||
| 269 | } | ||
| 270 | 0x2193 => { | ||
| 271 | for y in 0..13 { | ||
| 272 | g[y] = 0x18; | ||
| 273 | } | ||
| 274 | g[13] = 0x7E; | ||
| 275 | g[14] = 0x3C; | ||
| 276 | g[15] = 0x18; | ||
| 277 | } | ||
| 278 | _ => return None, | ||
| 279 | } | ||
| 280 | Some(g) | ||
| 281 | } | ||
| 282 | |||
| 138 | pub(crate) fn blit_glyph(col: usize, row: usize, ch: u16, fg: (u8, u8, u8), bg: (u8, u8, u8)) { | 283 | pub(crate) fn blit_glyph(col: usize, row: usize, ch: u16, fg: (u8, u8, u8), bg: (u8, u8, u8)) { |
| 139 | if let Some(fb) = unsafe { FB.as_ref() } { | 284 | if let Some(fb) = unsafe { FB.as_ref() } { |
| 140 | let glyph_idx = if ch < 256 { ch as usize } else { 32 }; | ||
| 141 | let glyph = font::GLYPH_8X16; | 285 | let glyph = font::GLYPH_8X16; |
| 142 | let data = match glyph.get(glyph_idx) { | 286 | let data = if ch < 256 { |
| 143 | Some(d) => *d, | 287 | match glyph.get(ch as usize) { |
| 144 | None => return, | 288 | Some(d) => *d, |
| 289 | None => return, | ||
| 290 | } | ||
| 291 | } else if let Some(bits) = unicode_glyph_bits(ch) { | ||
| 292 | bits | ||
| 293 | } else { | ||
| 294 | match glyph.get(32) { | ||
| 295 | Some(d) => *d, | ||
| 296 | None => return, | ||
| 297 | } | ||
| 145 | }; | 298 | }; |
| 146 | 299 | ||
| 147 | let fg = rgb(fg.0, fg.1, fg.2); | 300 | 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<KeyEvent> { | |||
| 337 | update_modifiers(byte, true, true); | 337 | update_modifiers(byte, true, true); |
| 338 | 338 | ||
| 339 | match byte { | 339 | match byte { |
| 340 | 0x48 => Some(if ctrl_held() { KeyEvent::CtrlArrow(0) } else { KeyEvent::ArrowUp }), | 340 | 0x48 => Some(if ctrl_held() { |
| 341 | 0x50 => Some(if ctrl_held() { KeyEvent::CtrlArrow(1) } else { KeyEvent::ArrowDown }), | 341 | KeyEvent::CtrlArrow(0) |
| 342 | 0x4B => Some(if ctrl_held() { KeyEvent::CtrlArrow(2) } else { KeyEvent::ArrowLeft }), | 342 | } else { |
| 343 | 0x4D => Some(if ctrl_held() { KeyEvent::CtrlArrow(3) } else { KeyEvent::ArrowRight }), | 343 | KeyEvent::ArrowUp |
| 344 | }), | ||
| 345 | 0x50 => Some(if ctrl_held() { | ||
| 346 | KeyEvent::CtrlArrow(1) | ||
| 347 | } else { | ||
| 348 | KeyEvent::ArrowDown | ||
| 349 | }), | ||
| 350 | 0x4B => Some(if ctrl_held() { | ||
| 351 | KeyEvent::CtrlArrow(2) | ||
| 352 | } else { | ||
| 353 | KeyEvent::ArrowLeft | ||
| 354 | }), | ||
| 355 | 0x4D => Some(if ctrl_held() { | ||
| 356 | KeyEvent::CtrlArrow(3) | ||
| 357 | } else { | ||
| 358 | KeyEvent::ArrowRight | ||
| 359 | }), | ||
| 344 | 0x47 => Some(KeyEvent::Home), | 360 | 0x47 => Some(KeyEvent::Home), |
| 345 | 0x4F => Some(KeyEvent::End), | 361 | 0x4F => Some(KeyEvent::End), |
| 346 | 0x49 => Some(KeyEvent::PageUp), | 362 | 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 { | |||
| 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 | } |
