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.
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:
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:
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.