JakeWharton/mosaic: terminal UI in Kotlin with the Jetpack Compose runtime
Build terminal UI in Kotlin using Jetpack Compose
At a glance
- What is it?
- Mosaic lets you write console interfaces as Composable functions and renders them with ANSI control sequences. It is experimental, it needs the JetBrains Compose compiler plugin, and it refuses to run inside Gradle or an IDE.
- Who is it for?
- Adopt Mosaic if you already write Kotlin and want Compose state and recomposition semantics in a console program, and you can accept an experimental API and a TTY-only runtime. Do not adopt it for a program that must run under Gradle, inside IntelliJ IDEA, in CI logs, or in any pipeline that strips ANSI control characters, because the README states those environments will not work.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 7 days ago.
- What is it written in?
- Mainly Kotlin, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What Mosaic solves, and who it is actually for
Console programs that update in place are usually written by hand: move the cursor, clear a line, print again. Mosaic replaces that with a declarative tree. You describe what the screen should contain, and the library diffs successive frames and redraws over the previous one. The README calls it "an experimental tool for building console UI in Kotlin using the Jetpack Compose compiler/runtime" and names Ink, the JavaScript library, as its inspiration.
The audience is narrow and specific. You need Kotlin, you need the JetBrains Compose compiler plugin on the module, and you need a program whose output is a live terminal rather than a log file. If your tool is a one-shot CLI that prints a result and exits, Mosaic is more machinery than the problem needs. The samples in the repository hint at the intended shape: a counter, a Jest-like test reporter, a robot game with keyboard control, an rrtop-style monitor, and Snake. Every one of those is a long-running, redrawing program.
runMosaic, state, and how a frame reaches the terminal
The entrypoint is the runMosaic function. The lambda you pass to it is responsible for both output and work, which is the design decision that separates Mosaic from a rendering library bolted onto a normal main function. Your coroutine does not return a string; it emits Composable content and runs its own effects alongside.
The README's counter example shows the data flow. A local property held with remember and mutableIntStateOf drives the displayed value. A LaunchedEffect starts a coroutine that increments that state on a delay. When the state changes, Compose recomposition runs the lambda again, Mosaic produces a new frame, and the terminal is redrawn over the old one. The README notes you may need to add imports for androidx.compose.runtime.getValue and androidx.compose.runtime.setValue manually, which is a small sign of how thin the tooling around this is compared with Android development.
The repository layout backs this up. Separate modules exist for mosaic-runtime, mosaic-terminal, mosaic-tty, mosaic-tty-terminal, mosaic-animation and mosaic-testing. The tty modules are the ones that talk to a real terminal; mosaic-testing exists so you can assert on rendered output without one. That split matters, because it tells you the terminal is a pluggable layer rather than something baked into the runtime.
Installing mosaic-runtime and writing your first frame
Mosaic is a library for Compose, and the README is explicit that it relies on JetBrains' Kotlin Compose plugin being present. Any module that calls runMosaic or defines Composable functions for Mosaic must have that plugin applied. The README points at the JetBrains Compose compiler documentation for that step rather than reproducing it, so that is where the plugin configuration comes from.
The library itself is an ordinary dependency. The README gives this exact coordinate for the 0.18.0 release:
dependencies {
implementation("com.jakewharton.mosaic:mosaic-runtime:0.18.0")
}If you want the development version instead, the README documents snapshots in the Central Portal Snapshots repository under the version 0.19.0-SNAPSHOT:
repository {
mavenCentral()
maven {
url 'https://central.sonatype.com/repository/maven-snapshots/'
}
}
dependencies {
implementation("com.jakewharton.mosaic:mosaic-runtime:0.19.0-SNAPSHOT")
}A first program is short. This is the README's starting example, a single static line:
suspend fun main() = runMosaic {
Text("The count is: 0")
}To make it move, hold state and update it from an effect. The README's counter does exactly this, counting to 20 at 250 millisecond intervals:
suspend fun main() = runMosaic {
var count by remember { mutableIntStateOf(0) }
Text("The count is: $count")
LaunchedEffect(Unit) {
for (i in 1..20) {
delay(250.milliseconds)
count = i
}
}
}Run that binary directly in a terminal emulator. You should see the number advance in place rather than twenty lines scrolling past. If you instead see twenty separate lines, you are almost certainly running it through Gradle or an IDE, which the next section covers. The repository's own samples are built with ./gradlew installDist, after which each binary lives under a path like ./samples/counter/build/install/counter/bin/counter.
The TTY requirement is the limitation that will bite you first
The README's FAQ answers the question directly: running within Gradle or IntelliJ IDEA will not work. Neither tool provides access to the TTY needed for interactivity, and for non-interactive programs both strip ANSI control characters, which prevents Mosaic from redrawing over a previous frame. The documented result is that output renders in successive lines instead.
That is a hard boundary, not a rough edge. It means the build task you use to launch the sample is also the thing that breaks it, and the IDE you use to debug is the thing that breaks it. You run your programs directly in a terminal emulator. Anything that captures stdout into a pipe, a log aggregator, or a CI transcript will lose the redraw behaviour, because the mechanism depends on those control characters surviving the trip.
The project also labels itself experimental in the first sentence of the README. Combined with a 0.x version line, that is a fair signal that the API can move between releases. The CHANGELOG.md file at the repository root is where you check what moved before upgrading.
One more honest caveat comes from the README itself: the demo GIFs have rendering problems caused by asciinema and agg that, per the note, do not appear in the real output. So the recorded demos understate what the library does on a live terminal.
How Mosaic differs from Ink and from hand-rolled ANSI code
Ink is the obvious reference point, and the README says Mosaic was inspired by it. The difference in approach is the runtime underneath. Ink is JavaScript and React: components are functions returning elements, and reconciliation is React's. Mosaic keeps the same declarative shape but swaps the engine for the Jetpack Compose compiler and runtime, which means your state primitives are remember, mutableIntStateOf and LaunchedEffect rather than hooks, and your build has to carry the Compose compiler plugin.
That trade is only worth making if you are already in Kotlin. If your console tool lives in a Node project, Ink is the shorter path and Mosaic buys you nothing. If you are writing Kotlin and would otherwise hand-roll cursor movement and line clearing with escape sequences, Mosaic gives you state tracking and a node tree instead, and the mosaic-testing module gives you a way to assert on rendered output that raw escape codes do not.
The FAQ also addresses the naming confusion head-on. Compose, it argues, is at its core a general-purpose compiler and runtime for state tracking and tree manipulation, usable on any Kotlin platform with any tree, while Compose UI is the Android-and-beyond UI toolkit. The README lists Cash App's Redwood, JetBrains' Compose HTML and Google's Jetpack Glance as other projects built on the core rather than on Compose UI. If your mental model of Compose is strictly Android UI, Mosaic will seem stranger than it is.
Maintenance, releases, and what the Apache-2.0 licence means for you
The repository is not archived, and the last push was on 2026-09-23. Releases are spaced rather than continuous: 0.16.0 in February 2025, 0.17.0 in April 2025, 0.18.0 in August 2025. Snapshot builds exist for 0.19.0-SNAPSHOT, which tells you work continues between releases, but a snapshot is not a substitute for a tagged version in anything you ship.
The upgrade cost is tied to the experimental label and the 0.x line. Because Mosaic consumes the Compose compiler, a Kotlin or Compose compiler upgrade can force a Mosaic upgrade, and a Mosaic upgrade can in turn change the API you call. Budget for reading CHANGELOG.md between versions rather than assuming a patch bump is inert.
Mosaic is licensed under Apache-2.0, and the README carries the standard header text: the software is distributed on an AS IS BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND. Apache-2.0 is a permissive licence that permits commercial use and modification and requires you to preserve the licence and attribution notices. That is the shape of it; whether it fits your organisation's policy is a question for your own legal review, not something this article can settle.
Editorial conclusion
Adopt Mosaic if you already write Kotlin and want Compose state and recomposition semantics in a console program, and you can accept an experimental API and a TTY-only runtime. Do not adopt it for a program that must run under Gradle, inside IntelliJ IDEA, in CI logs, or in any pipeline that strips ANSI control characters, because the README states those environments will not work. Before committing, verify the current released version on Maven Central, confirm the JetBrains Compose compiler plugin is applied to the module that calls runMosaic, and run one sample binary directly in a terminal emulator to see redraw behaviour for yourself.
Frequently asked questions
Does Mosaic still exist and is it maintained?
The repository is not archived and the last push was on 2026-09-23, with the most recent tagged release being 0.18.0. Development snapshots are published as 0.19.0-SNAPSHOT in the Central Portal Snapshots repository.
How do I install Mosaic in a Kotlin project?
Apply JetBrains' Kotlin Compose compiler plugin to any module that calls runMosaic or defines Composable functions, then add the dependency com.jakewharton.mosaic:mosaic-runtime:0.18.0. The README links to the JetBrains Compose compiler documentation for the plugin setup rather than showing it.
Why does Mosaic output render as successive lines instead of redrawing?
The README states that running within Gradle or IntelliJ IDEA will not work, because those tools do not give access to the TTY and strip ANSI control characters. Run the program directly in a terminal emulator, with no IDE and no Gradle.
I thought Compose was a UI toolkit for Android?
The README argues that Compose is at its core a general-purpose compiler and runtime for state tracking and tree node manipulation, usable on any Kotlin platform with any tree, while Compose UI is the separate UI toolkit. It cites Cash App's Redwood, JetBrains' Compose HTML and Google's Jetpack Glance as other projects built on the core.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/jakewharton-mosaic)