@heinrichb/console-toolkit 1.0.3 → 1.0.5
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 +162 -2
- package/dist/components/progress.d.ts +75 -0
- package/dist/components/progress.js +43 -0
- package/dist/components/progress.test.d.ts +1 -0
- package/dist/components/progress.test.js +101 -0
- package/dist/components/spinner.d.ts +36 -0
- package/dist/components/spinner.js +33 -0
- package/dist/components/spinner.test.d.ts +1 -0
- package/dist/components/spinner.test.js +53 -0
- package/dist/core/layout.d.ts +25 -0
- package/dist/core/layout.js +57 -0
- package/dist/core/layout.test.d.ts +1 -0
- package/dist/core/layout.test.js +41 -0
- package/dist/core/printer.d.ts +20 -0
- package/dist/core/printer.js +41 -0
- package/dist/core/printer.test.d.ts +1 -0
- package/dist/core/printer.test.js +36 -0
- package/dist/core/style.d.ts +51 -0
- package/dist/core/style.js +127 -0
- package/dist/core/style.test.d.ts +1 -0
- package/dist/core/style.test.js +53 -0
- package/dist/core/types.d.ts +42 -0
- package/dist/core/types.js +4 -0
- package/dist/core/utils.d.ts +25 -0
- package/dist/core/utils.js +39 -0
- package/dist/core/utils.test.d.ts +1 -0
- package/dist/core/utils.test.js +25 -0
- package/dist/demo.d.ts +0 -4
- package/dist/demo.js +119 -15
- package/dist/index.d.ts +8 -99
- package/dist/index.js +14 -234
- package/dist/index.test.js +26 -6
- package/dist/presets/ascii.d.ts +5 -0
- package/dist/presets/ascii.js +33 -0
- package/dist/presets/ascii.test.d.ts +1 -0
- package/dist/presets/ascii.test.js +10 -0
- package/dist/spinner.d.ts +36 -0
- package/dist/spinner.js +33 -0
- package/dist/spinner.test.d.ts +1 -0
- package/dist/spinner.test.js +53 -0
- package/package.json +8 -6
package/dist/index.d.ts
CHANGED
|
@@ -1,99 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
export
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
* It can also be a raw ANSI string (though discouraged) for backward compatibility or special cases.
|
|
10
|
-
*/
|
|
11
|
-
export type Style = StandardColor | StyleModifier | HexColor | string;
|
|
12
|
-
export interface StyledSegment {
|
|
13
|
-
text: string;
|
|
14
|
-
/** Style or array of styles to apply to the text */
|
|
15
|
-
style: Style | Style[];
|
|
16
|
-
}
|
|
17
|
-
export interface StyledLine {
|
|
18
|
-
segments: StyledSegment[];
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Configuration for the Printer engine.
|
|
22
|
-
*/
|
|
23
|
-
export interface PrinterOptions {
|
|
24
|
-
/** If true, the printer will overwrite previous lines instead of appending new ones. */
|
|
25
|
-
interactive?: boolean;
|
|
26
|
-
/** The default style to apply to padding or separators. */
|
|
27
|
-
defaultStyle?: Style | Style[];
|
|
28
|
-
}
|
|
29
|
-
export declare const RESET = "\u001B[0m";
|
|
30
|
-
/**
|
|
31
|
-
* Converts a hex color string to an RGB object.
|
|
32
|
-
* @param hex - Hex color in the form "#RRGGBB".
|
|
33
|
-
*/
|
|
34
|
-
export declare function hexToRgb(hex: string): {
|
|
35
|
-
r: number;
|
|
36
|
-
g: number;
|
|
37
|
-
b: number;
|
|
38
|
-
};
|
|
39
|
-
/**
|
|
40
|
-
* Converts RGB to a 24-bit ANSI foreground color escape sequence.
|
|
41
|
-
*/
|
|
42
|
-
export declare function rgbToAnsi(r: number, g: number, b: number): string;
|
|
43
|
-
/**
|
|
44
|
-
* Converts a hex color string directly to an ANSI escape sequence.
|
|
45
|
-
*/
|
|
46
|
-
export declare function hexToAnsi(hex: string): string;
|
|
47
|
-
/**
|
|
48
|
-
* Interpolates between two hex colors based on a factor (0 to 1) and returns a Hex color string.
|
|
49
|
-
*/
|
|
50
|
-
export declare function interpolateColor(color1: string, color2: string, factor: number): HexColor;
|
|
51
|
-
/**
|
|
52
|
-
* Resolves a single Style or array of Styles into an ANSI escape sequence.
|
|
53
|
-
*/
|
|
54
|
-
export declare function resolveStyle(style: Style | Style[]): string;
|
|
55
|
-
/**
|
|
56
|
-
* Gets the plain text length of a StyledLine.
|
|
57
|
-
*/
|
|
58
|
-
export declare function getLineLength(line: StyledLine): number;
|
|
59
|
-
/**
|
|
60
|
-
* Computes the maximum width among an array of StyledLines.
|
|
61
|
-
*/
|
|
62
|
-
export declare function computeMaxWidth(lines: StyledLine[]): number;
|
|
63
|
-
/**
|
|
64
|
-
* Pads a StyledLine to a target width by adding an empty segment at the end.
|
|
65
|
-
*/
|
|
66
|
-
export declare function padLine(line: StyledLine, targetWidth: number, padStyle: Style | Style[]): StyledLine;
|
|
67
|
-
/**
|
|
68
|
-
* Handles rendering StyledLines to the terminal with support for interactive overwriting.
|
|
69
|
-
*/
|
|
70
|
-
export declare class Printer {
|
|
71
|
-
private linesRendered;
|
|
72
|
-
private isInteractive;
|
|
73
|
-
constructor(options?: PrinterOptions);
|
|
74
|
-
/**
|
|
75
|
-
* Generates the clear sequence to move cursor and clear previously rendered lines.
|
|
76
|
-
*/
|
|
77
|
-
private getClearSequence;
|
|
78
|
-
/**
|
|
79
|
-
* Renders an array of StyledLines to the standard output.
|
|
80
|
-
*/
|
|
81
|
-
print(lines: StyledLine[]): void;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Merges two columns of StyledLines into a single layout.
|
|
85
|
-
*/
|
|
86
|
-
export declare function mergeColumns(leftColumn: StyledLine[], rightColumn: StyledLine[], leftWidth: number, separator: string, defaultStyle: Style | Style[]): StyledLine[];
|
|
87
|
-
/**
|
|
88
|
-
* Prints two columns of styled content to the console.
|
|
89
|
-
*/
|
|
90
|
-
export declare function printDualColumn(left: StyledLine[], right: StyledLine[], options?: {
|
|
91
|
-
leftWidth?: number;
|
|
92
|
-
separator?: string;
|
|
93
|
-
printer?: Printer;
|
|
94
|
-
}): void;
|
|
95
|
-
/**
|
|
96
|
-
* Returns the classic Dragon ASCII art as StyledLines with a vertical color gradient.
|
|
97
|
-
*/
|
|
98
|
-
export declare function getDragonLines(startColor?: string, endColor?: string): StyledLine[];
|
|
99
|
-
export * from "./progress";
|
|
1
|
+
export * from "./core/types";
|
|
2
|
+
export * from "./core/style";
|
|
3
|
+
export * from "./core/utils";
|
|
4
|
+
export * from "./core/printer";
|
|
5
|
+
export * from "./core/layout";
|
|
6
|
+
export * from "./components/progress";
|
|
7
|
+
export * from "./components/spinner";
|
|
8
|
+
export * from "./presets/ascii";
|
package/dist/index.js
CHANGED
|
@@ -1,234 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (h.length !== 6)
|
|
16
|
-
throw new Error("Invalid hex color.");
|
|
17
|
-
return {
|
|
18
|
-
r: parseInt(h.substring(0, 2), 16),
|
|
19
|
-
g: parseInt(h.substring(2, 4), 16),
|
|
20
|
-
b: parseInt(h.substring(4, 6), 16)
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Converts RGB to a 24-bit ANSI foreground color escape sequence.
|
|
25
|
-
*/
|
|
26
|
-
export function rgbToAnsi(r, g, b) {
|
|
27
|
-
return `${ESC}[38;2;${r};${g};${b}m`;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Converts a hex color string directly to an ANSI escape sequence.
|
|
31
|
-
*/
|
|
32
|
-
export function hexToAnsi(hex) {
|
|
33
|
-
const { r, g, b } = hexToRgb(hex);
|
|
34
|
-
return rgbToAnsi(r, g, b);
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Converts a single RGB component to a 2-digit hex string.
|
|
38
|
-
*/
|
|
39
|
-
function toHex(c) {
|
|
40
|
-
const hex = Math.max(0, Math.min(255, Math.round(c))).toString(16);
|
|
41
|
-
return hex.length === 1 ? "0" + hex : hex;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Interpolates between two hex colors based on a factor (0 to 1) and returns a Hex color string.
|
|
45
|
-
*/
|
|
46
|
-
export function interpolateColor(color1, color2, factor) {
|
|
47
|
-
const f = Math.max(0, Math.min(1, factor));
|
|
48
|
-
const c1 = hexToRgb(color1);
|
|
49
|
-
const c2 = hexToRgb(color2);
|
|
50
|
-
const r = c1.r + f * (c2.r - c1.r);
|
|
51
|
-
const g = c1.g + f * (c2.g - c1.g);
|
|
52
|
-
const b = c1.b + f * (c2.b - c1.b);
|
|
53
|
-
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
54
|
-
}
|
|
55
|
-
// -----------------
|
|
56
|
-
// Style Resolution
|
|
57
|
-
// -----------------
|
|
58
|
-
const STYLE_CODES = {
|
|
59
|
-
reset: "0",
|
|
60
|
-
default: "0",
|
|
61
|
-
bold: "1",
|
|
62
|
-
dim: "2",
|
|
63
|
-
italic: "3",
|
|
64
|
-
underline: "4",
|
|
65
|
-
inverse: "7",
|
|
66
|
-
hidden: "8",
|
|
67
|
-
strikethrough: "9",
|
|
68
|
-
black: "30",
|
|
69
|
-
red: "31",
|
|
70
|
-
green: "32",
|
|
71
|
-
yellow: "33",
|
|
72
|
-
blue: "34",
|
|
73
|
-
magenta: "35",
|
|
74
|
-
cyan: "36",
|
|
75
|
-
white: "37",
|
|
76
|
-
gray: "90",
|
|
77
|
-
grey: "90"
|
|
78
|
-
};
|
|
79
|
-
/**
|
|
80
|
-
* Resolves a single Style or array of Styles into an ANSI escape sequence.
|
|
81
|
-
*/
|
|
82
|
-
export function resolveStyle(style) {
|
|
83
|
-
if (Array.isArray(style)) {
|
|
84
|
-
return style.map(resolveStyle).join("");
|
|
85
|
-
}
|
|
86
|
-
if (typeof style !== "string") {
|
|
87
|
-
return "";
|
|
88
|
-
}
|
|
89
|
-
// Check for hex color
|
|
90
|
-
if (style.startsWith("#")) {
|
|
91
|
-
try {
|
|
92
|
-
return hexToAnsi(style);
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
return ""; // Invalid hex, ignore
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
// Check for standard styles/colors
|
|
99
|
-
const code = STYLE_CODES[style.toLowerCase()];
|
|
100
|
-
if (code) {
|
|
101
|
-
return `${ESC}[${code}m`;
|
|
102
|
-
}
|
|
103
|
-
// Fallback: return as raw string if it looks like ANSI or just text
|
|
104
|
-
// Ideally we would validate ANSI here, but for flexibility we return it.
|
|
105
|
-
return style;
|
|
106
|
-
}
|
|
107
|
-
// -----------------
|
|
108
|
-
// Line Manipulation Helpers
|
|
109
|
-
// -----------------
|
|
110
|
-
/**
|
|
111
|
-
* Gets the plain text length of a StyledLine.
|
|
112
|
-
*/
|
|
113
|
-
export function getLineLength(line) {
|
|
114
|
-
return line.segments.reduce((acc, seg) => acc + seg.text.length, 0);
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Computes the maximum width among an array of StyledLines.
|
|
118
|
-
*/
|
|
119
|
-
export function computeMaxWidth(lines) {
|
|
120
|
-
return lines.length > 0 ? Math.max(...lines.map(getLineLength)) : 0;
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Pads a StyledLine to a target width by adding an empty segment at the end.
|
|
124
|
-
*/
|
|
125
|
-
export function padLine(line, targetWidth, padStyle) {
|
|
126
|
-
const currentLength = getLineLength(line);
|
|
127
|
-
if (currentLength < targetWidth) {
|
|
128
|
-
return {
|
|
129
|
-
segments: [...line.segments, { text: " ".repeat(targetWidth - currentLength), style: padStyle }]
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
return line;
|
|
133
|
-
}
|
|
134
|
-
// -----------------
|
|
135
|
-
// Printer Class
|
|
136
|
-
// -----------------
|
|
137
|
-
/**
|
|
138
|
-
* Handles rendering StyledLines to the terminal with support for interactive overwriting.
|
|
139
|
-
*/
|
|
140
|
-
export class Printer {
|
|
141
|
-
linesRendered = 0;
|
|
142
|
-
isInteractive;
|
|
143
|
-
constructor(options = {}) {
|
|
144
|
-
this.isInteractive = options.interactive ?? false;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Generates the clear sequence to move cursor and clear previously rendered lines.
|
|
148
|
-
*/
|
|
149
|
-
getClearSequence() {
|
|
150
|
-
if (!this.isInteractive || this.linesRendered === 0)
|
|
151
|
-
return "";
|
|
152
|
-
return `${ESC}[1A${ESC}[2K\r`.repeat(this.linesRendered);
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Renders an array of StyledLines to the standard output.
|
|
156
|
-
*/
|
|
157
|
-
print(lines) {
|
|
158
|
-
let output = this.getClearSequence();
|
|
159
|
-
lines.forEach((line) => {
|
|
160
|
-
line.segments.forEach((seg) => {
|
|
161
|
-
const ansiStyle = resolveStyle(seg.style);
|
|
162
|
-
output += `${ansiStyle}${seg.text}${RESET}`;
|
|
163
|
-
});
|
|
164
|
-
output += "\n";
|
|
165
|
-
});
|
|
166
|
-
process.stdout.write(output);
|
|
167
|
-
this.linesRendered = lines.length;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
// -----------------
|
|
171
|
-
// Core Layout & Printing
|
|
172
|
-
// -----------------
|
|
173
|
-
const defaultPrinter = new Printer();
|
|
174
|
-
/**
|
|
175
|
-
* Merges two columns of StyledLines into a single layout.
|
|
176
|
-
*/
|
|
177
|
-
export function mergeColumns(leftColumn, rightColumn, leftWidth, separator, defaultStyle) {
|
|
178
|
-
const maxLines = Math.max(leftColumn.length, rightColumn.length);
|
|
179
|
-
const output = [];
|
|
180
|
-
for (let i = 0; i < maxLines; i++) {
|
|
181
|
-
const left = padLine(leftColumn[i] || { segments: [] }, leftWidth, defaultStyle);
|
|
182
|
-
const right = rightColumn[i] || { segments: [] };
|
|
183
|
-
output.push({
|
|
184
|
-
segments: [...left.segments, { text: separator, style: defaultStyle }, ...right.segments]
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
return output;
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* Prints two columns of styled content to the console.
|
|
191
|
-
*/
|
|
192
|
-
export function printDualColumn(left, right, options = {}) {
|
|
193
|
-
const { leftWidth, separator = " ", printer = defaultPrinter } = options;
|
|
194
|
-
const defaultStyle = RESET;
|
|
195
|
-
const finalLeftWidth = leftWidth ?? computeMaxWidth(left);
|
|
196
|
-
const mergedLines = mergeColumns(left, right, finalLeftWidth, separator, defaultStyle);
|
|
197
|
-
printer.print(mergedLines);
|
|
198
|
-
}
|
|
199
|
-
// -----------------
|
|
200
|
-
// Presets
|
|
201
|
-
// -----------------
|
|
202
|
-
/**
|
|
203
|
-
* Returns the classic Dragon ASCII art as StyledLines with a vertical color gradient.
|
|
204
|
-
*/
|
|
205
|
-
export function getDragonLines(startColor = "#EF4444", endColor = "#F59E0B") {
|
|
206
|
-
const rawDragon = [
|
|
207
|
-
" ^ ^",
|
|
208
|
-
" / \\ //\\",
|
|
209
|
-
" |\\___/| / \\// .\\",
|
|
210
|
-
" /O O \\__ / // | \\ \\",
|
|
211
|
-
"/ / \\_/_/ // | \\ \\",
|
|
212
|
-
"@___@' \\_// // | \\ \\ ",
|
|
213
|
-
" | \\_// // | \\ \\ ",
|
|
214
|
-
" | \\/// | \\ \\ ",
|
|
215
|
-
" _|_ / ) // | \\ _\\",
|
|
216
|
-
" '/,_ _ _/ ( ; -. | _ _\\.-~ .-~~~^-.",
|
|
217
|
-
" ,-{ _ `-.|.-~-. .~ `.",
|
|
218
|
-
" '/\\ / ~-. _ .-~ .-~^-. \\",
|
|
219
|
-
" `. { } / \\ \\",
|
|
220
|
-
" .----~-\\. \\-' .~ \\ `. \\^-.",
|
|
221
|
-
" ///.----..> c \\ _ -~ `. ^-` ^-_",
|
|
222
|
-
" ///-._ _ _ _ _ _ _}^ - - - - ~ ~--, .-~",
|
|
223
|
-
" /.-'"
|
|
224
|
-
];
|
|
225
|
-
return rawDragon.map((text, i) => {
|
|
226
|
-
const factor = rawDragon.length <= 1 ? 0 : i / (rawDragon.length - 1);
|
|
227
|
-
const colorStyle = interpolateColor(startColor, endColor, factor);
|
|
228
|
-
return { segments: [{ text, style: colorStyle }] };
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
// -----------------
|
|
232
|
-
// Progress Bar
|
|
233
|
-
// -----------------
|
|
234
|
-
export * from "./progress";
|
|
1
|
+
// Export Core Types
|
|
2
|
+
export * from "./core/types";
|
|
3
|
+
// Export Core Utilities
|
|
4
|
+
export * from "./core/style";
|
|
5
|
+
export * from "./core/utils";
|
|
6
|
+
// Export Printer Engine
|
|
7
|
+
export * from "./core/printer";
|
|
8
|
+
// Export Layout Helpers
|
|
9
|
+
export * from "./core/layout";
|
|
10
|
+
// Export Components
|
|
11
|
+
export * from "./components/progress";
|
|
12
|
+
export * from "./components/spinner";
|
|
13
|
+
// Export Presets
|
|
14
|
+
export * from "./presets/ascii";
|
package/dist/index.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect, test, describe, spyOn, afterEach } from "bun:test";
|
|
2
|
-
import { hexToRgb, rgbToAnsi, hexToAnsi, interpolateColor, getLineLength, computeMaxWidth, padLine, Printer,
|
|
2
|
+
import { hexToRgb, rgbToAnsi, hexToAnsi, interpolateColor, getLineLength, computeMaxWidth, padLine, Printer, printColumns, getDragonLines, mergeMultipleColumns, resolveStyle } from "./index";
|
|
3
3
|
const lineA = { segments: [{ text: "Hello", style: [] }] };
|
|
4
4
|
const lineB = { segments: [{ text: "World!!", style: [] }] };
|
|
5
5
|
describe("Color Utilities", () => {
|
|
@@ -37,7 +37,6 @@ describe("Style Resolution", () => {
|
|
|
37
37
|
});
|
|
38
38
|
test("resolveStyle handles modifiers", () => {
|
|
39
39
|
expect(resolveStyle("bold")).toBe("\x1b[1m");
|
|
40
|
-
expect(resolveStyle("reset")).toBe("\x1b[0m");
|
|
41
40
|
});
|
|
42
41
|
test("resolveStyle handles hex colors", () => {
|
|
43
42
|
expect(resolveStyle("#FF0000")).toBe("\x1b[38;2;255;0;0m");
|
|
@@ -45,6 +44,9 @@ describe("Style Resolution", () => {
|
|
|
45
44
|
test("resolveStyle handles arrays of styles", () => {
|
|
46
45
|
expect(resolveStyle(["bold", "red"])).toBe("\x1b[1m\x1b[31m");
|
|
47
46
|
});
|
|
47
|
+
test("resolveStyle handles undefined style", () => {
|
|
48
|
+
expect(resolveStyle(undefined)).toBe("");
|
|
49
|
+
});
|
|
48
50
|
test("resolveStyle passes through raw strings", () => {
|
|
49
51
|
const raw = "\x1b[31m";
|
|
50
52
|
expect(resolveStyle(raw)).toBe(raw);
|
|
@@ -71,8 +73,8 @@ describe("Line Utilities", () => {
|
|
|
71
73
|
expect(getLineLength(ignored)).toBe(7);
|
|
72
74
|
expect(ignored.segments.length).toBe(1);
|
|
73
75
|
});
|
|
74
|
-
test("
|
|
75
|
-
const merged =
|
|
76
|
+
test("mergeMultipleColumns handles asymmetric column lengths", () => {
|
|
77
|
+
const merged = mergeMultipleColumns([[lineA], [lineB, lineB]], " | ", "", [10]);
|
|
76
78
|
expect(merged.length).toBe(2);
|
|
77
79
|
expect(getLineLength(merged[1])).toBe(10 + 3 + 7);
|
|
78
80
|
});
|
|
@@ -110,8 +112,26 @@ describe("Printer & Layout", () => {
|
|
|
110
112
|
const callArg = stdoutSpy.mock.calls[0][0];
|
|
111
113
|
expect(callArg.startsWith(expectedClear)).toBe(true);
|
|
112
114
|
});
|
|
113
|
-
test("
|
|
114
|
-
|
|
115
|
+
test("printColumns executes correctly", () => {
|
|
116
|
+
printColumns([[lineA], [lineB]]);
|
|
117
|
+
expect(stdoutSpy).toHaveBeenCalled();
|
|
118
|
+
const output = stdoutSpy.mock.calls[0][0];
|
|
119
|
+
expect(output).toContain("Hello");
|
|
120
|
+
expect(output).toContain("World!!");
|
|
121
|
+
});
|
|
122
|
+
test("printColumns handles undefined style in segments", () => {
|
|
123
|
+
const lineNoStyle = { segments: [{ text: "NoStyle" }] };
|
|
124
|
+
printColumns([[lineNoStyle]]);
|
|
125
|
+
expect(stdoutSpy).toHaveBeenCalled();
|
|
126
|
+
const output = stdoutSpy.mock.calls[0][0];
|
|
127
|
+
expect(output).toContain("NoStyle");
|
|
128
|
+
});
|
|
129
|
+
test("printColumns handles empty columns", () => {
|
|
130
|
+
printColumns([]);
|
|
131
|
+
expect(stdoutSpy).toHaveBeenCalled(); // Should clear lines if interactive, or do nothing.
|
|
132
|
+
});
|
|
133
|
+
test("printColumns handles 3 columns", () => {
|
|
134
|
+
printColumns([[lineA], [lineA], [lineB]]);
|
|
115
135
|
expect(stdoutSpy).toHaveBeenCalled();
|
|
116
136
|
const output = stdoutSpy.mock.calls[0][0];
|
|
117
137
|
expect(output).toContain("Hello");
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { interpolateColor } from "../core/style";
|
|
2
|
+
// -----------------
|
|
3
|
+
// Presets
|
|
4
|
+
// -----------------
|
|
5
|
+
/**
|
|
6
|
+
* Returns the classic Dragon ASCII art as StyledLines with a vertical color gradient.
|
|
7
|
+
*/
|
|
8
|
+
export function getDragonLines(startColor = "#EF4444", endColor = "#F59E0B") {
|
|
9
|
+
const rawDragon = [
|
|
10
|
+
" ^ ^",
|
|
11
|
+
" / \\ //\\",
|
|
12
|
+
" |\\___/| / \\// .\\",
|
|
13
|
+
" /O O \\__ / // | \\ \\",
|
|
14
|
+
"/ / \\_/_/ // | \\ \\",
|
|
15
|
+
"@___@' \\_// // | \\ \\ ",
|
|
16
|
+
" | \\_// // | \\ \\ ",
|
|
17
|
+
" | \\/// | \\ \\ ",
|
|
18
|
+
" _|_ / ) // | \\ _\\",
|
|
19
|
+
" '/,_ _ _/ ( ; -. | _ _\\.-~ .-~~~^-.",
|
|
20
|
+
" ,-{ _ `-.|.-~-. .~ `.",
|
|
21
|
+
" '/\\ / ~-. _ .-~ .-~^-. \\",
|
|
22
|
+
" `. { } / \\ \\",
|
|
23
|
+
" .----~-\\. \\-' .~ \\ `. \\^-.",
|
|
24
|
+
" ///.----..> c \\ _ -~ `. ^-` ^-_",
|
|
25
|
+
" ///-._ _ _ _ _ _ _}^ - - - - ~ ~--, .-~",
|
|
26
|
+
" /.-'"
|
|
27
|
+
];
|
|
28
|
+
return rawDragon.map((text, i) => {
|
|
29
|
+
const factor = rawDragon.length <= 1 ? 0 : i / (rawDragon.length - 1);
|
|
30
|
+
const colorStyle = interpolateColor(startColor, endColor, factor);
|
|
31
|
+
return { segments: [{ text, style: colorStyle }] };
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { expect, test, describe } from "bun:test";
|
|
2
|
+
import { getDragonLines } from "./ascii";
|
|
3
|
+
describe("Presets", () => {
|
|
4
|
+
test("getDragonLines returns valid array", () => {
|
|
5
|
+
const lines = getDragonLines();
|
|
6
|
+
expect(lines.length).toBeGreaterThan(0);
|
|
7
|
+
const firstSegmentStyle = lines[0].segments[0].style;
|
|
8
|
+
expect(firstSegmentStyle.startsWith("#")).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type SpinnerFrames = string[];
|
|
2
|
+
export interface SpinnerOptions {
|
|
3
|
+
/**
|
|
4
|
+
* The array of frames to cycle through.
|
|
5
|
+
*/
|
|
6
|
+
frames: SpinnerFrames;
|
|
7
|
+
/**
|
|
8
|
+
* The interval in milliseconds between frames.
|
|
9
|
+
* Defaults to 80ms.
|
|
10
|
+
*/
|
|
11
|
+
interval?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Common spinner frame presets.
|
|
15
|
+
*/
|
|
16
|
+
export declare const SPINNERS: {
|
|
17
|
+
dots: string[];
|
|
18
|
+
lines: string[];
|
|
19
|
+
arrows: string[];
|
|
20
|
+
circle: string[];
|
|
21
|
+
square: string[];
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* A stateful spinner that calculates the current frame based on elapsed time.
|
|
25
|
+
* Designed to be used within a render loop (e.g. Printer.print loop).
|
|
26
|
+
*/
|
|
27
|
+
export declare class Spinner {
|
|
28
|
+
private frames;
|
|
29
|
+
private interval;
|
|
30
|
+
private startTime;
|
|
31
|
+
constructor(options: SpinnerOptions);
|
|
32
|
+
/**
|
|
33
|
+
* Returns the current frame based on the elapsed time.
|
|
34
|
+
*/
|
|
35
|
+
getFrame(): string;
|
|
36
|
+
}
|
package/dist/spinner.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { expect, test, describe, spyOn, afterEach, beforeEach } from "bun:test";
|
|
2
|
+
import { Spinner, SPINNERS } from "./spinner";
|
|
3
|
+
describe("Spinner Class", () => {
|
|
4
|
+
let now = 1000;
|
|
5
|
+
// Mock Date.now() to control time
|
|
6
|
+
const dateSpy = spyOn(Date, "now").mockImplementation(() => now);
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
now = 1000;
|
|
9
|
+
dateSpy.mockClear();
|
|
10
|
+
dateSpy.mockImplementation(() => now);
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
dateSpy.mockClear();
|
|
14
|
+
});
|
|
15
|
+
test("initializes with correct defaults", () => {
|
|
16
|
+
const spinner = new Spinner({ frames: ["a", "b"] });
|
|
17
|
+
expect(spinner.getFrame()).toBe("a");
|
|
18
|
+
});
|
|
19
|
+
test("advances frames over time", () => {
|
|
20
|
+
const spinner = new Spinner({ frames: ["a", "b", "c"], interval: 100 });
|
|
21
|
+
// t=0 (1000)
|
|
22
|
+
expect(spinner.getFrame()).toBe("a");
|
|
23
|
+
// t=50 (1050) -> still frame 0
|
|
24
|
+
now = 1050;
|
|
25
|
+
expect(spinner.getFrame()).toBe("a");
|
|
26
|
+
// t=100 (1100) -> frame 1
|
|
27
|
+
now = 1100;
|
|
28
|
+
expect(spinner.getFrame()).toBe("b");
|
|
29
|
+
// t=200 (1200) -> frame 2
|
|
30
|
+
now = 1200;
|
|
31
|
+
expect(spinner.getFrame()).toBe("c");
|
|
32
|
+
// t=300 (1300) -> frame 0 (loop)
|
|
33
|
+
now = 1300;
|
|
34
|
+
expect(spinner.getFrame()).toBe("a");
|
|
35
|
+
});
|
|
36
|
+
test("uses custom interval", () => {
|
|
37
|
+
const spinner = new Spinner({ frames: ["a", "b"], interval: 50 });
|
|
38
|
+
// t=0
|
|
39
|
+
expect(spinner.getFrame()).toBe("a");
|
|
40
|
+
// t=50 -> frame 1
|
|
41
|
+
now = 1050;
|
|
42
|
+
expect(spinner.getFrame()).toBe("b");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe("Spinner Presets", () => {
|
|
46
|
+
test("dots preset exists and has frames", () => {
|
|
47
|
+
expect(SPINNERS.dots).toBeDefined();
|
|
48
|
+
expect(SPINNERS.dots.length).toBeGreaterThan(0);
|
|
49
|
+
});
|
|
50
|
+
test("lines preset exists", () => {
|
|
51
|
+
expect(SPINNERS.lines).toBeDefined();
|
|
52
|
+
});
|
|
53
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heinrichb/console-toolkit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
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",
|
|
@@ -33,12 +33,14 @@
|
|
|
33
33
|
"useTabs": true
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@eslint/js": "^
|
|
37
|
-
"@types/bun": "
|
|
38
|
-
"eslint": "^
|
|
36
|
+
"@eslint/js": "^10.0.1",
|
|
37
|
+
"@types/bun": "^1.3.9",
|
|
38
|
+
"eslint": "^10.0.2",
|
|
39
39
|
"eslint-config-prettier": "^10.1.8",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
40
|
+
"globals": "^17.3.0",
|
|
41
|
+
"jiti": "^2.6.1",
|
|
42
|
+
"prettier": "^3.8.1",
|
|
43
|
+
"typescript": "^5.9.3",
|
|
42
44
|
"typescript-eslint": "^8.56.1"
|
|
43
45
|
}
|
|
44
46
|
}
|