CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-com-williamcallahan--tui4j

Terminal User Interface framework for Java that ports the Charmbracelet ecosystem (Bubble Tea, Bubbles, Lipgloss, Harmonica) from Go, enabling developers to build interactive CLI applications using The Elm Architecture pattern.

Overview
Eval results
Files

bubbletea-core.mddocs/

Bubble Tea Framework

The Bubble Tea framework is the core of TUI4J, providing an event-driven architecture based on The Elm Architecture pattern. It manages the application lifecycle through a Model-Update-View cycle with Commands for side effects.

Core Components

Program

The Program class is the main entry point that manages the event loop and program execution.

package com.williamcallahan.tui4j.compat.bubbletea;

class Program {
    // Constructors
    Program(Model initialModel);
    Program(Model initialModel, ProgramOption... options);

    // Execution
    void run();
    Model runWithFinalModel();

    // Runtime control
    void send(Message msg);
    boolean isRunning();
    void waitForInit();

    // Runtime control (continued)
    // ... other methods
}

ProgramOption

Program options are created using static factory methods on the ProgramOption interface.

package com.williamcallahan.tui4j.compat.bubbletea;

interface ProgramOption {
    void apply(Program program);

    // Static factory methods for options
    static ProgramOption withAltScreen();
    static ProgramOption withReportFocus();
    static ProgramOption withKittyKeyboard();
    static ProgramOption withMouseAllMotion();
    static ProgramOption withMouseCellMotion();
    static ProgramOption withMouseClicks(int maxClickCount);
    static ProgramOption withInput(InputStream input);
    static ProgramOption withOutput(OutputStream output);
    static ProgramOption withInputTTY();
    static ProgramOption withContext(CompletableFuture<?> cancelSignal);
    static ProgramOption withFilter(BiFunction<Model, Message, Message> filter);
    static ProgramOption withFps(int fps);
    static ProgramOption withoutSignalHandler();
    static ProgramOption withoutCatchPanics();
    static ProgramOption withoutSignals();
    static ProgramOption withoutBracketedPaste();
    static ProgramOption withoutRenderer();
}

Usage:

// Simple program
Model model = new MyModel();
Program program = new Program(model);
program.run();

// Program with options
Program program = new Program(
    model,
    ProgramOption.withAltScreen(),
    ProgramOption.withMouseCellMotion()
);
program.run();

// Send message from another thread
program.send(new CustomMessage());

// Run and get final model state
Model finalModel = program.runWithFinalModel();

Program Options:

  • withAltScreen() - Use alternate screen buffer (preserves terminal content)
  • withReportFocus() - Report window focus/blur events
  • withKittyKeyboard() - Enable Kitty keyboard protocol
  • withMouseAllMotion() - Track all mouse motion
  • withMouseCellMotion() - Track mouse motion when button pressed
  • withMouseClicks(n) - Enable click detection (1=single, 2=double)

Model

The Model interface defines application state and behavior.

package com.williamcallahan.tui4j.compat.bubbletea;

interface Model {
    // Initialize model, return initial command
    Command init();

    // Handle messages, return new model and command
    UpdateResult<? extends Model> update(Message msg);

    // Render current state to string
    String view();
}

Implementation Example:

class CounterModel implements Model {
    private final int count;

    public CounterModel(int count) {
        this.count = count;
    }

    @Override
    public Command init() {
        return Command.none();
    }

    @Override
    public UpdateResult<? extends Model> update(Message msg) {
        if (msg instanceof KeyPressMessage keyMsg) {
            Key key = keyMsg.key();

            if (key.type() == KeyType.KeyRunes && key.runes()[0] == '+') {
                return UpdateResult.from(new CounterModel(count + 1));
            }
            if (key.type() == KeyType.KeyRunes && key.runes()[0] == '-') {
                return UpdateResult.from(new CounterModel(count - 1));
            }
            if (key.type() == KeyType.KeyRunes && key.runes()[0] == 'q') {
                return UpdateResult.from(this, Command.quit());
            }
        }

        return UpdateResult.from(this);
    }

    @Override
    public String view() {
        return "Count: " + count + "\n\n" +
               "Press + to increment, - to decrement, q to quit";
    }
}

Command

Commands represent side effects and asynchronous operations.

package com.williamcallahan.tui4j.compat.bubbletea;

interface Command {
    // Execute command and return message
    Message execute();

    // Control flow
    static Command none();
    static boolean isNone(Command cmd);
    static Command quit();

    // Batching and sequencing
    static Command batch(Command... commands);
    static Command batch(Collection<Command> commands);
    static Command sequence(Command... commands);
    static Command sequentially(Command... commands);

    // Timing
    static Command tick(Duration duration, Function<Object, Message> fn);
    static Command every(Duration interval, Function<Object, Message> fn);

    // Terminal operations
    static Command println(Object... args);
    static Command printf(String format, Object... args);
    static Command setWindowTitle(String title);
    static Command clearScreen();
    static Command checkWindowSize();

    // Mouse cursor
    static Command setMouseCursorText();
    static Command setMouseCursorPointer();
    static Command resetMouseCursor();

    // Mouse tracking
    static Command enableMouseCellMotion();
    static Command enableMouseAllMotion();
    static Command disableMouse();

    // Clipboard
    static Command copyToClipboard(String text);
    static Command paste();

    // Bracketed paste
    static Command enableBracketedPaste();
    static Command disableBracketedPaste();

    // System integration
    static Command openUrl(String url);
    static Command execProcess(
        Process process,
        BiConsumer<String, Process> onOutput,
        BiConsumer<Integer, Process> onComplete
    );
}

Command Examples:

// Delay and execute
Command.tick(Duration.ofSeconds(2), id -> new TimeoutMessage())

// Batch multiple commands
Command.batch(
    Command.setWindowTitle("My App"),
    Command.enableMouseCellMotion(),
    Command.tick(Duration.ofSeconds(1), id -> new TickMessage())
)

// Execute commands sequentially
Command.sequence(
    Command.println("Starting..."),
    Command.tick(Duration.ofMillis(500), id -> new InitializedMessage()),
    Command.println("Done!")
)

// Open URL in browser
Command.openUrl("https://example.com")

// Copy to clipboard
Command.copyToClipboard("Hello, clipboard!")

// Execute external process
Process process = new ProcessBuilder("ls", "-la").start();
Command.execProcess(
    process,
    (output, proc) -> System.out.println(output),
    (exitCode, proc) -> System.out.println("Exit: " + exitCode)
)

Message

The Message interface is a marker interface for all messages in the system.

package com.williamcallahan.tui4j.compat.bubbletea;

interface Message {
    // Marker interface - no methods
}

All messages implement this interface. Common message types:

  • KeyPressMessage - Keyboard input
  • MouseMessage - Mouse events
  • WindowSizeMessage - Terminal resize
  • QuitMessage - Program quit request
  • ErrorMessage - Error occurred
  • Custom application messages

UpdateResult

Result type containing the new model and optional command.

package com.williamcallahan.tui4j.compat.bubbletea;

record UpdateResult<M extends Model>(M model, Command command) {
    // Factory methods
    static <M extends Model> UpdateResult<M> from(M model, Command cmd);
    static <M extends Model> UpdateResult<M> from(M model);
}

Usage:

// Return model with command
return UpdateResult.from(newModel, Command.quit());

// Return model without command (equivalent to Command.none())
return UpdateResult.from(newModel);

// Return unchanged model
return UpdateResult.from(this);

Built-in Messages

KeyPressMessage

Keyboard input message.

package com.williamcallahan.tui4j.compat.bubbletea;

class KeyPressMessage implements Message {
    Key key();
}

WindowSizeMessage

Terminal window resize event.

package com.williamcallahan.tui4j.compat.bubbletea;

record WindowSizeMessage(int width, int height) implements Message { }

Usage:

if (msg instanceof WindowSizeMessage size) {
    int width = size.width();
    int height = size.height();
    return UpdateResult.from(this.withSize(width, height));
}

QuitMessage

Request to quit the program.

package com.williamcallahan.tui4j.compat.bubbletea;

record QuitMessage() implements Message { }

ErrorMessage

Error occurred during command execution.

package com.williamcallahan.tui4j.compat.bubbletea;

record ErrorMessage(Throwable error) implements Message { }

Usage:

if (msg instanceof ErrorMessage err) {
    Throwable exception = err.error();
    return UpdateResult.from(this.withError(exception.getMessage()));
}

FocusMessage and BlurMessage

Window focus events (requires withReportFocus() option).

package com.williamcallahan.tui4j.compat.bubbletea;

record FocusMessage(boolean focused) implements Message { }
record BlurMessage(boolean blurred) implements Message { }

PasteMessage

Clipboard paste event.

package com.williamcallahan.tui4j.compat.bubbletea;

record PasteMessage(String text) implements Message { }

Usage:

if (msg instanceof PasteMessage paste) {
    String text = paste.text();
    return UpdateResult.from(this.withPastedText(text));
}

Mouse Control Messages

Messages for controlling mouse behavior.

package com.williamcallahan.tui4j.compat.bubbletea;

record EnableMouseCellMotionMessage() implements Message { }
record EnableMouseAllMotionMessage() implements Message { }
record DisableMouseMessage() implements Message { }

Clipboard Messages

package com.williamcallahan.tui4j.compat.bubbletea;

record ReadClipboardMessage(String contents) implements Message { }

Process Execution Messages

package com.williamcallahan.tui4j.compat.bubbletea;

record ExecProcessMessage(
    Process process,
    BiConsumer<String, Process> onOutput,
    BiConsumer<Integer, Process> onComplete
) implements Message { }

record ExecCompletedMessage(int exitCode, Process process) implements Message { }

Other Messages

package com.williamcallahan.tui4j.compat.bubbletea;

record ClearScreenMessage() implements Message { }
record PrintLineMessage(String text) implements Message { }
record SetWindowTitleMessage(String title) implements Message { }
record OpenUrlMessage(String url) implements Message { }
record BatchMessage(List<Message> messages) implements Message { }
record SequenceMessage(List<Message> messages) implements Message { }
record EnterAltScreenMessage() implements Message { }
record ExitAltScreenMessage() implements Message { }
record EnableBracketedPasteMessage() implements Message { }
record DisableBracketedPasteMessage() implements Message { }
record SuspendMessage() implements Message { }
record ResumeMessage() implements Message { }
record UnknownInputByteMessage(byte b) implements Message { }

Kitty Keyboard Protocol Messages

package com.williamcallahan.tui4j.message;

record EnterKeyModifierMessage(EnterKeyModifier modifier) implements Message {
    EnterKeyModifier modifier();
}

enum EnterKeyModifier {
    Normal, Alt, Shift, Ctrl, CtrlShift
}

Usage:

// Enable Kitty keyboard protocol for enhanced Enter key detection
Program program = new Program(
    model,
    Program.withKittyKeyboard()
);

// Handle Enter key modifiers in update()
@Override
public UpdateResult<? extends Model> update(Message msg) {
    if (msg instanceof EnterKeyModifierMessage enterMsg) {
        EnterKeyModifier modifier = enterMsg.modifier();
        switch (modifier) {
            case Normal -> handleNormalEnter();
            case Alt -> handleAltEnter();
            case Shift -> handleShiftEnter();
            case Ctrl -> handleCtrlEnter();
            case CtrlShift -> handleCtrlShiftEnter();
        }
    }
    return UpdateResult.from(this);
}

Advanced Patterns

Composing Models

Models can be composed by delegating to sub-models:

class ParentModel implements Model {
    private final ChildModel child;
    private final String title;

    @Override
    public UpdateResult<? extends Model> update(Message msg) {
        // Delegate to child
        UpdateResult<ChildModel> childResult = child.update(msg);

        // Create new parent with updated child
        ParentModel newParent = new ParentModel(
            childResult.model(),
            this.title
        );

        // Return parent with child's command
        return UpdateResult.from(newParent, childResult.command());
    }

    @Override
    public String view() {
        return title + "\n" + child.view();
    }
}

Tagged Messages

Use wrapper records to tag messages for routing:

record ComponentMessage(String componentId, Message inner) implements Message { }

@Override
public UpdateResult<? extends Model> update(Message msg) {
    if (msg instanceof ComponentMessage tagged) {
        if ("input".equals(tagged.componentId())) {
            // Route to input component
            return updateInput(tagged.inner());
        }
    }
    return UpdateResult.from(this);
}

Async Data Loading

Use commands to load data asynchronously:

@Override
public Command init() {
    // Start loading data
    return Command.tick(Duration.ZERO, id -> {
        try {
            String data = loadData();
            return new DataLoadedMessage(data);
        } catch (Exception e) {
            return new ErrorMessage(e);
        }
    });
}

@Override
public UpdateResult<? extends Model> update(Message msg) {
    if (msg instanceof DataLoadedMessage loaded) {
        return UpdateResult.from(this.withData(loaded.data()));
    }
    return UpdateResult.from(this);
}

Periodic Updates

Use Command.every() for periodic updates:

@Override
public Command init() {
    // Update every second
    return Command.every(
        Duration.ofSeconds(1),
        id -> new TickMessage()
    );
}

@Override
public UpdateResult<? extends Model> update(Message msg) {
    if (msg instanceof TickMessage) {
        return UpdateResult.from(
            this.incrementCounter(),
            Command.every(Duration.ofSeconds(1), id -> new TickMessage())
        );
    }
    return UpdateResult.from(this);
}

Coordinating Multiple Commands

Use Command.batch() to execute multiple commands:

@Override
public UpdateResult<? extends Model> update(Message msg) {
    if (msg instanceof StartMessage) {
        Command cmd = Command.batch(
            Command.setWindowTitle("Loading..."),
            Command.tick(Duration.ofSeconds(1), id -> new LoadMessage()),
            Command.println("Starting...")
        );
        return UpdateResult.from(this, cmd);
    }
    return UpdateResult.from(this);
}

Install with Tessl CLI

npx tessl i tessl/maven-com-williamcallahan--tui4j

docs

animation.md

bubbles-components.md

bubbletea-core.md

index.md

input-handling.md

lipgloss-styling.md

utilities.md

tile.json