Build swaggy terminal UIs in Scala 3 — a signals-driven widget toolkit with app chrome, animations, mouse support, and first-class GraalVM native-image binaries.
glyphora Widgets │ Log │ About
┌Menu────────────────┐Widgets │ Log │ About
│ dashboard │
│ deployments │ note: type here…
│ services │
│ settings │ 10%
│ │██▇▅▄▂▁ ▁▃▄▆▇██▇▆▅▃▂▁ ▁▂▃▅▆▇██▇▆▄▃▁
│ │
│ │── chrome ──────────────────────────────────────
└────────────────────┘Tab cycles focus · ctrl+p opens the palette
ctrl+t switch theme │ ctrl+n show a toast │ ctrl+o open modal │
(a real frame from examples/showcase, captured headlessly — see Testing)
📖 oleksandr-balyshyn.github.io/glyphora — the full guide, cookbook, and per-module API reference.
- 🧠 Signals, not spaghetti — state lives in
Signal/Computed; whatever your view reads, re-renders when it changes. No dispatch loops, no dependency arrays. - 🧱 40+ widgets — from
BlockandGaugetoDataTable,TextArea(undo, cluster-safe editing),DirectoryTree,Markdown, brailleCharts, and a half-blockImage. - 🏛️ App chrome built in —
scaffoldwith top bar / sidebar / status line, themes, key-binding registry, screens, toasts, and a fuzzyCtrl+Pcommand palette. - 🎬 Motion — a post-render effects engine (
fadeIn,coalesce,typewriter, …) with easing and combinators, plus skippable splash screens. - 🖱️ Mouse-aware — click to focus/activate, wheel to scroll, drag sliders and split panes.
- 🌍 Unicode-correct — display width from the Unicode Character Database: CJK, emoji ZWJ families, flags, combining marks all measure right.
- 📦 Native binaries — every example compiles with
native-image --no-fallbackand zero reflect-config, starting in milliseconds. - 🧪 Testable by design — a headless backend +
Pilotdriver run full event/render cycles in plain unit tests.
// build.mill
def mvnDeps = Seq(mvn"io.worxbend::tui-dsl:0.9.0")import io.worxbend.tui.dsl.*
object Hello extends TuiApp:
val count = Signal(0)
override def bindings = KeyBindings(
binding("+", "increment")(count.update(_ + 1)),
binding("q", "quit")(quit()),
)
def view(using ReactiveScope): Element =
scaffold(statusBar = Some(statusBar(bindings))) {
centered(30, 5) {
panel("Hello")(
text(s"count: ${count.get}").bold.color(Color.Cyan),
text("press + to bump it").dim,
).rounded
}
}
def main(args: Array[String]): Unit = run().foreach(_ => ())One import gives you every factory, the styling/layout extensions, and the core vocabulary.
More recipes in the 📖 cookbook; complete apps in examples/;
the full guide and API reference are on the docs site.
| 🧱 Layout & chrome | Block (per-side borders, padding), Row/Column, Spacer, Rule, Scrollbar, ScrollView, TabbedContent, Collapsible, SplitPane, layers |
| 📄 Content | Paragraph (cluster-safe wrap), ListView, Table, Tabs, BigText, Log (follow-tail), Markdown, Link (OSC 8), Image (half-block) |
| ⌨️ Input | TextInput, TextArea (undo), Checkbox, Toggle, Select, RadioGroup, Slider, NumberInput, MaskedInput, SelectionList, Autocomplete, FilePicker, Button, Form (compile-time derived) |
| 📊 Data viz | Gauge, LineGauge, Sparkline, DualSparkline, BarChart, StackedBarChart, Chart (braille/half-block), PieChart, Heatmap, Canvas + shapes, Calendar, DataTable (sort/filter) |
| ⏳ Motion & feedback | Spinner, Skeleton, IndeterminateBar, Marquee, WaveText, Dialog, toasts, splash screens, the Effect engine |
┌────────────┐ ┌────────────┐ ┌────────────┐
│ tui-dsl │──▶│tui-widgets │──▶│ tui-core │ Element tree → Widgets → Buffer
└─────┬──────┘ └────────────┘ └─────▲──────┘
│ ┌────────────┐ ┌─────┴──────┐
└─────────▶│tui-runtime │──▶│tui-terminal│ signals/loop → diff → ANSI
└────────────┘ └────────────┘
| Module | What it owns |
|---|---|
core/ |
Buffer/Cell, Style, Layout solver, Widget traits, event ADT, CharWidth (UCD-generated width table) |
terminal/ |
Backend trait, JLine 3 impl (diff flush, input decoding), HeadlessBackend |
widgets/ |
every built-in widget — backend-agnostic, render-to-Buffer tested |
runtime/ |
Signal/Computed, render thread, runner loop, Effect engine |
dsl/ |
TuiApp, Element tree, focus/mouse routing, chrome presets, screens/toasts/palette |
macros/ |
deriveForm/bindAction — compile-time only, keeps native-image reflect-config-free |
test-support/ |
Pilot driver + buffer assertions |
Per-module Scaladoc for every published module is on the docs site's API reference.
House rules (CI-enforced): no java.lang.reflect/Class.forName anywhere; no
String.length/substring for layout math outside CharWidth; warnings are errors;
scalafmt owns formatting.
val backend = HeadlessBackend(Size(60, 16))
val pilot = Pilot.start(backend) { app.runWith(backend) }
pilot.typeText("deploy").pressKey(KeyCode.Enter).waitForIdle()
assert(pilot.screenText.contains("deployed ✓"))Full event/render cycles, no PTY, CI-friendly. All 1,500+ tests in this repo run this way.
./mill show examples.showcase.nativeImage # → a self-contained executableEvery example builds with --no-fallback and no reflect-config JSON — the
framework bridges user code with Scala 3 inline/Mirror instead of reflection.
./mill __.compile # build everything
./mill __.test # ~1.5k tests, headless
./mill mill.scalalib.scalafmt.ScalafmtModule/reformatAll __.sources
./mill widgets.test.runMain io.worxbend.tui.widgets.RenderLoopBench # fps check
./mill examples.showcase.test.runMain \
io.worxbend.tui.examples.showcase.ScreenshotMain 70 17 # README shot
./mill examples.showcase.run # drive it for realAdding a widget — the checklist that keeps quality flat:
- Implement against
Widget/StatefulWidget[S]inwidgets/(state is caller-owned; all width math throughCharWidth). - Render-to-
Buffertests viaBufferAssertions.rendered. - DSL factory in
dsl/Element.scala+ export indsl.scala(focusable elements get abuiltinKeyHandler, mouse behavior viabuiltinMouseHandler). - If interactive: an end-to-end
Pilottest.
Pre-1.0: minor versions (0.x) may break APIs, patches never do. tui-core is the
stability anchor — additive changes only since 0.2. Releases are git tags (vX.Y.Z);
pushing a tag publishes all modules to Maven Central via the Publish workflow
(binary-compatibility gates via MiMa arrive with the first Central release as baseline).
MIT — go build something glyphorious. ✨