# 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 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 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 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 update(Message msg) { if (msg instanceof KeyPressMessage keyMsg && "esc".equals(keyMsg.key())) { return UpdateResult.from(this, QuitMessage::new); } UpdateResult 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 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 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 update(Message msg) { if (msg instanceof KeyPressMessage keyMsg && "q".equals(keyMsg.key())) { return UpdateResult.from(this, QuitMessage::new); } UpdateResult 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 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 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 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 timerResult = timer.update(msg); UpdateResult 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 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 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.