ANSI escape codes in Haskell — \ESC, ansi-terminal, rainbow
Haskell's string literals support `"\ESC"` — the canonical, readable, control-character-named ESC escape. It parses identically to `"\27"` (decimal) and `"\x1B"` (hex), but `\ESC` is the Haskell-idiomatic form (distinct from every other language this site documents — Haskell is the only mainstream language where the literal `\ESC` keyword renders to byte 27). `putStr "\ESC[31mred\ESC[0m\n"` works on every GHC since 7.0, on macOS, Linux, BSDs, and on Windows 10+ with Conhost 1709+ / Windows Terminal (both parse ANSI natively). For older Windows builds (pre-1607 Conhost) the **`ansi-terminal`** library hides the `SetConsoleMode` call behind `hSupportsANSI`. For the canonical helper reach for **`ansi-terminal`** — declarative `setSGR [SetColor Foreground Vivid Red, SetConsoleIntensity BoldIntensity]` API with explicit `Reset` clarity, cross-platform Windows VT enabling, named cursor / erase / scroll helpers, and a Type-Safe Way that catches bad combinations at compile time. **`rainbow`** layers a fluent `Text` / `String` colouring DSL on top — `chunk "error" & fore red & bold` builds a `Chunk` and `putChunkLn` renders it. For full TUIs link **`brick`** (vty-based, declarative panel-and-event-loop architecture — used by `lorri`, `matterhorn`, the `ghcid` TUI, `tasty-bench`-tui). For REPL-style line editing (history, completion, multi-line) reach for **`haskeline`** — the readline-equivalent that GHCi itself uses. Capability gating: `ansi-terminal`'s `hSupportsANSI stdout` is the canonical capability check (handles macOS/Linux/BSD isatty + Windows VT enablement); pair it with `lookupEnv "NO_COLOR"` (`System.Environment`) and `hIsTerminalDevice stdout` (`System.IO`) when you want to skip the library dep. The GHCi REPL parses ANSI in modern terminal hosts; `cabal repl` and `stack ghci` sometimes wrap stdout — known friction.
Recommended libraries
- ansi-terminal
Canonical Haskell ANSI library. Declarative `setSGR [SetColor Foreground Vivid Red, SetConsoleIntensity BoldIntensity]` API with explicit `Reset`. Cross-platform Windows VT enabling (hides `SetConsoleMode` behind `hSupportsANSI`). Named helpers for cursor (`cursorUp n`), erase (`clearScreen`, `clearLine`), and scroll. The Type-Safe approach catches bad SGR combinations at compile time.
- rainbow
Fluent `Text` / `String` colouring DSL layered on top of ansi-terminal. `chunk "error" & fore red & bold` builds a `Chunk` value; `putChunkLn` renders the chunk with `Reset` appended. Composable with `<>` for inline mixed styles. Auto-disables on non-TTY output streams.
- brick
vty-based declarative TUI framework — panel-and-event-loop architecture, used by lorri, matterhorn, the ghcid TUI, tasty-bench. Widgets, viewports, dialogs, edit boxes, list selection, key handling. The right call when you outgrow ansi-terminal's flat string output and need a full TUI.
- haskeline
Readline-equivalent for REPL-style line editing — history, tab completion, multi-line input, prompt continuation. GHCi itself uses haskeline. Cross-platform (Linux/macOS via line-editing libs, Windows via its own ANSI-aware code).
Idiomatic patterns
-- Haskell supports \ESC (named control character), \27
-- (decimal), and \x1B (hex) all parsing to byte 27. \ESC is
-- the canonical Haskell-idiomatic form — the only mainstream
-- language where the literal keyword "\ESC" expands to ESC.
module Main where
main :: IO ()
main = do
putStr "\ESC[1;31merror:\ESC[0m permission denied\n"
putStr "\ESC[33mwarn:\ESC[0m deprecated flag\n"
putStr "\ESC[32mok:\ESC[0m 142 tests passed\n"
-- Truecolor — 38;2;R;G;B
putStr "\ESC[38;2;255;128;0morange truecolor\ESC[0m\n"
-- Decimal + hex variants — identical bytes:
putStr "\27[33mwarn (decimal escape)\27[0m\n"
putStr "\x1B[36mcyan (hex escape)\x1B[0m\n"-- cabal install ansi-terminal
-- stack install ansi-terminal
import System.Console.ANSI
( setSGR, SGR (..), ColorIntensity (..), Color (..)
, ConsoleLayer (..), ConsoleIntensity (..)
, clearLine, cursorUp, hSupportsANSI
)
import System.IO (hFlush, stdout)
main :: IO ()
main = do
setSGR [SetColor Foreground Vivid Red, SetConsoleIntensity BoldIntensity]
putStr "FATAL"
setSGR [Reset]
putStrLn " server crashed"
-- 256-colour via SetPaletteColor:
setSGR [SetPaletteColor Foreground 208] -- orange
putStrLn "256-colour orange"
setSGR [Reset]
-- Truecolor via SetRGBColor (Data.Colour.SRGB):
-- setSGR [SetRGBColor Foreground (sRGB 1.0 0.5 0.0)]
-- Cursor + erase helpers — declarative, cross-platform:
putStr "loading"
hFlush stdout
cursorUp 0
clearLine
putStrLn "done"-- ansi-terminal's hSupportsANSI is the canonical answer:
-- - Linux/macOS/BSD: returns isatty(stdout) result
-- - Windows: also enables VT mode via SetConsoleMode and
-- returns whether the enable succeeded (true on 1709+).
--
-- GHCi REPL parses ANSI in modern terminal hosts. But:
-- - cabal repl / stack ghci sometimes wrap stdout
-- (output appears as raw \ESC[31m bytes — known friction)
-- - GHC's :type / :info commands occasionally print raw
-- ANSI when haskeline isn't between you and the terminal
import System.Console.ANSI (hSupportsANSI, setSGR, SGR (..), ColorIntensity (..), Color (..), ConsoleLayer (..))
import System.Environment (lookupEnv)
import System.IO (stdout)
ansiCapable :: IO Bool
ansiCapable = do
noColor <- lookupEnv "NO_COLOR"
case noColor of
Just _ -> pure False
Nothing -> hSupportsANSI stdout
withColour :: Color -> IO () -> IO ()
withColour c action = do
ok <- ansiCapable
if ok
then do setSGR [SetColor Foreground Vivid c]
action
setSGR [Reset]
else action
main :: IO ()
main = do
withColour Green (putStrLn "OK")
withColour Red (putStrLn "FAIL")-- cabal install rainbow
{-# LANGUAGE OverloadedStrings #-}
import Rainbow
( chunk, fore, back, bold, underline
, red, green, yellow, blue, magenta
, putChunkLn, putChunk, (&), color256, rgb
)
main :: IO ()
main = do
-- Build a Chunk and render it:
putChunkLn (chunk "error: permission denied" & fore red & bold)
putChunkLn (chunk "warn: deprecated" & fore yellow)
putChunkLn (chunk "ok: 142 tests passed" & fore green & underline)
-- Inline composition with <> — alternate styles in one line:
putChunkLn
( (chunk "FATAL" & fore red & bold)
<> chunk " server crashed at "
<> (chunk "line 42" & fore magenta)
)
-- 256-colour palette:
putChunkLn (chunk "256-colour orange" & fore (color256 208))
-- Truecolor — rgb r g b (0-255):
putChunkLn (chunk "truecolor warm orange" & fore (rgb 255 128 0))
-- rainbow auto-disables on non-TTY output streams,
-- so piping the binary to a file emits plain text.