diff --git a/.changeset/bright-chefs-double.md b/.changeset/bright-chefs-double.md new file mode 100644 index 00000000..e795167a --- /dev/null +++ b/.changeset/bright-chefs-double.md @@ -0,0 +1,5 @@ +--- +'@clack/prompts': minor +--- + +spinner timer progress indicator diff --git a/examples/basic/package.json b/examples/basic/package.json index d23f29c2..64397d80 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -11,7 +11,8 @@ "scripts": { "start": "jiti ./index.ts", "spinner": "jiti ./spinner.ts", - "spinner-ci": "npx cross-env CI=\"true\" jiti ./spinner-ci.ts" + "spinner-ci": "npx cross-env CI=\"true\" jiti ./spinner-ci.ts", + "spinner-timer": "jiti ./spinner-timer.ts" }, "devDependencies": { "jiti": "^1.17.0" diff --git a/examples/basic/spinner-timer.ts b/examples/basic/spinner-timer.ts new file mode 100644 index 00000000..ce36e2ec --- /dev/null +++ b/examples/basic/spinner-timer.ts @@ -0,0 +1,26 @@ +import * as p from '@clack/prompts'; + +p.intro('spinner start...'); + +async function main() { + const spin = p.spinner({ indicator: 'timer' }); + + spin.start('First spinner'); + + await sleep(3_000); + + spin.stop('Done first spinner'); + + spin.start('Second spinner'); + await sleep(5_000); + + spin.stop('Done second spinner'); + + p.outro('spinner stop.'); +} + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +main(); diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index 2c152845..222da9a1 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -675,7 +675,11 @@ export const log = { }, }; -export const spinner = () => { +export interface SpinnerOptions { + indicator?: 'dots' | 'timer'; +} + +export const spinner = ({ indicator = 'dots' }: SpinnerOptions = {}) => { const frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•', 'o', 'O', '0']; const delay = unicode ? 80 : 120; const isCI = process.env.CI === 'true'; @@ -685,6 +689,7 @@ export const spinner = () => { let isSpinnerActive = false; let _message = ''; let _prevMessage: string | undefined = undefined; + let _origin: number = performance.now(); const handleExit = (code: number) => { const msg = code > 1 ? 'Something went wrong' : 'Canceled'; @@ -725,13 +730,21 @@ export const spinner = () => { return msg.replace(/\.+$/, ''); }; + const formatTimer = (origin: number): string => { + const duration = (performance.now() - origin) / 1000; + const min = Math.floor(duration / 60); + const secs = Math.floor(duration % 60); + return min > 0 ? `[${min}m ${secs}s]` : `[${secs}s]`; + }; + const start = (msg = ''): void => { isSpinnerActive = true; unblock = block(); _message = parseMessage(msg); + _origin = performance.now(); process.stdout.write(`${color.gray(S_BAR)}\n`); let frameIndex = 0; - let dotsTimer = 0; + let indicatorTimer = 0; registerHooks(); loop = setInterval(() => { if (isCI && _message === _prevMessage) { @@ -740,10 +753,18 @@ export const spinner = () => { clearPrevMessage(); _prevMessage = _message; const frame = color.magenta(frames[frameIndex]); - const loadingDots = isCI ? '...' : '.'.repeat(Math.floor(dotsTimer)).slice(0, 3); - process.stdout.write(`${frame} ${_message}${loadingDots}`); + + if (isCI) { + process.stdout.write(`${frame} ${_message}...`); + } else if (indicator === 'timer') { + process.stdout.write(`${frame} ${_message} ${formatTimer(_origin)}`); + } else { + const loadingDots = '.'.repeat(Math.floor(indicatorTimer)).slice(0, 3); + process.stdout.write(`${frame} ${_message}${loadingDots}`); + } + frameIndex = frameIndex + 1 < frames.length ? frameIndex + 1 : 0; - dotsTimer = dotsTimer < frames.length ? dotsTimer + 0.125 : 0; + indicatorTimer = indicatorTimer < frames.length ? indicatorTimer + 0.125 : 0; }, delay); }; @@ -758,7 +779,11 @@ export const spinner = () => { ? color.red(S_STEP_CANCEL) : color.red(S_STEP_ERROR); _message = parseMessage(msg ?? _message); - process.stdout.write(`${step} ${_message}\n`); + if (indicator === 'timer') { + process.stdout.write(`${step} ${_message} ${formatTimer(_origin)}\n`); + } else { + process.stdout.write(`${step} ${_message}\n`); + } clearHooks(); unblock(); };