Skip to main content
ansicode
Kotlin

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

Direct println with \u001B — note: NOT \x1b
// 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")
}
mordant — Terminal + brightRed + Table
// 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"))
        }
    })
}
Capability gate — NO_COLOR + System.console()
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"))
}
Gradle daemon strips ANSI — pass --console=plain
// 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
//   )

Related sequences

Other languages