The ANSI Terminal

By Allison Parrish

In the beginning, there were very few computers, and they were centrally located and often many people needed to use the computer at once. As a consequence, most access to the computer happened through a terminal. Terminals (or TTYs) were not themselves “computers” but only sent characters to the computer and received results (over a wired connection, like POTS), displaying the results to the user. The first terminals (such as Teletype model 33) displayed results in the form of a printout, but in the late 1960s “glass TTYs” that used video displays instead of physical printouts were introduced.

Video terminals had new affordances, like being able to display text at arbitrary positions on the screen or in different colors, that were impossible (or difficult) to achieve on hard copy terminals. Terminal vendors introduced command sequences that the computer could send to the terminal to achieve these kinds of effects. To make it easier to write programs that could interoperate with terminals from many different vendors, the American National Standards Institute developed a standard set of command sequences, and this standard was widely adopted by terminal vendors. The standard (ANSI X3.64) is still supported to this day in terminal emulator programs (like macOS Terminal) that run on contemporary computers.

In this tutorial, I’m using a terminal emulator written entirely in JavaScript that runs in the browser: xterm.js. You can write text and ANSI escape sequences to this terminal emulator with a JavaScript program. I created a JavaScript bundle for in-browser use that also includes a JavaScript library called ansi-escape-sequences that provides easy-to-use functions that print ANSI escape sequences by name, so you don’t have to remember the codes (very handy).

You can get a quick start by opening this example in the p5.js web editor and making your own version (File > Duplicate and making your own version (File > Duplicate). This sketch already has the necessary JavaScript bundle included. It’s also using the p5.js framework, which gives us a number of handy behaviors and functions right out of the box; mainly, we’ll be taking advantage of the draw() function, which is called automatically every n frames (you can set n with a call to frameCount()). See the p5js reference for other functions that might pop up in the examples.

In the following code snippets, I’ll assume you’re using the p5js editor version.

Terminal and cursor

By default, the framework creates an xterm.js terminal that takes up the entire screen, and sets the number of rows and columns in the terminal to fill up the available space. (You can control the font and font size by changing parameters in xterm-ansi-setup.js.) There’s a global variable term, which has a method .write() that takes the string you pass to it and prints it to the screen:

term.write("Hello, world");

… writes Hello, world to the terminal.

Every terminal has a “cursor,” which is the position on the screen where text will appear when you call .write(). Each (non-control) character in the string passed to write advances the character one column to the right; if it advances past the last column, it will relocate to the first column in the subsequent row. For this reason, although the first call to write starts in the upper left-hand corner, subsequent calls to term.write() will write text starting at the row and column where the last write left off.

The control characters you might already know

There are two control characters that you can use in a terminal that you probably already know about: \n (Newline) moves the cursor one row down, and \r (Carriage return) moves the cursor back to the beginning of the line. When writing to a terminal, you need to use both to get the usual “new line” behavior:

term.write("Hello, world\r\n");

See “Hello, world” with newline and carriage return in action

The character \t (Tab) advances the cursor by a set amount, and the character \b (Backspace) moves the cursor back one column.

Here’s an example of the backspace character in action.

ANSI escape sequences

The control codes introduced in the ANSI standards allow you to do a number of useful things, like setting the position of the cursor to an arbitrary position and changing the color of the text. To use a control code (often called an “escape sequence”), you need to send a sequence of characters to the terminal. Every ANSI control code begins with the ASCII “Escape” character (written as \x1b in JavaScript) followed by [. For example, the code to advance the cursor by one column is “Escape” followed by [<n>C, where <n> is the number of columns to advance. Writing this to our xterm.js terminal would look like:

term.write("\x1b[4C"); // advances 4 columns

Or, if you wanted to advance a number of columns from a variable:

let colCount = 9;
term.write("\x1b["+colCount+"C");

Another example: The command “Escape” followed by [<n>;<m>H moves the cursor position to row <n>, column <m>. For example, the following code prints this is fun! at row 5, column 13:

term.write("\x1b[5;13Hthis is fun!");

And the command “Escape” followed by [<nn>m sets the color of the text. If you write a number between 30 and 37, it sets the foreground color; if a number between 40 and 47, the background color. See the full list of colors on Wikipedia. For example, the following code prints this is fun! in red:

term.write("\x1b[31mthis is fun!");

And this code writes the same text on a blue background:

term.write("\x1b[44mthis is fun!");

You can set both the foreground and background colors using the syntax \x1b[<nn>;<mm>m where <nn> is the foreground and <mm> is the background:

term.write("\x1b[31;44mthis is lots of fun!");

This prints this is lots of fun! in red on a blue background.

Using a library

The control codes themselves are strange and difficult to remember, so a number of people have made libraries that “wrap” the control codes in functions that you can call with easier-to-remember names. The bundled code includes one such library: ansi-escape-sequences. The library is available as the global variable ansi.

For example, the following function call returns a string with the control code necessary to set the cursor position to row 4, column 10:

ansi.cursor.position(4, 10)

This evaluates to \x1b[4;10H. (More verbose, but easier to remember!) To actually set the cursor to this position, you have to send the command sequence to the terminal:

term.write(ansi.cursor.position(4, 10));

There are a handful of ways to set the color of the text with ansi-escape-sequences. One is to use the ansi.styles() function, which returns a string containing command codes to set the output to display with the ANSI styles you specify in a list. For example, the following code:

ansi.styles(['red', 'bold'])

… evaluates to \x1b[31;1m. Again, to actually instruct the terminal to change colors, you need to write the command sequence:

term.write(ansi.styles(['red', 'bold']) + "this is bold red text!");

The ansi.format() function is a convenience function to set the styles you specify to a given string, and then reset back to the default. For example, this call:

ansi.format("this is fun!", ["green", "bold", "bg-magenta"])

… evaluates to the string \x1b[32;1;45mthis is fun!\x1b[0m. (The \x1b[0m at the end of the string resets the terminal to default styles.) Sending this to the terminal would display this is fun! in bold green text on a magenta background:

term.write(ansi.format("this is fun!", ["green", "bold", "bg-magenta"]))

Examples

References

Further reading