在 Haskell 中使用 ANSI 转义码 —— \ESC、ansi-terminal、rainbow
Haskell 的字符串字面量支持 `"\ESC"` —— 标准、易读、按控制字符命名的 ESC 转义形式。它与 `"\27"`(十进制)和 `"\x1B"`(十六进制)解析为完全相同的字节,但 `\ESC` 是 Haskell 惯用的形式(在本站记录的所有语言中独一无二 —— 只有 Haskell 把字面量关键字 `\ESC` 渲染成字节 27)。`putStr "\ESC[31mred\ESC[0m\n"` 在 GHC 7.0 以来的所有版本,以及 macOS、Linux、BSD、Windows 10+ 的 Conhost 1709+ / Windows Terminal(均原生解析 ANSI)上工作。对较旧的 Windows(1607 前的 Conhost),**`ansi-terminal`** 库把 `SetConsoleMode` 调用藏在 `hSupportsANSI` 后面。 标准辅助选 **`ansi-terminal`** —— 声明式 `setSGR [SetColor Foreground Vivid Red, SetConsoleIntensity BoldIntensity]` API,明确的 `Reset`、跨平台 Windows VT 启用、命名的光标 / 擦除 / 滚动辅助,以及编译期捕捉坏组合的类型安全形式。**`rainbow`** 在其上叠加流畅的 `Text` / `String` 着色 DSL —— `chunk "error" & fore red & bold` 构造 `Chunk`,`putChunkLn` 渲染输出。需要完整 TUI 时链接 **`brick`**(基于 vty,声明式面板与事件循环架构 —— 被 `lorri`、`matterhorn`、`ghcid` TUI、`tasty-bench`-tui 使用)。要做 REPL 风格的行编辑(历史、补全、多行)选 **`haskeline`** —— GHCi 自己使用的 readline 等价物。 能力门控:`ansi-terminal` 的 `hSupportsANSI stdout` 是标准能力检测(统一处理 macOS/Linux/BSD 的 isatty 与 Windows 的 VT 启用);当你想跳过这个库依赖时,搭配 `lookupEnv "NO_COLOR"`(`System.Environment`)与 `hIsTerminalDevice stdout`(`System.IO`)即可。GHCi REPL 在现代终端宿主中解析 ANSI;`cabal repl` 与 `stack ghci` 有时会包装 stdout —— 已知的摩擦点。
推荐库
- ansi-terminal
标准 Haskell ANSI 库。声明式 `setSGR [SetColor Foreground Vivid Red, SetConsoleIntensity BoldIntensity]` API,配显式 `Reset`。跨平台 Windows VT 启用(把 `SetConsoleMode` 藏在 `hSupportsANSI` 后面)。光标(`cursorUp n`)、擦除(`clearScreen`、`clearLine`)与滚动的命名辅助。类型安全形式在编译期捕捉错误的 SGR 组合。
- rainbow
在 ansi-terminal 之上的流畅 `Text` / `String` 着色 DSL。`chunk "error" & fore red & bold` 构造 `Chunk` 值;`putChunkLn` 渲染该 chunk 并自动附加 `Reset`。可用 `<>` 组合内联混合样式。在非 TTY 输出流上自动禁用。
- brick
基于 vty 的声明式 TUI 框架 —— 面板与事件循环架构,被 lorri、matterhorn、ghcid TUI、tasty-bench 使用。组件、视口、对话框、编辑框、列表选择、按键处理。当 ansi-terminal 的扁平字符串输出不够、需要完整 TUI 时选它。
- haskeline
REPL 风格行编辑的 readline 等价物 —— 历史、Tab 补全、多行输入、提示符续行。GHCi 本身就用 haskeline。跨平台(Linux/macOS 通过行编辑库,Windows 通过自带的 ANSI 感知代码)。
常用写法
-- 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.