@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
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { resolveStyle, RESET } from "./style";
|
|
2
|
+
const ESC = "\x1b";
|
|
3
|
+
// -----------------
|
|
4
|
+
// Printer Class
|
|
5
|
+
// -----------------
|
|
6
|
+
/**
|
|
7
|
+
* Handles rendering StyledLines to the terminal with support for interactive overwriting.
|
|
8
|
+
*/
|
|
9
|
+
export class Printer {
|
|
10
|
+
linesRendered = 0;
|
|
11
|
+
isInteractive;
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.isInteractive = options.interactive ?? false;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Generates the clear sequence to move cursor and clear previously rendered lines.
|
|
17
|
+
*/
|
|
18
|
+
getClearSequence() {
|
|
19
|
+
if (!this.isInteractive || this.linesRendered === 0)
|
|
20
|
+
return "";
|
|
21
|
+
return `${ESC}[1A${ESC}[2K\r`.repeat(this.linesRendered);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Renders an array of StyledLines to the standard output.
|
|
25
|
+
* If interactive mode is enabled, it clears the previously printed lines first.
|
|
26
|
+
*
|
|
27
|
+
* @param lines - The lines to print.
|
|
28
|
+
*/
|
|
29
|
+
print(lines) {
|
|
30
|
+
let output = this.getClearSequence();
|
|
31
|
+
lines.forEach((line) => {
|
|
32
|
+
line.segments.forEach((seg) => {
|
|
33
|
+
const ansiStyle = resolveStyle(seg.style);
|
|
34
|
+
output += `${ansiStyle}${seg.text}${RESET}`;
|
|
35
|
+
});
|
|
36
|
+
output += "\n";
|
|
37
|
+
});
|
|
38
|
+
process.stdout.write(output);
|
|
39
|
+
this.linesRendered = lines.length;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { expect, test, describe, spyOn, afterEach } from "bun:test";
|
|
2
|
+
import { Printer } from "./printer";
|
|
3
|
+
describe("Printer", () => {
|
|
4
|
+
const stdoutSpy = spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
stdoutSpy.mockClear();
|
|
7
|
+
});
|
|
8
|
+
test("Printer.print outputs to console with resolved styles", () => {
|
|
9
|
+
const printer = new Printer();
|
|
10
|
+
printer.print([{ segments: [{ text: "Test", style: "red" }] }]);
|
|
11
|
+
expect(stdoutSpy).toHaveBeenCalled();
|
|
12
|
+
const output = stdoutSpy.mock.calls[0][0];
|
|
13
|
+
expect(output).toContain("\x1b[31mTest");
|
|
14
|
+
});
|
|
15
|
+
test("Printer handles interactive clearing", () => {
|
|
16
|
+
const printer = new Printer({ interactive: true });
|
|
17
|
+
printer.print([{ segments: [{ text: "L1", style: "" }] }]);
|
|
18
|
+
printer.print([{ segments: [{ text: "L2", style: "" }] }]);
|
|
19
|
+
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining("\x1b[1A\x1b[2K\r"));
|
|
20
|
+
});
|
|
21
|
+
test("Printer optimizes clearing of multiple lines", () => {
|
|
22
|
+
const printer = new Printer({ interactive: true });
|
|
23
|
+
printer.print([
|
|
24
|
+
{ segments: [{ text: "L1", style: "" }] },
|
|
25
|
+
{ segments: [{ text: "L2", style: "" }] },
|
|
26
|
+
{ segments: [{ text: "L3", style: "" }] }
|
|
27
|
+
]);
|
|
28
|
+
stdoutSpy.mockClear();
|
|
29
|
+
printer.print([{ segments: [{ text: "New", style: "" }] }]);
|
|
30
|
+
const clearSeq = "\x1b[1A\x1b[2K\r";
|
|
31
|
+
const expectedClear = clearSeq.repeat(3);
|
|
32
|
+
expect(stdoutSpy).toHaveBeenCalledTimes(1);
|
|
33
|
+
const callArg = stdoutSpy.mock.calls[0][0];
|
|
34
|
+
expect(callArg.startsWith(expectedClear)).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { HexColor, Style } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* ANSI escape sequence to reset all styles.
|
|
4
|
+
*/
|
|
5
|
+
export declare const RESET = "\u001B[0m";
|
|
6
|
+
/**
|
|
7
|
+
* Converts a hex color string to an RGB object.
|
|
8
|
+
* Validates the hex string format.
|
|
9
|
+
*
|
|
10
|
+
* @param hex - Hex color in the form "#RRGGBB".
|
|
11
|
+
* @returns Object with r, g, b components (0-255).
|
|
12
|
+
*/
|
|
13
|
+
export declare function hexToRgb(hex: string): {
|
|
14
|
+
r: number;
|
|
15
|
+
g: number;
|
|
16
|
+
b: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Converts RGB to a 24-bit ANSI foreground color escape sequence.
|
|
20
|
+
*
|
|
21
|
+
* @param r - Red component (0-255).
|
|
22
|
+
* @param g - Green component (0-255).
|
|
23
|
+
* @param b - Blue component (0-255).
|
|
24
|
+
* @returns ANSI escape sequence string.
|
|
25
|
+
*/
|
|
26
|
+
export declare function rgbToAnsi(r: number, g: number, b: number): string;
|
|
27
|
+
/**
|
|
28
|
+
* Converts a hex color string directly to an ANSI escape sequence.
|
|
29
|
+
*
|
|
30
|
+
* @param hex - Hex color string.
|
|
31
|
+
* @returns ANSI escape sequence string.
|
|
32
|
+
*/
|
|
33
|
+
export declare function hexToAnsi(hex: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Interpolates between two hex colors based on a factor (0 to 1) and returns a Hex color string.
|
|
36
|
+
* Used for gradients.
|
|
37
|
+
*
|
|
38
|
+
* @param color1 - Start color (hex).
|
|
39
|
+
* @param color2 - End color (hex).
|
|
40
|
+
* @param factor - Interpolation factor (0.0 to 1.0).
|
|
41
|
+
* @returns Interpolated Hex color.
|
|
42
|
+
*/
|
|
43
|
+
export declare function interpolateColor(color1: string, color2: string, factor: number): HexColor;
|
|
44
|
+
/**
|
|
45
|
+
* Resolves a single Style or array of Styles into an ANSI escape sequence.
|
|
46
|
+
* Handles hex colors, standard colors, and modifiers.
|
|
47
|
+
*
|
|
48
|
+
* @param style - The style or array of styles to resolve.
|
|
49
|
+
* @returns The resulting ANSI escape sequence string.
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveStyle(style?: Style | Style[]): string;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// -----------------
|
|
2
|
+
// Color Utilities
|
|
3
|
+
// -----------------
|
|
4
|
+
const ESC = "\x1b";
|
|
5
|
+
/**
|
|
6
|
+
* ANSI escape sequence to reset all styles.
|
|
7
|
+
*/
|
|
8
|
+
export const RESET = `${ESC}[0m`;
|
|
9
|
+
/**
|
|
10
|
+
* Converts a hex color string to an RGB object.
|
|
11
|
+
* Validates the hex string format.
|
|
12
|
+
*
|
|
13
|
+
* @param hex - Hex color in the form "#RRGGBB".
|
|
14
|
+
* @returns Object with r, g, b components (0-255).
|
|
15
|
+
*/
|
|
16
|
+
export function hexToRgb(hex) {
|
|
17
|
+
const h = hex.replace(/^#/, "");
|
|
18
|
+
if (h.length !== 6)
|
|
19
|
+
throw new Error("Invalid hex color.");
|
|
20
|
+
return {
|
|
21
|
+
r: parseInt(h.substring(0, 2), 16),
|
|
22
|
+
g: parseInt(h.substring(2, 4), 16),
|
|
23
|
+
b: parseInt(h.substring(4, 6), 16)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Converts RGB to a 24-bit ANSI foreground color escape sequence.
|
|
28
|
+
*
|
|
29
|
+
* @param r - Red component (0-255).
|
|
30
|
+
* @param g - Green component (0-255).
|
|
31
|
+
* @param b - Blue component (0-255).
|
|
32
|
+
* @returns ANSI escape sequence string.
|
|
33
|
+
*/
|
|
34
|
+
export function rgbToAnsi(r, g, b) {
|
|
35
|
+
return `${ESC}[38;2;${r};${g};${b}m`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Converts a hex color string directly to an ANSI escape sequence.
|
|
39
|
+
*
|
|
40
|
+
* @param hex - Hex color string.
|
|
41
|
+
* @returns ANSI escape sequence string.
|
|
42
|
+
*/
|
|
43
|
+
export function hexToAnsi(hex) {
|
|
44
|
+
const { r, g, b } = hexToRgb(hex);
|
|
45
|
+
return rgbToAnsi(r, g, b);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Converts a single RGB component to a 2-digit hex string.
|
|
49
|
+
* Helper for interpolation.
|
|
50
|
+
*/
|
|
51
|
+
function toHex(c) {
|
|
52
|
+
const hex = Math.max(0, Math.min(255, Math.round(c))).toString(16);
|
|
53
|
+
return hex.length === 1 ? "0" + hex : hex;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Interpolates between two hex colors based on a factor (0 to 1) and returns a Hex color string.
|
|
57
|
+
* Used for gradients.
|
|
58
|
+
*
|
|
59
|
+
* @param color1 - Start color (hex).
|
|
60
|
+
* @param color2 - End color (hex).
|
|
61
|
+
* @param factor - Interpolation factor (0.0 to 1.0).
|
|
62
|
+
* @returns Interpolated Hex color.
|
|
63
|
+
*/
|
|
64
|
+
export function interpolateColor(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
|
+
// -----------------
|
|
74
|
+
// Style Resolution
|
|
75
|
+
// -----------------
|
|
76
|
+
const STYLE_CODES = {
|
|
77
|
+
default: "0",
|
|
78
|
+
bold: "1",
|
|
79
|
+
dim: "2",
|
|
80
|
+
italic: "3",
|
|
81
|
+
underline: "4",
|
|
82
|
+
inverse: "7",
|
|
83
|
+
hidden: "8",
|
|
84
|
+
strikethrough: "9",
|
|
85
|
+
black: "30",
|
|
86
|
+
red: "31",
|
|
87
|
+
green: "32",
|
|
88
|
+
yellow: "33",
|
|
89
|
+
blue: "34",
|
|
90
|
+
magenta: "35",
|
|
91
|
+
cyan: "36",
|
|
92
|
+
white: "37",
|
|
93
|
+
gray: "90",
|
|
94
|
+
grey: "90"
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Resolves a single Style or array of Styles into an ANSI escape sequence.
|
|
98
|
+
* Handles hex colors, standard colors, and modifiers.
|
|
99
|
+
*
|
|
100
|
+
* @param style - The style or array of styles to resolve.
|
|
101
|
+
* @returns The resulting ANSI escape sequence string.
|
|
102
|
+
*/
|
|
103
|
+
export function resolveStyle(style) {
|
|
104
|
+
if (Array.isArray(style)) {
|
|
105
|
+
return style.map(resolveStyle).join("");
|
|
106
|
+
}
|
|
107
|
+
if (typeof style !== "string") {
|
|
108
|
+
return "";
|
|
109
|
+
}
|
|
110
|
+
// Check for hex color
|
|
111
|
+
if (style.startsWith("#")) {
|
|
112
|
+
try {
|
|
113
|
+
return hexToAnsi(style);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return ""; // Invalid hex, ignore
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Check for standard styles/colors
|
|
120
|
+
const code = STYLE_CODES[style.toLowerCase()];
|
|
121
|
+
if (code) {
|
|
122
|
+
return `${ESC}[${code}m`;
|
|
123
|
+
}
|
|
124
|
+
// Fallback: return as raw string if it looks like ANSI or just text
|
|
125
|
+
// Ideally we would validate ANSI here, but for flexibility we return it.
|
|
126
|
+
return style;
|
|
127
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { expect, test, describe } from "bun:test";
|
|
2
|
+
import { hexToRgb, rgbToAnsi, hexToAnsi, interpolateColor, resolveStyle } from "./style";
|
|
3
|
+
describe("Color Utilities", () => {
|
|
4
|
+
test("hexToRgb converts correctly", () => {
|
|
5
|
+
expect(hexToRgb("#FFFFFF")).toEqual({ r: 255, g: 255, b: 255 });
|
|
6
|
+
expect(hexToRgb("#000000")).toEqual({ r: 0, g: 0, b: 0 });
|
|
7
|
+
});
|
|
8
|
+
test("hexToRgb throws on invalid hex", () => {
|
|
9
|
+
expect(() => hexToRgb("invalid")).toThrow("Invalid hex color.");
|
|
10
|
+
expect(() => hexToRgb("#FFF")).toThrow("Invalid hex color.");
|
|
11
|
+
});
|
|
12
|
+
test("rgbToAnsi converts correctly", () => {
|
|
13
|
+
expect(rgbToAnsi(255, 255, 255)).toBe("\x1b[38;2;255;255;255m");
|
|
14
|
+
expect(rgbToAnsi(0, 0, 0)).toBe("\x1b[38;2;0;0;0m");
|
|
15
|
+
});
|
|
16
|
+
test("hexToAnsi converts hex string directly to ANSI", () => {
|
|
17
|
+
expect(hexToAnsi("#FFFFFF")).toBe("\x1b[38;2;255;255;255m");
|
|
18
|
+
expect(hexToAnsi("#000000")).toBe("\x1b[38;2;0;0;0m");
|
|
19
|
+
expect(hexToAnsi("#FF0000")).toBe("\x1b[38;2;255;0;0m");
|
|
20
|
+
});
|
|
21
|
+
test("interpolateColor returns hex string", () => {
|
|
22
|
+
const start = "#000000";
|
|
23
|
+
const end = "#ffffff";
|
|
24
|
+
expect(interpolateColor(start, end, 0.5)).toBe("#808080");
|
|
25
|
+
});
|
|
26
|
+
test("interpolateColor clamps factors", () => {
|
|
27
|
+
expect(interpolateColor("#000000", "#ffffff", -1)).toBe("#000000");
|
|
28
|
+
expect(interpolateColor("#000000", "#ffffff", 2)).toBe("#ffffff");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe("Style Resolution", () => {
|
|
32
|
+
test("resolveStyle handles standard colors", () => {
|
|
33
|
+
expect(resolveStyle("red")).toBe("\x1b[31m");
|
|
34
|
+
expect(resolveStyle("blue")).toBe("\x1b[34m");
|
|
35
|
+
});
|
|
36
|
+
test("resolveStyle handles modifiers", () => {
|
|
37
|
+
expect(resolveStyle("bold")).toBe("\x1b[1m");
|
|
38
|
+
});
|
|
39
|
+
test("resolveStyle handles hex colors", () => {
|
|
40
|
+
expect(resolveStyle("#FF0000")).toBe("\x1b[38;2;255;0;0m");
|
|
41
|
+
});
|
|
42
|
+
test("resolveStyle handles arrays of styles", () => {
|
|
43
|
+
expect(resolveStyle(["bold", "red"])).toBe("\x1b[1m\x1b[31m");
|
|
44
|
+
});
|
|
45
|
+
test("resolveStyle handles undefined style", () => {
|
|
46
|
+
expect(resolveStyle(undefined)).toBe("");
|
|
47
|
+
});
|
|
48
|
+
test("resolveStyle passes through raw strings", () => {
|
|
49
|
+
const raw = "\x1b[31m";
|
|
50
|
+
expect(resolveStyle(raw)).toBe(raw);
|
|
51
|
+
expect(resolveStyle("unknown")).toBe("unknown");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard colors supported by most terminals.
|
|
3
|
+
*/
|
|
4
|
+
export type StandardColor = "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray" | "grey";
|
|
5
|
+
/**
|
|
6
|
+
* Text style modifiers.
|
|
7
|
+
*/
|
|
8
|
+
export type StyleModifier = "bold" | "dim" | "italic" | "underline" | "default" | "hidden" | "inverse" | "strikethrough";
|
|
9
|
+
/**
|
|
10
|
+
* A valid Hex color string (e.g., "#FF0000").
|
|
11
|
+
*/
|
|
12
|
+
export type HexColor = `#${string}`;
|
|
13
|
+
/**
|
|
14
|
+
* A style can be a standard color name, a hex color string, or a style modifier.
|
|
15
|
+
* It can also be a raw ANSI string (though discouraged) for backward compatibility or special cases.
|
|
16
|
+
*/
|
|
17
|
+
export type Style = StandardColor | StyleModifier | HexColor | string;
|
|
18
|
+
/**
|
|
19
|
+
* Represents a segment of text with applied styles.
|
|
20
|
+
*/
|
|
21
|
+
export interface StyledSegment {
|
|
22
|
+
/** The text content of the segment. */
|
|
23
|
+
text: string;
|
|
24
|
+
/** Style or array of styles to apply to the text. */
|
|
25
|
+
style?: Style | Style[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Represents a line of text composed of multiple styled segments.
|
|
29
|
+
*/
|
|
30
|
+
export interface StyledLine {
|
|
31
|
+
/** Array of segments that make up the line. */
|
|
32
|
+
segments: StyledSegment[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Configuration for the Printer engine.
|
|
36
|
+
*/
|
|
37
|
+
export interface PrinterOptions {
|
|
38
|
+
/** If true, the printer will overwrite previous lines instead of appending new ones. */
|
|
39
|
+
interactive?: boolean;
|
|
40
|
+
/** The default style to apply to padding or separators. */
|
|
41
|
+
defaultStyle?: Style | Style[];
|
|
42
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Style, StyledLine } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Gets the plain text length of a StyledLine (ignoring ANSI codes).
|
|
4
|
+
*
|
|
5
|
+
* @param line - The StyledLine to measure.
|
|
6
|
+
* @returns The length of the text content.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getLineLength(line: StyledLine): number;
|
|
9
|
+
/**
|
|
10
|
+
* Computes the maximum width among an array of StyledLines.
|
|
11
|
+
* Useful for aligning columns.
|
|
12
|
+
*
|
|
13
|
+
* @param lines - Array of StyledLines.
|
|
14
|
+
* @returns The maximum line length found.
|
|
15
|
+
*/
|
|
16
|
+
export declare function computeMaxWidth(lines: StyledLine[]): number;
|
|
17
|
+
/**
|
|
18
|
+
* Pads a StyledLine to a target width by adding an empty segment at the end.
|
|
19
|
+
*
|
|
20
|
+
* @param line - The line to pad.
|
|
21
|
+
* @param targetWidth - The desired minimum width.
|
|
22
|
+
* @param padStyle - The style to apply to the padding spaces.
|
|
23
|
+
* @returns A new StyledLine with padding added if necessary.
|
|
24
|
+
*/
|
|
25
|
+
export declare function padLine(line: StyledLine, targetWidth: number, padStyle: Style | Style[]): StyledLine;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// -----------------
|
|
2
|
+
// Line Manipulation Helpers
|
|
3
|
+
// -----------------
|
|
4
|
+
/**
|
|
5
|
+
* Gets the plain text length of a StyledLine (ignoring ANSI codes).
|
|
6
|
+
*
|
|
7
|
+
* @param line - The StyledLine to measure.
|
|
8
|
+
* @returns The length of the text content.
|
|
9
|
+
*/
|
|
10
|
+
export function getLineLength(line) {
|
|
11
|
+
return line.segments.reduce((acc, seg) => acc + seg.text.length, 0);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Computes the maximum width among an array of StyledLines.
|
|
15
|
+
* Useful for aligning columns.
|
|
16
|
+
*
|
|
17
|
+
* @param lines - Array of StyledLines.
|
|
18
|
+
* @returns The maximum line length found.
|
|
19
|
+
*/
|
|
20
|
+
export function computeMaxWidth(lines) {
|
|
21
|
+
return lines.length > 0 ? Math.max(...lines.map(getLineLength)) : 0;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Pads a StyledLine to a target width by adding an empty segment at the end.
|
|
25
|
+
*
|
|
26
|
+
* @param line - The line to pad.
|
|
27
|
+
* @param targetWidth - The desired minimum width.
|
|
28
|
+
* @param padStyle - The style to apply to the padding spaces.
|
|
29
|
+
* @returns A new StyledLine with padding added if necessary.
|
|
30
|
+
*/
|
|
31
|
+
export function padLine(line, targetWidth, padStyle) {
|
|
32
|
+
const currentLength = getLineLength(line);
|
|
33
|
+
if (currentLength < targetWidth) {
|
|
34
|
+
return {
|
|
35
|
+
segments: [...line.segments, { text: " ".repeat(targetWidth - currentLength), style: padStyle }]
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return line;
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { expect, test, describe } from "bun:test";
|
|
2
|
+
import { getLineLength, computeMaxWidth, padLine } from "./utils";
|
|
3
|
+
const lineA = { segments: [{ text: "Hello", style: [] }] };
|
|
4
|
+
const lineB = { segments: [{ text: "World!!", style: [] }] };
|
|
5
|
+
describe("Line Utilities", () => {
|
|
6
|
+
test("getLineLength calculates correctly", () => {
|
|
7
|
+
expect(getLineLength(lineA)).toBe(5);
|
|
8
|
+
expect(getLineLength(lineB)).toBe(7);
|
|
9
|
+
});
|
|
10
|
+
test("computeMaxWidth finds the longest line", () => {
|
|
11
|
+
expect(computeMaxWidth([lineA, lineB])).toBe(7);
|
|
12
|
+
expect(computeMaxWidth([])).toBe(0);
|
|
13
|
+
});
|
|
14
|
+
test("padLine adds padding when needed", () => {
|
|
15
|
+
const padded = padLine(lineA, 10, "red");
|
|
16
|
+
expect(getLineLength(padded)).toBe(10);
|
|
17
|
+
expect(padded.segments[1].style).toBe("red");
|
|
18
|
+
expect(padded.segments[1].text).toBe(" ");
|
|
19
|
+
});
|
|
20
|
+
test("padLine does nothing if line is already wide enough", () => {
|
|
21
|
+
const ignored = padLine(lineB, 5, "red");
|
|
22
|
+
expect(getLineLength(ignored)).toBe(7);
|
|
23
|
+
expect(ignored.segments.length).toBe(1);
|
|
24
|
+
});
|
|
25
|
+
});
|
package/dist/demo.d.ts
CHANGED
package/dist/demo.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { printColumns, getDragonLines, Printer, interpolateColor, createProgressBar, Spinner, SPINNERS } from "./index";
|
|
2
2
|
import pkg from "../package.json";
|
|
3
3
|
/**
|
|
4
4
|
* Run this demo to visually verify terminal output:
|
|
5
5
|
* bun run src/demo.ts
|
|
6
6
|
*/
|
|
7
|
+
function getProgressBarColor(factor) {
|
|
8
|
+
// Multi-stop gradient: Blue -> Cyan -> Green
|
|
9
|
+
if (factor < 0.5) {
|
|
10
|
+
return interpolateColor("#3B82F6", "#06B6D4", factor * 2);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
return interpolateColor("#06B6D4", "#10B981", (factor - 0.5) * 2);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
7
16
|
export async function runDemo() {
|
|
8
17
|
console.clear();
|
|
18
|
+
const staticPrinter = new Printer();
|
|
9
19
|
// 1. Test Static Dual Column Print
|
|
10
20
|
console.log("--- Static Dual Column Demo ---");
|
|
11
21
|
const purple = "#A78BFA";
|
|
@@ -22,17 +32,23 @@ export async function runDemo() {
|
|
|
22
32
|
{ segments: [{ text: pkg.version, style: green }] },
|
|
23
33
|
{ segments: [{ text: "Testing live output...", style: yellow }] }
|
|
24
34
|
];
|
|
25
|
-
|
|
35
|
+
printColumns([leftContent, rightContent], { separator: " => " });
|
|
36
|
+
console.log("\n");
|
|
37
|
+
// 1b. Test 3-Column Print
|
|
38
|
+
console.log("--- Static 3-Column Demo ---");
|
|
39
|
+
const col1 = [{ segments: [{ text: "Column 1", style: "red" }] }];
|
|
40
|
+
const col2 = [{ segments: [{ text: "Column 2", style: "green" }] }];
|
|
41
|
+
const col3 = [{ segments: [{ text: "Column 3", style: "blue" }] }];
|
|
42
|
+
printColumns([col1, col2, col3], { separator: " | " });
|
|
26
43
|
console.log("\n");
|
|
27
44
|
// 2. Test the Dragon Gradient Preset
|
|
28
45
|
console.log("--- Dragon Gradient Preset ---");
|
|
29
46
|
const dragon = getDragonLines("#EF4444", "#FDE047");
|
|
30
|
-
|
|
31
|
-
printer.print(dragon);
|
|
47
|
+
staticPrinter.print(dragon);
|
|
32
48
|
console.log("\n");
|
|
33
49
|
// 3. Test Progress Bar
|
|
34
50
|
console.log("--- Interactive Progress Bar Demo ---");
|
|
35
|
-
const
|
|
51
|
+
const interactiveProgressPrinter = new Printer({ interactive: true });
|
|
36
52
|
const gray = "#4B5563";
|
|
37
53
|
for (let i = 0; i <= 100; i += 2) {
|
|
38
54
|
const progressColor = interpolateColor("#3B82F6", "#10B981", i / 100);
|
|
@@ -47,14 +63,106 @@ export async function runDemo() {
|
|
|
47
63
|
emptyStyle: gray,
|
|
48
64
|
percentageStyle: progressColor
|
|
49
65
|
});
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
const factor = i / 100;
|
|
67
|
+
const gradientColor = getProgressBarColor(factor);
|
|
68
|
+
const gradientBar = createProgressBar({
|
|
69
|
+
progress: factor,
|
|
70
|
+
width: 40,
|
|
71
|
+
startChar: "▕",
|
|
72
|
+
endChar: "▏",
|
|
73
|
+
fillChar: "█",
|
|
74
|
+
emptyChar: "░",
|
|
75
|
+
startStyle: gradientColor,
|
|
76
|
+
endStyle: gradientColor,
|
|
77
|
+
fillStyle: gradientColor,
|
|
78
|
+
emptyStyle: "gray",
|
|
79
|
+
percentageStyle: ["bold", gradientColor]
|
|
80
|
+
});
|
|
81
|
+
const sharpBar = createProgressBar({
|
|
82
|
+
progress: factor,
|
|
83
|
+
width: 40,
|
|
84
|
+
startChar: "[",
|
|
85
|
+
endChar: "]",
|
|
86
|
+
fillChar: "#",
|
|
87
|
+
emptyChar: "-",
|
|
88
|
+
style: "white",
|
|
89
|
+
fillStyle: "green",
|
|
90
|
+
emptyStyle: "dim"
|
|
91
|
+
});
|
|
92
|
+
const noStyleBar = createProgressBar({
|
|
93
|
+
progress: factor,
|
|
94
|
+
width: 40,
|
|
95
|
+
startChar: "<",
|
|
96
|
+
endChar: ">",
|
|
97
|
+
fillChar: "=",
|
|
98
|
+
emptyChar: " "
|
|
99
|
+
});
|
|
100
|
+
interactiveProgressPrinter.print([
|
|
101
|
+
progressLine,
|
|
102
|
+
{ segments: [] },
|
|
103
|
+
{ segments: [{ text: "Gradient: ", style: "bold" }] },
|
|
104
|
+
gradientBar,
|
|
105
|
+
{ segments: [] },
|
|
106
|
+
{ segments: [{ text: "Sharp: ", style: "bold" }] },
|
|
107
|
+
sharpBar,
|
|
108
|
+
{ segments: [] },
|
|
109
|
+
{ segments: [{ text: "Minimal: ", style: "bold" }] },
|
|
110
|
+
noStyleBar
|
|
111
|
+
]);
|
|
112
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
113
|
+
}
|
|
114
|
+
console.log("\n");
|
|
115
|
+
// 4. Spinners Demo
|
|
116
|
+
console.log("--- Spinners Demo ---");
|
|
117
|
+
const spinnerDots = new Spinner({ frames: SPINNERS.dots, interval: 80 });
|
|
118
|
+
const spinnerLines = new Spinner({ frames: SPINNERS.lines, interval: 100 });
|
|
119
|
+
const spinnerArrows = new Spinner({ frames: SPINNERS.arrows, interval: 120 });
|
|
120
|
+
const spinnerCircle = new Spinner({ frames: SPINNERS.circle, interval: 150 });
|
|
121
|
+
const spinnerSquare = new Spinner({ frames: SPINNERS.square, interval: 200 });
|
|
122
|
+
const interactiveSpinnerPrinter = new Printer({ interactive: true });
|
|
123
|
+
const startTime = Date.now();
|
|
124
|
+
while (Date.now() - startTime < 3000) {
|
|
125
|
+
const lines = [
|
|
126
|
+
{
|
|
127
|
+
segments: [
|
|
128
|
+
{ text: "Dots: ", style: "dim" },
|
|
129
|
+
{ text: spinnerDots.getFrame(), style: "cyan" }
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
segments: [
|
|
134
|
+
{ text: "Lines: ", style: "dim" },
|
|
135
|
+
{ text: spinnerLines.getFrame(), style: "yellow" }
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
segments: [
|
|
140
|
+
{ text: "Arrows: ", style: "dim" },
|
|
141
|
+
{ text: spinnerArrows.getFrame(), style: "green" }
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
segments: [
|
|
146
|
+
{ text: "Circle: ", style: "dim" },
|
|
147
|
+
{ text: spinnerCircle.getFrame(), style: "magenta" }
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
segments: [
|
|
152
|
+
{ text: "Square: ", style: "dim" },
|
|
153
|
+
{ text: spinnerSquare.getFrame(), style: "blue" }
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
];
|
|
157
|
+
interactiveSpinnerPrinter.print(lines);
|
|
158
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
52
159
|
}
|
|
53
160
|
console.log("\n");
|
|
54
|
-
//
|
|
161
|
+
// 5. Style Codes Demo
|
|
55
162
|
console.log("--- Style Codes Demo ---");
|
|
56
163
|
const styles = [
|
|
57
|
-
{ name: "Default", style:
|
|
164
|
+
{ name: "Default", style: undefined },
|
|
165
|
+
{ name: "Bold + Red", style: ["bold", "red"] },
|
|
58
166
|
{ name: "Bold", style: "bold" },
|
|
59
167
|
{ name: "Dim", style: "dim" },
|
|
60
168
|
{ name: "Italic", style: "italic" },
|
|
@@ -73,12 +181,8 @@ export async function runDemo() {
|
|
|
73
181
|
{ name: "Gray", style: "gray" }
|
|
74
182
|
];
|
|
75
183
|
const styleLines = styles.map((s) => ({
|
|
76
|
-
segments: [
|
|
77
|
-
{ text: s.name.padEnd(15), style: "default" },
|
|
78
|
-
{ text: "Sample Text", style: s.style }
|
|
79
|
-
]
|
|
184
|
+
segments: [{ text: s.name.padEnd(15) }, { text: "Sample Text", style: s.style }]
|
|
80
185
|
}));
|
|
81
|
-
|
|
82
|
-
stylePrinter.print(styleLines);
|
|
186
|
+
staticPrinter.print(styleLines);
|
|
83
187
|
console.log("\n✨ Demo Complete!");
|
|
84
188
|
}
|