跳到主要内容
ansicode
Crystal

在 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` 组合够用就跳过。

常用写法

直接 puts 配合 \e —— Crystal 接受所有常见 ESC 形式
# 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")
Colorize 标准库 —— "text".colorize.red.bold 读起来像英语
# 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
term-cursor + term-screen —— 光标控制与 TTY 大小
# 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}"
能力门控 + 通过 {% if flag?(:win32) %} 启用 Windows VT
# 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 %}

相关序列

其他语言