Flash 150 lines
// keys — flibc's console key decoder, ported to Flash from its hand-written
// Zig. It turns the raw byte stream of a raw-mode console (kernel echo off,
// byte-at-a-time — the mode /bin/login's password loop relies on) into
// semantic Key events, including the multi-byte ESC-[ A/B/C/D arrow
// sequences. This is the input half of FlashOS's full-screen tools.
//
// The pure Decoder is a three-state VT100 machine (ground → esc → csi) and is
// host-testable in isolation. The SVC-driven readKey() loop is gated behind
// has_driver exactly like readline's and execvp's, so the host build never
// analyses the aarch64 syscall path. No allocator, no module state beyond the
// caller-held Decoder; zero footprint until referenced (no boot binary calls
// readKey yet — its first consumer, the /bin/mon hardware monitor, is not yet
// ported).
//
// The first leaf that is a pure port: it adds no new grammar. The byte state
// machine reuses what earlier leaves already landed — value and multi-pattern
// switch prongs, an inclusive `0x20...0x7e` range prong, a labeled-block prong
// (`blk: { … break :blk … }`), the driver-select `if (has_driver) struct {…}
// else struct {…}`, and `&&` for the comptime gate. Its core lowers to Zig
// whose token stream matches the reference.
use 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.
pub const Key = enum {
up,
down,
left,
right,
enter,
backspace,
tab,
escape,
ctrl_c,
ctrl_d,
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,
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 .{ .key = .none }
},
'\r', '\n' => .{ .key = .enter },
'\t' => .{ .key = .tab },
0x08, 0x7f => .{ .key = .backspace },
0x03 => .{ .key = .ctrl_c },
0x04 => .{ .key = .ctrl_d },
0x20...0x7e => .{ .key = .char, .ch = b },
else => .{ .key = .none },
}
}
fn atEsc(self *mut Decoder, b u8) Event {
if b == '[' {
self.state = .csi
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 {
// Parameter bytes (digits / ';') belong to the sequence — keep reading
// so ESC[5~ (PgUp etc.) is absorbed cleanly rather than leaking bytes.
if (b >= '0' && b <= '9') || b == ';' {
return .{ .key = .none }
}
self.state = .ground
return switch b {
'A' => .{ .key = .up },
'B' => .{ .key = .down },
'C' => .{ .key = .right },
'D' => .{ .key = .left },
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 {
use "syscalls" as sys
pub fn readKey() Event {
var dec = Decoder{}
var b u8 = 0
while true {
n := sys.read(0, #ptrCast(&b), 1)
if n <= 0 {
return .{ .key = .eof }
}
ev := dec.feed(b)
if ev.key != .none {
return ev
}
}
}
// Host-test stub: present only so the `pub const readKey` binding succeeds.
} else struct {
pub fn readKey() Event {
return .{ .key = .eof }
}
}