Columns of text that don’t fall apart.
Tessary measures plain strings and arranges them into aligned columns — wrapping, padding and truncating so the result reads the same in an 80-column terminal, a log file or a pasted snippet.
Install
Node 18+, or any runtime with Intl.Segmenter. No build step, no native parts.
npm add tessary
# or vendor the single file — it has no imports
cp vendor/tessary/table.js src/lib/
Ships as ESM with a CommonJS fallback and a hand-written .d.ts. It touches no globals and reads
no environment variable except COLUMNS.
Usage
One function does the work. Give it rows and a width; get back lines.
import { layout } from "tessary";
const rows = [
["cache", "warm", "12.4 MB"],
["indexer", "idle", "0 B"],
["reaper", "running", "812 kB"],
];
console.log(layout(rows, {
width: 48,
align: ["left", "left", "right"],
gap: 2,
}));
// cache warm 12.4 MB
// indexer idle 0 B
// reaper running 812 kB
Cells may hold anything printable. Wide characters count as two columns, combining marks as zero and colour escapes as empty, so styled output stays aligned instead of drifting a space per sequence.
Nothing is printed for you: layout() returns a string, byte-identical for the same input, so
one call serves a terminal, a snapshot test or a socket. When the budget is too small, the columns listed in
collapse drop out in order rather than overflowing the line.
Options
Everything is optional except the rows themselves.
| Option | Type | Default | Notes |
|---|---|---|---|
width | number | 80 | Budget in columns. Pass "auto" to read COLUMNS. |
gap | number | 1 | Spaces between columns, counted inside the budget. |
align | array | "left" | Per column: left, right, center, decimal. |
wrap | boolean | true | When false, long cells are cut with a single ellipsis. |
collapse | array | [] | Column indexes to drop, in order, when space runs out. |
Questions
Is this a table-drawing library?
No. No borders, no headers, no themes, no colour. Tessary solves measuring and wrapping; box characters around the returned lines are a few lines of your own code.
Why not just pad with padEnd?
Because it counts UTF-16 code units. A flag, a family emoji or a colourised status word reports a length that has nothing to do with the columns it occupies, and alignment slips. That mismatch is the whole reason this exists.
How fast is it?
Around 40k rows per second for three short columns on a laptop core. Widths are memoised per string, so re-laying the same data out at a new width is nearly free.
Is the API stable?
The signature has not changed since 0.4 and will not change before 1.0. Anything undocumented here is internal, even if it happens to be exported.
Recent changes
- 0.6.2 — fix a rounding case where
centerlost a space on odd widths. - 0.6.0 — add
decimalalignment and thecollapseorder. - 0.5.1 — treat terminal hyperlinks as zero width; drop the last Node 16 branch.
Release notes
Four or five short notes a year, only when something ships or breaks. No tracking anywhere on this page.