How can CLI (or terminal) apps like gradle, curl, docker,… show live progress bars or colorful the text updates in terminal, when all that a CLI app does is writing to a single output stream? Well, there’s a bit more to it (as I learned). Already in the early days of video terminals, vendors provided special byte sequences that allowed to perform non-text-output operations (like placing the cursor at a specific position on the screen). Eventually, those sequences got standardized and are now know as ANSI escape sequences. Modern terminals (like the built-in terminal in MacOS) still interpret those sequences and allow quite some cool stuff.
If you want to dig deeper, I can highly recommend: Build your own Command Line with ANSI escape codes on Haoyi’s Programming Blog.
I collected a few helpful escape codes in a Kotlin object:
object CliFormat {
const val RESET: String = "\u001b[0m"
const val BLACK: String = "\u001b[30m"
const val RED: String = "\u001b[31m"
const val GREEN: String = "\u001b[32m"
const val YELLOW: String = "\u001b[33m"
const val BLUE: String = "\u001b[34m"
const val MAGENTA: String = "\u001b[35m"
const val CYAN: String = "\u001b[36m"
const val WHITE: String = "\u001b[37m"
const val BRIGHT_BLACK: String = "\u001b[90m"
const val BRIGHT_RED: String = "\u001b[91m"
const val BRIGHT_GREEN: String = "\u001b[92m"
const val BRIGHT_YELLOW: String = "\u001b[93m"
const val BRIGHT_BLUE: String = "\u001b[94m"
const val BRIGHT_MAGENTA: String = "\u001b[95m"
const val BRIGHT_CYAN: String = "\u001b[96m"
const val BRIGHT_WHITE: String = "\u001b[97m"
const val BACKGROUND_BLACK: String = "\u001b[40m"
const val BACKGROUND_RED: String = "\u001b[41m"
const val BACKGROUND_GREEN: String = "\u001b[42m"
const val BACKGROUND_YELLOW: String = "\u001b[43m"
const val BACKGROUND_BLUE: String = "\u001b[44m"
const val BACKGROUND_MAGENTA: String = "\u001b[45m"
const val BACKGROUND_CYAN: String = "\u001b[46m"
const val BACKGROUND_WHITE: String = "\u001b[47m"
const val BACKGROUND_BRIGHT_BLACK: String = "\u001b[100m"
const val BACKGROUND_BRIGHT_RED: String = "\u001b[101m"
const val BACKGROUND_BRIGHT_GREEN: String = "\u001b[102m"
const val BACKGROUND_BRIGHT_YELLOW: String = "\u001b[103m"
const val BACKGROUND_BRIGHT_BLUE: String = "\u001b[104m"
const val BACKGROUND_BRIGHT_MAGENTA: String = "\u001b[105m"
const val BACKGROUND_BRIGHT_CYAN: String = "\u001b[106m"
const val BACKGROUND_BRIGHT_WHITE: String = "\u001b[107m"
const val BOLD: String = "\u001b[1m"
const val UNDERLINE: String = "\u001b[4m"
const val REVERSED: String = "\u001b[7m"
fun cursorUp(n: Int): String = "\u001b[${n}A"
fun cursorDown(n: Int): String = "\u001b[${n}B"
fun cursorRight(n: Int): String = "\u001b[${n}C"
fun cursorLeft(n: Int): String = "\u001b[${n}D"
const val CLEAR_UNTIL_END_OF_SCREEN: String = "\u001b[0J"
const val CLEAR_TO_BEGINNING_OF_SCREEN: String = "\u001b[1J"
const val CLEAR_ENTIRE_SCREEN: String = "\u001b[2J"
const val CLEAR_UNTIL_END_OF_LINE: String = "\u001b[0K"
const val CLEAR_UNTIL_START_OF_LINE: String = "\u001b[1K"
const val CLEAR_ENTIRE_LINE: String = "\u001b[2K"
}
As a basic example, you can use it like this:
fun main() {
println("${CliFormat.RED}Hello, ${CliFormat.GREEN}colorful ${CliFormat.BLUE}world${CliFormat.RESET}!")
println()
println("You can also ${CliFormat.BACKGROUND_BLUE}${CliFormat.YELLOW}${CliFormat.UNDERLINE}combine them${CliFormat.RESET}!")
}
Here’s an overview over the colors and formatting provided by CliFormat
(the exact colors depend on the terminal and the configuration of that terminal):
The cursor functions allow you to literally move the blinking cursor on the screen, and therefore also changes the position where the printed text is written to:
fun main() {
println("Hello!")
println("The cursor will blink here >> <<")
print(CliFormat.cursorUp(1) + CliFormat.cursorRight(29))
readln()
}
This is useful, to update text, and implement something like a progress bar:
fun main() {
println("Please wait…")
for (progress in 0..100) {
print(CliFormat.cursorLeft(15)) // move to start of line (it doesn't matter if there are less than 15 chars)
val blocks = progress / 10
val remaining = 10 - blocks
print("█".repeat(blocks) + "░".repeat(remaining) + " $progress%")
Thread.sleep(20)
}
readln()
}
The CLEAR…
escape sequences can be used to clear parts of the screen (as the name suggest). Note that the cursor doesn’t move for those operations!
You can also use CLEAR_UNTIL_END_OF_SCREEN
to change the background color of an entire line:
fun main() {
println("blabla…")
print("${CliFormat.BACKGROUND_BLUE}My Heading${CliFormat.CLEAR_UNTIL_END_OF_LINE}\n${CliFormat.RESET}")
println("blabla…")
}
I hope this was helpful! Now go and make some beautiful CLI apps!