CLI > AliveJTUI
Declarative, React-style TUI library for building terminal UIs as component trees with diff-based rendering, focus management, and themes.
╔══════════════════════════════════════════════════════════════════════════╗
║ ║
║ █████╗ ██╗ ██╗██╗ ██╗███████╗ ██╗████████╗██╗ ██╗██╗ ║
║ ██╔══██╗██║ ██║██║ ██║██╔════╝ ██║╚══██╔══╝██║ ██║██║ ║
║ ███████║██║ ██║╚██╗ ██╔╝█████╗ ██║ ██║ ██║ ██║██║ ║
║ ██╔══██║██║ ██║ ╚████╔╝ ██╔══╝ ██ ██║ ██║ ██║ ██║██║ ║
║ ██║ ██║███████╗██║ ╚═══╝ ███████╗ ╚█████╔╝ ██║ ╚██████╔╝██║ ║
║ ╚═╝ ╚═╝╚══════╝╚═╝ ╚══════╝ ╚════╝ ╚═╝ ╚═════╝ ╚═╝ ║
║ ║
║ Declarative TUI library for Java v0.2.0 ║
║ ───────────────────────────────────────────────────────── ║
║ crafted with pride by J A R V I S (AI) ║
╚══════════════════════════════════════════════════════════════════════════╝
AliveJTUI
A declarative TUI (Terminal User Interface) library for Java. Build terminal UIs as component trees — like React, but for the terminal.
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
Nodetree; the library diffs and redraws only changed cells - React-style components — subclass
Component, callsetState(), 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 management —
Tab/Shift+Tabcycle through focusable nodes;Enterfires the focused button - Theme system — swap
Theme.DARK/Theme.LIGHT(or implement your own) at runtime - CSS-like styling —
StyleSheetwith#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
UndoManagerfor reversible operations - Diff-based renderer — only changed terminal cells are redrawn; no full-screen flicker
- Pluggable backends —
LanternaBackend(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
| Method | Called 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
| Expression | Description |
|---|---|
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
| Expression | Description |
|---|---|
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
| Backend | Use 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
Navigation
| Key | Action |
|---|---|
1 – 6 |
Switch tab |
T |
Toggle Dark / Light theme |
D |
Open confirmation dialog |
N |
Show notification toast |
Tab |
Move focus to next widget |
Enter |
Click focused button |
↑ ↓ |
Navigate table rows / virtual list / scroll viewport (tab 5) |
PgUp PgDn |
Page through virtual list / viewport |
Home End |
Jump to top / bottom of virtual list |
+ / - |
Increase / decrease progress bar |
X |
Toggle checkbox |
C |
Expand / collapse section |
S |
Cycle color select |
ESC |
Quit |
Tabs
| Tab | Content |
|---|---|
1:Widgets |
Button, progress bar, checkbox, input, radio, spinner, select |
2:Table |
Scrollable data table with keyboard navigation |
3:VirtualList |
10,000 items — only visible rows rendered |
4:Text |
All text styles, inline markdown, word wrapping |
5:Layout |
BoxNode panels, collapsible section, scrollable viewport |
6:Login |
Login 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
| # | Description | Status |
|---|---|---|
| 1 | InputNode did not show focus highlight when tabbing between fields — bold style is invisible on empty inputs |
Fixed in 0.2.0 (underline style applied on focus) |
License
MIT