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.
TUI4J provides comprehensive keyboard and mouse input handling with support for all key types, modifiers, mouse buttons, motion tracking, and advanced features like click detection and selection tracking.
Keyboard input is delivered through KeyPressMessage:
package com.williamcallahan.tui4j.compat.bubbletea;
class KeyPressMessage implements Message {
Key key();
}The Key record represents a keyboard key with type, character data, and modifiers.
package com.williamcallahan.tui4j.compat.bubbletea.input.key;
record Key(KeyType type, char[] runes, boolean alt) {
// Constructors
Key(KeyType type);
Key(KeyType type, char[] runes);
Key(KeyType type, boolean alt);
Key(KeyType type, char[] runes, boolean alt);
}Usage:
@Override
public UpdateResult<? extends Model> update(Message msg) {
if (msg instanceof KeyPressMessage keyMsg) {
Key key = keyMsg.key();
// Check key type
if (key.type() == KeyType.KeyEnter) {
return UpdateResult.from(this.submit());
}
// Check for specific character
if (key.type() == KeyType.KeyRunes && key.runes().length > 0) {
char ch = key.runes()[0];
if (ch == 'q') {
return UpdateResult.from(this, Command.quit());
}
}
// Check for Alt modifier
if (key.alt()) {
// Handle Alt+key
}
}
return UpdateResult.from(this);
}Enumeration of all keyboard key types.
package com.williamcallahan.tui4j.compat.bubbletea.input.key;
enum KeyType {
// Control keys
KeyCtrlC, KeyCtrlD, KeyCtrlE, KeyCtrlF, KeyCtrlG, KeyCtrlH,
KeyCtrlJ, KeyCtrlK, KeyCtrlL, KeyCtrlN, KeyCtrlO, KeyCtrlP,
KeyCtrlQ, KeyCtrlR, KeyCtrlS, KeyCtrlT, KeyCtrlU, KeyCtrlV,
KeyCtrlW, KeyCtrlX, KeyCtrlY, KeyCtrlZ,
KeyCtrlBackslash, KeyCtrlRightBracket, KeyCtrlCaret,
KeyCtrlUnderscore, KeyCtrlQuestionMark,
// Navigation keys
KeyUp, KeyDown, KeyLeft, KeyRight,
KeyHome, KeyEnd, KeyPgUp, KeyPgDown,
// Editing keys
KeyDelete, KeyBackspace, KeyInsert,
// Special keys
KeySpace, KeyTab, KeyShiftTab, KeyEnter, KeyEsc,
// Function keys
KeyF1, KeyF2, KeyF3, KeyF4, KeyF5, KeyF6,
KeyF7, KeyF8, KeyF9, KeyF10, KeyF11, KeyF12,
KeyF13, KeyF14, KeyF15, KeyF16, KeyF17, KeyF18,
KeyF19, KeyF20,
// Character input
KeyRunes, // Regular character input (check runes array)
// Unknown
KeyUnknown
}Common Key Checks:
// Control keys
if (key.type() == KeyType.KeyCtrlC) {
return UpdateResult.from(this, Command.quit());
}
// Navigation
if (key.type() == KeyType.KeyUp) {
return UpdateResult.from(this.moveUp());
}
if (key.type() == KeyType.KeyDown) {
return UpdateResult.from(this.moveDown());
}
// Special keys
if (key.type() == KeyType.KeyEnter) {
return UpdateResult.from(this.submit());
}
if (key.type() == KeyType.KeyEsc) {
return UpdateResult.from(this.cancel());
}
if (key.type() == KeyType.KeyTab) {
return UpdateResult.from(this.nextField());
}
// Function keys
if (key.type() == KeyType.KeyF1) {
return UpdateResult.from(this.showHelp());
}
// Character input
if (key.type() == KeyType.KeyRunes && key.runes().length > 0) {
char ch = key.runes()[0];
return UpdateResult.from(this.addCharacter(ch));
}The Kitty keyboard protocol provides enhanced keyboard support with modifier detection for Enter keys.
package com.williamcallahan.tui4j.input.kitty;
final class KittyEnterKeyMappings {
static ProgramOption withKittyEnterKeyMappings();
static EnterKeyModifier parseEnterKeyModifier(String sequence);
}Usage:
// Enable Kitty keyboard protocol for Enter key modifiers
Program program = new Program(
model,
KittyEnterKeyMappings.withKittyEnterKeyMappings()
);
// Handle Enter key with modifiers
@Override
public UpdateResult<? extends Model> update(Message msg) {
if (msg instanceof EnterKeyModifierMessage enterMsg) {
switch (enterMsg.modifier()) {
case Normal -> handleEnter();
case Alt -> handleAltEnter();
case Shift -> handleShiftEnter();
case Ctrl -> handleCtrlEnter();
case CtrlShift -> handleCtrlShiftEnter();
}
}
return UpdateResult.from(this);
}See Kitty Keyboard Protocol Messages for message definitions.
Mouse events are delivered through MouseMessage:
package com.williamcallahan.tui4j.compat.bubbletea.input;
class MouseMessage implements Message {
// Position
int column();
int row();
// Button and action
MouseButton getButton();
MouseAction getAction();
// Modifiers
boolean isShift();
boolean isAlt();
boolean isCtrl();
// Utilities
boolean isWheel();
String describe();
// Parsing (for low-level use)
static MouseMessage parseX10MouseEvent(int col, int row, int button);
static MouseMessage parseSGRMouseEvent(int button, int col, int row, boolean release);
}Usage:
@Override
public UpdateResult<? extends Model> update(Message msg) {
if (msg instanceof MouseMessage mouse) {
int col = mouse.column();
int row = mouse.row();
// Handle click
if (mouse.getAction() == MouseAction.MouseActionPress &&
mouse.getButton() == MouseButton.MouseButtonLeft) {
return UpdateResult.from(this.handleClick(col, row));
}
// Handle wheel
if (mouse.isWheel()) {
if (mouse.getButton() == MouseButton.MouseButtonWheelUp) {
return UpdateResult.from(this.scrollUp());
}
if (mouse.getButton() == MouseButton.MouseButtonWheelDown) {
return UpdateResult.from(this.scrollDown());
}
}
// Handle drag with Shift
if (mouse.getAction() == MouseAction.MouseActionMotion && mouse.isShift()) {
return UpdateResult.from(this.extendSelection(col, row));
}
}
return UpdateResult.from(this);
}Enumeration of mouse button types.
package com.williamcallahan.tui4j.compat.bubbletea.input;
enum MouseButton {
MouseButtonNone,
MouseButtonLeft,
MouseButtonMiddle,
MouseButtonRight,
MouseButtonWheelUp,
MouseButtonWheelDown,
MouseButtonWheelLeft,
MouseButtonWheelRight,
MouseButtonBackward,
MouseButtonForward
}Enumeration of mouse action types.
package com.williamcallahan.tui4j.compat.bubbletea.input;
enum MouseAction {
MouseActionPress,
MouseActionRelease,
MouseActionMotion
}TUI4J provides additional mouse utilities beyond the core Bubble Tea framework.
Enhanced click message with double-click detection.
package com.williamcallahan.tui4j.input;
record MouseClickMessage(MouseMessage mouse, int clickCount) implements Message { }The clickCount field indicates:
1 - Single click2 - Double click3+ - Triple click or moreTracks mouse clicks and detects double-clicks.
package com.williamcallahan.tui4j.input;
class MouseClickTracker {
MouseClickTracker();
MouseClickMessage track(MouseMessage msg);
}Usage:
class MyModel implements Model {
private final MouseClickTracker clickTracker = new MouseClickTracker();
@Override
public UpdateResult<? extends Model> update(Message msg) {
if (msg instanceof MouseMessage mouse) {
MouseClickMessage click = clickTracker.track(mouse);
if (click.clickCount() == 2) {
// Handle double-click
return UpdateResult.from(this.onDoubleClick(click.mouse()));
}
}
return UpdateResult.from(this);
}
}Rectangular boundary for hit testing.
package com.williamcallahan.tui4j.input;
record MouseBounds(int x, int y, int width, int height) {
boolean contains(int col, int row);
}Clickable target region with identifier and data.
package com.williamcallahan.tui4j.input;
record MouseTarget(MouseBounds bounds, String id, Object data) { }Interface for providing clickable targets.
package com.williamcallahan.tui4j.input;
interface MouseTargetProvider {
List<MouseTarget> getTargets();
}Usage:
class MyModel implements Model, MouseTargetProvider {
@Override
public List<MouseTarget> getTargets() {
return List.of(
new MouseTarget(new MouseBounds(0, 0, 10, 1), "button1", null),
new MouseTarget(new MouseBounds(0, 2, 10, 1), "button2", null)
);
}
@Override
public UpdateResult<? extends Model> update(Message msg) {
if (msg instanceof MouseMessage mouse) {
MouseTarget target = MouseTargets.findTarget(
mouse.column(),
mouse.row(),
getTargets()
);
if (target != null) {
return UpdateResult.from(this.handleTarget(target.id()));
}
}
return UpdateResult.from(this);
}
}Utilities for mouse target management.
package com.williamcallahan.tui4j.input;
class MouseTargets {
static MouseTarget findTarget(int x, int y, List<MouseTarget> targets);
}Mouse cursor types.
package com.williamcallahan.tui4j.input;
enum MouseCursor {
Text, // Text cursor (I-beam)
Pointer, // Pointer/hand cursor
Default // Default cursor
}Set cursor using commands:
Command.setMouseCursorText()
Command.setMouseCursorPointer()
Command.resetMouseCursor()Detects when mouse hovers over text regions.
package com.williamcallahan.tui4j.input;
class MouseHoverTextDetector {
MouseHoverTextDetector();
MouseCursor detect(MouseMessage msg, MouseTargetProvider provider);
}Usage:
MouseHoverTextDetector detector = new MouseHoverTextDetector();
@Override
public UpdateResult<? extends Model> update(Message msg) {
if (msg instanceof MouseMessage mouse) {
MouseCursor cursor = detector.detect(mouse, this);
Command cmd = switch (cursor) {
case Text -> Command.setMouseCursorText();
case Pointer -> Command.setMouseCursorPointer();
case Default -> Command.resetMouseCursor();
};
return UpdateResult.from(this, cmd);
}
return UpdateResult.from(this);
}Tracks mouse selection regions (drag selection).
package com.williamcallahan.tui4j.input;
class MouseSelectionTracker {
MouseSelectionTracker();
MouseSelectionUpdate track(MouseMessage msg);
boolean hasSelection();
void clear();
}Result of selection tracking.
package com.williamcallahan.tui4j.input;
record MouseSelectionUpdate(
int startX,
int startY,
int endX,
int endY,
boolean active
) { }Usage:
class MyModel implements Model {
private final MouseSelectionTracker selectionTracker = new MouseSelectionTracker();
@Override
public UpdateResult<? extends Model> update(Message msg) {
if (msg instanceof MouseMessage mouse) {
MouseSelectionUpdate selection = selectionTracker.track(mouse);
if (selection.active()) {
// Selection is being made
return UpdateResult.from(this.withSelection(
selection.startX(), selection.startY(),
selection.endX(), selection.endY()
));
}
}
return UpdateResult.from(this);
}
}Automatic scrolling during mouse selection.
package com.williamcallahan.tui4j.input;
class MouseSelectionAutoScroller {
MouseSelectionAutoScroller(int topMargin, int bottomMargin);
int calculateScrollDelta(MouseMessage msg, int viewportHeight);
}Usage:
MouseSelectionAutoScroller scroller = new MouseSelectionAutoScroller(2, 2);
@Override
public UpdateResult<? extends Model> update(Message msg) {
if (msg instanceof MouseMessage mouse) {
int delta = scroller.calculateScrollDelta(mouse, viewportHeight);
if (delta != 0) {
return UpdateResult.from(this.scroll(delta));
}
}
return UpdateResult.from(this);
}Mouse support must be enabled through Program options:
// Cell motion (motion when button pressed)
Program program = new Program(
model,
Program.withMouseCellMotion()
);
// All motion (motion even without button)
Program program = new Program(
model,
Program.withMouseAllMotion()
);
// Click detection
Program program = new Program(
model,
Program.withMouseClicks(2) // Enable up to double-clicks
);
// Selection and auto-scroll
Program program = new Program(
model,
Program.withMouseSelectionExtendOnScroll(this::handleSelection),
Program.withMouseSelectionAutoScroll(2, 2, this::handleSelection)
);@Override
public UpdateResult<? extends Model> update(Message msg) {
if (msg instanceof KeyPressMessage keyMsg) {
Key key = keyMsg.key();
return switch (key.type()) {
case KeyUp -> UpdateResult.from(this.moveUp());
case KeyDown -> UpdateResult.from(this.moveDown());
case KeyLeft -> UpdateResult.from(this.moveLeft());
case KeyRight -> UpdateResult.from(this.moveRight());
case KeyHome -> UpdateResult.from(this.moveToStart());
case KeyEnd -> UpdateResult.from(this.moveToEnd());
case KeyPgUp -> UpdateResult.from(this.pageUp());
case KeyPgDown -> UpdateResult.from(this.pageDown());
case KeyEnter -> UpdateResult.from(this.select());
case KeyEsc -> UpdateResult.from(this.cancel());
default -> UpdateResult.from(this);
};
}
return UpdateResult.from(this);
}@Override
public UpdateResult<? extends Model> update(Message msg) {
if (msg instanceof KeyPressMessage keyMsg) {
Key key = keyMsg.key();
if (key.type() == KeyType.KeyRunes && key.runes().length > 0) {
// Add character to input
char ch = key.runes()[0];
return UpdateResult.from(this.addChar(ch));
}
if (key.type() == KeyType.KeyBackspace) {
return UpdateResult.from(this.deleteChar());
}
if (key.type() == KeyType.KeyDelete) {
return UpdateResult.from(this.deleteCharForward());
}
}
if (msg instanceof PasteMessage paste) {
return UpdateResult.from(this.insertText(paste.text()));
}
return UpdateResult.from(this);
}@Override
public UpdateResult<? extends Model> update(Message msg) {
if (msg instanceof MouseMessage mouse &&
mouse.getAction() == MouseAction.MouseActionPress) {
int col = mouse.column();
int row = mouse.row();
// Check if click is in bounds
if (col >= bounds.x && col < bounds.x + bounds.width &&
row >= bounds.y && row < bounds.y + bounds.height) {
// Handle different buttons
return switch (mouse.getButton()) {
case MouseButtonLeft -> UpdateResult.from(this.select(col, row));
case MouseButtonRight -> UpdateResult.from(this.contextMenu(col, row));
case MouseButtonMiddle -> UpdateResult.from(this.paste(col, row));
default -> UpdateResult.from(this);
};
}
}
return UpdateResult.from(this);
}@Override
public UpdateResult<? extends Model> update(Message msg) {
if (msg instanceof MouseMessage mouse && mouse.isWheel()) {
return switch (mouse.getButton()) {
case MouseButtonWheelUp -> UpdateResult.from(this.scrollUp(3));
case MouseButtonWheelDown -> UpdateResult.from(this.scrollDown(3));
case MouseButtonWheelLeft -> UpdateResult.from(this.scrollLeft(3));
case MouseButtonWheelRight -> UpdateResult.from(this.scrollRight(3));
default -> UpdateResult.from(this);
};
}
return UpdateResult.from(this);
}Install with Tessl CLI
npx tessl i tessl/maven-com-williamcallahan--tui4j@0.3.0