diff --git a/examples/basic/spinnerGroup.ts b/examples/basic/spinnerGroup.ts new file mode 100644 index 00000000..e64ff8a2 --- /dev/null +++ b/examples/basic/spinnerGroup.ts @@ -0,0 +1,34 @@ +import * as p from '@clack/prompts'; + +p.intro('spinner groups start...'); + +const s = p.spinner(); +s.start('example start'); +await new Promise((resolve) => setTimeout(resolve, 500)); +s.stop('example stopped'); + +await p.spinnerGroup('Outer group', [ + [ + 'First sub-task', + async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + }, + ], + [ + 'Second sub-task', + async () => { + if (process.env.THROW_ERROR) { + throw new Error(process.env.THROW_ERROR); + } + await new Promise((resolve) => setTimeout(resolve, 1000)); + }, + ], + [ + 'Third sub-task', + async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + }, + ], +]); + +p.outro('spinner group stop...'); diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index 81c32736..57b950cd 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -1,3 +1,4 @@ +import readline from 'node:readline'; import { stripVTControlCharacters as strip } from 'node:util'; import { ConfirmPrompt, @@ -641,10 +642,14 @@ export const log = { }, }; +function getIsCI() { + return process.env.CI === 'true'; +} + export const spinner = () => { const frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•', 'o', 'O', '0']; const delay = unicode ? 80 : 120; - const isCI = process.env.CI === 'true'; + const isCI = getIsCI(); let unblock: () => void; let loop: NodeJS.Timeout; @@ -717,14 +722,8 @@ export const spinner = () => { isSpinnerActive = false; clearInterval(loop); clearPrevMessage(); - const step = - code === 0 - ? color.green(S_STEP_SUBMIT) - : code === 1 - ? color.red(S_STEP_CANCEL) - : color.red(S_STEP_ERROR); _message = parseMessage(msg ?? _message); - process.stdout.write(`${step} ${_message}\n`); + process.stdout.write(`${stepForCode(code)} ${_message}\n`); clearHooks(); unblock(); }; @@ -740,6 +739,70 @@ export const spinner = () => { }; }; +function stepForCode(code: number) { + return code === 0 + ? color.green(S_STEP_SUBMIT) + : code === 1 + ? color.red(S_STEP_CANCEL) + : color.red(S_STEP_ERROR); +} + +export type SpinnerGroup = [message: string, run: () => Promise]; + +export const spinnerGroup = async (outerMessage: string, groups: SpinnerGroup[]) => { + process.stdout.write(`${color.gray(S_BAR)}\n ${S_BAR_START} ${outerMessage}\n`); + + const lines = getIsCI() + ? { + clearLine: () => {}, + clearScreenDown: () => {}, + moveCursor: () => {}, + } + : readline; + + const s = spinner(); + let caught: [group: SpinnerGroup, error: unknown] | undefined; + + for (const [message, run] of groups) { + const line = `${color.gray(S_BAR)} ${message}`; + + lines.clearLine(process.stdout, -1); + lines.moveCursor(process.stdout, -999, -1); + + s.start(line); + await run().catch((error) => { + caught = [[message, run], error]; + }); + s.stop(line); + + if (caught) { + break; + } + } + + if (caught) { + lines.moveCursor(process.stdout, -999, -1); + process.stdout.write( + [ + color.gray(S_BAR), + '', + stepForCode(1), + caught[0][0], + color.gray('>'), + (caught[1] as any).message || caught[1], + ].join(' ') + ); + } else { + lines.moveCursor(process.stdout, -999, -groups.length - 1); + lines.clearScreenDown(process.stdout); + process.stdout.write(`${stepForCode(0)} ${outerMessage}`); + } + + process.stdout.write('\n'); + + return caught; +}; + export type PromptGroupAwaitedReturn = { [P in keyof T]: Exclude, symbol>; };