ANSI escape codes in Kotlin — \u001B, mordant, kotter, picocli
Kotlin string literals use \u001B for the ESC byte — Kotlin has no \x hex escape, only \u#### Unicode escapes (same as Java). println("\u001B[31mred\u001B[0m") is byte-clean on macOS / Linux and on Windows 10+ (where stdout enables VT by default). For ergonomic colour + tables + progress + markdown rendering reach for mordant, the most popular Kotlin ANSI library — it auto-detects terminal capability, respects NO_COLOR, and downgrades cleanly when piped. For full interactive TUIs kotter provides a coroutine-friendly DSL. picocli (a JVM CLI argument parser) ships built-in ANSI colour support for --help and is the natural choice when you also need CLI parsing.
Gradle / IDE caveat: IntelliJ's Run console enables ANSI, but the Gradle daemon strips colour by default — pass --console=plain (or set org.gradle.console=plain in gradle.properties) to see escape bytes verbatim. Android Logcat does not interpret ANSI; use mordant's Theme.PlainAscii or capability-detect when targeting Android.
Recommended libraries
- mordant
Kotlin's de facto ANSI / TUI library — coloured text, tables, progress bars, markdown rendering, prompts. Auto-detects ANSI / 256 / TrueColor capability, respects
NO_COLOR, falls back to plain text when piped. Works on Kotlin/JVM, Kotlin/Native, and Kotlin/JS. - kotter
Modern Kotlin coroutine-friendly DSL for interactive TUIs —
section { textLine(red("error")) }style. Built for live-updating output (progress, animations, polling status) without the full ncurses-style window/panel model. - picocli
Mature JVM annotation-based CLI argument parser with built-in ANSI colour for
--helpoutput (Picocli.Help.Ansi). Kotlin-friendly. Use when you already need argument parsing — gets you coloured help for free. - JLine 3
Lower-level terminal abstraction shared by Kotlin REPL, Groovy, and Scala REPL — line editing, history, completion, raw-mode key reading. Reach for it when you need capability detection (
Terminal.getType(), mouse, focus events) or are building a REPL.
Idiomatic patterns
// Kotlin has no \x hex escape — only \u#### Unicode (like Java).
// \x1b is a compile error inside string literals; use \u001B.
fun main() {
println("\u001B[1;31merror:\u001B[0m permission denied")
println("\u001B[33mwarn:\u001B[0m deprecated flag")
println("\u001B[32mok:\u001B[0m 142 tests passed")
// Define once, reuse — top-level constant:
val ESC = "\u001B"
println("${ESC}[38;2;255;128;0morange truecolor${ESC}[0m")
}// build.gradle.kts:
// implementation("com.github.ajalt.mordant:mordant:2.7.0")
import com.github.ajalt.mordant.rendering.TextColors.*
import com.github.ajalt.mordant.terminal.Terminal
import com.github.ajalt.mordant.table.table
fun main() {
val t = Terminal() // auto-detects capability + honours NO_COLOR
t.println("${(brightRed + bold)("error:")} permission denied")
t.println("${yellow("warn:")} deprecated flag")
t.println("${green("ok:")} 142 tests passed")
// Truecolor via hex:
t.println(rgb("#ff8000")("orange truecolor"))
// Tables are first-class:
t.println(table {
header { row("File", "Status") }
body {
row("app.log", green("ok"))
row("db.log", red("missing"))
}
})
}fun ansiCapable(): Boolean {
// The Unix-standard kill switch — honour first.
if (System.getenv("NO_COLOR") != null) return false
// System.console() is null when stdout is redirected or piped —
// the JVM's portable "is this a TTY?" check. Works on JVM and
// Kotlin/JVM-Native; for Kotlin/Native use platform.posix.isatty.
if (System.console() == null) return false
return true
}
fun style(text: String, sgr: String) =
if (ansiCapable()) "\u001B[${sgr}m${text}\u001B[0m" else text
fun main() {
println(style("OK", "32"))
println(style("FAIL", "1;31"))
}// The #1 "my ANSI doesn't show up" surprise on Kotlin/JVM:
// Gradle's daemon wraps stdout and strips escape bytes by default.
//
// # Force plain (verbatim) output for one invocation:
// ./gradlew run --console=plain
//
// Or set it persistently in gradle.properties at the project root:
//
// # gradle.properties
// org.gradle.console=plain
//
// IntelliJ's Run console behaves differently — it parses ANSI inline,
// so running from the IDE will show colours even when Gradle CLI hides
// them. Android Logcat does NOT parse ANSI at all; for Android targets
// use mordant's Theme.PlainAscii or capability-gate the output:
//
// val t = Terminal(
// ansiLevel = if (isAndroidLogcat) AnsiLevel.NONE else null
// )