Kotter Documentation
repository·main·Indexed 20 days ago
https://github.com/varabyte/kotterA Kotlin terminal library providing a declarative, idiomatic API for building console applications. Kotter features text decoration, input handling, animations, and a reactive state model. The library is split into core primitives (com.varabyte.kotter) and higher-level abstractions (com.varabyte.kotterx), offering tools like offscreen buffers for sizing, the aside function for static text, and testSession for isolated in-memory terminal testing.
What's inside Kotter
- Mosaic is a command line library that implements the Compose pattern for the terminal. This project uses Mosaic examples to demonstrate how they compare and contrast with Kotter's implementation of terminal-based UI components.
What is Kotter?
mainKotter is a KOTlin TERminal library designed to provide a declarative, Kotlin-idiomatic API for writing terminal applications. It is more opinionated than raw
printlncalls but less complex than libraries like Java Curses.Key features include:
- 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 both JVM and native targets.
Explore Kotter example projects
mainThe
examples/directory contains several starter projects designed to demonstrate core Kotter capabilities:text: Demonstrates basic text rendering.keys: Demonstrates basic keypress handling.input: Demonstrates basic input handling.anim: Demonstrates basic animation implementation.extend: Demonstrates how to use Kotlin extension functions with Kotter.
Manage long-lived state with ConcurrentScopedData
mainAll Kotter scopes expose access to a
datafield of typeConcurrentScopedData. This is a thread-safe map used to manage state that persists across multiple render passes.Lifecycles
Data in the map is tied to a
Lifecycle. When a lifecycle ends, the associated data is automatically removed. Kotter provides:Session.LifecycleSection.Lifecycle(Recommended for most state, as it survives rerenders)MainRenderScope.Lifecycle(Dies after a single render pass)RunScope.Lifecycle
You can define custom lifecycles by implementing
ConcurrentScopedData.Lifecycleand setting aparent.Common Operations
- Overwrite:
data[key] = value - Add if absent:
data.tryPut(key, value) - Atomic initialization:
data.putIfAbsent(key, provideInitialValue = { value }) { /* follow-up logic */ }
object MyLifecycle : ConcurrentScopedData.Lifecycle { override val parent = RunScope.Lifecycle } private val MySetting = MyLifecycle.createKey<Boolean>() // Usage try { data.start(MyLifecycle) data[MySetting] = true } finally { data.stop(MyLifecycle) }Integrate non-Kotter-aware external code using rerender()
mainIf you are integrating external logic or libraries that do not natively support Kotter's reactive state model (such as a game engine or a custom simulation like Conway's Game of Life), you can manually trigger screen updates using thererender()function. This allows you to maintain an external state and force Kotter to repaint the terminal UI whenever that external state changes.How cell auto-layout works in grids
mainWhen declaring a
cellblock within agridwithout specifying aroworcol, Kotter automatically places the cell in the next available empty slot, following a left-to-right, top-to-bottom flow. This behavior is particularly useful when combined withrowSpanorcolSpanto create complex layouts.Important Constraints:
- A cell spanning multiple columns inherits its justification from its leftmost cell.
- Cells spanning multiple columns are excluded from fit-size calculations.
grid(Cols(1, 1, 1)) { cell(rowSpan = 2) { text("1") } // occupies (0,0) and (1,0) cell { text("2") } // occupies (0,1) cell { text("3") } // occupies (0,2) cell { text("4") } // occupies (1,1) }Use `aside` blocks to output text from active sections
mainKotter output consists of static history (finished sections) and a dynamic active area (the current section being rendered).
An
asideblock allows you to output text directly into the static history while a section is still active. This is ideal for long-running processes that generate side effects, such as a compiler printing warnings or a file walker printing matches, without interrupting the main UI (like a spinner) in the active section.You can call
asideinside arunblock or directly inside asectionblock.section { textLine("Searching...") }.run { aside { textLine("Match found: file.txt") } }Manage state and scopedState
mainTo prevent side effects from leaking, Kotter localizes state changes (like colors) to the current
sectionblock.To create a temporary scope where any state changes are automatically discarded when the block ends, use
scopedState { ... }.section { scopedState { red() blue(BG) underline() textLine("Underlined red on blue") } text("Text without color or decorations") }.run()Test Kotter widgets by breaking them into pieces
mainIf you prefer not to change your public API to include test hooks, you can test widgets by decomposing them into smaller, testable units:
- Render Logic: Move the
sectioncontent into aMainRenderScopeextension function. - Interaction Logic: Move
onKeyPressedhandlers into aRunScopeextension function.
In your test, you can then call these individual components directly within a
testSessionto verify their behavior in isolation.@Test fun `user can navigate to an answer using arrow keys`() = testSession { terminal -> var selectedIndex by liveVarOf(0) val colorChoices = listOf("Red", "Orange", "Yellow", "Green", "Blue", "Purple") section { renderChoices("Choose a color", colorChoices, selectedIndex) }.runUntilSignal { handleChoiceSelection( getSelectedIndex = { selectedIndex }, maxIndex = colorChoices.size - 1, setSelectedIndex = { selectedIndex = it } ) terminal.press(Keys.Down) terminal.press(Keys.Down) terminal.press(Keys.Enter) } assertThat(colorChoices[selectedIndex]).isEqualTo("Yellow") }- Render Logic: Move the
Understand the relationship between kotter and kotterx
mainKotter is organized into two conceptual layers:
com.varabyte.kotter: Contains the core primitives and fundamental building blocks of the library.com.varabyte.kotterx: Contains useful add-ons and higher-level abstractions built on top of the core primitives. While these add-ons are widely beneficial and often used, they are conceptually one layer above the foundation.
Important Architectural Rule: Code residing in the core
com.varabyte.kotterpackage must never import anything fromcom.varabyte.kotterx.Use `offscreen` blocks to calculate content sizing
mainTheoffscreenblock is a specialized rendering context used when you need to determine the sizing information (such as width or height) of a piece of content before it is displayed on the screen. This is particularly useful for layout calculations where the content's dimensions are not known upfront.Use the `aside` function to display static text alongside dynamic content
mainTheasidefunction allows you to generate extra, static text that appears before the active, dynamically updating text in your terminal UI. This is useful for creating a 'trail' or log of historical messages (like a compilation log) while keeping the primary, real-time state updates (like thread status) visible in the main area.