linegauge
Guides

Width plugins

When a terminal or font disagrees with Unicode about a code point, a plugin says so as data — ranges, columns and why — and npx linegauge check validates it before it ships.

linegauge hosts one plugin key, widths, and it answers one question: how many columns a code point occupies on this terminal, when that terminal disagrees with Unicode. A Nerd Font that draws Private Use icons two columns wide, a code point newer than the table in this release, a font that draws box-drawing characters wide — each is a fact about one terminal, so it is data the user supplies rather than a constant argued about upstream.

Writing one

A plugin is plain data: a name, and under widths one entry per rule, each with inclusive code-point ranges, a columns count of 0, 1 or 2, and why.

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

why is required, which no other plugin key in the family asks for. A width table with no provenance cannot be audited when it turns out to be wrong, and for ambiguous width, wrong is the normal outcome.

Registering it

branch.mjs
import { width } from 'linegauge';
import { register } from 'linegauge/plugin';

import nerdFont from './nerd-font.mjs';

const icon = '\u{E0A0}';
console.log(width(`${icon} main`));
register(nerdFont);
console.log(width(`${icon} main`));
node branch.mjs
6
7

U+E0A0 is Powerline's branch glyph. Unicode calls it narrow, so width says six until the plugin says two columns, and every wrap, slice, truncate and widest measured after that agrees.

  • Later registrations win, over earlier ones and over the built-in table: the user is the authority on their terminal.
  • Nothing is registered by default, and the override is consulted only while something is, so what the incumbent suites grade is Unicode's answer and a program with no plugin pays nothing.
  • Keys other packages read are ignored, so one plugin file can carry roundel tokens or flagstaff spinners beside its widths.

Refusals

A plugin that would be wrong is refused at register(), with a code and a fix:

refused.mjs
import { register } from 'linegauge/plugin';

try {
  register({ name: 'no-why', widths: { icons: { ranges: [[0xe000, 0xf8ff]], columns: 2 } } });
} catch (error) {
  console.log(error.code);
  console.log(error.message);
  console.log(error.fix);
}
node refused.mjs
E_PLUGIN_SCHEMA
widths.icons.why is required
say which terminal or font disagrees, and how you measured it — ambiguous width is wrong often enough that the next reader needs the provenance

A range that runs backwards is refused rather than tolerated: it would match nothing, register cleanly and never fire, which is the failure that takes longest to find. So is a column count outside 0 to 2.

Checking before it ships

npx linegauge check loads the file without registering it, validates it against the family's plugin schema, and prints what each rule changes:

npx linegauge check nerd-font.mjs
nerd-font — 1 widths
  icons  U+E000..U+F8FF  built-in 1 → 2  — this Nerd Font draws Private Use icons two columns wide
nerd-font: ok

A refusal exits 1 with the code and the fix, and running it with no file is a usage error, exit 2:

backwards.mjs
export default { name: 'backwards', widths: { icons: { ranges: [[0xf8ff, 0xe000]], columns: 2, why: 'a test' } } };
npx linegauge check backwards.mjs
E_PLUGIN_SCHEMA: widths.icons.ranges[0] runs backwards: [63743, 57344]
  fix: each range is [low, high], both integers in U+0000..U+10FFFF, low first

Not a plugin: ambiguous width

The ambiguous-width policy a CJK terminal needs is width(text, { ambiguousIsNarrow: false }), an option with tests behind it, not a plugin (Measuring).

On this page