This post announces the new safe-coloured-text library. The prompt to make this library was that the maintainer of the rainbow package didn't want to get rid of the lens dependency to decrease its dependency footprint.
I learnt a lot while writing this library, and it has produced some of the prettiest output that I have ever seen in my terminal. It turns out that most "coloured text" libraries have quite nasty failure modes like environment-dependent crashes, so I made sure to make the library safe, wherever possible.
Once I had gotten started, I figured I would implement things the way I like them.
Comparison to other libraries
There are a few "coloured text" libraries on hackage, but they all had bits that I didn't like.
Why not use ansi-terminal?
The ansi-terminal library is the oldest library I considered, so it is probably the most-used, however:
It uses
Stringand does not supportText.It doesn't have the concept of a
Chunk, but is rather more low-level.Nit: It spells "colour" as "color".
Why not use ansi-wl-pprint?
It uses
Stringand does not supportText.It has a rather large dependency footprint.
It does not support 8-bit or 24-bit colours.
It has first-class colours, but they are in the
Internalmodule.Nit: It spells "colour" as "color".
Why not use rainbow?
The rainbow library is what I would have recommended until recently. It is still a fine library, however:
It depends on
lens, which has an enormous dependency footprint.It does not support 24-bit colours.
Nit: It outputs rather more ANSI escape codes than necessary.
Nit: It uses the concept of a
byteStringMakerfunction instead of having a printable value that represents how many colours the terminal supports.Nit: It spells "colour" as "color".
Why not use colourista?
There is a nice library by the lovely folks at Kowainik that also deals with coloured text in the terminal: colourista. It is very nicely documented, of course, but it differs from safe-coloured-text
It uses the unsafe (and slower)
Data.Text.IO.putStrLn. See Haskell, the bad partsIt does not support 8-bit or 24-bit colours.
It does not determine whether where you want to print to supports colours.
It doesn't have the concept of a
Chunk, but is rather more low-level.Nit: It has constants for colour codes, rather than typed colours.
Nit: It has more dependencies.
Nit: It spells "colour" as "color" sometimes.
Why not use pretty-terminal?
It doesn't have the concept of a
Chunk, but is rather more low-level.It does not support 8-bit or 24-bit colours.
It uses
Textconcatenation, which isO(n), to add escape codes to text.Nit: It doesn't check terminal capabilities to determine whether the terminal supports colour.
Nit: It spells "colour" as "color".
Why use safe-coloured-text?
Safe outputting
The number one reason for me to prefer the safe-coloured-text library is that it uses the safer Data.ByteString.Builder.hPutBuilder to output the rendered colourful text.
-- | Print a list of chunks to the given 'Handle' with given 'TerminalCapabilities'. hPutChunksWith :: TerminalCapabilities -> Handle -> [Chunk] -> IO () hPutChunksWith tc h cs = SBB.hPutBuilder h $ renderChunks tc cs
See Haskell, the bad parts for a more detailed explanation of why this is important.
It is also probably more performant, but I don't really care about that.
Tests
The safe-coloured-text library is well-tested. It has functional tests and a large amount of golden tests to make sure that the library keeps working.
The golden tests take advantage of sydtest's built-in golden test support and they are beautiful:
Chunks
The safe-coloured-text library defines a Chunk as a piece of text that is styled the same way throughout. It is built using chunk :: Text -> Chunk or using the IsString instance, and can be styled using combinators that set different style attributes.
When determining how to output a chunk, the library throws away any the unsupported colours. This way one build up a Chunk-based data structure without worrying about terminal emulator colour support, and only render it when necessary.
String types
The library is rather opinionated about the string types it uses, on purpose. There is no built-in support for String, building coloured text uses Text, and the output is a ByteString Builder. There's distinctly more difficult to 'accidentally' use System.IO.putStr or Data.Text.IO.putStr and have code that works in development but crashes in production, than it is to just use the safe ByteString Builder output functions.
Considering coloured text "bytes" rather than "text" could be contentious because the necessary ANSI escape codes still fit in the ASCII. However, the fact that there is extra work required to circumvent this decision and get a Text value is entirely on purpose. Bite me.
Minimal dependencies
The only dependencies are bytestring, terminfo and text:
library: source-dirs: src dependencies: - bytestring - terminfo - text
If the terminfo dependency ever gets us into trouble, we can factor it out as well.
Minimal output
This is not a big deal, but safe-coloured-text takes some care to not output more than necessary. For example, these two strings look the same, but the second is almost twice as long:
safe-coloured-text:
\ESC[34mTests:\ESC[m Passed: \ESC[32m0\ESC[m Failed: \ESC[32m0\ESC[m Test suite took \ESC[33m 0.00 seconds\ESC[m
rainbow:
\ESC[0m\ESC[38;5;4mTests:\ESC[0m \ESC[0m\ESC[0m \ESC[0m\ESC[0m \ESC[0m\ESC[0m \ESC[0m\ESC[0m \ESC[0m \ESC[0m\ESC[0mPassed: \ESC[0m\ESC[0m\ESC[38;5;2m0\ESC[0m \ESC[0m \ESC[0m\ESC[0mFailed: \ESC[0m\ESC[0m\ESC[38;5;2m0\ESC[0m \ESC[0m \ESC[0m\ESC[0mTest suite took \ESC[0m\ESC[0m\ESC[38;5;3m 0.00 seconds\ESC[0m \ESC[0m\ESC[0m
The safe-coloured-text does not emit sequences if a chunk is completely plain, but it does not deal with inter-chunk inefficiencies.
8-bit and 24-bit colours
There are three types of colours that are regularly supported in terminal emulators:
The standard 8 terminal colours:
Black
Red
Green
Yellow
Blue
Magenta
Cyan
White
8-bit colours:
the above 8, with a "bright" version of each
24 shades of grey
216 colours of the emulator's choice
24-bit "true" colours
The safe-coloured-text library has support for all of these as first-class values:
data Colour = Colour8 !ColourIntensity !TerminalColour | Colour8Bit !Word8 -- The 8-bit colour | Colour24Bit !Word8 !Word8 !Word8 deriving (Show, Eq, Generic)
It also has support for the colour-part of CSI codes and SGR parameters:
-- https://en.wikipedia.org/wiki/Escape_character#ASCII_escape_character
newtype CSI
= SGR [SGR]
deriving (Show, Eq, Generic)
-- https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
data SGR
= Reset
| SetItalic !Bool
| SetUnderlining !Underlining
| SetConsoleIntensity !ConsoleIntensity
| SetColour !ColourIntensity !ConsoleLayer !TerminalColour
| Set8BitColour !ConsoleLayer !Word8
| Set24BitColour
!ConsoleLayer
!Word8 -- Red
!Word8 -- Green
!Word8 -- Blue
deriving (Show, Eq, Generic)
Terminal support for colours
Not every terminal supports all colour variants. The getTerminalCapabilitiesFromEnv and getTerminalCapabilitiesFromHandle functions produce a value that represents how many colours are supported:
-- Note that the order of these constructors matters!
data TerminalCapabilities
= -- | No colours
WithoutColours
| -- | Only 8 colours
With8Colours
| -- | Only 8-bit colours
With8BitColours
| -- | All 24-bit colours
With24BitColours
deriving (Show, Eq, Ord, Generic)
A value like this can then be used to output as many colours as are supported by the terminal emulator.
Spelling
This is such a minor thing, and it shouldn't bother me as much as it does, but it does. As such, the library uses "colour" in the docs, and has both "colour" and "color" versions for in the functions:
-- | Build an 8-bit RGB Colour -- -- This will not be rendered unless 'With8BitColours' is used. colour256 :: Word8 -> Colour colour256 = Colour8Bit -- | Alias for 'colour256', bloody americans... color256 :: Word8 -> Colour color256 = colour256
References
Thanks to whomever wrote the Wikipedia article about these escape codes! It was extremely helpful and surprisingly interesting, history-wise.
The code for the library is available online: