linegauge 0.3.3 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/dist/check.d.ts +14 -0
- package/dist/check.js +57 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +9 -0
- package/dist/plugin.d.ts +66 -0
- package/dist/plugin.js +125 -0
- package/dist/schema.json +1 -0
- package/dist/width.d.ts +2 -0
- package/dist/width.js +9 -0
- package/package.json +19 -5
package/README.md
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://github.com/ofri-peretz/burgee/tree/main/packages/linegauge" target="blank">
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ofri-peretz/burgee/main/brand-assets/linegauge-lockup.svg" />
|
|
5
|
+
<img src="https://raw.githubusercontent.com/ofri-peretz/burgee/main/brand-assets/linegauge-lockup-light.svg" alt="linegauge" width="360" />
|
|
6
|
+
</picture>
|
|
7
|
+
</a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
Docs: <a href="https://burgee.interlace.tools/docs/packages/linegauge">https://burgee.interlace.tools/docs/packages/linegauge</a>
|
|
12
|
+
</p>
|
|
2
13
|
|
|
3
14
|
**Measuring, wrapping, truncating and slicing styled terminal text — without the edge
|
|
4
15
|
fraying.**
|
|
@@ -154,10 +165,10 @@ its own suite — which this package passes. The runner reports that as a failur
|
|
|
154
165
|
to the incumbent an unexpected pass means a stale annotation; it is counted here as the
|
|
155
166
|
pass it is, and marked rather than left to look like the ones beside it.
|
|
156
167
|
|
|
157
|
-
Weight, installed and tree-inclusive: **
|
|
168
|
+
Weight, installed and tree-inclusive: **84,371 bytes** against **170,342** for the incumbents it replaces — a ratio of **0.4953**.
|
|
158
169
|
## Where it sits
|
|
159
170
|
|
|
160
|
-
|
|
171
|
+
Plugins register under the `widths` key, against the one schema the whole family shares.
|
|
161
172
|
|
|
162
173
|
`burgee`, `caique`, `flagstaff` build on it, and it builds on nothing in this family.
|
|
163
174
|
## Licence
|
package/dist/check.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E1, restated rather than imported: linegauge is an independent product and depends on nobody in
|
|
3
|
+
* the family for its exit contract. `scripts/exit-code-lock.test.ts` keeps the restatements in
|
|
4
|
+
* step.
|
|
5
|
+
*/
|
|
6
|
+
export declare const EXIT_OK = 0;
|
|
7
|
+
export declare const EXIT_RUNTIME = 1;
|
|
8
|
+
export declare const EXIT_USAGE = 2;
|
|
9
|
+
/**
|
|
10
|
+
* Every refusal leaves through here, wherever it was raised: `register()`, or a plugin file that
|
|
11
|
+
* registers itself on import — which throws inside the `import()`, before any line of `inspect`
|
|
12
|
+
* could catch it, and used to reach the bin as a bare message with no code and no fix (R8).
|
|
13
|
+
*/
|
|
14
|
+
export declare function check(argv: readonly string[], write: (s: string) => void): Promise<number>;
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { overrides, PluginError, register, setOverrides, validate } from './plugin.js';
|
|
4
|
+
import { width } from './width.js';
|
|
5
|
+
export const EXIT_OK = 0;
|
|
6
|
+
export const EXIT_RUNTIME = 1;
|
|
7
|
+
export const EXIT_USAGE = 2;
|
|
8
|
+
function refuse(code, message, fix, write) {
|
|
9
|
+
write(`${code}: ${message}\n fix: ${fix}\n`);
|
|
10
|
+
return EXIT_RUNTIME;
|
|
11
|
+
}
|
|
12
|
+
const HEX = 16;
|
|
13
|
+
const MIN_DIGITS = 4;
|
|
14
|
+
const codePoint = (n) => `U+${n.toString(HEX).toUpperCase().padStart(MIN_DIGITS, '0')}`;
|
|
15
|
+
const overridesOf = (plugin) => {
|
|
16
|
+
const widths = plugin?.widths;
|
|
17
|
+
return typeof widths === 'object' && widths !== null ? Object.entries(widths) : [];
|
|
18
|
+
};
|
|
19
|
+
const firstOf = (plugin) => overridesOf(plugin).flatMap(([, o]) => (o.ranges ?? []).map(([low]) => low));
|
|
20
|
+
const shadows = (names) => (names.length === 0 ? '' : ` (replaces ${names.join(', ')})`);
|
|
21
|
+
export async function check(argv, write) {
|
|
22
|
+
try {
|
|
23
|
+
return await inspect(argv, write);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (!(error instanceof PluginError))
|
|
27
|
+
throw error;
|
|
28
|
+
return refuse(error.code, error.message, error.fix, write);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function inspect(argv, write) {
|
|
32
|
+
const file = argv[0];
|
|
33
|
+
if (file === undefined) {
|
|
34
|
+
write('usage: linegauge check <plugin-file>\n');
|
|
35
|
+
return EXIT_USAGE;
|
|
36
|
+
}
|
|
37
|
+
const loaded = (await import(pathToFileURL(resolve(file)).href));
|
|
38
|
+
const plugin = loaded.default ?? loaded;
|
|
39
|
+
for (const registered of [...overrides().keys()])
|
|
40
|
+
setOverrides(registered, {});
|
|
41
|
+
const builtIn = new Map(firstOf(plugin).map((code) => [code, width(String.fromCodePoint(code))]));
|
|
42
|
+
validate(plugin);
|
|
43
|
+
register(plugin);
|
|
44
|
+
const name = plugin.name;
|
|
45
|
+
const rows = overridesOf(plugin).flatMap(([label, o]) => (o.ranges ?? []).map(([low, high]) => {
|
|
46
|
+
const span = low === high ? codePoint(low) : `${codePoint(low)}..${codePoint(high)}`;
|
|
47
|
+
return `${label} ${span} built-in ${String(builtIn.get(low) ?? '?')} → ${String(width(String.fromCodePoint(low)))} — ${o.why}`;
|
|
48
|
+
}));
|
|
49
|
+
write(`${name} — ${String(rows.length)} widths\n`);
|
|
50
|
+
if (rows.length === 0) {
|
|
51
|
+
return refuse('E_NO_CONTRIBUTION', `${name} registers, but contributes nothing linegauge reads`, 'add a `widths` section — a key another package in the family reads is allowed in the same object, but `linegauge check` cannot show it', write);
|
|
52
|
+
}
|
|
53
|
+
for (const row of rows)
|
|
54
|
+
write(` ${row}\n`);
|
|
55
|
+
write(`${name}: ok\n`);
|
|
56
|
+
return EXIT_OK;
|
|
57
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { check, EXIT_RUNTIME } from './check.js';
|
|
3
|
+
const [, , command, ...rest] = process.argv;
|
|
4
|
+
check(command === 'check' ? rest : [command, ...rest].filter((a) => a !== undefined), (s) => void process.stdout.write(s)).then((code) => {
|
|
5
|
+
process.exitCode = code;
|
|
6
|
+
}, (error) => {
|
|
7
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
8
|
+
process.exitCode = EXIT_RUNTIME;
|
|
9
|
+
});
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One plugin-supplied width override: the column count a set of code-point ranges occupies.
|
|
3
|
+
*/
|
|
4
|
+
export interface WidthOverride {
|
|
5
|
+
/** Inclusive `[low, high]` code-point pairs. */
|
|
6
|
+
ranges: readonly (readonly [number, number])[];
|
|
7
|
+
/** 0 for a zero-width mark, 1 narrow, 2 wide. */
|
|
8
|
+
columns: number;
|
|
9
|
+
/** Which terminal or font disagrees, and how it was measured. Required; see the file comment. */
|
|
10
|
+
why: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Later registrations win, which is the point: the user is the authority on their terminal.
|
|
14
|
+
*
|
|
15
|
+
* An empty table **removes** the plugin rather than recording that it contributes nothing.
|
|
16
|
+
* `overrides()` is what `linegauge check` prints and what a caller inspects, and a name in it
|
|
17
|
+
* with no ranges under it reads as *this plugin is active* when the truth is the opposite.
|
|
18
|
+
*
|
|
19
|
+
* The seam in `width.ts` is installed only while something is registered, so the last plugin
|
|
20
|
+
* leaving puts `measure` back on the path with no call in it at all.
|
|
21
|
+
*/
|
|
22
|
+
export declare function setOverrides(name: string, widths: Record<string, WidthOverride>): void;
|
|
23
|
+
/** Every override currently registered, by plugin name — what `linegauge check` prints. */
|
|
24
|
+
export declare function overrides(): ReadonlyMap<string, Record<string, WidthOverride>>;
|
|
25
|
+
/**
|
|
26
|
+
* The plugin contract version. One number for the family — the same `1` flagstaff, caique,
|
|
27
|
+
* closeout and paratext declare, written out rather than imported for the reason above.
|
|
28
|
+
*/
|
|
29
|
+
export declare const CONTRACT = 1;
|
|
30
|
+
/**
|
|
31
|
+
* The keys linegauge reads. Declared structurally: any object with these fields is a plugin
|
|
32
|
+
* here, whatever else it carries.
|
|
33
|
+
*/
|
|
34
|
+
export interface Plugin {
|
|
35
|
+
name: string;
|
|
36
|
+
contract?: number;
|
|
37
|
+
/** Width overrides by name. The key is the author's label; see {@link validate}. */
|
|
38
|
+
widths?: Record<string, WidthOverride>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Two codes, both already in the family's vocabulary
|
|
42
|
+
* (`scripts/plugin-error-vocabulary-lock.test.ts`).
|
|
43
|
+
*/
|
|
44
|
+
export type PluginErrorCode = 'E_PLUGIN_SCHEMA' | 'E_PLUGIN_CONTRACT' | 'E_NO_CONTRIBUTION';
|
|
45
|
+
/** A refused plugin says what is wrong and what to do about it — the family's one vocabulary. */
|
|
46
|
+
export declare class PluginError extends Error {
|
|
47
|
+
readonly code: PluginErrorCode;
|
|
48
|
+
readonly fix: string;
|
|
49
|
+
constructor(code: PluginErrorCode, message: string, fix: string);
|
|
50
|
+
}
|
|
51
|
+
type Problem = {
|
|
52
|
+
code: PluginErrorCode;
|
|
53
|
+
line: string;
|
|
54
|
+
fix: string;
|
|
55
|
+
};
|
|
56
|
+
/** Everything wrong with a plugin document, in the family's vocabulary. Empty means it registers. */
|
|
57
|
+
export declare function problems(plugin: unknown): Problem[];
|
|
58
|
+
/** Throws on the first problem, in the family's vocabulary. */
|
|
59
|
+
export declare function validate(plugin: unknown): asserts plugin is Plugin;
|
|
60
|
+
/**
|
|
61
|
+
* Register a plugin's width overrides. Later registrations win over earlier ones, and over the
|
|
62
|
+
* built-in tables — which is the point: the built-in answer is right for most terminals and the
|
|
63
|
+
* user is the authority on theirs.
|
|
64
|
+
*/
|
|
65
|
+
export declare function register(plugin: unknown): void;
|
|
66
|
+
export {};
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { setClaim } from './width.js';
|
|
2
|
+
let flattened = [];
|
|
3
|
+
const byPlugin = new Map();
|
|
4
|
+
function overridden(code) {
|
|
5
|
+
let found;
|
|
6
|
+
for (let i = 0; i < flattened.length; i += 3) {
|
|
7
|
+
if (code >= flattened[i] && code <= flattened[i + 1])
|
|
8
|
+
found = flattened[i + 2];
|
|
9
|
+
}
|
|
10
|
+
return found;
|
|
11
|
+
}
|
|
12
|
+
export function setOverrides(name, widths) {
|
|
13
|
+
if (Object.keys(widths).length === 0)
|
|
14
|
+
byPlugin.delete(name);
|
|
15
|
+
else
|
|
16
|
+
byPlugin.set(name, widths);
|
|
17
|
+
const rows = [];
|
|
18
|
+
for (const table of byPlugin.values()) {
|
|
19
|
+
for (const { ranges, columns } of Object.values(table)) {
|
|
20
|
+
for (const [low, high] of ranges)
|
|
21
|
+
rows.push(low, high, columns);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
flattened = rows;
|
|
25
|
+
setClaim(rows.length === 0 ? undefined : overridden);
|
|
26
|
+
}
|
|
27
|
+
export function overrides() {
|
|
28
|
+
return byPlugin;
|
|
29
|
+
}
|
|
30
|
+
export const CONTRACT = 1;
|
|
31
|
+
export class PluginError extends Error {
|
|
32
|
+
code;
|
|
33
|
+
fix;
|
|
34
|
+
constructor(code, message, fix) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.fix = fix;
|
|
38
|
+
this.name = 'PluginError';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
42
|
+
const MAX_CODE_POINT = 0x10_ff_ff;
|
|
43
|
+
const MAX_COLUMNS = 2;
|
|
44
|
+
function rangeProblem(label, range) {
|
|
45
|
+
if (!Array.isArray(range) || range.length !== 2)
|
|
46
|
+
return `${label} is not a [low, high] pair`;
|
|
47
|
+
const [low, high] = range;
|
|
48
|
+
if (!Number.isInteger(low) || !Number.isInteger(high))
|
|
49
|
+
return `${label} has a non-integer bound`;
|
|
50
|
+
const lo = low;
|
|
51
|
+
const hi = high;
|
|
52
|
+
if (lo < 0 || hi > MAX_CODE_POINT)
|
|
53
|
+
return `${label} is outside U+0000..U+10FFFF`;
|
|
54
|
+
if (lo > hi)
|
|
55
|
+
return `${label} runs backwards: [${String(lo)}, ${String(hi)}]`;
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
function headerProblems(plugin) {
|
|
59
|
+
const out = [];
|
|
60
|
+
if (typeof plugin['name'] !== 'string' || plugin['name'] === '') {
|
|
61
|
+
out.push({ code: 'E_PLUGIN_SCHEMA', line: 'a plugin needs a non-empty `name`', fix: 'add `name: "my-widths"` — it is what `linegauge check` and a later registration call it' });
|
|
62
|
+
}
|
|
63
|
+
const contract = plugin['contract'];
|
|
64
|
+
if (contract !== undefined && contract !== CONTRACT) {
|
|
65
|
+
out.push({
|
|
66
|
+
code: 'E_PLUGIN_CONTRACT',
|
|
67
|
+
line: `contract ${String(contract)} is not ${String(CONTRACT)}`,
|
|
68
|
+
fix: `set \`contract: ${String(CONTRACT)}\`, or drop the field — the family is on one version`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
function overrideProblems(name, value) {
|
|
74
|
+
if (!isRecord(value))
|
|
75
|
+
return [{ code: 'E_PLUGIN_SCHEMA', line: `widths.${name} is not an object`, fix: 'each override is { ranges, columns, why }' }];
|
|
76
|
+
const out = [];
|
|
77
|
+
const ranges = value['ranges'];
|
|
78
|
+
if (!Array.isArray(ranges) || ranges.length === 0) {
|
|
79
|
+
out.push({ code: 'E_PLUGIN_SCHEMA', line: `widths.${name}.ranges is a non-empty array of [low, high] pairs`, fix: 'ranges: [[0xE000, 0xF8FF]]' });
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
for (const [i, range] of ranges.entries()) {
|
|
83
|
+
const problem = rangeProblem(`widths.${name}.ranges[${String(i)}]`, range);
|
|
84
|
+
if (problem !== undefined)
|
|
85
|
+
out.push({ code: 'E_PLUGIN_SCHEMA', line: problem, fix: 'each range is [low, high], both integers in U+0000..U+10FFFF, low first' });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const columns = value['columns'];
|
|
89
|
+
if (!Number.isInteger(columns) || columns < 0 || columns > MAX_COLUMNS) {
|
|
90
|
+
out.push({ code: 'E_PLUGIN_SCHEMA', line: `widths.${name}.columns is 0, 1 or 2`, fix: '0 for a zero-width mark, 1 narrow, 2 wide' });
|
|
91
|
+
}
|
|
92
|
+
if (typeof value['why'] !== 'string' || value['why'] === '') {
|
|
93
|
+
out.push({
|
|
94
|
+
code: 'E_PLUGIN_SCHEMA',
|
|
95
|
+
line: `widths.${name}.why is required`,
|
|
96
|
+
fix: 'say which terminal or font disagrees, and how you measured it — ambiguous width is wrong often enough that the next reader needs the provenance',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
export function problems(plugin) {
|
|
102
|
+
if (!isRecord(plugin)) {
|
|
103
|
+
return [{ code: 'E_PLUGIN_SCHEMA', line: 'a plugin is an object', fix: 'export default { name: "my-widths", widths: { … } }' }];
|
|
104
|
+
}
|
|
105
|
+
const out = headerProblems(plugin);
|
|
106
|
+
const widths = plugin['widths'];
|
|
107
|
+
if (widths === undefined)
|
|
108
|
+
return out;
|
|
109
|
+
if (!isRecord(widths)) {
|
|
110
|
+
out.push({ code: 'E_PLUGIN_SCHEMA', line: '`widths` is an object keyed by name', fix: 'widths: { "nerd-font-icons": { ranges: [[0xE000, 0xF8FF]], columns: 2, why: "…" } }' });
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
for (const [name, value] of Object.entries(widths))
|
|
114
|
+
out.push(...overrideProblems(name, value));
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
export function validate(plugin) {
|
|
118
|
+
const [first] = problems(plugin);
|
|
119
|
+
if (first !== undefined)
|
|
120
|
+
throw new PluginError(first.code, first.line, first.fix);
|
|
121
|
+
}
|
|
122
|
+
export function register(plugin) {
|
|
123
|
+
validate(plugin);
|
|
124
|
+
setOverrides(plugin.name, plugin.widths ?? {});
|
|
125
|
+
}
|
package/dist/schema.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/schema.json","title":"flagstaff plugin","description":"A plugin is one plain object. Everything in it is data that can be read without running it; the only functions allowed are a component's `static` (required) and `frame` (optional). A spinner or component without a static projection is refused at register().","type":"object","required":["name"],"additionalProperties":true,"properties":{"name":{"type":"string","minLength":1,"description":"The plugin's name; also the prefix a host may use when two plugins contribute the same key."},"contract":{"type":"integer","minimum":1,"description":"The plugin contract this object follows. A host refuses a newer contract than it knows."},"tokens":{"type":"object","description":"A roundel theme: semantic token name to a hex colour, contrast-checked when flown.","additionalProperties":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"propertyNames":{"enum":["error","warn","ok","hint","muted","command","flag","value","heading","ground"],"description":"roundel's ten semantic tokens, and nothing else — `roundel`'s own validate() refuses any other name."}},"glyphs":{"type":"object","description":"Symbols by meaning: `ok`, `fail`, `warn`, `info`, `running`. A plugin that ships glyphs changes every built-in that draws one.","additionalProperties":{"type":"string","minLength":1}},"spinners":{"type":"object","description":"Spinner styles by name, in cli-spinners' shape plus the static projection.","additionalProperties":{"$ref":"#/$defs/spinner"}},"borders":{"type":"object","description":"Border styles a box can be drawn with, by name.","additionalProperties":{"$ref":"#/$defs/border"}},"components":{"type":"object","description":"Components by name: `static(state)` returns the text a pipe, an agent or a screen reader gets; `frame(t, state)` is the optional animated form.","additionalProperties":{"$ref":"#/$defs/component"}},"capabilities":{"$ref":"#/$defs/capabilities"},"widths":{"$ref":"#/$defs/widths"},"resolvers":{"type":"object","description":"bellpull resolvers by name: extra directories searched for an executable, before PATH (negative rank) or after it (positive). Two resolvers with one name shadow; two names both apply, in rank order.","additionalProperties":{"$ref":"#/$defs/resolver"}},"widgets":{"type":"object","description":"caique prompt widgets by kind. The six built-in kinds — text, confirm, select, multiselect, password, path — cannot be replaced: a plugin adds kinds of its own.","additionalProperties":{"$ref":"#/$defs/widget"}},"handlers":{"type":"array","description":"closeout exit handlers. The phase decides the order they run in, not their position in this list.","items":{"$ref":"#/$defs/handler"}},"sources":{"type":"object","description":"seniority configuration sources by name, ranked against the built-in layers: a flag is 0 and a declared default is 40, and a plugin source sits strictly between.","additionalProperties":{"$ref":"#/$defs/source"}},"commands":{"type":"array","description":"burgee commands this plugin contributes. Each is read by exactly the code a program's own command is, and a burgee plugin must declare `contract`.","items":{"$ref":"#/$defs/command"}},"hooks":{"type":"object","description":"burgee lifecycle hooks. preRun opens around a command, and exactly one of postRun or onError closes.","additionalProperties":false,"properties":{"preRun":{"$ref":"#/$defs/hook"},"postRun":{"$ref":"#/$defs/hook"},"onError":{"$ref":"#/$defs/hook"}}},"enforce":{"type":"string","pattern":"^(pre|post)$","description":"burgee: order this plugin's hooks before (`pre`) or after (`post`) the others."}},"$defs":{"spinner":{"type":"object","required":["frames","interval","static"],"properties":{"frames":{"type":"array","items":{"type":"string"},"minItems":1},"interval":{"type":"integer","minimum":1,"description":"Milliseconds between frames on a terminal."},"static":{"type":"string","description":"What a pipe prints instead of the animation."}}},"component":{"type":"object","required":["static"],"properties":{"static":{"description":"(state) => string. Required: the projection every non-terminal mode prints."},"frame":{"description":"(t, state) => string. Optional: the frame at t milliseconds since hoisting."},"sample":{"type":"object","required":["running","done"],"description":"Two states to *show* this component with: `flagstaff check` and the docs gallery render `running` then `done`. Omitted, they assume `{ phase: 'running' }` and `{ phase: 'done' }` and say so in the output. The loop never reads it — a running program's state comes from the program.","properties":{"running":{"description":"The state to open with."},"done":{"description":"The state to close with."}}},"interval":{"type":"integer","minimum":1,"description":"Milliseconds between repaints when `frame` is given; 80 when omitted."}}},"border":{"type":"object","required":["topLeft","top","topRight","left","right","bottomLeft","bottom","bottomRight"],"description":"cli-boxes' shape exactly, so that corpus imports unchanged.","properties":{"topLeft":{"type":"string"},"top":{"type":"string"},"topRight":{"type":"string"},"left":{"type":"string"},"right":{"type":"string"},"bottomLeft":{"type":"string"},"bottom":{"type":"string"},"bottomRight":{"type":"string"}}},"capability":{"type":"object","required":["name","osc","when","encode","fallback"],"additionalProperties":false,"description":"One paratext capability: what it says to the terminal, when the terminal is believed to understand it, and what prints when it does not. No functions, so it travels through JSON. `encode` and `fallback` are templates: `{field}` is the field's value, `{field|base64}` is it base64-encoded, and `[ … ]` is emitted only when every field inside it has a value.","properties":{"name":{"type":"string","minLength":1,"description":"How callers name it. Registering an existing name replaces it — how a caller corrects a guess we got wrong."},"osc":{"description":"The OSC code this speaks, or 'BEL' for the bell and for protocols that are not OSC at all, such as Kitty's.","oneOf":[{"type":"integer","minimum":0},{"const":"BEL"}]},"when":{"type":"object","description":"When the terminal is believed to understand it. Every clause must hold; `termProgram` and `envAny` are ORs within themselves. Guesses — no terminal answers 'do you do OSC 1337' — and therefore data a caller can replace.","additionalProperties":false,"properties":{"tty":{"type":"boolean","description":"Refuse a pipe. Almost always true: a file that receives OSC gets control bytes in it."},"termProgram":{"type":"array","items":{"type":"string"},"description":"Any one of these TERM_PROGRAM values."},"envAny":{"type":"array","items":{"type":"string"},"description":"Any one of these environment variables merely being set, as VTE announces itself."},"term":{"type":"string","description":"An exact TERM — Kitty is xterm-kitty."}}},"encode":{"type":"string","minLength":1,"description":"The bytes, as a template, for a terminal that does understand."},"fallback":{"type":"string","description":"What to print when it does not — PRINCIPLES rule 6. '' is a legitimate answer, a window title having nothing to say in a log; absence is not, which is why this is required and may be ''."}},"examples":[{"name":"kitty-image","osc":"BEL","when":{"tty":true,"term":"xterm-kitty"},"encode":"\u001b_Ga=T,f=100;{base64}\u001b\\","fallback":"{caption}"}]},"capabilities":{"type":"object","description":"paratext capabilities by name — the OSC section of a plugin, read the way flagstaff reads `spinners`.","additionalProperties":{"$ref":"#/$defs/capability"}},"capabilityDocument":{"description":"What paratext's `check()` accepts. Two shapes, and only one of them survives 1.0.","oneOf":[{"description":"The family shape: a plugin carrying its capabilities under `capabilities`.","type":"object","required":["name","capabilities"],"properties":{"name":{"type":"string","minLength":1},"contract":{"type":"integer","minimum":1},"capabilities":{"$ref":"#/$defs/capabilities"}}},{"deprecated":true,"description":"Deprecated: one capability as the whole document, the shape paratext's schema had before this one absorbed it. Accepted for one minor release, removed at 1.0 — `check()` validates it and says so.","$ref":"#/$defs/capability"}]},"widthRange":{"type":"array","description":"One inclusive code-point range, as [low, high]. A single code point is written [n, n].","items":{"type":"integer","minimum":0,"maximum":1114111},"minItems":2,"maxItems":2},"widthOverride":{"type":"object","description":"A linegauge width override: the column count a named set of code-point ranges occupies, for a terminal that disagrees with the Unicode tables. Data only — no function, so it can be written in a config file, diffed, and printed by `linegauge check` without running anyone code.","required":["ranges","columns","why"],"additionalProperties":false,"properties":{"ranges":{"type":"array","description":"The code points this override applies to.","items":{"$ref":"#/$defs/widthRange"},"minItems":1},"columns":{"type":"integer","description":"Columns each cluster in those ranges occupies: 0 for a zero-width mark, 1 narrow, 2 wide.","minimum":0,"maximum":2},"why":{"type":"string","description":"The terminal or the reason. Required, because a width table with no provenance is one nobody can audit when it turns out to be wrong — which is the normal outcome for ambiguous width.","minLength":1}}},"widths":{"type":"object","description":"linegauge width overrides by name — the measurement section of a plugin, read the way flagstaff reads `spinners`.","additionalProperties":{"$ref":"#/$defs/widthOverride"}},"resolver":{"type":"object","required":["rank","paths"],"description":"One bellpull resolver: where to look for an executable, and when.","properties":{"rank":{"type":"number","description":"Search order against PATH: negative searches before it, positive after."},"paths":{"type":"array","minItems":1,"description":"Directories to search. `{VAR}` is substituted from the environment; each must be absolute, or start with a `{VAR}` that holds an absolute path.","items":{"type":"string","minLength":1,"pattern":"^(/|\\{|[A-Za-z]:[\\\\/]|\\\\\\\\)"}},"extensions":{"type":"array","items":{"type":"string"},"description":"Extensions to try on Windows, in place of PATHEXT."},"when":{"type":"object","description":"When the resolver applies. Leave it out and it always does.","properties":{"platform":{"type":"array","items":{"type":"string"},"description":"process.platform values it applies on."},"envAny":{"type":"array","items":{"type":"string"},"description":"It applies when any of these environment variables is set."}}}}},"widget":{"type":"object","required":["static"],"description":"One caique widget: how a prompt kind of the plugin's own is drawn.","properties":{"static":{"description":"(spec) => string. Required: what a pipe, an agent or a screen reader gets instead of the interactive prompt."},"frame":{"description":"(t, spec) => string. Optional: the animated form; leave it out and the widget is line-mode only."},"sample":{"type":"object","required":["running","done"],"description":"Two named states of plain data that `caique check` renders the widget with."}}},"handler":{"type":"object","required":["name","run"],"description":"One closeout exit handler.","properties":{"name":{"type":"string","minLength":1,"description":"How the handler is named in a report, including the one the deadline prints when it does not return."},"phase":{"type":"string","pattern":"^(flush|release)$","description":"When it runs. Defaults to `release`; `restore` is closeout's own last phase and a plugin may not use it."},"run":{"description":"(info) => void | Promise<void>. Required: the cleanup itself."}}},"source":{"type":"object","required":["rank"],"description":"One seniority source. Exactly one of `values` (static) or `read(runtime)` (fetched) gives its answer.","properties":{"rank":{"type":"integer","minimum":1,"maximum":39,"description":"Precedence, strictly between the flag (0) and the declared default (40): a source may not beat what the user typed, nor sink below the default."},"values":{"type":"object","description":"Option name to value."},"read":{"description":"(runtime) => { values, location? } | undefined."},"location":{"type":"string","description":"The file, URL or variable set a person would go and look at."}}},"option":{"type":"object","required":["type"],"description":"One burgee option, keyed by its camelCase name; the flag is its kebab-case form.","properties":{"type":{"type":"string","pattern":"^(string|boolean|number)$"},"description":{"type":"string"},"short":{"type":"string","minLength":1},"required":{"type":"boolean"},"env":{"type":"string"},"choices":{"type":"array","items":{"type":"string"}},"dependsOn":{"type":"array","items":{"type":"string"},"description":"Options that must be set with this one; each must be another option of the same command."},"exclusive":{"type":"array","items":{"type":"string"},"description":"Options that may not be set with this one; each must be another option of the same command."},"deprecated":{"description":"What replaces this option, e.g. '--force'. `true` alone, or an empty string, is refused: a deprecation names its replacement."}}},"command":{"type":"object","required":["path"],"description":"One burgee command node, declared exactly as a program's own command is.","properties":{"path":{"type":"array","minItems":1,"items":{"type":"string","minLength":1},"description":"The words a user types. May not repeat a path the program or an earlier plugin already declares."},"description":{"type":"string"},"options":{"type":"object","additionalProperties":{"$ref":"#/$defs/option"}},"effects":{"type":"string","pattern":"^(read_only|idempotent|non_idempotent|withheld)$","description":"What running it does to the world. Required on any command that runs; `withheld` serves it to people and keeps it out of the MCP tool list."},"deprecated":{"description":"What replaces this command. `true` alone, or an empty string, is refused."},"run":{"description":"(ctx) => unknown. What the command does; its return value is the `--json` envelope's data."},"load":{"description":"() => Promise<module>. A lazy form of `run`, imported on dispatch."}}},"hook":{"type":"object","required":["handler"],"description":"One burgee hook.","properties":{"filter":{"type":"object","description":"`{ command: RegExp }`: fire only for commands whose path matches."},"handler":{"description":"(ctx) => void | Promise<void>. Required."}}}}}
|
package/dist/width.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** Installed by `plugin.ts` when the first override registers, and cleared when the last one goes. */
|
|
2
|
+
export declare function setClaim(fn: ((code: number) => number | undefined) | undefined): void;
|
|
1
3
|
/**
|
|
2
4
|
* Columns a string of *plain* text occupies — no escape scan. The wrapper below has
|
|
3
5
|
* already split its input into text runs and complete sequences, so rescanning would only
|
package/dist/width.js
CHANGED
|
@@ -178,9 +178,18 @@ function hangulColumns(visible, ambiguousIsWide) {
|
|
|
178
178
|
}
|
|
179
179
|
return columns;
|
|
180
180
|
}
|
|
181
|
+
let claim;
|
|
182
|
+
export function setClaim(fn) {
|
|
183
|
+
claim = fn;
|
|
184
|
+
}
|
|
181
185
|
export function measure(text, ambiguousIsWide = false) {
|
|
182
186
|
let columns = 0;
|
|
183
187
|
for (const { segment } of segmenter().segment(text)) {
|
|
188
|
+
const claimed = claim?.(segment.codePointAt(0) ?? 0);
|
|
189
|
+
if (claimed !== undefined) {
|
|
190
|
+
columns += claimed;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
184
193
|
if (ZERO_WIDTH_CLUSTER().test(segment))
|
|
185
194
|
continue;
|
|
186
195
|
if (RGI_EMOJI().test(segment) || isUnqualifiedEmojiSequence(segment)) {
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linegauge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "A printer's line gauge \u2014 the steel rule marked in picas and points. Measuring, wrapping, truncating and slicing styled terminal text without the edge fraying \u2014 grapheme-correct over Intl.Segmenter. Drop-in paths for string-width, wrap-ansi, strip-ansi and slice-ansi. Zero dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"sideEffects": ["./dist/cli.js"],
|
|
7
8
|
"engines": {
|
|
8
9
|
"node": ">=24"
|
|
9
10
|
},
|
|
@@ -37,7 +38,13 @@
|
|
|
37
38
|
"types": "./dist/strip.d.ts",
|
|
38
39
|
"import": "./dist/strip.js",
|
|
39
40
|
"default": "./dist/strip.js"
|
|
40
|
-
}
|
|
41
|
+
},
|
|
42
|
+
"./plugin": {
|
|
43
|
+
"types": "./dist/plugin.d.ts",
|
|
44
|
+
"import": "./dist/plugin.js",
|
|
45
|
+
"default": "./dist/plugin.js"
|
|
46
|
+
},
|
|
47
|
+
"./schema.json": "./dist/schema.json"
|
|
41
48
|
},
|
|
42
49
|
"files": [
|
|
43
50
|
"dist",
|
|
@@ -45,7 +52,7 @@
|
|
|
45
52
|
"!dist/**/*.test.*"
|
|
46
53
|
],
|
|
47
54
|
"scripts": {
|
|
48
|
-
"build": "tsc -p tsconfig.build.json && node ../../scripts/strip-comments.mjs dist",
|
|
55
|
+
"build": "tsc -p tsconfig.build.json && node ../../scripts/schema-to-dist.mjs && node ../../scripts/strip-comments.mjs dist",
|
|
49
56
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
50
57
|
"test": "vitest run --passWithNoTests",
|
|
51
58
|
"coverage": "vitest run --coverage.enabled",
|
|
@@ -56,7 +63,7 @@
|
|
|
56
63
|
"url": "git+https://github.com/ofri-peretz/burgee.git",
|
|
57
64
|
"directory": "packages/linegauge"
|
|
58
65
|
},
|
|
59
|
-
"homepage": "https://
|
|
66
|
+
"homepage": "https://burgee.interlace.tools/docs/packages/linegauge",
|
|
60
67
|
"bugs": {
|
|
61
68
|
"url": "https://github.com/ofri-peretz/burgee/issues"
|
|
62
69
|
},
|
|
@@ -72,9 +79,16 @@
|
|
|
72
79
|
"width",
|
|
73
80
|
"grapheme",
|
|
74
81
|
"ansi",
|
|
75
|
-
"unicode"
|
|
82
|
+
"unicode",
|
|
83
|
+
"plugin",
|
|
84
|
+
"plugins",
|
|
85
|
+
"extensible",
|
|
86
|
+
"zero-dependency"
|
|
76
87
|
],
|
|
77
88
|
"devDependencies": {
|
|
78
89
|
"vitest": "^5.0.1"
|
|
90
|
+
},
|
|
91
|
+
"bin": {
|
|
92
|
+
"linegauge": "./dist/cli.js"
|
|
79
93
|
}
|
|
80
94
|
}
|