Reactive signals · 50+ widgets · keyboard & mouse
composable motion · headless tests · GraalVM native-image
📖 Read the guide · 📚 Browse the wiki · 🍳 Cookbook · 🔎 Scaladoc · 🎮 Examples
Note
Not on Maven Central yet. v0.10.0 is tagged but unreleased, so the coordinates below
will not resolve. Until the first release lands, use ./mill __.publishLocal and depend on
0.10.0 from your local Ivy cache — see Build from source.
// build.mill
def mvnDeps = Seq(mvn"io.worxbend::tui-dsl:0.10.0")// build.sbt
libraryDependencies += "io.worxbend" %% "tui-dsl" % "0.10.0"Then return an ordinary Scala Element tree:
import io.worxbend.tui.dsl.*
object Counter extends TuiApp:
private val count = Signal(0)
override def bindings: KeyBindings = KeyBindings(
binding("+", "increment")(count.update(_ + 1)),
binding("-", "decrement")(count.update(_ - 1)),
binding("q", "quit")(quit()),
)
def view(using ReactiveScope): Element =
scaffold(statusBar = Some(statusBar(bindings))) {
centered(34, 7) {
panel("Counter")(
text(s"Count: ${count.get}").bold.color(Color.Cyan),
spacer,
text("Change state; the view follows.").dim,
).rounded
}
}
def main(args: Array[String]): Unit =
run().left.foreach(error => println(s"failed to run: $error"))Three ideas carry through the entire toolkit:
| 1️⃣ Model | changing values with Signal; derive cached values with Computed |
| 2️⃣ Compose | the screen from elements, constraints, semantic styles, and retained widget state |
| 3️⃣ Ship | on the JVM, test through HeadlessBackend, or compile a native binary |
📘 The guided walkthrough explains every line: Getting started →
| Module | Owns |
|---|---|
🧱 tui-core |
cells, buffer, geometry, style, layout, events, Unicode display width |
🖥️ tui-terminal |
backend contract, JLine 3, ANSI diffing, input decoder, headless backend |
🧩 tui-widgets |
backend-independent content, controls, data, visualization, and feedback widgets |
⚡ tui-runtime |
signals, render thread, loop, async work, timers, easing, effects |
🎨 tui-dsl |
element tree, TuiApp, focus/mouse routing, themes, shell, screens, toasts, palette |
🪄 tui-macros |
reflection-free form and action derivation at compile time |
Use the complete tui-dsl stack for applications, or stop at a lower layer for a custom
backend, renderer, or widget library. No widget depends on a terminal and no terminal
backend knows about signals.
The modules above are the structure; this is what one frame actually does:
flowchart LR
Input["⌨️ keyboard + mouse"] --> Router["focus & event routing"]
Router --> Chrome
subgraph Chrome["application scaffold"]
direction TB
Top["top bar · tabs · command palette"]
Sidebar["sidebar · navigation"]
Content["widgets · charts · forms"]
Status["status line · shortcuts · toasts"]
Top --> Content
Sidebar --> Content
Content --> Status
end
Chrome --> Buffer["headless buffer"]
Buffer --> Diff["minimal terminal diff"]
Diff --> ANSI["ANSI output"]
Signals["Signal / Computed"] -. "invalidate" .-> Content
Effects["effects engine"] -. "animate" .-> Content
Only the cells that changed reach the terminal, and the whole path up to ANSI output runs
without one — which is what makes headless testing
exact rather than approximate.
| Family | Highlights |
|---|---|
| 🧱 Layout & chrome | panel, row/column, spacer, rule, scroll view, tabs, collapsible, split pane, layers, scaffold, sidebar |
| 📄 Content | text, list, table, DataTable, tree, directory tree, log, Markdown, OSC 8 links, half-block image |
| ⌨️ Input | text input/area, checkbox, toggle, select, radio group, slider, masked/number input, autocomplete, file picker, button, derived form |
| 📊 Data viz | gauge, sparkline, bar/stacked/pie chart, line/scatter chart, heatmap, canvas shapes, calendar |
| ✨ Feedback | spinner, skeleton, indeterminate bar, marquee, wave text, dialog, tooltip, toasts, splash, effects |
Every interactive state object is caller-owned. Every widget renders into a Buffer. Every
width calculation goes through grapheme-aware CharWidth.
🧩 Browse the complete catalog →
val backend = HeadlessBackend(Size(50, 10))
val app = TodoApp()
val pilot = Pilot.start(backend) {
val _ = app.runWith(backend)
}
pilot
.waitForIdle()
.typeText("ship docs")
.pressKey(KeyCode.Enter)
.waitForIdle()
assert(pilot.screenText.contains("· ship docs"))Pilot posts the same event ADT used in production and exposes the last rendered screen as
text. Buffer helpers skip wide-character continuation cells, so assertions match what users see.
Tip
Pilot and BufferAssertions live in the repository's internal test-support module; the
public HeadlessBackend can be driven directly by downstream projects.
./mill examples.showcase.nativeImageCI compiles hello-world, counter, todo-list, dashboard, form-demo, and showcase
with GraalVM --no-fallback, then launches each without a TTY to verify a safe exit.
Reflection and dynamic class loading are rejected in main Scala sources.
git clone https://github.com/oleksandr-balyshyn/glyphora.git
cd glyphora
./mill __.compile # build everything
./mill __.test # run every suite
./mill __.publishLocal # install 0.10.0 into your local Ivy cacheDay-to-day development:
./mill widgets.test # one module's suite
./mill core.test.testOnly io.worxbend.tui.core.RectSpec # one suite
./mill core.test.testOnly io.worxbend.tui.core.RectSpec -- -z inset # one test
./mill mill.scalalib.scalafmt.ScalafmtModule/reformatAll __.sources
./mill mill.scalalib.scalafmt.ScalafmtModule/checkFormatAll __.sources # CI gate
./mill examples.showcase.run # manual product tour against a real terminalDocs and the shared Wiki export:
(cd website && npm ci && npm run build)
node scripts/export-wiki.mjs --output build/wiki🧰 Read Contributing for the widget checklist, quality
gates, docs workflow, and pull-request expectations. Shared visual and editorial rules live in
docs/STYLE_GUIDE.md.
The same Markdown publishes to the 📖 GitHub Pages site
and the 📚 GitHub Wiki — website/docs/
is canonical.
| Guides | |
|---|---|
| 🟢 Start | Introduction · Getting started |
| 🧠 Understand | State & signals · Layout & style · Architecture |
| 🏗️ Build | App shell · Widgets · Forms |
| ⚙️ Integrate | Async & timers · Mouse & focus · Motion |
| ✅ Ship | Testing · Native binaries · Troubleshooting |
Contributions are welcome across runtime behavior, widgets, examples, tests, documentation, and
design. CI enforces the constraints that protect the design: no runtime reflection, no
String.substring for layout math outside CharWidth, warnings-as-errors, Scalafmt, and six
native-image example builds.
MIT — go build something glyphorious. ✦