Hey Mr. TamboU(r)Ine man, create a TUI for me

Press any key to start…​#️⃣ β˜•

Hey Mr. TamboU(r)Ine Man

Hey Mr. TamboU(r)Ine man, create a TUI for me

In the jingle jangle JVM I’ll come followin' you

About Me

  • πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ πŸŠβ€β™‚οΈ 🚴 πŸƒ

  • Software Architect @adesso SE

  • JUG Paderborn Co-Organizer

  • JHipster Board Member

duke pb jhipster

Agenda

  • Java, in the terminal?

  • TamboUI

  • Api Levels

  • Features

  • Samples

  • Shipping & Conclusion

Java, in the terminal?

Where do developers live?

Movement towards the terminal

  • (AI) development tools

  • Keyboard-first workflows

  • Windows has a real terminal now

  • Lightweight (hopefully)

  • Fast feedback

Java, our beloved language is everywhere…​

  • βœ… Server Backends

  • βœ… Web Frontends

  • βœ… Cloud

  • βœ… Mobile

  • βœ… Agents

  • β›” Terminal Interfaces

…​except in the terminal

  • Go has Bubble Tea[1]

  • Python has Textual[2]

  • Rust has Ratatui[3]

  • Java has …​ πŸ«™

Pride and Prejudice

  • Java is too slow to start

  • Too much ceremony

  • Distribution is a nightmare

  • Too verbose!

Modern Java

  • Java is too slow to start! Java is fast, GraalVM even faster

  • Too much ceremony! JBang anyone?

  • Distribution is a nightmare! JReleaser?

  • Too verbose! Modern Java says no (or kotlin)

jbang demos@tamboui

TamboUI

TamboUI brings the TUI paradigms seen in things like Rust’s ratatui or Go’s bubbletea to the Java ecosystem. TamboUI is pronounced like "tambouille" (\tΙ‘Μƒ.buj) in French, or "tan-bouy". It provides a comprehensive set of widgets and a layout system for building rich terminal applications with modern Java idioms.

— github.com/tamboui/tamboui
TamboUI is still experimental and under active development.

API Levels

APIResponsibilityUse

Toolkit

Declarative components, focus and event routing

Building most applications

TuiRunner

Managed event loop and terminal lifecycle

Custom event handling without boilerplate

Immediate Mode

Direct rendering and complete control

Maximum control and speed or you want to create new widgets

Backends

ModuleDescriptionRequires

tamboui-jline3-backend

Reliable default on all platforms[1]

Java 8+

tamboui-panama-backend

Panama (FFM) without third-party dependencies

Java 22+

tamboui-aesh-backend

Aesh[2] based, for SSH/Browser-hosted terminals

Java 8+

How do rendering work?

Input → Event or Action → State → Render → Buffer diff → Terminal

Dig deeper[1]

var buffer = Buffer.empty(new Rect(0,0,80,24));
buffer.set(0, 0, new Cell("H", Style.EMPTY.fg(Color.RED).bold()));
var area = new Rect(10, 5, 60, 14)

One counter. Three implementations.

immediate counter

Level 1: Immediate Mode

  • Take care of the event loop

  • Read terminal events

  • Render widgets into the Buffer

  • You can control every detail

With great power comes great responsibility

— Uncle Ben to Peter Parker

Level 1: Immediate Mode

try (var terminal = new Terminal<>(new PanamaBackend())) {
    var backend = terminal.backend();
    backend.enableRawMode(); backend.enterAlternateScreen();
    backend.hideCursor();
    // ...
    while(running){
        terminal.draw(frame -> {
            frame.renderWidget(widget, frame.area());
        });
        int input = backend.read(100);
        switch (input) {
            // ...
            case 3:   // Ctrl+C in raw mode
            case -1:  // End of input
                running = false; break;
}}} // whole sample 100 LOC

Level 2: TuiRunner

  • Managed event loop

  • Animation ticks

  • Resize/redraw

  • Thread-safe runOnRenderThread

Level 2: TuiRunner

public class TuiRunnerCounter {
    private int counter, ticks = 0;

    public void run() throws Exception {
        var config = TuiConfig.builder().tickRate(Duration.ofMillis(100)).build();
        try (var tui = TuiRunner.create(config)) {
            tui.run(this::handleEvent, this::render);
        }
    }
    private boolean handleEvent(Event event, TuiRunner runner) { /**...**/ }
    private void render(Frame frame) { /** ... **/}
} // whole sample 50 LOC

Level 3: Toolkit

  • Declarative element tree

  • Event routing

  • Action handlers

  • A lot of interface methods to overwrite → structure

Level 3: Toolkit

public class ToolkitCounter extends ToolkitApp {
    @Override
    protected TuiConfig configure() {
        return TuiConfig.builder().tickRate(Duration.ofMillis(100)).build();
    }
    @Override
    protected Element render() {
        return panel(/*...*/).rounded().id("counter").focusable()
            .onKeyEvent(event -> {
                if (event.isChar('+')) {
                    counter++;
                    return EventResult.HANDLED; }
                // ...
                // Leave q unhandled so ToolkitRunner performs its standard quit handling.
                return EventResult.UNHANDLED;
            });
}}

Features - more than just green text

  • Responsive layouts

  • Keyboard and mouse input

  • Unicode, emoji

  • TCSS styling

  • Markdown/Text streaming

  • Images, visualizations

  • Testing

More than Single Files

  • Widgets (there are a lot of)

  • Layout

  • Classic MVC (Swing anyone?)

MVC, what was that?

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     reads      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Controller β”‚ ◄──────────────│    View     β”‚
β”‚   (State)   β”‚                β”‚  (Element)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β–²                              β”‚
       β”‚         dispatches           β”‚
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                 events

Samples

  • Build some small applications during the last weeks

  • Different features used

Quakus TUI

  • Forms and editable fields

  • Search and filtered lists

  • Focus and keyboard navigation

  • Background network or file operations

Quarkus TUI

@Override
protected void onStart() {
    reloadCatalog();
}
private void postToUi(Runnable action) {
    runner().runOnRenderThread(action);
}
//...
Executors.newVirtualThreadPerTaskExecutor().submit(() -> {
    Catalog catalog = api.loadCatalog();
    postToUi(() -> { presets = catalog.presets(); });
    }
});

Quarkus TUI

var groupId = new TextInputState("org.acme");
var buildToolState = new SelectFieldState(BuildTool.MAVEN.label, BuildTool.GRADLE_KOTLIN_DSL.label, BuildTool.GRADLE.label);

ListElement<?> presetList = list()
    .autoScroll().scrollbar()
    .highlightColor(Color.CYAN).highlightSymbol("β€Ί ")
    .id("catalog-list").focusable().fill();
//...
textInput(groupId).id("group-id").title("Group ID")
formField("Build tool", buildToolState).id("build-tool")
//...
presetList.items(presets)

Ollama Chat

  • Streaming markdown

  • Layouts

var layout = Toolkit.dock()
    .top(header, Toolkit.length(3))
    .center(conversation)
    .bottom(Toolkit.column().add(input).add(footer), Toolkit.length(4));
// Handles partial markdown gracefully,
// sanitize and trim before parse,
// stream of markdown can be displayed safely on every frame
// NEW 0.5.0: can also highlight source code
var view = MarkdownView.builder().source(source)

Fitpub TUI

FitPub is a free and open platform for sports and outdoor activities. Track your adventures, share your experiences, and connect with other active people. Built on open standards and powered by ActivityPub, FitPub is part of the Fediverse – a network of independent platforms that communicate with each other.

— https://fitpub.social/about
  • There is no official public API, so the TUI will break eventually (for now)

Fitpub TUI

  • Stylesheets (.tcss)

  • Main/Detail View

  • More lists

  • Graphs

  • More event handling

Stylesheets

var engine = StyleEngine.create();
engine.loadStylesheet("latte", "/themes/catppuccin-latte.tcss");
// ...
Toolkit.text(selected.displayName() + "  @" + selected.username())
.addClass("athlete")
// ...
theme = theme.next(); // simple wrapper around different theme names/stylesheets
styleEngine.setActiveStylesheet(theme.stylesheetName());
$mauve: #8839ef;
.athlete {
    color: $mauve;
}

Lists

// Maybe updated and postedOnRenderThread to load more activities
List<Activity> activities = controller.activities();

ListElement<Activity> activityList = list()
    .data(activities, activity -> activityRow(activity, width))
    .selected(controller.selectedIndex())
    .autoScroll()
    .scrollbar();

// Events handled in controller to track selection

Graphs

var dataset = Dataset.builder().data(points)
    .graphType(GraphType.BAR)
    .style(dataColor).build();

return chart().dataset(dataset)
    .xAxis(Axis.builder().bounds(0, original.size() - 1).build())
    .yAxis(Axis.builder().bounds(min, max).labels(minLabel, maxLabel).build())
    .hideLegend()
    .title(label + " Β· " + Formats.range(original, unit))
    .addClass(cssClass)
    .fill();

Lessons Learned

  • Minimize allocations in render

  • Use primitive arrays over collections

  • Keep state classes simple

  • Methods for modification

  • Immutable state/records for thread safety

  • Get to know the existing widget library (!!)

What’s missing?

  • Tests

  • Debugging

  • Distribution

Testing

  • Controllers/State classes with plain unit tests

  • Pilot testing for integration tests[1]

  • Record and playback for end-2-end like verifications[2]

Pilot

@Test
void keyboardShortcutsUpdateApplicationState() throws Exception {
    var app = new CounterApp();

    try (var test = ToolkitTestRunner.runTest(app::render)) {
        Pilot pilot = test.pilot();

        assertTrue(pilot.hasElement("counter-value"));

        pilot.press('+');
        pilot.pause();
        assertEquals(1, app.count());
    }
}

Debugging

  • Maybe the main "pain point" 😭

  • Most IDEs do not have a real terminal (IDEA, Eclipse)

  • VS Code worked best with least issues

Distribution

  • Run on the JVM

  • Launch examples with JBang

  • Build a GraalVM native executable

  • Publish with JReleaser[1]

  • Keep the terminal installation experience simple

Conclusion

  • A TUI is a user interface - not a fancy println, more than a CLI

  • Start with the Toolkit and move lower when necessary

  • Java now belongs in the modern terminal

  • TamboUI has a rich set of widgets to create nice and fast TUIs with ease

Create a TUI for me

jbang demos@tamboui
  • Samples ➑️

  • tamboui.dev

  • atomfrede.github.io/tamboui-jfn26-slides

  • github.com/atomfrede/tamboui-jfn26-samples

200

Questions

  • Questions⁉️

  • Slides ➑️

250