Skip to content

Repository files navigation

glyphora — terminal UI, written like Scala

✦ Terminal UI, written like Scala.

Reactive signals · 50+ widgets · keyboard & mouse
composable motion · headless tests · GraalVM native-image

CI Docs Release License

Scala JDK Mill native-image Zero reflection

📖 Read the guide · 📚 Browse the wiki · 🍳 Cookbook · 🔎 Scaladoc · 🎮 Examples


✦ Why glyphora

Reactive signals, rich widgets, motion, mouse input, and native binaries


⚡ Signals, not plumbing
Read Signal and Computed in a typed Scala view; glyphora tracks the dependencies and redraws what changed.

🧩 A real widget vocabulary
Inputs, tables, trees, Markdown, charts, spinners, dialogs, menus and app chrome all ship together.

⌨️ Interaction is first-class
Focus order, bubbling keys, bracketed paste, mouse hit-testing and resize events are built in.

🎬 Motion stays composable
Effects transform the completed frame and animated widgets are pure functions of elapsed time, so every renderer stays deterministic and testable.

🧪 Tests share one pipeline
Render to a real terminal or the in-memory HeadlessBackend — drive whole apps with no PTY.

📦 Native-image by design
Compile-time derivation replaces runtime reflection, so examples build with --no-fallback and no reflect config.

🌍 Unicode done properly
Grapheme clusters, CJK width, emoji ZWJ sequences and combining marks all go through one width table.

🏗️ Batteries-included shell
Scaffold, top bar, sidebar, status line, toasts, screens and a fuzzy command palette.

🚀 Your first app

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 →

🧭 One render pipeline

glyphora typed render pipeline and module architecture

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
Loading

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.

🧭 Architecture guide →

🧩 Widget atlas

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 →

🧪 Test the terminal without a terminal

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.

🧪 Testing guide →

📦 Native binaries, zero reflection config

./mill examples.showcase.nativeImage

CI 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.

📦 Native-image guide →

🧰 Build from source

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 cache

Day-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 terminal

Docs 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.

📚 Documentation map

The same Markdown publishes to the 📖 GitHub Pages site and the 📚 GitHub Wikiwebsite/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

🤝 Contributing

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.

📜 License

MIT — go build something glyphorious. ✦

Built with Scala 3 · Mill · JLine 3 · GraalVM

About

Terminal UI, written like Scala

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages