在 Kotlin 中使用 ANSI 转义码 —— \u001B、mordant、kotter、picocli
Kotlin 字符串字面量用 `\u001B` 表示 ESC 字节 —— Kotlin 没有 `\x` 十六进制转义,只有 `\u####` Unicode 转义(与 Java 一致)。`println("\u001B[31mred\u001B[0m")` 在 macOS / Linux 以及 Windows 10+(stdout 默认启用 VT)上按字节透传。需要顺手的颜色 + 表格 + 进度条 + Markdown 渲染时选 `mordant`,Kotlin 最受欢迎的 ANSI 库 —— 自动检测终端能力、尊重 `NO_COLOR`、被管道时优雅降级。需要完整交互 TUI 时 `kotter` 提供协程友好的 DSL。`picocli`(JVM CLI 参数解析器)为 `--help` 内建 ANSI 颜色,同时需要 CLI 解析时的自然选择。 **Gradle / IDE 注意**:IntelliJ Run 控制台启用 ANSI,但 Gradle daemon 默认剥离颜色 —— 传 `--console=plain`(或在 `gradle.properties` 设置 `org.gradle.console=plain`)以原样查看转义字节。Android Logcat 不解析 ANSI;目标为 Android 时使用 `mordant` 的 `Theme.PlainAscii` 或先做能力检测。
推荐库
- mordant
Kotlin 事实标准的 ANSI / TUI 库 —— 彩色文本、表格、进度条、Markdown 渲染、提示。自动检测 ANSI / 256 / TrueColor 能力,尊重 `NO_COLOR`,被管道时降级为纯文本。可在 Kotlin/JVM、Kotlin/Native、Kotlin/JS 上工作。
- kotter
现代 Kotlin 协程友好 DSL,用于构建交互 TUI —— `section { textLine(red("error")) }` 风格。专为动态更新输出(进度、动画、轮询状态)设计,无需 ncurses 风格的完整窗口 / 面板模型。
- picocli
成熟的基于注解的 JVM CLI 参数解析器,`--help` 输出内建 ANSI 颜色(`Picocli.Help.Ansi`)。Kotlin 友好。当你本来就需要参数解析时使用 —— 顺带免费拿到彩色帮助。
- JLine 3
Kotlin REPL、Groovy、Scala REPL 共享的较低层终端抽象 —— 行编辑、历史、补全、原始模式按键读取。需要能力检测(`Terminal.getType()`、鼠标、焦点事件)或构建 REPL 时使用。
常用写法
// 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
// )