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
String
and 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
String
and 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
Internal
module.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
byteStringMaker
function 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
Text
concatenation, 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: