@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.
@@ -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
- // Pattern fixtures available for focused output and interactive search.
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(renderPatterns(resolvedVariant.options, request.fixture));
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
- export { parseRenderRequest, rendererNames, renderJsonInput } from "./cli/render-command.js";
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
+ }