Kotter
A declarative, Kotlin-idiomatic API for writing dynamic console applications.
Install / Use
/learn @varabyte/KotterREADME
<a href="https://varabyte.github.io/kotter">
</a>
<br>
<br>
<a href="https://discord.gg/5NZ2GKV5Cs">
<img alt="Varabyte Discord" src="https://img.shields.io/discord/886036660767305799.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2" />
</a>
Kotter 🦦
session {
var wantsToLearn by liveVarOf(false)
section {
text("Would you like to learn "); cyan { text("Kotter") }; textLine("? (Y/n)")
text("> "); input(Completions("yes", "no"))
if (wantsToLearn) {
yellow(isBright = true) { p { textLine("""\(^o^)/""") } }
}
}.runUntilInputEntered {
onInputEntered { wantsToLearn = "yes".startsWith(input.lowercase()) }
}
}

See also: the game of life, snake, sliding tiles, doom fire, and Wordle implemented in Kotter!
Kotter (a KOTlin TERminal library) aims to be a relatively thin, declarative, Kotlin-idiomatic API that provides
useful functionality for writing delightful console applications. It strives to keep things simple, providing a solution
a bit more opinionated than making raw println calls but way less featured than something like Java Curses.
Specifically, this library helps with:
- Setting colors and text decorations (e.g. underline, bold)
- Handling user input
- Creating timers and animations
- Seamlessly repainting terminal text when values change
Kotter is multiplatform, supporting JVM and native targets.
The next sections deal with setting Kotter up, but you may wish to jump straight to the usage section ▼ to immediately start learning about this library.
🐘 Gradle
🎯 Dependency
Kotter supports JVM and native targets.
[!TIP] If you're not sure what you want, start with a JVM project. That target is far easier to distribute. It also means your project will have access to a very broad ecosystem of Kotlin and Java libraries.
In case it affects your decision, you can read more about distributing Kotter applications ▼ later in this document.
JVM
// build.gradle.kts (kotlin script)
plugins {
kotlin("jvm")
application
}
repositories {
mavenCentral()
}
dependencies {
implementation("com.varabyte.kotter:kotter-jvm:1.2.1")
testImplementation("com.varabyte.kotterx:kotter-test-support-jvm:1.2.1")
}
application {
applicationDefaultJvmArgs = listOf(
// JDK24 started reporting warnings for libraries that use restricted native methods, at least one which Kotter
// uses indirectly (via jline/jansi). It looks like this:
//
// WARNING: A restricted method in java.lang.System has been called
// WARNING: java.lang.System::loadLibrary has been called by ...
// WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
// WARNING: Restricted methods will be blocked in a future release unless native access is enabled
//
// The best solution we have for now is to disable the warning by explicitly enabling access.
// We also suggest the IgnoreUnrecognizedVMOptions flag here to allow kotter applications to be able to compile
// with JDKs older than JDK24. You can remove it if you are intentionally using JDK24+.
// See also: https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/lang/doc-files/RestrictedMethods.html
// And also: https://github.com/jline/jline3/issues/1067
"-XX:+IgnoreUnrecognizedVMOptions",
"--enable-native-access=ALL-UNNAMED",
)
// The following assumes a top-level `main.kt` file in your project; adjust as needed otherwise
mainClass.set("MainKt")
}
Multiplatform
Multiplatform can be useful if you want to distribute binaries to users without requiring they have Java installed on their machine.
// build.gradle.kts (kotlin script)
plugins {
kotlin("multiplatform")
}
repositories {
mavenCentral()
}
kotlin {
// Choose the targets you care about.
// Note: You will need the right machine to build each one; otherwise, the target is disabled automatically
listOf(
linuxX64(), // Linux
mingwX64(), // Windows
macosArm64(), // Mac M1
macosX64(), // Mac Legacy
).forEach { nativeTarget ->
nativeTarget.apply {
binaries {
executable {
entryPoint = "main"
}
}
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("com.varabyte.kotter:kotter:1.2.1")
}
}
val commonTest by getting {
dependencies {
implementation("com.varabyte.kotterx:kotter-test-support:1.2.1")
}
}
}
}
[!NOTE] Building native binaries is a little tricky, as you may need different host machines in order to build the various binaries. For example, here is Kotter's CI workflow which runs on both Linux and Mac targets to build platform-specific Kotter artifacts.
Testing snapshots
Most users won't ever need to run a Kotter snapshot, so feel free to skip over this section! However, occasionally, bug fixes and new features will be available for testing for a short period before they are released.
If you ever file a bug with Kotter and are asked to test a fix using a snapshot, you must add an entry for the sonatype
snapshots repository to your repositories block in order to allow Gradle to find it:
// build.gradle.kts
repositories {
mavenCentral()
+ maven("https://central.sonatype.com/repository/maven-snapshots/") {
+ mavenContent {
+ includeGroup("com.varabyte.kotter")
+ snapshotsOnly()
+ }
+ }
}
🚥 Running examples
If you've cloned this repository, examples are located under the examples folder.
JVM
Most of the examples (except examples/native) target the JVM. To try one of them, you can navigate into it on the
command line and run it via Gradle.
$ cd examples/life
$ ../../gradlew run
However, because Gradle itself has taken over the terminal to do its own fancy command line magic, the example will actually open up and run inside a virtual terminal.
If you want to run the program directly inside your system terminal, which is hopefully the way most users will see your
application, you should use the installDist task to accomplish this:
$ cd examples/life
$ ../../gradlew installDist
$ cd build/install/life/bin
$ ./life
[!WARNING] If your terminal does not support features needed by Kotter, which could happen on legacy machines for example, then this still may end up running inside a virtual terminal.
Multiplatform
Unlike the JVM target, native targets do not have a virtual terminal fallback. So be sure you do not use any of the
Gradle run tasks (e.g. runDebugExecutabule...). This will also fail if you try to run your program through the IDE via
the green "play" arrow.
Instead, you should link your executable and then run it directly.
For example, on Linux:
$ cd examples/native
$ ../../gradlew linkDebugExecutableLinuxX64
$ ./build/bin/linuxX64/debugExecutable/native.kexe
📖 Usage
👶 Basics
The following is equivalent to println("Hello, World"). In this simple case, it's definitely overkill!
session {
section { textLine("Hello, World") }.run()
}
section { ... } defines a Section which, on its own, is inert. It needs to be run to output text to the
console. Above, we use the run method to trigger this. The method blocks until the render (i.e. text printing to the
console) is finished (which, in the above case, will be almost instant).
session { ... } sets the outer scope for your whole program. While we're just calling it with default arguments here,
you can also pass in parameters that apply to the entire application.
While the above simple case is a bit verbose for what it's doing, Kotter starts to show its strength when doing background work (or other async tasks like waiting for user input) during which time the section block may render several times. We'll see many examples throughout this document later.
A Kotter session can contain one or more sections. Your own app may only ever contain a single section and that's
fine! But if you have multiple sections, it will feel to the user like your app has a current, active area, following
a history of text paragraphs from previous interactions that no longer change.
🎨 Text Effects
You can call color methods directly, which remain in effect until the next color method is called:
section {
green(layer = BG)
red() // defaults to FG layer if no layer specified
textLine("Red on green")
blue()
textLine("Blue on green")
}.run()
