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

utilities.mddocs/

Text Utilities

TUI4J provides comprehensive text processing utilities for handling ANSI escape sequences, calculating display widths, wrapping, truncating, and processing Unicode grapheme clusters.

ANSI Text Processing

TextWidth

Calculate the display width of text containing ANSI codes.

package com.williamcallahan.tui4j.ansi;

class TextWidth {
    // Static method to measure display width
    static int measureCellWidth(String input);
}

Usage:

// Text with ANSI codes
String styled = "\u001b[31mHello\u001b[0m";

// Width ignoring ANSI codes (counts cell width, ignores escape sequences)
int width = TextWidth.measureCellWidth(styled);  // Returns 5 (length of "Hello")

// Plain text width
int plainWidth = TextWidth.measureCellWidth("Hello");  // Returns 5

TextWrapper

Wrap text to specified width while preserving ANSI codes.

package com.williamcallahan.tui4j.ansi;

class TextWrapper {
    // Constructor
    TextWrapper();

    // Instance methods for wrapping
    String wrap(String text, int limit);
    String wrap(String text, int limit, boolean preserveStyle);
}

Usage:

String longText = "This is a very long line of text that needs to be wrapped.";

// Create wrapper instance
TextWrapper wrapper = new TextWrapper();

// Wrap at word boundaries
String wrapped = wrapper.wrap(longText, 20);

// Wrap with style preservation across lines
String styledWrap = wrapper.wrap(longText, 20, true);

Example Output:

String text = "The quick brown fox jumps over the lazy dog";
TextWrapper wrapper = new TextWrapper();

String wrapped = wrapper.wrap(text, 20);
// Output (wrapped at word boundaries):
// "The quick brown fox "
// "jumps over the lazy "
// "dog"

Truncate

Truncate text to specified width with optional tail.

package com.williamcallahan.tui4j.ansi;

class Truncate {
    static String truncate(String text, int width);
    static String truncate(String text, int width, String tail);
}

Usage:

String longText = "This is a very long string that needs truncation";

// Truncate without tail
String truncated = Truncate.truncate(longText, 20);
// "This is a very long "

// Truncate with ellipsis
String withTail = Truncate.truncate(longText, 20, "...");
// "This is a very lo..."

// Truncate with custom tail
String custom = Truncate.truncate(longText, 20, " [more]");
// "This is a [more]"

GraphemeCluster

Process Unicode grapheme clusters correctly.

package com.williamcallahan.tui4j.ansi;

class GraphemeCluster {
    static List<String> clusters(String text);
}

Usage:

// Text with emoji and combining characters
String text = "Hello 👨‍👩‍👧‍👦 World";

// Get grapheme clusters
List<String> clusters = GraphemeCluster.clusters(text);
// Correctly handles multi-codepoint graphemes

// Each cluster is a visually atomic unit
for (String cluster : clusters) {
    System.out.println(cluster);
}

Extended ANSI Utilities (x/ansi)

The com.williamcallahan.tui4j.compat.x.ansi package provides additional ANSI string operations.

Ansi

General ANSI string operations.

package com.williamcallahan.tui4j.compat.x.ansi;

class Ansi {
    static String strip(String text);
    static int length(String text);
    static String truncate(String text, int maxLength);
    static String truncate(String text, int maxLength, String tail);
}

Usage:

String styled = "\u001b[31;1mRed Bold Text\u001b[0m";

// Remove ANSI codes
String plain = Ansi.strip(styled);  // "Red Bold Text"

// Get visual length (ignoring ANSI)
int len = Ansi.length(styled);  // 13

// Truncate with ANSI awareness
String trunc = Ansi.truncate(styled, 10);  // Preserves color codes

StringWidth

Calculate string display width.

package com.williamcallahan.tui4j.compat.x.ansi;

class StringWidth {
    static int width(String text);
}

Usage:

// ASCII text
int width1 = StringWidth.width("Hello");  // 5

// Text with wide characters (CJK)
int width2 = StringWidth.width("你好");  // 4 (each character is 2 columns wide)

// Text with ANSI codes
int width3 = StringWidth.width("\u001b[31mHello\u001b[0m");  // 5

Strip

Remove ANSI escape sequences.

package com.williamcallahan.tui4j.compat.x.ansi;

class Strip {
    static String strip(String text);
}

Usage:

String styled = "\u001b[31mRed\u001b[0m \u001b[1mBold\u001b[0m";
String plain = Strip.strip(styled);  // "Red Bold"

WordWrap

Word wrapping with ANSI support.

package com.williamcallahan.tui4j.compat.x.ansi;

class WordWrap {
    static String wrap(String text, int width);
    static String wrap(String text, int width, String breakpoint);
}

Usage:

String text = "The quick brown fox jumps over the lazy dog";

// Wrap at word boundaries
String wrapped = WordWrap.wrap(text, 20);

// Wrap with custom breakpoint characters
String custom = WordWrap.wrap(text, 20, " -");  // Break on space or hyphen

HardWrap

Hard wrapping (break anywhere).

package com.williamcallahan.tui4j.compat.x.ansi;

class HardWrap {
    static String wrap(String text, int width);
}

Usage:

String text = "verylongwordwithoutspaces";
String wrapped = HardWrap.wrap(text, 10);
// "verylongwo"
// "rdwithouts"
// "paces"

Cut

Cut/slice ANSI strings.

package com.williamcallahan.tui4j.compat.x.ansi;

class Cut {
    static String cut(String text, int start, int end);
}

Usage:

String styled = "\u001b[31mRed Text\u001b[0m with normal text";

// Extract substring preserving ANSI codes
String substring = Cut.cut(styled, 0, 8);  // Gets "Red Text" with color codes
String substring2 = Cut.cut(styled, 9, 20);  // Gets "with norm" without codes

ANSI State Machine

Low-level ANSI parsing using state machine.

State

Parser states for ANSI sequences.

package com.williamcallahan.tui4j.ansi;

enum State {
    Ground,
    Escape,
    EscapeIntermediate,
    CsiEntry,
    CsiParam,
    CsiIntermediate,
    CsiIgnore,
    DcsEntry,
    DcsParam,
    DcsIntermediate,
    DcsPassthrough,
    DcsIgnore,
    OscString,
    SosPmApcString,
    Utf8
}

Action

Parser actions.

package com.williamcallahan.tui4j.ansi;

enum Action {
    None,
    Print,
    Execute,
    Ignore,
    Collect,
    Param,
    EscDispatch,
    CsiDispatch,
    Hook,
    Put,
    Unhook,
    OscStart,
    OscPut,
    OscEnd
}

Code

Character codes and ranges.

package com.williamcallahan.tui4j.ansi;

enum Code {
    C0_Start,
    C0_End,
    C1_Start,
    C1_End,
    Intermediate_Start,
    Intermediate_End,
    // ... many more codes
}

Common Patterns

Safe Text Display

Ensure text fits in terminal width:

String displayText(String text, int terminalWidth) {
    int width = TextWidth.measureCellWidth(text);

    if (width > terminalWidth) {
        return Truncate.truncate(text, terminalWidth - 3, "...");
    }

    return text;
}

Wrapping with Padding

Wrap text with padding considerations:

Style contentStyle = Style.newStyle()
    .width(40)
    .padding(1, 2);

String text = "Long text that needs wrapping";

// Account for padding in wrap width
int contentWidth = 40 - 4;  // Subtract horizontal padding
TextWrapper wrapper = new TextWrapper();
String wrapped = wrapper.wrap(text, contentWidth);

String output = contentStyle.render(wrapped);

Multiline Text Alignment

Align multiple lines of text:

String[] lines = text.split("\n");
int maxWidth = Arrays.stream(lines)
    .mapToInt(TextWidth::width)
    .max()
    .orElse(0);

// Center align each line
String aligned = Arrays.stream(lines)
    .map(line -> {
        int width = TextWidth.measureCellWidth(line);
        int padding = (maxWidth - width) / 2;
        return " ".repeat(padding) + line;
    })
    .collect(Collectors.joining("\n"));

Strip ANSI for Comparison

Remove ANSI codes before comparing strings:

boolean equalContent(String styled1, String styled2) {
    String plain1 = Strip.strip(styled1);
    String plain2 = Strip.strip(styled2);
    return plain1.equals(plain2);
}

Truncate with Style Preservation

Truncate while preserving ANSI styling:

String styledText = Style.newStyle()
    .foreground("#FF0000")
    .bold()
    .render("Long text to truncate");

// Truncate preserving ANSI codes
String truncated = Ansi.truncate(styledText, 20, "...");
// Result: "\u001b[31;1mLong text to trun...\u001b[0m"

Word Wrap for Terminal Output

@Override
public String view() {
    int terminalWidth = this.width;  // From WindowSizeMessage

    String content = "Your long text content here...";

    // Wrap to terminal width with margin
    String wrapped = WordWrap.wrap(content, terminalWidth - 4);

    return Style.newStyle()
        .padding(0, 2)
        .render(wrapped);
}

Calculating Layout Dimensions

String calculateBox(String content, int maxWidth) {
    // Split into lines
    String[] lines = content.split("\n");

    // Calculate required width
    int contentWidth = Arrays.stream(lines)
        .mapToInt(TextWidth::width)
        .max()
        .orElse(0);

    // Constrain to max width
    int boxWidth = Math.min(contentWidth + 4, maxWidth);  // +4 for padding

    // Wrap if needed
    if (contentWidth + 4 > maxWidth) {
        int wrapWidth = maxWidth - 4;
        TextWrapper wrapper = new TextWrapper();
        content = Arrays.stream(lines)
            .map(line -> wrapper.wrap(line, wrapWidth))
            .collect(Collectors.joining("\n"));
    }

    return Style.newStyle()
        .width(boxWidth)
        .padding(1, 2)
        .border(StandardBorder.normalBorder())
        .render(content);
}

Performance Considerations

  1. Width Calculation: TextWidth.measureCellWidth() is optimized but avoid calling repeatedly in tight loops

  2. Caching: Cache wrapped/truncated text when terminal size doesn't change:

    private String cachedWrapped;
    private int lastWidth;
    
    TextWrapper wrapper = new TextWrapper();
    
    String getWrapped(String text, int width) {
        if (width != lastWidth) {
            cachedWrapped = wrapper.wrap(text, width);
            lastWidth = width;
        }
        return cachedWrapped;
    }
  3. Lazy Wrapping: Only wrap text when actually displayed:

    @Override
    public String view() {
        // Wrap here, not in update()
        TextWrapper wrapper = new TextWrapper();
        return wrapper.wrap(content, terminalWidth);
    }
  4. Batch Processing: Process multiple lines together when possible:

    TextWrapper wrapper = new TextWrapper();
    String wrappedContent = Arrays.stream(lines)
        .map(line -> wrapper.wrap(line, width))
        .collect(Collectors.joining("\n"));

Unicode Support

TUI4J's text utilities correctly handle:

  • Wide characters (CJK): Each character takes 2 columns
  • Combining characters: Accents, diacritics don't add width
  • Emoji: Including multi-codepoint emoji sequences
  • Grapheme clusters: Visually atomic character sequences
  • Zero-width characters: Properly ignored in width calculations
// All handled correctly
String text1 = "Hello 世界";  // Wide CJK characters
String text2 = "café";  // Combining accent
String text3 = "👨‍👩‍👧‍👦";  // Multi-codepoint emoji
String text4 = "n̈ï̈ẅ̈";  // Multiple combining marks

int w1 = TextWidth.measureCellWidth(text1);  // Correct width with CJK
int w2 = TextWidth.measureCellWidth(text2);  // Correct width with combining
int w3 = TextWidth.measureCellWidth(text3);  // Correct emoji width
int w4 = TextWidth.measureCellWidth(text4);  // Correct combining marks width

Install with Tessl CLI

npx tessl i tessl/maven-com-williamcallahan--tui4j@0.3.0

docs

animation.md

bubbles-components.md

bubbletea-core.md

index.md

input-handling.md

lipgloss-styling.md

utilities.md

tile.json