在 Swift 中使用 ANSI 转义码 —— \u{1b}、Rainbow、Linux + macOS 上的 isatty
Swift 字符串字面量用 `\u{1b}` 表示 ESC 字节 —— **不是** `\x1b`(这是把 C / Rust / Python 配方复制到 Swift 时最常见的卡点)。`print("\u{1b}[31mred\u{1b}[0m")` 可在 macOS Terminal、iTerm2,以及面向服务器端的 swift-on-server 上工作。需要顺手的彩色 API 时选 `Rainbow`(Swift 中星标最多的 ANSI 字符串库),它为 `String` 添加 `"error".red.bold` 这类扩展。Apple 自家的 `swift-argument-parser` 通过相同原语提供彩色 `--help` 输出。需要完整 TUI(面板、鼠标、原始模式)时可选 ncurses 风格的 `TermKit`。能力检测使用 `Darwin` / `Glibc` 的 `isatty(STDOUT_FILENO)`,或 Swift 5.7+ 的 `FileHandle.standardOutput.isTerminal`(桥接到底层 isatty)。
推荐库
- Rainbow
Swift 事实标准的 ANSI 字符串库 —— 为 `String` 添加 `"error".red.bold.onWhite` 之类扩展。通过 `Color.hex("#ff8000")` 支持 TrueColor。通过 `Rainbow.outputTarget`(`.console` / `.unknown` / `.xcodeColors`)自动检测终端能力;在 CI 中显式设置即可启用颜色。
- swift-argument-parser
Apple 官方 CLI 参数库 —— `swift package`、`swift run` 以及大量 Swift CLI 均使用。`--help` 在 TTY 上输出 ANSI 强调,被管道时降为纯文本。是一份产品级 Swift CLI 如何在无第三方依赖下做能力检测的标准参考。
- TermKit
Miguel de Icaza 编写的 ncurses 风格 TUI 框架 —— 窗口、对话框、菜单、鼠标、焦点。纯 Swift,可在 macOS 与 Linux 上运行。当字符串着色不够、需要完整面板式 UI 时的正确选择。
- ANSITerminal
小而专注的库 —— 光标定位、屏幕清除、原始模式、按键读取、鼠标。当你需要光标 / 擦除 / 鼠标原语但不想引入 TermKit 完整 TUI 时很合适。支持 macOS + Linux。
常用写法
// Swift uses \u{1b} for the ESC byte. \x1b is a C-string escape and
// does NOT compile in Swift string literals — common copy-paste pitfall.
print("\u{1b}[1;31merror:\u{1b}[0m permission denied")
print("\u{1b}[33mwarn:\u{1b}[0m deprecated flag")
print("\u{1b}[32mok:\u{1b}[0m 142 tests passed")
// Optional convenience constant — define once, reuse:
let ESC = "\u{1b}"
print("\(ESC)[38;2;255;128;0morange truecolor\(ESC)[0m")// Package.swift:
// .package(url: "https://github.com/onevcat/Rainbow", from: "4.0.0")
import Rainbow
print("error:".red.bold + " permission denied")
print("warn:".yellow + " deprecated flag")
print("ok:".green + " 142 tests passed")
// Background + foreground + bold:
print("FATAL".white.onRed.bold)
// Truecolor via hex (Rainbow >= 4.0):
print("orange".hex("ff8000"))
// Force the output target — useful in CI / tests where auto-detect
// would otherwise strip the codes:
Rainbow.outputTarget = .console#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#endif
func ansiCapable() -> Bool {
// The Unix-standard kill switch — honour first.
if ProcessInfo.processInfo.environment["NO_COLOR"] != nil { return false }
// Piped or redirected — don't pollute logs / files.
if isatty(STDOUT_FILENO) == 0 { return false }
return true
}
func style(_ text: String, _ sgr: String) -> String {
ansiCapable() ? "\u{1b}[\(sgr)m\(text)\u{1b}[0m" : text
}
print(style("OK", "32"))
print(style("FAIL", "1;31"))import Foundation
// Swift 5.7 added FileHandle.isTerminal — a portable wrapper around
// isatty(_:). Works on Linux when targeting swift-on-server (5.7+).
// Older deployment targets need the isatty(STDOUT_FILENO) form above.
if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) {
let isTTY = FileHandle.standardOutput.isTerminal
if isTTY {
FileHandle.standardOutput.write(
Data("\u{1b}[32mready\u{1b}[0m\n".utf8)
)
} else {
FileHandle.standardOutput.write(Data("ready\n".utf8))
}
}
// Bonus — alt screen / cursor off for full-screen UI, restore on exit:
let altOn = "\u{1b}[?1049h\u{1b}[?25l"
let altOff = "\u{1b}[?25h\u{1b}[?1049l"
print(altOn, terminator: ""); defer { print(altOff, terminator: "") }