Scala 3 / kyo-ui

kyo-ui
in the terminal.

tui4s runs a kyo-ui value in the terminal, in Scala 3. It lays out your screen in cells, follows its signals and paints it with jatatui.
counter — tui4s
examples/target/bin/counter
kyo-uitui4sterminalYour UI. In cells.
01 / Explore the toolkit

What tui4s gives

    kyo-ui is the DSL
    tui4s has no UI DSL of its own. A kyo-ui value describes the screen, and its signals drive the frames.
    Tests with no terminal
    Headless runs the app, sends keys and reads the screen in a plain unit test.
    Composables and widgets
    tui4s-use and tui4s-widgets are plain kyo-ui code for the JVM and Scala.js.
    The same app on the web
    The assistant example runs in the terminal and in a browser, from one shared code base.
    The terminal comes back
    Each way that a run ends restores the terminal: a quit, a failure, a signal or System.exit.
    Ready for AI agents
    Each page is also Markdown, with llms.txt and an MCP server. A test checks each Scala block.
02 / Read the source

The smallest app

This is the complete Counter example. The UI is plain kyo-ui, and TuiApp is its main class.
Counter.scala
import kyo.*
import kyo.UI.*
import tui4s.{Tui, TuiApp, initSig}

/** The Counter: a kyo-ui UI with one piece of state (a `SignalRef`), key
  * handlers and two buttons. `+` and `-` change the count, Tab moves between
  * the buttons, Enter and Space press them, `q` quits (and so does Ctrl-C).
  */
object Counter:

    /** The Counter UI; `quit` ends the run. */
    def ui(quit: Unit < Sync): UI < Sync =
      initSig(0).map(count => view(count, quit))

    /** The Counter view over a count that the caller owns. */
    def view(count: SignalRef[Int], quit: Unit < Sync): UI =
      div.onKeyDown {
        case KeyboardEvent(Keyboard.Char('+'), _, _) =>
          count.updateAndGet(_ + 1)
        case KeyboardEvent(Keyboard.Char('-'), _, _) =>
          count.updateAndGet(_ - 1)
        case KeyboardEvent(Keyboard.Char('q'), _, _) => quit
        case _                                       => ()
      }(
        h1("Counter"),
        span("Count: ", count.map(_.toString)).id("count"),
        div.style(_.row.gap(1.px))(
          button("+").onClick(count.updateAndGet(_ + 1)),
          button("-").onClick(count.updateAndGet(_ - 1)),
        ),
        p("+ / - change it, Tab moves, Enter presses, q quits").style(
          _.color(Style.Color.gray),
        ),
      )
end Counter

/** Runs the Counter in the real terminal. Start it with its start script, not
  * with `sbt run` (sbt also reads the keyboard):
  * {{{
  * sbt examples/launchers
  * examples/target/bin/counter
  * }}}
  */
object CounterApp extends TuiApp:
    def ui: UI < (Async & Scope) = Counter.ui(Tui.quit)

tui4sA terminal runner for kyo-ui.