Flash 260 lines
// 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[<n>~)
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-<key> 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)
}