ANSIble
Using ANSI escape codes for more interactive CLI interfaces
Wednesday, August 6, 2014 · 3 min read
Have you ever wondered how to print color to terminals? Or why on some CLI
interfaces, the arrow keys print out mysterious sequences like ^[[A
? Or why
sometimes the keyboard shortcut control-C becomes ^C
? Well, the answers to
these are all based on ANSI escape codes.
In UNIX programs, there’s the concept of ‘raw’ and ‘canonical’ mode for input.
‘Raw’ mode takes input character-by-character, while ‘canonical’ mode lets the
user write a line and hit enter to send the program the written line. Canonical
mode is generally more useful: it lets you go back and delete something if you
make a mistake. But applications that work on a per-keypress basis need to
bypass this. In node, you can enter raw mode with
process.stdin.setRawMode(true);
.
CLI interactions also need the concept of control characters. When you type
control-C, you’re sending the program the byte 0x3
, which is… 3. But that’s
the ASCII control character which means ‘end of text’. The program takes
this, by convention, as a signal to stop executing (KeyboardInterrupt
in
Python, for example). We print control characters with a caret (^
), followed
by the letter we type on the keyboard. There are 32 of them, which Wikipedia
lists. You might
be familiar with using ^D
(‘end of transmission’) to quickly exit Python or
nodejs.
ANSI escape codes are a way to output to a terminal with more capability than just raw text (there was, for comparison, a time when computer output was printed, physically on paper, line by line). You can move the cursor back and overwrite or clear text. You can also color text or make it blink obnoxiously.
ANSI escape codes start with the CSI: the Control Sequence Introducer. The
most common one is \e[
. \e
is the ASCII escape character 0x1b
. You can
type it with the control character ^[
(that is, control-[
).
Next, they have a sequence of numerical arguments, separated by semicolons, and
finally, they have a letter which indicates the command. Once more, Wikipedia
lists these. As an
example, we can move the cursor to the top-left corner with \e[1;1H
(H is the
command to move, and the arguments are 1 and 1).
Colors are just as easy. We use the m
command, with an SGR (‘Set Graphics
Rendition’) parameter. 35 is the SGR parameter to set the text color to
magenta, while 42 makes the background green. So \e[35;42m
would give us a
horrible magenta-on-green color scheme. (\e[m
(no arguments) restores
everything).
This, by the way, explains the ^[[A
curiosity. When you press up-arrow, the
terminal sends the application the ANSI escape code to move the cursor up—the
command for this is A
. So we get \e[A
, and \e
gets rendered as its
control code equivalent of ^[
. (You can, in fact, manually enter
control+[-[-A in Bash, and get the standard up-arrow behavior of pulling up the
last entered command.)
Some nodejs code to get you started—it’s a utility to interactively display the bytes sent from a terminal when you press a key(combination).
process.stdin.resume();
process.stdin.setRawMode(true);
process.stdin.on("data", function(buffer) {
if (buffer.length === 1 && buffer[0] === 3) {// detect ^C
process.stdout.write("\n"); // A trailing \n prevents
// the shell prompt from
// messing up.
process.exit(0); // die
} else {
process.stdout.write("\x1b[1J\x1b[1;1H");
// clear line and go to top
process.stdout.write(
require('util').inspect(buffer)
// Nice output format
);
}
});
This should give you the tools to write shinier, interactive utilities. But keep in mind the UNIX philosophy—keep them simple, and make sure they cooperate as filters (you should be able to pipe stuff in and out of your utility).
P.S. I wrote this post—including the code sample—in vim running in tmux. Please pardon typos.