Skip to main content
ansicode
Haskell

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

Direct putStr with \ESC in a string literal
-- 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"
ansi-terminal — declarative setSGR, type-safe SGR composition
-- 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"
Capability gate — hSupportsANSI + NO_COLOR + GHCi caveat
-- 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")
rainbow — fluent Chunk DSL with truecolor + composition
-- 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.

Related sequences

Other languages