# linegauge

> A printer's line gauge — the steel rule marked in picas and points. Measuring, wrapping, truncating and slicing styled terminal text without the edge fraying — grapheme-correct over Intl.Segmenter. Drop-in paths for string-width, wrap-ansi, strip-ansi and slice-ansi. Zero dependencies.

Source: https://linegauge.interlace.tools/docs

**Measuring, wrapping, truncating and slicing styled terminal text — without the edge
fraying.**

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.

Zero dependencies. Grapheme-correct over the platform's own `Intl.Segmenter`.

Drop-in paths for **string-width** (the default export), **wrap-ansi**, **strip-ansi** and
**slice-ansi**. Nothing here reads `process`, so a pipe, `--json` and an agent get the same
columns a terminal does — burgee, caique and flagstaff measure their output with it.

```bash
npm i linegauge
```

## 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.

## Use

```js
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…'
slice(styled, 2, 4); // columns 2 and 3, styles intact
widest(['a', 'bbb', 'cc']); // 3
```

### The default export is `string-width`

Byte-for-byte call-compatible, so this resolves without a code change:

```json
{ "overrides": { "string-width": "npm:linegauge@^0.4" } }
```

## 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.

## Graded by the packages it replaces

The incumbent is the specification. `width` runs against `string-width`, `wrap` against
`wrap-ansi`, `slice` against `slice-ansi` and `truncate` against `cli-truncate`.

## API

| | |
| :-- | :-- |
| `width(text, { 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 |
| `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.

## Design notes

**Ambiguous-width characters count narrow**, which is what a terminal does unless told it is
rendering an East Asian locale. `string-width` makes that an option; nothing above this has
ever needed the other answer, so it is not one here.

**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.

**Still at the Design→Build gate:** an exported `strip`, and the ASCII fast path — a byte
scan when the string has no non-ASCII code unit, so the segmenter is reached only when it
earns its cost.

## Plugins

**linegauge hosts no plugin key, and that is a decision rather than an omission.** Every
other package in the family hosts one — `tokens` in roundel, `spinners` and `borders` and
`glyphs` and `components` in flagstaff, `capabilities` in paratext, `sources` in seniority,
`handlers` in closeout, `resolvers` in bellpull, `widgets` in caique. Each of those keys sits
over a question with more than one right answer: which colour, which glyph, which terminal,
where configuration lives, how an executable is found. A plugin settles it for one program
without making anybody else wrong.

These six functions are not that kind of question. `width('古代')` is 4 because Unicode
classes those code points East Asian Wide and a terminal gives each of them two columns;
`slice` returns the columns it was asked for or it returns the wrong string. A plugin key
here would not extend what linegauge does — it would let a caller redefine what the terminal
does, silently, for everything above it. The failure would not even surface as an error: a
box comes out a column short, a table gains a phantom column, and nothing throws.

There is a second reason, and it is the one that decides it. This package's correctness is
differential — `width` is graded against `string-width`, `wrap` against `wrap-ansi`, `slice`
against `slice-ansi`, `truncate` against `cli-truncate`. A registered contribution would put
answers under the published pass rate that no grader ever saw, so the number would stop
meaning what it says.

The two things that genuinely vary are already handled without a registry:

- **The Unicode data.** The Wide and Fullwidth table is Unicode's, and cluster boundaries
  come from the platform's `Intl.Segmenter`. When Unicode ships a version the table changes —
  that is a release of this package, re-graded, not a registration a caller can make.
- **The environment.** How wide the terminal is, and whether there is one, are the caller's
  to pass; nothing here reads `process`. That is a parameter, not a plugin.

The family's plugin contract records this refusal next to the other layers' keys (R5a), so
"no key" is one of the contract's answers rather than a hole in it. If a real second answer
ever arrives — an ambiguous-width policy some terminal actually needs — it lands as an option
with a differential test behind it, because the graders have to see it.

## Benchmarks

Every number here is produced by `npm run bench` and published at [/docs/benchmarks](https://burgee.interlace.tools/docs/benchmarks).

Graded by the incumbent's own test suite:

| suite | passing |
| :-- | --: |
| `slice-ansi` | 15 / 15 ¹ |
| `string-width` | 229 / 229 |
| `strip-ansi` | 8 / 8 |
| `wrap-ansi` | 80 / 80 |

¹ A case the incumbent marks `test.failing()` — it cannot do the thing and says so in
its own suite — which this package passes. The runner reports that as a failure, because
to the incumbent an unexpected pass means a stale annotation; it is counted here as the
pass it is, and marked rather than left to look like the ones beside it.

Weight, installed and tree-inclusive: **84,596 bytes** against **194,329** for the incumbents it replaces — a ratio of **0.4353**.
## Where it sits

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

`burgee`, `caique`, `flagstaff` build on it, and it builds on nothing in this family.
## Licence

MIT
