@ix-xs/node-comfort 1.2.0 → 2.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/CHANGELOG.md +340 -0
- package/README.md +137 -548
- package/index.js +80 -35
- package/package.json +161 -33
- package/src/Arr.js +1050 -418
- package/src/Async.js +817 -0
- package/src/Cache.js +433 -0
- package/src/Checker.js +1202 -539
- package/src/Cli.js +1293 -0
- package/src/Color.js +556 -0
- package/src/Crypto.js +1082 -0
- package/src/Emitter.js +356 -0
- package/src/Env.js +689 -0
- package/src/FS.js +1626 -894
- package/src/Func.js +823 -356
- package/src/Http.js +700 -0
- package/src/Id.js +568 -200
- package/src/Logger.js +963 -378
- package/src/Num.js +965 -360
- package/src/Obj.js +1077 -379
- package/src/SQLite.js +1629 -1005
- package/src/Schema.js +2101 -0
- package/src/Str.js +1209 -432
- package/src/Sys.js +654 -0
- package/src/Time.js +1533 -440
- package/src/Utils.js +342 -325
- package/src/errors.js +335 -0
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +735 -526
- package/types/src/Arr.d.ts +533 -145
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +372 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +221 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +651 -232
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +664 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +300 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +620 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +189 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +412 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +717 -238
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +508 -135
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +444 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +199 -66
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +414 -46
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +514 -137
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +453 -104
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +754 -308
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +1189 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +617 -166
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +314 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +759 -204
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +158 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +316 -0
- package/types/src/errors.d.ts.map +1 -0
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
- package/types/index.d.ts.map +0 -1
package/src/Color.js
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Terminal colors you can chain, like `chalk`. Truecolor is supported and
|
|
5
|
+
* downgraded on older terminals. Colors switch off by themselves when the
|
|
6
|
+
* output isn't a terminal or `NO_COLOR` is set; `FORCE_COLOR=1|2|3` turns
|
|
7
|
+
* them back on.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* console.log(color.green.bold("✔ Done"), color.gray("in 1.2s"));
|
|
11
|
+
* console.log(color.bgHex("#1e1e2e").hex("#cba6f7")(" nc "));
|
|
12
|
+
* console.log(color.gradient("node-comfort", ["#ff5f6d", "#ffc371"]));
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { segmenter, graphemes } = require("./internal/shared.js");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Every named style.
|
|
19
|
+
* @typedef {"reset" | "bold" | "dim" | "italic" | "underline" | "overline" | "inverse" | "hidden" | "strikethrough"
|
|
20
|
+
* | "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray" | "grey"
|
|
21
|
+
* | "blackBright" | "redBright" | "greenBright" | "yellowBright" | "blueBright" | "magentaBright" | "cyanBright" | "whiteBright"
|
|
22
|
+
* | "bgBlack" | "bgRed" | "bgGreen" | "bgYellow" | "bgBlue" | "bgMagenta" | "bgCyan" | "bgWhite" | "bgGray" | "bgGrey"
|
|
23
|
+
* | "bgBlackBright" | "bgRedBright" | "bgGreenBright" | "bgYellowBright" | "bgBlueBright" | "bgMagentaBright" | "bgCyanBright" | "bgWhiteBright"} StyleName
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How many colors the terminal can show: `0` none, `1` 16, `2` 256, `3` millions.
|
|
28
|
+
* @typedef {0 | 1 | 2 | 3} ColorLevel
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The named styles. Each one can be called on text or chained with others:
|
|
33
|
+
* `color.red("x")`, `color.red.bold.underline("x")`.
|
|
34
|
+
* @typedef {object} ColorStyleProps
|
|
35
|
+
* @property {ColorChain} reset Removes all styles.
|
|
36
|
+
* @property {ColorChain} bold Bold text.
|
|
37
|
+
* @property {ColorChain} dim Faint text.
|
|
38
|
+
* @property {ColorChain} italic Italic text, where the terminal supports it.
|
|
39
|
+
* @property {ColorChain} underline Underlined text.
|
|
40
|
+
* @property {ColorChain} overline Line above the text, where supported.
|
|
41
|
+
* @property {ColorChain} inverse Swaps text and background colors.
|
|
42
|
+
* @property {ColorChain} hidden Invisible text, still copyable.
|
|
43
|
+
* @property {ColorChain} strikethrough Crossed-out text.
|
|
44
|
+
* @property {ColorChain} black Black text.
|
|
45
|
+
* @property {ColorChain} red Red text.
|
|
46
|
+
* @property {ColorChain} green Green text.
|
|
47
|
+
* @property {ColorChain} yellow Yellow text.
|
|
48
|
+
* @property {ColorChain} blue Blue text.
|
|
49
|
+
* @property {ColorChain} magenta Magenta text.
|
|
50
|
+
* @property {ColorChain} cyan Cyan text.
|
|
51
|
+
* @property {ColorChain} white White text.
|
|
52
|
+
* @property {ColorChain} gray Gray text.
|
|
53
|
+
* @property {ColorChain} grey Same as `gray`.
|
|
54
|
+
* @property {ColorChain} blackBright Bright black text.
|
|
55
|
+
* @property {ColorChain} redBright Bright red text.
|
|
56
|
+
* @property {ColorChain} greenBright Bright green text.
|
|
57
|
+
* @property {ColorChain} yellowBright Bright yellow text.
|
|
58
|
+
* @property {ColorChain} blueBright Bright blue text.
|
|
59
|
+
* @property {ColorChain} magentaBright Bright magenta text.
|
|
60
|
+
* @property {ColorChain} cyanBright Bright cyan text.
|
|
61
|
+
* @property {ColorChain} whiteBright Bright white text.
|
|
62
|
+
* @property {ColorChain} bgBlack Black background.
|
|
63
|
+
* @property {ColorChain} bgRed Red background.
|
|
64
|
+
* @property {ColorChain} bgGreen Green background.
|
|
65
|
+
* @property {ColorChain} bgYellow Yellow background.
|
|
66
|
+
* @property {ColorChain} bgBlue Blue background.
|
|
67
|
+
* @property {ColorChain} bgMagenta Magenta background.
|
|
68
|
+
* @property {ColorChain} bgCyan Cyan background.
|
|
69
|
+
* @property {ColorChain} bgWhite White background.
|
|
70
|
+
* @property {ColorChain} bgGray Gray background.
|
|
71
|
+
* @property {ColorChain} bgGrey Same as `bgGray`.
|
|
72
|
+
* @property {ColorChain} bgBlackBright Bright black background.
|
|
73
|
+
* @property {ColorChain} bgRedBright Bright red background.
|
|
74
|
+
* @property {ColorChain} bgGreenBright Bright green background.
|
|
75
|
+
* @property {ColorChain} bgYellowBright Bright yellow background.
|
|
76
|
+
* @property {ColorChain} bgBlueBright Bright blue background.
|
|
77
|
+
* @property {ColorChain} bgMagentaBright Bright magenta background.
|
|
78
|
+
* @property {ColorChain} bgCyanBright Bright cyan background.
|
|
79
|
+
* @property {ColorChain} bgWhiteBright Bright white background.
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The named styles, read-only.
|
|
84
|
+
* @typedef {Readonly<ColorStyleProps>} ColorStyles
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A style you can call on text, or chain with more styles.
|
|
89
|
+
*
|
|
90
|
+
* @typedef {((...text: unknown[]) => string) & ColorStyles & ColorMethods} ColorChain
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Custom colors, available on every chain.
|
|
95
|
+
* @typedef {object} ColorMethods
|
|
96
|
+
* @property {(r: number, g: number, b: number) => ColorChain} rgb Text color from red, green and blue (0-255).
|
|
97
|
+
* @property {(r: number, g: number, b: number) => ColorChain} bgRgb Background from red, green and blue (0-255).
|
|
98
|
+
* @property {(hex: string) => ColorChain} hex Text color from a hex code like `"#f80"`.
|
|
99
|
+
* @property {(hex: string) => ColorChain} bgHex Background from a hex code.
|
|
100
|
+
* @property {(code: number) => ColorChain} ansi256 Text color from the 256-color palette.
|
|
101
|
+
* @property {(code: number) => ColorChain} bgAnsi256 Background from the 256-color palette.
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* `nc.color`: every style and color, plus a few helpers.
|
|
106
|
+
*
|
|
107
|
+
* @typedef {ColorStyles & ColorMethods & ColorHelpers} ColorModule
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Color helpers.
|
|
112
|
+
* @typedef {object} ColorHelpers
|
|
113
|
+
* @property {ColorLevel} level The current color level. Set it to force one: `0` turns colors off, `undefined` goes back to what the terminal supports.
|
|
114
|
+
* @property {boolean} enabled Whether colors are being output.
|
|
115
|
+
* @property {(level?: ColorLevel) => ColorModule} create A separate instance with its own level, for another stream or for tests.
|
|
116
|
+
* @property {(stream?: NodeJS.WriteStream | { isTTY?: boolean }) => ColorLevel} detect The color level a stream supports, taking `NO_COLOR` and `FORCE_COLOR` into account.
|
|
117
|
+
* @property {(text: string) => string} strip Removes colors and other terminal escape codes.
|
|
118
|
+
* @property {(text: string) => number} width How many columns the text takes, ignoring colors and counting emoji and CJK as 2.
|
|
119
|
+
* @property {(text: string, columns: number, options?: { ellipsis?: string }) => string} truncate Cuts text to a number of columns and adds `…`. Colors are kept and still closed properly.
|
|
120
|
+
* @property {(text: string, url: string) => string} link A clickable link in terminals that support it, `"text (url)"` elsewhere.
|
|
121
|
+
* @property {(text: string, colors: string[]) => string} gradient Colors the text along a gradient of hex colors.
|
|
122
|
+
*/
|
|
123
|
+
|
|
124
|
+
/** @type {Record<string, [number, number]>} */
|
|
125
|
+
const _CODES = {
|
|
126
|
+
reset: [0, 0], bold: [1, 22], dim: [2, 22], italic: [3, 23], underline: [4, 24], overline: [53, 55],
|
|
127
|
+
inverse: [7, 27], hidden: [8, 28], strikethrough: [9, 29],
|
|
128
|
+
black: [30, 39], red: [31, 39], green: [32, 39], yellow: [33, 39], blue: [34, 39], magenta: [35, 39], cyan: [36, 39], white: [37, 39],
|
|
129
|
+
gray: [90, 39], grey: [90, 39], blackBright: [90, 39], redBright: [91, 39], greenBright: [92, 39], yellowBright: [93, 39],
|
|
130
|
+
blueBright: [94, 39], magentaBright: [95, 39], cyanBright: [96, 39], whiteBright: [97, 39],
|
|
131
|
+
bgBlack: [40, 49], bgRed: [41, 49], bgGreen: [42, 49], bgYellow: [43, 49], bgBlue: [44, 49], bgMagenta: [45, 49], bgCyan: [46, 49], bgWhite: [47, 49],
|
|
132
|
+
bgGray: [100, 49], bgGrey: [100, 49], bgBlackBright: [100, 49], bgRedBright: [101, 49], bgGreenBright: [102, 49], bgYellowBright: [103, 49],
|
|
133
|
+
bgBlueBright: [104, 49], bgMagentaBright: [105, 49], bgCyanBright: [106, 49], bgWhiteBright: [107, 49],
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/** @type {Record<string, { open: string, close: string }>} */
|
|
137
|
+
const _STYLES = {};
|
|
138
|
+
for (const [name, [open, close]] of Object.entries(_CODES)) _STYLES[name] = { open: `\u001b[${open}m`, close: `\u001b[${close}m` };
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* A style in a chain: fixed codes, or a color resolved at output time.
|
|
142
|
+
* @typedef {{ open: string, close: string } | { rgb: [number, number, number], bg: boolean } | { ansi256: number, bg: boolean }} StyleEntry
|
|
143
|
+
*/
|
|
144
|
+
|
|
145
|
+
const _ANSI_REGEX = /[\u001b\u009b](?:\][^\u0007\u001b]*(?:\u0007|\u001b\\)|[[\]()#;?]*(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~])/g;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {string} hex
|
|
149
|
+
* @returns {[number, number, number]}
|
|
150
|
+
*/
|
|
151
|
+
const _hexToRgb = (hex) => {
|
|
152
|
+
const clean = String(hex).replace(/^#/, "");
|
|
153
|
+
const full = clean.length === 3 || clean.length === 4 ? [...clean.slice(0, 3)].map((c) => c + c).join("") : clean.slice(0, 6);
|
|
154
|
+
if (!/^[0-9a-f]{6}$/i.test(full)) throw new TypeError(`Invalid hex color "${hex}"`);
|
|
155
|
+
return [parseInt(full.slice(0, 2), 16), parseInt(full.slice(2, 4), 16), parseInt(full.slice(4, 6), 16)];
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {number} r
|
|
160
|
+
* @param {number} g
|
|
161
|
+
* @param {number} b
|
|
162
|
+
* @returns {number}
|
|
163
|
+
*/
|
|
164
|
+
const _rgbToAnsi256 = (r, g, b) => {
|
|
165
|
+
if (r === g && g === b) {
|
|
166
|
+
if (r < 8) return 16;
|
|
167
|
+
if (r > 248) return 231;
|
|
168
|
+
return Math.round(((r - 8) / 247) * 24) + 232;
|
|
169
|
+
}
|
|
170
|
+
return 16 + 36 * Math.round((r / 255) * 5) + 6 * Math.round((g / 255) * 5) + Math.round((b / 255) * 5);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/** @type {Array<[number, number, number]>} */
|
|
174
|
+
const _ANSI16_RGB = [
|
|
175
|
+
[0, 0, 0], [205, 0, 0], [0, 205, 0], [205, 205, 0], [0, 0, 238], [205, 0, 205], [0, 205, 205], [229, 229, 229],
|
|
176
|
+
[127, 127, 127], [255, 0, 0], [0, 255, 0], [255, 255, 0], [92, 92, 255], [255, 0, 255], [0, 255, 255], [255, 255, 255],
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {number} code
|
|
181
|
+
* @returns {[number, number, number]}
|
|
182
|
+
*/
|
|
183
|
+
const _ansi256ToRgb = (code) => {
|
|
184
|
+
if (code < 16) return _ANSI16_RGB[code];
|
|
185
|
+
if (code >= 232) {
|
|
186
|
+
const v = (code - 232) * 10 + 8;
|
|
187
|
+
return [v, v, v];
|
|
188
|
+
}
|
|
189
|
+
const n = code - 16;
|
|
190
|
+
const step = (/** @type {number} */ x) => (x === 0 ? 0 : x * 40 + 55);
|
|
191
|
+
return [step(Math.floor(n / 36)), step(Math.floor((n % 36) / 6)), step(n % 6)];
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @param {number} r
|
|
196
|
+
* @param {number} g
|
|
197
|
+
* @param {number} b
|
|
198
|
+
* @returns {number} A 16-color foreground code (30-37, 90-97).
|
|
199
|
+
*/
|
|
200
|
+
const _rgbToAnsi16 = (r, g, b) => {
|
|
201
|
+
const value = Math.round((Math.max(r, g, b) / 255) * 2);
|
|
202
|
+
if (value === 0) return 30;
|
|
203
|
+
const code = 30 + ((Math.round(b / 255) << 2) | (Math.round(g / 255) << 1) | Math.round(r / 255));
|
|
204
|
+
return value === 2 ? code + 60 : code;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Resolves a style entry into escape codes for a given level.
|
|
209
|
+
* @param {StyleEntry} entry
|
|
210
|
+
* @param {ColorLevel} level
|
|
211
|
+
* @returns {{ open: string, close: string }}
|
|
212
|
+
*/
|
|
213
|
+
const _resolve = (entry, level) => {
|
|
214
|
+
if ("open" in entry) return entry;
|
|
215
|
+
const close = entry.bg ? "\u001b[49m" : "\u001b[39m";
|
|
216
|
+
const rgb = "rgb" in entry ? entry.rgb : _ansi256ToRgb(entry.ansi256);
|
|
217
|
+
if (level >= 3 && "rgb" in entry) return { open: `\u001b[${entry.bg ? 48 : 38};2;${rgb[0]};${rgb[1]};${rgb[2]}m`, close };
|
|
218
|
+
if (level >= 2) {
|
|
219
|
+
const code = "ansi256" in entry ? entry.ansi256 : _rgbToAnsi256(...rgb);
|
|
220
|
+
return { open: `\u001b[${entry.bg ? 48 : 38};5;${code}m`, close };
|
|
221
|
+
}
|
|
222
|
+
const code = _rgbToAnsi16(...rgb) + (entry.bg ? 10 : 0);
|
|
223
|
+
return { open: `\u001b[${code}m`, close };
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Command line flags don't change while the program runs: read them once.
|
|
227
|
+
/** @type {{ off: boolean, level: ColorLevel | undefined, forced: boolean } | undefined} */
|
|
228
|
+
let _flags;
|
|
229
|
+
const _argvFlags = () => {
|
|
230
|
+
if (_flags) return _flags;
|
|
231
|
+
const argv = process.argv;
|
|
232
|
+
const has = (/** @type {string[]} */ ...names) => names.some((name) => argv.includes(name));
|
|
233
|
+
return (_flags = {
|
|
234
|
+
off: has("--no-color", "--no-colors", "--color=false", "--color=never"),
|
|
235
|
+
level: has("--color=16m", "--color=full", "--color=truecolor") ? 3 : has("--color=256") ? 2 : undefined,
|
|
236
|
+
forced: has("--color", "--colors", "--color=true", "--color=always"),
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const _CI_WITH_COLORS = ["TRAVIS", "CIRCLECI", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"];
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @param {NodeJS.WriteStream | { isTTY?: boolean } | undefined} stream
|
|
244
|
+
* @returns {ColorLevel}
|
|
245
|
+
*/
|
|
246
|
+
const _detect = (stream = process.stdout) => {
|
|
247
|
+
const env = process.env;
|
|
248
|
+
const flags = _argvFlags();
|
|
249
|
+
if (flags.off) return 0;
|
|
250
|
+
if (env.NO_COLOR !== undefined && env.NO_COLOR !== "") return 0;
|
|
251
|
+
if (env.NODE_DISABLE_COLORS !== undefined && env.NODE_DISABLE_COLORS !== "") return 0;
|
|
252
|
+
if (env.FORCE_COLOR !== undefined) {
|
|
253
|
+
const value = env.FORCE_COLOR.toLowerCase();
|
|
254
|
+
if (value === "false" || value === "0") return 0;
|
|
255
|
+
if (value === "2") return 2;
|
|
256
|
+
if (value === "3") return 3;
|
|
257
|
+
return 1;
|
|
258
|
+
}
|
|
259
|
+
if (flags.level !== undefined) return flags.level;
|
|
260
|
+
if (env.TERM === "dumb" && !flags.forced) return 0;
|
|
261
|
+
const tty = /** @type {NodeJS.WriteStream | undefined} */ (stream && "isTTY" in stream && stream.isTTY ? stream : undefined);
|
|
262
|
+
if (tty && typeof tty.getColorDepth === "function") {
|
|
263
|
+
const depth = tty.getColorDepth();
|
|
264
|
+
if (depth >= 24) return 3;
|
|
265
|
+
if (depth >= 8) return 2;
|
|
266
|
+
if (depth >= 4) return 1;
|
|
267
|
+
return flags.forced ? 1 : 0;
|
|
268
|
+
}
|
|
269
|
+
if (env.CI !== undefined) {
|
|
270
|
+
if (env.GITHUB_ACTIONS || env.GITEA_ACTIONS) return 3;
|
|
271
|
+
if (_CI_WITH_COLORS.some((name) => name in env)) return 1;
|
|
272
|
+
}
|
|
273
|
+
return flags.forced ? 1 : 0;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const _PLAIN = /^[\x20-\x7e]*$/;
|
|
277
|
+
// What can turn several code points into one character on screen: accents, joiners, flags, skin tones.
|
|
278
|
+
const _JOINED = /[\p{Mark}\u200d\ufe0e\ufe0f\u{1f1e6}-\u{1f1ff}\u{1f3fb}-\u{1f3ff}\u{e0020}-\u{e007f}]/u;
|
|
279
|
+
const _MARK = /^\p{Mark}+$/u;
|
|
280
|
+
const _EMOJI = /\p{Emoji_Presentation}/u;
|
|
281
|
+
const _WIDE = /[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-가-힣豈-︰-﹏-⦆¢-₩\u{20000}-\u{3FFFD}]/u;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Terminal width of one grapheme.
|
|
285
|
+
* @param {string} grapheme
|
|
286
|
+
* @returns {number}
|
|
287
|
+
*/
|
|
288
|
+
const _charWidth = (grapheme) => {
|
|
289
|
+
const cp = /** @type {number} */ (grapheme.codePointAt(0));
|
|
290
|
+
if (cp < 32 || (cp >= 0x7f && cp < 0xa0)) return 0;
|
|
291
|
+
if (cp < 0x300 && grapheme.length === 1) return 1;
|
|
292
|
+
// zero width space and joiners, direction marks, byte order mark
|
|
293
|
+
if ((cp >= 0x200b && cp <= 0x200f) || cp === 0x2060 || cp === 0xfeff) return 0;
|
|
294
|
+
if (_MARK.test(grapheme)) return 0;
|
|
295
|
+
if (_EMOJI.test(grapheme) || grapheme.includes("\ufe0f")) return 2;
|
|
296
|
+
return _WIDE.test(grapheme) ? 2 : 1;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @param {string} text
|
|
301
|
+
* @returns {string}
|
|
302
|
+
*/
|
|
303
|
+
const strip = (text) => {
|
|
304
|
+
const value = typeof text === "string" ? text : String(text);
|
|
305
|
+
return value.indexOf("\u001b") === -1 && value.indexOf("\u009b") === -1 ? value : value.replace(_ANSI_REGEX, "");
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* @param {string} text
|
|
310
|
+
* @returns {number}
|
|
311
|
+
*/
|
|
312
|
+
const width = (text) => {
|
|
313
|
+
const plain = strip(text);
|
|
314
|
+
let i = 0;
|
|
315
|
+
// ASCII and accented Latin letters: one column each.
|
|
316
|
+
for (; i < plain.length; i++) {
|
|
317
|
+
const code = plain.charCodeAt(i);
|
|
318
|
+
if (code < 0x20 || (code > 0x7e && code < 0xa0) || code > 0x2ff) break;
|
|
319
|
+
}
|
|
320
|
+
if (i === plain.length) return i;
|
|
321
|
+
let total = 0;
|
|
322
|
+
if (_JOINED.test(plain)) {
|
|
323
|
+
for (const { segment } of segmenter().segment(plain)) total += _charWidth(segment);
|
|
324
|
+
} else {
|
|
325
|
+
for (const char of plain) total += _charWidth(char);
|
|
326
|
+
}
|
|
327
|
+
return total;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* @param {string} text
|
|
332
|
+
* @param {number} columns
|
|
333
|
+
* @param {{ ellipsis?: string }} [options]
|
|
334
|
+
* @returns {string}
|
|
335
|
+
*/
|
|
336
|
+
const truncate = (text, columns, options = {}) => {
|
|
337
|
+
const value = typeof text === "string" ? text : String(text);
|
|
338
|
+
const max = Number.isFinite(columns) ? Math.max(0, Math.floor(columns)) : Infinity;
|
|
339
|
+
if (width(value) <= max) return value;
|
|
340
|
+
const mark = options.ellipsis ?? "…";
|
|
341
|
+
const ellipsis = width(mark) <= max ? mark : "";
|
|
342
|
+
const room = max - width(ellipsis);
|
|
343
|
+
if (_PLAIN.test(value)) return value.slice(0, room) + ellipsis;
|
|
344
|
+
let out = "";
|
|
345
|
+
let used = 0;
|
|
346
|
+
let cut = false;
|
|
347
|
+
const take = (/** @type {string} */ plain) => {
|
|
348
|
+
if (cut || !plain) return;
|
|
349
|
+
if (_PLAIN.test(plain) && used + plain.length <= room) {
|
|
350
|
+
out += plain;
|
|
351
|
+
used += plain.length;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
for (const { segment } of segmenter().segment(plain)) {
|
|
355
|
+
const columnsTaken = _charWidth(segment);
|
|
356
|
+
if (used + columnsTaken > room) {
|
|
357
|
+
cut = true;
|
|
358
|
+
out += ellipsis;
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
out += segment;
|
|
362
|
+
used += columnsTaken;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
let last = 0;
|
|
366
|
+
// Escape codes past the cut are kept: they are what closes the styles opened before it.
|
|
367
|
+
for (const match of value.matchAll(_ANSI_REGEX)) {
|
|
368
|
+
take(value.slice(last, match.index));
|
|
369
|
+
out += match[0];
|
|
370
|
+
last = match.index + match[0].length;
|
|
371
|
+
}
|
|
372
|
+
take(value.slice(last));
|
|
373
|
+
return out;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const _supportsHyperlinks = () => {
|
|
377
|
+
const env = process.env;
|
|
378
|
+
if (env.FORCE_HYPERLINK === "1") return true;
|
|
379
|
+
if (env.FORCE_HYPERLINK === "0") return false;
|
|
380
|
+
if (!process.stdout.isTTY) return false;
|
|
381
|
+
if (env.WT_SESSION || env.KONSOLE_VERSION || env.DOMTERM) return true;
|
|
382
|
+
if (["iTerm.app", "WezTerm", "vscode", "Hyper", "ghostty", "Tabby"].includes(env.TERM_PROGRAM ?? "")) return true;
|
|
383
|
+
if (env.VTE_VERSION && Number(env.VTE_VERSION) >= 5000) return true;
|
|
384
|
+
return false;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
/** @param {number} value */
|
|
388
|
+
const _byte = (value) => (value > 0 ? (value < 255 ? Math.round(value) : 255) : 0);
|
|
389
|
+
|
|
390
|
+
const _NEWLINE = /\r?\n/g;
|
|
391
|
+
const _STACK = Symbol("stack");
|
|
392
|
+
const _CUSTOM = Symbol("custom");
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Builds an independent color instance.
|
|
396
|
+
* @param {ColorLevel | undefined} forcedLevel
|
|
397
|
+
* @returns {ColorModule}
|
|
398
|
+
*/
|
|
399
|
+
const _createModule = (forcedLevel) => {
|
|
400
|
+
/** @type {ColorLevel | undefined} */
|
|
401
|
+
let level = forcedLevel;
|
|
402
|
+
// Goes up when the level changes, so every chain knows its codes are out of date.
|
|
403
|
+
let generation = 0;
|
|
404
|
+
const currentLevel = () => (level === undefined ? (level = _detect()) : level);
|
|
405
|
+
|
|
406
|
+
/** @type {any} */
|
|
407
|
+
const proto = Object.create(Function.prototype);
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* @param {StyleEntry[]} stack
|
|
411
|
+
* @returns {ColorChain}
|
|
412
|
+
*/
|
|
413
|
+
const chain = (stack) => {
|
|
414
|
+
let seen = -1;
|
|
415
|
+
let open = "";
|
|
416
|
+
let close = "";
|
|
417
|
+
/** @type {Array<{ open: string, close: string }>} */
|
|
418
|
+
let pairs = [];
|
|
419
|
+
const refresh = () => {
|
|
420
|
+
seen = generation;
|
|
421
|
+
open = "";
|
|
422
|
+
close = "";
|
|
423
|
+
pairs = [];
|
|
424
|
+
const current = currentLevel();
|
|
425
|
+
if (current === 0) return;
|
|
426
|
+
for (const entry of stack) {
|
|
427
|
+
const pair = _resolve(entry, current);
|
|
428
|
+
pairs.push(pair);
|
|
429
|
+
open += pair.open;
|
|
430
|
+
close = pair.close + close;
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
/** @type {any} */
|
|
434
|
+
const fn = (/** @type {unknown[]} */ ...parts) => {
|
|
435
|
+
const text = parts.length === 1 ? (typeof parts[0] === "string" ? parts[0] : String(parts[0])) : parts.map((part) => String(part)).join(" ");
|
|
436
|
+
if (seen !== generation) refresh();
|
|
437
|
+
if (open === "" || text === "") return text;
|
|
438
|
+
if (text.indexOf("\u001b") === -1 && text.indexOf("\n") === -1) return open + text + close;
|
|
439
|
+
let result = text;
|
|
440
|
+
// A style closed inside the text would also end ours: open it again right after.
|
|
441
|
+
for (const pair of pairs) {
|
|
442
|
+
if (result.includes(pair.close)) result = result.replaceAll(pair.close, pair.close + pair.open);
|
|
443
|
+
}
|
|
444
|
+
// Styles stop at the end of each line, or a background would run to the edge of the terminal.
|
|
445
|
+
if (result.includes("\n")) result = result.replace(_NEWLINE, (newline) => close + newline + open);
|
|
446
|
+
return open + result + close;
|
|
447
|
+
};
|
|
448
|
+
Object.setPrototypeOf(fn, proto);
|
|
449
|
+
fn[_STACK] = stack;
|
|
450
|
+
return fn;
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// Each chain is built the first time it's read, then kept on its parent:
|
|
454
|
+
// `color.red.bold` costs two property reads from then on.
|
|
455
|
+
for (const name of Object.keys(_STYLES)) {
|
|
456
|
+
Object.defineProperty(proto, name, {
|
|
457
|
+
get() {
|
|
458
|
+
const next = chain([...this[_STACK], _STYLES[name]]);
|
|
459
|
+
Object.defineProperty(this, name, { value: next });
|
|
460
|
+
return next;
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* @param {any} parent
|
|
467
|
+
* @param {string} key
|
|
468
|
+
* @param {() => StyleEntry} entry
|
|
469
|
+
* @returns {ColorChain}
|
|
470
|
+
*/
|
|
471
|
+
const custom = (parent, key, entry) => {
|
|
472
|
+
/** @type {Map<string, ColorChain>} */
|
|
473
|
+
const known = (parent[_CUSTOM] ??= new Map());
|
|
474
|
+
let next = known.get(key);
|
|
475
|
+
if (!next) {
|
|
476
|
+
if (known.size >= 256) known.clear();
|
|
477
|
+
known.set(key, (next = chain([...parent[_STACK], entry()])));
|
|
478
|
+
}
|
|
479
|
+
return next;
|
|
480
|
+
};
|
|
481
|
+
/** @type {Record<string, (this: any, ...args: any[]) => ColorChain>} */
|
|
482
|
+
const methods = {
|
|
483
|
+
rgb(r, g, b) { return custom(this, `f${r},${g},${b}`, () => ({ rgb: [_byte(r), _byte(g), _byte(b)], bg: false })); },
|
|
484
|
+
bgRgb(r, g, b) { return custom(this, `b${r},${g},${b}`, () => ({ rgb: [_byte(r), _byte(g), _byte(b)], bg: true })); },
|
|
485
|
+
hex(hex) { return custom(this, `F${hex}`, () => ({ rgb: _hexToRgb(hex), bg: false })); },
|
|
486
|
+
bgHex(hex) { return custom(this, `B${hex}`, () => ({ rgb: _hexToRgb(hex), bg: true })); },
|
|
487
|
+
ansi256(code) { return custom(this, `a${code}`, () => ({ ansi256: _byte(code), bg: false })); },
|
|
488
|
+
bgAnsi256(code) { return custom(this, `A${code}`, () => ({ ansi256: _byte(code), bg: true })); },
|
|
489
|
+
};
|
|
490
|
+
for (const [name, method] of Object.entries(methods)) Object.defineProperty(proto, name, { value: method });
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* @param {string} text
|
|
494
|
+
* @param {string[]} colors
|
|
495
|
+
* @returns {string}
|
|
496
|
+
*/
|
|
497
|
+
const gradient = (text, colors) => {
|
|
498
|
+
const value = String(text);
|
|
499
|
+
const stops = colors.map(_hexToRgb);
|
|
500
|
+
const current = currentLevel();
|
|
501
|
+
if (!stops.length || current === 0) return value;
|
|
502
|
+
const letters = graphemes(value);
|
|
503
|
+
let visible = 0;
|
|
504
|
+
for (const letter of letters) if (letter.trim()) visible++;
|
|
505
|
+
const last = stops.length - 1;
|
|
506
|
+
let index = 0;
|
|
507
|
+
let out = "";
|
|
508
|
+
for (const letter of letters) {
|
|
509
|
+
if (!letter.trim()) {
|
|
510
|
+
out += letter;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
const position = visible <= 1 ? 0 : (index++ / (visible - 1)) * last;
|
|
514
|
+
const from = Math.min(Math.floor(position), Math.max(0, last - 1));
|
|
515
|
+
const a = stops[from];
|
|
516
|
+
const b = stops[Math.min(from + 1, last)];
|
|
517
|
+
const share = position - from;
|
|
518
|
+
const pair = _resolve({ rgb: [Math.round(a[0] + (b[0] - a[0]) * share), Math.round(a[1] + (b[1] - a[1]) * share), Math.round(a[2] + (b[2] - a[2]) * share)], bg: false }, current);
|
|
519
|
+
out += pair.open + letter + pair.close;
|
|
520
|
+
}
|
|
521
|
+
return out;
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
/** @type {any} */
|
|
525
|
+
const root = Object.create(proto);
|
|
526
|
+
root[_STACK] = [];
|
|
527
|
+
Object.defineProperties(root, {
|
|
528
|
+
level: {
|
|
529
|
+
enumerable: true,
|
|
530
|
+
get: currentLevel,
|
|
531
|
+
set(/** @type {ColorLevel | undefined} */ value) {
|
|
532
|
+
// `undefined` goes back to what the terminal supports.
|
|
533
|
+
level = value === undefined ? undefined : /** @type {ColorLevel} */ (Number.isFinite(value) ? Math.max(0, Math.min(3, Math.floor(value))) : 0);
|
|
534
|
+
generation++;
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
enabled: { enumerable: true, get: () => currentLevel() > 0 },
|
|
538
|
+
create: { enumerable: true, value: (/** @type {ColorLevel | undefined} */ lvl) => _createModule(lvl) },
|
|
539
|
+
detect: { enumerable: true, value: _detect },
|
|
540
|
+
strip: { enumerable: true, value: strip },
|
|
541
|
+
width: { enumerable: true, value: width },
|
|
542
|
+
truncate: { enumerable: true, value: truncate },
|
|
543
|
+
link: {
|
|
544
|
+
enumerable: true,
|
|
545
|
+
value: (/** @type {string} */ text, /** @type {string} */ url) =>
|
|
546
|
+
currentLevel() > 0 && _supportsHyperlinks() ? `\u001b]8;;${url}\u0007${text}\u001b]8;;\u0007` : `${text} (${url})`,
|
|
547
|
+
},
|
|
548
|
+
gradient: { enumerable: true, value: gradient },
|
|
549
|
+
});
|
|
550
|
+
return root;
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
/** @type {ColorModule} */
|
|
554
|
+
const color = _createModule(undefined);
|
|
555
|
+
|
|
556
|
+
module.exports = color;
|