The first app
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)
The UI is plain kyo-ui. It has one piece of state: a SignalRef from initSig, the short name of Signal.initRef. The onKeyDown handler of the root gets each key that the focused element does not stop. + and - change the count. Tab moves the focus between the buttons. Enter and Space press the focused button. q and Ctrl-C quit.
object CounterApp extends TuiApp is the main class. Its ui gets Tui.quit, the action that ends the run.
Run it
Start the app with its start script, in a real terminal:
sbt examples/launchers
examples/target/bin/counter
The script uses $JAVA_HOME/bin/java, or java on PATH when JAVA_HOME is not set. It must be JDK 25.
sbt run or sbt runMain. sbt also reads the keyboard, so keys are lost, and the forked JVM gets no terminal (the app stops with NotATerminal).The script gives the JVM the flag --enable-native-access=ALL-UNNAMED, because the terminal library is native. Running and packaging has all the flags and the exit codes.