// lesson: termios-intro

Terminal Settings with termios

Everything the line discipline does β€” echo, buffering, signals, translation β€” is configurable, per terminal device, through one struct and two functions declared in termios.h:

#include <termios.h>

struct termios t;
tcgetattr(fd, &t);              /* read the device's current settings  */
t.c_lflag &= ~ECHO;             /* flip some bits                      */
tcsetattr(fd, TCSAFLUSH, &t);   /* write them back                     */

This is the API. stty is a thin wrapper over it; so is everything vim or ssh does to a terminal. Note that both calls take an fd: the settings belong to the device, not to your process. Change them and they stay changed for everyone using that device until something changes them back β€” which is why "restore on exit" is a hard requirement, not a courtesy.

The struct, field by field

struct termios {
    tcflag_t c_iflag;    /* input processing:  what happens to arriving bytes */
    tcflag_t c_oflag;    /* output processing: what happens to departing bytes */
    tcflag_t c_cflag;    /* hardware-ish: baud, character size, parity */
    tcflag_t c_lflag;    /* "local": the line discipline's personality */
    cc_t     c_cc[NCCS]; /* control characters: which byte means what */
};

Each tcflag_t is a bitmask; you flip flags with the usual idioms:

t.c_lflag &= ~(ECHO | ICANON);   /* clear (disable) */
t.c_cflag |= CS8;                /* set (enable)    */

c_lflag β€” the big personality switches

Flag When set (the default) Raw mode
ICANON line buffering + kernel line editing (Backspace, ^U) clear
ECHO kernel echoes input back to the display clear
ISIG ^C→SIGINT, ^Z→SIGTSTP, ^\→SIGQUIT clear
IEXTEN extended input processing (^V literal-next, ^O) clear

ICANON and ECHO are the headliners. Clearing ISIG means Ctrl+C becomes just a byte (0x03) delivered to you β€” your program decides what "interrupt" means. Clearing IEXTEN closes the odd loopholes.

c_iflag β€” input translation

Flag When set Raw mode
IXON ^S/^Q pause and resume output (software flow control) clear
ICRNL translate incoming \r (Enter) into \n clear
BRKINT serial "break" sends SIGINT clear
INPCK parity checking (serial-line era) clear
ISTRIP strip input bytes to 7 bits clear

With ICRNL cleared you'll see Enter as it truly arrives: \r, byte 0x0D. Your key decoder (a few lessons from now) must know that. BRKINT, INPCK, ISTRIP are fossils of real serial hardware β€” clearing them is tradition and costs nothing.

c_oflag β€” output translation

Flag When set Raw mode
OPOST enable all output processing clear
ONLCR (under OPOST) translate outgoing \n to \r\n β€”

Clearing OPOST turns off all output massaging β€” most visibly ONLCR. From then on \n moves the cursor down one row without returning to column 0, staircasing your text like

first line
          second line
                     third line

That's not a bug; it's two motions you're now responsible for distinguishing. Full-screen programs don't mind β€” they position the cursor explicitly anyway.

c_cflag and c_cc

c_cflag matters to us only for CS8: 8-bit characters, please (yet another serial fossil β€” 7-bit terminals were real). In c_cc, the control-character array, two "characters" aren't characters at all but read-timing knobs β€” and they're important enough to get their own section.

VMIN and VTIME: how much, how long

With ICANON cleared, when does read() return? Two bytes in c_cc decide:

  • VMIN β€” the minimum number of bytes before read may return.
  • VTIME β€” a timeout in tenths of a second.

The four quadrants:

VMIN VTIME read() behavior
0 0 never blocks: returns whatever is there, possibly 0
>0 0 blocks until VMIN bytes exist (classic blocking read)
0 >0 returns on first byte OR after VTIME expires with 0
>0 >0 inter-byte timer: first byte starts the clock

VMIN=1, VTIME=0 is the sane default for interactive programs: block until there's something, deliver it immediately. VMIN=0, VTIME=1 (return within 0.1 s no matter what) is the poor-man's event loop β€” kilo (antirez's ~1000-line C text editor, the tutorial in this course's extended reading, and a name that will keep coming up) uses it. We'll use VMIN=1 and get our timeouts a better way, with poll(), in the next lesson.

Applying settings: the third argument

tcsetattr(fd, when, &t) β€” the when matters:

  • TCSANOW β€” apply immediately, even mid-output.
  • TCSADRAIN β€” wait for pending output to finish first. Use when changing output flags so queued text isn't garbled.
  • TCSAFLUSH β€” drain output and discard pending unread input. The right choice when entering/leaving raw mode: keystrokes typed before the switch die instead of leaking into the new regime.

One more trap, straight from the man page: tcsetattr returns success if any requested change succeeded. Paranoid code calls tcgetattr afterwards and compares. (Our tests do exactly that to your code.)

Raw mode, assembled

Putting the whole lesson in one function β€” this is cfmakeraw(3) reimplemented by hand, and it's the next challenge:

raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
raw.c_oflag &= ~OPOST;
raw.c_cflag |= CS8;
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
raw.c_cc[VMIN]  = 1;
raw.c_cc[VTIME] = 0;

And the discipline that goes with it, which every serious tool follows: save the original struct before touching anything, restore it on every exit path β€” normal exit, error, fatal signal. A program that dies in raw mode leaves the shell deaf and mute (reset fixes it, but users shouldn't need to know that incantation).

β€Ί Enable Raw Mode

15 pts

Three functions:

  • termios_make_raw(struct termios *t) β€” mutate a settings struct into the raw configuration above. Pure: no fd, no syscalls, no side effects. (Testable to the last bit, and reusable on any fd.)
  • raw_mode_enable(int fd, struct termios *saved) β€” snapshot the device's current settings into *saved, then apply the raw configuration to it with TCSAFLUSH. Return 0, or -1 on error.
  • raw_mode_restore(int fd, const struct termios *saved) β€” put the snapshot back (again TCSAFLUSH). Return 0 or -1.

The tests run against a real pseudoterminal: they check every flag bit before and after, and then do the behavioral proof β€” in raw mode a single keystroke byte (no newline!) written to the master must be immediately readable from the slave.

Log in to submit a solution and earn points.

β€Ί Read Timeouts with VMIN and VTIME

15 pts

Before we abandon VMIN/VTIME for poll(), prove you can drive them β€” plenty of real code (including kilo) does its event timing this way, and understanding the quadrant table beats memorizing it.

Implement term_set_read_timing(fd, vmin, vtime): fetch the device's current termios, set c_cc[VMIN] and c_cc[VTIME], apply with TCSANOW. The tests put a pty slave into raw mode, then:

  1. VMIN=0, VTIME=0 with no data β†’ read returns 0 immediately (the polling read).
  2. VMIN=0, VTIME=3 with no data β†’ read blocks ~0.3 s, then returns 0. The test asserts the elapsed time is at least 0.15 s (it really waited) and under 3 s (it didn't hang).
  3. VMIN=0, VTIME=5 with data already waiting β†’ read returns it immediately, well before the timeout.

Log in to submit a solution and earn points.