tty — terminal input
For programs that react to one key at a time, like editors and menus. Normally a terminal hands lines over only after Enter; until then the kernel collects characters, echoes them and handles backspace (line buffering and echo). To move the cursor the moment an arrow key is pressed, that convenience gets in the way, so the terminal is switched to raw mode, receiving “immediately, without echo”. And an arrow key is not one byte — ↑ is the three bytes ESC [ A, Delete the four bytes ESC [ 3 ~. parse_key reads such byte strings as keys.
let p option u64 tty.parse_key buf. 0 . .
guard is_some p. . else return none . .
let code u64 tty.key_of some_value p. . . .
let used usize tty.len_of some_value p. . . .Raw mode changes the user’s terminal settings, and the change survives the program’s death
cap tty may use it — calling tty.raw without the capability is E-CAP-MISSING at compile time. At the same time, putting key parsing behind the capability too would make it untestable without a real terminal. So it was split in half — the pure half is much larger. The capability half is just three: raw mode, reading and screen size; key parsing (here) and ANSI assembly, screen diffs and display width (term) are pure (chapter 37).One keycode distinguishes characters from special keys. Ordinary bytes are their own value (1 … 255, control characters included — Ctrl-C is 3), and special keys are above
- So
is_charis a singlelt keycode. 1000 .. With no tuples, the result packs into one valuekeycode × 16 + length(length 1 … 8). The length is needed to know where the
next key starts in the buffer. Extract with key_of and len_of so code survives if the packing changes.
| op | Layer | What it does |
|---|---|---|
key_up · key_down · key_right · key_left | pure | 1001 · 1002 · 1003 · 1004 |
key_home · key_end · key_delete · key_pageup · key_pagedown · key_esc | pure | 1005 · 1006 · 1007 · 1008 · 1009 · 1010 |
key_unknown | pure | 1011 — an unrecognised special key |
parse_key buf at | pure (effects none) | byte string → some (keycode × 16 + length) or none |
key_of · len_of · is_char | pure | keycode · length from a packed value · is it an ordinary character (pass a keycode) |
tty.raw t. on. . | cap tty | enter (true) · leave (false) raw mode |
tty.read t. dst. . | cap tty | bytes that have arrived into the buffer → option u64 (some 0 . = nothing yet, not an error) |
tty.size t. . | cap tty | screen size → option u64 (rows = div v. 4294967296 ., columns = mod v. 4294967296 .) |
Table 50.1 — Ops of tty and the core ops
What parse_key recognises — one non-ESC byte (length 1), ESC with nothing after (the ESC key, length 1), ESC O A–D · H · F and ESC [ A–D · H · F (arrows, Home, End, length 3), ESC [ 1 ~ · 3 ~ · 4 ~ · 5 ~ · 6 ~ (Home, Delete, End, PageUp, PageDown, length 4). Incomplete sequences are not invented — with only ESC [ arrived it is none, not a guess at “probably an arrow”. One exception — a lone ESC with nothing after is the ESC key (waiting forever is not an option). If ESC is followed by neither [ nor O, it answers ESC key + length 1 (on terminals sending Alt combinations as ESC + character, the next call reads the character).
module keydemo .
use tty .
proc main input t cap tty . input al cap allocator . output u8 . effects alloc . do
let g option mut slice u8 alloc_bytes al. capacity 32 . .
guard is_some g. . else return 1 . .
let buf mut slice u8 some_value g. . .
guard tty.raw t. true . else return 1 . .
var going bool true .
var last u64 0 .
while going. do
let n option usize tty.read t. buf. . .
guard is_some n. . else do
set going. false .
continue .
end .
let got usize cast usize some_value n. . . .
var off usize 0 .
while lt off. got. . do
let p option u64 tty.parse_key subslice buf. 0 got. . off. . .
guard is_some p. . else do
set off. got. .
continue .
end .
let code u64 tty.key_of some_value p. . . .
set last. code. .
if eq code. 113 . do set going. false . end .
set off. add off. tty.len_of some_value p. . . . .
end .
end .
let r bool tty.raw t. false . .
return narrow u8 last. . .
end .Finish anything that can fail, like allocation, before entering raw mode — failing and returning after entering leaves the terminal raw. Terminal core ops take cap tty as an argument — the op holding the capability is not enough; it must be handed over where it is used. parse_key is effects none, so all key parsing is verified by VM/native comparison without a terminal — the dividend of the split.
Counter-example. Not restoring raw mode
guard … else return 2 after tty.raw t. true . skips restoration and leaves the shell broken. The language has no defer, so call tty.raw t. false . on every way out, or finish what can fail before entering raw mode. The example above is shaped so the loop always exits downwards.Counter-example. Reading none as an error and quitting · advancing by 1 regardless of length
none from parse_key usually means “more bytes needed” — a read boundary can cut a sequence in the middle. Quitting there makes a program that dies on every arrow key. set off. add off. 1 . reads ESC [ A as three keys — always advance by len_of. Comparing the packed value directly with 1001 fails because the length is mixed in.Cautions. Terminals differ in arrow sequences, so both ESC [ A and ESC O A are recognised. Sequences still not caught are none — never silently another key. Korean input comes from parse_key one byte at a time, so assemble code points with utf8. “Character” in is_char means “not a special key”, not “printable”. In raw mode Ctrl-C arrives as byte 3, not a signal. Screen size changes (SIGWINCH) are not reported — ask tty.size again each frame if needed. Mouse reports, bracketed paste and parsing terminal query replies were not built.