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

input-handling.mddocs/

Input Handling

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

KeyPressMessage

Keyboard input is delivered through KeyPressMessage:

package com.williamcallahan.tui4j.compat.bubbletea;

class KeyPressMessage implements Message {
    Key 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);
}

KeyType

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));
}

Kitty Keyboard Protocol

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 Input

MouseMessage

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);
}

MouseButton

Enumeration of mouse button types.

package com.williamcallahan.tui4j.compat.bubbletea.input;

enum MouseButton {
    MouseButtonNone,
    MouseButtonLeft,
    MouseButtonMiddle,
    MouseButtonRight,
    MouseButtonWheelUp,
    MouseButtonWheelDown,
    MouseButtonWheelLeft,
    MouseButtonWheelRight,
    MouseButtonBackward,
    MouseButtonForward
}

MouseAction

Enumeration of mouse action types.

package com.williamcallahan.tui4j.compat.bubbletea.input;

enum MouseAction {
    MouseActionPress,
    MouseActionRelease,
    MouseActionMotion
}

Enhanced Mouse Features

TUI4J provides additional mouse utilities beyond the core Bubble Tea framework.

MouseClickMessage

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 click
  • 2 - Double click
  • 3+ - Triple click or more

MouseClickTracker

Tracks 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);
    }
}

MouseBounds

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);
}

MouseTarget

Clickable target region with identifier and data.

package com.williamcallahan.tui4j.input;

record MouseTarget(MouseBounds bounds, String id, Object data) { }

MouseTargetProvider

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);
    }
}

MouseTargets

Utilities for mouse target management.

package com.williamcallahan.tui4j.input;

class MouseTargets {
    static MouseTarget findTarget(int x, int y, List<MouseTarget> targets);
}

MouseCursor

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()

MouseHoverTextDetector

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);
}

MouseSelectionTracker

Tracks mouse selection regions (drag selection).

package com.williamcallahan.tui4j.input;

class MouseSelectionTracker {
    MouseSelectionTracker();
    MouseSelectionUpdate track(MouseMessage msg);
    boolean hasSelection();
    void clear();
}

MouseSelectionUpdate

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);
    }
}

MouseSelectionAutoScroller

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);
}

Enabling Mouse Support

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)
);

Input Patterns

Keyboard Navigation

@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);
}

Text Input

@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);
}

Click Handling

@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);
}

Scroll Handling

@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

docs

animation.md

bubbles-components.md

bubbletea-core.md

index.md

input-handling.md

lipgloss-styling.md

utilities.md

tile.json