Skip to content

IO.read_char — single-character input portable across terminal and browser targets #618

@aallan

Description

@aallan

Vera version: vera 0.0.138
Origin: Friction surfaced while writing terminal Tetris on Vera 0.0.138.

Problem

Single-character input is currently impossible at any target. IO.read_line is line-buffered, so real-time CLI programs (Tetris-class games, paced REPLs, navigation tools that respond to keystrokes) can't be written in Vera at all — neither for terminal nor for browser.

Proposal

Single new operation in the existing IO effect:

effect IO {
  ...
  op read_char(Unit -> @Result<String, String>);
  ...
}

Returns one character (as a one-char String) or an Err on EOF / closed input. The runtime handles raw-mode entry/exit transparently per call — no separate set_raw_mode operation in the user-facing API.

Host implementations

  • Python wasmtime host (vera/codegen/api.py): termios.tcsetattr context manager on Unix (~5 lines, stdlib), msvcrt.getch on Windows (no separate raw-mode call). Handle EOF / interrupt cleanly.
  • Browser runtime (runtime.mjs): a keypress event listener pushes characters into a queue; read_char either pops the head or suspends-and-resumes via JSPI on the next keypress. Same suspend/resume primitive that #609 uses for IO.sleep.

What this unlocks

Real-time CLI programs that compile cleanly to either target with no source changes. Pairs with:

  • #609IO.sleep portable across targets (timing)
  • #610 — ANSI subset interpreter in browser (rendering)

Together those three close the input/timing/rendering trio for write-once-run-anywhere real-time programs (#608 umbrella).

Out of scope (separable filings)

  • set_raw_mode — explicit lifecycle control over raw mode. The implicit-per-call approach above covers the common case.
  • enable_alternate_screen — useful but not blocking; can be filed alongside #610.
  • get_size(rows, cols) from ioctl(TIOCGWINSZ) / GetConsoleScreenBufferInfo / window.innerWidth. Useful but separable.

Design alignment

DESIGN.md commits to WebAssembly both native and browser as first-class targets:

Target: WebAssembly (native + browser) — Portable, sandboxed, no ambient capabilities; vera run uses wasmtime; vera compile --target browser emits a JS bundle

A capability available on only one target violates that commitment. Single-character input has to be portable just as #609 made IO.sleep portable — both directions of the terminal-vs-browser seam need symmetric treatment.

Adding IO.read_char as a single new operation in the existing IO effect (rather than a new <Terminal> effect with multiple operations) follows precedent: IO.sleep, IO.time, and IO.stderr were added to existing IO in v0.0.114 (issue #463) rather than spun off into a new effect. Smaller surface, fewer effect labels, same expressivity.

Origin

Friction document from the terminal Tetris experiment.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions