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 explicitReset. Cross-platform Windows VT enabling (hidesSetConsoleModebehindhSupportsANSI). 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/Stringcolouring DSL layered on top of ansi-terminal.chunk "error" & fore red & boldbuilds aChunkvalue;putChunkLnrenders the chunk withResetappended. 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.