Monday, July 23, 2012

xterm-256color

For those of you using terminals, you are probably aware of ANSI escape codes. As a refresher, it allows formatting text with foreground and background colors, underlines, italics, blinking, among other characteristics.

Factor has support for formatted output streams that can apply styles to textual content. It is used extensively by the help system to produce documentation in both the UI and command-line, export to HTML or PDF, and even syntax highlighting.

This morning, I wanted to implement a formatted output stream that can be used in the terminal to produce colored text output easily.

You can look at documentation for the xterm-256color ANSI extensions, but basically you wrap your text with format codes, like so:

Setting foreground to blue:

Setting background to red:

There are varying methods of checking if a terminal supports the 256 color extension, but a simple way is to look at the TERM environment variable:

: 256color-terminal? ( -- ? )
    "TERM" os-env "-256color" tail? ;

The specification should tell us how to map RGB color codes to their "256color" code:

RGB Colors

A value between 16 and 231 is used for RGB colors with each color having 6 intensities. Red has a value between 0 and 5 multiplied by 36, Green has a value between 0 and 5 multiplied by 6, and Blue has a value between 0 and 5.

8 to 24 bit color conversion

To display xterm RGB colors as 24 bit RGB colors the following values are suggested for the 6 intensities: 0x00, 0x5F, 0x87, 0xAF, 0xD7, and 0xFF.

Following that guideline, we can build our map of "256color" codes. I also added system colors and grayscale colors, which is left as an exercise for the reader (or you can read the source code):

CONSTANT: intensities { 0x00 0x5F 0x87 0xAF 0xD7 0xFF }

CONSTANT: 256colors H{ }

! Add the RGB colors
intensities [| r i |
    intensities [| g j |
        intensities [| b k |
            i 36 * j 6 * + k + 16 +
            r g b 3array
            256colors set-at
        ] each-index
    ] each-index
] each-index

Given a RGBA Color, we want to convert it to an array of its red, green, and blue components, and then find the "256color" with the smallest "distance" (in this case Euclidean distance) from the desired value.

: color>rgb ( color -- rgb )
    [ red>> ] [ green>> ] [ blue>> ] tri
    [ 255 * round >integer ] tri@ 3array ;

: color>256color ( color -- 256color )
    color>rgb '[ _ distance ]
    256colors [ keys swap infimum-by ] [ at ] bi ;

Converting a color to either a foreground or a background ANSI escape code is now pretty easy:

: color>foreground ( color -- str )
    color>256color "\u00001b[38;5;%sm" sprintf ;

: color>background ( color -- str )
    color>256color "\u00001b[48;5;%sm" sprintf ;

With a basic implementation of the formatted stream protocol, we can now take something like this:

And easily (and automatically!) display it as something like this:

To see more colorful implementation of words:

Or even experiment with various background colors:

This has been committed to Factor's development branch and is available now.