Most programming tutorials published on the web reproduce source code excerpts inside preformatted text blocks. When they involve terminal user interfaces and colored outputs, they may include a capture from a terminal. The simple approach is to take a screenshot, but it comes with the disadvantages of raster images: it doesn't scale up, the fonts lose legibility, you cannot copy the content or edit it, and it consumes more space. This article explains how to capture the content of a terminal in ANSI text format, how to convert it to HTML, and how to make it responsive like an image.
The first step is to capture the content of the terminal with tmux, including the visible text and the hidden formatting elements composed of ANSI escape sequences.
§ANSI escape sequences
Terminals communicate with an application through a stream of bytes. Some of
them represent visible text, while others are hidden control characters such as
backspace, carriage return, or interrupt, which form an in-band communication
mechanism. Escape sequences start with the control character
ESC, followed by
the Control Sequence Introducer
CSI. They provide functions such as moving
the cursor, changing the style of the text, or clearing the screen. The general
ESC CSI B... F, where:
ESCis the escape control character (byte 0x1b or 0o33 in octal notation, often represented as
CSIis the Control Sequence Introducer (byte 0x9b or
B...corresponds to zero or more bytes. There is a distinction between parameter bytes in the range 0x30-0x3f (including numbers and
;) and intermediate bytes in the range 0x20-0x2f, but this article only deals with the former.
Fis the final byte that terminates the sequence, in the range 0x40-0x7e.
ESC [ mclears the screen (the final byte is
ESC [ 31 msets the foreground color to red (
31is the first parameter byte, coding for the foreground color red).
ESC [ 3 ; 43 Hmoves the cursor to the 3rd row and 43rd column (the arguments are separated by
;and the final byte is
Initially, ANSI terminals only supported 8 colors: Black, Red, Green, Yellow, Blue, Magenta, Cyan, White. Later, their bright variants were standardized, for a total of 16, identified by an index from 0 to 15. These colors, in addition to the default foreground and background, can be configured with a terminal color scheme. Further improvements brought new colors for a total of 256, and modern terminal emulators support 24-bit colors, defined by their RGB components.
For more information, see:
- ANSI escape code on Wikipedia.
urxvt(7), section "Rxvt-unicode Technical Reference" for a concise list of the common control codes.
- ECMA-48 for the technical specification.
The GNOME Terminal provides an option to export its content to HTML. If you want the true representation that you can view inside a terminal, you may prefer saving the content in ANSI text format first. Then, you can convert to HTML or any other format (including taking a screenshot).
Multiplexers such as tmux or screen provide a terminal agnostic solution to make a "hard copy" of their content. This article demonstrates how to do it with tmux.
Use the command
resize-window to resize the window containing the pane with
id 1 to a standard size suitable for publication, for instance, 80 columns by
capture-pane with the options
-e to include the escape sequences,
-p to print the output to stdout,
-t %1 to target the pane with id 1, and
redirect the output to the file
You can run
clear; cat demo.ans in any terminal to display the captured
content and you can also edit it with your preferred text editor.
Note that this capture doesn't have any intrinsic size. If all the lines are
narrower than the terminal window, the trailing whitespaces are not reproduced
in the output (unless you use the option
-N). On the contrary, if you display
it in a smaller terminal, longer lines may get wrapped. The capture doesn't
indicate the position of the cursor either, as it is usually drawn by the
terminal itself, not by the application.
You can add extra escape sequences in the output file to provide additional
information, such as the terminal size and the cursor position. The ANSI
ESC [ r ; c R to return the cursor coordinates, where
c are the 1-based row and column indices.
There are other escape sequences reserved for private use, identified by their
final byte. For instance, Xterm uses
ESC [ 9; h ; w t to return the size of
the terminal, where
w are the numbers of rows and columns.
From a shell script, you can start an escape sequence by calling
\033, the octal representation of the
ESC control character.
With the help of the
list-panes command, retrieve the pane size to complete
the escape sequence and save it to
Similarly, append the 1-based cursor coordinates:
Finally, capture the pane content:
The capture file now contains the information to faithfully reproduce the view of the original terminal.
for the full capture script.
This section explains how to take the raw ANSI text format, convert it to HTML, and make it responsive.
To use the additional information added in the last section, you have to make
your own interpreter. Thanks to the Rust terminal emulator project Alacritty,
vte provides all the logic needed
to parse ANSI control codes. All that is left is to implement the
At the minimum, you should track the basic styles: foreground and background
colors, bold, italic, underline, reverse, and the reset sequences. The text is
rendered inside an HTML preformatted text block
pre with consecutive
csi_dispatch is called for each escape sequence. Note that a single sequence
can set multiple attributes at once. For instance,
ESC [ 1 ; 4 m sets both
the bold and underline properties. Anything that you don't handle will just get
stripped from the output.
execute allows you to handle control characters, especially newlines. If the
cursor is past the end of the line, you need to append blank characters until
you reach the cursor position; otherwise you will never encounter any character
under the cursor. Similarly, styles like the text background color stop at the
end of the line, so you also need to extend them up to the terminal width. This
is why knowing the size of the terminal (or the output) is required.
There are other considerations such as HTML escaping. For the full
The inline elements inside the preformatted text block have short class names matching the text attributes:
f15for the 16 standard foreground colors.
b15for the 16 standard background colors.
sfor bold, italic, underline, reverse, strike.
c6for block and bar cursor shapes.
Most of them map directly to a CSS property:
CSS variables help configure the foreground, the background, and the 16
standard ANSI colors that you can reverse by adding rules for the combined
classes, for instance
The preformatted text block has the width of its longest line. Assuming the text was recorded with an 80-column terminal, you can set the width based on the number of columns:
ch is relative to the width of the character
0 in the target font.
pre elements use a monospace font by default, all the characters have
the same width:
80ch corresponds to 80 columns of text.
For the full stylesheet, see
The issue is that preformatted text blocks don't scale: either you have a scrollbar, or it overflows its parent. To scale a preformatted text block, the only property under your control is the font size.
The width depends on the font size, but also on the width of the characters and the number of columns. The width of a character is specific to each font, and if you don't use web fonts, it depends on the font configured by the user.
For an initial font size of 1rem, let
--parent-width be the width of the
--element-width the width of the preformatted text block in
pixels. Computing the appropriate font size in CSS looks like this:
The only way to assign
--element-width is by using a length in
Unfortunately, the function
calc is limited:
- In a multiplication, at least one of the factors must be unitless.
- In a division, the divisor must be unitless.
Furthermore, as soon as the font size changes,
--element-width would also
change. That rules out a pure CSS solution, but it is still possible to partly
implement this computation in CSS if you notice that the division
1rem / var(--element-width) corresponds to a constant width-to-font-size ratio. With
Assuming the ANSI text containers have the same
--parent-width, you can
update its value when the page gets resized, and the browser will take care of
recomputing the font size for each ANSI text block (which is more efficient
The text may get very close to the right border at certain sizes, so you can
add some horizontal padding relative to the font size (in
em) to keep the
text away from the edges. The code doesn't change, because
already takes the padding into account (but make sure
For the full implementation, see
As a demonstration, I leave you with this example that I hope you can decode to find out the topic of an upcoming article: