|
| 1 | +--- |
| 2 | +date: '2026-02-11T14:00:00+01:00' |
| 3 | +draft: false |
| 4 | +title: 'Table Display' |
| 5 | +weight: 19 |
| 6 | +--- |
| 7 | + |
| 8 | +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. |
| 9 | + |
| 10 | +## Quick Start |
| 11 | + |
| 12 | +```java |
| 13 | +import org.aesh.util.table.Table; |
| 14 | +import org.aesh.util.table.TableStyle; |
| 15 | + |
| 16 | +String output = Table.<User>builder() |
| 17 | + .maxWidth(80) |
| 18 | + .style(TableStyle.DUCKDB) |
| 19 | + .column("Name", u -> u.getName()) |
| 20 | + .column("Email", u -> u.getEmail()) |
| 21 | + .build() |
| 22 | + .render(userList); |
| 23 | + |
| 24 | +invocation.println(output); |
| 25 | +``` |
| 26 | + |
| 27 | +Output: |
| 28 | + |
| 29 | +``` |
| 30 | +┌──────┬──────────────────┐ |
| 31 | +│ Name │ Email │ |
| 32 | +├──────┼──────────────────┤ |
| 33 | +│ Alice│ alice@example.com│ |
| 34 | +│ Bob │ bob@example.com │ |
| 35 | +└──────┴──────────────────┘ |
| 36 | +``` |
| 37 | + |
| 38 | +## Static API |
| 39 | + |
| 40 | +For one-off table rendering, use the static `render` methods directly: |
| 41 | + |
| 42 | +```java |
| 43 | +import org.aesh.util.table.Table; |
| 44 | + |
| 45 | +import java.util.Arrays; |
| 46 | +import java.util.List; |
| 47 | +import java.util.function.Function; |
| 48 | + |
| 49 | +List<String> headers = Arrays.asList("Name", "Age", "Score"); |
| 50 | +List<Function<User, Object>> accessors = Arrays.asList( |
| 51 | + u -> u.getName(), |
| 52 | + u -> u.getAge(), |
| 53 | + u -> u.getScore() |
| 54 | +); |
| 55 | + |
| 56 | +// Render with default DUCKDB style |
| 57 | +String output = Table.render(80, users, headers, accessors); |
| 58 | + |
| 59 | +// Render with a specific style |
| 60 | +String output = Table.render(80, users, headers, accessors, |
| 61 | + TableStyle.SQLITE.characters()); |
| 62 | +``` |
| 63 | + |
| 64 | +## Builder API |
| 65 | + |
| 66 | +The builder API pairs each header with its accessor function, preventing mismatched list sizes: |
| 67 | + |
| 68 | +```java |
| 69 | +Table<User> table = Table.<User>builder() |
| 70 | + .maxWidth(120) |
| 71 | + .style(TableStyle.DOUBLE) |
| 72 | + .column("Name", u -> u.getName()) |
| 73 | + .column("Age", u -> u.getAge()) |
| 74 | + .column("Score", u -> u.getScore()) |
| 75 | + .build(); |
| 76 | + |
| 77 | +// The built Table instance is reusable |
| 78 | +String output1 = table.render(activeUsers); |
| 79 | +String output2 = table.render(inactiveUsers); |
| 80 | +``` |
| 81 | + |
| 82 | +You can also pass custom character maps to the builder instead of a predefined style: |
| 83 | + |
| 84 | +```java |
| 85 | +Table.<User>builder() |
| 86 | + .characters(myCustomCharacterMap) |
| 87 | + .column("Name", u -> u.getName()) |
| 88 | + .build(); |
| 89 | +``` |
| 90 | + |
| 91 | +## Table Styles |
| 92 | + |
| 93 | +Four predefined styles are available via `TableStyle`: |
| 94 | + |
| 95 | +### POSTGRES |
| 96 | + |
| 97 | +Simple ASCII without outside borders, similar to PostgreSQL's `psql` output: |
| 98 | + |
| 99 | +```java |
| 100 | +Table.render(80, users, headers, accessors, TableStyle.POSTGRES.characters()); |
| 101 | +``` |
| 102 | + |
| 103 | +``` |
| 104 | + Name | Age | Score |
| 105 | +-------+-----+------ |
| 106 | + Alice | 30 | 95.50 |
| 107 | + Bob | 25 | 87.12 |
| 108 | +``` |
| 109 | + |
| 110 | +### SQLITE |
| 111 | + |
| 112 | +ASCII with full outside borders: |
| 113 | + |
| 114 | +```java |
| 115 | +Table.render(80, users, headers, accessors, TableStyle.SQLITE.characters()); |
| 116 | +``` |
| 117 | + |
| 118 | +``` |
| 119 | ++-------+-----+-------+ |
| 120 | +| Name | Age | Score | |
| 121 | ++-------+-----+-------+ |
| 122 | +| Alice | 30 | 95.50 | |
| 123 | +| Bob | 25 | 87.12 | |
| 124 | ++-------+-----+-------+ |
| 125 | +``` |
| 126 | + |
| 127 | +### DUCKDB |
| 128 | + |
| 129 | +Unicode box-drawing characters (the default style): |
| 130 | + |
| 131 | +```java |
| 132 | +Table.render(80, users, headers, accessors, TableStyle.DUCKDB.characters()); |
| 133 | +``` |
| 134 | + |
| 135 | +``` |
| 136 | +┌───────┬─────┬───────┐ |
| 137 | +│ Name │ Age │ Score │ |
| 138 | +├───────┼─────┼───────┤ |
| 139 | +│ Alice │ 30 │ 95.50 │ |
| 140 | +│ Bob │ 25 │ 87.12 │ |
| 141 | +└───────┴─────┴───────┘ |
| 142 | +``` |
| 143 | + |
| 144 | +### DOUBLE |
| 145 | + |
| 146 | +Double-line box-drawing with row separators between data rows: |
| 147 | + |
| 148 | +```java |
| 149 | +Table.render(80, users, headers, accessors, TableStyle.DOUBLE.characters()); |
| 150 | +``` |
| 151 | + |
| 152 | +``` |
| 153 | +╔═══════╦═════╦═══════╗ |
| 154 | +║ Name ║ Age ║ Score ║ |
| 155 | +╠═══════╬═════╬═══════╣ |
| 156 | +║ Alice ║ 30 ║ 95.50 ║ |
| 157 | +╟───────╫─────╫───────╣ |
| 158 | +║ Bob ║ 25 ║ 87.12 ║ |
| 159 | +╚═══════╩═════╩═══════╝ |
| 160 | +``` |
| 161 | + |
| 162 | +## Numeric Formatting |
| 163 | + |
| 164 | +Column types are detected automatically from the data: |
| 165 | + |
| 166 | +| Value Type | Alignment | Formatting | |
| 167 | +|------------|-----------|------------| |
| 168 | +| `String` | Left-aligned | As-is | |
| 169 | +| `Integer`, `Long` | Right-aligned | `%d` format | |
| 170 | +| `Float`, `Double` | Right-aligned | 2 decimal places | |
| 171 | +| `null` | Left-aligned | Rendered as `"null"` | |
| 172 | + |
| 173 | +```java |
| 174 | +Table.<Item>builder() |
| 175 | + .column("Product", i -> i.name) // left-aligned string |
| 176 | + .column("Quantity", i -> i.quantity) // right-aligned integer |
| 177 | + .column("Price", i -> i.price) // right-aligned, 2 decimals |
| 178 | + .build() |
| 179 | + .render(items); |
| 180 | +``` |
| 181 | + |
| 182 | +## Multi-line Headers |
| 183 | + |
| 184 | +Headers can span multiple lines using `System.lineSeparator()`: |
| 185 | + |
| 186 | +```java |
| 187 | +List<String> headers = Arrays.asList( |
| 188 | + "First" + System.lineSeparator() + "Name", |
| 189 | + "Email" |
| 190 | +); |
| 191 | +``` |
| 192 | + |
| 193 | +``` |
| 194 | +┌───────┬──────────────────┐ |
| 195 | +│ First │ │ |
| 196 | +│ Name │ Email │ |
| 197 | +├───────┼──────────────────┤ |
| 198 | +│ Alice │ alice@example.com│ |
| 199 | +└───────┴──────────────────┘ |
| 200 | +``` |
| 201 | + |
| 202 | +Headers are center-aligned within their column, and shorter headers are padded with empty lines to match the tallest header. |
| 203 | + |
| 204 | +## Custom Border Characters |
| 205 | + |
| 206 | +### Using Templates |
| 207 | + |
| 208 | +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: |
| 209 | + |
| 210 | +```java |
| 211 | +import org.aesh.util.table.TableCharacters; |
| 212 | + |
| 213 | +// 6-line template: top border, header, header-body separator, body, row separator, bottom border |
| 214 | +String template = |
| 215 | + "╔═╦═╗" + System.lineSeparator() + |
| 216 | + "║h║h║" + System.lineSeparator() + |
| 217 | + "╠═╬═╣" + System.lineSeparator() + |
| 218 | + "║v║v║" + System.lineSeparator() + |
| 219 | + "╟─╫─╢" + System.lineSeparator() + |
| 220 | + "╚═╩═╝"; |
| 221 | + |
| 222 | +Map<String, String> chars = TableCharacters.templateToMap(template, |
| 223 | + TableStyle.DUCKDB.characters()); |
| 224 | +``` |
| 225 | + |
| 226 | +Templates with 3, 5, or 6 lines are supported, providing increasing levels of border detail. |
| 227 | + |
| 228 | +### Using Shorthand Expansion |
| 229 | + |
| 230 | +Define minimal character sets and expand them to all positions: |
| 231 | + |
| 232 | +```java |
| 233 | +Map<String, String> shorthand = new HashMap<>(); |
| 234 | +shorthand.put(TableCharacters.VERTICAL, "│"); |
| 235 | +shorthand.put(TableCharacters.HORIZONTAL, "─"); |
| 236 | +shorthand.put(TableCharacters.INTERSECT, "┼"); |
| 237 | + |
| 238 | +// Expand to all border positions (second parameter enables outside borders) |
| 239 | +Map<String, String> full = TableCharacters.convertToFullNames(shorthand, true); |
| 240 | +``` |
| 241 | + |
| 242 | +## ANSI Color Support |
| 243 | + |
| 244 | +Add ANSI color to table borders using `TableCharacters.prefix()`: |
| 245 | + |
| 246 | +```java |
| 247 | +import org.aesh.terminal.utils.ANSI; |
| 248 | +import org.aesh.util.table.TableCharacters; |
| 249 | + |
| 250 | +Map<String, String> colored = TableCharacters.prefix( |
| 251 | + ANSI.CYAN_TEXT, TableStyle.DUCKDB.characters()); |
| 252 | + |
| 253 | +String output = Table.render(80, users, headers, accessors, colored); |
| 254 | +``` |
| 255 | + |
| 256 | +This wraps each border character with the ANSI color prefix and `ANSI.RESET` suffix, so data values remain unaffected. |
| 257 | + |
| 258 | +## Using Tables in Commands |
| 259 | + |
| 260 | +A complete command that renders tabular output: |
| 261 | + |
| 262 | +```java |
| 263 | +import org.aesh.command.Command; |
| 264 | +import org.aesh.command.CommandDefinition; |
| 265 | +import org.aesh.command.CommandResult; |
| 266 | +import org.aesh.command.invocation.CommandInvocation; |
| 267 | +import org.aesh.util.table.Table; |
| 268 | +import org.aesh.util.table.TableStyle; |
| 269 | + |
| 270 | +@CommandDefinition(name = "users", description = "List all users") |
| 271 | +public class ListUsersCommand implements Command<CommandInvocation> { |
| 272 | + |
| 273 | + @Override |
| 274 | + public CommandResult execute(CommandInvocation invocation) { |
| 275 | + List<User> users = loadUsers(); |
| 276 | + |
| 277 | + String table = Table.<User>builder() |
| 278 | + .maxWidth(invocation.getShell().size().getWidth()) |
| 279 | + .style(TableStyle.DUCKDB) |
| 280 | + .column("Name", u -> u.getName()) |
| 281 | + .column("Email", u -> u.getEmail()) |
| 282 | + .column("Role", u -> u.getRole()) |
| 283 | + .build() |
| 284 | + .render(users); |
| 285 | + |
| 286 | + invocation.getShell().writeln(table); |
| 287 | + return CommandResult.SUCCESS; |
| 288 | + } |
| 289 | +} |
| 290 | +``` |
| 291 | + |
| 292 | +Using `invocation.getShell().size().getWidth()` adapts the table to the current terminal width. |
| 293 | + |
| 294 | +## Validation Helpers |
| 295 | + |
| 296 | +`TableCharacters` provides methods to inspect character maps: |
| 297 | + |
| 298 | +```java |
| 299 | +// Check if the map defines a complete outside border |
| 300 | +boolean bordered = TableCharacters.hasOutsideBorder(characters); |
| 301 | + |
| 302 | +// Check if the map defines row separators between data rows |
| 303 | +boolean separated = TableCharacters.hasRowSeparator(characters); |
| 304 | + |
| 305 | +// Check if the map has the minimum required entries for rendering |
| 306 | +boolean valid = TableCharacters.isValid(characters); |
| 307 | +``` |
| 308 | + |
| 309 | +If an invalid character map is passed to `Table.render()`, it falls back to the `SQLITE` style automatically. |
| 310 | + |
| 311 | +## API Reference |
| 312 | + |
| 313 | +### Table |
| 314 | + |
| 315 | +| Method | Description | |
| 316 | +|--------|-------------| |
| 317 | +| `Table.render(maxWidth, values, headers, accessors)` | Render with default DUCKDB style | |
| 318 | +| `Table.render(maxWidth, values, headers, accessors, characters)` | Render with custom border characters | |
| 319 | +| `Table.builder()` | Create a builder for fluent configuration | |
| 320 | +| `table.render(values)` | Render using a pre-built Table instance | |
| 321 | + |
| 322 | +### Table.Builder |
| 323 | + |
| 324 | +| Method | Description | |
| 325 | +|--------|-------------| |
| 326 | +| `maxWidth(int)` | Set maximum table width (default: 80) | |
| 327 | +| `style(TableStyle)` | Set a predefined border style | |
| 328 | +| `characters(Map)` | Set a custom character map | |
| 329 | +| `column(header, accessor)` | Add a column with header and value accessor | |
| 330 | +| `build()` | Build an immutable `Table` instance | |
| 331 | + |
| 332 | +### TableCharacters |
| 333 | + |
| 334 | +| Method | Description | |
| 335 | +|--------|-------------| |
| 336 | +| `convertToFullNames(map, border)` | Expand shorthand V/H/X to full position names | |
| 337 | +| `templateToMap(template, defaultMap)` | Parse a visual template into a character map | |
| 338 | +| `prefix(ansiPrefix, map)` | Wrap border characters with ANSI color codes | |
| 339 | +| `hasOutsideBorder(map)` | Check for complete outside border definition | |
| 340 | +| `hasRowSeparator(map)` | Check for row separator definition | |
| 341 | +| `isValid(map)` | Check for minimum required entries | |
| 342 | +| `lineSplit(headers)` | Split multi-line headers into aligned rows | |
| 343 | + |
| 344 | +### TableStyle |
| 345 | + |
| 346 | +| Style | Borders | Characters | Row Separators | |
| 347 | +|-------|---------|------------|----------------| |
| 348 | +| `POSTGRES` | None | ASCII (`\|`, `-`, `+`) | No | |
| 349 | +| `SQLITE` | Full | ASCII (`\|`, `-`, `+`) | No | |
| 350 | +| `DUCKDB` | Full | Unicode box-drawing | No | |
| 351 | +| `DOUBLE` | Full | Double-line box-drawing | Yes | |
0 commit comments