termios Cheat Sheet
Everything about the line discipline on one page.
struct termios {
tcflag_t c_iflag; /* input modes */
tcflag_t c_oflag; /* output modes */
tcflag_t c_cflag; /* control modes — mostly vestigial on a PTY */
tcflag_t c_lflag; /* "local" modes — the interesting ones */
cc_t c_cc[NCCS]; /* control characters */
};
One termios per PTY pair, reachable from either fd. Setting it on the master changes what the
child experiences — almost always a bug.
c_lflag — Local Modes
| Flag | Effect when set | Canonical | Raw |
|---|---|---|---|
ICANON | Line buffering; read() returns a line at a time; VERASE/VKILL editing | ✅ | ❌ |
ECHO | The kernel echoes input to the output side | ✅ | ❌ |
ECHOE | Echo VERASE as BS SP BS (visual erase) | ✅ | ❌ |
ECHOK | Echo VKILL by killing the line | ✅ | ❌ |
ECHONL | Echo NL even with ECHO off | — | ❌ |
ISIG | Generate SIGINT/SIGQUIT/SIGTSTP from VINTR/VQUIT/VSUSP | ✅ | ❌ |
IEXTEN | Implementation-defined processing (VLNEXT ^V, VDISCARD ^O) | ✅ | ❌ |
NOFLSH | Do not flush queues on SIGINT/SIGQUIT/SIGTSTP | ❌ | — |
TOSTOP | SIGTTOU when a background process writes | ❌ (default) | — |
TOSTOP off by default is why background jobs can scribble over your prompt. stty tostop changes it.
c_iflag — Input Modes
| Flag | Effect |
|---|---|
ICRNL | Translate incoming CR → NL. Why pressing Enter (CR) gives a program \n. |
INLCR | Translate incoming NL → CR |
IGNCR | Discard incoming CR |
ISTRIP | Strip the 8th bit (7-bit legacy) |
IXON | ^S pauses output, ^Q resumes. Why ^S seems to freeze a terminal. |
IXOFF | Send ^S/^Q to the sender for input flow control |
IUTF8 | Input is UTF-8, so VERASE erases a whole multi-byte character (Linux) |
BRKINT IGNBRK PARMRK INPCK | Break and parity handling — serial-line legacy |
c_oflag — Output Modes
| Flag | Effect |
|---|---|
OPOST | Enable output processing at all. Clearing it disables everything below. |
ONLCR | Translate outgoing NL → CR NL. Why programs can print "\n" and get a newline. |
OCRNL | Translate outgoing CR → NL |
ONLRET | NL also performs the carriage return |
TAB3 / XTABS | Expand tabs to spaces on output |
Clearing OPOST is what makes raw-mode output stair-step. Print \r\n yourself.
c_cc[] — Control Characters
| Index | Default | Role |
|---|---|---|
VINTR | ^C 0x03 | → SIGINT (needs ISIG) |
VQUIT | ^\ 0x1C | → SIGQUIT + core dump |
VSUSP | ^Z 0x1A | → SIGTSTP |
VERASE | ^? 0x7F | Erase the previous character (canonical) |
VKILL | ^U 0x15 | Erase the whole line |
VEOF | ^D 0x04 | read() returns immediately with what is buffered — 0 bytes if empty |
VEOL | — | An additional line terminator |
VSTART / VSTOP | ^Q / ^S | Flow control |
VLNEXT | ^V 0x16 | Take the next character literally (needs IEXTEN) |
VREPRINT | ^R 0x12 | Reprint the line |
VWERASE | ^W 0x17 | Erase the previous word |
VMIN | 1 | Non-canonical only: minimum bytes for read() to return |
VTIME | 0 | Non-canonical only: timeout in deciseconds |
VMIN/VTIMEshare storage withVEOF/VEOLon many systems, which is why they only have meaning whenICANONis off.
The four VMIN/VTIME combinations
VMIN | VTIME | read() behavior | Use for |
|---|---|---|---|
| 0 | 0 | Poll: return immediately, possibly 0 bytes | Non-blocking checks |
| >0 | 0 | Block until at least VMIN bytes | Interactive raw-mode reading |
| 0 | >0 | Block until 1 byte, or VTIME deciseconds | Timed reads |
| >0 | >0 | Inter-byte timer after the first byte | Protocol framing |
VMIN=1, VTIME=0 is what you want. cfmakeraw() does not set them; forgetting causes a
100%-CPU spin returning 0 bytes.
What cfmakeraw() Does
t.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
t.c_oflag &= ~OPOST;
t.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
t.c_cflag &= ~(CSIZE | PARENB);
t.c_cflag |= CS8;
/* It does NOT set VMIN/VTIME. You must: */
t.c_cc[VMIN] = 1;
t.c_cc[VTIME] = 0;
Note: "Raw mode" is a set of changes, not a flag. Many programs want something in between —
bash's readline clearsICANONandECHObut leavesISIGon, so^Cstill interrupts.
Applying Changes
tcgetattr(fd, &t);
tcsetattr(fd, TCSANOW, &t); /* immediately */
tcsetattr(fd, TCSADRAIN, &t); /* after pending output drains */
tcsetattr(fd, TCSAFLUSH, &t); /* drain output, DISCARD pending input, then apply */
Use TCSAFLUSH when entering raw mode, or input typed before the switch is reinterpreted under
the new rules and you get a phantom byte.
tcsetattr returns success if it changed any requested setting, not all of them. To be certain,
tcgetattr afterwards and compare.
The stty Equivalents
| Task | Command |
|---|---|
| Show everything | stty -a |
| Raw mode | stty raw -echo |
| Restore | stty sane |
| Just echo off | stty -echo |
| Non-canonical, byte at a time | stty -icanon min 1 time 0 |
Disable ^S/^Q flow control | stty -ixon |
| Stop background writes | stty tostop |
| Window size | stty size |
| Another tty (Linux) | stty -a -F /dev/pts/5 |
| Another tty (macOS) | stty -a -f /dev/ttys005 |
| Change a control character | stty intr ^X |
When a raw-mode program dies without restoring: type stty sane blind and press Enter — or
Ctrl+J if Enter does not work, because ICRNL is off. You will need this.
Canonical vs. Raw at a Glance
| Behavior | Canonical | Raw |
|---|---|---|
read() returns | On a line terminator | As soon as VMIN bytes are available |
| Backspace | Kernel edits the buffer | Your program's problem |
| Echo | Kernel does it | Your program's problem |
^C | SIGINT to the foreground group | Byte 0x03 as data |
^Z | SIGTSTP | Byte 0x1A as data |
^D | Ends the line / EOF | Byte 0x04 as data |
\n on output | ONLCR makes it \r\n | You emit \r\n |
| Used by | read, cat, simple filters | vim, less, top, shells with line editors, terminal emulators |
The Rust Guard
#![allow(unused)] fn main() { /// Restores on Drop — including during a panic unwind. pub struct RawMode { fd: i32, original: libc::termios } impl RawMode { pub fn enable(fd: i32) -> io::Result<Self> { // MaybeUninit, not mem::zeroed(): a zeroed termios is not a valid one, // and constructing an invalid value is UB even if you overwrite it. let mut original = MaybeUninit::<libc::termios>::uninit(); if unsafe { libc::tcgetattr(fd, original.as_mut_ptr()) } != 0 { return Err(io::Error::last_os_error()); } let original = unsafe { original.assume_init() }; let mut raw = original; unsafe { libc::cfmakeraw(&mut raw) }; raw.c_cc[libc::VMIN] = 1; // cfmakeraw does NOT set these raw.c_cc[libc::VTIME] = 0; if unsafe { libc::tcsetattr(fd, libc::TCSAFLUSH, &raw) } != 0 { return Err(io::Error::last_os_error()); } Ok(RawMode { fd, original }) } } impl Drop for RawMode { fn drop(&mut self) { // Drop must not panic; there is nothing useful to do on failure. unsafe { libc::tcsetattr(self.fd, libc::TCSAFLUSH, &self.original) }; } } }
Every exit path
| Path | Restored by |
|---|---|
| Normal return | Drop |
| Panic (unwind) | Drop |
panic = "abort" | A panic::set_hook that restores before aborting |
std::process::exit() | Restore explicitly first; Drop does not run |
SIGTERM / SIGHUP | A handler that restores and re-raises with the default disposition |
SIGKILL | Nothing. This is why stty sane exists. |
Common Mistakes
| Mistake | Symptom |
|---|---|
cfmakeraw without VMIN/VTIME | 100% CPU, read() returning 0 in a loop |
TCSANOW instead of TCSAFLUSH on entry | A stray byte from before the switch |
| No restore on panic | Unusable shell after any crash |
Printing \n in raw mode | Staircase output — OPOST is cleared |
Setting termios on the PTY master | You changed the child's terminal |
Assuming Backspace is 0x08 | It is 0x7F on most terminals; VERASE defaults to it |
Not handling EINTR from read | Spurious failures whenever a signal arrives |
mem::zeroed() for termios | UB; occasionally wrong flags |
See also: termios & the Line Discipline · Glossary