@heinrichb/console-toolkit 1.0.7 → 1.0.9
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/dist/core/layout.d.ts +2 -2
- package/dist/index.js +385 -14
- package/dist/index.min.js +2 -0
- package/package.json +6 -5
- package/dist/components/progress.js +0 -42
- package/dist/components/progress.test.d.ts +0 -1
- package/dist/components/progress.test.js +0 -105
- package/dist/components/spinner.js +0 -33
- package/dist/components/spinner.test.d.ts +0 -1
- package/dist/components/spinner.test.js +0 -53
- package/dist/core/layout.js +0 -55
- package/dist/core/layout.test.d.ts +0 -1
- package/dist/core/layout.test.js +0 -52
- package/dist/core/printer.js +0 -129
- package/dist/core/printer.test.d.ts +0 -1
- package/dist/core/printer.test.js +0 -124
- package/dist/core/style.js +0 -171
- package/dist/core/style.test.d.ts +0 -1
- package/dist/core/style.test.js +0 -108
- package/dist/core/types.js +0 -4
- package/dist/core/utils.js +0 -36
- package/dist/core/utils.test.d.ts +0 -1
- package/dist/core/utils.test.js +0 -25
- package/dist/demo.d.ts +0 -1
- package/dist/demo.js +0 -145
- package/dist/demo.test.d.ts +0 -1
- package/dist/demo.test.js +0 -28
- package/dist/presets/ascii.js +0 -35
- package/dist/presets/ascii.test.d.ts +0 -1
- package/dist/presets/ascii.test.js +0 -21
package/dist/core/layout.d.ts
CHANGED
|
@@ -10,10 +10,10 @@ import { Printer } from "./printer";
|
|
|
10
10
|
* @param widths - Optional fixed widths for each column.
|
|
11
11
|
* @returns A single array of PrintLines representing the merged output.
|
|
12
12
|
*/
|
|
13
|
-
export declare function
|
|
13
|
+
export declare function mergeColumns(columns: PrintLine[][], separator?: string, defaultStyle?: PrintStyle, widths?: number[]): PrintLine[];
|
|
14
14
|
/**
|
|
15
15
|
* Prints multiple columns of styled content to the console.
|
|
16
|
-
* A convenience wrapper around `
|
|
16
|
+
* A convenience wrapper around `mergeColumns` and `Printer.print`.
|
|
17
17
|
*
|
|
18
18
|
* @param columns - Array of columns to print.
|
|
19
19
|
* @param options - Layout options (widths, separator, custom printer).
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,385 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
// src/core/style.ts
|
|
2
|
+
var ESC = "\x1B";
|
|
3
|
+
var RESET = `${ESC}[0m`;
|
|
4
|
+
var STANDARD_COLORS = {
|
|
5
|
+
black: "#000000",
|
|
6
|
+
red: "#EF4444",
|
|
7
|
+
green: "#10B981",
|
|
8
|
+
yellow: "#F59E0B",
|
|
9
|
+
blue: "#3B82F6",
|
|
10
|
+
magenta: "#EC4899",
|
|
11
|
+
cyan: "#06B6D4",
|
|
12
|
+
white: "#FFFFFF",
|
|
13
|
+
gray: "#6B7280",
|
|
14
|
+
grey: "#6B7280"
|
|
15
|
+
};
|
|
16
|
+
var MODIFIER_CODES = {
|
|
17
|
+
default: "0",
|
|
18
|
+
bold: "1",
|
|
19
|
+
dim: "2",
|
|
20
|
+
italic: "3",
|
|
21
|
+
underline: "4",
|
|
22
|
+
inverse: "7",
|
|
23
|
+
hidden: "8",
|
|
24
|
+
strikethrough: "9"
|
|
25
|
+
};
|
|
26
|
+
function colorToHex(color) {
|
|
27
|
+
if (color.startsWith("#"))
|
|
28
|
+
return color;
|
|
29
|
+
const hex = STANDARD_COLORS[color.toLowerCase()];
|
|
30
|
+
if (!hex)
|
|
31
|
+
return "#FFFFFF";
|
|
32
|
+
return hex;
|
|
33
|
+
}
|
|
34
|
+
function hexToRgb(hex) {
|
|
35
|
+
const h = hex.replace(/^#/, "");
|
|
36
|
+
if (h.length !== 6)
|
|
37
|
+
return { r: 255, g: 255, b: 255 };
|
|
38
|
+
return {
|
|
39
|
+
r: parseInt(h.substring(0, 2), 16),
|
|
40
|
+
g: parseInt(h.substring(2, 4), 16),
|
|
41
|
+
b: parseInt(h.substring(4, 6), 16)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function rgbToAnsi(r, g, b) {
|
|
45
|
+
return `${ESC}[38;2;${r};${g};${b}m`;
|
|
46
|
+
}
|
|
47
|
+
function resolveColorToAnsi(color) {
|
|
48
|
+
const hex = colorToHex(color);
|
|
49
|
+
const { r, g, b } = hexToRgb(hex);
|
|
50
|
+
return rgbToAnsi(r, g, b);
|
|
51
|
+
}
|
|
52
|
+
function resolveModifiersToAnsi(modifiers) {
|
|
53
|
+
if (!modifiers || modifiers.length === 0)
|
|
54
|
+
return "";
|
|
55
|
+
return modifiers.map((m) => {
|
|
56
|
+
const code = MODIFIER_CODES[m];
|
|
57
|
+
return code ? `${ESC}[${code}m` : "";
|
|
58
|
+
}).join("");
|
|
59
|
+
}
|
|
60
|
+
function toHex(c) {
|
|
61
|
+
const hex = Math.max(0, Math.min(255, Math.round(c))).toString(16);
|
|
62
|
+
return hex.length === 1 ? "0" + hex : hex;
|
|
63
|
+
}
|
|
64
|
+
function interpolateHex(color1, color2, factor) {
|
|
65
|
+
const f = Math.max(0, Math.min(1, factor));
|
|
66
|
+
const c1 = hexToRgb(color1);
|
|
67
|
+
const c2 = hexToRgb(color2);
|
|
68
|
+
const r = c1.r + f * (c2.r - c1.r);
|
|
69
|
+
const g = c1.g + f * (c2.g - c1.g);
|
|
70
|
+
const b = c1.b + f * (c2.b - c1.b);
|
|
71
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
72
|
+
}
|
|
73
|
+
function interpolateColor(color1, color2, factor) {
|
|
74
|
+
return interpolateHex(colorToHex(color1), colorToHex(color2), factor);
|
|
75
|
+
}
|
|
76
|
+
function getGradientColor(colors, factor) {
|
|
77
|
+
if (colors.length === 0)
|
|
78
|
+
return "";
|
|
79
|
+
if (colors.length === 1)
|
|
80
|
+
return resolveColorToAnsi(colors[0]);
|
|
81
|
+
const f = Math.max(0, Math.min(1, factor));
|
|
82
|
+
const segmentLength = 1 / (colors.length - 1);
|
|
83
|
+
const segmentIndex = Math.min(Math.floor(f / segmentLength), colors.length - 2);
|
|
84
|
+
const segmentFactor = (f - segmentIndex * segmentLength) / segmentLength;
|
|
85
|
+
const c1 = colors[segmentIndex];
|
|
86
|
+
const c2 = colors[segmentIndex + 1];
|
|
87
|
+
const hex = interpolateColor(c1, c2, segmentFactor);
|
|
88
|
+
return resolveColorToAnsi(hex);
|
|
89
|
+
}
|
|
90
|
+
function mergeStyles(parent, child) {
|
|
91
|
+
if (!parent && !child)
|
|
92
|
+
return {};
|
|
93
|
+
if (!parent)
|
|
94
|
+
return child ?? {};
|
|
95
|
+
if (!child)
|
|
96
|
+
return parent;
|
|
97
|
+
const mergedModifiers = Array.from(new Set([...parent.modifiers ?? [], ...child.modifiers ?? []]));
|
|
98
|
+
return {
|
|
99
|
+
modifiers: mergedModifiers,
|
|
100
|
+
color: child.color ?? parent.color
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function resolveStyle(style, gradientFactor = 0) {
|
|
104
|
+
if (!style)
|
|
105
|
+
return "";
|
|
106
|
+
let ansi = "";
|
|
107
|
+
if (style.modifiers) {
|
|
108
|
+
ansi += resolveModifiersToAnsi(style.modifiers);
|
|
109
|
+
}
|
|
110
|
+
if (style.color) {
|
|
111
|
+
if (Array.isArray(style.color)) {
|
|
112
|
+
const hex = getGradientColor(style.color, gradientFactor);
|
|
113
|
+
ansi += hex;
|
|
114
|
+
} else {
|
|
115
|
+
ansi += resolveColorToAnsi(style.color);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return ansi;
|
|
119
|
+
}
|
|
120
|
+
// src/core/utils.ts
|
|
121
|
+
function getLineLength(line) {
|
|
122
|
+
return line.segments.reduce((acc, seg) => acc + seg.text.length, 0);
|
|
123
|
+
}
|
|
124
|
+
function computeMaxWidth(lines) {
|
|
125
|
+
return lines.length > 0 ? Math.max(...lines.map(getLineLength)) : 0;
|
|
126
|
+
}
|
|
127
|
+
function padLine(line, targetWidth, padStyle) {
|
|
128
|
+
const currentLength = getLineLength(line);
|
|
129
|
+
if (currentLength < targetWidth) {
|
|
130
|
+
return {
|
|
131
|
+
segments: [...line.segments, { text: " ".repeat(targetWidth - currentLength), style: padStyle }]
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return line;
|
|
135
|
+
}
|
|
136
|
+
// src/core/printer.ts
|
|
137
|
+
var ESC2 = "\x1B";
|
|
138
|
+
|
|
139
|
+
class Printer {
|
|
140
|
+
linesRendered = 0;
|
|
141
|
+
isLive;
|
|
142
|
+
data;
|
|
143
|
+
constructor(options = {}) {
|
|
144
|
+
this.isLive = options.live ?? false;
|
|
145
|
+
this.data = options.data;
|
|
146
|
+
}
|
|
147
|
+
getClearSequence() {
|
|
148
|
+
if (!this.isLive || this.linesRendered === 0)
|
|
149
|
+
return "";
|
|
150
|
+
return `${ESC2}[1A${ESC2}[2K\r`.repeat(this.linesRendered);
|
|
151
|
+
}
|
|
152
|
+
clear() {
|
|
153
|
+
if (this.linesRendered > 0) {
|
|
154
|
+
process.stdout.write(this.getClearSequence());
|
|
155
|
+
this.linesRendered = 0;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
print(data) {
|
|
159
|
+
if (data) {
|
|
160
|
+
this.data = data;
|
|
161
|
+
}
|
|
162
|
+
if (!this.data) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
let output = this.getClearSequence();
|
|
166
|
+
const lines = this.data.lines;
|
|
167
|
+
const blockStyle = this.data.style ?? {};
|
|
168
|
+
lines.forEach((line, lineIndex) => {
|
|
169
|
+
output += this.renderLine(line, lineIndex, lines.length, blockStyle);
|
|
170
|
+
output += `
|
|
171
|
+
`;
|
|
172
|
+
});
|
|
173
|
+
process.stdout.write(output);
|
|
174
|
+
this.linesRendered = lines.length;
|
|
175
|
+
}
|
|
176
|
+
resolveBlockColorForLine(blockStyle, lineIndex, totalLines) {
|
|
177
|
+
if (!blockStyle.color)
|
|
178
|
+
return;
|
|
179
|
+
if (Array.isArray(blockStyle.color)) {
|
|
180
|
+
if (totalLines <= 1)
|
|
181
|
+
return blockStyle.color[0];
|
|
182
|
+
const colors = blockStyle.color;
|
|
183
|
+
const factor = lineIndex / (totalLines - 1);
|
|
184
|
+
const f = Math.max(0, Math.min(1, factor));
|
|
185
|
+
const segmentLength = 1 / (colors.length - 1);
|
|
186
|
+
const segmentIndex = Math.min(Math.floor(f / segmentLength), colors.length - 2);
|
|
187
|
+
const segmentFactor = (f - segmentIndex * segmentLength) / segmentLength;
|
|
188
|
+
const c1 = colors[segmentIndex];
|
|
189
|
+
const c2 = colors[segmentIndex + 1];
|
|
190
|
+
return interpolateColor(c1, c2, segmentFactor);
|
|
191
|
+
}
|
|
192
|
+
return blockStyle.color;
|
|
193
|
+
}
|
|
194
|
+
renderLine(line, lineIndex, totalLines, parentBlockStyle) {
|
|
195
|
+
const blockColorForLine = this.resolveBlockColorForLine(parentBlockStyle, lineIndex, totalLines);
|
|
196
|
+
const baseLineStyle = {
|
|
197
|
+
modifiers: parentBlockStyle.modifiers,
|
|
198
|
+
color: blockColorForLine
|
|
199
|
+
};
|
|
200
|
+
const effectiveLineStyle = mergeStyles(baseLineStyle, line.style);
|
|
201
|
+
const totalChars = line.segments.reduce((acc, seg) => acc + seg.text.length, 0);
|
|
202
|
+
let currentCharIndex = 0;
|
|
203
|
+
let lineOutput = "";
|
|
204
|
+
line.segments.forEach((seg) => {
|
|
205
|
+
const effectiveSegmentStyle = mergeStyles(effectiveLineStyle, seg.style);
|
|
206
|
+
if (Array.isArray(effectiveSegmentStyle.color)) {
|
|
207
|
+
const colors = effectiveSegmentStyle.color;
|
|
208
|
+
const text = seg.text;
|
|
209
|
+
const isGlobalGradient = effectiveSegmentStyle.color === effectiveLineStyle.color;
|
|
210
|
+
const modifiersAnsi = resolveModifiersToAnsi(effectiveSegmentStyle.modifiers);
|
|
211
|
+
for (let i = 0;i < text.length; i++) {
|
|
212
|
+
let factor = 0;
|
|
213
|
+
if (isGlobalGradient && totalChars > 1) {
|
|
214
|
+
factor = (currentCharIndex + i) / (totalChars - 1);
|
|
215
|
+
} else if (!isGlobalGradient && text.length > 1) {
|
|
216
|
+
factor = i / (text.length - 1);
|
|
217
|
+
}
|
|
218
|
+
const colorAnsi = getGradientColor(colors, factor);
|
|
219
|
+
lineOutput += `${modifiersAnsi}${colorAnsi}${text[i]}`;
|
|
220
|
+
}
|
|
221
|
+
lineOutput += RESET;
|
|
222
|
+
} else {
|
|
223
|
+
const ansi = resolveStyle(effectiveSegmentStyle);
|
|
224
|
+
lineOutput += `${ansi}${seg.text}${RESET}`;
|
|
225
|
+
}
|
|
226
|
+
currentCharIndex += seg.text.length;
|
|
227
|
+
});
|
|
228
|
+
return lineOutput;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// src/core/layout.ts
|
|
232
|
+
function mergeColumns(columns, separator = " ", defaultStyle, widths) {
|
|
233
|
+
if (columns.length === 0)
|
|
234
|
+
return [];
|
|
235
|
+
const maxLines = Math.max(...columns.map((c) => c.length));
|
|
236
|
+
const colWidths = columns.map((col, i) => {
|
|
237
|
+
if (widths?.[i] !== undefined)
|
|
238
|
+
return widths[i];
|
|
239
|
+
return computeMaxWidth(col);
|
|
240
|
+
});
|
|
241
|
+
const output = [];
|
|
242
|
+
for (let i = 0;i < maxLines; i++) {
|
|
243
|
+
let segments = [];
|
|
244
|
+
for (let j = 0;j < columns.length; j++) {
|
|
245
|
+
const line = columns[j][i] || { segments: [] };
|
|
246
|
+
if (j < columns.length - 1) {
|
|
247
|
+
const padded = padLine(line, colWidths[j], defaultStyle);
|
|
248
|
+
segments = [...segments, ...padded.segments, { text: separator, style: defaultStyle }];
|
|
249
|
+
} else {
|
|
250
|
+
segments = [...segments, ...line.segments];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
output.push({ segments });
|
|
254
|
+
}
|
|
255
|
+
return output;
|
|
256
|
+
}
|
|
257
|
+
function printColumns(columns, options = {}) {
|
|
258
|
+
const { widths, separator = " ", printer = new Printer } = options;
|
|
259
|
+
const mergedLines = mergeColumns(columns, separator, undefined, widths);
|
|
260
|
+
printer.print({ lines: mergedLines });
|
|
261
|
+
}
|
|
262
|
+
// src/components/progress.ts
|
|
263
|
+
function createProgressBar(options) {
|
|
264
|
+
const {
|
|
265
|
+
progress,
|
|
266
|
+
width = 20,
|
|
267
|
+
style,
|
|
268
|
+
bracketStyle,
|
|
269
|
+
startStyle,
|
|
270
|
+
endStyle,
|
|
271
|
+
barStyle,
|
|
272
|
+
fillStyle,
|
|
273
|
+
emptyStyle,
|
|
274
|
+
percentageStyle,
|
|
275
|
+
startChar = "[",
|
|
276
|
+
endChar = "]",
|
|
277
|
+
fillChar = "█",
|
|
278
|
+
emptyChar = "░",
|
|
279
|
+
showPercentage = true,
|
|
280
|
+
formatPercentage
|
|
281
|
+
} = options;
|
|
282
|
+
const p = Math.max(0, Math.min(1, progress));
|
|
283
|
+
const filledWidth = Math.round(p * width);
|
|
284
|
+
const emptyWidth = width - filledWidth;
|
|
285
|
+
const resolvedBracketStyle = bracketStyle ?? style;
|
|
286
|
+
const resolvedStartStyle = startStyle ?? resolvedBracketStyle;
|
|
287
|
+
const resolvedEndStyle = endStyle ?? resolvedBracketStyle;
|
|
288
|
+
const resolvedBarStyle = barStyle ?? style;
|
|
289
|
+
const resolvedFillStyle = fillStyle ?? resolvedBarStyle;
|
|
290
|
+
const resolvedEmptyStyle = emptyStyle ?? resolvedBarStyle;
|
|
291
|
+
const resolvedPercentageStyle = percentageStyle ?? style;
|
|
292
|
+
const segments = [];
|
|
293
|
+
if (startChar) {
|
|
294
|
+
segments.push({ text: startChar, style: resolvedStartStyle });
|
|
295
|
+
}
|
|
296
|
+
if (filledWidth > 0) {
|
|
297
|
+
segments.push({ text: fillChar.repeat(filledWidth), style: resolvedFillStyle });
|
|
298
|
+
}
|
|
299
|
+
if (emptyWidth > 0) {
|
|
300
|
+
segments.push({ text: emptyChar.repeat(emptyWidth), style: resolvedEmptyStyle });
|
|
301
|
+
}
|
|
302
|
+
if (endChar) {
|
|
303
|
+
segments.push({ text: endChar, style: resolvedEndStyle });
|
|
304
|
+
}
|
|
305
|
+
if (showPercentage) {
|
|
306
|
+
const percentageText = formatPercentage ? formatPercentage(p) : ` ${Math.round(p * 100)}%`;
|
|
307
|
+
segments.push({ text: percentageText, style: resolvedPercentageStyle });
|
|
308
|
+
}
|
|
309
|
+
return { segments };
|
|
310
|
+
}
|
|
311
|
+
// src/components/spinner.ts
|
|
312
|
+
var SPINNERS = {
|
|
313
|
+
dots: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
|
314
|
+
lines: ["-", "\\", "|", "/"],
|
|
315
|
+
arrows: ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"],
|
|
316
|
+
circle: ["◐", "◓", "◑", "◒"],
|
|
317
|
+
square: ["▖", "▘", "▝", "▗"]
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
class Spinner {
|
|
321
|
+
frames;
|
|
322
|
+
interval;
|
|
323
|
+
startTime;
|
|
324
|
+
constructor(options) {
|
|
325
|
+
this.frames = options.frames;
|
|
326
|
+
this.interval = options.interval ?? 80;
|
|
327
|
+
this.startTime = Date.now();
|
|
328
|
+
}
|
|
329
|
+
getFrame() {
|
|
330
|
+
const now = Date.now();
|
|
331
|
+
const elapsed = now - this.startTime;
|
|
332
|
+
const index = Math.floor(elapsed / this.interval) % this.frames.length;
|
|
333
|
+
return this.frames[index];
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// src/presets/ascii.ts
|
|
337
|
+
function getDragon(startColor = "#EF4444", endColor = "#F59E0B") {
|
|
338
|
+
const rawDragon = [
|
|
339
|
+
" ^ ^",
|
|
340
|
+
" / \\ //\\",
|
|
341
|
+
" |\\___/| / \\// .\\",
|
|
342
|
+
" /O O \\__ / // | \\ \\",
|
|
343
|
+
"/ / \\_/_/ // | \\ \\",
|
|
344
|
+
"@___@' \\_// // | \\ \\ ",
|
|
345
|
+
" | \\_// // | \\ \\ ",
|
|
346
|
+
" | \\/// | \\ \\ ",
|
|
347
|
+
" _|_ / ) // | \\ _\\",
|
|
348
|
+
" '/,_ _ _/ ( ; -. | _ _\\.-~ .-~~~^-.",
|
|
349
|
+
" ,-{ _ `-.|.-~-. .~ `.",
|
|
350
|
+
" '/\\ / ~-. _ .-~ .-~^-. \\",
|
|
351
|
+
" `. { } / \\ \\",
|
|
352
|
+
" .----~-\\. \\-' .~ \\ `. \\^-.",
|
|
353
|
+
" ///.----..> c \\ _ -~ `. ^-` ^-_",
|
|
354
|
+
" ///-._ _ _ _ _ _ _}^ - - - - ~ ~--, .-~",
|
|
355
|
+
" /.-'"
|
|
356
|
+
];
|
|
357
|
+
return rawDragon.map((text, i) => {
|
|
358
|
+
const factor = rawDragon.length <= 1 ? 0 : i / (rawDragon.length - 1);
|
|
359
|
+
const colorStyle = interpolateColor(startColor, endColor, factor);
|
|
360
|
+
return { segments: [{ text, style: { color: colorStyle } }] };
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
export {
|
|
364
|
+
rgbToAnsi,
|
|
365
|
+
resolveStyle,
|
|
366
|
+
resolveModifiersToAnsi,
|
|
367
|
+
resolveColorToAnsi,
|
|
368
|
+
printColumns,
|
|
369
|
+
padLine,
|
|
370
|
+
mergeStyles,
|
|
371
|
+
mergeColumns,
|
|
372
|
+
interpolateHex,
|
|
373
|
+
interpolateColor,
|
|
374
|
+
hexToRgb,
|
|
375
|
+
getLineLength,
|
|
376
|
+
getGradientColor,
|
|
377
|
+
getDragon,
|
|
378
|
+
createProgressBar,
|
|
379
|
+
computeMaxWidth,
|
|
380
|
+
colorToHex,
|
|
381
|
+
Spinner,
|
|
382
|
+
SPINNERS,
|
|
383
|
+
RESET,
|
|
384
|
+
Printer
|
|
385
|
+
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var A="\x1B",T=`${A}[0m`,a={black:"#000000",red:"#EF4444",green:"#10B981",yellow:"#F59E0B",blue:"#3B82F6",magenta:"#EC4899",cyan:"#06B6D4",white:"#FFFFFF",gray:"#6B7280",grey:"#6B7280"},i={default:"0",bold:"1",dim:"2",italic:"3",underline:"4",inverse:"7",hidden:"8",strikethrough:"9"};function W(q){if(q.startsWith("#"))return q;let z=a[q.toLowerCase()];if(!z)return"#FFFFFF";return z}function E(q){let z=q.replace(/^#/,"");if(z.length!==6)return{r:255,g:255,b:255};return{r:parseInt(z.substring(0,2),16),g:parseInt(z.substring(2,4),16),b:parseInt(z.substring(4,6),16)}}function s(q,z,J){return`${A}[38;2;${q};${z};${J}m`}function P(q){let z=W(q),{r:J,g:K,b:Q}=E(z);return s(J,K,Q)}function w(q){if(!q||q.length===0)return"";return q.map((z)=>{let J=i[z];return J?`${A}[${J}m`:""}).join("")}function R(q){let z=Math.max(0,Math.min(255,Math.round(q))).toString(16);return z.length===1?"0"+z:z}function r(q,z,J){let K=Math.max(0,Math.min(1,J)),Q=E(q),U=E(z),Y=Q.r+K*(U.r-Q.r),V=Q.g+K*(U.g-Q.g),X=Q.b+K*(U.b-Q.b);return`#${R(Y)}${R(V)}${R(X)}`}function O(q,z,J){return r(W(q),W(z),J)}function I(q,z){if(q.length===0)return"";if(q.length===1)return P(q[0]);let J=Math.max(0,Math.min(1,z)),K=1/(q.length-1),Q=Math.min(Math.floor(J/K),q.length-2),U=(J-Q*K)/K,Y=q[Q],V=q[Q+1],X=O(Y,V,U);return P(X)}function k(q,z){if(!q&&!z)return{};if(!q)return z??{};if(!z)return q;return{modifiers:Array.from(new Set([...q.modifiers??[],...z.modifiers??[]])),color:z.color??q.color}}function C(q,z=0){if(!q)return"";let J="";if(q.modifiers)J+=w(q.modifiers);if(q.color)if(Array.isArray(q.color)){let K=I(q.color,z);J+=K}else J+=P(q.color);return J}function f(q){return q.segments.reduce((z,J)=>z+J.text.length,0)}function u(q){return q.length>0?Math.max(...q.map(f)):0}function S(q,z,J){let K=f(q);if(K<z)return{segments:[...q.segments,{text:" ".repeat(z-K),style:J}]};return q}var m="\x1B";class v{linesRendered=0;isLive;data;constructor(q={}){this.isLive=q.live??!1,this.data=q.data}getClearSequence(){if(!this.isLive||this.linesRendered===0)return"";return`${m}[1A${m}[2K\r`.repeat(this.linesRendered)}clear(){if(this.linesRendered>0)process.stdout.write(this.getClearSequence()),this.linesRendered=0}print(q){if(q)this.data=q;if(!this.data)return;let z=this.getClearSequence(),J=this.data.lines,K=this.data.style??{};J.forEach((Q,U)=>{z+=this.renderLine(Q,U,J.length,K),z+=`
|
|
2
|
+
`}),process.stdout.write(z),this.linesRendered=J.length}resolveBlockColorForLine(q,z,J){if(!q.color)return;if(Array.isArray(q.color)){if(J<=1)return q.color[0];let K=q.color,Q=z/(J-1),U=Math.max(0,Math.min(1,Q)),Y=1/(K.length-1),V=Math.min(Math.floor(U/Y),K.length-2),X=(U-V*Y)/Y,Z=K[V],$=K[V+1];return O(Z,$,X)}return q.color}renderLine(q,z,J,K){let Q=this.resolveBlockColorForLine(K,z,J),U={modifiers:K.modifiers,color:Q},Y=k(U,q.style),V=q.segments.reduce(($,_)=>$+_.text.length,0),X=0,Z="";return q.segments.forEach(($)=>{let _=k(Y,$.style);if(Array.isArray(_.color)){let j=_.color,G=$.text,D=_.color===Y.color,B=w(_.modifiers);for(let N=0;N<G.length;N++){let M=0;if(D&&V>1)M=(X+N)/(V-1);else if(!D&&G.length>1)M=N/(G.length-1);let H=I(j,M);Z+=`${B}${H}${G[N]}`}Z+=T}else{let j=C(_);Z+=`${j}${$.text}${T}`}X+=$.text.length}),Z}}function n(q,z=" ",J,K){if(q.length===0)return[];let Q=Math.max(...q.map((V)=>V.length)),U=q.map((V,X)=>{if(K?.[X]!==void 0)return K[X];return u(V)}),Y=[];for(let V=0;V<Q;V++){let X=[];for(let Z=0;Z<q.length;Z++){let $=q[Z][V]||{segments:[]};if(Z<q.length-1){let _=S($,U[Z],J);X=[...X,..._.segments,{text:z,style:J}]}else X=[...X,...$.segments]}Y.push({segments:X})}return Y}function Kq(q,z={}){let{widths:J,separator:K=" ",printer:Q=new v}=z,U=n(q,K,void 0,J);Q.print({lines:U})}function Uq(q){let{progress:z,width:J=20,style:K,bracketStyle:Q,startStyle:U,endStyle:Y,barStyle:V,fillStyle:X,emptyStyle:Z,percentageStyle:$,startChar:_="[",endChar:j="]",fillChar:G="█",emptyChar:D="░",showPercentage:B=!0,formatPercentage:N}=q,M=Math.max(0,Math.min(1,z)),H=Math.round(M*J),b=J-H,L=Q??K,h=U??L,p=Y??L,x=V??K,y=X??x,g=Z??x,d=$??K,F=[];if(_)F.push({text:_,style:h});if(H>0)F.push({text:G.repeat(H),style:y});if(b>0)F.push({text:D.repeat(b),style:g});if(j)F.push({text:j,style:p});if(B){let c=N?N(M):` ${Math.round(M*100)}%`;F.push({text:c,style:d})}return{segments:F}}var Xq={dots:["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"],lines:["-","\\","|","/"],arrows:["←","↖","↑","↗","→","↘","↓","↙"],circle:["◐","◓","◑","◒"],square:["▖","▘","▝","▗"]};class l{frames;interval;startTime;constructor(q){this.frames=q.frames,this.interval=q.interval??80,this.startTime=Date.now()}getFrame(){let z=Date.now()-this.startTime,J=Math.floor(z/this.interval)%this.frames.length;return this.frames[J]}}function $q(q="#EF4444",z="#F59E0B"){let J=[" ^ ^"," / \\ //\\"," |\\___/| / \\// .\\"," /O O \\__ / // | \\ \\","/ / \\_/_/ // | \\ \\","@___@' \\_// // | \\ \\ "," | \\_// // | \\ \\ "," | \\/// | \\ \\ "," _|_ / ) // | \\ _\\"," '/,_ _ _/ ( ; -. | _ _\\.-~ .-~~~^-."," ,-{ _ `-.|.-~-. .~ `."," '/\\ / ~-. _ .-~ .-~^-. \\"," `. { } / \\ \\"," .----~-\\. \\-' .~ \\ `. \\^-."," ///.----..> c \\ _ -~ `. ^-` ^-_"," ///-._ _ _ _ _ _ _}^ - - - - ~ ~--, .-~"," /.-'"];return J.map((K,Q)=>{let U=J.length<=1?0:Q/(J.length-1),Y=O(q,z,U);return{segments:[{text:K,style:{color:Y}}]}})}export{s as rgbToAnsi,C as resolveStyle,w as resolveModifiersToAnsi,P as resolveColorToAnsi,Kq as printColumns,S as padLine,k as mergeStyles,n as mergeColumns,r as interpolateHex,O as interpolateColor,E as hexToRgb,f as getLineLength,I as getGradientColor,$q as getDragon,Uq as createProgressBar,u as computeMaxWidth,W as colorToHex,l as Spinner,Xq as SPINNERS,T as RESET,v as Printer};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heinrichb/console-toolkit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "A versatile TypeScript utility library for enhanced console logging, formatting, and layout management.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
16
|
"demo": "bun run scripts/run-demo.ts",
|
|
17
|
-
"test": "bun
|
|
18
|
-
"test:coverage": "bun
|
|
19
|
-
"build": "bun
|
|
17
|
+
"test": "bun test src",
|
|
18
|
+
"test:coverage": "bun test src --coverage --coverage-threshold=100",
|
|
19
|
+
"build": "bun run scripts/build.ts",
|
|
20
|
+
"publish": "bun run build && bun publish --tag latest",
|
|
21
|
+
"typecheck": "bun x tsc --noEmit",
|
|
20
22
|
"format:check": "prettier --check \"src/**/*.{ts,json,md}\"",
|
|
21
23
|
"format:write": "prettier --write \"src/**/*.{ts,json,md}\"",
|
|
22
24
|
"lint": "eslint \"src/**/*.ts\"",
|
|
@@ -37,7 +39,6 @@
|
|
|
37
39
|
"eslint": "^10.0.2",
|
|
38
40
|
"eslint-config-prettier": "^10.1.8",
|
|
39
41
|
"globals": "^17.3.0",
|
|
40
|
-
"jiti": "^2.6.1",
|
|
41
42
|
"prettier": "^3.8.1",
|
|
42
43
|
"typescript": "^5.9.3",
|
|
43
44
|
"typescript-eslint": "^8.56.1"
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Creates a PrintLine representing a progress bar.
|
|
3
|
-
*/
|
|
4
|
-
export function createProgressBar(options) {
|
|
5
|
-
const { progress, width = 20, style, bracketStyle, startStyle, endStyle, barStyle, fillStyle, emptyStyle, percentageStyle, startChar = "[", endChar = "]", fillChar = "█", emptyChar = "░", showPercentage = true, formatPercentage } = options;
|
|
6
|
-
// Clamp progress
|
|
7
|
-
const p = Math.max(0, Math.min(1, progress));
|
|
8
|
-
// Calculate filled width
|
|
9
|
-
const filledWidth = Math.round(p * width);
|
|
10
|
-
const emptyWidth = width - filledWidth;
|
|
11
|
-
// Resolve styles (Defaults)
|
|
12
|
-
const resolvedBracketStyle = bracketStyle ?? style;
|
|
13
|
-
const resolvedStartStyle = startStyle ?? resolvedBracketStyle;
|
|
14
|
-
const resolvedEndStyle = endStyle ?? resolvedBracketStyle;
|
|
15
|
-
const resolvedBarStyle = barStyle ?? style;
|
|
16
|
-
const resolvedFillStyle = fillStyle ?? resolvedBarStyle;
|
|
17
|
-
const resolvedEmptyStyle = emptyStyle ?? resolvedBarStyle;
|
|
18
|
-
const resolvedPercentageStyle = percentageStyle ?? style;
|
|
19
|
-
const segments = [];
|
|
20
|
-
// Start Bracket
|
|
21
|
-
if (startChar) {
|
|
22
|
-
segments.push({ text: startChar, style: resolvedStartStyle });
|
|
23
|
-
}
|
|
24
|
-
// Filled Part
|
|
25
|
-
if (filledWidth > 0) {
|
|
26
|
-
segments.push({ text: fillChar.repeat(filledWidth), style: resolvedFillStyle });
|
|
27
|
-
}
|
|
28
|
-
// Empty Part
|
|
29
|
-
if (emptyWidth > 0) {
|
|
30
|
-
segments.push({ text: emptyChar.repeat(emptyWidth), style: resolvedEmptyStyle });
|
|
31
|
-
}
|
|
32
|
-
// End Bracket
|
|
33
|
-
if (endChar) {
|
|
34
|
-
segments.push({ text: endChar, style: resolvedEndStyle });
|
|
35
|
-
}
|
|
36
|
-
// Percentage
|
|
37
|
-
if (showPercentage) {
|
|
38
|
-
const percentageText = formatPercentage ? formatPercentage(p) : ` ${Math.round(p * 100)}%`;
|
|
39
|
-
segments.push({ text: percentageText, style: resolvedPercentageStyle });
|
|
40
|
-
}
|
|
41
|
-
return { segments };
|
|
42
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { expect, test, describe } from "bun:test";
|
|
2
|
-
import { createProgressBar } from "./progress";
|
|
3
|
-
function getText(line) {
|
|
4
|
-
return line.segments.map((s) => s.text).join("");
|
|
5
|
-
}
|
|
6
|
-
describe("createProgressBar", () => {
|
|
7
|
-
test("generates a default progress bar at 0%", () => {
|
|
8
|
-
const line = createProgressBar({ progress: 0 });
|
|
9
|
-
const text = getText(line);
|
|
10
|
-
// Default format: [░░░░░░░░░░░░░░░░░░░░] 0%
|
|
11
|
-
expect(text).toContain("[");
|
|
12
|
-
expect(text).toContain("]");
|
|
13
|
-
expect(text).toContain("0%");
|
|
14
|
-
expect(text).toContain("░");
|
|
15
|
-
});
|
|
16
|
-
test("generates a default progress bar at 50%", () => {
|
|
17
|
-
const line = createProgressBar({ progress: 0.5 });
|
|
18
|
-
const text = getText(line);
|
|
19
|
-
expect(text).toContain("50%");
|
|
20
|
-
expect(text).toContain("█");
|
|
21
|
-
expect(text).toContain("░");
|
|
22
|
-
});
|
|
23
|
-
test("generates a default progress bar at 100%", () => {
|
|
24
|
-
const line = createProgressBar({ progress: 1.0 });
|
|
25
|
-
const text = getText(line);
|
|
26
|
-
expect(text).toContain("100%");
|
|
27
|
-
// Should not contain empty char
|
|
28
|
-
// But wait, width default 20. 100% means 20 filled. 0 empty.
|
|
29
|
-
expect(text).not.toContain("░");
|
|
30
|
-
});
|
|
31
|
-
test("allows custom width", () => {
|
|
32
|
-
const width = 10;
|
|
33
|
-
const line = createProgressBar({ progress: 0.5, width });
|
|
34
|
-
const segments = line.segments;
|
|
35
|
-
const filled = segments.find((s) => s.text.includes("█"));
|
|
36
|
-
const empty = segments.find((s) => s.text.includes("░"));
|
|
37
|
-
// 50% of 10 is 5.
|
|
38
|
-
expect(filled?.text.length).toBe(5);
|
|
39
|
-
expect(empty?.text.length).toBe(5);
|
|
40
|
-
});
|
|
41
|
-
test("applies styles correctly", () => {
|
|
42
|
-
const line = createProgressBar({
|
|
43
|
-
progress: 0.5,
|
|
44
|
-
style: { color: "blue" },
|
|
45
|
-
bracketStyle: { color: "red" },
|
|
46
|
-
barStyle: { color: "green" },
|
|
47
|
-
percentageStyle: { color: "yellow" }
|
|
48
|
-
});
|
|
49
|
-
const start = line.segments.find((s) => s.text === "[");
|
|
50
|
-
const filled = line.segments.find((s) => s.text.includes("█"));
|
|
51
|
-
const end = line.segments.find((s) => s.text === "]");
|
|
52
|
-
const percentage = line.segments.find((s) => s.text.includes("%"));
|
|
53
|
-
expect(start?.style).toEqual({ color: "red" });
|
|
54
|
-
expect(filled?.style).toEqual({ color: "green" });
|
|
55
|
-
expect(end?.style).toEqual({ color: "red" });
|
|
56
|
-
expect(percentage?.style).toEqual({ color: "yellow" });
|
|
57
|
-
});
|
|
58
|
-
test("cascades styles (general style -> specific)", () => {
|
|
59
|
-
const line = createProgressBar({
|
|
60
|
-
progress: 0.5,
|
|
61
|
-
style: { color: "blue" }
|
|
62
|
-
});
|
|
63
|
-
line.segments.forEach((s) => {
|
|
64
|
-
if (s.text.trim().length > 0) {
|
|
65
|
-
expect(s.style).toEqual({ color: "blue" });
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
test("allows custom characters", () => {
|
|
70
|
-
const line = createProgressBar({
|
|
71
|
-
progress: 0.5,
|
|
72
|
-
startChar: "<",
|
|
73
|
-
endChar: ">",
|
|
74
|
-
fillChar: "=",
|
|
75
|
-
emptyChar: "-"
|
|
76
|
-
});
|
|
77
|
-
const text = getText(line);
|
|
78
|
-
expect(text).toContain("<");
|
|
79
|
-
expect(text).toContain(">");
|
|
80
|
-
expect(text).toContain("=");
|
|
81
|
-
expect(text).toContain("-");
|
|
82
|
-
});
|
|
83
|
-
test("hides percentage", () => {
|
|
84
|
-
const line = createProgressBar({
|
|
85
|
-
progress: 0.5,
|
|
86
|
-
showPercentage: false
|
|
87
|
-
});
|
|
88
|
-
const text = getText(line);
|
|
89
|
-
expect(text).not.toContain("%");
|
|
90
|
-
});
|
|
91
|
-
test("formats percentage custom", () => {
|
|
92
|
-
const line = createProgressBar({
|
|
93
|
-
progress: 0.5,
|
|
94
|
-
formatPercentage: (p) => `${p * 10}/10`
|
|
95
|
-
});
|
|
96
|
-
const text = getText(line);
|
|
97
|
-
expect(text).toContain("5/10");
|
|
98
|
-
});
|
|
99
|
-
test("clamping progress", () => {
|
|
100
|
-
const lineLow = createProgressBar({ progress: -0.1 });
|
|
101
|
-
expect(getText(lineLow)).toContain("0%");
|
|
102
|
-
const lineHigh = createProgressBar({ progress: 1.1 });
|
|
103
|
-
expect(getText(lineHigh)).toContain("100%");
|
|
104
|
-
});
|
|
105
|
-
});
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Common spinner frame presets.
|
|
3
|
-
*/
|
|
4
|
-
export const SPINNERS = {
|
|
5
|
-
dots: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
|
6
|
-
lines: ["-", "\\", "|", "/"],
|
|
7
|
-
arrows: ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"],
|
|
8
|
-
circle: ["◐", "◓", "◑", "◒"],
|
|
9
|
-
square: ["▖", "▘", "▝", "▗"]
|
|
10
|
-
};
|
|
11
|
-
/**
|
|
12
|
-
* A stateful spinner that calculates the current frame based on elapsed time.
|
|
13
|
-
* Designed to be used within a render loop (e.g. Printer.print loop).
|
|
14
|
-
*/
|
|
15
|
-
export class Spinner {
|
|
16
|
-
frames;
|
|
17
|
-
interval;
|
|
18
|
-
startTime;
|
|
19
|
-
constructor(options) {
|
|
20
|
-
this.frames = options.frames;
|
|
21
|
-
this.interval = options.interval ?? 80;
|
|
22
|
-
this.startTime = Date.now();
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Returns the current frame based on the elapsed time.
|
|
26
|
-
*/
|
|
27
|
-
getFrame() {
|
|
28
|
-
const now = Date.now();
|
|
29
|
-
const elapsed = now - this.startTime;
|
|
30
|
-
const index = Math.floor(elapsed / this.interval) % this.frames.length;
|
|
31
|
-
return this.frames[index];
|
|
32
|
-
}
|
|
33
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|