How terminal works. Part 2: Xterm, CLI tools output
- Bash output
- History
- Practice 1: ask xterm to execute commands
- Practice 2: Explore vim output
- Conclusion
Bash output
In this blog post, we will discuss what terminal emulators emulate.
Terminals (either hardware devices or software programs) perform two primary tasks:
- visualize the output of tools like bash, cat, top, vi, etc. to the user;
- pass user input to command-line tools.
We’ve already discussed how the terminal handles user input. Now let’s discuss how it visualizes bash output. xterm maintains the state of a “scene” (a buffer) and exposes API to modify that state. API has a concept of a cursor and provides instructions to move cursor and change the buffer. For example:
- insert a character in the current position and move the cursor to the right (I am sorry for RTL scripts);
- move the cursor in different directions;
- change color and other properties of newly rendered characters;
- erase characters, clear a line or clear the whole buffer;
- etc. …
I guess the phrase “xterm exposes API” sounds too modern for how API is organized. In reality, xterm just parses text which comes from bash and separates printable characters from commands. Commands are either
- single-byte ASCII control characters;
- escape sequences: a sequence of several bytes starting with an ESC character and several printable ASCII characters.
In a nutshell, the behavior of xterm is quite simple. It reads bash (well,
tty) output character by character and decides:
- this is printable, let’s print it in the current cursor position;
- this is a command, let’s execute the command.
Additional difficulties which make this scheme a little more complicated are:
- errors handling: both for ill-formed Unicode characters and ill-formed escape sequences;
- emulation: terminal emulators emulate the behavior of previously existing hardware terminals; for that reason, they provide instructions (escape sequences) to choose which terminal model to emulate; this alters the emulator’s behavior;
- there is no single standard that covers the behavior of all terminals and terminal emulators.
The last point is probably the biggest difficulty associated with terminals. There is a variety of different hardware terminals and terminal emulators which support distinct features and behave slightly differently. Digging through standards is hard and doesn’t pay well: terminals rarely follow standards by implementing only a subset of features or they implement non-specified behavior differently or they introduce their own new features.
History
My current understanding is that longlong
ago terminal vendors were
implementing terminals with similar features, but they implemented different
APIs. This led to the creation of terminfo - a database of terminal features
which led to the creation of software terminal compatibility libraries such as
ncurses. At some point, industry matured and produced an ANSI standard that
standardized terminal API and gave us the words ANSI-compatible terminal and
ANSI-escape sequences. The first popular terminal that supported the ANSI
standard was VT100. VT100 was so successful that it made ANSI not only a
standard on paper but also a de facto standard. That’s why nowadays by saying
“VT100-compatible terminal” and “ANSI-compatible terminal” some people mean the
same thing. Modern command-line tools sometimes don’t care about compatibility
with ancient terminals and they might not use terminfo. Instead, they expect an
ANSI-compatible terminal emulator and assume support for certain features.
The evolution of terminals didn’t stop after the release of the ANSI standard. Modern terminal emulators continue to invent new features which have a different level of adoption. Here are examples of features with different levels of support:
- a widely supported non-standard feature: enable/disable alternative screen
buffer
\ESC[?1049h
and\ESC[?1049l
; tui programs use it to restore terminal content after they finish; - a rare feature: urxvt’s “set background image”
\ESC]20
doesn’t seem to have support in other terminals; - somewhere in between: truecolor escape codes are supported by many popular terminals but not by all.
The moral here is that:
-
terminals are difficult to understand because of an uncountable amount of details;
history led us to a situation when there are many terminals (both hardware and software emulators); they have different behavior and there is no single document describing a “reference terminal”;
-
focusing on modern software terminal emulators makes life easier;
modern major terminal emulators are ANSI compatible and hence:
- they support similar syntax for ANSI escape sequences (with differences in error handling and parsing of arguments for non-standard commands, a good example is truecolor escape codes);
- they support many similar features, so by using widely supported features and writing enough “if” conditions, one can achieve terminal-independent code for modern terminals.
Practice 1: ask xterm to execute commands
Let’s ask xterm to execute something interesting for us. First, let’s consult with the Internet wisdom to find commonly used escape sequences:
Then open two terminals. Get tty
from one terminal and try to write something
into this tty using the other terminal window:
xterm1 | xterm2
---------------------|-------------------------------
~$ tty |
/dev/pts/1 |
| ~$ echo "\n123\n" > /dev/pts1
123 |
|
It seems to work. Let’s try to send an escape sequence to clear the screen:
printf "\x1b[2J" > /dev/pts/1
That also works. Let’s extend this idea to implement a simple interactive drawing program which handles the following keybindings:
wasd
orhjkl
- move cursor;space
- insertx
;.
- erase current character by inserting a space;ESC
- exit.
The main idea of the script is to send ANSI escape sequences into a given tty, so that xterm reads the sequences and executes requested commands:
case $char in
[wk] ) cmd="A" ;; # move cursor up
[sj] ) cmd="B" ;; # move cursor down
[ah] ) cmd="D" ;; # move cursor left
[dl] ) cmd="C" ;; # move cursor right
...
esac
printf "\x1b[$cmd" > $pts
Here is my self-portrait which I’ve created using our new tool:
$ /tmp xx xxxxxxx xxxx x
$ /tmp xx x x x
$ /tmp x x x x
$ /tmp x x xx x
$ /tmp xx x x
$ /tmp x xxxxxxxx xxxxxxx x x
$ /tmp x xxxx x xxx x x
$ /tmp x x xx x x x x x x x x x
$ /tmp x xxxx x xxx x x x x
$ /tmp x x x x x x
$ /tmp x x x x x x
$ /tmp x x x x x x
$ /tmp x x x x x x x x x
$ /tmp x x xxxxx xxx x x x x
$ /tmp x xx xxxxxxxxx xxx x x x x
$ /tmp xxxx x x xxx x x x x x
$ /tmp xxx xxxxx xx x x x x x
$ /tmp xx xxx x x x x
$ /tmp xxxxxxxx xx x x x x
$ /tmp x x x x x x x
$ /tmp xxxxxxxxx x x x
$ /tmp x x x x x x x
$ /tmp x x x x x x x
$ /tmp x x x x x x
$ /tmp x x x x x x
$ /tmp x x x x x
x x x
x x x
x
Practice 2: Explore vim output
In the last experiment, we were sending ANSI escape sequences to xterm. Now
let’s go in the opposite direction. Let’s see what existing TUI programs send to
xterm. One way to capture the output of command-line tools is to use utilities
like script,
autoexpect or
aciinema (we will discuss how these tools work in part
4). They all capture the output of command-line programs in different formats.
script
captures only the output and stores it as is. autoexpect
captures
both inputs and outputs, but it uses a backslash to quote certain characters.
autoexpect -f vi.exp vi
Opening vi.exp
in emacs shows many expect
and send
commands with very long
lines full of ANSI escape sequences. expect
statement correspond to vi
output. send
statements correspond to data that comes from tty (which is
either user input or xterm responses to commands which came from vi).
expect -exact "^[\[?2004h^[\[?1049h^[\[?1h ...
send -- "^[\[2;2R^[\[>85;95;0c^[\]10;rgb:5 ...
expect -exact "^[\[?25l^[\[2;1H^[\[38;5;12m~ ...
^[\[3;1H~ ...
^[\[4;1H~ ...
My first guess is that the first line is a terminal-independent part of vim’s
initialization. Then there should be an ANSI escape sequence that asks xterm to
give its current configuration or its capabilities. Then xterm responds with
^[\[2;2R^[\[>85;95;0c^[\]10;rgb:5 ...
. Initialization continues, and then vi
renders its interface using commands which jump to the beginning of each line
and then output its famous ~
symbol visualizing a non-existing line.
^[\[3;1H~ ...
^[\[4;1H~ ...
Let’s consult xterm’s user manual to find out the meaning of ANSI escape sequences emitted by vim:
Sequence | Meaning | Source of info |
---|---|---|
^[[?2004h |
Set bracketed paste mode | |
^[[?1049h |
Save cursor as in DECSC and use Alternate Screen Buffer, clearing it first … | |
^[[?1h |
Application Cursor Keys (DECCKM) | |
^[= |
Application Keypad (DECPAM) | |
^[[?2004h |
Set bracketed paste mode | |
^[[1;52r |
Set Scrolling Region [top;bottom] (default = full size of window) (DECSTBM) | |
^[[?12h |
Start Blinking Cursor (att610) | |
^[[?12l |
Stop Blinking Cursor (att610) | |
^[[27m |
Character Attributes (SGR) Positive (not inverse) | |
^[[23m |
set italic mode | the nice gist |
^[[29m |
set strikethrough mode | the nice gist |
^[[m |
Character Attributes (SGR) P s = 0 → Normal (default) | |
^[[H |
Cursor Position [row;column] (default = [1,1]) (CUP) | |
^[[2J |
Erase in Display (ED) P s = 2 → Erase All | |
^[[2;1H |
Cursor Position [row;column] (default = [1,1]) (CUP) | |
▽ |
plain text | |
^[[6n |
Device Status Report (DSR) Report Cursor Position (CPR) [row;column] as | |
^[[2;1H |
Cursor Position [row;column] (default = [1,1]) (CUP) | |
` ` | plain text | |
^[[1;1H |
Cursor Position [row;column] (default = [1,1]) (CUP) | |
^[[>c |
request the terminal’s identification code | |
^[]10;?^G |
ask text foreground color | |
^[]11;?^G |
ask text background color | |
^[[?25l |
Hide Cursor (DECTCEM) | |
^[[2;1H |
Cursor Position [row;column] (default = [1,1]) (CUP) | |
^[[38;5;12m |
Character Attributes (SGR) Set foreground color | |
~ |
plain text |
I wish there would be a tool that would create a table above automatically ¯_(ツ)_/¯.
Some comments:
- “Application Cursor Keys” and “Application Keypad” configure what xterm sends when the user presses arrow keys or keypad keys;
- changing cursor position and character attributes should be self-explanatory;
- bracketed paste causes xterm to wrap data inserted from an X clipboard into
\ESC[200~
\ESC[201~
so that the application can distinguish between keyboard input and copy-paste from the clipboard; - “Alternate Screen Buffer” is a feature that helps vim to restore the screen after it exists; it’s common for tui apps to enable alternative screen buffer and then disable it when they finish.
Existing apps are a useful source of information. Sometimes, the easiest way to answer your questions is to investigate the behavior or source code of an existing tool. Digging through ANSI escape codes might seem complicated, but with practice, it quickly becomes easier. One also can consult the source code of existing terminal emulators. One good example is recent Alacritty.
Conclusion
To explore how xterm visualizes bash output, we’ve created a small drawing
program. Then to dig deeper and to understand non-trivial output with ANSI
escape sequences, we’ve manually explored the output of the vi
command. This
practice should give a basic understanding of how xterm visualizes bash output.
In part 3, we will explore tty behavior. Stay tuned :)