MMotor OS
System

Terminals

Motor OS has no kernel pty, no termios, no line discipline, and no signals. A "terminal" is an ordinary userspace program that provides terminal behavior, ANSI interpretation, interactive input, and the size protocol below, over plain stdio pipes. Nothing in the kernel knows what a terminal is.

The in-tree terminal providers are:

Nesting composes with no special cases: rush inside rmux inside SSH, each hop a terminal to the next. Programs built on std::io::IsTerminal, C isatty, or crossterm work unmodified: what they see is a stdio pipe whose far end behaves like a terminal, and is_terminal() tells them whether it does.

What is_terminal() means

Terminal status is immutable metadata on a descriptor, fixed when the descriptor is created. A provider marks the pipes it creates for a child by putting MOTURUS_STDIO_IS_TERMINAL=true in the child's spawn environment; the runtime consumes the key during spawn and it never becomes process state. Consequently:

Motor reserves descriptor 3 as the terminal stream (moto_rt::FD_TERMINAL). It is present exactly when a process belongs to a terminal session but its stdin is not that terminal, for example the last stage of a pipeline, or program < file. Keys and size reports arrive on it; it is read-only. The spawning runtime creates it automatically, so a full-screen program at the end of a pipeline still gets its keys. Background jobs do not get it, because Motor has no way to stop them from stealing input, and a spawner can suppress it with MOTURUS_STDIO_NO_TERMINAL.

Terminal size

On Linux the kernel stores a per-tty window size and delivers SIGWINCH when it changes. Motor OS keeps the useful half of that split, discovery happens once at the component that owns the terminal and applications get a first size and a change event, but with no signal available the change event travels in the byte stream the program is already reading.

The carrier is DEC private mode 2048, an emerging convention that foot, Ghostty, iTerm2, and kitty also implement:

SequenceDirectionMeaning
CSI ? 2048 happ to terminalsubscribe; answered at once with a report
CSI ? 2048 lapp to terminalunsubscribe
CSI ? 2048 $ papp to terminaldo you support this?
CSI ? 2048 ; 1 $ y / ; 2 $ yterminal to appsubscribed / not subscribed
CSI 48 ; rows ; cols ; h_px ; w_px tterminal to appthe report (rows before columns)

A report cannot arrive early enough for a program's first frame, so an owner that already knows the size, rmux for a pane or russhd for a pty session, puts it in $COLUMNS and $LINES when it spawns the child, and rush re-exports them before every command. Only the physical console lacks this, because nobody there knows the size in advance: a full-screen program on the console paints its first frame at 80x24 and repaints when the first report arrives.

Most host terminals do not implement mode 2048, so a client tries three methods in order and falls back only while the better one goes unanswered: the subscription; the one-shot CSI 18 t question; and finally the cursor-position probe in the bottom-right corner. Polling backs off from 250 ms to ten seconds and then thirty, and a confirmed subscription silences polling entirely. russhd answers the sequences itself and swallows them, because a sequence passed through would be answered a second time by the user's own terminal with a different size. crossterm's Motor backend does all of this for every TUI program, turning a report into Event::Resize, so red, rush, and the rmux client need no Motor-specific code.

Ctrl+C

Ctrl+C is terminal control, not terminal data. The provider that writes terminal input into a child's stdin scans for byte 0x03 at that one point; relays below it carry the event in the pipe and never rescan data, so 0x03 in an ordinary pipe or file stays an ordinary byte.

rush treats an exit status of 130 as an interrupt: $? stays 130, a pending trap INT runs once, and the rest of the pipeline, list, or loop is abandoned. Motor has no process groups, so foreground ownership is the relay route, not a group broadcast.

Stdio redirection

A child can be given a pipe, nothing, an inherited descriptor, or a real file. The last case has two implementations, and which one a program gets is decided by the shell:

Direct transferRelay
what the child getsa real file descriptor of its owna pipe
who owns the offsetthe child, independentlythe parent's live file
chosen whena fresh redirect to a regular file on a single commanda file-backed stream is inherited, or shared by several commands
file identity visible to the childyesno

The direct route exists because a program may need to know what its stdout is: ripgrep refuses to search the file it is writing, and with a pipe it cannot tell, so rg alpha . >> results.txt would find alpha in its own output. The relay exists because a snapshot of an offset is safe exactly once: sequential grandchildren inheriting a file must share one live offset or they would overwrite each other. In rush, cmd > f, cmd > f &, and cmd 1> f 2>&1 use the direct route; compound commands, functions, pipelines, and builtins with a redirect use a pipe.

wait() includes the completion of a child's file relays, so a shell cannot start the next command while the previous one's output is still in flight. When a parent with live relays exits, bytes a child was told were written are never dropped. Measured on a 64 MiB payload, the direct route ran at 376 MiB/s and the relay at 524 MiB/s with a few megabytes of memory each, against 130 MiB/s and 203 MiB for the old approach of capturing the child's output and writing the file afterwards.

Security properties

is_terminal() guarantees descriptor consistency, nothing more. It does not prove a human is present, identify the peer, or grant authority; a process creator can always construct an endpoint that claims terminal behavior, on Motor as on systems with ptys. Programs may use it to decide whether prompting, colors, or raw-mode editing are practical; code that authorizes an operation must use an explicit policy.

The full design, including the tests that check each rule, is docs/tui.md in the repository.