@heinrichb/console-toolkit 1.0.5 → 1.0.6
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/components/progress.d.ts +12 -12
- package/dist/components/progress.js +5 -6
- package/dist/components/progress.test.js +14 -10
- package/dist/core/layout.d.ts +6 -6
- package/dist/core/layout.js +9 -11
- package/dist/core/layout.test.js +16 -5
- package/dist/core/printer.d.ts +20 -7
- package/dist/core/printer.js +105 -17
- package/dist/core/printer.test.js +112 -24
- package/dist/core/style.d.ts +31 -29
- package/dist/core/style.js +114 -70
- package/dist/core/style.test.js +81 -26
- package/dist/core/types.d.ts +42 -12
- package/dist/core/utils.d.ts +10 -10
- package/dist/core/utils.js +6 -9
- package/dist/core/utils.test.js +5 -5
- package/dist/demo.js +79 -122
- package/dist/presets/ascii.d.ts +3 -3
- package/dist/presets/ascii.js +5 -3
- package/dist/presets/ascii.test.js +15 -4
- package/package.json +46 -46
- package/dist/index.test.d.ts +0 -1
- package/dist/index.test.js +0 -146
- package/dist/progress.d.ts +0 -75
- package/dist/progress.js +0 -43
- package/dist/progress.test.d.ts +0 -1
- package/dist/progress.test.js +0 -101
- package/dist/spinner.d.ts +0 -36
- package/dist/spinner.js +0 -33
- package/dist/spinner.test.d.ts +0 -1
- package/dist/spinner.test.js +0 -53
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PrintLine, PrintStyle } from "../core/types";
|
|
2
2
|
export interface ProgressBarOptions {
|
|
3
3
|
/**
|
|
4
4
|
* Progress value between 0.0 and 1.0.
|
|
@@ -12,38 +12,38 @@ export interface ProgressBarOptions {
|
|
|
12
12
|
/**
|
|
13
13
|
* Base style for the entire progress bar.
|
|
14
14
|
*/
|
|
15
|
-
style?:
|
|
15
|
+
style?: PrintStyle;
|
|
16
16
|
/**
|
|
17
17
|
* Style for the brackets (start and end characters).
|
|
18
|
-
* Defaults to `style
|
|
18
|
+
* Defaults to `style`.
|
|
19
19
|
*/
|
|
20
|
-
bracketStyle?:
|
|
20
|
+
bracketStyle?: PrintStyle;
|
|
21
21
|
/**
|
|
22
22
|
* Specific style for the start bracket. Overrides `bracketStyle`.
|
|
23
23
|
*/
|
|
24
|
-
startStyle?:
|
|
24
|
+
startStyle?: PrintStyle;
|
|
25
25
|
/**
|
|
26
26
|
* Specific style for the end bracket. Overrides `bracketStyle`.
|
|
27
27
|
*/
|
|
28
|
-
endStyle?:
|
|
28
|
+
endStyle?: PrintStyle;
|
|
29
29
|
/**
|
|
30
30
|
* Style for the bar (filled and empty parts).
|
|
31
31
|
* Defaults to `style`.
|
|
32
32
|
*/
|
|
33
|
-
barStyle?:
|
|
33
|
+
barStyle?: PrintStyle;
|
|
34
34
|
/**
|
|
35
35
|
* Specific style for the filled part. Overrides `barStyle`.
|
|
36
36
|
*/
|
|
37
|
-
fillStyle?:
|
|
37
|
+
fillStyle?: PrintStyle;
|
|
38
38
|
/**
|
|
39
39
|
* Specific style for the empty part. Overrides `barStyle`.
|
|
40
40
|
*/
|
|
41
|
-
emptyStyle?:
|
|
41
|
+
emptyStyle?: PrintStyle;
|
|
42
42
|
/**
|
|
43
43
|
* Style for the percentage text.
|
|
44
44
|
* Defaults to `style`.
|
|
45
45
|
*/
|
|
46
|
-
percentageStyle?:
|
|
46
|
+
percentageStyle?: PrintStyle;
|
|
47
47
|
/**
|
|
48
48
|
* Character to use for the start bracket. Defaults to `[`.
|
|
49
49
|
*/
|
|
@@ -70,6 +70,6 @@ export interface ProgressBarOptions {
|
|
|
70
70
|
formatPercentage?: (progress: number) => string;
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
|
-
* Creates a
|
|
73
|
+
* Creates a PrintLine representing a progress bar.
|
|
74
74
|
*/
|
|
75
|
-
export declare function createProgressBar(options: ProgressBarOptions):
|
|
75
|
+
export declare function createProgressBar(options: ProgressBarOptions): PrintLine;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Creates a
|
|
2
|
+
* Creates a PrintLine representing a progress bar.
|
|
3
3
|
*/
|
|
4
4
|
export function createProgressBar(options) {
|
|
5
5
|
const { progress, width = 20, style, bracketStyle, startStyle, endStyle, barStyle, fillStyle, emptyStyle, percentageStyle, startChar = "[", endChar = "]", fillChar = "█", emptyChar = "░", showPercentage = true, formatPercentage } = options;
|
|
@@ -8,15 +8,14 @@ export function createProgressBar(options) {
|
|
|
8
8
|
// Calculate filled width
|
|
9
9
|
const filledWidth = Math.round(p * width);
|
|
10
10
|
const emptyWidth = width - filledWidth;
|
|
11
|
-
// Resolve styles
|
|
12
|
-
const
|
|
13
|
-
const resolvedBracketStyle = bracketStyle ?? baseStyle;
|
|
11
|
+
// Resolve styles (Defaults)
|
|
12
|
+
const resolvedBracketStyle = bracketStyle ?? style;
|
|
14
13
|
const resolvedStartStyle = startStyle ?? resolvedBracketStyle;
|
|
15
14
|
const resolvedEndStyle = endStyle ?? resolvedBracketStyle;
|
|
16
|
-
const resolvedBarStyle = barStyle ??
|
|
15
|
+
const resolvedBarStyle = barStyle ?? style;
|
|
17
16
|
const resolvedFillStyle = fillStyle ?? resolvedBarStyle;
|
|
18
17
|
const resolvedEmptyStyle = emptyStyle ?? resolvedBarStyle;
|
|
19
|
-
const resolvedPercentageStyle = percentageStyle ??
|
|
18
|
+
const resolvedPercentageStyle = percentageStyle ?? style;
|
|
20
19
|
const segments = [];
|
|
21
20
|
// Start Bracket
|
|
22
21
|
if (startChar) {
|
|
@@ -7,6 +7,7 @@ describe("createProgressBar", () => {
|
|
|
7
7
|
test("generates a default progress bar at 0%", () => {
|
|
8
8
|
const line = createProgressBar({ progress: 0 });
|
|
9
9
|
const text = getText(line);
|
|
10
|
+
// Default format: [░░░░░░░░░░░░░░░░░░░░] 0%
|
|
10
11
|
expect(text).toContain("[");
|
|
11
12
|
expect(text).toContain("]");
|
|
12
13
|
expect(text).toContain("0%");
|
|
@@ -23,6 +24,8 @@ describe("createProgressBar", () => {
|
|
|
23
24
|
const line = createProgressBar({ progress: 1.0 });
|
|
24
25
|
const text = getText(line);
|
|
25
26
|
expect(text).toContain("100%");
|
|
27
|
+
// Should not contain empty char
|
|
28
|
+
// But wait, width default 20. 100% means 20 filled. 0 empty.
|
|
26
29
|
expect(text).not.toContain("░");
|
|
27
30
|
});
|
|
28
31
|
test("allows custom width", () => {
|
|
@@ -31,34 +34,35 @@ describe("createProgressBar", () => {
|
|
|
31
34
|
const segments = line.segments;
|
|
32
35
|
const filled = segments.find((s) => s.text.includes("█"));
|
|
33
36
|
const empty = segments.find((s) => s.text.includes("░"));
|
|
37
|
+
// 50% of 10 is 5.
|
|
34
38
|
expect(filled?.text.length).toBe(5);
|
|
35
39
|
expect(empty?.text.length).toBe(5);
|
|
36
40
|
});
|
|
37
41
|
test("applies styles correctly", () => {
|
|
38
42
|
const line = createProgressBar({
|
|
39
43
|
progress: 0.5,
|
|
40
|
-
style: "blue",
|
|
41
|
-
bracketStyle: "red",
|
|
42
|
-
barStyle: "green",
|
|
43
|
-
percentageStyle: "yellow"
|
|
44
|
+
style: { color: "blue" },
|
|
45
|
+
bracketStyle: { color: "red" },
|
|
46
|
+
barStyle: { color: "green" },
|
|
47
|
+
percentageStyle: { color: "yellow" }
|
|
44
48
|
});
|
|
45
49
|
const start = line.segments.find((s) => s.text === "[");
|
|
46
50
|
const filled = line.segments.find((s) => s.text.includes("█"));
|
|
47
51
|
const end = line.segments.find((s) => s.text === "]");
|
|
48
52
|
const percentage = line.segments.find((s) => s.text.includes("%"));
|
|
49
|
-
expect(start?.style).
|
|
50
|
-
expect(filled?.style).
|
|
51
|
-
expect(end?.style).
|
|
52
|
-
expect(percentage?.style).
|
|
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" });
|
|
53
57
|
});
|
|
54
58
|
test("cascades styles (general style -> specific)", () => {
|
|
55
59
|
const line = createProgressBar({
|
|
56
60
|
progress: 0.5,
|
|
57
|
-
style: "blue"
|
|
61
|
+
style: { color: "blue" }
|
|
58
62
|
});
|
|
59
63
|
line.segments.forEach((s) => {
|
|
60
64
|
if (s.text.trim().length > 0) {
|
|
61
|
-
expect(s.style).
|
|
65
|
+
expect(s.style).toEqual({ color: "blue" });
|
|
62
66
|
}
|
|
63
67
|
});
|
|
64
68
|
});
|
package/dist/core/layout.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PrintLine, PrintStyle } from "./types";
|
|
2
2
|
import { Printer } from "./printer";
|
|
3
3
|
/**
|
|
4
|
-
* Merges multiple columns of
|
|
4
|
+
* Merges multiple columns of PrintLines into a single layout.
|
|
5
5
|
* Ensures proper alignment by padding shorter lines.
|
|
6
6
|
*
|
|
7
|
-
* @param columns - Array of columns, where each column is an array of
|
|
7
|
+
* @param columns - Array of columns, where each column is an array of PrintLines.
|
|
8
8
|
* @param separator - String used to separate columns.
|
|
9
9
|
* @param defaultStyle - Style to apply to the separator and padding.
|
|
10
10
|
* @param widths - Optional fixed widths for each column.
|
|
11
|
-
* @returns A single array of
|
|
11
|
+
* @returns A single array of PrintLines representing the merged output.
|
|
12
12
|
*/
|
|
13
|
-
export declare function mergeMultipleColumns(columns:
|
|
13
|
+
export declare function mergeMultipleColumns(columns: PrintLine[][], separator?: string, defaultStyle?: PrintStyle, widths?: number[]): PrintLine[];
|
|
14
14
|
/**
|
|
15
15
|
* Prints multiple columns of styled content to the console.
|
|
16
16
|
* A convenience wrapper around `mergeMultipleColumns` and `Printer.print`.
|
|
@@ -18,7 +18,7 @@ export declare function mergeMultipleColumns(columns: StyledLine[][], separator:
|
|
|
18
18
|
* @param columns - Array of columns to print.
|
|
19
19
|
* @param options - Layout options (widths, separator, custom printer).
|
|
20
20
|
*/
|
|
21
|
-
export declare function printColumns(columns:
|
|
21
|
+
export declare function printColumns(columns: PrintLine[][], options?: {
|
|
22
22
|
widths?: number[];
|
|
23
23
|
separator?: string;
|
|
24
24
|
printer?: Printer;
|
package/dist/core/layout.js
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import { computeMaxWidth, padLine } from "./utils";
|
|
2
2
|
import { Printer } from "./printer";
|
|
3
|
-
import { RESET } from "./style";
|
|
4
3
|
// -----------------
|
|
5
4
|
// Core Layout & Printing
|
|
6
5
|
// -----------------
|
|
7
|
-
const defaultPrinter = new Printer();
|
|
8
6
|
/**
|
|
9
|
-
* Merges multiple columns of
|
|
7
|
+
* Merges multiple columns of PrintLines into a single layout.
|
|
10
8
|
* Ensures proper alignment by padding shorter lines.
|
|
11
9
|
*
|
|
12
|
-
* @param columns - Array of columns, where each column is an array of
|
|
10
|
+
* @param columns - Array of columns, where each column is an array of PrintLines.
|
|
13
11
|
* @param separator - String used to separate columns.
|
|
14
12
|
* @param defaultStyle - Style to apply to the separator and padding.
|
|
15
13
|
* @param widths - Optional fixed widths for each column.
|
|
16
|
-
* @returns A single array of
|
|
14
|
+
* @returns A single array of PrintLines representing the merged output.
|
|
17
15
|
*/
|
|
18
|
-
export function mergeMultipleColumns(columns, separator, defaultStyle, widths) {
|
|
16
|
+
export function mergeMultipleColumns(columns, separator = " ", defaultStyle, widths) {
|
|
19
17
|
if (columns.length === 0)
|
|
20
18
|
return [];
|
|
21
19
|
const maxLines = Math.max(...columns.map((c) => c.length));
|
|
@@ -29,12 +27,13 @@ export function mergeMultipleColumns(columns, separator, defaultStyle, widths) {
|
|
|
29
27
|
let segments = [];
|
|
30
28
|
for (let j = 0; j < columns.length; j++) {
|
|
31
29
|
const line = columns[j][i] || { segments: [] };
|
|
32
|
-
//
|
|
30
|
+
// If not the last column, pad it
|
|
33
31
|
if (j < columns.length - 1) {
|
|
34
32
|
const padded = padLine(line, colWidths[j], defaultStyle);
|
|
35
33
|
segments = [...segments, ...padded.segments, { text: separator, style: defaultStyle }];
|
|
36
34
|
}
|
|
37
35
|
else {
|
|
36
|
+
// Last column, just add segments
|
|
38
37
|
segments = [...segments, ...line.segments];
|
|
39
38
|
}
|
|
40
39
|
}
|
|
@@ -50,8 +49,7 @@ export function mergeMultipleColumns(columns, separator, defaultStyle, widths) {
|
|
|
50
49
|
* @param options - Layout options (widths, separator, custom printer).
|
|
51
50
|
*/
|
|
52
51
|
export function printColumns(columns, options = {}) {
|
|
53
|
-
const { widths, separator = " ", printer =
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
printer.print(mergedLines);
|
|
52
|
+
const { widths, separator = " ", printer = new Printer() } = options;
|
|
53
|
+
const mergedLines = mergeMultipleColumns(columns, separator, undefined, widths);
|
|
54
|
+
printer.print({ lines: mergedLines });
|
|
57
55
|
}
|
package/dist/core/layout.test.js
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import { expect, test, describe, spyOn, afterEach } from "bun:test";
|
|
2
2
|
import { mergeMultipleColumns, printColumns } from "./layout";
|
|
3
3
|
import { getLineLength } from "./utils";
|
|
4
|
-
const lineA = { segments: [{ text: "Hello"
|
|
5
|
-
const lineB = { segments: [{ text: "World!!"
|
|
4
|
+
const lineA = { segments: [{ text: "Hello" }] };
|
|
5
|
+
const lineB = { segments: [{ text: "World!!" }] };
|
|
6
6
|
describe("Layout Utilities", () => {
|
|
7
7
|
const stdoutSpy = spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
8
8
|
afterEach(() => {
|
|
9
9
|
stdoutSpy.mockClear();
|
|
10
10
|
});
|
|
11
11
|
test("mergeMultipleColumns handles asymmetric column lengths", () => {
|
|
12
|
-
|
|
12
|
+
// Column 1: [lineA]
|
|
13
|
+
// Column 2: [lineB, lineB]
|
|
14
|
+
// Separator: " | "
|
|
15
|
+
// Default Style: undefined
|
|
16
|
+
// Widths: [10] (first col width 10)
|
|
17
|
+
const merged = mergeMultipleColumns([[lineA], [lineB, lineB]], " | ", undefined, [10]);
|
|
13
18
|
expect(merged.length).toBe(2);
|
|
14
|
-
|
|
19
|
+
// Second merged line:
|
|
20
|
+
// Col 1 is empty (pad to 10) + " | " + Col 2 (lineB, length 7)
|
|
21
|
+
// 10 + 3 + 7 = 20
|
|
22
|
+
expect(getLineLength(merged[1])).toBe(20);
|
|
15
23
|
});
|
|
16
24
|
test("printColumns executes correctly", () => {
|
|
17
25
|
printColumns([[lineA], [lineB]]);
|
|
@@ -29,7 +37,10 @@ describe("Layout Utilities", () => {
|
|
|
29
37
|
});
|
|
30
38
|
test("printColumns handles empty columns", () => {
|
|
31
39
|
printColumns([]);
|
|
32
|
-
|
|
40
|
+
// Should just print nothing or minimal output (if logic handles empty array gracefully)
|
|
41
|
+
// mergeMultipleColumns returns []
|
|
42
|
+
// printer.print({ lines: [] }) -> might output clear sequence or empty string
|
|
43
|
+
expect(stdoutSpy).toHaveBeenCalled();
|
|
33
44
|
});
|
|
34
45
|
test("printColumns handles 3 columns", () => {
|
|
35
46
|
printColumns([[lineA], [lineA], [lineB]]);
|
package/dist/core/printer.d.ts
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PrintBlock, PrinterOptions } from "./types";
|
|
2
2
|
/**
|
|
3
|
-
* Handles rendering
|
|
3
|
+
* Handles rendering PrintBlocks to the terminal with support for interactive/live overwriting.
|
|
4
4
|
*/
|
|
5
5
|
export declare class Printer {
|
|
6
6
|
private linesRendered;
|
|
7
|
-
private
|
|
7
|
+
private isLive;
|
|
8
|
+
private data?;
|
|
8
9
|
constructor(options?: PrinterOptions);
|
|
9
10
|
/**
|
|
10
11
|
* Generates the clear sequence to move cursor and clear previously rendered lines.
|
|
11
12
|
*/
|
|
12
13
|
private getClearSequence;
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
|
-
|
|
15
|
+
* Clears the console using the stored line count.
|
|
16
|
+
*/
|
|
17
|
+
clear(): void;
|
|
18
|
+
/**
|
|
19
|
+
* Renders the PrintBlock to the standard output.
|
|
20
|
+
* If data is provided, updates the internal state.
|
|
16
21
|
*
|
|
17
|
-
* @param
|
|
22
|
+
* @param data - Optional data to update the printer with.
|
|
23
|
+
*/
|
|
24
|
+
print(data?: PrintBlock): void;
|
|
25
|
+
/**
|
|
26
|
+
* Resolves the block's vertical gradient (if any) to a solid color for the specific line.
|
|
27
|
+
*/
|
|
28
|
+
private resolveBlockColorForLine;
|
|
29
|
+
/**
|
|
30
|
+
* Renders a single line.
|
|
18
31
|
*/
|
|
19
|
-
|
|
32
|
+
private renderLine;
|
|
20
33
|
}
|
package/dist/core/printer.js
CHANGED
|
@@ -1,41 +1,129 @@
|
|
|
1
|
-
import { resolveStyle, RESET } from "./style";
|
|
1
|
+
import { getGradientColor, mergeStyles, resolveStyle, RESET, interpolateColor, resolveModifiersToAnsi } from "./style";
|
|
2
2
|
const ESC = "\x1b";
|
|
3
|
-
// -----------------
|
|
4
|
-
// Printer Class
|
|
5
|
-
// -----------------
|
|
6
3
|
/**
|
|
7
|
-
* Handles rendering
|
|
4
|
+
* Handles rendering PrintBlocks to the terminal with support for interactive/live overwriting.
|
|
8
5
|
*/
|
|
9
6
|
export class Printer {
|
|
10
7
|
linesRendered = 0;
|
|
11
|
-
|
|
8
|
+
isLive;
|
|
9
|
+
data;
|
|
12
10
|
constructor(options = {}) {
|
|
13
|
-
this.
|
|
11
|
+
this.isLive = options.live ?? false;
|
|
12
|
+
this.data = options.data;
|
|
14
13
|
}
|
|
15
14
|
/**
|
|
16
15
|
* Generates the clear sequence to move cursor and clear previously rendered lines.
|
|
17
16
|
*/
|
|
18
17
|
getClearSequence() {
|
|
19
|
-
if (!this.
|
|
18
|
+
if (!this.isLive || this.linesRendered === 0)
|
|
20
19
|
return "";
|
|
21
20
|
return `${ESC}[1A${ESC}[2K\r`.repeat(this.linesRendered);
|
|
22
21
|
}
|
|
23
22
|
/**
|
|
24
|
-
*
|
|
25
|
-
|
|
23
|
+
* Clears the console using the stored line count.
|
|
24
|
+
*/
|
|
25
|
+
clear() {
|
|
26
|
+
if (this.linesRendered > 0) {
|
|
27
|
+
process.stdout.write(this.getClearSequence());
|
|
28
|
+
this.linesRendered = 0;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Renders the PrintBlock to the standard output.
|
|
33
|
+
* If data is provided, updates the internal state.
|
|
26
34
|
*
|
|
27
|
-
* @param
|
|
35
|
+
* @param data - Optional data to update the printer with.
|
|
28
36
|
*/
|
|
29
|
-
print(
|
|
37
|
+
print(data) {
|
|
38
|
+
if (data) {
|
|
39
|
+
this.data = data;
|
|
40
|
+
}
|
|
41
|
+
if (!this.data) {
|
|
42
|
+
return; // Nothing to print
|
|
43
|
+
}
|
|
30
44
|
let output = this.getClearSequence();
|
|
31
|
-
lines
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
});
|
|
45
|
+
const lines = this.data.lines;
|
|
46
|
+
const blockStyle = this.data.style ?? {};
|
|
47
|
+
lines.forEach((line, lineIndex) => {
|
|
48
|
+
output += this.renderLine(line, lineIndex, lines.length, blockStyle);
|
|
36
49
|
output += "\n";
|
|
37
50
|
});
|
|
38
51
|
process.stdout.write(output);
|
|
39
52
|
this.linesRendered = lines.length;
|
|
40
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolves the block's vertical gradient (if any) to a solid color for the specific line.
|
|
56
|
+
*/
|
|
57
|
+
resolveBlockColorForLine(blockStyle, lineIndex, totalLines) {
|
|
58
|
+
if (!blockStyle.color)
|
|
59
|
+
return undefined;
|
|
60
|
+
if (Array.isArray(blockStyle.color)) {
|
|
61
|
+
// Vertical Gradient
|
|
62
|
+
if (totalLines <= 1)
|
|
63
|
+
return blockStyle.color[0]; // Single line, use first color
|
|
64
|
+
const colors = blockStyle.color;
|
|
65
|
+
const factor = lineIndex / (totalLines - 1);
|
|
66
|
+
// Interpolate manually to obtain Hex Color
|
|
67
|
+
const f = Math.max(0, Math.min(1, factor));
|
|
68
|
+
const segmentLength = 1 / (colors.length - 1);
|
|
69
|
+
const segmentIndex = Math.min(Math.floor(f / segmentLength), colors.length - 2);
|
|
70
|
+
const segmentFactor = (f - segmentIndex * segmentLength) / segmentLength;
|
|
71
|
+
const c1 = colors[segmentIndex];
|
|
72
|
+
const c2 = colors[segmentIndex + 1];
|
|
73
|
+
return interpolateColor(c1, c2, segmentFactor);
|
|
74
|
+
}
|
|
75
|
+
return blockStyle.color;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Renders a single line.
|
|
79
|
+
*/
|
|
80
|
+
renderLine(line, lineIndex, totalLines, parentBlockStyle) {
|
|
81
|
+
// 1. Resolve Block Gradient to Solid Color for this line
|
|
82
|
+
const blockColorForLine = this.resolveBlockColorForLine(parentBlockStyle, lineIndex, totalLines);
|
|
83
|
+
// 2. Create Base Line Style (Block Modifiers + Resolved Block Color)
|
|
84
|
+
const baseLineStyle = {
|
|
85
|
+
modifiers: parentBlockStyle.modifiers,
|
|
86
|
+
color: blockColorForLine
|
|
87
|
+
};
|
|
88
|
+
// 3. Merge with Line's own style
|
|
89
|
+
// If line.style has color, it overrides baseLineStyle.color
|
|
90
|
+
const effectiveLineStyle = mergeStyles(baseLineStyle, line.style);
|
|
91
|
+
// 4. Pre-calculate total chars for horizontal gradients
|
|
92
|
+
const totalChars = line.segments.reduce((acc, seg) => acc + seg.text.length, 0);
|
|
93
|
+
let currentCharIndex = 0;
|
|
94
|
+
let lineOutput = "";
|
|
95
|
+
line.segments.forEach((seg) => {
|
|
96
|
+
const effectiveSegmentStyle = mergeStyles(effectiveLineStyle, seg.style);
|
|
97
|
+
if (Array.isArray(effectiveSegmentStyle.color)) {
|
|
98
|
+
// Gradient Handling (Horizontal)
|
|
99
|
+
const colors = effectiveSegmentStyle.color;
|
|
100
|
+
const text = seg.text;
|
|
101
|
+
// Determine if we are using the Line's gradient (Global) or Segment's gradient (Local)
|
|
102
|
+
// If effectiveSegmentStyle.color === effectiveLineStyle.color, it's inherited (Global)
|
|
103
|
+
// Otherwise it's the segment's own gradient (Local)
|
|
104
|
+
const isGlobalGradient = effectiveSegmentStyle.color === effectiveLineStyle.color;
|
|
105
|
+
// Iterate characters to apply gradient
|
|
106
|
+
const modifiersAnsi = resolveModifiersToAnsi(effectiveSegmentStyle.modifiers);
|
|
107
|
+
for (let i = 0; i < text.length; i++) {
|
|
108
|
+
let factor = 0;
|
|
109
|
+
if (isGlobalGradient && totalChars > 1) {
|
|
110
|
+
factor = (currentCharIndex + i) / (totalChars - 1);
|
|
111
|
+
}
|
|
112
|
+
else if (!isGlobalGradient && text.length > 1) {
|
|
113
|
+
factor = i / (text.length - 1);
|
|
114
|
+
}
|
|
115
|
+
const colorAnsi = getGradientColor(colors, factor); // Returns ANSI Color Code
|
|
116
|
+
lineOutput += `${modifiersAnsi}${colorAnsi}${text[i]}`;
|
|
117
|
+
}
|
|
118
|
+
lineOutput += RESET;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
// Solid Color Handling
|
|
122
|
+
const ansi = resolveStyle(effectiveSegmentStyle);
|
|
123
|
+
lineOutput += `${ansi}${seg.text}${RESET}`;
|
|
124
|
+
}
|
|
125
|
+
currentCharIndex += seg.text.length;
|
|
126
|
+
});
|
|
127
|
+
return lineOutput;
|
|
128
|
+
}
|
|
41
129
|
}
|
|
@@ -1,36 +1,124 @@
|
|
|
1
1
|
import { expect, test, describe, spyOn, afterEach } from "bun:test";
|
|
2
2
|
import { Printer } from "./printer";
|
|
3
|
+
import { resolveColorToAnsi } from "./style";
|
|
4
|
+
const ESC = "\x1b";
|
|
3
5
|
describe("Printer", () => {
|
|
6
|
+
// Mock process.stdout.write to prevent actual output during tests
|
|
4
7
|
const stdoutSpy = spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
5
8
|
afterEach(() => {
|
|
6
9
|
stdoutSpy.mockClear();
|
|
7
10
|
});
|
|
8
|
-
test("Printer.print outputs
|
|
11
|
+
test("Printer.print outputs basic text with solid styles", () => {
|
|
9
12
|
const printer = new Printer();
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
const block = {
|
|
14
|
+
lines: [{ segments: [{ text: "Hello", style: { color: "red" } }] }]
|
|
15
|
+
};
|
|
16
|
+
printer.print(block);
|
|
17
|
+
expect(stdoutSpy).toHaveBeenCalledTimes(1);
|
|
12
18
|
const output = stdoutSpy.mock.calls[0][0];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
printer
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
// Check for Red ANSI + Text + Reset
|
|
20
|
+
expect(output).toContain(`${ESC}[38;2;239;68;68mHello${ESC}[0m`);
|
|
21
|
+
});
|
|
22
|
+
test("Printer.print applies Block style to lines (inheritance)", () => {
|
|
23
|
+
const printer = new Printer();
|
|
24
|
+
const block = {
|
|
25
|
+
style: { color: "blue", modifiers: ["bold"] },
|
|
26
|
+
lines: [{ segments: [{ text: "Line 1" }] }, { segments: [{ text: "Line 2" }] }]
|
|
27
|
+
};
|
|
28
|
+
printer.print(block);
|
|
29
|
+
const output = stdoutSpy.mock.calls[0][0];
|
|
30
|
+
const blueAnsi = resolveColorToAnsi("blue");
|
|
31
|
+
const boldAnsi = `${ESC}[1m`;
|
|
32
|
+
// Both lines should be Blue + Bold
|
|
33
|
+
// Matches: Bold + Blue + Text + Reset
|
|
34
|
+
expect(output).toContain(`${boldAnsi}${blueAnsi}Line 1${ESC}[0m`);
|
|
35
|
+
expect(output).toContain(`${boldAnsi}${blueAnsi}Line 2${ESC}[0m`);
|
|
36
|
+
});
|
|
37
|
+
test("Printer handles Block Vertical Gradient (Lines inherit solid colors)", () => {
|
|
38
|
+
const printer = new Printer();
|
|
39
|
+
const block = {
|
|
40
|
+
style: { color: ["#000000", "#FFFFFF"] }, // Black to White
|
|
41
|
+
lines: [
|
|
42
|
+
{ segments: [{ text: "Start" }] }, // Should be Black
|
|
43
|
+
{ segments: [{ text: "Middle" }] }, // Should be Gray
|
|
44
|
+
{ segments: [{ text: "End" }] } // Should be White
|
|
45
|
+
]
|
|
46
|
+
};
|
|
47
|
+
printer.print(block);
|
|
48
|
+
const output = stdoutSpy.mock.calls[0][0];
|
|
49
|
+
const blackAnsi = resolveColorToAnsi("#000000");
|
|
50
|
+
const whiteAnsi = resolveColorToAnsi("#FFFFFF");
|
|
51
|
+
const grayAnsi = resolveColorToAnsi("#808080");
|
|
52
|
+
expect(output).toContain(`${blackAnsi}Start${ESC}[0m`);
|
|
53
|
+
expect(output).toContain(`${grayAnsi}Middle${ESC}[0m`);
|
|
54
|
+
expect(output).toContain(`${whiteAnsi}End${ESC}[0m`);
|
|
55
|
+
});
|
|
56
|
+
test("Printer handles Line Override (Horizontal Gradient)", () => {
|
|
57
|
+
const printer = new Printer();
|
|
58
|
+
const block = {
|
|
59
|
+
lines: [
|
|
60
|
+
{
|
|
61
|
+
style: { color: ["#FF0000", "#0000FF"] }, // Red to Blue
|
|
62
|
+
segments: [{ text: "GB" }] // Gradient applies to these 2 chars
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
};
|
|
66
|
+
printer.print(block);
|
|
67
|
+
const output = stdoutSpy.mock.calls[0][0];
|
|
68
|
+
// First char 'G' should be Red (start)
|
|
69
|
+
// Second char 'B' should be Blue (end) - wait, factor logic?
|
|
70
|
+
// Length 2. index 0 -> factor 0. index 1 -> factor 1.
|
|
71
|
+
const redAnsi = resolveColorToAnsi("#FF0000");
|
|
72
|
+
const blueAnsi = resolveColorToAnsi("#0000FF");
|
|
73
|
+
expect(output).toContain(`${redAnsi}G`);
|
|
74
|
+
expect(output).toContain(`${blueAnsi}B`);
|
|
75
|
+
});
|
|
76
|
+
test("Printer handles Segment Override (Solid overrides Line Gradient)", () => {
|
|
77
|
+
const printer = new Printer();
|
|
78
|
+
const block = {
|
|
79
|
+
lines: [
|
|
80
|
+
{
|
|
81
|
+
style: { color: ["#FF0000", "#0000FF"] }, // Line Gradient
|
|
82
|
+
segments: [
|
|
83
|
+
{ text: "A" }, // Inherits Gradient (Red)
|
|
84
|
+
{ text: "B", style: { color: "green" } } // Override Solid (Green)
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
};
|
|
89
|
+
printer.print(block);
|
|
90
|
+
const output = stdoutSpy.mock.calls[0][0];
|
|
91
|
+
const redAnsi = resolveColorToAnsi("#FF0000");
|
|
92
|
+
const greenAnsi = resolveColorToAnsi("green");
|
|
93
|
+
expect(output).toContain(`${redAnsi}A`);
|
|
94
|
+
expect(output).toContain(`${greenAnsi}B`);
|
|
95
|
+
});
|
|
96
|
+
test("Printer handles live clearing", () => {
|
|
97
|
+
const printer = new Printer({ live: true });
|
|
98
|
+
// Print 2 lines
|
|
99
|
+
printer.print({
|
|
100
|
+
lines: [{ segments: [{ text: "1" }] }, { segments: [{ text: "2" }] }]
|
|
101
|
+
});
|
|
102
|
+
// Print again (should clear 2 lines)
|
|
103
|
+
printer.print({
|
|
104
|
+
lines: [{ segments: [{ text: "New" }] }]
|
|
105
|
+
});
|
|
106
|
+
const clearSeq = `${ESC}[1A${ESC}[2K\r`;
|
|
107
|
+
const expectedClear = clearSeq.repeat(2);
|
|
108
|
+
const secondCallOutput = stdoutSpy.mock.calls[1][0];
|
|
109
|
+
expect(secondCallOutput.startsWith(expectedClear)).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
test("Printer.clear() manually clears the output", () => {
|
|
112
|
+
const printer = new Printer({ live: true });
|
|
113
|
+
printer.print({
|
|
114
|
+
lines: [{ segments: [{ text: "Line 1" }] }]
|
|
115
|
+
});
|
|
32
116
|
expect(stdoutSpy).toHaveBeenCalledTimes(1);
|
|
33
|
-
|
|
34
|
-
expect(
|
|
117
|
+
printer.clear();
|
|
118
|
+
expect(stdoutSpy).toHaveBeenCalledTimes(2);
|
|
119
|
+
const clearSeq = `${ESC}[1A${ESC}[2K\r`;
|
|
120
|
+
const expectedClear = clearSeq.repeat(1);
|
|
121
|
+
const clearOutput = stdoutSpy.mock.calls[1][0];
|
|
122
|
+
expect(clearOutput).toBe(expectedClear);
|
|
35
123
|
});
|
|
36
124
|
});
|