Skip to content

Repository files navigation

termwidth

CI npm version license zero dependencies node

Measure terminal cell width from ground truth, not a hardcoded table.

termwidth demo — a naive code-point count drifts the box border row by row; termwidth measures each grapheme and keeps it flush

Every other width library treats cell width as a property of the character (wcwidth + a static table). But it's really a property of this terminal, right now — emoji, ZWJ sequences, flags, and CJK render at different widths across terminals, so box borders break when someone pastes an emoji.

termwidth asks the terminal itself: print a grapheme offscreen, fire CSI 6n, read where the cursor landed. It probes a few hundred ambiguous graphemes once at startup, caches the result to disk (keyed by terminal + version), and everything after that is pure arithmetic.

Why not just string-width?

Static libraries (string-width, wcwidth) encode one fixed width per character. That works for the unambiguous majority — but a whole class of graphemes has no single correct answer: their width depends on the terminal and its configuration, not the character. A static table has to pick one guess, and it's wrong wherever your terminal disagrees. That off-by-one is exactly what drifts box borders and table columns by a cell.

Grapheme Static table termwidth
— East Asian Ambiguous one hardcoded guess (usually 1) the width this terminal renders
🇯🇵 flag (regional indicators) one fixed guess 1 or 2 cells, per terminal
👍🏽 emoji + skin tone one fixed guess measured per terminal
👨‍👩‍👧 ZWJ family one fixed guess measured per terminal
a — unambiguous correct correct (both agree)

termwidth ships a fallback table too, so it's never worse than a static library out of the box — but once the probe lands, the ambiguous rows above switch from guess to your terminal's actual value.

Features

  • Zero runtime dependencies. Uses the built-in Intl.Segmenter.
  • Synchronous measure(). Correct from the moment you import (vendored fallback table), sharper once the probe lands.
  • Safe by design. Raw-mode restore guard on every exit path, per-query timeout, and a silent bail — it never wedges or spams a terminal.
  • Drop-in shims for string-width, wrap-ansi, and cli-truncate.

Install

npm install termwidth

Requires Node 16+ (for Intl.Segmenter). Ships dual ESM + CJS.

Usage

import { measure, wrap, truncate, ready } from "termwidth";

measure("👨‍👩‍👧");        // width from the fallback table immediately
await ready();             // optional: wait for the live probe before first paint
measure("👨‍👩‍👧");        // now the terminal's ground-truth width

measure() is always safe to call synchronously in a render loop. The probe fires once on import behind a TTY gate; await ready() only if you want the sharpened table before your first frame.

Drop-in replacements

Swap one import; the signatures match.

import stringWidth from "termwidth/string-width";
import wrapAnsi from "termwidth/wrap-ansi";
import cliTruncate from "termwidth/cli-truncate";

How the probe stays safe

The probe only runs when stdout and stdin are both TTYs, CI is unset, and TERM isn't dumb. It hides the cursor, batches its CSI 6n queries, and:

  • restores raw mode + cursor on exit, SIGINT, SIGTERM, and uncaughtException;
  • bails to the vendored table on any per-query or overall timeout;
  • validates sentinel graphemes (a → 1, → 2) and rejects impossible widths, so a mangled stream (e.g. tmux passthrough) is discarded, not cached.

Set TERMWIDTH_DEBUG=1 for diagnostics on stderr (silent otherwise). Set TERMWIDTH_DISABLE=1 to force the vendored table and skip the probe.

Cache

Probed tables live in $XDG_CACHE_HOME/termwidth/<key>.json (or ~/.cache/termwidth/). Probe once per terminal + version, reuse forever.

CLI

npx termwidth report   # probe this terminal, print canonical JSON

License

MIT

About

Measure terminal cell width from ground truth — probe the terminal, cache, then it's pure arithmetic. Drop-in for string-width / wrap-ansi / cli-truncate.

Topics

Resources

Code of conduct

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages