在 Elixir 中使用 ANSI 转义码
Elixir 标准库提供 IO.ANSI 模块 —— 每个命名 SGR 和少量光标 / 屏幕辅助都是返回其字节形式的函数(IO.ANSI.red() 返回 "\e[31m")。规范用法是 IO.ANSI.format/2,它接受混合原子(命名转义)与二进制(你的文本)的列表,返回适合 IO.puts/1 或 IO.write/1 的 iolist —— 例如 IO.puts(IO.ANSI.format([:bright, :red, "error: ", :reset, "permission denied"]))。IO.ANSI.enabled?/0 是内置于标准库的能力门控:遵守 :elixir 应用环境 :ansi_enabled(iex 在 tty 上设为 true、在管道上设为 false)以及 NO_COLOR 环境变量。
超出零散 print 的场景:Bunt(savonarola/bunt)为 IO.ANSI 加糖,支持字符串内的命名颜色标签 —— Bunt.puts([:red, "error: ", :default_color, "permission denied"]) 或行内形式 Bunt.puts("[red]error:[/red] permission denied")。Owl(fuelen/owl)是现代输出工具集(盒子、表格、进度条、多行 spinner、ANSI 超链接)—— 被 Phoenix 生成的 CLI 任务、mix 扩展、livebook 启动输出使用。完整全屏 TUI 请用 Ratatouille(ndreynolds/ratatouille),Elm 风格的 model / update / view 架构经 termbox2 渲染为 ANSI。
推荐库
- IO.ANSI
标准库模块 ——
IO.ANSI.red/0、IO.ANSI.bright/0、IO.ANSI.format/2(将原子 + 二进制混合为 iolist)、IO.ANSI.enabled?/0(能力门控,遵守:elixir的:ansi_enabled应用环境 +NO_COLOR)。所有转义生成器都是返回二进制的纯函数 —— 零运行时开销、零依赖。 - Bunt
基于 IO.ANSI 的标签字符串语法糖 ——
Bunt.puts("[red]error:[/red] permission denied")或Bunt.puts([:red, "error", :reset])。新增命名颜色别名(:chartreuse、:burlywood3等)以更精细地控制 SGR-256。credo、ex_doc以及众多mix任务 UI 都在用。 - Owl
现代输出工具集 —— 盒子(
Owl.Box.new/2)、表格(Owl.Table.new/2)、进度条(Owl.ProgressBar)、多行 spinner、ANSI 超链接(Owl.Data.tag/2)。被 Phoenix 生成的 CLI 任务、mix livebook.install以及近期的 Elixir 工具 UI 使用。 - Ratatouille
全屏 TUI 框架 —— Elm 风格的
model / update / view架构,经内置的termbox2NIF 渲染为 ANSI。view 函数返回元素树,运行时做 diff 并绘制。被mix任务仪表板、日志查看器、内部开发工具使用。
常用写法
# Elixir string literals do support \e (= \x1b). All three forms are
# byte-equivalent: \e, \x1b, \u{1b}. IO.puts is byte-clean.
IO.puts("\e[1;31merror:\e[0m permission denied")
IO.puts("\x1b[1;32mok:\x1b[0m all 142 tests passed")IO.puts(IO.ANSI.format([:bright, :red, "error: ", :reset, "permission denied"]))
IO.puts(IO.ANSI.format([:green, "ok: ", :reset, "all 142 tests passed"]))
# IO.ANSI.format/2 takes an optional second arg: emit_codes? boolean.
# Pass IO.ANSI.enabled? so the same call works in pipes / CI / NO_COLOR.
IO.puts(IO.ANSI.format([:red, "error"], IO.ANSI.enabled?()))defmodule MyApp.Color do
# IO.ANSI.enabled? already honours :ansi_enabled + NO_COLOR. Layer
# FORCE_COLOR on top for CI runs that want colour despite no tty.
def enabled? do
cond do
System.get_env("NO_COLOR") -> false
System.get_env("FORCE_COLOR") -> true
true -> IO.ANSI.enabled?()
end
end
def paint(chunks) when is_list(chunks) do
IO.ANSI.format(chunks, enabled?())
end
end
IO.puts(MyApp.Color.paint([:bright, :red, "error: ", :reset, "no perm"]))# Truecolor — IO.ANSI has no helper for SGR \x1b[38;2;r;g;b m, but you
# can splice raw bytes into the format list. Or use Owl, which accepts
# RGB tuples directly.
# Hand-rolled truecolor:
fg_rgb = fn r, g, b -> "\e[38;2;#{r};#{g};#{b}m" end
IO.puts([fg_rgb.(203, 166, 247), "lavender truecolor", IO.ANSI.reset()])
# Owl box with truecolor border:
# {:owl, "~> 0.12"} in mix.exs deps
"Build complete — 142 tests, 0 failures"
|> Owl.Box.new(border_style: :double, title: " status ", padding: 1)
|> Owl.IO.puts()