AliveJTUI

April 2, 2026 · View on GitHub

╔══════════════════════════════════════════════════════════════════════════╗
║                                                                          ║
║   █████╗ ██╗     ██╗██╗   ██╗███████╗      ██╗████████╗██╗   ██╗██╗      ║
║  ██╔══██╗██║     ██║██║   ██║██╔════╝      ██║╚══██╔══╝██║   ██║██║      ║
║  ███████║██║     ██║╚██╗ ██╔╝█████╗        ██║   ██║   ██║   ██║██║      ║
║  ██╔══██║██║     ██║ ╚████╔╝ ██╔══╝   ██   ██║   ██║   ██║   ██║██║      ║
║  ██║  ██║███████╗██║  ╚═══╝  ███████╗ ╚█████╔╝   ██║   ╚██████╔╝██║      ║
║  ╚═╝  ╚═╝╚══════╝╚═╝         ╚══════╝  ╚════╝    ╚═╝    ╚═════╝ ╚═╝      ║
║                                                                          ║
║          Declarative TUI library for Java              v0.2.0            ║
║          ─────────────────────────────────────────────────────────       ║
║                    crafted with pride by  J A R V I S  (AI)              ║
╚══════════════════════════════════════════════════════════════════════════╝

AliveJTUI

Maven Central SonarCloud Quality Gate Status Coverage Bugs Java License

A declarative TUI (Terminal User Interface) library for Java. Build terminal UIs as component trees — like React, but for the terminal.

Documentation

 AliveJTUI Demo v0.2.0  theme: [Dark]
 1:Widgets  2:Table  3:VirtualList  4:Text  5:Layout  6:Login
──────────────────────────────────────────────────────────────
  [ Click Me! ]  Clicked: 3  Spin: |
  Progress [+][-]
  [████████████░░░░░░░░]  60%
  ☑ Notifications enabled    Input: [hello_]
  Theme radio [Up/Down]:  (x) Dark  ( ) Light
  Color select [S]:  << Cyan >>
──────────────────────────────────────────────────────────────
  1-6:Tab  T:Theme  D:Dialog  N:Notify  C:Collapse  X:Checkbox  S:Select  +/-:Progress  ESC:Quit

Features

  • Declarative rendering — describe UI as a Node tree; the library diffs and redraws only changed cells
  • React-style components — subclass Component, call setState(), let the framework re-render
  • Rich node library — text, buttons, inputs, checkboxes, radio groups, selects, tables, virtual lists, viewports, dialogs, spinners, progress bars, and more
  • Focus managementTab / Shift+Tab cycle through focusable nodes; Enter fires the focused button
  • Theme system — swap Theme.DARK / Theme.LIGHT (or implement your own) at runtime
  • CSS-like stylingStyleSheet with #id, .class, and type selectors
  • Overlay API — push/pop dialogs and toast notifications on top of any UI
  • Async state — run background work and apply results safely on the event loop thread
  • Timers — one-shot and repeating callbacks with automatic re-render
  • Virtual lists — render 10,000+ items with only visible rows drawn
  • Undo/Redo — built-in UndoManager for reversible operations
  • Diff-based renderer — only changed terminal cells are redrawn; no full-screen flicker
  • Pluggable backendsLanternaBackend (default), MockBackend (testing), or bring your own

Requirements

  • Java 17+
  • Maven 3.8+

Quick Start

1. Add the dependency

<dependency>
    <groupId>io.github.yehorsyrin</groupId>
    <artifactId>alivejTUI</artifactId>
    <version>0.2.0</version>
</dependency>

2. Write a component

import io.github.yehorsyrin.tui.core.*;
import io.github.yehorsyrin.tui.event.*;
import io.github.yehorsyrin.tui.node.*;
import io.github.yehorsyrin.tui.style.Color;

public class CounterApp extends Component {

    private int count = 0;

    @Override
    public void mount(Runnable onStateChange, EventBus eventBus) {
        super.mount(onStateChange, eventBus);
        onKey(KeyType.ARROW_UP,   () -> setState(() -> count++));
        onKey(KeyType.ARROW_DOWN, () -> setState(() -> count--));
    }

    @Override
    public Node render() {
        return VBox.of(
            Text.of("  Counter Demo").bold().color(Color.CYAN),
            Divider.horizontal(),
            HBox.of(
                Text.of("  Count: ").dim(),
                Text.of(String.valueOf(count)).bold().color(Color.GREEN)
            ),
            Text.of(""),
            Text.of("  Up/Down: +/-   ESC: quit").dim()
        );
    }

    public static void main(String[] args) {
        AliveJTUI.run(new CounterApp());
    }
}

3. Run the demo jar

java -jar alivejTUI-demo.jar

On GUI desktops (macOS/Windows/X11/Wayland) a Swing window opens. On headless servers and CI the app runs directly in the terminal via the native backend.


Component Model

Subclassing Component

public class MyApp extends Component {

    // State fields
    private String text = "";
    private boolean checked = false;

    @Override
    public void mount(Runnable onStateChange, EventBus eventBus) {
        super.mount(onStateChange, eventBus);
        // Register key handlers here (auto-unregistered on unmount)
        onKey(KeyType.ENTER, () -> setState(() -> text += "!"));
        eventBus.registerCharacter(c -> {
            if (c >= 32) setState(() -> text += c);
        });
    }

    @Override
    public Node render() {
        // Return a new Node tree every call — the diff engine handles the rest
        return VBox.of(
            Text.of("Input: " + text),
            Checkbox.of("Option", checked, () -> setState(() -> checked = !checked))
        );
    }
}

setState

setState(Runnable mutation) — applies the mutation and triggers a re-render.

setState(() -> {
    this.count++;
    this.label = "clicked";
});

Async state

setStateAsync(Supplier<Runnable> task) — runs work on a background thread, then applies the mutation on the event loop thread.

setStateAsync(() -> {
    String result = fetchFromNetwork(); // background thread
    return () -> this.data = result;    // event loop thread
});

Or use AliveJTUI.runAsync(AsyncTask) directly:

AliveJTUI.runAsync(AsyncTask.of(
    () -> fetchData(),
    result -> setState(() -> this.data = result),
    err    -> setState(() -> this.error = err.getMessage())
));

Lifecycle

MethodCalled when
mount(onStateChange, eventBus)Component enters the UI tree
render()State changes; should return a pure Node tree
unmount()Component leaves the UI tree; key handlers auto-unregistered
onError(Exception)render() throws; return a fallback node
shouldUpdate()Override to skip re-render (optimization; default: always)

Node Reference

All factory methods return a node with a fluent builder API.

Text & Display

ExpressionDescription
Text.of("hello")Plain single-line text
Text.of("hello").bold()Bold text
Text.of("hello").italic()Italic text
Text.of("hello").underline()Underlined text
Text.of("hello").strikethrough()Strikethrough text
Text.of("hello").dim()Dimmed/muted text
Text.of("hello").color(Color.CYAN)Foreground color
Text.of("hello").background(Color.BLUE)Background color
Text.ofMarkdown("**bold** and *italic*")Inline markdown
Paragraph.of("long text...")Word-wrapped plain text
Paragraph.ofMarkdown("**bold** paragraph")Word-wrapped markdown
Divider.horizontal()Horizontal rule ───────
Divider.vertical()Vertical rule

Supported markdown syntax: **bold**, *italic*, `code`, ~~strikethrough~~

Layout

ExpressionDescription
VBox.of(node1, node2, ...)Vertical stack
VBox.of(nodes).gap(1)Vertical stack with spacing
HBox.of(node1, node2, ...)Horizontal stack
HBox.of(nodes).gap(2)Horizontal stack with spacing
new BoxNode(child, true, borderStyle)Bordered container

Interactive Widgets

Button

ButtonNode btn = Button.of("[ OK ]", () -> System.out.println("clicked"));
registerFocusable(btn);   // participate in Tab cycling
// Tab to focus, Enter to click

Text Input

InputNode input = Input.of("initial", value -> setState(() -> this.text = value));
registerFocusable(input); // Tab to focus; typed characters update value

TextArea

TextAreaNode area = TextArea.of("", 5); // 5 visible rows
area.insertChar('h');
area.insertChar('i');
String content = area.getText();

Checkbox

CheckboxNode cb = Checkbox.of("Enable feature", checked, () -> setState(() -> checked = !checked));
// Press X (or bind any key) to toggle

RadioGroup

RadioGroupNode radio = RadioGroup.of("Option A", "Option B", "Option C");
// radio.getSelectedIndex(), radio.setSelectedIndex(1)

Select

SelectNode sel = Select.of("Red", "Green", "Blue");
// sel.getSelectedValue(), sel.setSelectedIndex(2)

Lists & Tables

Table

List<String> headers = List.of("Name", "Role", "City");
List<List<String>> rows = List.of(
    List.of("Alice", "Engineer", "Berlin"),
    List.of("Bob",   "Designer", "London")
);
TableNode table = Table.of(headers, rows, 8); // show 8 rows
table.selectDown(); // navigate
table.selectUp();

VirtualList (large data sets)

List<String> items = IntStream.range(1, 100_001)
    .mapToObj(i -> "Item " + i)
    .collect(toList());

VirtualListNode list = VirtualList.of(items, 15); // 15 visible rows
list.selectDown();
list.selectUp();
list.pageDown();
list.pageUp();
list.selectFirst();
list.selectLast();
int idx = list.getSelectedIndex();

Viewport (scrollable window)

Node content = VBox.of(/* many nodes */);
ViewportNode vp = Viewport.of(content, 10); // 10 visible rows
vp.scrollDown();
vp.scrollUp();
vp.pageDown();
vp.pageUp();
vp.scrollToTop();
vp.scrollToBottom();
vp.showScrollbar(false); // hide scroll bar

Store the viewport as a field and wire scroll to key handlers:

// in mount():
onKey(KeyType.ARROW_DOWN, () -> setState(() -> viewport.scrollDown()));
onKey(KeyType.ARROW_UP,   () -> setState(() -> viewport.scrollUp()));

Progress Bar

ProgressBarNode bar = new ProgressBarNode(0.65); // 65%
bar.setProgress(0.80);
bar.filledStyle(Style.DEFAULT.withForeground(Color.GREEN));
bar.emptyStyle(Style.DEFAULT.withForeground(Color.BRIGHT_BLACK));

Spinner

SpinnerNode spin = Spinner.of();            // default frames: | / - \
SpinnerNode spin = Spinner.of(new String[]{ "⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏" });

// Advance frame on a timer:
AliveJTUI.scheduleRepeating(100, () -> setState(() -> spin.nextFrame()));

Collapsible

Node section = Collapsible.of("Settings",
    Text.of("  Option A"),
    Text.of("  Option B")
);
// Collapsible.expanded("Title", ...) — starts expanded
CollapsibleNode col = (CollapsibleNode) section;
col.toggle();    // expand/collapse
col.expand();
col.collapse();
boolean open = col.isExpanded();

Dialog

// Push a dialog overlay
Node dialog = Dialog.of("Confirm", VBox.of(
    Text.of("Are you sure?"),
    HBox.of(
        Button.of("[Yes]", () -> setState(() -> { dialogNode = null; /* confirm */ })),
        Button.of("[No]",  () -> setState(() -> dialogNode = null))
    )
));
AliveJTUI.pushOverlay(dialog);

// Dismiss
AliveJTUI.popOverlay();

Help Panel

Node help = HelpPanel.of(
    new KeyBinding("Tab",     "Next field"),
    new KeyBinding("Enter",   "Confirm"),
    new KeyBinding("ESC",     "Quit")
);

Styling

Style

Style is immutable. Build from Style.DEFAULT:

Style bold  = Style.DEFAULT.withBold(true);
Style fancy = Style.DEFAULT
    .withForeground(Color.CYAN)
    .withBackground(Color.BRIGHT_BLACK)
    .withBold(true)
    .withItalic(true);

Apply to a node:

Text.of("hello").style(fancy)
// or shorthand:
Text.of("hello").bold().color(Color.CYAN)

Color

// Standard ANSI 16
Color.RED, Color.GREEN, Color.YELLOW, Color.BLUE,
Color.CYAN, Color.MAGENTA, Color.WHITE, Color.BLACK,
Color.BRIGHT_RED, Color.BRIGHT_GREEN, /* ... */

// 256-color
Color.ansi256(202) // orange

// True color
Color.rgb(255, 128, 0) // orange (if terminal supports it)

Theme

// Global theme (default: DARK)
AliveJTUI.setTheme(Theme.LIGHT);
AliveJTUI.setTheme(Theme.DARK);

// Use in components
Theme t = AliveJTUI.getTheme();
Text.of("Title").style(t.primary())
Text.of("Hint").style(t.muted())
Text.of("OK").style(t.success())
Text.of("Error").style(t.error())

Semantic roles: foreground(), muted(), primary(), secondary(), success(), warning(), error(), focused().

Custom theme:

Theme myTheme = new Theme.BuiltinTheme(
    Style.DEFAULT,                                         // foreground
    Style.DEFAULT.withDim(true),                          // muted
    Style.DEFAULT.withForeground(Color.rgb(0,200,255)).withBold(true), // primary
    Style.DEFAULT.withForeground(Color.MAGENTA),          // secondary
    Style.DEFAULT.withForeground(Color.GREEN),            // success
    Style.DEFAULT.withForeground(Color.YELLOW),           // warning
    Style.DEFAULT.withForeground(Color.RED),              // error
    Style.DEFAULT.withForeground(Color.CYAN).withBold(true) // focused
);
AliveJTUI.setTheme(myTheme);

StyleSheet (CSS-like)

StyleSheet sheet = new StyleSheet()
    .add(Selector.byId("title"),        Style.DEFAULT.withForeground(Color.CYAN).withBold(true))
    .add(Selector.byClass("muted"),     Style.DEFAULT.withDim(true))
    .add(Selector.byType(ButtonNode.class), Style.DEFAULT.withForeground(Color.YELLOW));

// Tag nodes
Text.of("Hello").withId("title")
Text.of("hint").withClassName("muted")

// Apply to tree
sheet.applyToTree(rootNode);

Notifications

NotificationManager notif = new NotificationManager(() -> setState(() -> {}));

notif.show("Saved successfully!", 2000);
notif.show("Warning: low disk space", 4000, NotificationType.WARNING);
notif.show("Error: connection failed", 5000, NotificationType.ERROR);
notif.show("File uploaded",            2500, NotificationType.SUCCESS);
// NotificationType: INFO (default), SUCCESS, WARNING, ERROR

// In render():
Node overlay = notif.buildOverlay();
if (overlay != null) AliveJTUI.pushOverlay(overlay);

Timers

// One-shot after 2 seconds
AliveJTUI.schedule(2000, () -> setState(() -> this.message = "done"));

// Repeating every 150ms (e.g. spinner animation)
Runnable tick = () -> setState(() -> spinFrame = (spinFrame + 1) % SPIN.length);
AliveJTUI.scheduleRepeating(150, tick);

// Cancel
AliveJTUI.cancelTimer(tick);

Focus Management

// Register focusable nodes in mount():
registerFocusable(myButton);
registerFocusable(myInput);
registerFocusable(anotherButton);

// Tab  → focusNext()
// Shift+Tab → focusPrev()
// Enter → click() on the focused ButtonNode

Nodes implementing Focusable: ButtonNode, InputNode, TextAreaNode, CheckboxNode, RadioGroupNode, SelectNode, VirtualListNode.


Undo / Redo

UndoManager undo = new UndoManager(); // default 100 entries
// or: new UndoManager(50);

String prev = text;
text = "new value";
undo.record(
    () -> setState(() -> text = prev),          // undo
    () -> setState(() -> text = "new value")    // redo
);

// In key handlers:
onKey(KeyType.CHARACTER, () -> {
    if (event.ctrl() && event.character() == 'z') undo.undo();
    if (event.ctrl() && event.character() == 'y') undo.redo();
});

boolean canUndo = undo.canUndo();
boolean canRedo = undo.canRedo();
undo.clear();

Key Handling

@Override
public void mount(Runnable onStateChange, EventBus eventBus) {
    super.mount(onStateChange, eventBus);

    // Special keys
    onKey(KeyType.ARROW_DOWN, () -> setState(() -> selectedRow++));
    onKey(KeyType.ARROW_UP,   () -> setState(() -> selectedRow--));
    onKey(KeyType.PAGE_DOWN,  () -> setState(() -> selectedRow += 10));
    onKey(KeyType.ENTER,      () -> confirmSelection());

    // Consuming (return true stops propagation)
    onKey(KeyType.BACKSPACE, () -> {
        if (myInput.length() > 0) {
            setState(() -> myInput = myInput.substring(0, myInput.length() - 1));
            return true; // consumed
        }
        return false;
    });

    // Any printable character
    eventBus.registerCharacter(c -> {
        if (c >= 32) setState(() -> inputText += c);
    });
}

All key types: CHARACTER, ENTER, BACKSPACE, DELETE, ARROW_UP, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, ESCAPE, TAB, SHIFT_TAB, HOME, END, PAGE_UP, PAGE_DOWN, EOF.


Backends

BackendUse case
Backends.createAuto()Default. Opens a Swing window on GUI desktops; uses native terminal on headless/server environments.
Backends.createSwing()Swing window — for GUI desktop environments.
Backends.createNative()Raw ANSI/POSIX or Windows VT — for terminal emulators and headless servers.
Backends.createMock(w, h)Unit testing — no real terminal required.
// Default (auto-selects Swing or native)
AliveJTUI.run(new MyApp());

// Explicit backend
AliveJTUI.run(new MyApp(), Backends.createNative());
AliveJTUI.run(new MyApp(), Backends.createSwing());

// Custom backend — implement TerminalBackend
AliveJTUI.run(new MyApp(), new MyCustomBackend());

// Testing
AliveJTUI.run(new MyApp(), Backends.createMock(80, 24));

TerminalBackend is a plain interface — implement it to integrate any other rendering layer (ncurses, raw ANSI, WebSocket, etc.).


Testing

Use MockBackend to test components without a real terminal:

MockBackend backend = (MockBackend) Backends.createMock(80, 24);
AliveJTUI.run(new MyApp(), backend);

// Simulate key presses
backend.sendKey(KeyEvent.of(KeyType.ARROW_DOWN));
backend.sendKey(KeyEvent.ofCharacter('x'));

// Inspect rendered output
String cell = backend.getCell(0, 0); // character at col=0, row=0

Demo Application

java -jar alivejTUI-demo.jar
KeyAction
16Switch tab
TToggle Dark / Light theme
DOpen confirmation dialog
NShow notification toast
TabMove focus to next widget
EnterClick focused button
↑ ↓Navigate table rows / virtual list / scroll viewport (tab 5)
PgUp PgDnPage through virtual list / viewport
Home EndJump to top / bottom of virtual list
+ / -Increase / decrease progress bar
XToggle checkbox
CExpand / collapse section
SCycle color select
ESCQuit

Tabs

TabContent
1:WidgetsButton, progress bar, checkbox, input, radio, spinner, select
2:TableScrollable data table with keyboard navigation
3:VirtualList10,000 items — only visible rows rendered
4:TextAll text styles, inline markdown, word wrapping
5:LayoutBoxNode panels, collapsible section, scrollable viewport
6:LoginLogin form demonstrating focus management, inputs, and validation

Building

# Compile and run tests
mvn test

# Build library jar + demo fat-jar
mvn package
# produces target/alivejTUI-demo.jar

Project Structure

src/
  main/java/io/github/yehorsyrin/tui/
    core/         AliveJTUI, Component, Node, FocusManager, NotificationManager,
                  TimerManager, UndoManager, AsyncTask, Focusable
    node/         All node types: Text, VBox, HBox, Button, Input, TextArea,
                  Checkbox, RadioGroup, Select, Table, VirtualList, Viewport,
                  ProgressBar, Spinner, Dialog, Collapsible, HelpPanel, ...
    style/        Color, Style, Theme, StyleSheet, Selector
    event/        EventBus, KeyEvent, KeyType
    backend/      TerminalBackend (interface), MockBackend, TerminalCapabilities
    platform/     Backends (factory), NativeTerminalBackend, SwingBackend,
                  AnsiKeyDecoder, AnsiWriter, PosixRawMode, WindowsRawMode,
                  ResizePoller, TerminalSizeDetector
    render/       Renderer, LayoutEngine, Differ, TreeFlattener
    example/      DemoNative, DemoApp, TodoApp, Showcase
  test/           unit tests

Known Issues

#DescriptionStatus
1InputNode did not show focus highlight when tabbing between fields — bold style is invisible on empty inputsFixed in 0.2.0 (underline style applied on focus)

License

MIT