npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

linegauge

v1.2.0

Published

Measures, wraps, truncates and slices styled terminal text, grapheme-correct over Intl.Segmenter, with the same result on a terminal, a pipe or --json. Zero dependencies. Drop-in paths for string-width, wrap-ansi, strip-ansi, slice-ansi, cli-truncate and

Readme

linegauge measures, wraps, truncates and slices styled terminal text without the edge fraying, grapheme-correct over the platform's own Intl.Segmenter. Nothing here reads process, so a pipe, --json and an agent get the same columns a terminal does. Zero dependencies.

Drop-in for string-width (the default export), wrap-ansi, strip-ansi and slice-ansi: npx burgee migrate moves the imports, each graded by the incumbent's own suite.

Over the platform. Intl.Segmenter splits graphemes and util.stripVTControlCharacters strips escapes; neither one measures a column. linegauge adds the width table (East Asian wide characters, emoji, zero-width marks) and, on Node before 26, strips the colon form of a colour escape (ESC[38:2::255:0:0m) that the builtin leaves half behind. The repository's absorption probe asks every supported Node line how much of this the builtins now do alone.

A printer's line gauge is the steel rule marked in picas and points: a compositor holds it against a line of type and checks it fits the measure it was set to.

Install

npm install linegauge
pnpm add linegauge
yarn add linegauge
bun add linegauge

Quick start

import { width, wrap, truncate, slice, widest } from 'linegauge';

width('古代'); // 4 — East Asian wide, two columns each
width('👨‍👩‍👧‍👦'); // 2 — one cluster, not four people

wrap('a long sentence that needs folding', 12);
truncate('the quick brown fox', 10); // 'the quick…'
const styled = '\u001B[1mbold\u001B[22m'; // the word, in bold
slice(styled, 2, 4); // 'ld' still in bold — columns 2 and 3, styles intact
widest(['a', 'bbb', 'cc']); // 3

One problem wearing five names

width · wrap · truncate · slice · widest

They look like five utilities. They are one: cutting styled text without letting the edge come apart — no dangling escape sequence, no half a grapheme, no severed emoji cluster. Each has to know where the ANSI is and where the cluster boundaries are, and once you know that, you may as well answer all five.

The ecosystem splits it across twelve packages — strip-ansi, string-width, ansi-regex, wrap-ansi, emoji-regex, slice-ansi, get-east-asian-width, eastasianwidth, string-length, wcwidth, cli-truncate and widest-line — which between them sit under most of the terminal ecosystem.

What "without the edge fraying" means

A cluster is atomic. A cut that would land inside a grapheme drops the whole cluster rather than half of it. Half an emoji is not a narrower emoji, it is mojibake, and a flag cut down the middle is two unrelated regional-indicator letters.

A style that was open stays open — and gets closed. A cut re-emits the styles active at its start and closes them at its end, so the result is self-contained: paste it anywhere and it neither loses its colour nor leaks it into what follows.

The ellipsis is inside the budget, not on top of it. truncate(text, 10) occupies ten columns or fewer, never eleven. That is the property a table column depends on, and getting it wrong is how a layout gains a phantom column under one input.

widest takes lines, not a blob. It accepts any iterable of strings, so the caller says where the boundaries are rather than having a newline convention assumed for them.

Design notes

Ambiguous-width characters count narrow by default, which is what a terminal does unless it is rendering with a CJK font. width(text, { ambiguousIsNarrow: false }) counts them two columns wide for that context — string-width's option, under its name and with its default. Nothing can detect which a terminal is doing, so it is the caller's decision.

Not a terminal emulator. Semicolon-delimited SGR, colon-delimited extended colour and OSC 8 hyperlinks are understood. Every other complete CSI or OSC command is carried through as an opaque zero-width unit, and anything that only looks like an introducer stays plain text.

strip is exported, from the root and as the linegauge/strip subpath whose default export is the strip-ansi drop-in. width measures what it leaves.

An ASCII fast path. A string of printable ASCII is measured by a scan of its code units, so the segmenter is reached only when it earns its cost.

Plugins

linegauge hosts one key, widths, and it answers one question: how many columns a code point occupies on this terminal, when that terminal disagrees with Unicode.

import { register } from 'linegauge/plugin';

register({
  name: 'nerd-font',
  widths: {
    icons: { ranges: [[0xe000, 0xf8ff]], columns: 2, why: 'this Nerd Font draws Private Use icons two columns wide' },
  },
});

The disagreements it exists for are real and local: a Nerd Font that put a two-column icon in the Private Use Area, a code point newer than the table compiled into this release, a font that draws box-drawing characters wide. Each is a fact about one terminal, so the honest shape for it is data the user supplies rather than a constant somebody argues about upstream.

  • Plain data, no functions. { ranges, columns, why } — inclusive code-point pairs, a column count of 0, 1 or 2, and a sentence — so a plugin can arrive as JSON, be diffed, and be printed by npx linegauge check without running its author's code.
  • why is required, which no other key in the family asks for: a width table with no provenance cannot be audited when it turns out wrong, and for ambiguous width, wrong is the normal outcome.
  • Later registrations win, over earlier ones and over the built-in table — the user is the authority on their terminal. A reversed range or a column count of 3 is refused at register() with a code and a fix.
  • Nothing is registered by default, and the seam is installed only while something is, so what the incumbent suites grade is Unicode's answer, and a program with no plugin pays nothing.

The ambiguous-width policy a CJK terminal needs is not a plugin: it is ambiguousIsNarrow, an option with tests behind it.

Migrating

One import per incumbent — each default export is the incumbent's:

- import stringWidth from 'string-width';
+ import stringWidth from 'linegauge';
- import wrapAnsi from 'wrap-ansi';
+ import wrapAnsi from 'linegauge/wrap';
- import stripAnsi from 'strip-ansi';
+ import stripAnsi from 'linegauge/strip';
- import sliceAnsi from 'slice-ansi';
+ import sliceAnsi from 'linegauge/slice';
- import cliTruncate from 'cli-truncate';
+ import cliTruncate from 'linegauge/cli-truncate';
- import widestLine from 'widest-line';
+ import widestLine from 'linegauge/widest-line';

The default export is string-width, byte-for-byte call-compatible, so a transitive copy resolves without a code change too:

{ "overrides": { "string-width": "npm:linegauge@^1" } }

Or let the codemod make the import change: npx burgee migrate --dry-run lists every import it would rewrite — only drop-ins graded level with their incumbent — and npx burgee migrate makes it (Migrate).

Compatibility

The incumbent is the specification. width runs against string-width, wrap against wrap-ansi, slice against slice-ansi, and the cli-truncate and widest-line paths against their namesakes.

The six drop-in paths are graded by each incumbent's own suite, unedited, through compat-oracle; the grades are generated under Benchmarks below and published on the compatibility page. linegauge/cli-truncate passes all 17 of cli-truncate 6.2.0's cases and linegauge/widest-line widest-line 6.0.0's one, level with each incumbent. truncate and widest stay linegauge's own API: truncate counts the ellipsis inside the budget and cuts by measured width, and widest takes any iterable of lines.

Benchmarks

Every number here is produced by npm run bench and published at burgee.interlace.tools/docs/benchmarks.

Graded by the incumbent's own test suite:

| suite | passing | | :-- | --: | | cli-truncate | 17 / 17 | | slice-ansi | 106 / 106 | | string-width | 233 / 233 | | strip-ansi | 8 / 8 | | widest-line | 1 / 1 | | wrap-ansi | 85 / 85 |

Weight, installed and tree-inclusive: 107,698 bytes against 194,329 for the incumbents it replaces — a ratio of 0.5542 (measured).

For agents

  • The same columns everywhere. Nothing here reads process, so a pipe, --json and an agent get the columns a terminal does — which is why burgee, caique and flagstaff measure their output with it.
  • Non-strings answer 0 rather than throwing, so a width call reached with whatever a template produced never takes a program down.
  • A width plugin can be checked before it ships. npx linegauge check ./widths.mjs validates it against the family schema and exits 0, 1 with a code and a fix, or 2 on a usage error.
  • The docs are machine-readable at linegauge.interlace.tools/llms.txt and llms-full.txt.

API

| | | | :-- | :-- | | width(text, { ambiguousIsNarrow, countAnsiEscapeCodes }) | terminal columns the text occupies | | wrap(text, columns, options) | fold to a width, styles preserved across rows | | truncate(text, columns, { position, ellipsis }) | cut to a budget, ellipsis counted inside it | | slice(text, start, end) | the columns [start, end), self-contained | | strip(text) | the text with its escape sequences removed (linegauge/strip) | | widest(lines) | the width of the widest line of any iterable | | lineCount(text, columns) | rows the text occupies at that width | | measure(text) | columns of plain text, no escape scan |

Non-strings answer 0 rather than throwing, because a width function is usually reached with whatever a template produced.

Every export, with its types, is on linegauge.interlace.tools.

Where it sits

Plugins register under the widths key, against the one schema the whole family shares.

burgee, caique, controlroom, flagstaff build on it, and it builds on nothing in this family.

The family

Eleven packages, one repository, one release pipeline. Each installs and works on its own, and each owns one job. Six of them — bellpull, closeout, linegauge, paratext, roundel, seniority — depend on nothing; the others depend only on packages in this table, and every dependency points one way, down the layers.

No package declares a dependency from outside the family; react and react-reconciler are optional peers of controlroom, which npm does not install; vitest is an optional peer of oxmutate, which npm does not install. package-shape-lock holds that for every manifest, and independence-install-lock installs each package alone and finds nothing but the family packages it declares.

Releases are published by one workflow through npm trusted publishing: no npm token is used, and each release carries SLSA provenance, which npm view <package> dist.attestations shows. trusted-publishing-lock keeps both true.

| Package | What it is | Migrates from | | :-- | :-- | :-- | | burgee | The CLI framework: one declaration, every surface | commander, yargs and meow | | roundel | Colour: one output policy, semantic tokens, a theme | chalk, colorette, kleur, picocolors, supports-color and yoctocolors | | flagstaff | The frame loop: spinners, progress, boxes and tables | ora, log-update, boxen and cli-table3 | | caique | Prompts that are flags first, and never hang | @inquirer/core | | linegauge (this package) | Measuring, wrapping, truncating and slicing styled text | string-width, wrap-ansi, strip-ansi, slice-ansi, cli-truncate and widest-line | | paratext | Hyperlinks, images, title, clipboard and notifications | ansi-escapes, terminal-link and term-img | | seniority | Configuration precedence and discovery, with provenance | cosmiconfig, lilconfig, dotenv and rc | | closeout | Exit handlers, terminal restore and a bounded shutdown | signal-exit, exit-hook, restore-cursor, cli-cursor and onetime | | bellpull | Subprocesses, and which executable actually ran | cross-spawn and which | | controlroom | Keyboard-driven terminal screens, inline or full-screen | ink | | oxmutate | Mutation testing on the oxc engine, inside your own Vitest | Stryker |

Every migration guide, and the family-wide compatibility and benchmarks pages, are on burgee.interlace.tools.

Contributing

Issues and pull requests are welcome at ofri-peretz/burgee; read CONTRIBUTING.md first. Report a vulnerability privately, as SECURITY.md describes — never in a public issue.

Licence

MIT © Ofri Peretz — see LICENSE.