Terminal

The terminal PLATFORM LAYER (issue #3): is this a terminal, how big is it, and writing to it.

Deliberately the small half. Turning input bytes into semantic key events (Key.Up, Ctrl+C) is #35, and it is pure Plum with no C in it, which is why it is not here. What belongs in a standard library is the part that needs a shim and has no design space to get wrong: isatty is isatty. How to model a modifier key is taste, and taste does not belong in an API nobody can change.

Raw mode and the alternate screen come next, as handle types, so the terminal is restored on every exit path including a panic -- which is the machinery a TUI otherwise writes in C.

enum Stream

Which stream to ask about. A closed set of small integers across the extern boundary, the same shape Os.Mode and Os.Seek use: an extern taking a file descriptor NUMBER would let a caller pass anything at all.

let is_tty (s: Stream): Bool

Whether that stream is attached to a terminal.

This is the check behind "should I use colour?" and "is this being piped somewhere?", and the answer differs per stream: a program in a pipeline routinely has a terminal on stderr and a pipe on stdout. Asking about the wrong one is how progress bars end up in log files.

struct Size

The size of the terminal, in character cells.

let size (): Result[Size, String]

The terminal's size, in character cells, measured on STDOUT. The size of the thing being drawn to. A program whose stdin is a pipe and whose stdout is a terminal still has a size worth knowing.

Err when there is no terminal to ask, which is not a failure the caller caused: output redirected to a file has no size. A program that wants to carry on regardless should pick its own default rather than be handed one, because 80x24 is a guess and this module should not make guesses on anyone's behalf.

Poll this rather than waiting to be told. POSIX reports a resize with SIGWINCH, Plum has no signal handling, and this module is not the place to introduce it. Comparing the size between iterations of an event loop costs one syscall against a redraw, and works the same way on Windows, which has no such signal at all.

let write (text: String): Result[Unit, String]

Writes to stdout with NO trailing newline and NO flush.

Both omissions are deliberate. A terminal program positions its own output with escape sequences, so an automatic newline would corrupt every frame it draws; and flushing per write turns one redraw into hundreds of syscalls. Build a frame, then flush it.

println is still the right thing for line-oriented output. This is for programs that are drawing.

let flush (): Result[Unit, String]

Makes everything written so far visible. Nothing a terminal program draws is on screen until this returns.

handle RawMode

Raw mode, held. The terminal is restored when this value dies, including on a panic.

While it lives, Ctrl+C arrives as a BYTE (0x03) rather than a signal — see Terminal.enter_raw.

handle AltScreen

The alternate screen, held. The previous screen and the user's scrollback come back when this value dies.

handle HiddenCursor

A hidden cursor, held. The cursor is shown again when this value dies, including on a panic — which is what stops a crash leaving a terminal with no cursor.

let enter_raw (): Result[RawMode, String]

Line editing and echo off, so a program sees each keystroke as it happens rather than a line at a time.

Ctrl+C stops being a signal and becomes a byte (0x03). That is what raw mode means everywhere, and here it is also what keeps cleanup working: the default action for SIGINT ends the process without running any cleanup, so a Ctrl+C in raw mode with signals still enabled would leave the terminal raw. Delivered as a byte, a program can exit normally and everything is restored. A raw-mode program is responsible for noticing 0x03 and quitting.

Err when stdin is not a terminal; there is nothing to put into raw mode, and pretending otherwise would leave a program reading a pipe and waiting for keys that cannot come.

let enter_alt_screen (): Result[AltScreen, String]

The xterm alternate screen: a blank screen that leaves the user's scrollback untouched, and gives their shell back exactly as it was. What a full-screen program should use instead of clearing.

let hide_cursor (): Result[HiddenCursor, String]

Hides the cursor while the value lives. A program drawing its own frames does not want a blinking block wherever the last write happened to end.