Skip to content

feat(@clack/prompts): custom spinner indicator support #247

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 22, 2025
5 changes: 5 additions & 0 deletions .changeset/thin-socks-travel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clack/prompts": minor
---

Added support for custom frames in spinner prompt
6 changes: 4 additions & 2 deletions packages/prompts/src/spinner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export interface SpinnerOptions extends CommonOptions {
onCancel?: () => void;
cancelMessage?: string;
errorMessage?: string;
frames?: string[];
delay?: number;
}

export interface SpinnerResult {
Expand All @@ -31,9 +33,9 @@ export const spinner = ({
output = process.stdout,
cancelMessage,
errorMessage,
frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•', 'o', 'O', '0'],
delay = unicode ? 80 : 120,
}: SpinnerOptions = {}): SpinnerResult => {
const frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•', 'o', 'O', '0'];
const delay = unicode ? 80 : 120;
const isCI = isCIFn();

let unblock: () => void;
Expand Down
126 changes: 126 additions & 0 deletions packages/prompts/test/__snapshots__/spinner.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,51 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`spinner (isCI = false) > indicator customization > custom delay 1`] = `
[
"<cursor.hide>",
"│
",
"◒ ",
"<cursor.backward count=999>",
"<erase.down>",
"◐ ",
"<cursor.backward count=999>",
"<erase.down>",
"◓ ",
"<cursor.backward count=999>",
"<erase.down>",
"◑ ",
"<cursor.backward count=999>",
"<erase.down>",
"◇
",
"<cursor.show>",
]
`;

exports[`spinner (isCI = false) > indicator customization > custom frames 1`] = `
[
"<cursor.hide>",
"│
",
"🐴 ",
"<cursor.backward count=999>",
"<erase.down>",
"🦋 ",
"<cursor.backward count=999>",
"<erase.down>",
"🐙 ",
"<cursor.backward count=999>",
"<erase.down>",
"🐶 ",
"<cursor.backward count=999>",
"<erase.down>",
"◇
",
"<cursor.show>",
]
`;

exports[`spinner (isCI = false) > message > sets message for next frame 1`] = `
[
"<cursor.hide>",
Expand All @@ -9,6 +55,11 @@ exports[`spinner (isCI = false) > message > sets message for next frame 1`] = `
"<cursor.backward count=999>",
"<erase.down>",
"◐ foo",
"<cursor.backward count=999>",
"<erase.down>",
"◇
",
"<cursor.show>",
]
`;

Expand Down Expand Up @@ -104,6 +155,11 @@ exports[`spinner (isCI = false) > start > renders frames at interval 1`] = `
"<cursor.backward count=999>",
"<erase.down>",
"◑ ",
"<cursor.backward count=999>",
"<erase.down>",
"◇
",
"<cursor.show>",
]
`;

Expand All @@ -113,6 +169,11 @@ exports[`spinner (isCI = false) > start > renders message 1`] = `
"│
",
"◒ foo",
"<cursor.backward count=999>",
"<erase.down>",
"◇
",
"<cursor.show>",
]
`;

Expand All @@ -122,6 +183,11 @@ exports[`spinner (isCI = false) > start > renders timer when indicator is "timer
"│
",
"◒ [0s]",
"<cursor.backward count=999>",
"<erase.down>",
"◇ [0s]
",
"<cursor.show>",
]
`;

Expand Down Expand Up @@ -195,6 +261,38 @@ exports[`spinner (isCI = false) > stop > renders submit symbol and stops spinner
]
`;

exports[`spinner (isCI = true) > indicator customization > custom delay 1`] = `
[
"<cursor.hide>",
"│
",
"◒ ...",
"
",
"<cursor.backward count=999>",
"<erase.down>",
"◇
",
"<cursor.show>",
]
`;

exports[`spinner (isCI = true) > indicator customization > custom frames 1`] = `
[
"<cursor.hide>",
"│
",
"🐴 ...",
"
",
"<cursor.backward count=999>",
"<erase.down>",
"◇
",
"<cursor.show>",
]
`;

exports[`spinner (isCI = true) > message > sets message for next frame 1`] = `
[
"<cursor.hide>",
Expand All @@ -206,6 +304,13 @@ exports[`spinner (isCI = true) > message > sets message for next frame 1`] = `
"<cursor.backward count=999>",
"<erase.down>",
"◐ foo...",
"
",
"<cursor.backward count=999>",
"<erase.down>",
"◇
",
"<cursor.show>",
]
`;

Expand Down Expand Up @@ -292,6 +397,13 @@ exports[`spinner (isCI = true) > start > renders frames at interval 1`] = `
"│
",
"◒ ...",
"
",
"<cursor.backward count=999>",
"<erase.down>",
"◇
",
"<cursor.show>",
]
`;

Expand All @@ -301,6 +413,13 @@ exports[`spinner (isCI = true) > start > renders message 1`] = `
"│
",
"◒ foo...",
"
",
"<cursor.backward count=999>",
"<erase.down>",
"◇
",
"<cursor.show>",
]
`;

Expand All @@ -310,6 +429,13 @@ exports[`spinner (isCI = true) > start > renders timer when indicator is "timer"
"│
",
"◒ ...",
"
",
"<cursor.backward count=999>",
"<erase.down>",
"◇ [0s]
",
"<cursor.show>",
]
`;

Expand Down
40 changes: 40 additions & 0 deletions packages/prompts/test/spinner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ describe.each(['true', 'false'])('spinner (isCI = %s)', (isCI) => {
vi.advanceTimersByTime(80);
}

result.stop();

expect(output.buffer).toMatchSnapshot();
});

Expand All @@ -55,6 +57,8 @@ describe.each(['true', 'false'])('spinner (isCI = %s)', (isCI) => {

vi.advanceTimersByTime(80);

result.stop();

expect(output.buffer).toMatchSnapshot();
});

Expand All @@ -65,6 +69,8 @@ describe.each(['true', 'false'])('spinner (isCI = %s)', (isCI) => {

vi.advanceTimersByTime(80);

result.stop();

expect(output.buffer).toMatchSnapshot();
});
});
Expand Down Expand Up @@ -145,6 +151,40 @@ describe.each(['true', 'false'])('spinner (isCI = %s)', (isCI) => {

vi.advanceTimersByTime(80);

result.stop();

expect(output.buffer).toMatchSnapshot();
});
});

describe('indicator customization', () => {
test('custom frames', () => {
const result = prompts.spinner({ output, frames: ['🐴', '🦋', '🐙', '🐶'] });

result.start();

// there are 4 frames
for (let i = 0; i < 4; i++) {
vi.advanceTimersByTime(80);
}

result.stop();

expect(output.buffer).toMatchSnapshot();
});

test('custom delay', () => {
const result = prompts.spinner({ output, delay: 200 });

result.start();

// there are 4 frames
for (let i = 0; i < 4; i++) {
vi.advanceTimersByTime(200);
}

result.stop();

expect(output.buffer).toMatchSnapshot();
});
});
Expand Down