Kotter Documentation

repository·main·Indexed 20 days ago

https://github.com/varabyte/kotter

A 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.

Tokens
21.2K
Snippets
83
Records
105
Agent score
71%

What's inside Kotter

  1. What is Kotter?

    main

    Kotter is a KOTlin TERminal library designed to provide a declarative, Kotlin-idiomatic API for writing terminal applications. It is more opinionated than raw println calls 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.

  2. Explore Kotter example projects

    main

    The 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.
  3. Manage long-lived state with ConcurrentScopedData

    main

    All Kotter scopes expose access to a data field of type ConcurrentScopedData. 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.Lifecycle
    • Section.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.Lifecycle and setting a parent.

    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)
    }
  4. Integrate non-Kotter-aware external code using rerender()

    main
    If 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 the rerender() function. This allows you to maintain an external state and force Kotter to repaint the terminal UI whenever that external state changes.
  5. How cell auto-layout works in grids

    main

    When declaring a cell block within a grid without specifying a row or col, 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 with rowSpan or colSpan to 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)
    }
  6. Use `aside` blocks to output text from active sections

    main

    Kotter output consists of static history (finished sections) and a dynamic active area (the current section being rendered).

    An aside block 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 aside inside a run block or directly inside a section block.

    section {
      textLine("Searching...")
    }.run {
      aside {
        textLine("Match found: file.txt")
      }
    }
  7. Manage state and scopedState

    main

    To prevent side effects from leaking, Kotter localizes state changes (like colors) to the current section block.

    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()
  8. Test Kotter widgets by breaking them into pieces

    main

    If you prefer not to change your public API to include test hooks, you can test widgets by decomposing them into smaller, testable units:

    1. Render Logic: Move the section content into a MainRenderScope extension function.
    2. Interaction Logic: Move onKeyPressed handlers into a RunScope extension function.

    In your test, you can then call these individual components directly within a testSession to 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")
    }
  9. Understand the relationship between kotter and kotterx

    main

    Kotter is organized into two conceptual layers:

    1. com.varabyte.kotter: Contains the core primitives and fundamental building blocks of the library.
    2. 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.kotter package must never import anything from com.varabyte.kotterx.

  10. Use `offscreen` blocks to calculate content sizing

    main
    The offscreen block 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.
  11. Use the `aside` function to display static text alongside dynamic content

    main
    The aside function 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.