diff --git a/.changeset/odd-bikes-nail.md b/.changeset/odd-bikes-nail.md new file mode 100644 index 00000000..349e6ede --- /dev/null +++ b/.changeset/odd-bikes-nail.md @@ -0,0 +1,27 @@ +--- +"@clack/prompts": minor +--- + +Updates the API for stopping spinners and progress bars to be clearer + +Previously, both the spinner and progress bar components used a single `stop` method that accepted a code to indicate success, cancellation, or error. This update separates these into distinct methods: `stop()`, `cancel()`, and `error()`: + +```diff +const spinner = prompts.spinner(); +spinner.start(); + +// Cancelling a spinner +- spinner.stop(undefined, 1); ++ spinner.cancel(); + +// Stopping with an error +- spinner.stop(undefined, 2); ++ spinner.error(); +``` + +As before, you can pass a message to each method to customize the output displayed: + +```js +spinner.cancel("Operation cancelled by user"); +progressBar.error("An error occurred during processing"); +``` diff --git a/packages/prompts/src/progress-bar.ts b/packages/prompts/src/progress-bar.ts index 8801833c..88595141 100644 --- a/packages/prompts/src/progress-bar.ts +++ b/packages/prompts/src/progress-bar.ts @@ -63,6 +63,8 @@ export function progress({ return { start, stop: spin.stop, + cancel: spin.cancel, + error: spin.error, advance, isCancelled: spin.isCancelled, message: (msg: string) => advance(0, msg), diff --git a/packages/prompts/src/spinner.ts b/packages/prompts/src/spinner.ts index 498a1f56..65dd591e 100644 --- a/packages/prompts/src/spinner.ts +++ b/packages/prompts/src/spinner.ts @@ -24,7 +24,9 @@ export interface SpinnerOptions extends CommonOptions { export interface SpinnerResult { start(msg?: string): void; - stop(msg?: string, code?: number): void; + stop(msg?: string): void; + cancel(msg?: string): void; + error(msg?: string): void; message(msg?: string): void; readonly isCancelled: boolean; } @@ -61,7 +63,7 @@ export const spinner = ({ : (cancelMessage ?? settings.messages.cancel); isCancelled = code === 1; if (isSpinnerActive) { - stop(msg, code); + _stop(msg, code); if (isCancelled && typeof onCancel === 'function') { onCancel(); } @@ -163,7 +165,7 @@ export const spinner = ({ }, delay); }; - const stop = (msg = '', code = 0): void => { + const _stop = (msg = '', code = 0): void => { if (!isSpinnerActive) return; isSpinnerActive = false; clearInterval(loop); @@ -184,6 +186,10 @@ export const spinner = ({ unblock(); }; + const stop = (msg = ''): void => _stop(msg, 0); + const cancel = (msg = ''): void => _stop(msg, 1); + const error = (msg = ''): void => _stop(msg, 2); + const message = (msg = ''): void => { _message = removeTrailingDots(msg ?? _message); }; @@ -192,6 +198,8 @@ export const spinner = ({ start, stop, message, + cancel, + error, get isCancelled() { return isCancelled; }, diff --git a/packages/prompts/test/__snapshots__/progress-bar.test.ts.snap b/packages/prompts/test/__snapshots__/progress-bar.test.ts.snap index 72508fbc..8296f517 100644 --- a/packages/prompts/test/__snapshots__/progress-bar.test.ts.snap +++ b/packages/prompts/test/__snapshots__/progress-bar.test.ts.snap @@ -125,7 +125,7 @@ exports[`prompts - progress (isCI = false) > start > renders timer when indicato ] `; -exports[`prompts - progress (isCI = false) > stop > renders cancel symbol if code = 1 1`] = ` +exports[`prompts - progress (isCI = false) > stop > renders cancel symbol when calling cancel() 1`] = ` [ "", "│ @@ -139,7 +139,7 @@ exports[`prompts - progress (isCI = false) > stop > renders cancel symbol if cod ] `; -exports[`prompts - progress (isCI = false) > stop > renders error symbol if code > 1 1`] = ` +exports[`prompts - progress (isCI = false) > stop > renders error symbol when calling error() 1`] = ` [ "", "│ @@ -167,6 +167,34 @@ exports[`prompts - progress (isCI = false) > stop > renders message 1`] = ` ] `; +exports[`prompts - progress (isCI = false) > stop > renders message when cancelling 1`] = ` +[ + "", + "│ +", + "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ", + "", + "", + "■ cancelled :-( +", + "", +] +`; + +exports[`prompts - progress (isCI = false) > stop > renders message when erroring 1`] = ` +[ + "", + "│ +", + "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ", + "", + "", + "▲ FATAL ERROR! +", + "", +] +`; + exports[`prompts - progress (isCI = false) > stop > renders message without removing dots 1`] = ` [ "", @@ -382,7 +410,7 @@ exports[`prompts - progress (isCI = true) > start > renders timer when indicator ] `; -exports[`prompts - progress (isCI = true) > stop > renders cancel symbol if code = 1 1`] = ` +exports[`prompts - progress (isCI = true) > stop > renders cancel symbol when calling cancel() 1`] = ` [ "", "│ @@ -398,7 +426,7 @@ exports[`prompts - progress (isCI = true) > stop > renders cancel symbol if code ] `; -exports[`prompts - progress (isCI = true) > stop > renders error symbol if code > 1 1`] = ` +exports[`prompts - progress (isCI = true) > stop > renders error symbol when calling error() 1`] = ` [ "", "│ @@ -430,6 +458,38 @@ exports[`prompts - progress (isCI = true) > stop > renders message 1`] = ` ] `; +exports[`prompts - progress (isCI = true) > stop > renders message when cancelling 1`] = ` +[ + "", + "│ +", + "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ...", + " +", + "", + "", + "■ cancelled :-( +", + "", +] +`; + +exports[`prompts - progress (isCI = true) > stop > renders message when erroring 1`] = ` +[ + "", + "│ +", + "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ...", + " +", + "", + "", + "▲ FATAL ERROR! +", + "", +] +`; + exports[`prompts - progress (isCI = true) > stop > renders message without removing dots 1`] = ` [ "", diff --git a/packages/prompts/test/__snapshots__/spinner.test.ts.snap b/packages/prompts/test/__snapshots__/spinner.test.ts.snap index 62a94c3f..3ee9e291 100644 --- a/packages/prompts/test/__snapshots__/spinner.test.ts.snap +++ b/packages/prompts/test/__snapshots__/spinner.test.ts.snap @@ -461,7 +461,7 @@ exports[`spinner (isCI = false) > start > renders timer when indicator is "timer ] `; -exports[`spinner (isCI = false) > stop > renders cancel symbol if code = 1 1`] = ` +exports[`spinner (isCI = false) > stop > renders cancel symbol when calling cancel() 1`] = ` [ "", "│ @@ -475,7 +475,7 @@ exports[`spinner (isCI = false) > stop > renders cancel symbol if code = 1 1`] = ] `; -exports[`spinner (isCI = false) > stop > renders error symbol if code > 1 1`] = ` +exports[`spinner (isCI = false) > stop > renders error symbol when calling error() 1`] = ` [ "", "│ @@ -503,6 +503,34 @@ exports[`spinner (isCI = false) > stop > renders message 1`] = ` ] `; +exports[`spinner (isCI = false) > stop > renders message when cancelling 1`] = ` +[ + "", + "│ +", + "◒ ", + "", + "", + "■ too dizzy — spinning cancelled +", + "", +] +`; + +exports[`spinner (isCI = false) > stop > renders message when erroring 1`] = ` +[ + "", + "│ +", + "◒ ", + "", + "", + "▲ error: spun too fast! +", + "", +] +`; + exports[`spinner (isCI = false) > stop > renders message without removing dots 1`] = ` [ "", @@ -789,7 +817,7 @@ exports[`spinner (isCI = true) > start > renders timer when indicator is "timer" ] `; -exports[`spinner (isCI = true) > stop > renders cancel symbol if code = 1 1`] = ` +exports[`spinner (isCI = true) > stop > renders cancel symbol when calling cancel() 1`] = ` [ "", "│ @@ -805,7 +833,7 @@ exports[`spinner (isCI = true) > stop > renders cancel symbol if code = 1 1`] = ] `; -exports[`spinner (isCI = true) > stop > renders error symbol if code > 1 1`] = ` +exports[`spinner (isCI = true) > stop > renders error symbol when calling error() 1`] = ` [ "", "│ @@ -837,6 +865,38 @@ exports[`spinner (isCI = true) > stop > renders message 1`] = ` ] `; +exports[`spinner (isCI = true) > stop > renders message when cancelling 1`] = ` +[ + "", + "│ +", + "◒ ...", + " +", + "", + "", + "■ too dizzy — spinning cancelled +", + "", +] +`; + +exports[`spinner (isCI = true) > stop > renders message when erroring 1`] = ` +[ + "", + "│ +", + "◒ ...", + " +", + "", + "", + "▲ error: spun too fast! +", + "", +] +`; + exports[`spinner (isCI = true) > stop > renders message without removing dots 1`] = ` [ "", diff --git a/packages/prompts/test/progress-bar.test.ts b/packages/prompts/test/progress-bar.test.ts index 007b9ce7..a64061c8 100644 --- a/packages/prompts/test/progress-bar.test.ts +++ b/packages/prompts/test/progress-bar.test.ts @@ -87,26 +87,26 @@ describe.each(['true', 'false'])('prompts - progress (isCI = %s)', (isCI) => { expect(output.buffer).toMatchSnapshot(); }); - test('renders cancel symbol if code = 1', () => { + test('renders cancel symbol when calling cancel()', () => { const result = prompts.progress({ output }); result.start(); vi.advanceTimersByTime(80); - result.stop('', 1); + result.cancel(); expect(output.buffer).toMatchSnapshot(); }); - test('renders error symbol if code > 1', () => { + test('renders error symbol when calling error()', () => { const result = prompts.progress({ output }); result.start(); vi.advanceTimersByTime(80); - result.stop('', 2); + result.error(); expect(output.buffer).toMatchSnapshot(); }); @@ -134,6 +134,30 @@ describe.each(['true', 'false'])('prompts - progress (isCI = %s)', (isCI) => { expect(output.buffer).toMatchSnapshot(); }); + + test('renders message when cancelling', () => { + const result = prompts.progress({ output }); + + result.start(); + + vi.advanceTimersByTime(80); + + result.cancel('cancelled :-('); + + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders message when erroring', () => { + const result = prompts.progress({ output }); + + result.start(); + + vi.advanceTimersByTime(80); + + result.error('FATAL ERROR!'); + + expect(output.buffer).toMatchSnapshot(); + }); }); describe('message', () => { diff --git a/packages/prompts/test/spinner.test.ts b/packages/prompts/test/spinner.test.ts index 63ecc533..d0f771cc 100644 --- a/packages/prompts/test/spinner.test.ts +++ b/packages/prompts/test/spinner.test.ts @@ -117,26 +117,26 @@ describe.each(['true', 'false'])('spinner (isCI = %s)', (isCI) => { expect(output.buffer).toMatchSnapshot(); }); - test('renders cancel symbol if code = 1', () => { + test('renders cancel symbol when calling cancel()', () => { const result = prompts.spinner({ output }); result.start(); vi.advanceTimersByTime(80); - result.stop('', 1); + result.cancel(); expect(output.buffer).toMatchSnapshot(); }); - test('renders error symbol if code > 1', () => { + test('renders error symbol when calling error()', () => { const result = prompts.spinner({ output }); result.start(); vi.advanceTimersByTime(80); - result.stop('', 2); + result.error(); expect(output.buffer).toMatchSnapshot(); }); @@ -165,6 +165,30 @@ describe.each(['true', 'false'])('spinner (isCI = %s)', (isCI) => { expect(output.buffer).toMatchSnapshot(); }); + test('renders message when cancelling', () => { + const result = prompts.spinner({ output }); + + result.start(); + + vi.advanceTimersByTime(80); + + result.cancel('too dizzy — spinning cancelled'); + + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders message when erroring', () => { + const result = prompts.spinner({ output }); + + result.start(); + + vi.advanceTimersByTime(80); + + result.error('error: spun too fast!'); + + expect(output.buffer).toMatchSnapshot(); + }); + test('does not throw if called before start', () => { const result = prompts.spinner({ output });