在 Crystal 中使用 ANSI 转义码 —— \e、Colorize 标准库、term-cursor
Crystal 拥有所有现代语言中最人体工学的标准库 ANSI 支持。`"..."` 字符串字面量接受所有常见的 ESC 形式 —— `"\e"`、`"\033"`(八进制)、`"\x1b"`(十六进制)、`"\u001b"`(4 位 Unicode)、`"\u{1b}"`(变长 Unicode)—— 团队读起来最快的就用哪种。`puts` 与 `print` 直接写原始字节(在刚装好的编译器上、不装任何 shard,`puts "\e[1;31merror:\e[0m permission denied"` 直接可用)。在 macOS、Linux、BSD、Windows 10+ Conhost 1709+ 上终端原生解析这些字节。 首选标准库 **`Colorize`** 模块(`require "colorize"`)—— 随编译器一起发布,API 读起来像英语:`"error".colorize.red.bold`、`"warning".colorize(:yellow)`、`"hot".colorize.fore(255, 80, 0)` 给 truecolor、`"x".colorize.fore(196)` 给 256-palette。Colorize **首次使用时自动检查 `STDOUT.tty?`**,重定向到文件或管道时静默 no-op —— 零能力门控样板就得到正确行为。 光标移动 / 清屏 / 滚动 / 保存恢复选 **`term-cursor`** shard(`Term::Cursor.move(0, 0)`、`Term::Cursor.clear_screen`、`Term::Cursor.save`)。TTY 大小 —— Crystal 标准库刻意省略公开的终端大小 API —— 引入 **`term-screen`** shard(`Term::Screen.size # => {height, width}`)。需要完整的 blessed 等价 TUI 运行时(窗口、焦点、鼠标、重绘循环)选 **`crysterm`**,Crystal 最成熟的 TUI 库。 能力门控:`STDOUT.tty?` 是标准库检查 —— stdout 重定向到文件、管道或非终端 IO 时返回 `false`。搭配 `ENV["NO_COLOR"]?` 处理通用 env 约定;`Colorize.enabled = false` 全局强制禁用。**Windows VT 注意**:Crystal 编译为原生二进制 —— 在 Conhost 1709 之前的 Windows 上,终端要等到你用 `ENABLE_VIRTUAL_TERMINAL_PROCESSING` 调用 `LibC.SetConsoleMode` 才会解析 ANSI 字节。Crystal 的编译期 `{% if flag?(:win32) %}` 宏是惯用门控:libc 绑定只在 Windows 目标上编译,因此一份源文件在所有受支持平台都干净构建。
推荐库
- Colorize (stdlib)
随编译器一起发布 —— `require "colorize"` 即可。读起来像英语:`"error".colorize.red.bold`、`"warning".colorize(:yellow)`、`"x".colorize.fore(255, 80, 0)`(truecolor)、`.fore(196)`(256-palette)、`.back(:blue)`、`.mode(:underline)`。方法从左到右链式调用。首次使用时自动检查 `STDOUT.tty?`,重定向时静默 no-op —— 零能力门控样板。任何现代语言中最人体工学的标准库 ANSI 辅助。
- term-cursor
光标移动 / 清屏 / 保存恢复 / 隐藏显示。`Term::Cursor.move(x, y)`、`Term::Cursor.clear_screen`、`Term::Cursor.clear_line`、`Term::Cursor.save` / `restore`、`Term::Cursor.hide` / `show`。返回普通 `String`(原始转义序列)—— 用 `print Term::Cursor.move(10, 5)` 应用,或保存字符串与正文拼接。底层纯标准库,无 native 绑定。
- term-screen
终端大小 —— Crystal 标准库刻意省略公开的终端大小 API,本 shard 填补此缺口。`Term::Screen.size # => {rows, cols}`、`Term::Screen.width`、`Term::Screen.height`。POSIX 上读 `ioctl(TIOCGWINSZ)`,Windows 上经 libc 读 `GetConsoleScreenBufferInfo`,回退到 `$LINES` / `$COLUMNS` env,再回退到 24×80。任何布局工作都搭配 `term-cursor`。
- crysterm
完整 TUI 框架 —— Crystal 对 blessed(Node)/ notcurses(C)/ textual(Python)的回答。窗口 / 盒 / 列表 / 表单组件、焦点管理、鼠标 + 键盘事件循环、双缓冲重绘、备用屏生命周期。只有当 ANSI 单独不够用时才用 —— 你在构建多面板交互 UI,而不是仅着色日志行。本列表中依赖最重;如果单个 `Colorize` + `term-cursor` 组合够用就跳过。
常用写法
# Crystal "..." string literals accept five ESC forms.
# Pick whichever your team reads fastest — they all
# compile to the same single byte (0x1b).
#
# "\e" — ESC escape (most readable)
# "\033" — octal (1, 2, or 3 digits)
# "\x1b" — hex (exactly 2 digits)
# "\u001b" — Unicode (exactly 4 hex digits)
# "\u{1b}" — Unicode variable width
puts "\e[1;31merror:\e[0m permission denied"
puts "\033[33mwarn:\033[0m deprecated flag"
puts "\x1b[32mok:\x1b[0m 142 tests passed"
puts "\u001b[36minfo:\u001b[0m skipping cache"
puts "\u{1b}[35mdebug:\u{1b}[0m checkpoint hit"
# Truecolor — 38;2;R;G;B
puts "\e[38;2;255;128;0morange truecolor\e[0m"
# 256-palette — 38;5;n (n = 0..255)
puts "\e[38;5;196mbright red\e[0m"
# Reusable helper — works without any shard:
def esc(sgr : String, text : String) : String
"\e[#{sgr}m#{text}\e[0m"
end
puts esc("1;31", "FATAL") + " server crashed"
puts esc("32", "ok:") + " 142 tests passed"
puts esc("38;2;255;128;0", "truecolor orange")# Ships with the compiler — no shard needed.
require "colorize"
# Named SGR colours — chain methods left-to-right:
puts "error: ".colorize.red.bold.to_s + "permission denied"
puts "ok: ".colorize.green.to_s + "142 tests passed"
puts " CAUTION ".colorize.black.on_yellow.to_s + " brakes wet"
# Symbol form — equivalent, sometimes nicer in data-driven code:
levels = {error: :red, warn: :yellow, ok: :green, info: :cyan}
levels.each do |lvl, colour|
puts "#{lvl.to_s.upcase.colorize(colour).bold}: example message"
end
# Truecolor — 38;2;R;G;B via .fore(r, g, b):
puts "hot pink".colorize.fore(255, 80, 200)
puts "deep teal".colorize.fore(0, 110, 130).bold
# 256-palette — .fore(n) with n in 0..255:
(0..255).step(16) do |n|
print "#{n.to_s.rjust(3)} ".colorize.fore(n)
end
puts
# Background + mode chaining:
puts "selected".colorize.fore(:white).back(:blue).mode(:underline)
# Auto-disabled when stdout is redirected — try:
# crystal run example.cr | cat
# Colorize.enabled? returns false; no escapes emitted.
puts "ok".colorize.green # writes "ok" plain when piped# shard.yml:
# dependencies:
# term-cursor:
# github: crystal-term/cursor
# term-screen:
# github: crystal-term/screen
#
# Then `shards install` and require below:
require "term-cursor"
require "term-screen"
# Term::Cursor methods return plain Strings (the raw
# escape sequence) — print them to apply, or concat:
print Term::Cursor.clear_screen
print Term::Cursor.move(0, 0)
# Right-aligned status bar pinned to bottom row:
rows, cols = Term::Screen.size
status = " 142 tests passed "
print Term::Cursor.save
print Term::Cursor.move(0, rows - 1)
print status.rjust(cols)
print Term::Cursor.restore
# Spinner — hide cursor, animate, restore:
frames = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏]
print Term::Cursor.hide
begin
10.times do |i|
print "\r#{frames[i % frames.size]} working..."
sleep 0.08.seconds
end
ensure
print "\r"
print Term::Cursor.clear_line
print Term::Cursor.show
puts "done"
end
# Term::Screen.size handles every fallback chain:
# TIOCGWINSZ ioctl → GetConsoleScreenBufferInfo
# → $LINES / $COLUMNS → 24×80 default
puts "terminal: #{Term::Screen.width}×#{Term::Screen.height}"# Capability gating in Crystal:
# STDOUT.tty? — stdlib, returns false when piped/redirected
# ENV["NO_COLOR"]? — universal opt-out env var
# ENV["FORCE_COLOR"]? — universal opt-in env var
# Colorize.enabled = false — global force-disable for the Colorize module
#
# Colorize auto-respects STDOUT.tty?, so most apps need
# nothing more than the require. Manual gating is for code
# that emits ANSI without going through Colorize.
require "colorize"
def ansi_capable? : Bool
return false if ENV["NO_COLOR"]?
return true if ENV["FORCE_COLOR"]?
STDOUT.tty?
end
# Honour the env vars in Colorize too — Colorize doesn't
# look at NO_COLOR on its own, so wire it explicitly:
Colorize.enabled = ansi_capable?
def styled(text : String, sgr : String) : String
return text unless ansi_capable?
"\e[#{sgr}m#{text}\e[0m"
end
puts styled("OK", "32")
puts styled("FAIL", "1;31")
# Windows VT enable — pre-Conhost-1709 builds need an
# explicit SetConsoleMode call. The {% if flag?(:win32) %}
# macro guards the libc binding so the same source file
# compiles cleanly on Linux/macOS without the import.
{% if flag?(:win32) %}
lib LibC
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004_u32
STD_OUTPUT_HANDLE = -11_i32
fun GetStdHandle(nStdHandle : Int32) : Void*
fun GetConsoleMode(hConsoleHandle : Void*, lpMode : UInt32*) : Int32
fun SetConsoleMode(hConsoleHandle : Void*, dwMode : UInt32) : Int32
end
def enable_vt_on_windows
handle = LibC.GetStdHandle(LibC::STD_OUTPUT_HANDLE)
mode = uninitialized UInt32
LibC.GetConsoleMode(handle, pointerof(mode))
LibC.SetConsoleMode(handle, mode | LibC::ENABLE_VIRTUAL_TERMINAL_PROCESSING)
end
enable_vt_on_windows
{% end %}