Documentation channel: current development main at compiler checkpoint 1d7e15e. The latest tagged release is v0.0.3; pages identify APIs that are not yet released.

Pseudo-terminals

Release status: Unreleased. This page documents the complete P7-P9 PTY API and P10 application composition at compiler checkpoint 1d7e15e; PTYs are not part of the published v0.0.3 contract.

Spawn

pty_spawn(string program, string[] args) -> pty : PtyError
pty_spawn(string program, string[] args, json options) -> pty : PtyError
pty_spawn(string program, string[] args, cancellation_token token) -> pty : PtyError
pty_spawn(string program, string[] args, json options, cancellation_token token) -> pty : PtyError

Spawn is argv-safe and performs no shell interpretation. Options accept cwd, a string-valued env overlay, and positive integer rows and columns. The initial dimensions default to 24 by 80. POSIX applies cwd through native spawn actions. Windows resolves a bare program through the overlaid child PATH, anchors relative path entries to the effective child cwd, passes an absolute application path to CreateProcessW, and rejects ambiguous drive-relative executable paths.

function drain(pty terminal) -> string : PtyError {
    string transcript := "";
    while (!terminal.eof()) {
        bytes chunk := terminal.read_bytes(4096);
        if (!chunk.empty()) {
            transcript = transcript + chunk.to_string();
        }
    }
    return transcript;
}
function run_terminal(cancellation_token token) -> int : PtyError {
    terminal := pty_spawn("sh", [], {
        "cwd": ".",
        "env": {"TERM": "xterm-256color"},
        "rows": 30,
        "columns": 100
    }, token);
    terminal.write_bytes(bytes.from_string("printf ready; exit 0\n"));
    string transcript := drain(terminal);
    return terminal.wait();
}
function main() -> int {
    return 0;
}

Binary terminal stream

terminal.read_bytes(int_64 max_bytes) -> bytes : PtyError
terminal.write_bytes(bytes data) -> void : PtyError
terminal.eof() -> bool

Standard input, output, and error share one opaque terminal stream. Output and error are therefore merged in terminal order. Strut performs no escape parsing or terminal emulation and exposes no native terminal handle. POSIX preserves arbitrary terminal bytes. ConPTY transports UTF-8/virtual-terminal traffic through byte pipes, so arbitrary non-UTF-8 data, NUL, control bytes, and exact CR/LF round trips are not portable Windows behavior.

Reads are pull-based and return at most the requested bytes. An empty read is EOF only when eof() is true; POSIX master HUP, slave closure, and Linux EIO converge on EOF. Writes have complete-write-or-error semantics and synchronously follow kernel backpressure. There is no unbounded PTY queue.

A handle is a copyable view of shared PTY state. One reader and one writer may run concurrently; a second active operation in the same direction raises PtyError. Descriptor, resize, signal, close, wait, and status races are synchronized across every copy.

Resize and controls

terminal.resize(int rows, int columns) -> void : PtyError
terminal.interrupt() -> void : PtyError
terminal.terminate() -> void : PtyError
terminal.kill() -> void : PtyError
terminal.hangup() -> void : PtyError

resize accepts rows and columns from 1 through 32767. POSIX applies TIOCSWINSZ, relies on kernel SIGWINCH, and maps the four controls to SIGINT, SIGTERM, SIGKILL, and SIGHUP. Before signalling, Strut verifies the pinned leader and terminal session, then targets the foreground or validated leader group. POSIX cannot make foreground lookup and signalling atomic.

Windows controls are terminal/lifecycle approximations, not POSIX signals. Resize calls ResizePseudoConsole. Interrupt writes ETX (0x03) and depends on the child's processed-input console mode. Hangup closes input. Terminate closes unread output and runs bounded pseudo-console/Job cleanup. Kill immediately terminates the Job and reports status 137.

Cancellation

The token supplied at spawn is the only cancellation token for the PTY. Cancellation wakes blocked reads and writes with PtyError("PTY I/O cancelled", 125) and a blocked wait() with PtyError("PTY wait cancelled", 125). It never signals the child. A pre-cancelled token fails before I/O, while completion already observed and cached wins over cancellation.

If cancellation wins while the child is running, a cancelled wait() releases lifecycle ownership so a later wait or close can reap normally. Once close() publishes its cleanup request, close wins over later cancellation, cleanup polling remains bounded, and waiters receive the cached final status.

Lifecycle

terminal.wait() -> int : PtyError
terminal.running() -> bool
terminal.exit_code() -> int
terminal.close() -> void

wait() waits for the direct child and returns one cached exit status to concurrent callers. exit_code() returns -1 while running. close() is idempotent across copies and wakes active I/O. POSIX performs bounded TERM/KILL cleanup and single-owner reaping. Windows creates the child suspended, assigns it to a kill-on-close Job, then resumes it. A weak process monitor caches natural completion and terminates remaining Job descendants without retaining PTY state; dropping the final public copy also cleans up a live child.

If detached-reaper thread creation fails after the cleanup deadline, Strut synchronously reaps rather than abandoning a zombie; this exceptional resource-exhaustion fallback is not latency-bounded. Closing the master lets the kernel hang up the foreground group, but explicit cleanup can safely target only the original leader group. Other background groups in the session and deliberately detached groups may survive; use an external supervisor or cgroup when stronger containment is required.

Platforms

Linux: PTYs require glibc 2.34 or newer for session creation, close-from, and atomic cwd spawn actions. Older glibc and other POSIX libcs compile a deterministic unsupported path rather than using post-fork application code or descriptor enumeration.

macOS: a hidden launcher entry in the current executable becomes the session leader, validates its non-elevated identity and inherited descriptors, claims the already-open slave after session creation, restores standard descriptors, and immediately calls execve with the parent-resolved target and original argv. A close-on-exec status channel reports setup or target-exec failure. Requested values are never evaluated as shell text, and the target retains the launcher's PID and session identity.

Windows: native ConPTY requires Windows 10 version 1809/build 17763 or Windows Server 2019. Entry points are resolved dynamically, so older systems still start and only pty_spawn fails with PtyError("ConPTY requires Windows 10 version 1809 or newer"). ConPTY uses non-inheritable terminal pipes, overlapped host ends, bInheritHandles=FALSE, and a kill-on-close Job. Internal named pipes use system-random names, first-instance enforcement, remote-client rejection, and an owner/SYSTEM-only DACL. Pending reads initiate asynchronous pseudo-console close while draining final output; exited-but-undrained handles do not accumulate close workers. Explicit close and terminate retire output first to avoid the pre-build-26100 ClosePseudoConsole flush deadlock.

WebSocket composition (P10)

P10 certifies composition through ordinary public APIs without adding a bridge API or a runtime dependency in either direction. An application may keep one task in websocket.read() while another copies PTY output in chunks of at most 4096 bytes through synchronous websocket.write_bytes(). Binary messages carry terminal input; text messages should be accepted only as an explicitly validated control vocabulary.

The WebSocket handler remains the lifecycle owner. It passes request.cancellation to pty_spawn, closes the PTY on natural exit, peer close, disconnect, error, or server shutdown, and joins the output task before returning. Both WebSocket and PTY writes are synchronous backpressure boundaries; adding an unbounded queue between them would violate the certified composition.