Skip to content

Component Model

AliveJTUI's component model is deliberately close to React. If you've used React class components, you already know 80% of this.


The Component Class

Every UI in AliveJTUI is a Component. Subclass it to create your application:

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

public class MyApp extends Component {

    // --- State fields ---
    private String inputText = "";
    private boolean checked = false;
    private int counter = 0;

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

        // Key handlers are registered here and auto-removed on unmount
        onKey(KeyType.ENTER, () -> setState(() -> counter++));

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

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

State fields are plain Java fields

Unlike React hooks, state in AliveJTUI is just instance fields on your class. There is no special state container — setState() is purely a signal to re-render.


setState

setState(Runnable mutation) applies the mutation and queues a re-render. Multiple rapid calls within the same event are batched.

// Single mutation
setState(() -> this.count++);

// Multiple fields in one mutation
setState(() -> {
    this.count++;
    this.label = "clicked";
    this.lastAction = Instant.now();
});

Mutation must be synchronous

The Runnable passed to setState runs on the event loop thread. Do not perform blocking I/O or slow computation inside it. For background work, use setStateAsync instead.


Async State

For background work (network calls, file I/O, database queries), use setStateAsync:

setStateAsync(() -> {
    // This lambda runs on a background thread
    String result = fetchFromDatabase();

    // Return a Runnable that runs on the event loop thread
    return () -> this.data = result;
});

For more control — including error handling — use AliveJTUI.runAsync:

AliveJTUI.runAsync(AsyncTask.of(
    () -> fetchUserData(userId),                         // background thread
    result -> setState(() -> this.user = result),        // success — event loop
    err    -> setState(() -> this.error = err.getMessage()) // failure — event loop
));

Thread safety

AliveJTUI has a single event loop thread. setState and all render callbacks always execute on that thread. You never need explicit synchronization as long as you don't modify state fields directly from background threads — always funnel changes back through setState.


Lifecycle

Method When it is called
mount(onStateChange, eventBus) The component enters the UI tree. Register key handlers and timers here.
render() Called after every setState(). Should return a pure, stateless Node tree.
unmount() The component leaves the UI tree. Key handlers registered via onKey() are automatically removed.
onError(Exception e) If render() throws, this is called. Return a fallback Node to display.
shouldUpdate() Override to prevent unnecessary re-renders. Default: always true.

mount

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

    // Register key handlers (auto-removed on unmount)
    onKey(KeyType.ARROW_DOWN, () -> setState(() -> selectedRow++));
    onKey(KeyType.ARROW_UP,   () -> setState(() -> selectedRow--));

    // Register focusable nodes
    registerFocusable(myButton);
    registerFocusable(myInput);

    // Start a repeating timer
    AliveJTUI.scheduleRepeating(100, () -> setState(() -> spin.nextFrame()));
}

render

@Override
public Node render() {
    // Pure function — no side effects, no I/O
    return VBox.of(
        Text.of("Row: " + selectedRow).bold(),
        myButton,
        myInput
    );
}

render() must be pure

Do not call setState() from inside render() — it will cause an infinite render loop. render() should only read state fields and build a Node tree.

onError

@Override
public Node onError(Exception e) {
    return VBox.of(
        Text.of("Something went wrong:").bold().color(Color.RED),
        Text.of("  " + e.getMessage()).color(Color.BRIGHT_RED)
    );
}

shouldUpdate

@Override
public boolean shouldUpdate() {
    // Only re-render if the data actually changed
    return !Objects.equals(previousData, currentData);
}

Key Handling

Register key handlers in mount(). They are automatically unregistered when the component unmounts.

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

    // Named key types
    onKey(KeyType.ARROW_DOWN, () -> setState(() -> selectedRow++));
    onKey(KeyType.ARROW_UP,   () -> setState(() -> selectedRow--));
    onKey(KeyType.PAGE_DOWN,  () -> setState(() -> selectedRow += 10));
    onKey(KeyType.ENTER,      () -> confirmSelection());
    onKey(KeyType.ESCAPE,     () -> AliveJTUI.stop());

    // A handler that may consume the event (stops propagation)
    onKey(KeyType.BACKSPACE, () -> {
        if (!inputText.isEmpty()) {
            setState(() -> inputText = inputText.substring(0, inputText.length() - 1));
            return true; // consumed — no further processing
        }
        return false; // not consumed — propagate
    });

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

All Key Types

Key Type Description
CHARACTER Any printable character
ENTER Enter / Return
BACKSPACE Backspace
DELETE Delete
ARROW_UP Up arrow
ARROW_DOWN Down arrow
ARROW_LEFT Left arrow
ARROW_RIGHT Right arrow
ESCAPE Escape
TAB Tab
SHIFT_TAB Shift+Tab
HOME Home
END End
PAGE_UP Page Up
PAGE_DOWN Page Down
EOF Ctrl+D / end of input

Running the Application

// Default backend (Lanterna — opens Swing window or terminal)
AliveJTUI.run(new MyApp());

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

// Programmatic stop
AliveJTUI.stop();

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, Button, Input, ...)
  style/     Color, Style, Theme, StyleSheet, Selector
  event/     EventBus, KeyEvent, KeyType
  backend/   TerminalBackend, LanternaBackend, MockBackend
  render/    Renderer, LayoutEngine, Differ, TreeFlattener