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
Stringextensions like"error".red.bold.onWhite. Truecolor viaColor.hex("#ff8000"). Auto-detects terminal capability viaRainbow.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--helpoutput 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: "") }