flagstaff 0.0.1 → 0.1.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/README.md +333 -20
- package/THIRD-PARTY.md +44 -0
- package/dist/box.d.ts +22 -0
- package/dist/box.js +59 -0
- package/dist/boxen.d.ts +56 -0
- package/dist/boxen.js +251 -0
- package/dist/builtins.d.ts +13 -0
- package/dist/builtins.js +19 -0
- package/dist/cli-table3.d.ts +138 -0
- package/dist/cli-table3.js +785 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +126 -0
- package/dist/cursor.d.ts +13 -0
- package/dist/cursor.js +49 -0
- package/dist/import.d.ts +50 -0
- package/dist/import.js +20 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.js +8 -7
- package/dist/log-update.d.ts +29 -0
- package/dist/log-update.js +173 -0
- package/dist/loop.d.ts +30 -0
- package/dist/loop.js +67 -0
- package/dist/ora.d.ts +82 -0
- package/dist/ora.js +515 -0
- package/dist/plugin.d.ts +107 -0
- package/dist/plugin.js +163 -0
- package/dist/progress.d.ts +19 -0
- package/dist/progress.js +25 -0
- package/dist/projection.d.ts +46 -0
- package/dist/projection.js +83 -0
- package/dist/schema.json +1 -0
- package/dist/spinner.d.ts +9 -0
- package/dist/spinner.js +24 -0
- package/dist/spinners.json +1 -0
- package/dist/table.d.ts +18 -0
- package/dist/table.js +69 -0
- package/dist/tasks.d.ts +18 -0
- package/dist/tasks.js +31 -0
- package/dist/width.d.ts +13 -0
- package/dist/width.js +86 -0
- package/dist/wrap.d.ts +10 -0
- package/dist/wrap.js +447 -0
- package/package.json +77 -4
package/dist/table.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { heading, muted } from 'roundel/tokens';
|
|
2
|
+
import { width } from './width.js';
|
|
3
|
+
import { wrap } from './wrap.js';
|
|
4
|
+
const DEFAULT_WIDTH = 80;
|
|
5
|
+
const CHROME_PER_COLUMN = 3;
|
|
6
|
+
const CHROME_FIXED = 1;
|
|
7
|
+
const MIN_COLUMN = 3;
|
|
8
|
+
const V = '│';
|
|
9
|
+
const H = '─';
|
|
10
|
+
const CORNERS = { topLeft: '┌', topRight: '┐', bottomLeft: '└', bottomRight: '┘', top: '┬', bottom: '┴', left: '├', right: '┤', cross: '┼' };
|
|
11
|
+
const padTo = (line, cells, align) => {
|
|
12
|
+
const gap = ' '.repeat(Math.max(0, cells - width(line)));
|
|
13
|
+
return align === 'right' ? gap + line : line + gap;
|
|
14
|
+
};
|
|
15
|
+
function naturalWidths(rows, columns) {
|
|
16
|
+
const widths = Array.from({ length: columns }, () => 0);
|
|
17
|
+
for (const row of rows) {
|
|
18
|
+
for (let index = 0; index < columns; index += 1)
|
|
19
|
+
widths[index] = Math.max(widths[index] ?? 0, width(row[index] ?? ''));
|
|
20
|
+
}
|
|
21
|
+
return widths;
|
|
22
|
+
}
|
|
23
|
+
function fitWidths(natural, available) {
|
|
24
|
+
const widths = [...natural];
|
|
25
|
+
let total = widths.reduce((sum, w) => sum + w, 0);
|
|
26
|
+
while (total > available) {
|
|
27
|
+
let widest = 0;
|
|
28
|
+
for (let index = 1; index < widths.length; index += 1)
|
|
29
|
+
if ((widths[index] ?? 0) > (widths[widest] ?? 0))
|
|
30
|
+
widest = index;
|
|
31
|
+
if ((widths[widest] ?? 0) <= MIN_COLUMN)
|
|
32
|
+
break;
|
|
33
|
+
widths[widest] = (widths[widest] ?? 0) - 1;
|
|
34
|
+
total -= 1;
|
|
35
|
+
}
|
|
36
|
+
return widths;
|
|
37
|
+
}
|
|
38
|
+
function layoutRow(row, widths, align) {
|
|
39
|
+
const cells = widths.map((w, index) => wrap(row[index] ?? '', w, { hard: true, trim: false }).split('\n'));
|
|
40
|
+
const height = Math.max(1, ...cells.map((lines) => lines.length));
|
|
41
|
+
return Array.from({ length: height }, (_, line) => `${V} ${widths.map((w, index) => padTo(cells[index]?.[line] ?? '', w, align[index] ?? 'left')).join(` ${V} `)} ${V}`);
|
|
42
|
+
}
|
|
43
|
+
const rule = (widths, left, mid, right) => left + widths.map((w) => H.repeat(w + 2)).join(mid) + right;
|
|
44
|
+
export function table(rows, options = {}) {
|
|
45
|
+
const columns = Math.max(options.head?.length ?? 0, ...rows.map((row) => row.length), 1);
|
|
46
|
+
const total = options.width ?? DEFAULT_WIDTH;
|
|
47
|
+
const align = options.align ?? [];
|
|
48
|
+
const available = Math.max(columns * MIN_COLUMN, total - columns * CHROME_PER_COLUMN - CHROME_FIXED);
|
|
49
|
+
const all = options.head === undefined ? rows : [options.head, ...rows];
|
|
50
|
+
const widths = fitWidths(naturalWidths(all, columns), available);
|
|
51
|
+
const out = [rule(widths, CORNERS.topLeft, CORNERS.top, CORNERS.topRight)];
|
|
52
|
+
if (options.head !== undefined) {
|
|
53
|
+
out.push(...layoutRow(options.head.map((cell) => heading(cell)), widths, align), rule(widths, CORNERS.left, CORNERS.cross, CORNERS.right));
|
|
54
|
+
}
|
|
55
|
+
for (const row of rows)
|
|
56
|
+
out.push(...layoutRow(row, widths, align));
|
|
57
|
+
out.push(rule(widths, CORNERS.bottomLeft, CORNERS.bottom, CORNERS.bottomRight));
|
|
58
|
+
return out.join('\n');
|
|
59
|
+
}
|
|
60
|
+
export function tableComponent(options = {}) {
|
|
61
|
+
return {
|
|
62
|
+
name: 'table',
|
|
63
|
+
static: (state) => {
|
|
64
|
+
const head = state.head ?? options.head;
|
|
65
|
+
return state.rows.map((row) => (head === undefined ? row.join('\t') : row.map((cell, index) => `${head[index] ?? index}: ${cell}`).join(', '))).join('\n');
|
|
66
|
+
},
|
|
67
|
+
frame: (_t, state) => muted(table(state.rows, state.head === undefined ? options : { ...options, head: state.head })),
|
|
68
|
+
};
|
|
69
|
+
}
|
package/dist/tasks.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Component } from './plugin.js';
|
|
2
|
+
export type TaskStatus = 'pending' | 'running' | 'ok' | 'fail' | 'warn' | 'info';
|
|
3
|
+
export interface Task {
|
|
4
|
+
title: string;
|
|
5
|
+
/** `pending` when omitted. */
|
|
6
|
+
status?: TaskStatus;
|
|
7
|
+
/** A line under the title — the step's current detail, shown only on a terminal. */
|
|
8
|
+
detail?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface TasksState {
|
|
11
|
+
tasks: Task[];
|
|
12
|
+
}
|
|
13
|
+
export interface TasksOptions {
|
|
14
|
+
/** The spinner style the running task is drawn with. Default `dots`. */
|
|
15
|
+
spinner?: string;
|
|
16
|
+
}
|
|
17
|
+
/** A task list: settled tasks one line each off a terminal, the whole list drawn on one. */
|
|
18
|
+
export declare function tasks({ spinner }?: TasksOptions): Component<TasksState>;
|
package/dist/tasks.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { error, hint, muted, ok, warn } from 'roundel/tokens';
|
|
2
|
+
import { glyph, lookupSpinner } from './plugin.js';
|
|
3
|
+
const PAINT = { ok, fail: error, warn, info: hint };
|
|
4
|
+
const PENDING = ' ';
|
|
5
|
+
const settled = (status) => status !== 'pending' && status !== 'running';
|
|
6
|
+
export function tasks({ spinner = 'dots' } = {}) {
|
|
7
|
+
const style = lookupSpinner(spinner);
|
|
8
|
+
const symbol = (status, t) => {
|
|
9
|
+
if (settled(status))
|
|
10
|
+
return PAINT[status](glyph(status));
|
|
11
|
+
if (status === 'pending')
|
|
12
|
+
return muted(PENDING);
|
|
13
|
+
const at = Math.floor(t / style.interval) % style.frames.length;
|
|
14
|
+
return style.frames[at] ?? '';
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
name: 'tasks',
|
|
18
|
+
interval: style.interval,
|
|
19
|
+
static: (state) => state.tasks
|
|
20
|
+
.filter((task) => settled(task.status ?? 'pending'))
|
|
21
|
+
.map((task) => `${PAINT[task.status](glyph(task.status))} ${task.title}`)
|
|
22
|
+
.join('\n'),
|
|
23
|
+
frame: (t, state) => state.tasks
|
|
24
|
+
.flatMap((task) => {
|
|
25
|
+
const status = task.status ?? 'pending';
|
|
26
|
+
const title = `${symbol(status, t)} ${status === 'pending' ? muted(task.title) : task.title}`;
|
|
27
|
+
return task.detail === undefined || status !== 'running' ? [title] : [title, ` ${muted(task.detail)}`];
|
|
28
|
+
})
|
|
29
|
+
.join('\n'),
|
|
30
|
+
};
|
|
31
|
+
}
|
package/dist/width.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
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
|
+
}
|