ANSI escape codes in Swift — \u{1b}, Rainbow, isatty on Linux + macOS
Swift's string literal uses `\u{1b}` for the ESC byte — **not** `\x1b` (which is the #1 friction point copy-pasting C / Rust / Python recipes into Swift). `print("\u{1b}[31mred\u{1b}[0m")` works on macOS Terminal, iTerm2, and on Linux when targeting swift-on-server. For ergonomic colour APIs reach for `Rainbow` (the most-starred Swift ANSI string library), which adds `String` extensions like `"error".red.bold`. Apple's own `swift-argument-parser` ships coloured `--help` output via the same primitives. For full TUI work (panels, mouse, raw mode) `TermKit` is the ncurses-style option. Capability gating uses `isatty(STDOUT_FILENO)` from `Darwin` / `Glibc`, or `FileHandle.standardOutput.isTerminal` on Swift 5.7+ where it's bridged.
Recommended libraries
- Rainbow
The de facto Swift ANSI string library — adds `String` extensions like `"error".red.bold.onWhite`. Truecolor via `Color.hex("#ff8000")`. Auto-detects terminal capability via `Rainbow.outputTarget` (`.console` / `.unknown` / `.xcodeColors`); set explicitly to opt into colour in CI.
- swift-argument-parser
Apple's first-party CLI argument library — used by `swift package`, `swift run`, and countless Swift CLIs. The `--help` output emits ANSI for emphasis on TTY stdout, plain text when piped. A canonical reference for how a production Swift CLI handles capability gating without third-party deps.
- TermKit
ncurses-style TUI framework by Miguel de Icaza — windows, dialogs, menus, mouse, focus. Pure Swift, runs on macOS and Linux. The right call when you outgrow string colouring and need a full panel-based UI.
- ANSITerminal
Small focused library — cursor positioning, screen clearing, raw mode, keystroke reading, mouse. Useful when you need cursor / erase / mouse primitives but don't want the full TUI weight of TermKit. macOS + Linux.
Idiomatic patterns
// 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: "") }