Table Display

Table Display

The table display utility renders structured data as formatted text tables in the terminal. It supports multiple border styles, automatic column width calculation, numeric formatting, and multi-line headers.

Quick Start

import org.aesh.util.table.Table;
import org.aesh.util.table.TableStyle;

String output = Table.<User>builder()
        .maxWidth(80)
        .style(TableStyle.DUCKDB)
        .column("Name", u -> u.getName())
        .column("Email", u -> u.getEmail())
        .build()
        .render(userList);

invocation.println(output);

Output:

┌──────┬──────────────────┐
│ Name │      Email       │
├──────┼──────────────────┤
│ Alice│ alice@example.com│
│ Bob  │ bob@example.com  │
└──────┴──────────────────┘

Static API

For one-off table rendering, use the static render methods directly:

import org.aesh.util.table.Table;

import java.util.Arrays;
import java.util.List;
import java.util.function.Function;

List<String> headers = Arrays.asList("Name", "Age", "Score");
List<Function<User, Object>> accessors = Arrays.asList(
        u -> u.getName(),
        u -> u.getAge(),
        u -> u.getScore()
);

// Render with default DUCKDB style
String output = Table.render(80, users, headers, accessors);

// Render with a specific style
String output = Table.render(80, users, headers, accessors,
        TableStyle.SQLITE.characters());

Builder API

The builder API pairs each header with its accessor function, preventing mismatched list sizes:

Table<User> table = Table.<User>builder()
        .maxWidth(120)
        .style(TableStyle.DOUBLE)
        .column("Name", u -> u.getName())
        .column("Age", u -> u.getAge())
        .column("Score", u -> u.getScore())
        .build();

// The built Table instance is reusable
String output1 = table.render(activeUsers);
String output2 = table.render(inactiveUsers);

You can also pass custom character maps to the builder instead of a predefined style:

Table.<User>builder()
        .characters(myCustomCharacterMap)
        .column("Name", u -> u.getName())
        .build();

Table Styles

Four predefined styles are available via TableStyle:

POSTGRES

Simple ASCII without outside borders, similar to PostgreSQL’s psql output:

Table.render(80, users, headers, accessors, TableStyle.POSTGRES.characters());
 Name  | Age | Score
-------+-----+------
 Alice |  30 | 95.50
 Bob   |  25 | 87.12

SQLITE

ASCII with full outside borders:

Table.render(80, users, headers, accessors, TableStyle.SQLITE.characters());
+-------+-----+-------+
| Name  | Age | Score |
+-------+-----+-------+
| Alice |  30 | 95.50 |
| Bob   |  25 | 87.12 |
+-------+-----+-------+

DUCKDB

Unicode box-drawing characters (the default style):

Table.render(80, users, headers, accessors, TableStyle.DUCKDB.characters());
┌───────┬─────┬───────┐
│ Name  │ Age │ Score │
├───────┼─────┼───────┤
│ Alice │  30 │ 95.50 │
│ Bob   │  25 │ 87.12 │
└───────┴─────┴───────┘

DOUBLE

Double-line box-drawing with row separators between data rows:

Table.render(80, users, headers, accessors, TableStyle.DOUBLE.characters());
╔═══════╦═════╦═══════╗
║ Name  ║ Age ║ Score ║
╠═══════╬═════╬═══════╣
║ Alice ║  30 ║ 95.50 ║
╟───────╫─────╫───────╣
║ Bob   ║  25 ║ 87.12 ║
╚═══════╩═════╩═══════╝

Numeric Formatting

Column types are detected automatically from the data:

Value TypeAlignmentFormatting
StringLeft-alignedAs-is
Integer, LongRight-aligned%d format
Float, DoubleRight-aligned2 decimal places
nullLeft-alignedRendered as "null"
Table.<Item>builder()
        .column("Product", i -> i.name)        // left-aligned string
        .column("Quantity", i -> i.quantity)     // right-aligned integer
        .column("Price", i -> i.price)           // right-aligned, 2 decimals
        .build()
        .render(items);

Multi-line Headers

Headers can span multiple lines using System.lineSeparator():

List<String> headers = Arrays.asList(
        "First" + System.lineSeparator() + "Name",
        "Email"
);
┌───────┬──────────────────┐
│ First │                  │
│ Name  │      Email       │
├───────┼──────────────────┤
│ Alice │ alice@example.com│
└───────┴──────────────────┘

Headers are center-aligned within their column, and shorter headers are padded with empty lines to match the tallest header.

Custom Border Characters

Using Templates

Define custom borders with a visual 5-character-wide template. The template is a multi-line string where each line represents a part of the table border:

import org.aesh.util.table.TableCharacters;

// 6-line template: top border, header, header-body separator, body, row separator, bottom border
String template =
        "╔═╦═╗" + System.lineSeparator() +
        "║h║h║" + System.lineSeparator() +
        "╠═╬═╣" + System.lineSeparator() +
        "║v║v║" + System.lineSeparator() +
        "╟─╫─╢" + System.lineSeparator() +
        "╚═╩═╝";

Map<String, String> chars = TableCharacters.templateToMap(template,
        TableStyle.DUCKDB.characters());

Templates with 3, 5, or 6 lines are supported, providing increasing levels of border detail.

Using Shorthand Expansion

Define minimal character sets and expand them to all positions:

Map<String, String> shorthand = new HashMap<>();
shorthand.put(TableCharacters.VERTICAL, "│");
shorthand.put(TableCharacters.HORIZONTAL, "─");
shorthand.put(TableCharacters.INTERSECT, "┼");

// Expand to all border positions (second parameter enables outside borders)
Map<String, String> full = TableCharacters.convertToFullNames(shorthand, true);

ANSI Color Support

Add ANSI color to table borders using TableCharacters.prefix():

import org.aesh.terminal.utils.ANSI;
import org.aesh.util.table.TableCharacters;

Map<String, String> colored = TableCharacters.prefix(
        ANSI.CYAN_TEXT, TableStyle.DUCKDB.characters());

String output = Table.render(80, users, headers, accessors, colored);

This wraps each border character with the ANSI color prefix and ANSI.RESET suffix, so data values remain unaffected.

Using Tables in Commands

Inside a command’s execute() method, the CommandInvocation gives you access to the Shell object via getShell(). From the Shell you can get the current terminal dimensions with size() and write output with write() or writeln(). This makes it straightforward to render tables that adapt to the user’s terminal:

import org.aesh.command.Command;
import org.aesh.command.CommandDefinition;
import org.aesh.command.CommandResult;
import org.aesh.command.invocation.CommandInvocation;
import org.aesh.command.shell.Shell;
import org.aesh.util.table.Table;
import org.aesh.util.table.TableStyle;

@CommandDefinition(name = "users", description = "List all users")
public class ListUsersCommand implements Command<CommandInvocation> {

    @Override
    public CommandResult execute(CommandInvocation invocation) {
        List<User> users = loadUsers();
        Shell shell = invocation.getShell();

        String table = Table.<User>builder()
                .maxWidth(shell.size().getWidth())
                .style(TableStyle.DUCKDB)
                .column("Name", u -> u.getName())
                .column("Email", u -> u.getEmail())
                .column("Role", u -> u.getRole())
                .build()
                .render(users);

        shell.writeln(table);
        return CommandResult.SUCCESS;
    }
}

The key calls in the chain:

CallReturnsPurpose
invocation.getShell()ShellAccess the terminal
shell.size()SizeGet terminal dimensions
size.getWidth()intTerminal width in columns
shell.writeln(text)voidWrite output to the terminal

This ensures the table fits the user’s terminal regardless of window size. See the CommandInvocation API documentation for the full set of Shell methods available.

Validation Helpers

TableCharacters provides methods to inspect character maps:

// Check if the map defines a complete outside border
boolean bordered = TableCharacters.hasOutsideBorder(characters);

// Check if the map defines row separators between data rows
boolean separated = TableCharacters.hasRowSeparator(characters);

// Check if the map has the minimum required entries for rendering
boolean valid = TableCharacters.isValid(characters);

If an invalid character map is passed to Table.render(), it falls back to the SQLITE style automatically.

API Reference

Table

MethodDescription
Table.render(maxWidth, values, headers, accessors)Render with default DUCKDB style
Table.render(maxWidth, values, headers, accessors, characters)Render with custom border characters
Table.builder()Create a builder for fluent configuration
table.render(values)Render using a pre-built Table instance

Table.Builder

MethodDescription
maxWidth(int)Set maximum table width (default: 80)
style(TableStyle)Set a predefined border style
characters(Map)Set a custom character map
column(header, accessor)Add a column with header and value accessor
build()Build an immutable Table instance

TableCharacters

MethodDescription
convertToFullNames(map, border)Expand shorthand V/H/X to full position names
templateToMap(template, defaultMap)Parse a visual template into a character map
prefix(ansiPrefix, map)Wrap border characters with ANSI color codes
hasOutsideBorder(map)Check for complete outside border definition
hasRowSeparator(map)Check for row separator definition
isValid(map)Check for minimum required entries
lineSplit(headers)Split multi-line headers into aligned rows

TableStyle

StyleBordersCharactersRow Separators
POSTGRESNoneASCII (|, -, +)No
SQLITEFullASCII (|, -, +)No
DUCKDBFullUnicode box-drawingNo
DOUBLEFullDouble-line box-drawingYes