tui4s
Guide

Running and packaging

The start scripts, the JVM flags, the AOT cache, a native binary, the options of a TuiApp, the exit codes and the signals.
sbt examples/launchers
examples/target/bin/counter
sbt examples/Universal/packageBin   # a ZIP with a launcher for each example and its JARs

A TuiApp reads its command-line arguments with args, and gives its options with options. The Printing example sends the captured output to a log file:

Printing.scala
    def options(args: Chunk[String]): TuiOptions = args.toSeq match
        case Seq("--log", file, _*) =>
          TuiOptions(logFile = Some(Paths.get(file)))
        case _                      => TuiOptions()

Its main class has override def options: TuiOptions = Printing.options(args).

The JVM flags

The start scripts of the examples (sbt-native-packager) give these flags:

FlagWhy
--enable-native-access=ALL-UNNAMEDThe terminal library (crossterm) is native. Without the flag, JDK 25 prints a warning into the terminal before the UI starts. With --illegal-native-access=deny, the run fails with TuiError.NativeAccessDenied.
--sun-misc-unsafe-memory-access=allowKyo and Scala use sun.misc.Unsafe.
-Xss16mKyo's deep stacks.
-XX:TieredStopAtLevel=1 and -XX:+UseSerialGCA faster start and less memory (ADR 0015).
-Dkyo.scheduler.cycleIntervalNs=10000000 and three more scheduler valuesFewer split frames under load, and less CPU when the app is idle.

A TuiApp sets the scheduler values in code before Kyo starts its scheduler. An app with a main of its own (a KyoApp that calls Tui.run) gives them as JVM flags. The ZIP does not hold a JDK: the machine needs JDK 25.

Start modes

A start script starts the JVM. Two modes start faster: the AOT cache of JDK 25, and a GraalVM native binary.

The AOT cache

sbt examples/launchers
examples/target/bin/aot-train            # all the apps; or: aot-train counter dashboard
examples/target/bin/counter              # starts with aot/counter.aot

In an unpacked ZIP, run bin/aot-train. It starts each app in a pseudo-terminal of its own, sends keys and quits. Then it writes aot/<app>.aot (about 40 MB, about 9 s for each app) and aot/<app>.jdk (the text of java -version). It needs python3. After each training, it starts the JVM once with the new cache, and it fails when the JVM cannot use the cache.

A start script uses the cache only when:

  • aot/<app>.aot and aot/<app>.jdk exist,
  • java -version gives the text of aot/<app>.jdk, and
  • the command has no -java-home.

Else the script starts without the cache, as before. The JVM also rejects a cache when the jars changed: the app then starts without the cache, with no message. Train again after a change of the JDK or of the jars. The ZIP holds bin/aot-train, but no cache: a cache is valid only for the JDK that made it.

A native binary

scripts/native-image.sh counter -Os      # -Os: a small binary; -Ob: a quick build
examples/target/native/counter
python3 scripts/native-smoke.py examples/target/native/counter

The script needs GraalVM for JDK 25. The Community Edition is sufficient. Put native-image on PATH, or set GRAALVM_HOME. The script stages the distribution, reads the main class and the class path of the start script, and writes examples/target/native/<app>. native-smoke.py starts the binary in a pseudo-terminal, presses + and q, and checks the screen, the exit code and the restore.

The jar of tui4s-runner holds the native-image metadata (META-INF/native-image): the crossterm library of each platform and the isatty downcall. This metadata is sufficient for the examples. An app that uses more of Kyo or of the JDK can need more entries: at run time, a Missing...RegistrationError names the entry. Put such entries in the metadata of the app (its own META-INF/native-image/<group>/<artifact>/reachability-metadata.json).

The numbers

The Counter on the machine container-4vcpu of the bench: 4 vCPU (Intel Xeon, 2.10 GHz), 15.7 GB, Linux, a shared build container; the median of 3 runs, 2026-10-20 (bench/PERFORMANCE.md).

Start modeFirst frameRSS
JVM, without the AOT cache and the two -XX flags1,227 ms129 MB
JVM, with the AOT cache and the two -XX flags444 ms115 MB
Native binary (GraalVM CE 25)25 ms40 MB

Output while the UI runs

A print to stdout or stderr would write over the screen. So the run captures System.out, System.err and Scala's println, and prints the lines after it restores the terminal. With logFile, the lines go to that file.

Exit codes and signals

How the run endsExit code
Tui.quit, or the key Ctrl-C0
A failure of the UI (printed after the restore)1
kill -TERM143
kill -INT130
kill -HUP129

On a signal or a System.exit, a shutdown hook interrupts the run and waits at most 2 s for the restore of the terminal. In raw mode, the key Ctrl-C is a key, not a signal: TuiOptions(ctrlCQuits = false) gives it to the app.

Copyright © 2026