// flibc key decoder — raw console bytes → semantic Key events. // // The input half of FlashOS's shell-first navigation (the full-screen tools). // A full-screen tool puts the console in mode 0 (raw: kernel echo off, // byte-at-a-time — the same mode /bin/login's password loop relies on) and // calls readKey() in a loop until it returns .eof. The pure Decoder turns a // byte stream — including the multi-byte ESC-[ A/B/C/D arrow sequences — into // Key values and is host-tested; the SVC-driven readKey() driver is gated // behind has_driver exactly like readline's, so the host build never analyses // inline asm. // // No allocator, no module state beyond the caller-held Decoder. Zero footprint // until referenced — no boot binary calls readKey yet (the first consumer, // /bin/mon, lands with the goal.md §4 hardware monitor). const builtin = #import("builtin") // Driver compiles only on aarch64-freestanding (the real flibc target); the // host-test build flips this off so the SVC trampoline never enters semantic // analysis. Only the pure Decoder is exercised on host. const has_driver = builtin.cpu.arch == .aarch64 && builtin.target.os.tag == .freestanding /// A decoded key. `.char` carries its byte in Event.ch; `.none` means the byte /// was consumed mid-escape-sequence (feed more); `.eof` means the stream closed. /// The navigation set (delete / home / end / page_up / page_down) and the editor /// command chords (ctrl_o / ctrl_w / ctrl_x) land here for /bin/edit; a pager or /// readline that does not use them lets them fall through its switch default. pub const Key = enum { up, down, left, right, enter, backspace, delete, tab, escape, home, end, page_up, page_down, ctrl_c, ctrl_d, ctrl_o, ctrl_w, ctrl_x, char, none, eof, } /// A key event. `ch` is meaningful only for `.char`. pub const Event = struct { key Key, ch u8 = 0, } /// Incremental VT100 input decoder. Feed it one byte at a time; it returns /// `.none` while inside an ESC sequence and a real key when one completes. A /// fresh Decoder per readKey() call is correct — a whole sequence is consumed /// within one call. pub const Decoder = struct { state State = .ground, param u16 = 0, // accumulates the CSI numeric parameter (ESC[~) const State = enum { ground, esc, csi } pub fn feed(self *mut Decoder, b u8) Event { return switch self.state { .ground => self.atGround(b), .esc => self.atEsc(b), .csi => self.atCsi(b), } } fn atGround(self *mut Decoder, b u8) Event { return switch b { 0x1b => blk: { self.state = .esc break :blk Event{ .key = .none } }, '\r', '\n' => .{ .key = .enter }, '\t' => .{ .key = .tab }, 0x08, 0x7f => .{ .key = .backspace }, 0x03 => .{ .key = .ctrl_c }, 0x04 => .{ .key = .ctrl_d }, 0x0f => .{ .key = .ctrl_o }, 0x17 => .{ .key = .ctrl_w }, 0x18 => .{ .key = .ctrl_x }, 0x20...0x7e => .{ .key = .char, .ch = b }, else => .{ .key = .none }, } } fn atEsc(self *mut Decoder, b u8) Event { if b == '[' { self.state = .csi self.param = 0 return .{ .key = .none } } if b == 0x1b { // A second ESC — stay pending on the newer one. return .{ .key = .none } } // ESC then anything else: a bare Escape; the trailing byte is dropped // (Alt- chords are out of scope for v1). self.state = .ground return .{ .key = .escape } } fn atCsi(self *mut Decoder, b u8) Event { // Digits accumulate the parameter so ESC[3~ (Delete), ESC[5~ (PgUp) etc. // are distinguished rather than collapsed. A ';' starts a sub-parameter // (e.g. ESC[1;5C modified arrows); reset so the final group wins — the // arrow/tilde keys ignore modifiers here. if b >= '0' && b <= '9' { self.param = self.param * 10 + #as(u16, b - '0') return .{ .key = .none } } if b == ';' { self.param = 0 return .{ .key = .none } } self.state = .ground return switch b { 'A' => .{ .key = .up }, 'B' => .{ .key = .down }, 'C' => .{ .key = .right }, 'D' => .{ .key = .left }, 'H' => .{ .key = .home }, // ESC[H — Home (no parameter form) 'F' => .{ .key = .end }, // ESC[F — End '~' => switch self.param { 1, 7 => .{ .key = .home }, // ESC[1~ / ESC[7~ 3 => .{ .key = .delete }, // ESC[3~ 4, 8 => .{ .key = .end }, // ESC[4~ / ESC[8~ 5 => .{ .key = .page_up }, // ESC[5~ 6 => .{ .key = .page_down }, // ESC[6~ else => .{ .key = .none }, }, else => .{ .key = .none }, } } } /// Block until one whole key is read from fd 0. Returns `.eof` when the stream /// closes. Use inside a full-screen loop; pair with console_ui.screen.enter / /// leave and console mode 0. pub const readKey = driver.readKey const driver = if (has_driver) struct { const sys = #import("syscalls.zig") pub fn readKey() Event { var dec = Decoder{} var b u8 = 0 while true { const n = sys.read(0, #ptrCast(&b), 1) if n <= 0 { return .{ .key = .eof } } const ev = dec.feed(b) if ev.key != .none { return ev } } } } else struct { // Host-test stub: present only so the `pub const readKey` binding succeeds. pub fn readKey() Event { return .{ .key = .eof } } } // ---- host tests ------------------------------------------------------------ const std = #import("std") const testing = std.testing fn decodeOne(seq []u8) Event { var d = Decoder{} var last Event = .{ .key = .none } for b in seq { last = d.feed(b) if last.key != .none { return last } } return last } test "printable byte decodes to char" { const e = decodeOne("a") try testing.expectEqual(Key.char, e.key) try testing.expectEqual(#as(u8, 'a'), e.ch) } test "CR and LF decode to enter" { try testing.expectEqual(Key.enter, decodeOne("\r").key) try testing.expectEqual(Key.enter, decodeOne("\n").key) } test "tab decodes to tab" { try testing.expectEqual(Key.tab, decodeOne("\t").key) } test "ctrl-c and ctrl-d" { try testing.expectEqual(Key.ctrl_c, decodeOne(&.{0x03}).key) try testing.expectEqual(Key.ctrl_d, decodeOne(&.{0x04}).key) } test "arrow sequences decode through ESC [ A..D" { try testing.expectEqual(Key.up, decodeOne("\x1b[A").key) try testing.expectEqual(Key.down, decodeOne("\x1b[B").key) try testing.expectEqual(Key.right, decodeOne("\x1b[C").key) try testing.expectEqual(Key.left, decodeOne("\x1b[D").key) } test "parametrized CSI (ESC[5~) decodes on the terminator, not before" { var d = Decoder{} try testing.expectEqual(Key.none, d.feed(0x1b).key) try testing.expectEqual(Key.none, d.feed('[').key) try testing.expectEqual(Key.none, d.feed('5').key) // parameter byte buffered try testing.expectEqual(Key.page_up, d.feed('~').key) // terminator yields the key } test "bare ESC then a letter yields escape" { var d = Decoder{} try testing.expectEqual(Key.none, d.feed(0x1b).key) try testing.expectEqual(Key.escape, d.feed('x').key) } test "editor command chords decode at ground" { try testing.expectEqual(Key.ctrl_o, decodeOne(&.{0x0f}).key) try testing.expectEqual(Key.ctrl_w, decodeOne(&.{0x17}).key) try testing.expectEqual(Key.ctrl_x, decodeOne(&.{0x18}).key) } test "tilde navigation sequences decode by parameter" { try testing.expectEqual(Key.home, decodeOne("\x1b[1~").key) try testing.expectEqual(Key.delete, decodeOne("\x1b[3~").key) try testing.expectEqual(Key.end, decodeOne("\x1b[4~").key) try testing.expectEqual(Key.page_up, decodeOne("\x1b[5~").key) try testing.expectEqual(Key.page_down, decodeOne("\x1b[6~").key) try testing.expectEqual(Key.home, decodeOne("\x1b[7~").key) try testing.expectEqual(Key.end, decodeOne("\x1b[8~").key) } test "letter Home and End decode (ESC[H / ESC[F)" { try testing.expectEqual(Key.home, decodeOne("\x1b[H").key) try testing.expectEqual(Key.end, decodeOne("\x1b[F").key) } test "modified arrow (ESC[1;5C) still decodes to a plain arrow" { try testing.expectEqual(Key.right, decodeOne("\x1b[1;5C").key) } test "an unknown tilde parameter is absorbed, not leaked" { try testing.expectEqual(Key.none, decodeOne("\x1b[99~").key) }