[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.

refactor: add custom themed CLI example, remove unnecessary tests & update snapshots

Paul Valladares (Dec 5, 2025, 5:55 PM -0600) 75170763 a43ce247

+88 -312
+72
examples/basic/text-theme-example.ts
··· 1 + import * as p from '@clack/prompts'; 2 + import color from 'picocolors'; 3 + 4 + async function main() { 5 + console.clear(); 6 + 7 + p.intro(`${color.bgMagenta(color.black(' Custom Themed CLI '))}`); 8 + 9 + // Custom theme with a purple/violet color scheme 10 + // Defaults: active=cyan, submit=green, cancel=red, error=yellow 11 + // Guide defaults: guide=cyan, submit=gray, cancel=gray, error=yellow 12 + const purpleTheme = { 13 + formatSymbolActive: (str: string) => color.magenta(str), // default: cyan 14 + formatSymbolSubmit: (str: string) => color.green(str), // default: green (matching guide) 15 + formatSymbolCancel: (str: string) => color.red(str), // default: red 16 + formatSymbolError: (str: string) => color.yellow(str), // default: yellow 17 + formatGuide: (str: string) => color.magenta(str), // default: cyan 18 + formatGuideSubmit: (str: string) => color.green(str), // default: gray 19 + formatGuideCancel: (str: string) => color.red(str), // default: gray - red for cancel 20 + formatGuideError: (str: string) => color.yellow(str), // default: yellow 21 + formatErrorMessage: (str: string) => color.red(str), // default: yellow 22 + }; 23 + 24 + const name = await p.text({ 25 + message: 'What is your project name?', 26 + placeholder: 'my-awesome-project', 27 + theme: purpleTheme, 28 + validate: (value) => { 29 + if (!value) return 'Project name is required'; 30 + if (value.includes(' ')) return 'Project name cannot contain spaces'; 31 + }, 32 + }); 33 + 34 + if (p.isCancel(name)) { 35 + p.cancel('Setup cancelled.'); 36 + process.exit(0); 37 + } 38 + 39 + const description = await p.text({ 40 + message: 'Describe your project in a few words:', 41 + placeholder: 'A blazing fast CLI tool', 42 + theme: purpleTheme, 43 + }); 44 + 45 + if (p.isCancel(description)) { 46 + p.cancel('Setup cancelled.'); 47 + process.exit(0); 48 + } 49 + 50 + const author = await p.text({ 51 + message: 'Who is the author?', 52 + placeholder: 'Your Name <you@example.com>', 53 + theme: purpleTheme, 54 + validate: (value) => { 55 + if (!value) return 'Author is required'; 56 + }, 57 + }); 58 + 59 + if (p.isCancel(author)) { 60 + p.cancel('Setup cancelled.'); 61 + process.exit(0); 62 + } 63 + 64 + p.note( 65 + `Name: ${color.cyan(name as string)}\nDescription: ${color.cyan((description as string) || 'N/A')}\nAuthor: ${color.cyan(author as string)}`, 66 + 'Project Summary' 67 + ); 68 + 69 + p.outro(`${color.green('✓')} Project ${color.magenta(name as string)} configured!`); 70 + } 71 + 72 + main().catch(console.error);
+16 -1
packages/prompts/src/text.ts
··· 79 79 } 80 80 })(); 81 81 82 - const titlePrefix = `${hasGuide ? `${color.gray(S_BAR)}\n` : ''}${symbolText} `; 82 + // Resolve connector bar color based on state 83 + const connectorBar = (() => { 84 + switch (this.state) { 85 + case 'initial': 86 + case 'active': 87 + return theme.formatGuide(S_BAR); 88 + case 'cancel': 89 + return theme.formatGuideCancel(S_BAR); 90 + case 'error': 91 + return theme.formatGuideError(S_BAR); 92 + case 'submit': 93 + return theme.formatGuideSubmit(S_BAR); 94 + } 95 + })(); 96 + 97 + const titlePrefix = `${hasGuide ? `${connectorBar}\n` : ''}${symbolText} `; 83 98 const title = `${titlePrefix}${opts.message}\n`; 84 99 const placeholder = opts.placeholder 85 100 ? color.inverse(opts.placeholder[0]) + color.dim(opts.placeholder.slice(1))
-216
packages/prompts/test/__snapshots__/text.test.ts.snap
··· 33 33 ] 34 34 `; 35 35 36 - exports[`text (isCI = false) > custom theme changes active symbol color 1`] = ` 37 - [ 38 - "<cursor.hide>", 39 - "│ 40 - [MAGENTA]◆[/MAGENTA] foo 41 - [BLUE]│[/BLUE] _ 42 - [BLUE]└[/BLUE] 43 - ", 44 - "<cursor.backward count=999><cursor.up count=4>", 45 - "<cursor.down count=1>", 46 - "<erase.down>", 47 - "◇ foo 48 - │", 49 - " 50 - ", 51 - "<cursor.show>", 52 - ] 53 - `; 54 - 55 - exports[`text (isCI = false) > custom theme changes cancel symbol color 1`] = ` 56 - [ 57 - "<cursor.hide>", 58 - "│ 59 - ◆ foo 60 - │ _ 61 - └ 62 - ", 63 - "<cursor.backward count=999><cursor.up count=4>", 64 - "<cursor.down count=1>", 65 - "<erase.down>", 66 - "[ORANGE]■[/ORANGE] foo 67 - [BROWN]│[/BROWN]", 68 - " 69 - ", 70 - "<cursor.show>", 71 - ] 72 - `; 73 - 74 - exports[`text (isCI = false) > custom theme changes error colors 1`] = ` 75 - [ 76 - "<cursor.hide>", 77 - "│ 78 - ◆ foo 79 - │ _ 80 - └ 81 - ", 82 - "<cursor.backward count=999><cursor.up count=4>", 83 - "<cursor.down count=2>", 84 - "<erase.line><cursor.left count=1>", 85 - "│ x█", 86 - "<cursor.down count=2>", 87 - "<cursor.backward count=999><cursor.up count=4>", 88 - "<cursor.down count=1>", 89 - "<erase.down>", 90 - "[RED_BG]▲[/RED_BG] foo 91 - [RED]│[/RED] x█ 92 - [RED]└[/RED] [BOLD_RED]custom error[/BOLD_RED] 93 - ", 94 - "<cursor.backward count=999><cursor.up count=4>", 95 - "<cursor.down count=1>", 96 - "<erase.down>", 97 - "■ foo 98 - │ x 99 - │", 100 - " 101 - ", 102 - "<cursor.show>", 103 - ] 104 - `; 105 - 106 - exports[`text (isCI = false) > custom theme changes submit symbol color 1`] = ` 107 - [ 108 - "<cursor.hide>", 109 - "│ 110 - ◆ foo 111 - │ _ 112 - └ 113 - ", 114 - "<cursor.backward count=999><cursor.up count=4>", 115 - "<cursor.down count=1>", 116 - "<erase.down>", 117 - "[PURPLE]◇[/PURPLE] foo 118 - [PINK]│[/PINK]", 119 - " 120 - ", 121 - "<cursor.show>", 122 - ] 123 - `; 124 - 125 36 exports[`text (isCI = false) > defaultValue sets the value but does not render 1`] = ` 126 37 [ 127 38 "<cursor.hide>", ··· 171 82 "<erase.down>", 172 83 "◇ foo 173 84 ", 174 - " 175 - ", 176 - "<cursor.show>", 177 - ] 178 - `; 179 - 180 - exports[`text (isCI = false) > partial theme only overrides specified options 1`] = ` 181 - [ 182 - "<cursor.hide>", 183 - "│ 184 - [CUSTOM]◆[/CUSTOM] foo 185 - │ _ 186 - └ 187 - ", 188 - "<cursor.backward count=999><cursor.up count=4>", 189 - "<cursor.down count=1>", 190 - "<erase.down>", 191 - "◇ foo 192 - │", 193 85 " 194 86 ", 195 87 "<cursor.show>", ··· 438 330 ] 439 331 `; 440 332 441 - exports[`text (isCI = true) > custom theme changes active symbol color 1`] = ` 442 - [ 443 - "<cursor.hide>", 444 - "│ 445 - [MAGENTA]◆[/MAGENTA] foo 446 - [BLUE]│[/BLUE] _ 447 - [BLUE]└[/BLUE] 448 - ", 449 - "<cursor.backward count=999><cursor.up count=4>", 450 - "<cursor.down count=1>", 451 - "<erase.down>", 452 - "◇ foo 453 - │", 454 - " 455 - ", 456 - "<cursor.show>", 457 - ] 458 - `; 459 - 460 - exports[`text (isCI = true) > custom theme changes cancel symbol color 1`] = ` 461 - [ 462 - "<cursor.hide>", 463 - "│ 464 - ◆ foo 465 - │ _ 466 - └ 467 - ", 468 - "<cursor.backward count=999><cursor.up count=4>", 469 - "<cursor.down count=1>", 470 - "<erase.down>", 471 - "[ORANGE]■[/ORANGE] foo 472 - [BROWN]│[/BROWN]", 473 - " 474 - ", 475 - "<cursor.show>", 476 - ] 477 - `; 478 - 479 - exports[`text (isCI = true) > custom theme changes error colors 1`] = ` 480 - [ 481 - "<cursor.hide>", 482 - "│ 483 - ◆ foo 484 - │ _ 485 - └ 486 - ", 487 - "<cursor.backward count=999><cursor.up count=4>", 488 - "<cursor.down count=2>", 489 - "<erase.line><cursor.left count=1>", 490 - "│ x█", 491 - "<cursor.down count=2>", 492 - "<cursor.backward count=999><cursor.up count=4>", 493 - "<cursor.down count=1>", 494 - "<erase.down>", 495 - "[RED_BG]▲[/RED_BG] foo 496 - [RED]│[/RED] x█ 497 - [RED]└[/RED] [BOLD_RED]custom error[/BOLD_RED] 498 - ", 499 - "<cursor.backward count=999><cursor.up count=4>", 500 - "<cursor.down count=1>", 501 - "<erase.down>", 502 - "■ foo 503 - │ x 504 - │", 505 - " 506 - ", 507 - "<cursor.show>", 508 - ] 509 - `; 510 - 511 - exports[`text (isCI = true) > custom theme changes submit symbol color 1`] = ` 512 - [ 513 - "<cursor.hide>", 514 - "│ 515 - ◆ foo 516 - │ _ 517 - └ 518 - ", 519 - "<cursor.backward count=999><cursor.up count=4>", 520 - "<cursor.down count=1>", 521 - "<erase.down>", 522 - "[PURPLE]◇[/PURPLE] foo 523 - [PINK]│[/PINK]", 524 - " 525 - ", 526 - "<cursor.show>", 527 - ] 528 - `; 529 - 530 333 exports[`text (isCI = true) > defaultValue sets the value but does not render 1`] = ` 531 334 [ 532 335 "<cursor.hide>", ··· 576 379 "<erase.down>", 577 380 "◇ foo 578 381 ", 579 - " 580 - ", 581 - "<cursor.show>", 582 - ] 583 - `; 584 - 585 - exports[`text (isCI = true) > partial theme only overrides specified options 1`] = ` 586 - [ 587 - "<cursor.hide>", 588 - "│ 589 - [CUSTOM]◆[/CUSTOM] foo 590 - │ _ 591 - └ 592 - ", 593 - "<cursor.backward count=999><cursor.up count=4>", 594 - "<cursor.down count=1>", 595 - "<erase.down>", 596 - "◇ foo 597 - │", 598 382 " 599 383 ", 600 384 "<cursor.show>",
-95
packages/prompts/test/text.test.ts
··· 238 238 239 239 expect(output.buffer).toMatchSnapshot(); 240 240 }); 241 - 242 - test('custom theme changes active symbol color', async () => { 243 - const result = prompts.text({ 244 - message: 'foo', 245 - input, 246 - output, 247 - theme: { 248 - formatSymbolActive: (str) => `[MAGENTA]${str}[/MAGENTA]`, 249 - formatGuide: (str) => `[BLUE]${str}[/BLUE]`, 250 - }, 251 - }); 252 - 253 - input.emit('keypress', '', { name: 'return' }); 254 - 255 - await result; 256 - 257 - expect(output.buffer).toMatchSnapshot(); 258 - }); 259 - 260 - test('custom theme changes submit symbol color', async () => { 261 - const result = prompts.text({ 262 - message: 'foo', 263 - input, 264 - output, 265 - theme: { 266 - formatSymbolSubmit: (str) => `[PURPLE]${str}[/PURPLE]`, 267 - formatGuideSubmit: (str) => `[PINK]${str}[/PINK]`, 268 - }, 269 - }); 270 - 271 - input.emit('keypress', '', { name: 'return' }); 272 - 273 - await result; 274 - 275 - expect(output.buffer).toMatchSnapshot(); 276 - }); 277 - 278 - test('custom theme changes cancel symbol color', async () => { 279 - const result = prompts.text({ 280 - message: 'foo', 281 - input, 282 - output, 283 - theme: { 284 - formatSymbolCancel: (str) => `[ORANGE]${str}[/ORANGE]`, 285 - formatGuideCancel: (str) => `[BROWN]${str}[/BROWN]`, 286 - }, 287 - }); 288 - 289 - input.emit('keypress', 'escape', { name: 'escape' }); 290 - 291 - await result; 292 - 293 - expect(output.buffer).toMatchSnapshot(); 294 - }); 295 - 296 - test('custom theme changes error colors', async () => { 297 - const result = prompts.text({ 298 - message: 'foo', 299 - validate: () => 'custom error', 300 - input, 301 - output, 302 - theme: { 303 - formatSymbolError: (str) => `[RED_BG]${str}[/RED_BG]`, 304 - formatGuideError: (str) => `[RED]${str}[/RED]`, 305 - formatErrorMessage: (str) => `[BOLD_RED]${str}[/BOLD_RED]`, 306 - }, 307 - }); 308 - 309 - input.emit('keypress', 'x', { name: 'x' }); 310 - input.emit('keypress', '', { name: 'return' }); 311 - // Cancel to exit after seeing the error 312 - input.emit('keypress', 'escape', { name: 'escape' }); 313 - 314 - await result; 315 - 316 - expect(output.buffer).toMatchSnapshot(); 317 - }); 318 - 319 - test('partial theme only overrides specified options', async () => { 320 - const result = prompts.text({ 321 - message: 'foo', 322 - input, 323 - output, 324 - theme: { 325 - // Only override the symbol, let guide use default 326 - formatSymbolActive: (str) => `[CUSTOM]${str}[/CUSTOM]`, 327 - }, 328 - }); 329 - 330 - input.emit('keypress', '', { name: 'return' }); 331 - 332 - await result; 333 - 334 - expect(output.buffer).toMatchSnapshot(); 335 - }); 336 241 });