Tessary

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.

  • v0.6.2
  • zero dependencies
  • MIT
  • ~6 kB

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.

OptionTypeDefaultNotes
widthnumber80Budget in columns. Pass "auto" to read COLUMNS.
gapnumber1Spaces between columns, counted inside the budget.
alignarray"left"Per column: left, right, center, decimal.
wrapbooleantrueWhen false, long cells are cut with a single ellipsis.
collapsearray[]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 center lost a space on odd widths.
  • 0.6.0 — add decimal alignment and the collapse order.
  • 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.