[READ-ONLY] Mirror of https://github.com/bombshell-dev/clack. Effortlessly build beautiful command-line apps bomb.sh/docs/clack/basics/getting-started/
cli command-line command-line-app node prompt prompts
5

Configure Feed

Select the types of activity you want to include in your feed.

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

Signed-off-by: Alexander Mangel <cygnusfear@gmail.com>
Co-authored-by: James Garbutt <43081j@users.noreply.github.com>

authored by

Alexander Mangel
James Garbutt
and committed by
GitHub
(Apr 22, 2025, 9:40 AM +0100) 19558b9d bb5ef54a

+175 -2
+5
.changeset/thin-socks-travel.md
··· 1 + --- 2 + "@clack/prompts": minor 3 + --- 4 + 5 + Added support for custom frames in spinner prompt
+4 -2
packages/prompts/src/spinner.ts
··· 16 16 onCancel?: () => void; 17 17 cancelMessage?: string; 18 18 errorMessage?: string; 19 + frames?: string[]; 20 + delay?: number; 19 21 } 20 22 21 23 export interface SpinnerResult { ··· 31 33 output = process.stdout, 32 34 cancelMessage, 33 35 errorMessage, 36 + frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•', 'o', 'O', '0'], 37 + delay = unicode ? 80 : 120, 34 38 }: SpinnerOptions = {}): SpinnerResult => { 35 - const frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•', 'o', 'O', '0']; 36 - const delay = unicode ? 80 : 120; 37 39 const isCI = isCIFn(); 38 40 39 41 let unblock: () => void;
+126
packages/prompts/test/__snapshots__/spinner.test.ts.snap
··· 1 1 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 2 3 + exports[`spinner (isCI = false) > indicator customization > custom delay 1`] = ` 4 + [ 5 + "<cursor.hide>", 6 + "│ 7 + ", 8 + "◒ ", 9 + "<cursor.backward count=999>", 10 + "<erase.down>", 11 + "◐ ", 12 + "<cursor.backward count=999>", 13 + "<erase.down>", 14 + "◓ ", 15 + "<cursor.backward count=999>", 16 + "<erase.down>", 17 + "◑ ", 18 + "<cursor.backward count=999>", 19 + "<erase.down>", 20 + "◇ 21 + ", 22 + "<cursor.show>", 23 + ] 24 + `; 25 + 26 + exports[`spinner (isCI = false) > indicator customization > custom frames 1`] = ` 27 + [ 28 + "<cursor.hide>", 29 + "│ 30 + ", 31 + "🐴 ", 32 + "<cursor.backward count=999>", 33 + "<erase.down>", 34 + "🦋 ", 35 + "<cursor.backward count=999>", 36 + "<erase.down>", 37 + "🐙 ", 38 + "<cursor.backward count=999>", 39 + "<erase.down>", 40 + "🐶 ", 41 + "<cursor.backward count=999>", 42 + "<erase.down>", 43 + "◇ 44 + ", 45 + "<cursor.show>", 46 + ] 47 + `; 48 + 3 49 exports[`spinner (isCI = false) > message > sets message for next frame 1`] = ` 4 50 [ 5 51 "<cursor.hide>", ··· 9 55 "<cursor.backward count=999>", 10 56 "<erase.down>", 11 57 "◐ foo", 58 + "<cursor.backward count=999>", 59 + "<erase.down>", 60 + "◇ 61 + ", 62 + "<cursor.show>", 12 63 ] 13 64 `; 14 65 ··· 104 155 "<cursor.backward count=999>", 105 156 "<erase.down>", 106 157 "◑ ", 158 + "<cursor.backward count=999>", 159 + "<erase.down>", 160 + "◇ 161 + ", 162 + "<cursor.show>", 107 163 ] 108 164 `; 109 165 ··· 113 169 "│ 114 170 ", 115 171 "◒ foo", 172 + "<cursor.backward count=999>", 173 + "<erase.down>", 174 + "◇ 175 + ", 176 + "<cursor.show>", 116 177 ] 117 178 `; 118 179 ··· 122 183 "│ 123 184 ", 124 185 "◒ [0s]", 186 + "<cursor.backward count=999>", 187 + "<erase.down>", 188 + "◇ [0s] 189 + ", 190 + "<cursor.show>", 125 191 ] 126 192 `; 127 193 ··· 195 261 ] 196 262 `; 197 263 264 + exports[`spinner (isCI = true) > indicator customization > custom delay 1`] = ` 265 + [ 266 + "<cursor.hide>", 267 + "│ 268 + ", 269 + "◒ ...", 270 + " 271 + ", 272 + "<cursor.backward count=999>", 273 + "<erase.down>", 274 + "◇ 275 + ", 276 + "<cursor.show>", 277 + ] 278 + `; 279 + 280 + exports[`spinner (isCI = true) > indicator customization > custom frames 1`] = ` 281 + [ 282 + "<cursor.hide>", 283 + "│ 284 + ", 285 + "🐴 ...", 286 + " 287 + ", 288 + "<cursor.backward count=999>", 289 + "<erase.down>", 290 + "◇ 291 + ", 292 + "<cursor.show>", 293 + ] 294 + `; 295 + 198 296 exports[`spinner (isCI = true) > message > sets message for next frame 1`] = ` 199 297 [ 200 298 "<cursor.hide>", ··· 206 304 "<cursor.backward count=999>", 207 305 "<erase.down>", 208 306 "◐ foo...", 307 + " 308 + ", 309 + "<cursor.backward count=999>", 310 + "<erase.down>", 311 + "◇ 312 + ", 313 + "<cursor.show>", 209 314 ] 210 315 `; 211 316 ··· 292 397 "│ 293 398 ", 294 399 "◒ ...", 400 + " 401 + ", 402 + "<cursor.backward count=999>", 403 + "<erase.down>", 404 + "◇ 405 + ", 406 + "<cursor.show>", 295 407 ] 296 408 `; 297 409 ··· 301 413 "│ 302 414 ", 303 415 "◒ foo...", 416 + " 417 + ", 418 + "<cursor.backward count=999>", 419 + "<erase.down>", 420 + "◇ 421 + ", 422 + "<cursor.show>", 304 423 ] 305 424 `; 306 425 ··· 310 429 "│ 311 430 ", 312 431 "◒ ...", 432 + " 433 + ", 434 + "<cursor.backward count=999>", 435 + "<erase.down>", 436 + "◇ [0s] 437 + ", 438 + "<cursor.show>", 313 439 ] 314 440 `; 315 441
+40
packages/prompts/test/spinner.test.ts
··· 45 45 vi.advanceTimersByTime(80); 46 46 } 47 47 48 + result.stop(); 49 + 48 50 expect(output.buffer).toMatchSnapshot(); 49 51 }); 50 52 ··· 55 57 56 58 vi.advanceTimersByTime(80); 57 59 60 + result.stop(); 61 + 58 62 expect(output.buffer).toMatchSnapshot(); 59 63 }); 60 64 ··· 64 68 result.start(); 65 69 66 70 vi.advanceTimersByTime(80); 71 + 72 + result.stop(); 67 73 68 74 expect(output.buffer).toMatchSnapshot(); 69 75 }); ··· 144 150 result.message('foo'); 145 151 146 152 vi.advanceTimersByTime(80); 153 + 154 + result.stop(); 155 + 156 + expect(output.buffer).toMatchSnapshot(); 157 + }); 158 + }); 159 + 160 + describe('indicator customization', () => { 161 + test('custom frames', () => { 162 + const result = prompts.spinner({ output, frames: ['🐴', '🦋', '🐙', '🐶'] }); 163 + 164 + result.start(); 165 + 166 + // there are 4 frames 167 + for (let i = 0; i < 4; i++) { 168 + vi.advanceTimersByTime(80); 169 + } 170 + 171 + result.stop(); 172 + 173 + expect(output.buffer).toMatchSnapshot(); 174 + }); 175 + 176 + test('custom delay', () => { 177 + const result = prompts.spinner({ output, delay: 200 }); 178 + 179 + result.start(); 180 + 181 + // there are 4 frames 182 + for (let i = 0; i < 4; i++) { 183 + vi.advanceTimersByTime(200); 184 + } 185 + 186 + result.stop(); 147 187 148 188 expect(output.buffer).toMatchSnapshot(); 149 189 });