flagstaff 0.1.0 → 0.2.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ofri Peretz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,4 +1,22 @@
1
- # flagstaff
1
+ <p align="center">
2
+ <a href="https://github.com/ofri-peretz/burgee/tree/main/packages/flagstaff" target="blank">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ofri-peretz/burgee/main/brand-assets/flagstaff-lockup.svg" />
5
+ <img src="https://raw.githubusercontent.com/ofri-peretz/burgee/main/brand-assets/flagstaff-lockup-light.svg" alt="flagstaff" width="360" />
6
+ </picture>
7
+ </a>
8
+ </p>
9
+
10
+ <p align="center">
11
+ The staff the flag flies from — a terminal frame loop that an agent can read.
12
+ </p>
13
+
14
+ <p align="center">
15
+ <a href="https://www.npmjs.com/package/flagstaff"><img src="https://img.shields.io/npm/v/flagstaff?style=flat-square&color=0a6b47" alt="npm version" /></a>
16
+ <img src="https://img.shields.io/badge/dependencies-roundel-0a6b47?style=flat-square" alt="One dependency: roundel" />
17
+ <img src="https://img.shields.io/badge/Node.js-24+-green.svg?style=flat-square" alt="Node.js 24+" />
18
+ <img src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" alt="License: MIT" />
19
+ </p>
2
20
 
3
21
  ora animates a spinner and, off a terminal, prints frames anyway — `\r` after `\r` into the
4
22
  log an agent reads back. Ink fixes the terminal by shipping React and a layout engine.
@@ -316,8 +334,8 @@ never the plugin registry; `flagstaff/plugin` 8.4 KB, of which 2.4 KB is the sch
316
334
  `flagstaff/spinner` 9.4 KB; `flagstaff/ora` 46.5 KB — 55.9 KB with roundel counted, against
317
335
  ora's own 113.6 KB; `flagstaff/log-update` 29.6 KB, reaching **no package at all**, against
318
336
  log-update's own 113.4 KB across sixteen; `flagstaff/boxen` 33.7 KB — 43.0 KB with roundel
319
- counted, against boxen's own 151.4 KB across fourteen; `flagstaff/cli-table3` 32.9 KB —
320
- 42.3 KB with roundel, against cli-table3's own 161.7 KB across seven. The three façades share `wrap.js` and
337
+ counted, against boxen's own 132.4 KB across nineteen; `flagstaff/cli-table3` 32.9 KB —
338
+ 42.3 KB with roundel, against cli-table3's own 106.0 KB across seven. The three façades share `wrap.js` and
321
339
  `width.js`, and the first two share `cursor.js`; none reaches another's port, and none
322
340
  reaches the core. `sideEffects: false` lets a
323
341
  bundler drop what a program does not use. ESM with a `default` condition, so
@@ -333,6 +351,22 @@ Every component, every registered plugin and every border is on the
333
351
  which is generated by running them — the static projection beside the animation, in all
334
352
  five modes.
335
353
 
336
- Part of the [burgee](https://github.com/ofri-peretz/burgee) family: a CLI on burgee declares
337
- what it is, roundel carries its colours, flagstaff flies it, caique answers back. Each is
338
- an independent package; none requires the others.
354
+ ## Following along
355
+
356
+ The intent and design are committed before the code is, so you can read what it will be —
357
+ and argue with it — before it exists:
358
+
359
+ - [`.sdlc/intents/flagstaff/`](https://github.com/ofri-peretz/burgee/tree/main/.sdlc/intents/flagstaff)
360
+ — the intent and the design.
361
+ - [Open an issue](https://github.com/ofri-peretz/burgee/issues) if your CLI needs something
362
+ this list does not cover. That is the cheapest time to say so.
363
+
364
+ ---
365
+
366
+ Part of the [burgee](https://github.com/ofri-peretz/burgee) family: a CLI on
367
+ [burgee](https://www.npmjs.com/package/burgee) declares what it is,
368
+ [roundel](https://www.npmjs.com/package/roundel) carries its colours, flagstaff flies it, and
369
+ [caique](https://www.npmjs.com/package/caique) answers back. Each is an independent package;
370
+ none requires the others.
371
+
372
+ MIT © Ofri Peretz — see [LICENSE](./LICENSE).
package/dist/box.js CHANGED
@@ -1,7 +1,7 @@
1
+ import { width } from 'linegauge';
2
+ import { wrap } from 'linegauge/wrap';
1
3
  import { muted } from 'roundel/tokens';
2
4
  import { lookupBorder } from './plugin.js';
3
- import { width } from './width.js';
4
- import { wrap } from './wrap.js';
5
5
  const DEFAULT_WIDTH = 80;
6
6
  const DEFAULT_PAD_X = 1;
7
7
  const DEFAULT_PAD_Y = 0;
package/dist/boxen.js CHANGED
@@ -1,6 +1,6 @@
1
+ import { width as stringWidth } from 'linegauge';
2
+ import { wrap as wrapAnsi } from 'linegauge/wrap';
1
3
  import chalk from 'roundel/chalk';
2
- import { width as stringWidth } from './width.js';
3
- import { wrap as wrapAnsi } from './wrap.js';
4
4
  const NEWLINE = '\n';
5
5
  const PAD = ' ';
6
6
  const NONE = 'none';
@@ -1,5 +1,5 @@
1
+ import { measure } from 'linegauge';
1
2
  import chalk from 'roundel/chalk';
2
- import { measure } from './width.js';
3
3
  const ESC = '\u001B';
4
4
  const SGR = /\u001B\[(?:\d*;){0,5}\d*m/g;
5
5
  const SGR_CAPTURE = /\u001B\[((?:\d*;){0,5}\d*)m/g;
package/dist/cli.js CHANGED
@@ -8,6 +8,8 @@ const MODES = ['tty', 'pipe', 'ci', 'json', 'accessible'];
8
8
  const ENV = { tty: {}, pipe: {}, ci: { CI: 'true' }, json: {}, accessible: { CLI_ACCESSIBLE: '1' } };
9
9
  const LABEL_WIDTH = 12;
10
10
  const FRAMES_SHOWN = 3;
11
+ const EXIT_OK = 0;
12
+ const EXIT_RUNTIME = 1;
11
13
  const EXIT_USAGE = 2;
12
14
  const KNOWN = ['$schema', 'name', 'contract', 'tokens', 'glyphs', 'spinners', 'borders', 'components'];
13
15
  const KINDS = ['spinners', 'borders', 'components', 'glyphs', 'tokens'];
@@ -57,7 +59,7 @@ function census(plugin) {
57
59
  }
58
60
  function refuse(code, message, fix, write) {
59
61
  write(`${code}: ${message}\n fix: ${fix}\n`);
60
- return 1;
62
+ return EXIT_RUNTIME;
61
63
  }
62
64
  function sampleFor(def) {
63
65
  const own = def.sample;
@@ -114,7 +116,7 @@ async function main(argv, write) {
114
116
  if (broke.length > 0)
115
117
  return refuse('E_COMPONENT_THREW', broke.join('; '), FIX_THREW, write);
116
118
  write(`${name}: ok${before.has(name) ? ' (replaces an earlier registration)' : ''}\n`);
117
- return 0;
119
+ return EXIT_OK;
118
120
  }
119
121
  const [, , command, ...rest] = process.argv;
120
122
  const args = command === 'check' ? rest : [command, ...rest].filter((a) => a !== undefined);
@@ -122,5 +124,5 @@ main(args, (s) => process.stdout.write(s)).then((code) => {
122
124
  process.exitCode = code;
123
125
  }, (e) => {
124
126
  process.stderr.write(`${e instanceof Error ? e.message : String(e)}\n`);
125
- process.exitCode = 1;
127
+ process.exitCode = EXIT_RUNTIME;
126
128
  });
@@ -1,6 +1,6 @@
1
1
  import process from 'node:process';
2
+ import { wrap } from 'linegauge/wrap';
2
3
  import { HIDE_CURSOR, restoreCursorOnExit, SHOW_CURSOR } from './cursor.js';
3
- import { wrap } from './wrap.js';
4
4
  const CSI = '\u001B[';
5
5
  const SYNCHRONIZED_OUTPUT_ENABLE = `${CSI}?2026h`;
6
6
  const SYNCHRONIZED_OUTPUT_DISABLE = `${CSI}?2026l`;
package/dist/ora.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import process from 'node:process';
3
+ import { lineCount } from 'linegauge';
3
4
  import chalk from 'roundel/chalk';
4
5
  import { HIDE_CURSOR, restoreCursorOnExit, SHOW_CURSOR } from './cursor.js';
5
6
  import spinnerCorpus from './spinners.json' with { type: 'json' };
6
- import { lineCount } from './width.js';
7
7
  export const spinners = spinnerCorpus;
8
8
  function isUnicodeSupported() {
9
9
  const { env } = process;
package/dist/table.js CHANGED
@@ -1,6 +1,6 @@
1
+ import { width } from 'linegauge';
2
+ import { wrap } from 'linegauge/wrap';
1
3
  import { heading, muted } from 'roundel/tokens';
2
- import { width } from './width.js';
3
- import { wrap } from './wrap.js';
4
4
  const DEFAULT_WIDTH = 80;
5
5
  const CHROME_PER_COLUMN = 3;
6
6
  const CHROME_FIXED = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flagstaff",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "The staff the flag flies from. A terminal frame loop with a static projection for agents and screen readers, and a plugin host for spinners, progress, boxes and tables. Drop-in paths for ora, log-update, boxen and cli-table3. Zero dependencies.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -89,6 +89,7 @@
89
89
  "build": "tsc -p tsconfig.build.json && node ../../scripts/strip-comments.mjs dist",
90
90
  "typecheck": "tsc -p tsconfig.json --noEmit",
91
91
  "test": "vitest run --passWithNoTests",
92
+ "coverage": "vitest run --coverage.enabled",
92
93
  "lint": "eslint src"
93
94
  },
94
95
  "repository": {
@@ -114,13 +115,12 @@
114
115
  "plugins"
115
116
  ],
116
117
  "dependencies": {
117
- "roundel": "^0.1.0"
118
+ "linegauge": "^0.1.0",
119
+ "roundel": "^0.2.0"
118
120
  },
119
121
  "devDependencies": {
120
122
  "cli-boxes": "^4.0.1",
121
123
  "cli-spinners": "^3.2.0",
122
- "string-width": "^8.1.0",
123
- "vitest": "^5.0.0",
124
- "wrap-ansi": "^10.0.1"
124
+ "vitest": "^5.0.0"
125
125
  }
126
126
  }
package/dist/width.d.ts DELETED
@@ -1,13 +0,0 @@
1
- /**
2
- * Columns a string of *plain* text occupies — no escape scan. The wrapper below has
3
- * already split its input into text runs and complete sequences, so rescanning would only
4
- * give a malformed sequence a second chance to be mistaken for one.
5
- */
6
- export declare function measure(text: string): number;
7
- /** How many terminal columns `input` occupies once its escape sequences are removed. */
8
- export declare function width(input: string): number;
9
- /**
10
- * Lines a string occupies in a terminal `columns` wide — the measurement the frame loop
11
- * actually asks for. An empty line still occupies one.
12
- */
13
- export declare function lineCount(text: string, columns: number): number;
package/dist/width.js DELETED
@@ -1,86 +0,0 @@
1
- import { stripVTControlCharacters } from 'node:util';
2
- const WIDE = [
3
- 0x1100, 0x115F, 0x231A, 0x231B, 0x2329, 0x232A, 0x23E9, 0x23EC, 0x23F0, 0x23F0,
4
- 0x23F3, 0x23F3, 0x25FD, 0x25FE, 0x2614, 0x2615, 0x2630, 0x2637, 0x2648, 0x2653,
5
- 0x267F, 0x267F, 0x268A, 0x268F, 0x2693, 0x2693, 0x26A1, 0x26A1, 0x26AA, 0x26AB,
6
- 0x26BD, 0x26BE, 0x26C4, 0x26C5, 0x26CE, 0x26CE, 0x26D4, 0x26D4, 0x26EA, 0x26EA,
7
- 0x26F2, 0x26F3, 0x26F5, 0x26F5, 0x26FA, 0x26FA, 0x26FD, 0x26FD, 0x2705, 0x2705,
8
- 0x270A, 0x270B, 0x2728, 0x2728, 0x274C, 0x274C, 0x274E, 0x274E, 0x2753, 0x2755,
9
- 0x2757, 0x2757, 0x2795, 0x2797, 0x27B0, 0x27B0, 0x27BF, 0x27BF, 0x2B1B, 0x2B1C,
10
- 0x2B50, 0x2B50, 0x2B55, 0x2B55, 0x2E80, 0x2E99, 0x2E9B, 0x2EF3, 0x2F00, 0x2FD5,
11
- 0x2FF0, 0x303E, 0x3041, 0x3096, 0x3099, 0x30FF, 0x3105, 0x312F, 0x3131, 0x318E,
12
- 0x3190, 0x31E5, 0x31EF, 0x321E, 0x3220, 0x3247, 0x3250, 0xA48C, 0xA490, 0xA4C6,
13
- 0xA960, 0xA97C, 0xAC00, 0xD7A3, 0xF900, 0xFAFF, 0xFE10, 0xFE19, 0xFE30, 0xFE52,
14
- 0xFE54, 0xFE66, 0xFE68, 0xFE6B, 0xFF01, 0xFF60, 0xFFE0, 0xFFE6, 0x16FE0, 0x16FE4,
15
- 0x16FF0, 0x16FF1, 0x17000, 0x187F7, 0x18800, 0x18CD5, 0x18CFF, 0x18D08, 0x1AFF0, 0x1AFF3,
16
- 0x1AFF5, 0x1AFFB, 0x1AFFD, 0x1AFFE, 0x1B000, 0x1B122, 0x1B132, 0x1B132, 0x1B150, 0x1B152,
17
- 0x1B155, 0x1B155, 0x1B164, 0x1B167, 0x1B170, 0x1B2FB, 0x1D300, 0x1D356, 0x1D360, 0x1D376,
18
- 0x1F004, 0x1F004, 0x1F0CF, 0x1F0CF, 0x1F18E, 0x1F18E, 0x1F191, 0x1F19A, 0x1F200, 0x1F202,
19
- 0x1F210, 0x1F23B, 0x1F240, 0x1F248, 0x1F250, 0x1F251, 0x1F260, 0x1F265, 0x1F300, 0x1F320,
20
- 0x1F32D, 0x1F335, 0x1F337, 0x1F37C, 0x1F37E, 0x1F393, 0x1F3A0, 0x1F3CA, 0x1F3CF, 0x1F3D3,
21
- 0x1F3E0, 0x1F3F0, 0x1F3F4, 0x1F3F4, 0x1F3F8, 0x1F43E, 0x1F440, 0x1F440, 0x1F442, 0x1F4FC,
22
- 0x1F4FF, 0x1F53D, 0x1F54B, 0x1F54E, 0x1F550, 0x1F567, 0x1F57A, 0x1F57A, 0x1F595, 0x1F596,
23
- 0x1F5A4, 0x1F5A4, 0x1F5FB, 0x1F64F, 0x1F680, 0x1F6C5, 0x1F6CC, 0x1F6CC, 0x1F6D0, 0x1F6D2,
24
- 0x1F6D5, 0x1F6D7, 0x1F6DC, 0x1F6DF, 0x1F6EB, 0x1F6EC, 0x1F6F4, 0x1F6FC, 0x1F7E0, 0x1F7EB,
25
- 0x1F7F0, 0x1F7F0, 0x1F90C, 0x1F93A, 0x1F93C, 0x1F945, 0x1F947, 0x1F9FF, 0x1FA70, 0x1FA7C,
26
- 0x1FA80, 0x1FA89, 0x1FA8F, 0x1FAC6, 0x1FACE, 0x1FADC, 0x1FADF, 0x1FAE9, 0x1FAF0, 0x1FAF8,
27
- 0x20000, 0x2FFFD, 0x30000, 0x3FFFD,
28
- ];
29
- const NARROW = 1;
30
- const WIDE_COLUMNS = 2;
31
- const PAIR = 2;
32
- function isWide(codePoint) {
33
- let low = 0;
34
- let high = WIDE.length / PAIR - 1;
35
- while (low <= high) {
36
- const mid = (low + high) >> 1;
37
- const start = WIDE[mid * PAIR] ?? 0;
38
- const end = WIDE[mid * PAIR + 1] ?? 0;
39
- if (codePoint < start)
40
- high = mid - 1;
41
- else if (codePoint > end)
42
- low = mid + 1;
43
- else
44
- return true;
45
- }
46
- return false;
47
- }
48
- const ZERO_WIDTH_CLUSTER = /^(?:\p{Default_Ignorable_Code_Point}|\p{Control}|\p{Mark}|\p{Surrogate})+$/v;
49
- const LEADING_NON_PRINTING = /^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Mark}\p{Surrogate}]+/v;
50
- const RGI_EMOJI = /^\p{RGI_Emoji}$/v;
51
- const FORMS_FIRST = 0xff00;
52
- const FORMS_LAST = 0xffef;
53
- const segmenter = new Intl.Segmenter();
54
- function trailingForms(cluster) {
55
- let extra = 0;
56
- for (const character of [...cluster].slice(1)) {
57
- const codePoint = character.codePointAt(0) ?? 0;
58
- if (codePoint >= FORMS_FIRST && codePoint <= FORMS_LAST)
59
- extra += isWide(codePoint) ? WIDE_COLUMNS : NARROW;
60
- }
61
- return extra;
62
- }
63
- export function measure(text) {
64
- let columns = 0;
65
- for (const { segment } of segmenter.segment(text)) {
66
- if (ZERO_WIDTH_CLUSTER.test(segment))
67
- continue;
68
- if (RGI_EMOJI.test(segment)) {
69
- columns += WIDE_COLUMNS;
70
- continue;
71
- }
72
- const codePoint = segment.replace(LEADING_NON_PRINTING, '').codePointAt(0) ?? 0;
73
- columns += isWide(codePoint) ? WIDE_COLUMNS : NARROW;
74
- columns += trailingForms(segment);
75
- }
76
- return columns;
77
- }
78
- export function width(input) {
79
- return input === '' ? 0 : measure(stripVTControlCharacters(input));
80
- }
81
- export function lineCount(text, columns) {
82
- let count = 0;
83
- for (const line of stripVTControlCharacters(text).split('\n'))
84
- count += Math.max(1, Math.ceil(width(line) / columns));
85
- return count;
86
- }
package/dist/wrap.d.ts DELETED
@@ -1,10 +0,0 @@
1
- export interface WrapOptions {
2
- /** Trim leading and trailing whitespace from each row. Default true. */
3
- trim?: boolean;
4
- /** Break a word longer than `columns` rather than let it overflow. Default false. */
5
- hard?: boolean;
6
- /** Break on any character rather than at word boundaries. Default true. */
7
- wordWrap?: boolean;
8
- }
9
- /** Wrap `string` to `columns`, keeping its ANSI intact and each row self-contained. */
10
- export declare function wrap(string: string, columns: number, options?: WrapOptions): string;
package/dist/wrap.js DELETED
@@ -1,447 +0,0 @@
1
- import { measure } from './width.js';
2
- const ESC = '\u001B';
3
- const BELL = '\u0007';
4
- const C1_CSI = '\u009B';
5
- const CSI = '[';
6
- const OSC = ']';
7
- const SGR_TERMINATOR = 'm';
8
- const SGR_RESET = 0;
9
- const SGR_RESET_FOREGROUND = 39;
10
- const SGR_RESET_BACKGROUND = 49;
11
- const SGR_RESET_UNDERLINE_COLOR = 59;
12
- const SGR_FOREGROUND_EXTENDED = 38;
13
- const SGR_BACKGROUND_EXTENDED = 48;
14
- const SGR_UNDERLINE_COLOR_EXTENDED = 58;
15
- const SGR_COLOR_MODE_RGB = 2;
16
- const SGR_COLOR_MODE_256 = 5;
17
- const FOREGROUND_FIRST = 30;
18
- const FOREGROUND_LAST = 37;
19
- const FOREGROUND_BRIGHT_FIRST = 90;
20
- const FOREGROUND_BRIGHT_LAST = 97;
21
- const BACKGROUND_FIRST = 40;
22
- const BACKGROUND_LAST = 47;
23
- const BACKGROUND_BRIGHT_FIRST = 100;
24
- const BACKGROUND_BRIGHT_LAST = 107;
25
- const TAB_SIZE = 8;
26
- const COLOR_256_PARTS = 3;
27
- const COLOR_RGB_PARTS = 3;
28
- const COLON_RGB_WITH_SPACE = 6;
29
- const ESCAPES = new Set([ESC, C1_CSI]);
30
- const ESCAPE_CHARACTERS = [...ESCAPES].join('');
31
- const CSI_INTRODUCER = `(?:${ESC}\\${CSI}|${C1_CSI})`;
32
- const CSI_PARAMETERS = '[0-?]*[ -/]*[@-~]';
33
- const SGR_PARAMETERS = `(?<sgr>[0-9;:]*)${SGR_TERMINATOR}`;
34
- const OSC_TERMINATOR = `(?:${BELL}|${ESC}\\\\)`;
35
- const OSC_PAYLOAD = String.raw `[^\u0000-\u001F\u007F-\u009F]*`;
36
- const LINK_PARAMETERS = String.raw `8;(?<parameters>[^;\u0000-\u001F\u007F-\u009F]*);(?<uri>${OSC_PAYLOAD})${OSC_TERMINATOR}`;
37
- const ANSI_ESCAPE = new RegExp(`${CSI_INTRODUCER}(?:${SGR_PARAMETERS}|${CSI_PARAMETERS})|${ESC}\\${OSC}(?:${LINK_PARAMETERS}|${OSC_PAYLOAD}${OSC_TERMINATOR})`, 'y');
38
- const ESCAPE_INTRODUCER = new RegExp(`[${ESCAPE_CHARACTERS}]`, 'g');
39
- const ROW_BOUNDARY = new RegExp(`[\\n${ESCAPE_CHARACTERS}]`, 'g');
40
- const ASCII_PRINTABLE = /^[ -~]*$/;
41
- const MODIFIER_CLOSE = new Map([
42
- [1, 22],
43
- [2, 22],
44
- [3, 23],
45
- [4, 24],
46
- [7, 27],
47
- [8, 28],
48
- [9, 29],
49
- [53, 55],
50
- ]);
51
- const MODIFIER_CLOSE_CODES = new Set(MODIFIER_CLOSE.values());
52
- const segmenter = new Intl.Segmenter();
53
- const sgr = (code) => `${ESC}${CSI}${code}${SGR_TERMINATOR}`;
54
- const hyperlink = (url, parameters = '') => `${ESC}${OSC}8;${parameters};${url}${BELL}`;
55
- function matchEscape(string, index) {
56
- if (!ESCAPES.has(string[index] ?? ''))
57
- return undefined;
58
- ANSI_ESCAPE.lastIndex = index;
59
- return ANSI_ESCAPE.exec(string) ?? undefined;
60
- }
61
- function forEachSegment(string, onPlainText, onEscape = () => undefined) {
62
- let plainStart = 0;
63
- let index = 0;
64
- while (index < string.length) {
65
- ESCAPE_INTRODUCER.lastIndex = index;
66
- const introducer = ESCAPE_INTRODUCER.exec(string);
67
- if (introducer === null)
68
- break;
69
- const escape = matchEscape(string, introducer.index);
70
- if (escape === undefined) {
71
- index = introducer.index + 1;
72
- continue;
73
- }
74
- if (introducer.index > plainStart)
75
- onPlainText(string.slice(plainStart, introducer.index));
76
- onEscape(escape[0]);
77
- index = introducer.index + escape[0].length;
78
- plainStart = index;
79
- }
80
- if (plainStart < string.length)
81
- onPlainText(string.slice(plainStart));
82
- }
83
- function visibleWidth(string) {
84
- let plainText = '';
85
- forEachSegment(string, (part) => {
86
- plainText += part;
87
- });
88
- return measure(plainText);
89
- }
90
- function tokenize(string) {
91
- const tokens = [];
92
- forEachSegment(string, (plainText) => {
93
- if (ASCII_PRINTABLE.test(plainText)) {
94
- for (const character of plainText)
95
- tokens.push({ value: character, width: 1 });
96
- return;
97
- }
98
- for (const { segment } of segmenter.segment(plainText))
99
- tokens.push({ value: segment, width: measure(segment) });
100
- }, (escape) => tokens.push({ value: escape, width: 0 }));
101
- return tokens;
102
- }
103
- function splitWords(string) {
104
- let current = { value: '', plainText: '', width: 0 };
105
- const words = [current];
106
- forEachSegment(string, (plainText) => {
107
- const parts = plainText.split(' ');
108
- current.value += parts[0] ?? '';
109
- current.plainText += parts[0] ?? '';
110
- for (let index = 1; index < parts.length; index += 1) {
111
- const part = parts[index] ?? '';
112
- current = { value: part, plainText: part, width: 0 };
113
- words.push(current);
114
- }
115
- }, (escape) => {
116
- current.value += escape;
117
- });
118
- for (const word of words)
119
- word.width = measure(word.plainText);
120
- return words;
121
- }
122
- const isDigits = (value) => /^\d+$/.test(value);
123
- function colonColorToken(parameter) {
124
- const parts = parameter.split(':');
125
- const code = Number.parseInt(parts[0] ?? '', 10);
126
- const mode = Number.parseInt(parts[1] ?? '', 10);
127
- if (![SGR_FOREGROUND_EXTENDED, SGR_BACKGROUND_EXTENDED, SGR_UNDERLINE_COLOR_EXTENDED].includes(code))
128
- return undefined;
129
- if (mode === SGR_COLOR_MODE_256 && parts.length === COLOR_256_PARTS && isDigits(parts[2] ?? '')) {
130
- return { code, open: parameter, hasArguments: true };
131
- }
132
- if (mode !== SGR_COLOR_MODE_RGB)
133
- return undefined;
134
- const withSpace = parts.length === COLON_RGB_WITH_SPACE;
135
- const components = withSpace ? parts.slice(3) : parts.slice(2);
136
- const colorSpace = withSpace ? parts[2] : undefined;
137
- if (components.length === COLOR_RGB_PARTS && components.every(isDigits) && (colorSpace === undefined || /^\d*$/.test(colorSpace))) {
138
- return { code, open: parameter, hasArguments: true };
139
- }
140
- return undefined;
141
- }
142
- function extendedColorToken(code, parameters, index) {
143
- const mode = Number.parseInt(parameters[index + 1] ?? '', 10);
144
- const first = Number.parseInt(parameters[index + 2] ?? '', 10);
145
- if (mode === SGR_COLOR_MODE_256 && Number.isFinite(first)) {
146
- return { token: { code, open: [code, mode, first].join(';'), hasArguments: true }, consumed: 2 };
147
- }
148
- const green = Number.parseInt(parameters[index + 3] ?? '', 10);
149
- const blue = Number.parseInt(parameters[index + 4] ?? '', 10);
150
- if (mode === SGR_COLOR_MODE_RGB && Number.isFinite(first) && Number.isFinite(green) && Number.isFinite(blue)) {
151
- return { token: { code, open: [code, mode, first, green, blue].join(';'), hasArguments: true }, consumed: 4 };
152
- }
153
- return undefined;
154
- }
155
- const isExtendedColor = (code) => code === SGR_FOREGROUND_EXTENDED || code === SGR_BACKGROUND_EXTENDED || code === SGR_UNDERLINE_COLOR_EXTENDED;
156
- function sgrTokens(parameters) {
157
- const parts = parameters.split(';');
158
- const tokens = [];
159
- for (let index = 0; index < parts.length; index += 1) {
160
- const parameter = parts[index] ?? '';
161
- if (parameter.includes(':')) {
162
- const token = colonColorToken(parameter);
163
- if (token !== undefined)
164
- tokens.push(token);
165
- continue;
166
- }
167
- const code = parameter === '' ? SGR_RESET : Number.parseInt(parameter, 10);
168
- if (!Number.isFinite(code))
169
- continue;
170
- if (isExtendedColor(code)) {
171
- if (index + 1 >= parts.length)
172
- break;
173
- const extended = extendedColorToken(code, parts, index);
174
- if (extended === undefined)
175
- break;
176
- tokens.push(extended.token);
177
- index += extended.consumed;
178
- continue;
179
- }
180
- tokens.push({ code, open: String(code), hasArguments: false });
181
- }
182
- return tokens;
183
- }
184
- function removeFamily(active, family) {
185
- const at = active.findIndex((style) => style.family === family);
186
- if (at !== -1)
187
- active.splice(at, 1);
188
- }
189
- function colorStyle(token) {
190
- const { code, open, hasArguments } = token;
191
- if ((code >= FOREGROUND_FIRST && code <= FOREGROUND_LAST) || (code >= FOREGROUND_BRIGHT_FIRST && code <= FOREGROUND_BRIGHT_LAST) || (code === SGR_FOREGROUND_EXTENDED && hasArguments)) {
192
- return { family: 'foreground', open, close: SGR_RESET_FOREGROUND };
193
- }
194
- if ((code >= BACKGROUND_FIRST && code <= BACKGROUND_LAST) || (code >= BACKGROUND_BRIGHT_FIRST && code <= BACKGROUND_BRIGHT_LAST) || (code === SGR_BACKGROUND_EXTENDED && hasArguments)) {
195
- return { family: 'background', open, close: SGR_RESET_BACKGROUND };
196
- }
197
- if (code === SGR_UNDERLINE_COLOR_EXTENDED && hasArguments) {
198
- return { family: 'underlineColor', open, close: SGR_RESET_UNDERLINE_COLOR };
199
- }
200
- return undefined;
201
- }
202
- function applyResetCode(code, active) {
203
- if (code === SGR_RESET) {
204
- active.length = 0;
205
- return true;
206
- }
207
- if (code === SGR_RESET_FOREGROUND) {
208
- removeFamily(active, 'foreground');
209
- return true;
210
- }
211
- if (code === SGR_RESET_BACKGROUND) {
212
- removeFamily(active, 'background');
213
- return true;
214
- }
215
- if (code === SGR_RESET_UNDERLINE_COLOR) {
216
- removeFamily(active, 'underlineColor');
217
- return true;
218
- }
219
- if (MODIFIER_CLOSE_CODES.has(code)) {
220
- for (let index = active.length - 1; index >= 0; index -= 1) {
221
- const style = active[index];
222
- if (style !== undefined && style.family.startsWith('modifier-') && style.close === code)
223
- active.splice(index, 1);
224
- }
225
- return true;
226
- }
227
- return false;
228
- }
229
- function applyToken(token, active) {
230
- if (applyResetCode(token.code, active))
231
- return;
232
- const color = colorStyle(token);
233
- if (color !== undefined) {
234
- removeFamily(active, color.family);
235
- active.push(color);
236
- return;
237
- }
238
- const close = MODIFIER_CLOSE.get(token.code);
239
- if (close !== undefined && close !== SGR_RESET) {
240
- const family = `modifier-${token.code}`;
241
- removeFamily(active, family);
242
- active.push({ family, open: token.open, close });
243
- }
244
- }
245
- const applyParameters = (parameters, active) => {
246
- for (const token of sgrTokens(parameters))
247
- applyToken(token, active);
248
- };
249
- const applyResets = (parameters, active) => {
250
- for (const { code } of sgrTokens(parameters))
251
- applyResetCode(code, active);
252
- };
253
- function applyLeadingResets(string, startIndex, active) {
254
- let index = startIndex;
255
- while (index < string.length) {
256
- const match = matchEscape(string, index);
257
- if (match === undefined)
258
- break;
259
- if (match.groups?.['sgr'] !== undefined)
260
- applyResets(match.groups['sgr'], active);
261
- index += match[0].length;
262
- }
263
- }
264
- const closingSequence = (active) => [...active].reverse().map((style) => sgr(style.close)).join('');
265
- const openingSequence = (active) => active.map((style) => sgr(style.open)).join('');
266
- function wrapWord(rows, word, columns, rowWidth) {
267
- const tokens = tokenize(word);
268
- let visible = rowWidth;
269
- for (const [index, token] of tokens.entries()) {
270
- if (token.width > 0 && visible > 0 && visible + token.width > columns) {
271
- rows.push('');
272
- visible = 0;
273
- }
274
- rows[rows.length - 1] += token.value;
275
- visible += token.width;
276
- if (visible === columns && index < tokens.length - 1) {
277
- rows.push('');
278
- visible = 0;
279
- }
280
- }
281
- const last = rows.at(-1) ?? '';
282
- if (!visible && last.length > 0 && rows.length > 1)
283
- rows[rows.length - 2] += rows.pop() ?? '';
284
- return visibleWidth(rows.at(-1) ?? '');
285
- }
286
- function trimVisibleEnd(string) {
287
- if (!string.includes(' '))
288
- return string;
289
- const segments = [];
290
- forEachSegment(string, (plainText) => segments.push({ value: plainText, isEscape: false }), (escape) => segments.push({ value: escape, isEscape: true }));
291
- for (let index = segments.length - 1; index >= 0; index -= 1) {
292
- const segment = segments[index];
293
- if (segment === undefined || segment.isEscape)
294
- continue;
295
- let end = segment.value.length;
296
- while (end > 0 && segment.value[end - 1] === ' ')
297
- end -= 1;
298
- segment.value = segment.value.slice(0, end);
299
- if (measure(segment.value) > 0)
300
- break;
301
- }
302
- return segments.map((segment) => segment.value).join('');
303
- }
304
- function expandTabs(line) {
305
- if (!line.includes('\t'))
306
- return line;
307
- let visible = 0;
308
- let expanded = '';
309
- let sinceTab = '';
310
- forEachSegment(line, (plainText) => {
311
- const parts = plainText.split('\t');
312
- for (const [index, part] of parts.entries()) {
313
- expanded += part;
314
- sinceTab += part;
315
- if (index < parts.length - 1) {
316
- visible += measure(sinceTab);
317
- sinceTab = '';
318
- const spaces = TAB_SIZE - (visible % TAB_SIZE);
319
- expanded += ' '.repeat(spaces);
320
- visible += spaces;
321
- }
322
- }
323
- }, (escape) => {
324
- expanded += escape;
325
- });
326
- return expanded;
327
- }
328
- function restoreStylesAcrossRows(preString) {
329
- let out = '';
330
- let activeHyperlink;
331
- const active = [];
332
- let index = 0;
333
- let copied = 0;
334
- while (index < preString.length) {
335
- ROW_BOUNDARY.lastIndex = index;
336
- const boundary = ROW_BOUNDARY.exec(preString);
337
- if (boundary === null)
338
- break;
339
- index = boundary.index;
340
- if (boundary[0] !== '\n') {
341
- const escape = matchEscape(preString, index);
342
- if (escape === undefined) {
343
- index += 1;
344
- continue;
345
- }
346
- const groups = escape.groups ?? {};
347
- if (groups['sgr'] !== undefined)
348
- applyParameters(groups['sgr'], active);
349
- else if (groups['uri'] !== undefined)
350
- activeHyperlink = groups['uri'].length === 0 ? undefined : { parameters: groups['parameters'] ?? '', uri: groups['uri'] };
351
- index += escape[0].length;
352
- continue;
353
- }
354
- out += preString.slice(copied, index);
355
- if (index > copied) {
356
- if (activeHyperlink !== undefined)
357
- out += hyperlink('');
358
- out += closingSequence(active);
359
- }
360
- out += '\n';
361
- index += 1;
362
- copied = index;
363
- if (index < preString.length && preString[index] !== '\n') {
364
- const opening = [...active];
365
- applyLeadingResets(preString, index, opening);
366
- out += openingSequence(opening);
367
- if (activeHyperlink !== undefined)
368
- out += hyperlink(activeHyperlink.uri, activeHyperlink.parameters);
369
- }
370
- }
371
- return out + preString.slice(copied);
372
- }
373
- function shouldStartLongWordOnNextRow(wordWidth, columns, rowLength) {
374
- const remaining = columns - rowLength;
375
- const breaksStartingThisRow = 1 + Math.floor((wordWidth - remaining - 1) / columns);
376
- const breaksStartingNextRow = Math.floor((wordWidth - 1) / columns);
377
- return breaksStartingNextRow < breaksStartingThisRow;
378
- }
379
- function wrapLine(string, columns, options) {
380
- const trim = options.trim !== false;
381
- if (trim && string.trim() === '')
382
- return '';
383
- const words = splitWords(string);
384
- let rows = [''];
385
- let rowLength = 0;
386
- let trimmedRowIndex = -1;
387
- let isFirstWord = true;
388
- for (const word of words) {
389
- const rowIndex = rows.length - 1;
390
- if (trim && trimmedRowIndex !== rowIndex) {
391
- const row = rows[rowIndex] ?? '';
392
- const trimmedRow = row.trimStart();
393
- if (trimmedRow.length !== row.length) {
394
- rows[rowIndex] = trimmedRow;
395
- rowLength = visibleWidth(trimmedRow);
396
- }
397
- if (trimmedRow.length > 0)
398
- trimmedRowIndex = rowIndex;
399
- }
400
- if (isFirstWord) {
401
- isFirstWord = false;
402
- }
403
- else {
404
- if (rowLength >= columns && (options.wordWrap === false || !trim)) {
405
- rows.push('');
406
- rowLength = 0;
407
- }
408
- if (rowLength > 0 || !trim) {
409
- rows[rows.length - 1] += ' ';
410
- rowLength += 1;
411
- }
412
- }
413
- if (options.hard === true && options.wordWrap !== false && word.width > columns) {
414
- if (shouldStartLongWordOnNextRow(word.width, columns, rowLength)) {
415
- rows.push('');
416
- rowLength = 0;
417
- }
418
- rowLength = wrapWord(rows, word.value, columns, rowLength);
419
- continue;
420
- }
421
- if (rowLength + word.width > columns && rowLength > 0 && word.width > 0) {
422
- if (options.wordWrap === false && rowLength < columns) {
423
- rowLength = wrapWord(rows, word.value, columns, rowLength);
424
- continue;
425
- }
426
- rows.push('');
427
- rowLength = 0;
428
- }
429
- if (rowLength + word.width > columns && options.wordWrap === false) {
430
- rowLength = wrapWord(rows, word.value, columns, rowLength);
431
- continue;
432
- }
433
- rows[rows.length - 1] += word.value;
434
- rowLength += word.width;
435
- }
436
- if (trim)
437
- rows = rows.map((row) => trimVisibleEnd(row));
438
- return restoreStylesAcrossRows(rows.join('\n'));
439
- }
440
- export function wrap(string, columns, options = {}) {
441
- return String(string)
442
- .normalize()
443
- .replaceAll('\r\n', '\n')
444
- .split('\n')
445
- .map((line) => wrapLine(expandTabs(line), columns, options))
446
- .join('\n');
447
- }