Skip to content
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