ajhahn.de
← Flash
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 }
    }
}