Skip to content

6.1 Terminal IO

Terminal I/O and Configuration refers to how programs interact with the terminal (your command-line interface) to:

  1. Read input from the user (keyboard).
  2. Write output back to the screen.
  3. Customize behavior of this interaction—like how and when input is processed, whether it’s echoed back, and how special keys like Ctrl+C or Ctrl+D behave.

1. 🔧 Breaking it Down:

1.1 Terminal I/O (Input/Output)

  • Input (stdin): User types something.
  • Output (stdout/stderr): Program prints results.

1.2 Canonical vs Non-Canonical Mode

  • Canonical Mode: Input is line-buffered—you type a full line and press Enter before the program processes it.
  • Non-Canonical (Raw) Mode: Each keystroke is sent directly to the program without waiting for Enter.

1.3 Echo

  • By default, typed characters appear on screen.
  • Echo can be turned off (e.g. for password input).

1.4 Signals

  • Ctrl+C sends a SIGINT to interrupt the program.
  • Ctrl+D signals EOF (End of File/Input).

1.5 Configuration Tools

  • stty: Command-line tool to change terminal behavior.
  • termios: C library/API for programmatically configuring terminal settings.
  • (n)curses: Library for building full-screen terminal apps (e.g. menus, games, text editors).

1.6 🛠️ Why It Matters

Terminal I/O configuration is essential for building:

  • Shells (like Bash or Zsh)
  • Text editors (like Vim)
  • Command-line utilities with custom input behavior (like password prompts or games)

we explore how terminal behavior can be customized, particularly regarding input and output. These functionalities are essential when building command-line programs that require fine-grained control of how characters are processed. The exercises focus on stty, the termios API, and an introduction to (n)curses.

2. stty – Terminal Settings via Command Line

The stty command is used to view and modify terminal line settings. A key command to remember is:

  • stty sane – Resets the terminal to its default, “sane” state. This is crucial if you’ve misconfigured your terminal (e.g., disabled echo and can no longer see what you’re typing).

Avoid placing stty commands in your shell startup files like .bash_profile, since they may lock you out of a usable terminal session if misconfigured.

Useful stty Commands

  • stty --help: Displays help and available options.
  • stty sane: Resets terminal to default settings.
  • stty -echo: Disables echoing input (useful for passwords).
  • stty echo: Re-enables echo.
  • stty raw: Disables canonical input mode and special character handling (e.g., Ctrl+C), allowing single-character I/O.
  • stty rows <n> cols <m>: Adjusts terminal size (virtual dimensions). May not be fully supported in WSL.

3. termios – Terminal Settings via C Code

The termios API provides programmatic control over terminal I/O behavior. It’s more powerful and flexible than using stty.

Typical Workflow Using termios

  1. Open the Terminal Device
    Use open() or standard file descriptors (e.g., stdin/fd=0).

  2. Retrieve Current Attributes
    Use tcgetattr(fd, &term) to read current terminal settings into a termios struct.

  3. Modify Attributes
    Change flags or control characters as needed in the struct.

  4. Apply Changes
    Use tcsetattr(fd, TCSANOW, &term) to apply changes immediately.

  5. Restore Original Settings
    Save a copy of the original termios struct and restore it at the end of your program to prevent lingering effects on the terminal.

3.1 (a) – Basic Rotate Program

int c;
while ((c = getchar()) != EOF) {
if (c == 'z') c = 'a';
else if (islower(c)) c++;
putchar(c);
}

This simple program increments each lowercase character by one (ab, za). EOF (Ctrl+D) ends the loop.

Terminal Behavior (Defaults)

  • Echoed Input: Yes.
  • Buffered Input: Yes – Input is line-buffered; data is processed only after pressing Enter.
  • Ctrl+C: Sends SIGINT and terminates the program.

3.2 (b) – Modifying Terminal with stty

Experiment with these:

  • stty raw: Disables canonical mode and input buffering; each character is processed immediately.
  • stty -echo: Prevents characters from being displayed as typed.
  • stty sane: Resets terminal back to normal behavior.

Warning: In raw mode, even Ctrl+C and Ctrl+D won’t work. You may need to forcibly close the terminal window.

3.3 (c) – Disabling Canonical Mode in C

#include <termios.h>
#include <unistd.h>
int main() {
struct termios term, original;
tcgetattr(0, &term); // Get current settings
original = term; // Save original settings
term.c_lflag &= ~ICANON; // Disable canonical mode
term.c_cc[VMIN] = 1; // Require 1 character to read
term.c_cc[VTIME] = 0; // No timeout
tcsetattr(0, TCSANOW, &term); // Apply new settings
...
...
return 0;
}

After modifying and applying terminal settings, restore the original settings using tcsetattr(0, TCSANOW, &original);.

3.4 (d) – Disabling Echo

To disable both canonical mode and echoing:

term.c_lflag &= ~(ICANON | ECHO);

This ensures characters are not echoed to the screen when typed.

3.5 (e) – Adding a Timeout

term.c_lflag &= ~(ICANON | ECHO);
term.c_cc[VMIN] = 2;
term.c_cc[VTIME] = 30; // Timeout in deciseconds (3 seconds)

The timeout begins after the first character is entered. If a second character is not typed within 3 seconds, the read will time out.

3.6 (f) – Handling SIGINT and Restoring Terminal

To ensure terminal settings are always restored—even if the user presses Ctrl+C—add a signal handler for SIGINT.

#include <signal.h>
void handler() {
printf("Received SIGINT, restoring terminal...\n");
tcsetattr(0, TCSANOW, &original);
exit(0);
}
...
signal(SIGINT, handler);

Make original a global variable so it’s accessible within the signal handler.

4. (n)curses – (Optional)

ncurses is a widely-used library that helps build rich, interactive text-based interfaces in the terminal. Students are encouraged to explore the provided code examples for hands-on practice.

5. Sample Code

rotate.c
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
int main ()
{
int c;
while ((c = getchar ()) != EOF) /* use Ctrl+d to provide EOF */
{
if (c == 'z')
{
c = 'a';
}
else if (islower (c))
{
c++;
}
putchar (c);
}
printf("!!\n");
return 0;
}

helloWorld09.c
** Author: Michael Borck
** Username: michael
** Unit: SPD251/551
** Purpose:Demonstrate 2D animation by bouncing a message around
** the screen defined by some parameters
**
** user input:
** s slow down x component, S: slow y component
** f speed up x component, F: speed y component
** q quit
**
** Blocks on read, but timer tick sets SIGALRM which are
** caught by move
**
** This program really should be split into a few files.
** At the very least a header file, perhaps and ADT for
** "the ball"
*/
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <ncurses.h>
#include <signal.h>
#include <sys/time.h>
#include <string.h>
/* some parameters */
#define BLANK " "
#define MESSAGE " Hello World! "
#define DFL_message '*'
#define TOP_ROW 5
#define BOT_ROW 20
#define LEFT_EDGE 5
#define RIGHT_EDGE 65
#define X_INIT 10 /* starting col */
#define Y_INIT 10 /* starting row */
#define TICKS_PER_SEC 90 /* affects speed */
#define XTICK 5
#define YTICK 8
#define STOP 0
struct ball
{
int y, yTick, yTime, yDirection,
x, xTick, xTime, xDirection;
char * message;
};
struct ball theBall;
void doMove ();
void setTick (long msec);
void edgeCollision (struct ball *bp);
int main (void)
{
int delay = 1000; /* 1000 ms = 1 second */
int ndelay = 0;
char cmd;
theBall.y = Y_INIT;
theBall.x = X_INIT;
theBall.yTime = theBall.yTick = YTICK;
theBall.xTime = theBall.xTick = XTICK;
theBall.yDirection = 1;
theBall.xDirection = 1;
theBall.message = MESSAGE;
/* Initialize ncurses */
if (initscr() == NULL)
{
fprintf (stderr, "Error initialising ncurses.\n");
exit (EXIT_FAILURE);
}
crmode ();
noecho ();
refresh ();
signal (SIGINT, SIG_IGN);
signal (SIGALRM, doMove);
setTick (delay / TICKS_PER_SEC); /* send millisecs per tick */
while (cmd != 'q')
{
cmd = getch ();
switch (cmd)
{
case 'f':
theBall.xTick--;
break;
case 'F':
theBall.yTick--;
break;
case 's':
theBall.xTick++;
break;
case 'S':
theBall.yTick++;
break;
default:
break;
}
}
/* Clean up after ourselves */
refresh ();
endwin ();
setTick (STOP);
return EXIT_SUCCESS;
}
/*
* doMove()
*/
void doMove ()
{
int y_cur, x_cur, moved;
signal (SIGALRM, SIG_IGN); /* dont get caught now*/
y_cur = theBall.y;
x_cur = theBall.x;
moved = 0;
if ((theBall.yTick > 0) && (theBall.yTime-- == 1))
{
theBall.y += theBall.yDirection; /* move */
theBall.yTime = theBall.yTick; /* reset */
moved = 1;
}
if ((theBall.xTick > 0) && (theBall.xTime-- == 1))
{
theBall.x += theBall.xDirection; /* move */
theBall.xTime = theBall.xTick; /* reset */
moved = 1;
}
if (moved)
{
move (y_cur, x_cur);
addstr (BLANK);
move (theBall.y, theBall.x);
addstr (theBall.message);
edgeCollision (&theBall);
refresh ();
}
signal (SIGALRM, doMove); /* for unreliable systems */
}
/*
* edgeCollision()
*/
void edgeCollision (struct ball *bp)
{
if (bp->y == TOP_ROW)
{
bp->yDirection = 1;
}
else if (bp->y == BOT_ROW)
{
bp->yDirection = -1;
}
if (bp->x == LEFT_EDGE)
{
bp->xDirection = 1;
}
else if (bp->x == RIGHT_EDGE)
{
bp->xDirection = -1;
}
}
/*
* setTick()
*/
void setTick (long msecs)
{
struct itimerval value, ovalue, pvalue;
int which = ITIMER_REAL;
long n_sec, n_usecs;
/* Calculate interval and ensure integer number of intervals */
n_sec = msecs / 1000;
n_usecs = (msecs % 1000) * 1000L;
/* Set a real time interval timer to repeat every 200 milliseconds */
value.it_interval.tv_sec = n_sec;
value.it_interval.tv_usec = n_usecs;
value.it_value.tv_sec = n_sec;
value.it_value.tv_usec = n_usecs;
setitimer (which, &value, &ovalue);
}