跳到主要内容
ansicode
Swift

在 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。

常用写法

直接 print 配合 \u{1b} —— 注意不是 \x1b
// 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")
Rainbow —— String 扩展
// 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
能力门控 —— NO_COLOR + isatty(STDOUT_FILENO)
#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"))
Swift 5.7+ —— FileHandle.isTerminal(Linux + macOS)
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: "") }

相关序列

其他语言