# TUI4J
TUI4J (Terminal User Interface for Java) is a Java TUI framework inspired by [Bubble Tea](https://github.com/charmbracelet/bubbletea) from the Charm ecosystem. It provides a compatibility module that mirrors the original Go API, allowing developers familiar with Bubble Tea to build terminal applications in Java using The Elm Architecture pattern. The framework manages terminal I/O, keyboard input, mouse events, and rendering through an event loop that processes messages and updates the application state.
The core of TUI4J follows The Elm Architecture with three main concepts: **Model** (application state implementing `init()`, `update()`, and `view()` methods), **Message** (events like key presses or timers), and **Command** (side effects that produce messages). This architecture ensures predictable state management and clean separation between business logic and rendering. TUI4J also ports several Charm ecosystem libraries including Bubbles (UI components), Lipgloss (styling), and Harmonica (spring physics animation).
## Installation
Add TUI4J to your project using Maven or Gradle to access the complete TUI framework and all ported components.
```xml
com.williamcallahan
tui4j
0.3.0-PREVIEW
```
```groovy
// Gradle
implementation 'com.williamcallahan:tui4j:0.3.0-PREVIEW'
```
## Model Interface
The `Model` interface is the foundation of every TUI4J application. It defines the contract for application state and behavior through three methods: `init()` returns an initial command to run at startup, `update()` handles incoming messages and returns the next state, and `view()` renders the current state as a string for display.
```java
import com.williamcallahan.tui4j.compat.bubbletea.*;
import com.williamcallahan.tui4j.compat.lipgloss.Style;
import com.williamcallahan.tui4j.compat.lipgloss.color.Color;
public class CounterApp implements Model {
private int count = 0;
@Override
public Command init() {
// Return null for no startup command, or a Command for initial side effects
return null;
}
@Override
public UpdateResult extends Model> update(Message msg) {
if (msg instanceof KeyPressMessage keyMsg) {
return switch (keyMsg.key()) {
case "up", "k" -> {
count++;
yield UpdateResult.from(this);
}
case "down", "j" -> {
count--;
yield UpdateResult.from(this);
}
case "q", "ctrl+c" -> UpdateResult.from(this, QuitMessage::new);
default -> UpdateResult.from(this);
};
}
return UpdateResult.from(this);
}
@Override
public String view() {
Style highlight = Style.newStyle().foreground(Color.color("205")).bold(true);
return String.format("""
Counter: %s
Press ā/k to increment, ā/j to decrement
Press q to quit
""", highlight.render(String.valueOf(count)));
}
}
```
## Program
The `Program` class runs the TUI event loop, managing terminal initialization, input handling, and rendering. It takes control of the terminal when `run()` is called and releases it when the model returns a `QuitMessage`. Program supports various options for mouse input, alternate screen mode, and focus reporting.
```java
import com.williamcallahan.tui4j.compat.bubbletea.Program;
public class Main {
public static void main(String[] args) {
// Basic usage
Program program = new Program(new CounterApp());
program.run();
// With options for fullscreen and mouse support
Program fullscreenProgram = new Program(new MyApp())
.withAltScreen() // Use alternate screen buffer
.withMouseAllMotion() // Enable mouse tracking
.withMouseClicks() // Enable click detection
.withReportFocus(); // Track window focus
fullscreenProgram.run();
// Get final model state after exit
Model finalState = new Program(new MyApp()).runWithFinalModel();
}
}
```
## Command
Commands represent side effects that produce messages. TUI4J provides factory methods for common commands like timers, batching multiple commands, sequential execution, and terminal operations like clipboard access and URL opening.
```java
import com.williamcallahan.tui4j.compat.bubbletea.Command;
import java.time.Duration;
// Timer command - emits a message after delay
Command tickCmd = Command.tick(
Duration.ofSeconds(1),
time -> new TickMessage(time)
);
// Batch multiple commands to run concurrently
Command batchCmd = Command.batch(
() -> new StartLoadingMessage(),
Command.tick(Duration.ofMillis(100), t -> new SpinnerTickMessage())
);
// Sequential commands - run in order
Command seqCmd = Command.sequence(
() -> new Step1Message(),
() -> new Step2Message(),
() -> new Step3Message()
);
// Terminal commands
Command quit = Command.quit();
Command clear = Command.clearScreen();
Command title = Command.setWindowTitle("My TUI App");
Command openBrowser = Command.openUrl("https://github.com/WilliamAGH/tui4j");
Command copy = Command.copyToClipboard("Hello, clipboard!");
// In your update method:
@Override
public UpdateResult extends Model> update(Message msg) {
if (msg instanceof KeyPressMessage keyMsg && keyMsg.key().equals("t")) {
// Return model with command to execute
return UpdateResult.from(this,
Command.tick(Duration.ofSeconds(1), t -> new TimerExpiredMessage()));
}
return UpdateResult.from(this);
}
```
## Style (Lipgloss)
The `Style` class provides a fluent API for terminal styling including colors, borders, padding, margins, and text alignment. It mirrors the Lipgloss library from the Charm ecosystem.
```java
import com.williamcallahan.tui4j.compat.lipgloss.Style;
import com.williamcallahan.tui4j.compat.lipgloss.Borders;
import com.williamcallahan.tui4j.compat.lipgloss.Position;
import com.williamcallahan.tui4j.compat.lipgloss.color.Color;
import com.williamcallahan.tui4j.compat.lipgloss.color.AdaptiveColor;
// Basic text styling
Style titleStyle = Style.newStyle()
.foreground(Color.color("#FF5733")) // Hex color
.background(Color.color("63")) // ANSI 256 color
.bold(true)
.italic(true)
.underline(true);
String styledTitle = titleStyle.render("Hello, TUI4J!");
// Box with borders, padding, and margins
Style boxStyle = Style.newStyle()
.border(Borders.roundedBorder())
.borderForeground(Color.color("99"))
.padding(1, 2) // vertical, horizontal
.margin(1) // all sides
.width(40)
.align(Position.Center); // horizontal alignment
String box = boxStyle.render("Centered content in a rounded box");
// Adaptive colors (different for light/dark terminals)
AdaptiveColor adaptiveColor = new AdaptiveColor("#000000", "#FFFFFF");
Style adaptiveStyle = Style.newStyle().foreground(adaptiveColor);
// CSS-like shorthand for padding/margin (top, right, bottom, left)
Style complexBox = Style.newStyle()
.padding(1, 2, 1, 2) // top, right, bottom, left
.margin(0, 1, 0, 1)
.maxWidth(80)
.maxHeight(20)
.ellipsis("..."); // truncation indicator
// Get frame dimensions for layout calculations
int horizontalFrame = boxStyle.getHorizontalFrameSize();
int verticalFrame = boxStyle.getVerticalFrameSize();
```
## Join (Layout)
The `Join` class provides utilities for combining multiple styled strings horizontally or vertically, useful for creating complex layouts.
```java
import com.williamcallahan.tui4j.compat.lipgloss.Join;
import com.williamcallahan.tui4j.compat.lipgloss.Position;
import com.williamcallahan.tui4j.compat.lipgloss.Style;
Style leftPanel = Style.newStyle().width(30).border(Borders.normalBorder());
Style rightPanel = Style.newStyle().width(50).border(Borders.normalBorder());
String left = leftPanel.render("Navigation\n- Home\n- Settings\n- Help");
String right = rightPanel.render("Main Content\n\nWelcome to the application!");
// Horizontal join with top alignment
String layout = Join.joinHorizontal(Position.Top, left, right);
// Vertical join with center alignment
String header = Style.newStyle().bold(true).render("Application Title");
String body = Join.joinHorizontal(Position.Top, left, right);
String footer = Style.newStyle().faint(true).render("Press q to quit");
String fullLayout = Join.joinVertical(Position.Center, header, body, footer);
```
## TextInput
The `TextInput` component provides single-line text input with cursor movement, suggestions, placeholder text, and password mode support.
```java
import com.williamcallahan.tui4j.compat.bubbles.textinput.TextInput;
import com.williamcallahan.tui4j.compat.bubbles.textinput.EchoMode;
public class LoginForm implements Model {
private TextInput usernameInput;
private TextInput passwordInput;
private int focusIndex = 0;
public LoginForm() {
usernameInput = new TextInput();
usernameInput.setPlaceholder("Enter username");
usernameInput.setWidth(30);
usernameInput.setCharLimit(50);
usernameInput.focus();
passwordInput = new TextInput();
passwordInput.setPlaceholder("Enter password");
passwordInput.setEchoMode(EchoMode.EchoPassword);
passwordInput.setEchoCharacter('*');
passwordInput.setWidth(30);
// Optional: autocomplete suggestions
usernameInput.setSuggestions(new String[]{"admin", "user", "guest"});
usernameInput.setShowSuggestions(true);
}
@Override
public Command init() {
return TextInput::blink; // Start cursor blinking
}
@Override
public UpdateResult extends Model> update(Message msg) {
if (msg instanceof KeyPressMessage keyMsg) {
if ("tab".equals(keyMsg.key())) {
// Switch focus between inputs
if (focusIndex == 0) {
usernameInput.blur();
passwordInput.focus();
focusIndex = 1;
} else {
passwordInput.blur();
usernameInput.focus();
focusIndex = 0;
}
return UpdateResult.from(this);
}
if ("enter".equals(keyMsg.key())) {
// Submit form
String username = usernameInput.value();
String password = passwordInput.value();
// Process login...
return UpdateResult.from(this, QuitMessage::new);
}
}
// Update the focused input
if (focusIndex == 0) {
usernameInput.update(msg);
} else {
passwordInput.update(msg);
}
return UpdateResult.from(this);
}
@Override
public String view() {
return String.format("""
Login
Username: %s
Password: %s
Tab to switch fields, Enter to submit
""", usernameInput.view(), passwordInput.view());
}
}
```
## Textarea
The `Textarea` component provides multi-line text editing with cursor movement, line numbers, and scrolling support.
```java
import com.williamcallahan.tui4j.compat.bubbles.textarea.Textarea;
import com.williamcallahan.tui4j.compat.lipgloss.Style;
import com.williamcallahan.tui4j.compat.lipgloss.color.Color;
public class EditorApp implements Model {
private Textarea textarea;
public EditorApp() {
textarea = new Textarea();
textarea.setPlaceholder("Start typing...");
textarea.setWidth(60);
textarea.setHeight(10);
textarea.setShowLineNumbers(true);
// Custom styling
Textarea.Style style = new Textarea.Style()
.lineNumber(Style.newStyle().foreground(Color.color("240")))
.cursorLine(Style.newStyle().background(Color.color("236")))
.placeholder(Style.newStyle().foreground(Color.color("240")));
textarea.setStyle(style);
textarea.focus();
}
@Override
public Command init() {
return textarea::init;
}
@Override
public UpdateResult extends Model> update(Message msg) {
if (msg instanceof KeyPressMessage keyMsg && "esc".equals(keyMsg.key())) {
return UpdateResult.from(this, QuitMessage::new);
}
UpdateResult extends Model> result = textarea.update(msg);
return UpdateResult.from(this, result.command());
}
@Override
public String view() {
return String.format("Editor\n\n%s\n\nPress ESC to exit", textarea.view());
}
}
```
## Viewport
The `Viewport` component provides a scrollable view for content that exceeds the available screen space.
```java
import com.williamcallahan.tui4j.compat.bubbles.viewport.Viewport;
import com.williamcallahan.tui4j.compat.lipgloss.Style;
import com.williamcallahan.tui4j.compat.lipgloss.Borders;
public class PagerApp implements Model {
private Viewport viewport;
private String title = "README.md";
public PagerApp(String content) {
viewport = new Viewport(content, 80, 20);
viewport.setStyle(Style.newStyle().border(Borders.roundedBorder()));
}
@Override
public Command init() {
return null;
}
@Override
public UpdateResult extends Model> update(Message msg) {
if (msg instanceof KeyPressMessage keyMsg) {
switch (keyMsg.key()) {
case "q" -> { return UpdateResult.from(this, QuitMessage::new); }
case "g" -> viewport.gotoTop();
case "G" -> viewport.gotoBottom();
}
}
if (msg instanceof WindowSizeMessage sizeMsg) {
viewport.setWidth(sizeMsg.width());
viewport.setHeight(sizeMsg.height() - 3);
}
viewport.update(msg);
return UpdateResult.from(this);
}
@Override
public String view() {
double percent = viewport.scrollPercent() * 100;
String footer = String.format(" %s - %.0f%% ", title, percent);
return viewport.view() + "\n" + footer;
}
}
```
## Table
The `Table` component displays tabular data with column headers, keyboard navigation, and selection support.
```java
import com.williamcallahan.tui4j.compat.bubbles.table.*;
public class DataTableApp implements Model {
private Table table;
public DataTableApp() {
table = Table.create()
.columns(
new Column("ID", 8),
new Column("Name", 20),
new Column("Status", 12),
new Column("Priority", 10)
)
.rows(
new Row("001", "Setup database", "Done", "High"),
new Row("002", "Write tests", "In Progress", "Medium"),
new Row("003", "Deploy to prod", "Pending", "High"),
new Row("004", "Update docs", "Pending", "Low"),
new Row("005", "Code review", "In Progress", "Medium")
)
.height(10)
.width(60)
.focused(true);
// Custom styles
table.styles(Styles.defaultStyles()
.header(Style.newStyle().bold(true).foreground(Color.color("99")))
.selected(Style.newStyle().background(Color.color("57")).foreground(Color.color("229")))
);
}
@Override
public Command init() { return null; }
@Override
public UpdateResult extends Model> update(Message msg) {
if (msg instanceof KeyPressMessage keyMsg) {
if ("q".equals(keyMsg.key())) {
return UpdateResult.from(this, QuitMessage::new);
}
if ("enter".equals(keyMsg.key())) {
Row selected = table.selectedRow();
if (selected != null) {
System.out.println("Selected: " + String.join(", ", selected.cells()));
}
}
}
table.update(msg);
return UpdateResult.from(this);
}
@Override
public String view() {
return "Tasks\n\n" + table.view() + "\n\nā/ā navigate, Enter select, q quit";
}
}
```
## List
The `List` component displays filterable, paginated lists with support for both static arrays and dynamic data sources.
```java
import com.williamcallahan.tui4j.compat.bubbles.list.*;
// Define items using the DefaultItem interface
record Task(String title, String description) implements DefaultItem {
@Override
public String filterValue() {
return title(); // Used for filtering
}
}
public class TaskListApp implements Model {
private List taskList;
public TaskListApp() {
Item[] items = {
new Task("Buy groceries", "Milk, bread, eggs"),
new Task("Call dentist", "Schedule annual checkup"),
new Task("Finish report", "Q4 sales analysis"),
new Task("Walk dog", "30 minute evening walk"),
new Task("Review PRs", "Check pending pull requests")
};
taskList = new List(items, 50, 15); // width, height
taskList.setTitle("My Tasks");
taskList.setShowTitle(true);
taskList.setShowFilter(true);
taskList.setShowStatusBar(true);
taskList.setShowPagination(true);
taskList.setFilteringEnabled(true);
}
@Override
public Command init() { return null; }
@Override
public UpdateResult extends Model> update(Message msg) {
if (msg instanceof KeyPressMessage keyMsg && "q".equals(keyMsg.key())) {
return UpdateResult.from(this, QuitMessage::new);
}
UpdateResult extends Model> result = taskList.update(msg);
return UpdateResult.from(this, result.command());
}
@Override
public String view() {
return taskList.view();
}
}
```
## Spinner
The `Spinner` component provides animated loading indicators with multiple built-in styles.
```java
import com.williamcallahan.tui4j.compat.bubbles.spinner.*;
public class LoadingApp implements Model {
private Spinner spinner;
private boolean loading = true;
public LoadingApp() {
spinner = new Spinner(SpinnerType.Dots); // or Line, MiniDot, Jump, etc.
spinner.setStyle(Style.newStyle().foreground(Color.color("205")));
}
@Override
public Command init() {
return spinner.init(); // Start spinner animation
}
@Override
public UpdateResult extends Model> update(Message msg) {
if (msg instanceof KeyPressMessage keyMsg && "q".equals(keyMsg.key())) {
return UpdateResult.from(this, QuitMessage::new);
}
// Update spinner for animation frames
UpdateResult result = spinner.update(msg);
spinner = result.model();
return UpdateResult.from(this, result.command());
}
@Override
public String view() {
if (loading) {
return spinner.view() + " Loading data...";
}
return "Done!";
}
}
```
## Progress
The `Progress` component displays progress bars with optional gradient colors and percentage text.
```java
import com.williamcallahan.tui4j.compat.bubbles.progress.Progress;
public class DownloadApp implements Model {
private Progress progress;
private double percent = 0.0;
public DownloadApp() {
progress = new Progress()
.withWidth(50)
.withDefaultGradient() // Gradient from purple to pink
.withShowPercentage(true)
.withPercentFormat(" %.1f%%");
// Or custom gradient
// progress.withGradient("#5A56E0", "#EE6FF8");
// Or solid color
// progress.withFullColor("#7571F9").withEmptyColor("#606060");
}
@Override
public Command init() {
return () -> new ProgressTickMessage();
}
@Override
public UpdateResult extends Model> update(Message msg) {
if (msg instanceof ProgressTickMessage) {
percent += 0.05;
if (percent >= 1.0) {
return UpdateResult.from(this, QuitMessage::new);
}
progress.setPercent(percent);
return UpdateResult.from(this,
Command.tick(Duration.ofMillis(100), t -> new ProgressTickMessage()));
}
return UpdateResult.from(this);
}
@Override
public String view() {
return String.format("Downloading...\n\n%s", progress.view());
}
}
record ProgressTickMessage() implements Message {}
```
## Timer and Stopwatch
The `Timer` and `Stopwatch` bubbles provide countdown and elapsed time functionality.
```java
import com.williamcallahan.tui4j.compat.bubbles.timer.Timer;
import com.williamcallahan.tui4j.compat.bubbles.stopwatch.Stopwatch;
import java.time.Duration;
public class TimerApp implements Model {
private Timer timer;
private Stopwatch stopwatch;
public TimerApp() {
// Countdown timer
timer = new Timer(Duration.ofMinutes(5));
// Elapsed time stopwatch
stopwatch = new Stopwatch();
}
@Override
public Command init() {
return Command.batch(timer.init(), stopwatch.init());
}
@Override
public UpdateResult extends Model> update(Message msg) {
if (msg instanceof KeyPressMessage keyMsg) {
switch (keyMsg.key()) {
case "s" -> timer.toggle(); // Start/stop timer
case "r" -> timer.reset(); // Reset timer
case "w" -> stopwatch.toggle(); // Start/stop stopwatch
case "q" -> { return UpdateResult.from(this, QuitMessage::new); }
}
}
// Update both components
UpdateResult extends Model> timerResult = timer.update(msg);
UpdateResult extends Model> stopwatchResult = stopwatch.update(msg);
return UpdateResult.from(this,
Command.batch(timerResult.command(), stopwatchResult.command()));
}
@Override
public String view() {
return String.format("""
Timer: %s %s
Stopwatch: %s %s
s: toggle timer, r: reset timer
w: toggle stopwatch, q: quit
""",
timer.view(), timer.running() ? "(running)" : "(stopped)",
stopwatch.view(), stopwatch.running() ? "(running)" : "(stopped)");
}
}
```
## Help
The `Help` component displays keyboard shortcuts with customizable styling.
```java
import com.williamcallahan.tui4j.compat.bubbles.help.Help;
import com.williamcallahan.tui4j.compat.bubbles.key.Binding;
public class HelpfulApp implements Model {
private Help help;
// Define key bindings
private final Binding quitKey = new Binding("q", "quit");
private final Binding upKey = new Binding("k", "up", "move up");
private final Binding downKey = new Binding("j", "down", "move down");
private final Binding enterKey = new Binding("enter", "select");
public HelpfulApp() {
help = new Help();
help.setShowAll(true); // Show all bindings, not just short form
}
@Override
public String view() {
// Render help at the bottom of your view
return content + "\n\n" + help.view(
java.util.List.of(upKey, downKey, enterKey, quitKey)
);
}
}
```
## Window Size Handling
Handle terminal resize events to make your application responsive using the `WindowSizeMessage`.
```java
import com.williamcallahan.tui4j.compat.bubbletea.WindowSizeMessage;
public class ResponsiveApp implements Model {
private int width = 80;
private int height = 24;
@Override
public UpdateResult extends Model> update(Message msg) {
if (msg instanceof WindowSizeMessage sizeMsg) {
this.width = sizeMsg.width();
this.height = sizeMsg.height();
// Resize your components accordingly
viewport.setWidth(width);
viewport.setHeight(height - 3); // Leave room for header/footer
}
return UpdateResult.from(this);
}
@Override
public String view() {
Style container = Style.newStyle()
.width(width)
.height(height);
return container.render(content);
}
}
```
## Mouse Support
Enable mouse input for click detection and tracking using Program options and mouse messages.
```java
import com.williamcallahan.tui4j.compat.bubbletea.input.MouseMessage;
import com.williamcallahan.tui4j.compat.bubbletea.input.MouseButton;
import com.williamcallahan.tui4j.compat.bubbletea.input.MouseAction;
public class MouseApp implements Model {
private int clickX = 0;
private int clickY = 0;
public static void main(String[] args) {
new Program(new MouseApp())
.withMouseCellMotion() // Track mouse movement between cells
.withMouseClicks() // Enable click detection
.run();
}
@Override
public UpdateResult extends Model> update(Message msg) {
if (msg instanceof MouseMessage mouseMsg) {
if (mouseMsg.getAction() == MouseAction.MouseActionPress &&
mouseMsg.getButton() == MouseButton.MouseButtonLeft) {
clickX = mouseMsg.column();
clickY = mouseMsg.row();
}
}
return UpdateResult.from(this);
}
@Override
public String view() {
return String.format("Last click: (%d, %d)\nClick anywhere...", clickX, clickY);
}
}
```
## Summary
TUI4J provides a comprehensive toolkit for building terminal user interfaces in Java. The main use cases include interactive CLI tools with menus and forms, data dashboards with tables and progress indicators, text editors and file browsers, and terminal-based games. The framework excels at applications requiring rich user interaction, real-time updates, and polished terminal aesthetics. By following The Elm Architecture, applications remain maintainable with predictable state management and clear separation of concerns.
Integration with existing Java applications is straightforward since TUI4J runs in its own event loop and can be embedded into larger systems. For Spring Boot integration, see the examples in `src/main/resources/examples/spring`. The compatibility with Bubble Tea's API makes it easy to port existing Go terminal applications to Java or to follow Bubble Tea tutorials while developing in Java. The extensive component library (viewport, textarea, table, list, progress, spinner, timer, etc.) covers most common TUI patterns, while the Lipgloss styling system provides fine-grained control over visual presentation.