ANSI escape codes in Scala — \u001B, fansi, scala.io.AnsiColor
Scala's string literals do **not** support `\e` or `\x1b` — the same constraint as Java and Kotlin. The portable spelling is the Unicode escape `\u001B`, which works on Scala 2 (since 2.0) and Scala 3 alike. Scala 2 additionally accepted octal `\033`, but Scala 3 removed octal escapes from string literals, so write `"\u001B[31m"` if you want code that compiles on every modern Scala. `println("\u001B[1;31merror:\u001B[0m permission denied")` works on every JVM ≥ 8, on macOS, Linux, BSDs, and on Windows 10+ with Conhost 1709+ / Windows Terminal (both parse ANSI natively). For the idiomatic helper reach for **`fansi`** (`com.lihaoyi::fansi`) — Li Haoyi's canonical Scala ANSI library used by Ammonite, mill, Scalafix, scalafmt, and most of the modern Scala ecosystem. `fansi.Color.Red("error")` returns a `fansi.Str` value with structured colour spans rather than embedded bytes; `++` concatenates spans, `.overlay(fansi.Bold.On, 0, 5)` overlays attributes on a sub-range, `.render` materialises the byte string. **`scala.io.AnsiColor`** is the stdlib mixin — `Console.RED + "error" + Console.RESET`-style constants for one-off bytes without a library dependency. **`pprint`** (also Li Haoyi) is the canonical coloured pretty-printer — `pprintln(value)` emits indented, fansi-coloured output, and the Ammonite REPL ships it as the default `println` replacement. For richer terminals (line editing, raw mode, capability detection) reach for **JLine 3** — the JVM terminal abstraction Scala's REPL itself uses. Capability gating: `System.console() != null` is the JVM-portable check (returns `null` when stdin/stdout is redirected). Pair with `sys.env.get("NO_COLOR")` for the env-var convention. **SBT caveat**: SBT wraps the build's stdout to inject its own progress UI, so child-process ANSI output sometimes loses colour — pass `-Dsbt.color=always` on the SBT command line, or set `ThisBuild / outputStrategy := Some(StdoutOutput)` in `build.sbt`, when launching scripts that need true colour passthrough. `scala-cli` and `mill` don't share this quirk.
Recommended libraries
- fansi
Canonical Scala ANSI library by Li Haoyi — used by Ammonite, mill, Scalafix, scalafmt. `fansi.Color.Red("x")` returns a structured `fansi.Str` (colour spans, not raw bytes); `++` concatenates, `.overlay(fansi.Bold.On, 0, 5)` layers attributes on a sub-range, `.render` materialises the ANSI byte string, `fansi.Str.ansiRegex.replaceAllIn(s, "")` is the canonical Scala log scrubber.
- scala.io.AnsiColor (stdlib)
Scala stdlib mixin — predefined string constants `Console.RED` / `GREEN` / `YELLOW` / `BLUE` / `MAGENTA` / `CYAN` / `WHITE` (foreground), `RED_B` / etc. (background), `BOLD` / `UNDERLINED` / `BLINK` / `REVERSED`, and `RESET`. Use for one-off coloured output without adding a library dependency. No composition primitive — just string concatenation.
- pprint
Coloured Scala pretty-printer (Li Haoyi) — `pprintln(value)` emits indented, fansi-coloured output for case classes, collections, and nested structures. The Ammonite REPL uses it as the default `println` replacement. Switch palette via `pprint.PPrinter.BlackWhite` when output isn't a TTY (or use `pprint.copy(defaultWidth = 120, defaultIndent = 4)` for custom layout).
- JLine 3
JVM terminal abstraction — same library Scala's REPL itself uses. `TerminalBuilder` constructs an ANSI-aware terminal with capability detection (raw mode, signal handling, Windows VT enablement); `LineReader` provides readline-equivalent line editing with history, completion, multi-line input. Cross-platform — Linux/macOS via JNI to termios, Windows via ConPTY.
Idiomatic patterns
// Scala 3 dropped octal escapes (\033) from string literals;
// \u001B (Unicode) is the only spelling that compiles on both
// Scala 2 and Scala 3. \e and \x1b are NOT supported in Scala
// string literals — the same constraint as Java and Kotlin.
object Main extends App {
println("\u001B[1;31merror:\u001B[0m permission denied")
println("\u001B[33mwarn:\u001B[0m deprecated flag")
println("\u001B[32mok:\u001B[0m 142 tests passed")
// Truecolor — 38;2;R;G;B
println("\u001B[38;2;255;128;0morange truecolor\u001B[0m")
// scala.io.AnsiColor — stdlib constants, no extra dep.
// Console mixes in AnsiColor, so the constants are directly
// accessible as Console.RED / GREEN / YELLOW / BLUE /
// MAGENTA / CYAN / WHITE plus _B variants for backgrounds,
// plus BOLD / UNDERLINED / BLINK / REVERSED / RESET.
println(Console.RED + "error: " + Console.RESET + "permission denied")
println(Console.BOLD + Console.GREEN + "ok" + Console.RESET +
" 142 tests passed")
println(Console.YELLOW_B + Console.BLACK + " CAUTION " +
Console.RESET + " brakes wet")
}// build.sbt:
// libraryDependencies += "com.lihaoyi" %% "fansi" % "0.5.0"
//
// scala-cli:
// //> using lib "com.lihaoyi::fansi:0.5.0"
import fansi.{Str, Color, Back, Bold, Underlined, Reversed}
// A fansi.Str carries structured colour spans, NOT raw bytes.
// .render materialises to the actual ANSI byte string.
val err: Str = Color.Red("error: ") ++ Str("permission denied")
println(err.render)
// Compose attributes via ++ (concatenate) or by stacking
// constructors:
val heading: Str =
Color.Blue(Bold.On(Underlined.On("Section 1")))
println(heading.render)
// .overlay layers an attribute on a sub-range — bold the
// first 5 chars only:
val highlighted: Str =
Str("FATAL server crashed").overlay(Bold.On, 0, 5)
println(highlighted.render)
// 256-palette via Color.Full / Back.Full (integer 0-255):
println(Color.Full(208)("256-palette orange").render)
// Truecolor — Color.True(r, g, b):
println(Color.True(255, 128, 0)("truecolor orange").render)
// Canonical Scala log scrubber — fansi.Str.ansiRegex is the
// right answer; rolling your own regex misses 256 / truecolor
// SGR forms and OSC sequences:
val dirty = "\u001B[1;31mERROR\u001B[0m at line 42"
val clean = fansi.Str.ansiRegex.replaceAllIn(dirty, "")
println(clean) // → "ERROR at line 42"// JVM-portable capability check:
// - System.console() returns a Console when stdin AND stdout
// are connected to a real terminal.
// - Returns null when stdin/stdout is redirected to a file
// or pipe (so 'scala foo.scala > out.log' disables colour).
//
// Pair with NO_COLOR env-var convention (https://no-color.org).
//
// SBT CAVEAT: SBT wraps the build's stdout to inject its own
// progress UI. Child-process ANSI output from `sbt run`
// sometimes loses colour. Fixes:
// 1) sbt -Dsbt.color=always run
// 2) In build.sbt:
// ThisBuild / outputStrategy := Some(StdoutOutput)
// 3) Inside the sbt shell:
// set ThisBuild / outputStrategy := Some(StdoutOutput)
// reload
// run
// scala-cli and mill do NOT share this quirk — they pass
// stdout through unchanged.
object AnsiCapable {
def isCapable: Boolean = {
if (sys.env.contains("NO_COLOR")) false
else if (sys.env.contains("FORCE_COLOR")) true
else System.console() != null
}
def styled(text: String, sgr: String): String =
if (isCapable) s"\u001B[${sgr}m${text}\u001B[0m" else text
}
@main def demo(): Unit = {
println(AnsiCapable.styled("OK", "32"))
println(AnsiCapable.styled("FAIL", "1;31"))
}// build.sbt:
// libraryDependencies ++= Seq(
// "com.lihaoyi" %% "pprint" % "0.9.0",
// "org.jline" % "jline" % "3.27.1",
// )
// --- pprint: coloured pretty-printer (the Ammonite default) ---
import pprint.{pprintln, PPrinter}
case class User(name: String, email: String, age: Int)
// Default printer uses fansi colour for keys, types, strings.
pprintln(User("alice", "[email protected]", 30))
pprintln(List(1, 2, 3, 4, 5).map(x => x * x))
// Switch to the no-colour printer when output isn't a TTY:
val pp: PPrinter =
if (System.console() != null) PPrinter.Color
else PPrinter.BlackWhite
pp.pprintln(User("bob", "[email protected]", 25))
// --- JLine 3: interactive line editor with history + colour ---
import org.jline.terminal.TerminalBuilder
import org.jline.reader.LineReaderBuilder
val terminal = TerminalBuilder.builder()
.system(true)
.build()
val reader = LineReaderBuilder.builder()
.terminal(terminal)
.build()
// readLine accepts a prompt that may contain ANSI escapes:
val line = reader.readLine("\u001B[32m> \u001B[0m")
println(s"got: $line")
// JLine handles raw-mode termios on Linux/macOS and ConPTY on
// Windows automatically, so the prompt's ANSI bytes render
// correctly on every platform Scala's own REPL supports.