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

FlagEffect when setCanonicalRaw
ICANONLine buffering; read() returns a line at a time; VERASE/VKILL editing✅❌
ECHOThe kernel echoes input to the output side✅❌
ECHOEEcho VERASE as BS SP BS (visual erase)✅❌
ECHOKEcho VKILL by killing the line✅❌
ECHONLEcho NL even with ECHO off—❌
ISIGGenerate SIGINT/SIGQUIT/SIGTSTP from VINTR/VQUIT/VSUSP✅❌
IEXTENImplementation-defined processing (VLNEXT ^V, VDISCARD ^O)✅❌
NOFLSHDo not flush queues on SIGINT/SIGQUIT/SIGTSTP❌—
TOSTOPSIGTTOU 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

FlagEffect
ICRNLTranslate incoming CR → NL. Why pressing Enter (CR) gives a program \n.
INLCRTranslate incoming NL → CR
IGNCRDiscard incoming CR
ISTRIPStrip the 8th bit (7-bit legacy)
IXON^S pauses output, ^Q resumes. Why ^S seems to freeze a terminal.
IXOFFSend ^S/^Q to the sender for input flow control
IUTF8Input is UTF-8, so VERASE erases a whole multi-byte character (Linux)
BRKINT IGNBRK PARMRK INPCKBreak and parity handling — serial-line legacy

c_oflag — Output Modes

FlagEffect
OPOSTEnable output processing at all. Clearing it disables everything below.
ONLCRTranslate outgoing NL → CR NL. Why programs can print "\n" and get a newline.
OCRNLTranslate outgoing CR → NL
ONLRETNL also performs the carriage return
TAB3 / XTABSExpand tabs to spaces on output

Clearing OPOST is what makes raw-mode output stair-step. Print \r\n yourself.


c_cc[] — Control Characters

IndexDefaultRole
VINTR^C 0x03→ SIGINT (needs ISIG)
VQUIT^\ 0x1C→ SIGQUIT + core dump
VSUSP^Z 0x1A→ SIGTSTP
VERASE^? 0x7FErase the previous character (canonical)
VKILL^U 0x15Erase the whole line
VEOF^D 0x04read() returns immediately with what is buffered — 0 bytes if empty
VEOL—An additional line terminator
VSTART / VSTOP^Q / ^SFlow control
VLNEXT^V 0x16Take the next character literally (needs IEXTEN)
VREPRINT^R 0x12Reprint the line
VWERASE^W 0x17Erase the previous word
VMIN1Non-canonical only: minimum bytes for read() to return
VTIME0Non-canonical only: timeout in deciseconds

VMIN/VTIME share storage with VEOF/VEOL on many systems, which is why they only have meaning when ICANON is off.

The four VMIN/VTIME combinations

VMINVTIMEread() behaviorUse for
00Poll: return immediately, possibly 0 bytesNon-blocking checks
>00Block until at least VMIN bytesInteractive raw-mode reading
0>0Block until 1 byte, or VTIME decisecondsTimed reads
>0>0Inter-byte timer after the first byteProtocol 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 clears ICANON and ECHO but leaves ISIG on, so ^C still 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

TaskCommand
Show everythingstty -a
Raw modestty raw -echo
Restorestty sane
Just echo offstty -echo
Non-canonical, byte at a timestty -icanon min 1 time 0
Disable ^S/^Q flow controlstty -ixon
Stop background writesstty tostop
Window sizestty size
Another tty (Linux)stty -a -F /dev/pts/5
Another tty (macOS)stty -a -f /dev/ttys005
Change a control characterstty 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

BehaviorCanonicalRaw
read() returnsOn a line terminatorAs soon as VMIN bytes are available
BackspaceKernel edits the bufferYour program's problem
EchoKernel does itYour program's problem
^CSIGINT to the foreground groupByte 0x03 as data
^ZSIGTSTPByte 0x1A as data
^DEnds the line / EOFByte 0x04 as data
\n on outputONLCR makes it \r\nYou emit \r\n
Used byread, cat, simple filtersvim, 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

PathRestored by
Normal returnDrop
Panic (unwind)Drop
panic = "abort"A panic::set_hook that restores before aborting
std::process::exit()Restore explicitly first; Drop does not run
SIGTERM / SIGHUPA handler that restores and re-raises with the default disposition
SIGKILLNothing. This is why stty sane exists.

Common Mistakes

MistakeSymptom
cfmakeraw without VMIN/VTIME100% CPU, read() returning 0 in a loop
TCSANOW instead of TCSAFLUSH on entryA stray byte from before the switch
No restore on panicUnusable shell after any crash
Printing \n in raw modeStaircase output — OPOST is cleared
Setting termios on the PTY masterYou changed the child's terminal
Assuming Backspace is 0x08It is 0x7F on most terminals; VERASE defaults to it
Not handling EINTR from readSpurious failures whenever a signal arrives
mem::zeroed() for termiosUB; occasionally wrong flags

See also: termios & the Line Discipline · Glossary