@lewishowles/cli-style 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/adapters/bash/cli-style.sh +50 -0
- package/adapters/python/cli_style.py +104 -0
- package/bin/cli-style.js +82 -0
- package/examples/diagnostic.mjs +30 -0
- package/examples/diagnostic.py +38 -0
- package/examples/diagnostic.sh +29 -0
- package/examples/gallery.mjs +18 -0
- package/package.json +42 -0
- package/src/capability/resolve-profile.js +69 -0
- package/src/capability/terminal.js +105 -0
- package/src/cli/gallery-command.js +238 -0
- package/src/cli/help.js +35 -0
- package/src/cli/render-command.js +283 -0
- package/src/create-cli-style.js +53 -0
- package/src/formatters/ansi.js +154 -0
- package/src/gallery/render-gallery.js +758 -0
- package/src/index.js +52 -0
- package/src/patterns/agent-transcript.js +180 -0
- package/src/patterns/audit-finding.js +107 -0
- package/src/patterns/command-result.js +91 -0
- package/src/patterns/compact-data-table.js +67 -0
- package/src/patterns/confirmation-result.js +88 -0
- package/src/patterns/diagnostic-report.js +144 -0
- package/src/patterns/helpers.js +106 -0
- package/src/patterns/next-step-block.js +118 -0
- package/src/patterns/task-summary.js +59 -0
- package/src/primitives/bar-chart.js +131 -0
- package/src/primitives/chip.js +33 -0
- package/src/primitives/divider.js +64 -0
- package/src/primitives/feedback.js +83 -0
- package/src/primitives/panel.js +100 -0
- package/src/primitives/progress-bar.js +121 -0
- package/src/primitives/row.js +80 -0
- package/src/primitives/status.js +39 -0
- package/src/primitives/step-progress.js +184 -0
- package/src/primitives/table.js +239 -0
- package/src/profiles/profiles.js +24 -0
- package/src/theme/colours.js +141 -0
- package/src/theme/results.js +98 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { foreground, style } from "../formatters/ansi.js";
|
|
2
|
+
import { getResultSymbol, resultTypes } from "../theme/results.js";
|
|
3
|
+
import { panel } from "./panel.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Render an informational hint.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} message
|
|
9
|
+
* Hint text.
|
|
10
|
+
* @param {object} options
|
|
11
|
+
* Rendering options.
|
|
12
|
+
* @param {boolean} options.colour
|
|
13
|
+
* Whether ANSI colour should be applied.
|
|
14
|
+
* @returns {string}
|
|
15
|
+
* Rendered hint.
|
|
16
|
+
*/
|
|
17
|
+
export function hint(message = "", options = {}) {
|
|
18
|
+
const prefix = "i Hint:";
|
|
19
|
+
|
|
20
|
+
if (options.colour !== true) {
|
|
21
|
+
return `${prefix} ${message}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const renderedPrefix = style(foreground(prefix, "info", options), "bold", options);
|
|
25
|
+
|
|
26
|
+
return `${renderedPrefix} ${message}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Render an empty-state message.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} title
|
|
33
|
+
* Empty-state title.
|
|
34
|
+
* @param {string} detail
|
|
35
|
+
* Optional supporting detail.
|
|
36
|
+
* @param {object} options
|
|
37
|
+
* Rendering options.
|
|
38
|
+
* @param {boolean} options.colour
|
|
39
|
+
* Whether ANSI colour should be applied.
|
|
40
|
+
* @param {boolean} options.unicode
|
|
41
|
+
* Whether Unicode symbols should be used.
|
|
42
|
+
* @returns {string}
|
|
43
|
+
* Rendered empty state.
|
|
44
|
+
*/
|
|
45
|
+
export function emptyState(title = "No results", detail = "", options = {}) {
|
|
46
|
+
const symbol = getResultSymbol(resultTypes.SKIPPED, options);
|
|
47
|
+
const prefix = `${symbol} ${title}`;
|
|
48
|
+
const detailText = detail === "" ? "" : ` ${detail}`;
|
|
49
|
+
|
|
50
|
+
if (options.colour !== true) {
|
|
51
|
+
return `${prefix}${detailText}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const renderedPrefix = style(foreground(prefix, "muted", options), "bold", options);
|
|
55
|
+
|
|
56
|
+
return `${renderedPrefix}${detailText}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Render a structured error block.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} title
|
|
63
|
+
* Error title.
|
|
64
|
+
* @param {string[]} lines
|
|
65
|
+
* Supporting error details.
|
|
66
|
+
* @param {object} options
|
|
67
|
+
* Rendering options.
|
|
68
|
+
* @param {boolean} options.unicode
|
|
69
|
+
* Whether Unicode symbols should be used.
|
|
70
|
+
* @returns {string}
|
|
71
|
+
* Rendered error block.
|
|
72
|
+
*/
|
|
73
|
+
export function errorBlock(title = "Failed", lines = [], options = {}) {
|
|
74
|
+
const symbol = getResultSymbol(resultTypes.FAILED, options);
|
|
75
|
+
const detailLines = Array.isArray(lines) ? lines : [];
|
|
76
|
+
|
|
77
|
+
return panel({
|
|
78
|
+
...options,
|
|
79
|
+
lines: detailLines,
|
|
80
|
+
title: `${symbol} Error: ${title}`,
|
|
81
|
+
tone: "danger",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { background, foreground, style } from "../formatters/ansi.js";
|
|
2
|
+
import { profiles } from "../profiles/profiles.js";
|
|
3
|
+
import { panelColours } from "../theme/colours.js";
|
|
4
|
+
|
|
5
|
+
// Panels use a stable width so grouped output aligns across a report.
|
|
6
|
+
const defaultWidth = 40;
|
|
7
|
+
|
|
8
|
+
// Info is the neutral semantic accent for grouped explanatory content.
|
|
9
|
+
const defaultTone = "info";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Render a grouped block with a semantic left accent.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} options
|
|
15
|
+
* Rendering options.
|
|
16
|
+
* @param {boolean} options.colour
|
|
17
|
+
* Whether ANSI colour should be applied.
|
|
18
|
+
* @param {string[]} options.lines
|
|
19
|
+
* Content lines to render.
|
|
20
|
+
* @param {number} options.panelWidth
|
|
21
|
+
* Total panel width.
|
|
22
|
+
* @param {string} options.profile
|
|
23
|
+
* Active output profile.
|
|
24
|
+
* @param {string} options.title
|
|
25
|
+
* Optional panel title.
|
|
26
|
+
* @param {string} options.tone
|
|
27
|
+
* Semantic accent tone.
|
|
28
|
+
* @param {boolean} options.unicode
|
|
29
|
+
* Whether Unicode symbols should be used.
|
|
30
|
+
* @returns {string}
|
|
31
|
+
* Rendered panel.
|
|
32
|
+
*/
|
|
33
|
+
export function panel(options = {}) {
|
|
34
|
+
const lines = options.lines ?? [];
|
|
35
|
+
|
|
36
|
+
if (options.profile === profiles.CI) {
|
|
37
|
+
return [
|
|
38
|
+
...(options.title === undefined ? [] : [options.title]),
|
|
39
|
+
...lines,
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const width = Math.max(options.panelWidth ?? defaultWidth, 4);
|
|
44
|
+
const panelLines = [
|
|
45
|
+
"",
|
|
46
|
+
...(options.title === undefined ? [] : [options.title]),
|
|
47
|
+
...lines,
|
|
48
|
+
"",
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
return panelLines
|
|
52
|
+
.map((line, index) => renderLine(line, width, index === 1 && options.title !== undefined, options))
|
|
53
|
+
.join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Render one fixed-width panel line.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} line
|
|
60
|
+
* Content line.
|
|
61
|
+
* @param {number} width
|
|
62
|
+
* Total panel width.
|
|
63
|
+
* @param {boolean} isTitle
|
|
64
|
+
* Whether the line is the panel title.
|
|
65
|
+
* @param {object} options
|
|
66
|
+
* Rendering options.
|
|
67
|
+
* @returns {string}
|
|
68
|
+
* Rendered content line.
|
|
69
|
+
*/
|
|
70
|
+
function renderLine(line, width, isTitle, options) {
|
|
71
|
+
const accent = options.unicode === false ? "|" : "▌";
|
|
72
|
+
const contentWidth = width - 5;
|
|
73
|
+
const visibleLine = line.slice(0, contentWidth);
|
|
74
|
+
const content = ` ${visibleLine.padEnd(contentWidth, " ")} `;
|
|
75
|
+
|
|
76
|
+
if (options.colour !== true) {
|
|
77
|
+
return `${accent}${content}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const renderedAccent = renderPanelColour(accent, options);
|
|
81
|
+
const renderedContent = background(content, panelColours.background, options);
|
|
82
|
+
|
|
83
|
+
return `${renderedAccent}${isTitle ? style(renderedContent, "bold", options) : renderedContent}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Apply the panel's semantic colour.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} value
|
|
90
|
+
* Panel text to colour.
|
|
91
|
+
* @param {object} options
|
|
92
|
+
* Rendering options.
|
|
93
|
+
* @returns {string}
|
|
94
|
+
* Coloured panel text.
|
|
95
|
+
*/
|
|
96
|
+
function renderPanelColour(value, options) {
|
|
97
|
+
const tone = panelColours[options.tone ?? defaultTone] ?? panelColours[defaultTone];
|
|
98
|
+
|
|
99
|
+
return foreground(value, tone, options);
|
|
100
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { foreground } from "../formatters/ansi.js";
|
|
2
|
+
import { getToneColour } from "../theme/colours.js";
|
|
3
|
+
|
|
4
|
+
// Progress bars balance useful resolution with compact terminal output.
|
|
5
|
+
const defaultWidth = 20;
|
|
6
|
+
|
|
7
|
+
// Success is the default tone for work progressing normally.
|
|
8
|
+
const defaultTone = "success";
|
|
9
|
+
|
|
10
|
+
// Invalid maxima fall back to a percentage-friendly range.
|
|
11
|
+
const defaultMaximum = 100;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Render a single value against a maximum.
|
|
15
|
+
*
|
|
16
|
+
* @param {object} options
|
|
17
|
+
* Rendering options.
|
|
18
|
+
* @param {number} options.barWidth
|
|
19
|
+
* Number of characters used by the bar.
|
|
20
|
+
* @param {boolean} options.colour
|
|
21
|
+
* Whether ANSI colour should be applied.
|
|
22
|
+
* @param {number} options.max
|
|
23
|
+
* Maximum progress value.
|
|
24
|
+
* @param {string} options.tone
|
|
25
|
+
* Semantic colour tone for completed progress.
|
|
26
|
+
* @param {boolean} options.unicode
|
|
27
|
+
* Whether Unicode bar characters should be used.
|
|
28
|
+
* @param {number} options.value
|
|
29
|
+
* Current progress value.
|
|
30
|
+
* @returns {string}
|
|
31
|
+
* Rendered progress bar.
|
|
32
|
+
*/
|
|
33
|
+
export function progressBar(options = {}) {
|
|
34
|
+
const maximum = normaliseMaximum(options.max);
|
|
35
|
+
const value = normaliseValue(options.value);
|
|
36
|
+
const width = normaliseWidth(options.barWidth);
|
|
37
|
+
const ratio = Math.min(Math.max(value / maximum, 0), 1);
|
|
38
|
+
const filledWidth = Math.round(width * ratio);
|
|
39
|
+
const emptyWidth = width - filledWidth;
|
|
40
|
+
const symbols = barSymbols(options);
|
|
41
|
+
const filled = symbols.filled.repeat(filledWidth);
|
|
42
|
+
const empty = symbols.empty.repeat(emptyWidth);
|
|
43
|
+
const percentage = Math.round(ratio * 100);
|
|
44
|
+
const numericText = `${percentage}% (${value}/${maximum})`;
|
|
45
|
+
|
|
46
|
+
if (options.colour !== true) {
|
|
47
|
+
return `[${filled}${empty}] ${numericText}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const tone = getToneColour(options.tone) === null ? defaultTone : options.tone;
|
|
51
|
+
const renderedFilled = filled === "" ? "" : foreground(filled, tone ?? defaultTone, options);
|
|
52
|
+
const renderedEmpty = empty === "" ? "" : foreground(empty, "muted", options);
|
|
53
|
+
|
|
54
|
+
return `[${renderedFilled}${renderedEmpty}] ${numericText}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Return progress characters for the active capability.
|
|
59
|
+
*
|
|
60
|
+
* @param {object} options
|
|
61
|
+
* Rendering options.
|
|
62
|
+
* @returns {object}
|
|
63
|
+
* Filled and empty progress symbols.
|
|
64
|
+
*/
|
|
65
|
+
function barSymbols(options) {
|
|
66
|
+
if (options.unicode === false) {
|
|
67
|
+
return {
|
|
68
|
+
empty: "-",
|
|
69
|
+
filled: "#",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
empty: "░",
|
|
75
|
+
filled: "█",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Return a usable maximum.
|
|
81
|
+
*
|
|
82
|
+
* @param {number} maximum
|
|
83
|
+
* Requested maximum.
|
|
84
|
+
* @returns {number}
|
|
85
|
+
* Positive finite maximum.
|
|
86
|
+
*/
|
|
87
|
+
function normaliseMaximum(maximum) {
|
|
88
|
+
if (!Number.isFinite(maximum) || maximum <= 0) {
|
|
89
|
+
return defaultMaximum;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return maximum;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Return a usable progress value.
|
|
97
|
+
*
|
|
98
|
+
* @param {number} value
|
|
99
|
+
* Requested progress value.
|
|
100
|
+
* @returns {number}
|
|
101
|
+
* Finite progress value.
|
|
102
|
+
*/
|
|
103
|
+
function normaliseValue(value) {
|
|
104
|
+
return Number.isFinite(value) ? value : 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Return a usable integer bar width.
|
|
109
|
+
*
|
|
110
|
+
* @param {number} width
|
|
111
|
+
* Requested bar width.
|
|
112
|
+
* @returns {number}
|
|
113
|
+
* Positive integer width.
|
|
114
|
+
*/
|
|
115
|
+
function normaliseWidth(width) {
|
|
116
|
+
if (!Number.isFinite(width)) {
|
|
117
|
+
return defaultWidth;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return Math.max(Math.floor(width), 1);
|
|
121
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { foreground } from "../formatters/ansi.js";
|
|
2
|
+
|
|
3
|
+
// Row labels use the same subdued tone as table headers by default.
|
|
4
|
+
const defaultLabelColour = "muted";
|
|
5
|
+
|
|
6
|
+
// Rows use a compact gap that still reads cleanly in plain terminals.
|
|
7
|
+
const defaultSeparator = " ";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Render a labelled value row.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} label
|
|
13
|
+
* Label text to render.
|
|
14
|
+
* @param {string} value
|
|
15
|
+
* Value text to render.
|
|
16
|
+
* @param {object} options
|
|
17
|
+
* Rendering options.
|
|
18
|
+
* @param {boolean} options.colour
|
|
19
|
+
* Whether ANSI colour should be applied.
|
|
20
|
+
* @param {string} options.labelColour
|
|
21
|
+
* Colour token for the label.
|
|
22
|
+
* @param {number} options.labelWidth
|
|
23
|
+
* Minimum label width before the separator.
|
|
24
|
+
* @param {string} options.separator
|
|
25
|
+
* Text between label and value.
|
|
26
|
+
* @param {string} options.valueColour
|
|
27
|
+
* Optional colour token for the value.
|
|
28
|
+
* @returns {string}
|
|
29
|
+
* Rendered row.
|
|
30
|
+
*/
|
|
31
|
+
export function row(label, value = "", options = {}) {
|
|
32
|
+
const labelWidth = Math.max(label.length, options.labelWidth ?? 0);
|
|
33
|
+
const labelText = label.padEnd(labelWidth, " ");
|
|
34
|
+
const renderedLabel = renderLabel(labelText, options);
|
|
35
|
+
|
|
36
|
+
if (value === "") {
|
|
37
|
+
return renderedLabel;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const renderedValue = renderValue(value, options);
|
|
41
|
+
const separator = options.separator ?? defaultSeparator;
|
|
42
|
+
|
|
43
|
+
return `${renderedLabel}${separator}${renderedValue}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Render a row label.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} label
|
|
50
|
+
* Label text to render.
|
|
51
|
+
* @param {object} options
|
|
52
|
+
* Rendering options.
|
|
53
|
+
* @returns {string}
|
|
54
|
+
* Rendered label.
|
|
55
|
+
*/
|
|
56
|
+
function renderLabel(label, options) {
|
|
57
|
+
if (options.colour !== true) {
|
|
58
|
+
return label;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return foreground(label, options.labelColour ?? defaultLabelColour, options);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Render a row value.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} value
|
|
68
|
+
* Value text to render.
|
|
69
|
+
* @param {object} options
|
|
70
|
+
* Rendering options.
|
|
71
|
+
* @returns {string}
|
|
72
|
+
* Rendered value.
|
|
73
|
+
*/
|
|
74
|
+
function renderValue(value, options) {
|
|
75
|
+
if (options.colour !== true || options.valueColour === undefined) {
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return foreground(value, options.valueColour, options);
|
|
80
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { foreground, style } from "../formatters/ansi.js";
|
|
2
|
+
import { getResultSymbol, getResultToken, resultTypes } from "../theme/results.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Render a result status line.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} resultType
|
|
8
|
+
* Result type to render.
|
|
9
|
+
* @param {string} detail
|
|
10
|
+
* Optional detail text.
|
|
11
|
+
* @param {object} options
|
|
12
|
+
* Rendering options.
|
|
13
|
+
* @param {boolean} options.colour
|
|
14
|
+
* Whether ANSI colour should be applied.
|
|
15
|
+
* @param {string} options.label
|
|
16
|
+
* Optional text label override.
|
|
17
|
+
* @param {boolean} options.unicode
|
|
18
|
+
* Whether Unicode symbols should be used.
|
|
19
|
+
* @returns {string}
|
|
20
|
+
* Rendered status line.
|
|
21
|
+
*/
|
|
22
|
+
export function status(resultType = resultTypes.UNKNOWN, detail = "", options = {}) {
|
|
23
|
+
const token = getResultToken(resultType);
|
|
24
|
+
const symbol = getResultSymbol(resultType, options);
|
|
25
|
+
const label = typeof options.label === "string" && options.label !== ""
|
|
26
|
+
? options.label
|
|
27
|
+
: token.label;
|
|
28
|
+
const prefix = `${symbol} ${label}`;
|
|
29
|
+
const detailText = detail === "" ? "" : ` ${detail}`;
|
|
30
|
+
|
|
31
|
+
if (options.colour !== true) {
|
|
32
|
+
return `${prefix}${detailText}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const colouredPrefix = foreground(prefix, token.tone, options);
|
|
36
|
+
const boldPrefix = style(colouredPrefix, "bold", options);
|
|
37
|
+
|
|
38
|
+
return `${boldPrefix}${detailText}`;
|
|
39
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { foreground } from "../formatters/ansi.js";
|
|
2
|
+
import { getResultSymbol, getResultToken, resultTypes } from "../theme/results.js";
|
|
3
|
+
|
|
4
|
+
// Step states describe position in an ordered flow.
|
|
5
|
+
export const stepStates = {
|
|
6
|
+
COMPLETE: "complete",
|
|
7
|
+
CURRENT: "current",
|
|
8
|
+
PENDING: "pending",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Current steps use the running symbol defined by the package contract.
|
|
12
|
+
const currentToken = {
|
|
13
|
+
symbols: {
|
|
14
|
+
ascii: "...",
|
|
15
|
+
unicode: "…",
|
|
16
|
+
},
|
|
17
|
+
tone: "info",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Render one labelled step state.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} label
|
|
24
|
+
* Step label.
|
|
25
|
+
* @param {string} state
|
|
26
|
+
* Step state or result type.
|
|
27
|
+
* @param {object} options
|
|
28
|
+
* Rendering options.
|
|
29
|
+
* @param {boolean} options.colour
|
|
30
|
+
* Whether ANSI colour should be applied.
|
|
31
|
+
* @param {boolean} options.unicode
|
|
32
|
+
* Whether Unicode symbols should be used.
|
|
33
|
+
* @returns {string}
|
|
34
|
+
* Rendered step.
|
|
35
|
+
*/
|
|
36
|
+
export function step(label = "", state = stepStates.PENDING, options = {}) {
|
|
37
|
+
const token = stepToken(state);
|
|
38
|
+
const symbol = stepSymbol(state, token, options);
|
|
39
|
+
|
|
40
|
+
if (options.colour !== true) {
|
|
41
|
+
return `${symbol} ${label}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const renderedSymbol = foreground(symbol, token.tone, options);
|
|
45
|
+
|
|
46
|
+
return `${renderedSymbol} ${label}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Render an ordered sequence with inferred states and explicit counts.
|
|
51
|
+
*
|
|
52
|
+
* @param {object} options
|
|
53
|
+
* Rendering options.
|
|
54
|
+
* @param {number} options.current
|
|
55
|
+
* Zero-based current step index.
|
|
56
|
+
* @param {string[]} options.steps
|
|
57
|
+
* Ordered step labels.
|
|
58
|
+
* @returns {string}
|
|
59
|
+
* Rendered step progress.
|
|
60
|
+
*/
|
|
61
|
+
export function stepProgress(options = {}) {
|
|
62
|
+
const steps = normaliseSteps(options.steps);
|
|
63
|
+
|
|
64
|
+
if (steps.length === 0) {
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const current = normaliseCurrent(options.current, steps.length);
|
|
69
|
+
|
|
70
|
+
return steps.map((label, index) => {
|
|
71
|
+
const state = stateForIndex(index, current);
|
|
72
|
+
const count = `${index + 1}/${steps.length}`;
|
|
73
|
+
|
|
74
|
+
return `${count} ${step(label, state, options)}`;
|
|
75
|
+
}).join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Return valid step labels.
|
|
80
|
+
*
|
|
81
|
+
* @param {string[]} steps
|
|
82
|
+
* Requested step labels.
|
|
83
|
+
* @returns {string[]}
|
|
84
|
+
* Non-empty labels.
|
|
85
|
+
*/
|
|
86
|
+
function normaliseSteps(steps) {
|
|
87
|
+
if (!Array.isArray(steps)) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return steps.filter((label) => typeof label === "string" && label !== "");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Return a current index within the step range.
|
|
96
|
+
*
|
|
97
|
+
* @param {number} current
|
|
98
|
+
* Requested current index.
|
|
99
|
+
* @param {number} stepCount
|
|
100
|
+
* Number of available steps.
|
|
101
|
+
* @returns {number}
|
|
102
|
+
* Clamped current index.
|
|
103
|
+
*/
|
|
104
|
+
function normaliseCurrent(current, stepCount) {
|
|
105
|
+
if (!Number.isFinite(current)) {
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return Math.min(Math.max(Math.floor(current), 0), stepCount - 1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Infer a positional state for a step index.
|
|
114
|
+
*
|
|
115
|
+
* @param {number} index
|
|
116
|
+
* Step index.
|
|
117
|
+
* @param {number} current
|
|
118
|
+
* Current step index.
|
|
119
|
+
* @returns {string}
|
|
120
|
+
* Positional step state.
|
|
121
|
+
*/
|
|
122
|
+
function stateForIndex(index, current) {
|
|
123
|
+
if (index < current) {
|
|
124
|
+
return stepStates.COMPLETE;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (index === current) {
|
|
128
|
+
return stepStates.CURRENT;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return stepStates.PENDING;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Return the display token for a step state.
|
|
136
|
+
*
|
|
137
|
+
* @param {string} state
|
|
138
|
+
* Step state or result type.
|
|
139
|
+
* @returns {object}
|
|
140
|
+
* Symbol and tone token.
|
|
141
|
+
*/
|
|
142
|
+
function stepToken(state) {
|
|
143
|
+
if (state === stepStates.COMPLETE) {
|
|
144
|
+
return getResultToken(resultTypes.SUCCESS);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (state === stepStates.CURRENT) {
|
|
148
|
+
return currentToken;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (state === stepStates.PENDING) {
|
|
152
|
+
return getResultToken(resultTypes.SKIPPED);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return getResultToken(state);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Return the display symbol for a step state.
|
|
160
|
+
*
|
|
161
|
+
* @param {string} state
|
|
162
|
+
* Step state or result type.
|
|
163
|
+
* @param {object} token
|
|
164
|
+
* Resolved step token.
|
|
165
|
+
* @param {object} options
|
|
166
|
+
* Rendering options.
|
|
167
|
+
* @returns {string}
|
|
168
|
+
* Step symbol.
|
|
169
|
+
*/
|
|
170
|
+
function stepSymbol(state, token, options) {
|
|
171
|
+
if (state === stepStates.CURRENT) {
|
|
172
|
+
return options.unicode === false ? token.symbols.ascii : token.symbols.unicode;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (state === stepStates.COMPLETE) {
|
|
176
|
+
return getResultSymbol(resultTypes.SUCCESS, options);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (state === stepStates.PENDING) {
|
|
180
|
+
return getResultSymbol(resultTypes.SKIPPED, options);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return getResultSymbol(state, options);
|
|
184
|
+
}
|