Skip to content

Commit 613179d

Browse files
feat: timer indicator for spinner (#230)
Co-authored-by: Nate Moore <[email protected]>
1 parent 251518b commit 613179d

File tree

4 files changed

+72
-7
lines changed

4 files changed

+72
-7
lines changed

.changeset/bright-chefs-double.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@clack/prompts': minor
3+
---
4+
5+
Adds a new `indicator` option to `spinner`, which supports the original `"dots"` loading animation or a new `"timer"` loading animation.
6+
7+
```ts
8+
import * as p from '@clack/prompts';
9+
10+
const spin = p.spinner({ indicator: 'timer' });
11+
spin.start('Loading');
12+
await sleep(3000);
13+
spin.stop('Loaded');

examples/basic/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"start": "jiti ./index.ts",
1313
"stream": "jiti ./stream.ts",
1414
"spinner": "jiti ./spinner.ts",
15-
"spinner-ci": "npx cross-env CI=\"true\" jiti ./spinner-ci.ts"
15+
"spinner-ci": "npx cross-env CI=\"true\" jiti ./spinner-ci.ts",
16+
"spinner-timer": "jiti ./spinner-timer.ts"
1617
},
1718
"devDependencies": {
1819
"jiti": "^1.17.0"

examples/basic/spinner-timer.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as p from '@clack/prompts';
2+
3+
p.intro('spinner start...');
4+
5+
async function main() {
6+
const spin = p.spinner({ indicator: 'timer' });
7+
8+
spin.start('First spinner');
9+
10+
await sleep(3_000);
11+
12+
spin.stop('Done first spinner');
13+
14+
spin.start('Second spinner');
15+
await sleep(5_000);
16+
17+
spin.stop('Done second spinner');
18+
19+
p.outro('spinner stop.');
20+
}
21+
22+
function sleep(ms: number) {
23+
return new Promise((resolve) => setTimeout(resolve, ms));
24+
}
25+
26+
main();

packages/prompts/src/index.ts

+31-6
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,11 @@ export const stream = {
720720
},
721721
};
722722

723-
export const spinner = () => {
723+
export interface SpinnerOptions {
724+
indicator?: 'dots' | 'timer';
725+
}
726+
727+
export const spinner = ({ indicator = 'dots' }: SpinnerOptions = {}) => {
724728
const frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•', 'o', 'O', '0'];
725729
const delay = unicode ? 80 : 120;
726730
const isCI = process.env.CI === 'true';
@@ -730,6 +734,7 @@ export const spinner = () => {
730734
let isSpinnerActive = false;
731735
let _message = '';
732736
let _prevMessage: string | undefined = undefined;
737+
let _origin: number = performance.now();
733738

734739
const handleExit = (code: number) => {
735740
const msg = code > 1 ? 'Something went wrong' : 'Canceled';
@@ -770,13 +775,21 @@ export const spinner = () => {
770775
return msg.replace(/\.+$/, '');
771776
};
772777

778+
const formatTimer = (origin: number): string => {
779+
const duration = (performance.now() - origin) / 1000;
780+
const min = Math.floor(duration / 60);
781+
const secs = Math.floor(duration % 60);
782+
return min > 0 ? `[${min}m ${secs}s]` : `[${secs}s]`;
783+
};
784+
773785
const start = (msg = ''): void => {
774786
isSpinnerActive = true;
775787
unblock = block();
776788
_message = parseMessage(msg);
789+
_origin = performance.now();
777790
process.stdout.write(`${color.gray(S_BAR)}\n`);
778791
let frameIndex = 0;
779-
let dotsTimer = 0;
792+
let indicatorTimer = 0;
780793
registerHooks();
781794
loop = setInterval(() => {
782795
if (isCI && _message === _prevMessage) {
@@ -785,10 +798,18 @@ export const spinner = () => {
785798
clearPrevMessage();
786799
_prevMessage = _message;
787800
const frame = color.magenta(frames[frameIndex]);
788-
const loadingDots = isCI ? '...' : '.'.repeat(Math.floor(dotsTimer)).slice(0, 3);
789-
process.stdout.write(`${frame} ${_message}${loadingDots}`);
801+
802+
if (isCI) {
803+
process.stdout.write(`${frame} ${_message}...`);
804+
} else if (indicator === 'timer') {
805+
process.stdout.write(`${frame} ${_message} ${formatTimer(_origin)}`);
806+
} else {
807+
const loadingDots = '.'.repeat(Math.floor(indicatorTimer)).slice(0, 3);
808+
process.stdout.write(`${frame} ${_message}${loadingDots}`);
809+
}
810+
790811
frameIndex = frameIndex + 1 < frames.length ? frameIndex + 1 : 0;
791-
dotsTimer = dotsTimer < frames.length ? dotsTimer + 0.125 : 0;
812+
indicatorTimer = indicatorTimer < frames.length ? indicatorTimer + 0.125 : 0;
792813
}, delay);
793814
};
794815

@@ -803,7 +824,11 @@ export const spinner = () => {
803824
? color.red(S_STEP_CANCEL)
804825
: color.red(S_STEP_ERROR);
805826
_message = parseMessage(msg ?? _message);
806-
process.stdout.write(`${step} ${_message}\n`);
827+
if (indicator === 'timer') {
828+
process.stdout.write(`${step} ${_message} ${formatTimer(_origin)}\n`);
829+
} else {
830+
process.stdout.write(`${step} ${_message}\n`);
831+
}
807832
clearHooks();
808833
unblock();
809834
};

0 commit comments

Comments
 (0)