Terminals
“Terminal” is an overloaded word. It was once a real piece of hardware. Today it is usually a terminal emulator, a program pretending to be that hardware, with a surprising amount of machinery in between. Here is the path from a keypress to a character on screen, and where uncurses plugs in.
Then and now
A terminal used to be hardware: a keyboard and display wired to a host computer by a serial cable. It shipped your keystrokes down the wire and painted whatever came back, obeying coded messages called escape sequences. Today it is a terminal emulator: a program that draws a grid of cells and pretends to be that old hardware. The hardware terminal is mostly gone, but its byte language remains.
flowchart TB
subgraph then["Then"]
direction LR
hw["Keyboard + screen"] <-->|serial cable| big["Computer"]
end
subgraph now["Now"]
direction LR
emu["Terminal emulator"] <-->|pretends to be hardware| prog["Your program"]
end
then ~~~ now
The tty and pty
Your program and the emulator never talk directly. The kernel sits between them as the tty, which is not a dumb pipe: it has a small line editor baked in, the line discipline. In its default cooked mode it edits your line as you type, echoes keystrokes, waits for Enter, and turns Ctrl-C into an interrupt signal. When everything is software, that tty is a pseudo-terminal (PTY): a kernel-provided master/slave pair. The emulator holds the master; your program sees the slave as a normal terminal device.
flowchart TB
you["You press a key"] --> emu["Emulator"]
emu --> tty["PTY + line discipline"]
tty --> app["Program reads input"]
app --> tty2["PTY + line discipline"]
tty2 --> emu2["Emulator paints cells"]
emu2 --> eyes["You see it"]
Both directions are buffered. Keystrokes can queue before they reach you; printed bytes can queue before they get painted. Writing to a terminal does not mean the pixels changed yet, so you decide when to flush.
Cooked vs raw
An interactive program usually wants the tty out of the way: every keystroke as it happens, with arrow keys, Ctrl-C, and pasted bytes delivered as input bytes. So it flips the tty into raw mode, telling the line discipline to step aside.
flowchart TB
subgraph cooked["Cooked mode"]
direction TB
k1["Keys"] --> ld1["Line discipline"] --> p1["Program (whole lines)"]
end
subgraph raw["Raw mode"]
direction TB
k2["Keys"] --> p2["Program (every byte, now)"]
end
cooked ~~~ raw
The trade-off: you inherit every job the tty did for free, from echoing to handling Ctrl-C. The one rule is etiquette. Raw mode is a global change to a shared device, so it is borrowed, not owned: turn it on when you start, and put everything back on the way out.
The Terminal handle
In uncurses, that borrow-and-restore flow lives on the
terminal module’s Terminal handle. It
pairs input and output handles, snapshots the environment, and caches the
pre-raw state returned by make_raw so restore can re-apply it:
use uncurses::terminal::Terminal;
fn main() -> std::io::Result<()> {
let mut term = Terminal::stdio();
term.make_raw()?; // borrow raw mode from the tty
let size = term.get_window_size();
term.restore()?; // hand the tty back exactly as we found it
let size = size?;
println!("{} x {}", size.col, size.row);
Ok(())
}Most apps never touch this directly. Screen
manages this lifecycle for you: init borrows raw mode, and finish() restores
the terminal in one call. pause and resume temporarily leave and re-enter raw
mode. Reach for Terminal::stdio or Terminal::open when you want the raw
connection yourself.
Going deeper
The mechanics, for the curious: termios(3), pty(7), line discipline, pseudoterminal, Windows console modes.