Disabling Terminal Transmission Codes

UNIX was not designed to stop its users from doing stupid things, as that would also stop them from doing clever things.

Doug Gwyn

Introduction

As part of my university studies which I have recently restarted1, I had to implement the popular dice game Yatzy2. We were only expected to implement a basic print()/readline() interface since it’s an intro class and everyone is just learning to code for the first time.

Being the already well-experienced programmer I am, I decided to take this opportunity to learn something new and decided to implement a more fully-featured game with a curses UI and various keyboard shortcuts for more efficient navigation of the various game screens.

This post is going to cover how I handled the keyboard shortcuts Ctrl+Q and Ctrl+S, and how it isn’t as trivial as you might first expect. For the sake of brevity I will be refering to keychords of the form Ctrl+K as ^K for the remainder of this article.


A Simple Test

In order to better describe the issue at hand, allow me to provide you with a basic test program in C using the aforementioned curses API. The following program3 implements a basic loop where you can enter a key on your keyboard and the corresponding character is displayed on the terminal. Since we are interested in keyboard shortcuts involving the control key we need a way to actually visualize control characters. Luckily the vis(3)4 family of functions exists to do just that.

#include <curses.h>
#include <stdio.h>
#include <vis.h>

int
main(void)
{
	initscr();                  /* Init screen */
	noecho();                   /* Don’t echo keypresses */

	for (;;) {
		/* Read char and encode it as a string  */
		char buf[5];
		vis(buf, getch(), VIS_WHITE, 0);

		clear();                /* Clear screen */
		printw("%s\n", buf);    /* Write char to screen */
		refresh();              /* Refresh display */
	}
	/* unreachable */
}

If you were to run the above example program you would get for the most part, expected results. Pressing a renders ‘a’, and pressing ^X renders ‘\^X’. There’s an issue though: try to enter ^S and notice that what happens is …nothing! Not only does nothing happen but the entire terminal freezes up. The only way to unfreeze the terminal is to hit ^Q.

This is really unfortunate because ^Q and ^S are really useful mnemonic shortcuts: just think of ‘quit’, ‘save’, and other common actions an application might want shortcuts for.


What’s Going On?

So what’s going on here is in simple terms, a historical relic of times gone by. It just so happens that the ^S and ^Q keys correspond to the XOFF and XON terminal transmission codes, which stand for ‘transmit off’ and ‘transmit on’. There may still be a modern usecase for these two transmission codes, but from my surface-level understanding they were mostly useful in the earlier years of computing where devices were slower and had to tell input devices to stop sending data to avoid getting overwhelmed. As a user of a terminal emulator on a modern personal computer, this is totally useless functionality.

So now that we know what the problem is, we need to figure out how to solve it. Luckily for us this is actually a lot easier than you might expect thanks to two simple functions: tcgetattr(3) and tcsetattr(3).

Here’s how we can disable the XOFF/XON codes completely, and allow the user to input ^S and ^Q:

 #include <curses.h>
 #include <stdio.h>
+#include <termios.h>
+#include <unistd.h>
 #include <vis.h>

 int
 main(void)
 {
+	struct termios tp;
+	tcgetattr(STDIN_FILENO, &tp);
+	tp.c_iflag &= ~(IXANY | IXON | IXOFF);
+	tcsetattr(STDIN_FILENO, TCSANOW, &tp);
+
 	initscr();                  /* Init screen */
 	noecho();                   /* Don’t echo keypresses */

What’s going on here is actually really simple. We first get the current terminal attributes for the standard input using tcgetattr and store them in a struct termios. This structure (amongst other things) has various bitmasks describing the terminal modes including input-, output-, control-, and local-modes. What most of those are is out of the scope of this article but what we are interested in are the input modes.

The input modes are stored in the c_iflag member of the struct termios structure and there are various flags here that we can toggle which are documented in the tcgetattr(3) manual. The three we’re interested in are IXANY, IXON, and IXOFF. These flags are described as such by the aforementioned manual:

IXANY
Typing any character will restart stopped output.
IXON
Enable XON/XOFF flow control on output.
IXOFF
Enable XON/XOFF flow control on input.

In other words… this is all stuff we want to turn off. Since we are working with bitmasks we can unset the relevant bits by ORing all the bits together, negating the mask, and doing a bitwise AND; it’s all standard bitwise operations.

The last thing we need to do is to take our modified attributes and tell the terminal to use them. This is where tcsetattr() comes into play; we just give it our attributes and file descriptor and we can now use ^S without freezing our terminal! The only other thing to note is the TCSANOW flag — it just means that we want our changes to take effect immediately.

Finishing Touches

What we’ve got here is almost all that we need, but we’re missing one crucial part: when the user exits our program these settings will still persist! Chances are that most users will never realize this, but that doesn’t mean that we shouldn’t be good engineers and clean up after ourselves. Luckily to do this we don’t actually need to do anything new, we just need to keep track of the original attributes when we first queried them:

 #include <curses.h>
 #include <stdio.h>
+#include <stdlib.h>
 #include <termios.h>
 #include <unistd.h>
 #include <vis.h>

+static void restore_tcattrs(void);
+
+struct termios old_attrs;
+
 int
 main(void)
 {
 	struct termios tp;
 	tcgetattr(STDIN_FILENO, &tp);
+	old_attrs = tp;
 	tp.c_iflag &= ~(IXANY | IXON | IXOFF);
 	tcsetattr(STDIN_FILENO, TCSANOW, &tp);
+	atexit(restore_tcattrs);

 	initscr();                  /* Init screen */
 	noecho();                   /* Don’t echo keypresses */
@@ -25,19 +18,10 @@
 	for (;;) {
 		/* Read char and encode it as a string  */
 		char buf[5];
+		int ch = getch();
+		if (ch == '\x11')       /* ^Q */
+			break;
+		vis(buf, ch, VIS_WHITE, 0);
-		vis(buf, getch(), VIS_WHITE, 0);

 		clear();                /* Clear screen */
 		printw("%s\n", buf);    /* Write char to screen */
 		refresh();              /* Refresh display */
 	}
-	/* unreachable */
+	endwin();                   /* Destroy screen */
+	return EXIT_SUCCESS;
 }
+
+void
+restore_tcattrs(void)
+{
+	tcsetattr(STDIN_FILENO, TCSANOW, &old_attrs);
+}

With these simple changes, we can now use ^S and ^Q as expected in our application but once we exit the application with ^Q the XON/XOFF functionality of our terminal is restored and behaves as expected.