Skip to content

Troubleshooting

w0rxbend edited this page Aug 1, 2026 · 1 revision

Troubleshooting

Start here when an app does not render, input is missing, or native behavior differs from the JVM. Work from the first relevant symptom; most problems come from TTY availability, state lifetime, tracked reads, or event consumption.

UnsupportedTerminal at startup

glyphora needs a controlling TTY. An IDE output panel, redirected pipe, or headless CI process intentionally returns UnsupportedTerminal instead of entering raw mode. Run the app in a terminal:

./mill examples.showcase.run

For CI, inject HeadlessBackend; see Testing.

The view does not update

Check these in order:

  1. The view must read reactive state with .get, not .peek.
  2. A signal must live outside view; recreating it resets the value every pass.
  3. A signal only notifies when the new value is not equal (!=) to the old one.
  4. Replace immutable collections instead of mutating one in place.
  5. Third-party callbacks must write on the render thread.
private val rows = Signal(Vector.empty[Row])

def view(using ReactiveScope) = renderRows(rows.get)
def append(row: Row) = rows.update(_ :+ row)

Input edits, then resets

TextInputState, TextAreaState, ListState, and other interactive state objects must be created once on the app or owning screen:

private val inputState = TextInputState()

def view(using ReactiveScope) = input(inputState)

Creating TextInputState() in view replaces it with an empty editor on the next redraw.

Input or mouse events do not arrive

  • Confirm the element is focusable and press Tab to move focus.
  • Return false from a custom handler when the event should bubble.
  • A user handler that always returns true can block built-in input/list behavior.
  • Keep global behavior in KeyBindings; focused handlers run before them.
  • Do not parse escape sequences in the app; JLine3Backend owns decoding.
  • TuiApp enables backend interaction modes. A custom runner owns that lifecycle.

See Mouse & focus for the exact routing order.

Focus jumps after conditional rendering

Focus is positional unless an element has a stable key. Add .key("unique-name") to interactive controls that can move when branches appear or disappear. Keys must be unique among focusable elements in the current tree.

Toasts never disappear or effects do not animate

Both advance on ticks. Configure a cadence:

import io.worxbend.tui.runtime.RunnerConfig
import scala.concurrent.duration.*

override def config = RunnerConfig(tickRate = Some(100.millis))

A splash supplies ticks automatically when the app has none; normal toasts and runEffect calls do not.

Text alignment breaks with emoji or CJK

Use CharWidth for custom width calculations. Java/Scala string length counts UTF-16 code units, not terminal cells. Built-in widgets already use CharWidth for clipping, wrapping, and cursor placement.

If built-in widgets agree but the emulator still looks wrong, check the emulator's ambiguous/emoji width policy and font fallback. An application cannot force an emulator to assign a particular width to a new emoji.

A Signal update throws off the render thread

Key, mouse, binding, and tick handlers are already safe. Futures, HTTP clients, and other callbacks must hop back before writing.

Prefer the structured helper:

Async.runCatching(fetch()) {
  case Right(value) => data.set(value)
  case Left(error)  => failure.set(Some(error.getMessage))
}

Its completion already runs on the render thread. For an externally owned callback, use RenderThread.runOnRenderThread.

A panel or child looks empty

Borders consume space. A panel needs at least three rows to leave one inner row. In a row, child constraints allocate width; in a column, they allocate height.

Temporarily replace nested content with labeled text elements and inspect each container's .length, .percent, and .fill constraints from the outside in.

DataTable opens the wrong row

DataTableState.selected indexes the filtered and sorted view, not the original row sequence. Resolve the selection with:

state.selected.flatMap(table.visibleRows(state).lift)

Native-image compilation fails

First confirm the JVM build and tests:

./mill app.compile
./mill app.test
./mill app.nativeImage

Keep --no-fallback enabled. Inspect your own dependencies for runtime reflection, dynamic class loading, resources, JNI, and proxies. glyphora's derivation APIs use compile-time Scala 3 macros and require no reflection config. See Native binaries.

The screen is corrupted after a crash

It should not be. glyphora owns the terminal's signal handling instead of leaving it to JLine's defaults, so every ordinary way of ending a TUI restores the terminal. Measured on a real PTY with examples/hello-world:

How the app ended termios alternate screen cursor paste / focus / kitty modes
normal return (q)
uncaught exception from view
Ctrl+C (SIGINT)
SIGQUIT
SIGTERM
SIGHUP (window closed)
System.exit from a handler
SIGKILL

Ctrl+C arrives as Event.Interrupt and quits through the same teardown as any other exit; override TuiApp.onInterrupt() and return true to intercept it (to confirm, or to cancel in-flight work) instead. Signal-terminated exits additionally go through a JVM shutdown hook that writes the mode-reset sequences straight to the stdout descriptor, so restoration does not depend on the backend still being intact.

SIGKILL cannot be caught by anything; after kill -9, run reset.

Set GLYPHORA_DEBUG=1 to have failed teardown steps report themselves on stderr rather than being swallowed.

When launching an external interactive program, use TuiApp.suspend { ... } so the terminal is deliberately handed over and restored.

Debug output destroys the live UI

Ordinary println writes into the terminal glyphora is repainting. Use a Log widget, write to a file, or call printAbove(...) from a handler to add durable lines to scrollback without corrupting the frame.

Still stuck?

Search existing issues, then open a minimal reproduction with:

  • glyphora, Scala, JVM, and OS versions;
  • terminal emulator and shell;
  • smallest terminal size that reproduces it;
  • whether HeadlessBackend reproduces it;
  • relevant stack trace and key/mouse sequence;
  • JVM/native-image difference, if any.

Clone this wiki locally