@lewishowles/cli-style 0.9.0 → 0.11.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/CHANGELOG.md +19 -0
- package/README.md +102 -1
- package/bin/cli-style.js +10 -0
- package/package.json +1 -1
- package/src/capability/terminal.js +98 -0
- package/src/catalogue/renderer-catalogue.js +62 -0
- package/src/cli/catalogue-command.js +108 -0
- package/src/cli/gallery-command.js +8 -1
- package/src/cli/help.js +5 -0
- package/src/cli/render-command.js +17 -2
- package/src/create-cli-style.js +14 -0
- package/src/formatters/ansi.js +17 -1
- package/src/gallery/render-gallery.js +163 -2
- package/src/index.js +19 -2
- package/src/interactive/spinner.js +335 -0
- package/src/patterns/diff-block.js +333 -0
- package/src/primitives/chip.js +1 -1
- package/src/primitives/panel.js +6 -1
- package/src/primitives/sparkline.js +209 -0
- package/src/theme/colours.js +172 -36
package/src/formatters/ansi.js
CHANGED
|
@@ -100,7 +100,7 @@ export function stripAnsi(value) {
|
|
|
100
100
|
* Coloured text, or the original text when colour is disabled.
|
|
101
101
|
*/
|
|
102
102
|
function wrapColour(value, colour, options, createCode) {
|
|
103
|
-
const resolvedColour = resolveColourValue(colour);
|
|
103
|
+
const resolvedColour = resolveColourValue(colour, options);
|
|
104
104
|
|
|
105
105
|
if (options.colour !== true || resolvedColour === null) {
|
|
106
106
|
return value;
|
|
@@ -118,6 +118,14 @@ function wrapColour(value, colour, options, createCode) {
|
|
|
118
118
|
* ANSI foreground sequence.
|
|
119
119
|
*/
|
|
120
120
|
function createForegroundCode(colour) {
|
|
121
|
+
if (colour === "default-foreground") {
|
|
122
|
+
return "\u001b[39m";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (colour.startsWith("ansi-256:")) {
|
|
126
|
+
return `\u001b[38;5;${colour.slice("ansi-256:".length)}m`;
|
|
127
|
+
}
|
|
128
|
+
|
|
121
129
|
const { blue, green, red } = parseHexColour(colour);
|
|
122
130
|
|
|
123
131
|
return `\u001b[38;2;${red};${green};${blue}m`;
|
|
@@ -132,6 +140,14 @@ function createForegroundCode(colour) {
|
|
|
132
140
|
* ANSI background sequence.
|
|
133
141
|
*/
|
|
134
142
|
function createBackgroundCode(colour) {
|
|
143
|
+
if (colour === "default-background") {
|
|
144
|
+
return "\u001b[49m";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (colour.startsWith("ansi-256:")) {
|
|
148
|
+
return `\u001b[48;5;${colour.slice("ansi-256:".length)}m`;
|
|
149
|
+
}
|
|
150
|
+
|
|
135
151
|
const { blue, green, red } = parseHexColour(colour);
|
|
136
152
|
|
|
137
153
|
return `\u001b[48;2;${red};${green};${blue}m`;
|
|
@@ -4,9 +4,11 @@ import { commandResult } from "../patterns/command-result.js";
|
|
|
4
4
|
import { compactDataTable } from "../patterns/compact-data-table.js";
|
|
5
5
|
import { confirmationResult } from "../patterns/confirmation-result.js";
|
|
6
6
|
import { diagnosticReport } from "../patterns/diagnostic-report.js";
|
|
7
|
+
import { diffBlock } from "../patterns/diff-block.js";
|
|
7
8
|
import { nextStepBlock } from "../patterns/next-step-block.js";
|
|
8
9
|
import { taskSummary } from "../patterns/task-summary.js";
|
|
9
10
|
import { stripAnsi } from "../formatters/ansi.js";
|
|
11
|
+
import { renderSpinnerFrame } from "../interactive/spinner.js";
|
|
10
12
|
import { barChart } from "../primitives/bar-chart.js";
|
|
11
13
|
import { chip } from "../primitives/chip.js";
|
|
12
14
|
import { divider } from "../primitives/divider.js";
|
|
@@ -15,6 +17,7 @@ import { panel } from "../primitives/panel.js";
|
|
|
15
17
|
import { progressBar } from "../primitives/progress-bar.js";
|
|
16
18
|
import { row } from "../primitives/row.js";
|
|
17
19
|
import { rowGroup } from "../primitives/row-group.js";
|
|
20
|
+
import { sparkline } from "../primitives/sparkline.js";
|
|
18
21
|
import { span } from "../primitives/span.js";
|
|
19
22
|
import { status } from "../primitives/status.js";
|
|
20
23
|
import { step, stepProgress, stepStates } from "../primitives/step-progress.js";
|
|
@@ -115,7 +118,7 @@ export const galleryVariants = ["current", "no-colour", "no-unicode", "plain"];
|
|
|
115
118
|
// Gallery sections available for focused output.
|
|
116
119
|
export const gallerySections = ["primitives", "patterns"];
|
|
117
120
|
|
|
118
|
-
//
|
|
121
|
+
// Fixtures available for focused output and interactive search.
|
|
119
122
|
export const galleryFixtures = [
|
|
120
123
|
"diagnostic-report",
|
|
121
124
|
"reporter",
|
|
@@ -127,6 +130,9 @@ export const galleryFixtures = [
|
|
|
127
130
|
"compact-data-table-narrow",
|
|
128
131
|
"confirmation-result",
|
|
129
132
|
"next-step-block",
|
|
133
|
+
"diff-block",
|
|
134
|
+
"sparkline",
|
|
135
|
+
"spinner",
|
|
130
136
|
];
|
|
131
137
|
|
|
132
138
|
/**
|
|
@@ -179,7 +185,7 @@ function renderVariant(variant, options, request) {
|
|
|
179
185
|
const sections = [];
|
|
180
186
|
|
|
181
187
|
if (request.fixture !== undefined) {
|
|
182
|
-
sections.push(
|
|
188
|
+
sections.push(renderFocusedFixture(request.fixture, resolvedVariant.options));
|
|
183
189
|
} else {
|
|
184
190
|
if (request.section === undefined || request.section === "primitives") {
|
|
185
191
|
sections.push(renderPrimitives(resolvedVariant.options));
|
|
@@ -434,6 +440,36 @@ function renderPatterns(options, fixture) {
|
|
|
434
440
|
options,
|
|
435
441
|
);
|
|
436
442
|
|
|
443
|
+
const diff = diffBlock(
|
|
444
|
+
{
|
|
445
|
+
lines: [
|
|
446
|
+
{
|
|
447
|
+
text: "render()",
|
|
448
|
+
type: "header",
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
newLineNumber: 102,
|
|
452
|
+
text: "return nextStep;",
|
|
453
|
+
type: "added",
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
oldLineNumber: 101,
|
|
457
|
+
text: "return currentStep;",
|
|
458
|
+
type: "removed",
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
newLineNumber: 100,
|
|
462
|
+
oldLineNumber: 100,
|
|
463
|
+
text: "const currentStep = getStep();",
|
|
464
|
+
type: "context",
|
|
465
|
+
},
|
|
466
|
+
],
|
|
467
|
+
path: "src/render.js",
|
|
468
|
+
title: "Renderer change",
|
|
469
|
+
},
|
|
470
|
+
options,
|
|
471
|
+
);
|
|
472
|
+
|
|
437
473
|
const reporter = createReporter(options);
|
|
438
474
|
|
|
439
475
|
reporter.divider("Project setup", "Claude + Codex");
|
|
@@ -561,6 +597,7 @@ function renderPatterns(options, fixture) {
|
|
|
561
597
|
"compact-data-table-narrow": ["Compact data table (narrow)", narrowCompactTable],
|
|
562
598
|
"confirmation-result": ["Confirmation result", confirmation],
|
|
563
599
|
"diagnostic-report": ["Diagnostic report", report],
|
|
600
|
+
"diff-block": ["Diff block", diff],
|
|
564
601
|
"next-step-block": ["Next-step block", nextStep],
|
|
565
602
|
reporter: ["Reporter", reporterOutput],
|
|
566
603
|
"task-summary": ["Task summary", summary],
|
|
@@ -594,6 +631,9 @@ function renderPatterns(options, fixture) {
|
|
|
594
631
|
"Audit finding",
|
|
595
632
|
frameExample(audit, options),
|
|
596
633
|
"",
|
|
634
|
+
"Diff block",
|
|
635
|
+
frameExample(diff, options),
|
|
636
|
+
"",
|
|
597
637
|
"Compact data table",
|
|
598
638
|
frameExample(compactTable, options),
|
|
599
639
|
"",
|
|
@@ -622,6 +662,52 @@ function renderPatternFixture(fixture, options) {
|
|
|
622
662
|
return [fixture[0], frameExample(fixture[1], options)].join("\n");
|
|
623
663
|
}
|
|
624
664
|
|
|
665
|
+
/**
|
|
666
|
+
* Render the focused sparkline fixture.
|
|
667
|
+
*
|
|
668
|
+
* @param {object} options
|
|
669
|
+
* Rendering options.
|
|
670
|
+
* @returns {string}
|
|
671
|
+
* Framed sparkline fixture output.
|
|
672
|
+
*/
|
|
673
|
+
function renderSparklineFixture(options) {
|
|
674
|
+
return ["Sparkline", frameExample(renderSparklineExample(options), options)].join("\n");
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Render the focused spinner fixture.
|
|
679
|
+
*
|
|
680
|
+
* @param {object} options
|
|
681
|
+
* Rendering options.
|
|
682
|
+
* @returns {string}
|
|
683
|
+
* Framed spinner fixture output.
|
|
684
|
+
*/
|
|
685
|
+
function renderSpinnerFixture(options) {
|
|
686
|
+
return ["Spinner", frameExample(renderSpinnerExample(options), options)].join("\n");
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Render one fixture selected through `--fixture`.
|
|
691
|
+
*
|
|
692
|
+
* @param {string} fixture
|
|
693
|
+
* Requested fixture name.
|
|
694
|
+
* @param {object} options
|
|
695
|
+
* Rendering options.
|
|
696
|
+
* @returns {string}
|
|
697
|
+
* Framed fixture output.
|
|
698
|
+
*/
|
|
699
|
+
function renderFocusedFixture(fixture, options) {
|
|
700
|
+
if (fixture === "sparkline") {
|
|
701
|
+
return renderSparklineFixture(options);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (fixture === "spinner") {
|
|
705
|
+
return renderSpinnerFixture(options);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return renderPatterns(options, fixture);
|
|
709
|
+
}
|
|
710
|
+
|
|
625
711
|
/**
|
|
626
712
|
* Add gallery-only framing around one rendered example.
|
|
627
713
|
*
|
|
@@ -665,9 +751,15 @@ function renderPrimitives(options) {
|
|
|
665
751
|
"Progress bars",
|
|
666
752
|
...renderProgressExamples(options),
|
|
667
753
|
"",
|
|
754
|
+
"Spinner",
|
|
755
|
+
renderSpinnerExample(options),
|
|
756
|
+
"",
|
|
668
757
|
"Bar charts",
|
|
669
758
|
renderBarChartExample(options),
|
|
670
759
|
"",
|
|
760
|
+
"Sparklines",
|
|
761
|
+
renderSparklineExample(options),
|
|
762
|
+
"",
|
|
671
763
|
"Tables",
|
|
672
764
|
renderTableExample(options),
|
|
673
765
|
"",
|
|
@@ -806,6 +898,75 @@ function renderBarChartExample(options) {
|
|
|
806
898
|
});
|
|
807
899
|
}
|
|
808
900
|
|
|
901
|
+
/**
|
|
902
|
+
* Render representative sparkline sequences.
|
|
903
|
+
*
|
|
904
|
+
* @param {object} options
|
|
905
|
+
* Rendering options.
|
|
906
|
+
* @returns {string}
|
|
907
|
+
* Sparkline examples.
|
|
908
|
+
*/
|
|
909
|
+
function renderSparklineExample(options) {
|
|
910
|
+
const examples = [
|
|
911
|
+
{
|
|
912
|
+
label: "Rising",
|
|
913
|
+
tone: "success",
|
|
914
|
+
values: [1, 2, 3, 4, 5, 6],
|
|
915
|
+
},
|
|
916
|
+
{
|
|
917
|
+
label: "Falling",
|
|
918
|
+
tone: "danger",
|
|
919
|
+
values: [6, 5, 4, 3, 2, 1],
|
|
920
|
+
},
|
|
921
|
+
{
|
|
922
|
+
label: "Flat",
|
|
923
|
+
tone: "info",
|
|
924
|
+
values: [4, 4, 4, 4, 4, 4],
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
label: "Negative",
|
|
928
|
+
tone: "warning",
|
|
929
|
+
values: [-6, -4, -5, -2, -3, -1],
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
label: "Narrow mixed",
|
|
933
|
+
tone: "info",
|
|
934
|
+
values: [0, 10, -5, 5, 2, 8, -2],
|
|
935
|
+
width: 4,
|
|
936
|
+
},
|
|
937
|
+
];
|
|
938
|
+
|
|
939
|
+
return examples
|
|
940
|
+
.map((example) =>
|
|
941
|
+
sparkline({
|
|
942
|
+
...options,
|
|
943
|
+
...example,
|
|
944
|
+
width: example.width ?? 16,
|
|
945
|
+
}),
|
|
946
|
+
)
|
|
947
|
+
.join("\n");
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Render static loading, success, and fail frames for the spinner primitive.
|
|
952
|
+
*
|
|
953
|
+
* The gallery shows fixed snapshots rather than a live animation, so no timer is started.
|
|
954
|
+
*
|
|
955
|
+
* @param {object} options
|
|
956
|
+
* Rendering options.
|
|
957
|
+
* @returns {string}
|
|
958
|
+
* Spinner state examples.
|
|
959
|
+
*/
|
|
960
|
+
function renderSpinnerExample(options) {
|
|
961
|
+
const label = "Running task";
|
|
962
|
+
|
|
963
|
+
return [
|
|
964
|
+
renderSpinnerFrame(label, 0, options),
|
|
965
|
+
status(resultTypes.SUCCESS, "", { ...options, label }),
|
|
966
|
+
status(resultTypes.FAILED, "", { ...options, label }),
|
|
967
|
+
].join("\n");
|
|
968
|
+
}
|
|
969
|
+
|
|
809
970
|
/**
|
|
810
971
|
* Render common progress values and tones.
|
|
811
972
|
*
|
package/src/index.js
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
export { createCliStyle, profiles } from "./create-cli-style.js";
|
|
2
2
|
|
|
3
3
|
export { resolveProfile } from "./capability/resolve-profile.js";
|
|
4
|
-
export { resolveTerminalCapabilities } from "./capability/terminal.js";
|
|
4
|
+
export { resolveTerminalCapabilities, resolveTheme } from "./capability/terminal.js";
|
|
5
5
|
export { isProfile } from "./profiles/profiles.js";
|
|
6
6
|
|
|
7
7
|
export { renderHelp } from "./cli/help.js";
|
|
8
|
+
export { parseCatalogueRequest, renderCatalogue } from "./cli/catalogue-command.js";
|
|
9
|
+
export {
|
|
10
|
+
getRendererMetadata,
|
|
11
|
+
rendererCatalogue,
|
|
12
|
+
rendererNames,
|
|
13
|
+
} from "./catalogue/renderer-catalogue.js";
|
|
14
|
+
|
|
8
15
|
export {
|
|
9
16
|
adapterNames,
|
|
10
17
|
parseAdapterPathRequest,
|
|
11
18
|
renderAdapterPath,
|
|
12
19
|
} from "./cli/adapter-path-command.js";
|
|
13
|
-
|
|
20
|
+
|
|
21
|
+
export { parseRenderRequest, renderJsonInput } from "./cli/render-command.js";
|
|
22
|
+
|
|
14
23
|
export {
|
|
15
24
|
galleryFixtures,
|
|
16
25
|
gallerySections,
|
|
@@ -20,12 +29,14 @@ export {
|
|
|
20
29
|
|
|
21
30
|
export { background, foreground, stripAnsi, style } from "./formatters/ansi.js";
|
|
22
31
|
export { defaultWidth, minimumWidth, normaliseWidth } from "./formatters/width.js";
|
|
32
|
+
export { renderSpinnerFrame, runSpinner, spinner } from "./interactive/spinner.js";
|
|
23
33
|
export { agentTranscript } from "./patterns/agent-transcript.js";
|
|
24
34
|
export { auditFinding } from "./patterns/audit-finding.js";
|
|
25
35
|
export { commandResult } from "./patterns/command-result.js";
|
|
26
36
|
export { compactDataTable } from "./patterns/compact-data-table.js";
|
|
27
37
|
export { confirmationResult } from "./patterns/confirmation-result.js";
|
|
28
38
|
export { diagnosticReport } from "./patterns/diagnostic-report.js";
|
|
39
|
+
export { diffBlock } from "./patterns/diff-block.js";
|
|
29
40
|
export { nextStepBlock } from "./patterns/next-step-block.js";
|
|
30
41
|
export { taskSummary } from "./patterns/task-summary.js";
|
|
31
42
|
export { barChart } from "./primitives/bar-chart.js";
|
|
@@ -36,10 +47,12 @@ export { panel } from "./primitives/panel.js";
|
|
|
36
47
|
export { progressBar } from "./primitives/progress-bar.js";
|
|
37
48
|
export { row } from "./primitives/row.js";
|
|
38
49
|
export { rowGroup } from "./primitives/row-group.js";
|
|
50
|
+
export { sparkline } from "./primitives/sparkline.js";
|
|
39
51
|
export { span } from "./primitives/span.js";
|
|
40
52
|
export { status } from "./primitives/status.js";
|
|
41
53
|
export { step, stepProgress, stepStates } from "./primitives/step-progress.js";
|
|
42
54
|
export { table } from "./primitives/table.js";
|
|
55
|
+
|
|
43
56
|
export {
|
|
44
57
|
createReporter,
|
|
45
58
|
renderGroup,
|
|
@@ -47,10 +60,12 @@ export {
|
|
|
47
60
|
renderReporterStatus,
|
|
48
61
|
renderSection,
|
|
49
62
|
} from "./reporters/create-reporter.js";
|
|
63
|
+
|
|
50
64
|
export {
|
|
51
65
|
chartColours,
|
|
52
66
|
chipColours,
|
|
53
67
|
colourTokens,
|
|
68
|
+
diffColours,
|
|
54
69
|
getColourToken,
|
|
55
70
|
getToneColour,
|
|
56
71
|
panelColours,
|
|
@@ -58,8 +73,10 @@ export {
|
|
|
58
73
|
resolveColourValue,
|
|
59
74
|
tableColours,
|
|
60
75
|
terminalColours,
|
|
76
|
+
themes,
|
|
61
77
|
toneColours,
|
|
62
78
|
} from "./theme/colours.js";
|
|
79
|
+
|
|
63
80
|
export {
|
|
64
81
|
getHighestSeverityResult,
|
|
65
82
|
getResultSymbol,
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { foreground } from "../formatters/ansi.js";
|
|
2
|
+
import { profiles } from "../profiles/profiles.js";
|
|
3
|
+
import { status } from "../primitives/status.js";
|
|
4
|
+
import { resultTypes } from "../theme/results.js";
|
|
5
|
+
|
|
6
|
+
// Frame glyphs cycled while a spinner is animating.
|
|
7
|
+
const unicodeFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
8
|
+
const asciiFrames = ["-", "\\", "|", "/"];
|
|
9
|
+
|
|
10
|
+
// Time between animated frame redraws.
|
|
11
|
+
const frameIntervalMs = 80;
|
|
12
|
+
|
|
13
|
+
// Built from a character code, rather than a string escape, so the control byte stays visible in diffs.
|
|
14
|
+
const escapeCharacter = String.fromCharCode(27);
|
|
15
|
+
|
|
16
|
+
// ANSI cursor and line-clear sequences used only on an interactive TTY.
|
|
17
|
+
const hideCursor = `${escapeCharacter}[?25l`;
|
|
18
|
+
const showCursor = `${escapeCharacter}[?25h`;
|
|
19
|
+
const clearLine = `\r${escapeCharacter}[2K`;
|
|
20
|
+
|
|
21
|
+
// Profiles that always use the static, non-animated spinner output.
|
|
22
|
+
const nonInteractiveProfiles = new Set([
|
|
23
|
+
profiles.AGENT,
|
|
24
|
+
profiles.CI,
|
|
25
|
+
profiles.JSON,
|
|
26
|
+
profiles.PLAIN,
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
// Spinner currently writing output; only one may be active at a time.
|
|
30
|
+
let runningSpinner = null;
|
|
31
|
+
|
|
32
|
+
// Whether the process-exit cleanup listener has been registered.
|
|
33
|
+
let cleanupRegistered = false;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Start a spinner and return a manual handle for advanced lifecycle control.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} text
|
|
39
|
+
* Initial spinner text.
|
|
40
|
+
* @param {object} options
|
|
41
|
+
* Rendering and capability options.
|
|
42
|
+
* @param {boolean} options.colour
|
|
43
|
+
* Whether ANSI colour should be applied.
|
|
44
|
+
* @param {boolean} options.isTty
|
|
45
|
+
* Whether stdout is an interactive terminal.
|
|
46
|
+
* @param {string} options.profile
|
|
47
|
+
* Resolved output profile.
|
|
48
|
+
* @param {object} options.stream
|
|
49
|
+
* Writable stream the spinner writes to. Defaults to `process.stderr`.
|
|
50
|
+
* @param {boolean} options.unicode
|
|
51
|
+
* Whether Unicode symbols should be used.
|
|
52
|
+
* @returns {object}
|
|
53
|
+
* Handle with `update`, `succeed`, `fail`, and `stop` methods.
|
|
54
|
+
*/
|
|
55
|
+
export function spinner(text, options = {}) {
|
|
56
|
+
if (runningSpinner !== null) {
|
|
57
|
+
throw new Error("A spinner is already active; stop it before starting another.");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const state = {
|
|
61
|
+
active: true,
|
|
62
|
+
frameIndex: 0,
|
|
63
|
+
interactive: isInteractive(options),
|
|
64
|
+
options,
|
|
65
|
+
stream: options.stream ?? process.stderr,
|
|
66
|
+
text,
|
|
67
|
+
timer: null,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
runningSpinner = state;
|
|
71
|
+
registerCleanup();
|
|
72
|
+
startSpinnerState(state);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
fail: (finalText) => finishSpinnerState(state, resultTypes.FAILED, finalText),
|
|
76
|
+
stop: () => stopSpinnerState(state),
|
|
77
|
+
succeed: (finalText) => finishSpinnerState(state, resultTypes.SUCCESS, finalText),
|
|
78
|
+
update: (nextText) => updateSpinnerState(state, nextText),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Run an async task behind a spinner, resolving or rejecting it unchanged.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} text
|
|
86
|
+
* Spinner text shown while the task runs.
|
|
87
|
+
* @param {Function} fn
|
|
88
|
+
* Async task to run.
|
|
89
|
+
* @param {object} options
|
|
90
|
+
* Rendering and capability options, forwarded to `spinner()`.
|
|
91
|
+
* @returns {Promise<*>}
|
|
92
|
+
* Resolved value of `fn`, or its rejection rethrown after cleanup.
|
|
93
|
+
*/
|
|
94
|
+
export async function runSpinner(text, fn, options = {}) {
|
|
95
|
+
const handle = spinner(text, options);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const result = await fn();
|
|
99
|
+
|
|
100
|
+
handle.succeed();
|
|
101
|
+
|
|
102
|
+
return result;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
handle.fail();
|
|
105
|
+
|
|
106
|
+
throw error;
|
|
107
|
+
} finally {
|
|
108
|
+
handle.stop();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Render one animated spinner frame without starting a timer.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} text
|
|
116
|
+
* Spinner text to show alongside the frame glyph.
|
|
117
|
+
* @param {number} frameIndex
|
|
118
|
+
* Frame position to render.
|
|
119
|
+
* @param {object} options
|
|
120
|
+
* Rendering options.
|
|
121
|
+
* @param {boolean} options.colour
|
|
122
|
+
* Whether ANSI colour should be applied.
|
|
123
|
+
* @param {boolean} options.unicode
|
|
124
|
+
* Whether Unicode symbols should be used.
|
|
125
|
+
* @returns {string}
|
|
126
|
+
* Rendered frame line.
|
|
127
|
+
*/
|
|
128
|
+
export function renderSpinnerFrame(text, frameIndex, options = {}) {
|
|
129
|
+
const frames = options.unicode === false ? asciiFrames : unicodeFrames;
|
|
130
|
+
const glyph = frames[frameIndex % frames.length];
|
|
131
|
+
const colouredGlyph = foreground(glyph, "info", options);
|
|
132
|
+
|
|
133
|
+
return `${colouredGlyph} ${text}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Determine whether a spinner should animate on the current output target.
|
|
138
|
+
*
|
|
139
|
+
* @param {object} options
|
|
140
|
+
* Rendering and capability options.
|
|
141
|
+
* @returns {boolean}
|
|
142
|
+
* Whether the spinner can animate.
|
|
143
|
+
*/
|
|
144
|
+
function isInteractive(options) {
|
|
145
|
+
return options.isTty === true && !nonInteractiveProfiles.has(options.profile);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Write the initial spinner output for a state, starting the frame timer when interactive.
|
|
150
|
+
*
|
|
151
|
+
* @param {object} state
|
|
152
|
+
* Spinner state to start.
|
|
153
|
+
* @returns {void}
|
|
154
|
+
*/
|
|
155
|
+
function startSpinnerState(state) {
|
|
156
|
+
if (!state.interactive) {
|
|
157
|
+
state.stream.write(
|
|
158
|
+
`${status(resultTypes.INFO, "", { ...state.options, label: state.text })}\n`,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
state.stream.write(hideCursor);
|
|
165
|
+
renderFrame(state);
|
|
166
|
+
|
|
167
|
+
state.timer = setInterval(() => tickSpinnerState(state), frameIntervalMs);
|
|
168
|
+
state.timer.unref?.();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Advance and redraw the animated frame for a state.
|
|
173
|
+
*
|
|
174
|
+
* @param {object} state
|
|
175
|
+
* Spinner state to advance.
|
|
176
|
+
* @returns {void}
|
|
177
|
+
*/
|
|
178
|
+
function tickSpinnerState(state) {
|
|
179
|
+
state.frameIndex += 1;
|
|
180
|
+
|
|
181
|
+
renderFrame(state);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Redraw the current animated frame in place.
|
|
186
|
+
*
|
|
187
|
+
* @param {object} state
|
|
188
|
+
* Spinner state to render.
|
|
189
|
+
* @returns {void}
|
|
190
|
+
*/
|
|
191
|
+
function renderFrame(state) {
|
|
192
|
+
state.stream.write(
|
|
193
|
+
`${clearLine}${renderSpinnerFrame(state.text, state.frameIndex, state.options)}`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Update the text shown by an active spinner.
|
|
199
|
+
*
|
|
200
|
+
* @param {object} state
|
|
201
|
+
* Spinner state to update.
|
|
202
|
+
* @param {string} nextText
|
|
203
|
+
* Replacement spinner text.
|
|
204
|
+
* @returns {void}
|
|
205
|
+
*/
|
|
206
|
+
function updateSpinnerState(state, nextText) {
|
|
207
|
+
if (!state.active) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
state.text = nextText;
|
|
212
|
+
|
|
213
|
+
if (state.interactive) {
|
|
214
|
+
renderFrame(state);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Stop a spinner without printing a result line.
|
|
220
|
+
*
|
|
221
|
+
* @param {object} state
|
|
222
|
+
* Spinner state to stop.
|
|
223
|
+
* @returns {void}
|
|
224
|
+
*/
|
|
225
|
+
function stopSpinnerState(state) {
|
|
226
|
+
if (!state.active) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
state.active = false;
|
|
231
|
+
clearSpinnerTimer(state);
|
|
232
|
+
|
|
233
|
+
if (state.interactive) {
|
|
234
|
+
state.stream.write(`${clearLine}${showCursor}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (runningSpinner === state) {
|
|
238
|
+
runningSpinner = null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Stop a spinner and print a success or fail result line.
|
|
244
|
+
*
|
|
245
|
+
* @param {object} state
|
|
246
|
+
* Spinner state to finish.
|
|
247
|
+
* @param {string} resultType
|
|
248
|
+
* Result type to display, from `resultTypes`.
|
|
249
|
+
* @param {string} finalText
|
|
250
|
+
* Optional replacement text for the result line.
|
|
251
|
+
* @returns {void}
|
|
252
|
+
*/
|
|
253
|
+
function finishSpinnerState(state, resultType, finalText) {
|
|
254
|
+
if (!state.active) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
state.active = false;
|
|
259
|
+
clearSpinnerTimer(state);
|
|
260
|
+
|
|
261
|
+
const label = finalText ?? state.text;
|
|
262
|
+
const line = status(resultType, "", { ...state.options, label });
|
|
263
|
+
|
|
264
|
+
if (state.interactive) {
|
|
265
|
+
state.stream.write(`${clearLine}${showCursor}${line}\n`);
|
|
266
|
+
} else {
|
|
267
|
+
state.stream.write(`${line}\n`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (runningSpinner === state) {
|
|
271
|
+
runningSpinner = null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Clear an active state's frame timer, when one exists.
|
|
277
|
+
*
|
|
278
|
+
* @param {object} state
|
|
279
|
+
* Spinner state to clear.
|
|
280
|
+
* @returns {void}
|
|
281
|
+
*/
|
|
282
|
+
function clearSpinnerTimer(state) {
|
|
283
|
+
if (state.timer !== null) {
|
|
284
|
+
clearInterval(state.timer);
|
|
285
|
+
state.timer = null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Restore the cursor for an active, interactive spinner, stop its frame timer, and clear the
|
|
291
|
+
* running reference.
|
|
292
|
+
*
|
|
293
|
+
* @returns {void}
|
|
294
|
+
*/
|
|
295
|
+
function restoreRunningSpinner() {
|
|
296
|
+
if (runningSpinner !== null) {
|
|
297
|
+
clearSpinnerTimer(runningSpinner);
|
|
298
|
+
|
|
299
|
+
if (runningSpinner.interactive) {
|
|
300
|
+
runningSpinner.stream.write(`${clearLine}${showCursor}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
runningSpinner = null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Restore the cursor on SIGINT, then re-raise the signal so the process still terminates with
|
|
309
|
+
* Node's default SIGINT exit code. A listener that did not re-raise would silently replace that
|
|
310
|
+
* default termination, leaving the process running whenever other work still holds the event loop
|
|
311
|
+
* open.
|
|
312
|
+
*
|
|
313
|
+
* @returns {void}
|
|
314
|
+
*/
|
|
315
|
+
function handleSpinnerSigint() {
|
|
316
|
+
restoreRunningSpinner();
|
|
317
|
+
process.removeListener("SIGINT", handleSpinnerSigint);
|
|
318
|
+
process.kill(process.pid, "SIGINT");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Register process-exit and SIGINT listeners, once, that restore the cursor for an interrupted spinner.
|
|
323
|
+
*
|
|
324
|
+
* @returns {void}
|
|
325
|
+
*/
|
|
326
|
+
function registerCleanup() {
|
|
327
|
+
if (cleanupRegistered) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
cleanupRegistered = true;
|
|
332
|
+
|
|
333
|
+
process.on("exit", restoreRunningSpinner);
|
|
334
|
+
process.on("SIGINT", handleSpinnerSigint);
|
|
335
|
+
}
|