跳到主要内容
ansicode
Haskell

在 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 感知代码)。

常用写法

直接 putStr 配合字符串字面量中的 \ESC
-- 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 —— 声明式 setSGR、类型安全的 SGR 组合
-- 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"
能力门控 —— hSupportsANSI + NO_COLOR + GHCi 注意事项
-- 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 —— 流畅 Chunk DSL 配合 truecolor 与组合
-- 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.

相关序列

其他语言