From a43ce247588fcdb4e69ab6c388a5ccd1e6e05d3c Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Thu, 4 Dec 2025 01:22:34 -0600 Subject: [PATCH 1/3] feat(@clack/prompts): add theme support for text prompt customization --- .changeset/dark-roses-report.md | 34 +++ packages/prompts/src/common.ts | 35 +++ packages/prompts/src/text.ts | 78 ++++++- .../test/__snapshots__/text.test.ts.snap | 216 ++++++++++++++++++ packages/prompts/test/text.test.ts | 95 ++++++++ 5 files changed, 448 insertions(+), 10 deletions(-) create mode 100644 .changeset/dark-roses-report.md diff --git a/.changeset/dark-roses-report.md b/.changeset/dark-roses-report.md new file mode 100644 index 00000000..3c8bb811 --- /dev/null +++ b/.changeset/dark-roses-report.md @@ -0,0 +1,34 @@ +--- +"@clack/prompts": minor +--- + +Add theme support for the text prompt. Users can now customize the colors of symbols, guide lines, and error messages by passing a `theme` option. + +Example usage: +```typescript +import { text } from '@clack/prompts'; +import color from 'picocolors'; + +const result = await text({ + message: 'Enter your name', + theme: { + formatSymbolActive: (str) => color.magenta(str), + formatGuide: (str) => color.blue(str), + formatErrorMessage: (str) => color.bgRed(color.white(str)), + } +}); +``` + +Available theme options for text prompt: +- `formatSymbolActive` - Format the prompt symbol in active/initial state +- `formatSymbolSubmit` - Format the prompt symbol on submit +- `formatSymbolCancel` - Format the prompt symbol on cancel +- `formatSymbolError` - Format the prompt symbol on error +- `formatErrorMessage` - Format error messages +- `formatGuide` - Format the left guide line in active state +- `formatGuideSubmit` - Format the guide line on submit +- `formatGuideCancel` - Format the guide line on cancel +- `formatGuideError` - Format the guide line on error + +This establishes the foundation for theming support that will be extended to other prompts. + diff --git a/packages/prompts/src/common.ts b/packages/prompts/src/common.ts index 64359053..b96f890b 100644 --- a/packages/prompts/src/common.ts +++ b/packages/prompts/src/common.ts @@ -73,3 +73,38 @@ export interface CommonOptions { signal?: AbortSignal; withGuide?: boolean; } + +export type ColorFormatter = (str: string) => string; + +/** + * Global theme options shared across all prompts. + * These control the common visual elements like the guide line. + */ +export interface GlobalTheme { + /** Format the left guide/border line (default: cyan) */ + formatGuide?: ColorFormatter; + /** Format the guide line on submit (default: gray) */ + formatGuideSubmit?: ColorFormatter; + /** Format the guide line on cancel (default: gray) */ + formatGuideCancel?: ColorFormatter; + /** Format the guide line on error (default: yellow) */ + formatGuideError?: ColorFormatter; +} + +export interface ThemeOptions { + theme?: T & GlobalTheme; +} + +export const defaultGlobalTheme: Required = { + formatGuide: color.cyan, + formatGuideSubmit: color.gray, + formatGuideCancel: color.gray, + formatGuideError: color.yellow, +}; + +export function resolveTheme( + theme: Partial | undefined, + defaults: T +): T { + return { ...defaults, ...theme }; +} diff --git a/packages/prompts/src/text.ts b/packages/prompts/src/text.ts index 2c3dbc8c..4ce778f3 100644 --- a/packages/prompts/src/text.ts +++ b/packages/prompts/src/text.ts @@ -1,8 +1,48 @@ import { settings, TextPrompt } from '@clack/core'; import color from 'picocolors'; -import { type CommonOptions, S_BAR, S_BAR_END, symbol } from './common.js'; +import { + type ColorFormatter, + type CommonOptions, + defaultGlobalTheme, + type GlobalTheme, + resolveTheme, + S_BAR, + S_BAR_END, + S_STEP_ACTIVE, + S_STEP_CANCEL, + S_STEP_ERROR, + S_STEP_SUBMIT, + type ThemeOptions, +} from './common.js'; -export interface TextOptions extends CommonOptions { +/** + * Theme options specific to the text prompt. + * All formatters are optional - defaults will be used if not provided. + */ +export interface TextTheme { + /** Format the prompt symbol in active/initial state (default: cyan) */ + formatSymbolActive?: ColorFormatter; + /** Format the prompt symbol on submit (default: green) */ + formatSymbolSubmit?: ColorFormatter; + /** Format the prompt symbol on cancel (default: red) */ + formatSymbolCancel?: ColorFormatter; + /** Format the prompt symbol on error (default: yellow) */ + formatSymbolError?: ColorFormatter; + /** Format error messages (default: yellow) */ + formatErrorMessage?: ColorFormatter; +} + +/** Default theme values for the text prompt */ +const defaultTextTheme: Required = { + ...defaultGlobalTheme, + formatSymbolActive: color.cyan, + formatSymbolSubmit: color.green, + formatSymbolCancel: color.red, + formatSymbolError: color.yellow, + formatErrorMessage: color.yellow, +}; + +export interface TextOptions extends CommonOptions, ThemeOptions { message: string; placeholder?: string; defaultValue?: string; @@ -11,6 +51,8 @@ export interface TextOptions extends CommonOptions { } export const text = (opts: TextOptions) => { + const theme = resolveTheme>(opts.theme, defaultTextTheme); + return new TextPrompt({ validate: opts.validate, placeholder: opts.placeholder, @@ -21,7 +63,23 @@ export const text = (opts: TextOptions) => { input: opts.input, render() { const hasGuide = (opts?.withGuide ?? settings.withGuide) !== false; - const titlePrefix = `${hasGuide ? `${color.gray(S_BAR)}\n` : ''}${symbol(this.state)} `; + + // Resolve symbol based on state + const symbolText = (() => { + switch (this.state) { + case 'initial': + case 'active': + return theme.formatSymbolActive(S_STEP_ACTIVE); + case 'cancel': + return theme.formatSymbolCancel(S_STEP_CANCEL); + case 'error': + return theme.formatSymbolError(S_STEP_ERROR); + case 'submit': + return theme.formatSymbolSubmit(S_STEP_SUBMIT); + } + })(); + + const titlePrefix = `${hasGuide ? `${color.gray(S_BAR)}\n` : ''}${symbolText} `; const title = `${titlePrefix}${opts.message}\n`; const placeholder = opts.placeholder ? color.inverse(opts.placeholder[0]) + color.dim(opts.placeholder.slice(1)) @@ -31,24 +89,24 @@ export const text = (opts: TextOptions) => { switch (this.state) { case 'error': { - const errorText = this.error ? ` ${color.yellow(this.error)}` : ''; - const errorPrefix = hasGuide ? `${color.yellow(S_BAR)} ` : ''; - const errorPrefixEnd = hasGuide ? color.yellow(S_BAR_END) : ''; + const errorText = this.error ? ` ${theme.formatErrorMessage(this.error)}` : ''; + const errorPrefix = hasGuide ? `${theme.formatGuideError(S_BAR)} ` : ''; + const errorPrefixEnd = hasGuide ? theme.formatGuideError(S_BAR_END) : ''; return `${title.trim()}\n${errorPrefix}${userInput}\n${errorPrefixEnd}${errorText}\n`; } case 'submit': { const valueText = value ? ` ${color.dim(value)}` : ''; - const submitPrefix = hasGuide ? color.gray(S_BAR) : ''; + const submitPrefix = hasGuide ? theme.formatGuideSubmit(S_BAR) : ''; return `${title}${submitPrefix}${valueText}`; } case 'cancel': { const valueText = value ? ` ${color.strikethrough(color.dim(value))}` : ''; - const cancelPrefix = hasGuide ? color.gray(S_BAR) : ''; + const cancelPrefix = hasGuide ? theme.formatGuideCancel(S_BAR) : ''; return `${title}${cancelPrefix}${valueText}${value.trim() ? `\n${cancelPrefix}` : ''}`; } default: { - const defaultPrefix = hasGuide ? `${color.cyan(S_BAR)} ` : ''; - const defaultPrefixEnd = hasGuide ? color.cyan(S_BAR_END) : ''; + const defaultPrefix = hasGuide ? `${theme.formatGuide(S_BAR)} ` : ''; + const defaultPrefixEnd = hasGuide ? theme.formatGuide(S_BAR_END) : ''; return `${title}${defaultPrefix}${userInput}\n${defaultPrefixEnd}\n`; } } diff --git a/packages/prompts/test/__snapshots__/text.test.ts.snap b/packages/prompts/test/__snapshots__/text.test.ts.snap index 43c38c78..7363e1cd 100644 --- a/packages/prompts/test/__snapshots__/text.test.ts.snap +++ b/packages/prompts/test/__snapshots__/text.test.ts.snap @@ -33,6 +33,95 @@ exports[`text (isCI = false) > can cancel 1`] = ` ] `; +exports[`text (isCI = false) > custom theme changes active symbol color 1`] = ` +[ + "", + "│ +[MAGENTA]◆[/MAGENTA] foo +[BLUE]│[/BLUE] _ +[BLUE]└[/BLUE] +", + "", + "", + "", + "◇ foo +│", + " +", + "", +] +`; + +exports[`text (isCI = false) > custom theme changes cancel symbol color 1`] = ` +[ + "", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "[ORANGE]■[/ORANGE] foo +[BROWN]│[/BROWN]", + " +", + "", +] +`; + +exports[`text (isCI = false) > custom theme changes error colors 1`] = ` +[ + "", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ x█", + "", + "", + "", + "", + "[RED_BG]▲[/RED_BG] foo +[RED]│[/RED] x█ +[RED]└[/RED] [BOLD_RED]custom error[/BOLD_RED] +", + "", + "", + "", + "■ foo +│ x +│", + " +", + "", +] +`; + +exports[`text (isCI = false) > custom theme changes submit symbol color 1`] = ` +[ + "", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "[PURPLE]◇[/PURPLE] foo +[PINK]│[/PINK]", + " +", + "", +] +`; + exports[`text (isCI = false) > defaultValue sets the value but does not render 1`] = ` [ "", @@ -88,6 +177,25 @@ exports[`text (isCI = false) > global withGuide: false removes guide 1`] = ` ] `; +exports[`text (isCI = false) > partial theme only overrides specified options 1`] = ` +[ + "", + "│ +[CUSTOM]◆[/CUSTOM] foo +│ _ +└ +", + "", + "", + "", + "◇ foo +│", + " +", + "", +] +`; + exports[`text (isCI = false) > placeholder is not used as value when pressing enter 1`] = ` [ "", @@ -330,6 +438,95 @@ exports[`text (isCI = true) > can cancel 1`] = ` ] `; +exports[`text (isCI = true) > custom theme changes active symbol color 1`] = ` +[ + "", + "│ +[MAGENTA]◆[/MAGENTA] foo +[BLUE]│[/BLUE] _ +[BLUE]└[/BLUE] +", + "", + "", + "", + "◇ foo +│", + " +", + "", +] +`; + +exports[`text (isCI = true) > custom theme changes cancel symbol color 1`] = ` +[ + "", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "[ORANGE]■[/ORANGE] foo +[BROWN]│[/BROWN]", + " +", + "", +] +`; + +exports[`text (isCI = true) > custom theme changes error colors 1`] = ` +[ + "", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "│ x█", + "", + "", + "", + "", + "[RED_BG]▲[/RED_BG] foo +[RED]│[/RED] x█ +[RED]└[/RED] [BOLD_RED]custom error[/BOLD_RED] +", + "", + "", + "", + "■ foo +│ x +│", + " +", + "", +] +`; + +exports[`text (isCI = true) > custom theme changes submit symbol color 1`] = ` +[ + "", + "│ +◆ foo +│ _ +└ +", + "", + "", + "", + "[PURPLE]◇[/PURPLE] foo +[PINK]│[/PINK]", + " +", + "", +] +`; + exports[`text (isCI = true) > defaultValue sets the value but does not render 1`] = ` [ "", @@ -385,6 +582,25 @@ exports[`text (isCI = true) > global withGuide: false removes guide 1`] = ` ] `; +exports[`text (isCI = true) > partial theme only overrides specified options 1`] = ` +[ + "", + "│ +[CUSTOM]◆[/CUSTOM] foo +│ _ +└ +", + "", + "", + "", + "◇ foo +│", + " +", + "", +] +`; + exports[`text (isCI = true) > placeholder is not used as value when pressing enter 1`] = ` [ "", diff --git a/packages/prompts/test/text.test.ts b/packages/prompts/test/text.test.ts index 62de9067..3421642d 100644 --- a/packages/prompts/test/text.test.ts +++ b/packages/prompts/test/text.test.ts @@ -238,4 +238,99 @@ describe.each(['true', 'false'])('text (isCI = %s)', (isCI) => { expect(output.buffer).toMatchSnapshot(); }); + + test('custom theme changes active symbol color', async () => { + const result = prompts.text({ + message: 'foo', + input, + output, + theme: { + formatSymbolActive: (str) => `[MAGENTA]${str}[/MAGENTA]`, + formatGuide: (str) => `[BLUE]${str}[/BLUE]`, + }, + }); + + input.emit('keypress', '', { name: 'return' }); + + await result; + + expect(output.buffer).toMatchSnapshot(); + }); + + test('custom theme changes submit symbol color', async () => { + const result = prompts.text({ + message: 'foo', + input, + output, + theme: { + formatSymbolSubmit: (str) => `[PURPLE]${str}[/PURPLE]`, + formatGuideSubmit: (str) => `[PINK]${str}[/PINK]`, + }, + }); + + input.emit('keypress', '', { name: 'return' }); + + await result; + + expect(output.buffer).toMatchSnapshot(); + }); + + test('custom theme changes cancel symbol color', async () => { + const result = prompts.text({ + message: 'foo', + input, + output, + theme: { + formatSymbolCancel: (str) => `[ORANGE]${str}[/ORANGE]`, + formatGuideCancel: (str) => `[BROWN]${str}[/BROWN]`, + }, + }); + + input.emit('keypress', 'escape', { name: 'escape' }); + + await result; + + expect(output.buffer).toMatchSnapshot(); + }); + + test('custom theme changes error colors', async () => { + const result = prompts.text({ + message: 'foo', + validate: () => 'custom error', + input, + output, + theme: { + formatSymbolError: (str) => `[RED_BG]${str}[/RED_BG]`, + formatGuideError: (str) => `[RED]${str}[/RED]`, + formatErrorMessage: (str) => `[BOLD_RED]${str}[/BOLD_RED]`, + }, + }); + + input.emit('keypress', 'x', { name: 'x' }); + input.emit('keypress', '', { name: 'return' }); + // Cancel to exit after seeing the error + input.emit('keypress', 'escape', { name: 'escape' }); + + await result; + + expect(output.buffer).toMatchSnapshot(); + }); + + test('partial theme only overrides specified options', async () => { + const result = prompts.text({ + message: 'foo', + input, + output, + theme: { + // Only override the symbol, let guide use default + formatSymbolActive: (str) => `[CUSTOM]${str}[/CUSTOM]`, + }, + }); + + input.emit('keypress', '', { name: 'return' }); + + await result; + + expect(output.buffer).toMatchSnapshot(); + }); }); From 751707632be5943485b6e115c3ff4e631d87dcd3 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:55:48 -0600 Subject: [PATCH 2/3] refactor: add custom themed CLI example, remove unnecessary tests & update snapshots --- examples/basic/text-theme-example.ts | 72 ++++++ packages/prompts/src/text.ts | 17 +- .../test/__snapshots__/text.test.ts.snap | 216 ------------------ packages/prompts/test/text.test.ts | 95 -------- 4 files changed, 88 insertions(+), 312 deletions(-) create mode 100644 examples/basic/text-theme-example.ts diff --git a/examples/basic/text-theme-example.ts b/examples/basic/text-theme-example.ts new file mode 100644 index 00000000..bb2ad674 --- /dev/null +++ b/examples/basic/text-theme-example.ts @@ -0,0 +1,72 @@ +import * as p from '@clack/prompts'; +import color from 'picocolors'; + +async function main() { + console.clear(); + + p.intro(`${color.bgMagenta(color.black(' Custom Themed CLI '))}`); + + // Custom theme with a purple/violet color scheme + // Defaults: active=cyan, submit=green, cancel=red, error=yellow + // Guide defaults: guide=cyan, submit=gray, cancel=gray, error=yellow + const purpleTheme = { + formatSymbolActive: (str: string) => color.magenta(str), // default: cyan + formatSymbolSubmit: (str: string) => color.green(str), // default: green (matching guide) + formatSymbolCancel: (str: string) => color.red(str), // default: red + formatSymbolError: (str: string) => color.yellow(str), // default: yellow + formatGuide: (str: string) => color.magenta(str), // default: cyan + formatGuideSubmit: (str: string) => color.green(str), // default: gray + formatGuideCancel: (str: string) => color.red(str), // default: gray - red for cancel + formatGuideError: (str: string) => color.yellow(str), // default: yellow + formatErrorMessage: (str: string) => color.red(str), // default: yellow + }; + + const name = await p.text({ + message: 'What is your project name?', + placeholder: 'my-awesome-project', + theme: purpleTheme, + validate: (value) => { + if (!value) return 'Project name is required'; + if (value.includes(' ')) return 'Project name cannot contain spaces'; + }, + }); + + if (p.isCancel(name)) { + p.cancel('Setup cancelled.'); + process.exit(0); + } + + const description = await p.text({ + message: 'Describe your project in a few words:', + placeholder: 'A blazing fast CLI tool', + theme: purpleTheme, + }); + + if (p.isCancel(description)) { + p.cancel('Setup cancelled.'); + process.exit(0); + } + + const author = await p.text({ + message: 'Who is the author?', + placeholder: 'Your Name ', + theme: purpleTheme, + validate: (value) => { + if (!value) return 'Author is required'; + }, + }); + + if (p.isCancel(author)) { + p.cancel('Setup cancelled.'); + process.exit(0); + } + + p.note( + `Name: ${color.cyan(name as string)}\nDescription: ${color.cyan((description as string) || 'N/A')}\nAuthor: ${color.cyan(author as string)}`, + 'Project Summary' + ); + + p.outro(`${color.green('✓')} Project ${color.magenta(name as string)} configured!`); +} + +main().catch(console.error); diff --git a/packages/prompts/src/text.ts b/packages/prompts/src/text.ts index 4ce778f3..a2b0b69f 100644 --- a/packages/prompts/src/text.ts +++ b/packages/prompts/src/text.ts @@ -79,7 +79,22 @@ export const text = (opts: TextOptions) => { } })(); - const titlePrefix = `${hasGuide ? `${color.gray(S_BAR)}\n` : ''}${symbolText} `; + // Resolve connector bar color based on state + const connectorBar = (() => { + switch (this.state) { + case 'initial': + case 'active': + return theme.formatGuide(S_BAR); + case 'cancel': + return theme.formatGuideCancel(S_BAR); + case 'error': + return theme.formatGuideError(S_BAR); + case 'submit': + return theme.formatGuideSubmit(S_BAR); + } + })(); + + const titlePrefix = `${hasGuide ? `${connectorBar}\n` : ''}${symbolText} `; const title = `${titlePrefix}${opts.message}\n`; const placeholder = opts.placeholder ? color.inverse(opts.placeholder[0]) + color.dim(opts.placeholder.slice(1)) diff --git a/packages/prompts/test/__snapshots__/text.test.ts.snap b/packages/prompts/test/__snapshots__/text.test.ts.snap index 7363e1cd..43c38c78 100644 --- a/packages/prompts/test/__snapshots__/text.test.ts.snap +++ b/packages/prompts/test/__snapshots__/text.test.ts.snap @@ -33,95 +33,6 @@ exports[`text (isCI = false) > can cancel 1`] = ` ] `; -exports[`text (isCI = false) > custom theme changes active symbol color 1`] = ` -[ - "", - "│ -[MAGENTA]◆[/MAGENTA] foo -[BLUE]│[/BLUE] _ -[BLUE]└[/BLUE] -", - "", - "", - "", - "◇ foo -│", - " -", - "", -] -`; - -exports[`text (isCI = false) > custom theme changes cancel symbol color 1`] = ` -[ - "", - "│ -◆ foo -│ _ -└ -", - "", - "", - "", - "[ORANGE]■[/ORANGE] foo -[BROWN]│[/BROWN]", - " -", - "", -] -`; - -exports[`text (isCI = false) > custom theme changes error colors 1`] = ` -[ - "", - "│ -◆ foo -│ _ -└ -", - "", - "", - "", - "│ x█", - "", - "", - "", - "", - "[RED_BG]▲[/RED_BG] foo -[RED]│[/RED] x█ -[RED]└[/RED] [BOLD_RED]custom error[/BOLD_RED] -", - "", - "", - "", - "■ foo -│ x -│", - " -", - "", -] -`; - -exports[`text (isCI = false) > custom theme changes submit symbol color 1`] = ` -[ - "", - "│ -◆ foo -│ _ -└ -", - "", - "", - "", - "[PURPLE]◇[/PURPLE] foo -[PINK]│[/PINK]", - " -", - "", -] -`; - exports[`text (isCI = false) > defaultValue sets the value but does not render 1`] = ` [ "", @@ -177,25 +88,6 @@ exports[`text (isCI = false) > global withGuide: false removes guide 1`] = ` ] `; -exports[`text (isCI = false) > partial theme only overrides specified options 1`] = ` -[ - "", - "│ -[CUSTOM]◆[/CUSTOM] foo -│ _ -└ -", - "", - "", - "", - "◇ foo -│", - " -", - "", -] -`; - exports[`text (isCI = false) > placeholder is not used as value when pressing enter 1`] = ` [ "", @@ -438,95 +330,6 @@ exports[`text (isCI = true) > can cancel 1`] = ` ] `; -exports[`text (isCI = true) > custom theme changes active symbol color 1`] = ` -[ - "", - "│ -[MAGENTA]◆[/MAGENTA] foo -[BLUE]│[/BLUE] _ -[BLUE]└[/BLUE] -", - "", - "", - "", - "◇ foo -│", - " -", - "", -] -`; - -exports[`text (isCI = true) > custom theme changes cancel symbol color 1`] = ` -[ - "", - "│ -◆ foo -│ _ -└ -", - "", - "", - "", - "[ORANGE]■[/ORANGE] foo -[BROWN]│[/BROWN]", - " -", - "", -] -`; - -exports[`text (isCI = true) > custom theme changes error colors 1`] = ` -[ - "", - "│ -◆ foo -│ _ -└ -", - "", - "", - "", - "│ x█", - "", - "", - "", - "", - "[RED_BG]▲[/RED_BG] foo -[RED]│[/RED] x█ -[RED]└[/RED] [BOLD_RED]custom error[/BOLD_RED] -", - "", - "", - "", - "■ foo -│ x -│", - " -", - "", -] -`; - -exports[`text (isCI = true) > custom theme changes submit symbol color 1`] = ` -[ - "", - "│ -◆ foo -│ _ -└ -", - "", - "", - "", - "[PURPLE]◇[/PURPLE] foo -[PINK]│[/PINK]", - " -", - "", -] -`; - exports[`text (isCI = true) > defaultValue sets the value but does not render 1`] = ` [ "", @@ -582,25 +385,6 @@ exports[`text (isCI = true) > global withGuide: false removes guide 1`] = ` ] `; -exports[`text (isCI = true) > partial theme only overrides specified options 1`] = ` -[ - "", - "│ -[CUSTOM]◆[/CUSTOM] foo -│ _ -└ -", - "", - "", - "", - "◇ foo -│", - " -", - "", -] -`; - exports[`text (isCI = true) > placeholder is not used as value when pressing enter 1`] = ` [ "", diff --git a/packages/prompts/test/text.test.ts b/packages/prompts/test/text.test.ts index 3421642d..62de9067 100644 --- a/packages/prompts/test/text.test.ts +++ b/packages/prompts/test/text.test.ts @@ -238,99 +238,4 @@ describe.each(['true', 'false'])('text (isCI = %s)', (isCI) => { expect(output.buffer).toMatchSnapshot(); }); - - test('custom theme changes active symbol color', async () => { - const result = prompts.text({ - message: 'foo', - input, - output, - theme: { - formatSymbolActive: (str) => `[MAGENTA]${str}[/MAGENTA]`, - formatGuide: (str) => `[BLUE]${str}[/BLUE]`, - }, - }); - - input.emit('keypress', '', { name: 'return' }); - - await result; - - expect(output.buffer).toMatchSnapshot(); - }); - - test('custom theme changes submit symbol color', async () => { - const result = prompts.text({ - message: 'foo', - input, - output, - theme: { - formatSymbolSubmit: (str) => `[PURPLE]${str}[/PURPLE]`, - formatGuideSubmit: (str) => `[PINK]${str}[/PINK]`, - }, - }); - - input.emit('keypress', '', { name: 'return' }); - - await result; - - expect(output.buffer).toMatchSnapshot(); - }); - - test('custom theme changes cancel symbol color', async () => { - const result = prompts.text({ - message: 'foo', - input, - output, - theme: { - formatSymbolCancel: (str) => `[ORANGE]${str}[/ORANGE]`, - formatGuideCancel: (str) => `[BROWN]${str}[/BROWN]`, - }, - }); - - input.emit('keypress', 'escape', { name: 'escape' }); - - await result; - - expect(output.buffer).toMatchSnapshot(); - }); - - test('custom theme changes error colors', async () => { - const result = prompts.text({ - message: 'foo', - validate: () => 'custom error', - input, - output, - theme: { - formatSymbolError: (str) => `[RED_BG]${str}[/RED_BG]`, - formatGuideError: (str) => `[RED]${str}[/RED]`, - formatErrorMessage: (str) => `[BOLD_RED]${str}[/BOLD_RED]`, - }, - }); - - input.emit('keypress', 'x', { name: 'x' }); - input.emit('keypress', '', { name: 'return' }); - // Cancel to exit after seeing the error - input.emit('keypress', 'escape', { name: 'escape' }); - - await result; - - expect(output.buffer).toMatchSnapshot(); - }); - - test('partial theme only overrides specified options', async () => { - const result = prompts.text({ - message: 'foo', - input, - output, - theme: { - // Only override the symbol, let guide use default - formatSymbolActive: (str) => `[CUSTOM]${str}[/CUSTOM]`, - }, - }); - - input.emit('keypress', '', { name: 'return' }); - - await result; - - expect(output.buffer).toMatchSnapshot(); - }); }); From 5232930ecbd71cf2d26e798e3e314af5f285d762 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:58:25 -0600 Subject: [PATCH 3/3] fix: update snapshots to reflect changes in text prompt rendering --- .../test/__snapshots__/text.test.ts.snap | 356 +++++++++--------- 1 file changed, 178 insertions(+), 178 deletions(-) diff --git a/packages/prompts/test/__snapshots__/text.test.ts.snap b/packages/prompts/test/__snapshots__/text.test.ts.snap index 43c38c78..9a3600c2 100644 --- a/packages/prompts/test/__snapshots__/text.test.ts.snap +++ b/packages/prompts/test/__snapshots__/text.test.ts.snap @@ -3,10 +3,10 @@ exports[`text (isCI = false) > can be aborted by a signal 1`] = ` [ "", - "│ -◆ foo -│ _ -└ + "│ +◆ foo +│ _ +└ ", " ", @@ -17,16 +17,16 @@ exports[`text (isCI = false) > can be aborted by a signal 1`] = ` exports[`text (isCI = false) > can cancel 1`] = ` [ "", - "│ -◆ foo -│ _ -└ + "│ +◆ foo +│ _ +└ ", "", "", "", - "■ foo -│", + "■ foo +│", " ", "", @@ -36,16 +36,16 @@ exports[`text (isCI = false) > can cancel 1`] = ` exports[`text (isCI = false) > defaultValue sets the value but does not render 1`] = ` [ "", - "│ -◆ foo -│ _ -└ + "│ +◆ foo +│ _ +└ ", "", "", "", - "◇ foo -│ bar", + "◇ foo +│ bar", " ", "", @@ -55,16 +55,16 @@ exports[`text (isCI = false) > defaultValue sets the value but does not render 1 exports[`text (isCI = false) > empty string when no value and no default 1`] = ` [ "", - "│ -◆ foo -│   (hit Enter to use default) -└ + "│ +◆ foo +│ (hit Enter to use default) +└ ", "", "", "", - "◇ foo -│", + "◇ foo +│", " ", "", @@ -74,13 +74,13 @@ exports[`text (isCI = false) > empty string when no value and no default 1`] = ` exports[`text (isCI = false) > global withGuide: false removes guide 1`] = ` [ "", - "◆ foo -_ + "◆ foo +_ ", "", "", - "◇ foo + "◇ foo ", " ", @@ -91,16 +91,16 @@ exports[`text (isCI = false) > global withGuide: false removes guide 1`] = ` exports[`text (isCI = false) > placeholder is not used as value when pressing enter 1`] = ` [ "", - "│ -◆ foo -│   (hit Enter to use default) -└ + "│ +◆ foo +│ (hit Enter to use default) +└ ", "", "", "", - "◇ foo -│ default-value", + "◇ foo +│ default-value", " ", "", @@ -110,27 +110,27 @@ exports[`text (isCI = false) > placeholder is not used as value when pressing en exports[`text (isCI = false) > renders cancelled value if one set 1`] = ` [ "", - "│ -◆ foo -│ _ -└ + "│ +◆ foo +│ _ +└ ", "", "", "", - "│ x█", + "│ x█", "", "", "", "", - "│ xy█", + "│ xy█", "", "", "", "", - "■ foo -│ xy -│", + "■ foo +│ xy +│", " ", "", @@ -140,16 +140,16 @@ exports[`text (isCI = false) > renders cancelled value if one set 1`] = ` exports[`text (isCI = false) > renders message 1`] = ` [ "", - "│ -◆ foo -│ _ -└ + "│ +◆ foo +│ _ +└ ", "", "", "", - "◇ foo -│", + "◇ foo +│", " ", "", @@ -159,16 +159,16 @@ exports[`text (isCI = false) > renders message 1`] = ` exports[`text (isCI = false) > renders placeholder if set 1`] = ` [ "", - "│ -◆ foo -│ bar -└ + "│ +◆ foo +│ bar +└ ", "", "", "", - "◇ foo -│", + "◇ foo +│", " ", "", @@ -178,26 +178,26 @@ exports[`text (isCI = false) > renders placeholder if set 1`] = ` exports[`text (isCI = false) > renders submitted value 1`] = ` [ "", - "│ -◆ foo -│ _ -└ + "│ +◆ foo +│ _ +└ ", "", "", "", - "│ x█", + "│ x█", "", "", "", "", - "│ xy█", + "│ xy█", "", "", "", "", - "◇ foo -│ xy", + "◇ foo +│ xy", " ", "", @@ -207,35 +207,35 @@ exports[`text (isCI = false) > renders submitted value 1`] = ` exports[`text (isCI = false) > validation errors render and clear (using Error) 1`] = ` [ "", - "│ -◆ foo -│ _ -└ + "│ +◆ foo +│ _ +└ ", "", "", "", - "│ x█", + "│ x█", "", "", "", "", - "▲ foo -│ x█ -└ should be xy + "▲ foo +│ x█ +└ should be xy ", "", "", "", - "◆ foo -│ xy█ -└ + "◆ foo +│ xy█ +└ ", "", "", "", - "◇ foo -│ xy", + "◇ foo +│ xy", " ", "", @@ -245,35 +245,35 @@ exports[`text (isCI = false) > validation errors render and clear (using Error) exports[`text (isCI = false) > validation errors render and clear 1`] = ` [ "", - "│ -◆ foo -│ _ -└ + "│ +◆ foo +│ _ +└ ", "", "", "", - "│ x█", + "│ x█", "", "", "", "", - "▲ foo -│ x█ -└ should be xy + "▲ foo +│ x█ +└ should be xy ", "", "", "", - "◆ foo -│ xy█ -└ + "◆ foo +│ xy█ +└ ", "", "", "", - "◇ foo -│ xy", + "◇ foo +│ xy", " ", "", @@ -283,13 +283,13 @@ exports[`text (isCI = false) > validation errors render and clear 1`] = ` exports[`text (isCI = false) > withGuide: false removes guide 1`] = ` [ "", - "◆ foo -_ + "◆ foo +_ ", "", "", - "◇ foo + "◇ foo ", " ", @@ -300,10 +300,10 @@ exports[`text (isCI = false) > withGuide: false removes guide 1`] = ` exports[`text (isCI = true) > can be aborted by a signal 1`] = ` [ "", - "│ -◆ foo -│ _ -└ + "│ +◆ foo +│ _ +└ ", " ", @@ -314,16 +314,16 @@ exports[`text (isCI = true) > can be aborted by a signal 1`] = ` exports[`text (isCI = true) > can cancel 1`] = ` [ "", - "│ -◆ foo -│ _ -└ + "│ +◆ foo +│ _ +└ ", "", "", "", - "■ foo -│", + "■ foo +│", " ", "", @@ -333,16 +333,16 @@ exports[`text (isCI = true) > can cancel 1`] = ` exports[`text (isCI = true) > defaultValue sets the value but does not render 1`] = ` [ "", - "│ -◆ foo -│ _ -└ + "│ +◆ foo +│ _ +└ ", "", "", "", - "◇ foo -│ bar", + "◇ foo +│ bar", " ", "", @@ -352,16 +352,16 @@ exports[`text (isCI = true) > defaultValue sets the value but does not render 1` exports[`text (isCI = true) > empty string when no value and no default 1`] = ` [ "", - "│ -◆ foo -│   (hit Enter to use default) -└ + "│ +◆ foo +│ (hit Enter to use default) +└ ", "", "", "", - "◇ foo -│", + "◇ foo +│", " ", "", @@ -371,13 +371,13 @@ exports[`text (isCI = true) > empty string when no value and no default 1`] = ` exports[`text (isCI = true) > global withGuide: false removes guide 1`] = ` [ "", - "◆ foo -_ + "◆ foo +_ ", "", "", - "◇ foo + "◇ foo ", " ", @@ -388,16 +388,16 @@ exports[`text (isCI = true) > global withGuide: false removes guide 1`] = ` exports[`text (isCI = true) > placeholder is not used as value when pressing enter 1`] = ` [ "", - "│ -◆ foo -│   (hit Enter to use default) -└ + "│ +◆ foo +│ (hit Enter to use default) +└ ", "", "", "", - "◇ foo -│ default-value", + "◇ foo +│ default-value", " ", "", @@ -407,27 +407,27 @@ exports[`text (isCI = true) > placeholder is not used as value when pressing ent exports[`text (isCI = true) > renders cancelled value if one set 1`] = ` [ "", - "│ -◆ foo -│ _ -└ + "│ +◆ foo +│ _ +└ ", "", "", "", - "│ x█", + "│ x█", "", "", "", "", - "│ xy█", + "│ xy█", "", "", "", "", - "■ foo -│ xy -│", + "■ foo +│ xy +│", " ", "", @@ -437,16 +437,16 @@ exports[`text (isCI = true) > renders cancelled value if one set 1`] = ` exports[`text (isCI = true) > renders message 1`] = ` [ "", - "│ -◆ foo -│ _ -└ + "│ +◆ foo +│ _ +└ ", "", "", "", - "◇ foo -│", + "◇ foo +│", " ", "", @@ -456,16 +456,16 @@ exports[`text (isCI = true) > renders message 1`] = ` exports[`text (isCI = true) > renders placeholder if set 1`] = ` [ "", - "│ -◆ foo -│ bar -└ + "│ +◆ foo +│ bar +└ ", "", "", "", - "◇ foo -│", + "◇ foo +│", " ", "", @@ -475,26 +475,26 @@ exports[`text (isCI = true) > renders placeholder if set 1`] = ` exports[`text (isCI = true) > renders submitted value 1`] = ` [ "", - "│ -◆ foo -│ _ -└ + "│ +◆ foo +│ _ +└ ", "", "", "", - "│ x█", + "│ x█", "", "", "", "", - "│ xy█", + "│ xy█", "", "", "", "", - "◇ foo -│ xy", + "◇ foo +│ xy", " ", "", @@ -504,35 +504,35 @@ exports[`text (isCI = true) > renders submitted value 1`] = ` exports[`text (isCI = true) > validation errors render and clear (using Error) 1`] = ` [ "", - "│ -◆ foo -│ _ -└ + "│ +◆ foo +│ _ +└ ", "", "", "", - "│ x█", + "│ x█", "", "", "", "", - "▲ foo -│ x█ -└ should be xy + "▲ foo +│ x█ +└ should be xy ", "", "", "", - "◆ foo -│ xy█ -└ + "◆ foo +│ xy█ +└ ", "", "", "", - "◇ foo -│ xy", + "◇ foo +│ xy", " ", "", @@ -542,35 +542,35 @@ exports[`text (isCI = true) > validation errors render and clear (using Error) 1 exports[`text (isCI = true) > validation errors render and clear 1`] = ` [ "", - "│ -◆ foo -│ _ -└ + "│ +◆ foo +│ _ +└ ", "", "", "", - "│ x█", + "│ x█", "", "", "", "", - "▲ foo -│ x█ -└ should be xy + "▲ foo +│ x█ +└ should be xy ", "", "", "", - "◆ foo -│ xy█ -└ + "◆ foo +│ xy█ +└ ", "", "", "", - "◇ foo -│ xy", + "◇ foo +│ xy", " ", "", @@ -580,13 +580,13 @@ exports[`text (isCI = true) > validation errors render and clear 1`] = ` exports[`text (isCI = true) > withGuide: false removes guide 1`] = ` [ "", - "◆ foo -_ + "◆ foo +_ ", "", "", - "◇ foo + "◇ foo ", " ",