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 `--help` output (`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
// )