Skip to main content
ansicode
Swift

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

Direct print with \u{1b} — note: NOT \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 extensions
// 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
Capability gate — 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: "") }

Related sequences

Other languages