diff --git a/.changeset/huge-items-throw.md b/.changeset/huge-items-throw.md new file mode 100644 index 00000000..6be63e72 --- /dev/null +++ b/.changeset/huge-items-throw.md @@ -0,0 +1,6 @@ +--- +"@clack/prompts": minor +"@clack/core": minor +--- + +Replaces `picocolors` with Node.js built-in `styleText`. diff --git a/packages/core/package.json b/packages/core/package.json index 1e802924..8bdf5e28 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -53,7 +53,6 @@ "test": "vitest run" }, "dependencies": { - "picocolors": "^1.0.0", "sisteransi": "^1.0.5" }, "devDependencies": { diff --git a/packages/core/src/prompts/autocomplete.ts b/packages/core/src/prompts/autocomplete.ts index 6e308ff5..91c154ea 100644 --- a/packages/core/src/prompts/autocomplete.ts +++ b/packages/core/src/prompts/autocomplete.ts @@ -1,5 +1,5 @@ import type { Key } from 'node:readline'; -import color from 'picocolors'; +import { styleText } from 'node:util'; import Prompt, { type PromptOptions } from './prompt.js'; interface OptionLike { @@ -71,14 +71,14 @@ export default class AutocompletePrompt extends Prompt< get userInputWithCursor() { if (!this.userInput) { - return color.inverse(color.hidden('_')); + return styleText(['inverse', 'hidden'], '_'); } if (this._cursor >= this.userInput.length) { return `${this.userInput}█`; } const s1 = this.userInput.slice(0, this._cursor); const [s2, ...s3] = this.userInput.slice(this._cursor); - return `${s1}${color.inverse(s2)}${s3.join('')}`; + return `${s1}${styleText('inverse', s2)}${s3.join('')}`; } get options(): T[] { diff --git a/packages/core/src/prompts/password.ts b/packages/core/src/prompts/password.ts index a9864577..d7885407 100644 --- a/packages/core/src/prompts/password.ts +++ b/packages/core/src/prompts/password.ts @@ -1,4 +1,4 @@ -import color from 'picocolors'; +import { styleText } from 'node:util'; import Prompt, { type PromptOptions } from './prompt.js'; interface PasswordOptions extends PromptOptions { @@ -18,12 +18,12 @@ export default class PasswordPrompt extends Prompt { } const userInput = this.userInput; if (this.cursor >= userInput.length) { - return `${this.masked}${color.inverse(color.hidden('_'))}`; + return `${this.masked}${styleText(['inverse', 'hidden'], '_')}`; } const masked = this.masked; const s1 = masked.slice(0, this.cursor); const s2 = masked.slice(this.cursor); - return `${s1}${color.inverse(s2[0])}${s2.slice(1)}`; + return `${s1}${styleText('inverse', s2[0])}${s2.slice(1)}`; } clear() { this._clearUserInput(); diff --git a/packages/core/src/prompts/text.ts b/packages/core/src/prompts/text.ts index c76a5188..e4d47840 100644 --- a/packages/core/src/prompts/text.ts +++ b/packages/core/src/prompts/text.ts @@ -1,4 +1,4 @@ -import color from 'picocolors'; +import { styleText } from 'node:util'; import Prompt, { type PromptOptions } from './prompt.js'; interface TextOptions extends PromptOptions { @@ -17,7 +17,7 @@ export default class TextPrompt extends Prompt { } const s1 = userInput.slice(0, this.cursor); const [s2, ...s3] = userInput.slice(this.cursor); - return `${s1}${color.inverse(s2)}${s3.join('')}`; + return `${s1}${styleText('inverse', s2)}${s3.join('')}`; } get cursor() { return this._cursor; diff --git a/packages/core/test/prompts/password.test.ts b/packages/core/test/prompts/password.test.ts index dc145fc7..4847b616 100644 --- a/packages/core/test/prompts/password.test.ts +++ b/packages/core/test/prompts/password.test.ts @@ -1,4 +1,4 @@ -import color from 'picocolors'; +import { styleText } from 'node:util'; import { cursor } from 'sisteransi'; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { default as PasswordPrompt } from '../../src/prompts/password.js'; @@ -65,7 +65,9 @@ describe('PasswordPrompt', () => { }); instance.prompt(); input.emit('keypress', 'x', { name: 'x' }); - expect(instance.userInputWithCursor).to.equal(`•${color.inverse(color.hidden('_'))}`); + expect(instance.userInputWithCursor).to.equal( + `•${styleText(['inverse', 'hidden'], '_')}` + ); }); test('renders cursor inside value', () => { @@ -80,7 +82,7 @@ describe('PasswordPrompt', () => { input.emit('keypress', 'z', { name: 'z' }); input.emit('keypress', 'left', { name: 'left' }); input.emit('keypress', 'left', { name: 'left' }); - expect(instance.userInputWithCursor).to.equal(`•${color.inverse('•')}•`); + expect(instance.userInputWithCursor).to.equal(`•${styleText('inverse', '•')}•`); }); test('renders custom mask', () => { @@ -92,7 +94,9 @@ describe('PasswordPrompt', () => { }); instance.prompt(); input.emit('keypress', 'x', { name: 'x' }); - expect(instance.userInputWithCursor).to.equal(`X${color.inverse(color.hidden('_'))}`); + expect(instance.userInputWithCursor).to.equal( + `X${styleText(['inverse', 'hidden'], '_')}` + ); }); }); }); diff --git a/packages/core/test/prompts/text.test.ts b/packages/core/test/prompts/text.test.ts index 9ae20333..0372d174 100644 --- a/packages/core/test/prompts/text.test.ts +++ b/packages/core/test/prompts/text.test.ts @@ -1,4 +1,4 @@ -import color from 'picocolors'; +import { styleText } from 'node:util'; import { cursor } from 'sisteransi'; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { default as TextPrompt } from '../../src/prompts/text.js'; @@ -93,7 +93,7 @@ describe('TextPrompt', () => { input.emit('keypress', keys[i], { name: keys[i] }); } input.emit('keypress', 'left', { name: 'left' }); - expect(instance.userInputWithCursor).to.equal(`fo${color.inverse('o')}`); + expect(instance.userInputWithCursor).to.equal(`fo${styleText('inverse', 'o')}`); }); test('shows cursor at end if beyond value', () => { diff --git a/packages/prompts/package.json b/packages/prompts/package.json index 0a636349..ab7936b5 100644 --- a/packages/prompts/package.json +++ b/packages/prompts/package.json @@ -54,7 +54,6 @@ }, "dependencies": { "@clack/core": "workspace:*", - "picocolors": "^1.0.0", "sisteransi": "^1.0.5" }, "devDependencies": { diff --git a/packages/prompts/src/autocomplete.ts b/packages/prompts/src/autocomplete.ts index e55b285f..41710157 100644 --- a/packages/prompts/src/autocomplete.ts +++ b/packages/prompts/src/autocomplete.ts @@ -1,5 +1,5 @@ +import { styleText } from 'node:util'; import { AutocompletePrompt } from '@clack/core'; -import color from 'picocolors'; import { type CommonOptions, S_BAR, @@ -89,7 +89,7 @@ export const autocomplete = (opts: AutocompleteOptions) => { validate: opts.validate, render() { // Title and message display - const headings = [`${color.gray(S_BAR)}`, `${symbol(this.state)} ${opts.message}`]; + const headings = [`${styleText('gray', S_BAR)}`, `${symbol(this.state)} ${opts.message}`]; const userInput = this.userInput; const valueAsString = String(this.value ?? ''); const options = this.options; @@ -102,13 +102,15 @@ export const autocomplete = (opts: AutocompleteOptions) => { // Show selected value const selected = getSelectedOptions(this.selectedValues, options); const label = - selected.length > 0 ? ` ${color.dim(selected.map(getLabel).join(', '))}` : ''; - return `${headings.join('\n')}\n${color.gray(S_BAR)}${label}`; + selected.length > 0 ? ` ${styleText('dim', selected.map(getLabel).join(', '))}` : ''; + return `${headings.join('\n')}\n${styleText('gray', S_BAR)}${label}`; } case 'cancel': { - const userInputText = userInput ? ` ${color.strikethrough(color.dim(userInput))}` : ''; - return `${headings.join('\n')}\n${color.gray(S_BAR)}${userInputText}`; + const userInputText = userInput + ? ` ${styleText(['strikethrough', 'dim'], userInput)}` + : ''; + return `${headings.join('\n')}\n${styleText('gray', S_BAR)}${userInputText}`; } default: { @@ -116,7 +118,7 @@ export const autocomplete = (opts: AutocompleteOptions) => { let searchText = ''; if (this.isNavigating || showPlaceholder) { const searchTextValue = showPlaceholder ? placeholder : userInput; - searchText = searchTextValue !== '' ? ` ${color.dim(searchTextValue)}` : ''; + searchText = searchTextValue !== '' ? ` ${styleText('dim', searchTextValue)}` : ''; } else { searchText = ` ${this.userInputWithCursor}`; } @@ -124,7 +126,8 @@ export const autocomplete = (opts: AutocompleteOptions) => { // Show match count if filtered const matches = this.filteredOptions.length !== options.length - ? color.dim( + ? styleText( + 'dim', ` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? '' : 'es'})` ) : ''; @@ -132,29 +135,31 @@ export const autocomplete = (opts: AutocompleteOptions) => { // No matches message const noResults = this.filteredOptions.length === 0 && userInput - ? [`${color.cyan(S_BAR)} ${color.yellow('No matches found')}`] + ? [`${styleText('cyan', S_BAR)} ${styleText('yellow', 'No matches found')}`] : []; const validationError = - this.state === 'error' ? [`${color.yellow(S_BAR)} ${color.yellow(this.error)}`] : []; + this.state === 'error' + ? [`${styleText('yellow', S_BAR)} ${styleText('yellow', this.error)}`] + : []; headings.push( - `${color.cyan(S_BAR)}`, - `${color.cyan(S_BAR)} ${color.dim('Search:')}${searchText}${matches}`, + `${styleText('cyan', S_BAR)}`, + `${styleText('cyan', S_BAR)} ${styleText('dim', 'Search:')}${searchText}${matches}`, ...noResults, ...validationError ); // Show instructions const instructions = [ - `${color.dim('↑/↓')} to select`, - `${color.dim('Enter:')} confirm`, - `${color.dim('Type:')} to search`, + `${styleText('dim', '↑/↓')} to select`, + `${styleText('dim', 'Enter:')} confirm`, + `${styleText('dim', 'Type:')} to search`, ]; const footers = [ - `${color.cyan(S_BAR)} ${color.dim(instructions.join(' • '))}`, - `${color.cyan(S_BAR_END)}`, + `${styleText('cyan', S_BAR)} ${styleText('dim', instructions.join(' • '))}`, + `${styleText('cyan', S_BAR_END)}`, ]; // Render options with selection @@ -170,12 +175,12 @@ export const autocomplete = (opts: AutocompleteOptions) => { const label = getLabel(option); const hint = option.hint && option.value === this.focusedValue - ? color.dim(` (${option.hint})`) + ? styleText('dim', ` (${option.hint})`) : ''; return active - ? `${color.green(S_RADIO_ACTIVE)} ${label}${hint}` - : `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}${hint}`; + ? `${styleText('green', S_RADIO_ACTIVE)} ${label}${hint}` + : `${styleText('dim', S_RADIO_INACTIVE)} ${styleText('dim', label)}${hint}`; }, maxItems: opts.maxItems, output: opts.output, @@ -184,7 +189,7 @@ export const autocomplete = (opts: AutocompleteOptions) => { // Return the formatted prompt return [ ...headings, - ...displayOptions.map((option) => `${color.cyan(S_BAR)} ${option}`), + ...displayOptions.map((option) => `${styleText('cyan', S_BAR)} ${option}`), ...footers, ].join('\n'); } @@ -222,14 +227,16 @@ export const autocompleteMultiselect = (opts: AutocompleteMultiSelectOpti const label = option.label ?? String(option.value ?? ''); const hint = option.hint && focusedValue !== undefined && option.value === focusedValue - ? color.dim(` (${option.hint})`) + ? styleText('dim', ` (${option.hint})`) : ''; - const checkbox = isSelected ? color.green(S_CHECKBOX_SELECTED) : color.dim(S_CHECKBOX_INACTIVE); + const checkbox = isSelected + ? styleText('green', S_CHECKBOX_SELECTED) + : styleText('dim', S_CHECKBOX_INACTIVE); if (active) { return `${checkbox} ${label}${hint}`; } - return `${checkbox} ${color.dim(label)}`; + return `${checkbox} ${styleText('dim', label)}`; }; // Create text prompt which we'll use as foundation @@ -251,7 +258,7 @@ export const autocompleteMultiselect = (opts: AutocompleteMultiSelectOpti output: opts.output, render() { // Title and symbol - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + const title = `${styleText('gray', S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; // Selection counter const userInput = this.userInput; @@ -261,14 +268,15 @@ export const autocompleteMultiselect = (opts: AutocompleteMultiSelectOpti // Search input display const searchText = this.isNavigating || showPlaceholder - ? color.dim(showPlaceholder ? placeholder : userInput) // Just show plain text when in navigation mode + ? styleText('dim', showPlaceholder ? placeholder : userInput) // Just show plain text when in navigation mode : this.userInputWithCursor; const options = this.options; const matches = this.filteredOptions.length !== options.length - ? color.dim( + ? styleText( + 'dim', ` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? '' : 'es'})` ) : ''; @@ -276,28 +284,30 @@ export const autocompleteMultiselect = (opts: AutocompleteMultiSelectOpti // Render prompt state switch (this.state) { case 'submit': { - return `${title}${color.gray(S_BAR)} ${color.dim(`${this.selectedValues.length} items selected`)}`; + return `${title}${styleText('gray', S_BAR)} ${styleText('dim', `${this.selectedValues.length} items selected`)}`; } case 'cancel': { - return `${title}${color.gray(S_BAR)} ${color.strikethrough(color.dim(userInput))}`; + return `${title}${styleText('gray', S_BAR)} ${styleText('strikethrough', styleText('dim', userInput))}`; } default: { // Instructions const instructions = [ - `${color.dim('↑/↓')} to navigate`, - `${color.dim(this.isNavigating ? 'Space/Tab:' : 'Tab:')} select`, - `${color.dim('Enter:')} confirm`, - `${color.dim('Type:')} to search`, + `${styleText('dim', '↑/↓')} to navigate`, + `${styleText('dim', this.isNavigating ? 'Space/Tab:' : 'Tab:')} select`, + `${styleText('dim', 'Enter:')} confirm`, + `${styleText('dim', 'Type:')} to search`, ]; // No results message const noResults = this.filteredOptions.length === 0 && userInput - ? [`${color.cyan(S_BAR)} ${color.yellow('No matches found')}`] + ? [`${styleText('cyan', S_BAR)} ${styleText('yellow', 'No matches found')}`] : []; const errorMessage = - this.state === 'error' ? [`${color.cyan(S_BAR)} ${color.yellow(this.error)}`] : []; + this.state === 'error' + ? [`${styleText('cyan', S_BAR)} ${styleText('yellow', this.error)}`] + : []; // Get limited options for display const displayOptions = limitOptions({ @@ -312,12 +322,12 @@ export const autocompleteMultiselect = (opts: AutocompleteMultiSelectOpti // Build the prompt display return [ title, - `${color.cyan(S_BAR)} ${color.dim('Search:')} ${searchText}${matches}`, + `${styleText('cyan', S_BAR)} ${styleText('dim', 'Search:')} ${searchText}${matches}`, ...noResults, ...errorMessage, - ...displayOptions.map((option) => `${color.cyan(S_BAR)} ${option}`), - `${color.cyan(S_BAR)} ${color.dim(instructions.join(' • '))}`, - `${color.cyan(S_BAR_END)}`, + ...displayOptions.map((option) => `${styleText('cyan', S_BAR)} ${option}`), + `${styleText('cyan', S_BAR)} ${styleText('dim', instructions.join(' • '))}`, + `${styleText('cyan', S_BAR_END)}`, ].join('\n'); } } diff --git a/packages/prompts/src/common.ts b/packages/prompts/src/common.ts index 57670ab3..33dac766 100644 --- a/packages/prompts/src/common.ts +++ b/packages/prompts/src/common.ts @@ -1,7 +1,7 @@ import type { Readable, Writable } from 'node:stream'; +import { styleText } from 'node:util'; import type { State } from '@clack/core'; import isUnicodeSupported from 'is-unicode-supported'; -import color from 'picocolors'; export const unicode = isUnicodeSupported(); export const isCI = (): boolean => process.env.CI === 'true'; @@ -43,13 +43,13 @@ export const symbol = (state: State) => { switch (state) { case 'initial': case 'active': - return color.cyan(S_STEP_ACTIVE); + return styleText('cyan', S_STEP_ACTIVE); case 'cancel': - return color.red(S_STEP_CANCEL); + return styleText('red', S_STEP_CANCEL); case 'error': - return color.yellow(S_STEP_ERROR); + return styleText('yellow', S_STEP_ERROR); case 'submit': - return color.green(S_STEP_SUBMIT); + return styleText('green', S_STEP_SUBMIT); } }; diff --git a/packages/prompts/src/confirm.ts b/packages/prompts/src/confirm.ts index 4ee30acc..559e7e1a 100644 --- a/packages/prompts/src/confirm.ts +++ b/packages/prompts/src/confirm.ts @@ -1,5 +1,5 @@ +import { styleText } from 'node:util'; import { ConfirmPrompt } from '@clack/core'; -import color from 'picocolors'; import { type CommonOptions, S_BAR, @@ -26,26 +26,27 @@ export const confirm = (opts: ConfirmOptions) => { output: opts.output, initialValue: opts.initialValue ?? true, render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + const title = `${styleText('gray', S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; const value = this.value ? active : inactive; switch (this.state) { case 'submit': - return `${title}${color.gray(S_BAR)} ${color.dim(value)}`; + return `${title}${styleText('gray', S_BAR)} ${styleText('dim', value)}`; case 'cancel': - return `${title}${color.gray(S_BAR)} ${color.strikethrough( - color.dim(value) - )}\n${color.gray(S_BAR)}`; + return `${title}${styleText('gray', S_BAR)} ${styleText( + ['strikethrough', 'dim'], + value + )}\n${styleText('gray', S_BAR)}`; default: { - return `${title}${color.cyan(S_BAR)} ${ + return `${title}${styleText('cyan', S_BAR)} ${ this.value - ? `${color.green(S_RADIO_ACTIVE)} ${active}` - : `${color.dim(S_RADIO_INACTIVE)} ${color.dim(active)}` - } ${color.dim('/')} ${ + ? `${styleText('green', S_RADIO_ACTIVE)} ${active}` + : `${styleText('dim', S_RADIO_INACTIVE)} ${styleText('dim', active)}` + } ${styleText('dim', '/')} ${ !this.value - ? `${color.green(S_RADIO_ACTIVE)} ${inactive}` - : `${color.dim(S_RADIO_INACTIVE)} ${color.dim(inactive)}` - }\n${color.cyan(S_BAR_END)}\n`; + ? `${styleText('green', S_RADIO_ACTIVE)} ${inactive}` + : `${styleText('dim', S_RADIO_INACTIVE)} ${styleText('dim', inactive)}` + }\n${styleText('cyan', S_BAR_END)}\n`; } } }, diff --git a/packages/prompts/src/group-multi-select.ts b/packages/prompts/src/group-multi-select.ts index 4464b074..80b3d1fb 100644 --- a/packages/prompts/src/group-multi-select.ts +++ b/packages/prompts/src/group-multi-select.ts @@ -1,5 +1,5 @@ +import { styleText } from 'node:util'; import { GroupMultiSelectPrompt } from '@clack/core'; -import color from 'picocolors'; import { type CommonOptions, S_BAR, @@ -42,40 +42,42 @@ export const groupMultiselect = (opts: GroupMultiSelectOptions) => const prefix = isItem ? (selectableGroups ? `${isLast ? S_BAR_END : S_BAR} ` : ' ') : ''; let spacingPrefix = ''; if (groupSpacing > 0 && !isItem) { - const spacingPrefixText = `\n${color.cyan(S_BAR)}`; + const spacingPrefixText = `\n${styleText('cyan', S_BAR)}`; spacingPrefix = `${spacingPrefixText.repeat(groupSpacing - 1)}${spacingPrefixText} `; } if (state === 'active') { - return `${spacingPrefix}${color.dim(prefix)}${color.cyan(S_CHECKBOX_ACTIVE)} ${label}${ - option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' + return `${spacingPrefix}${styleText('dim', prefix)}${styleText('cyan', S_CHECKBOX_ACTIVE)} ${label}${ + option.hint ? ` ${styleText('dim', `(${option.hint})`)}` : '' }`; } if (state === 'group-active') { - return `${spacingPrefix}${prefix}${color.cyan(S_CHECKBOX_ACTIVE)} ${color.dim(label)}`; + return `${spacingPrefix}${prefix}${styleText('cyan', S_CHECKBOX_ACTIVE)} ${styleText('dim', label)}`; } if (state === 'group-active-selected') { - return `${spacingPrefix}${prefix}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; + return `${spacingPrefix}${prefix}${styleText('green', S_CHECKBOX_SELECTED)} ${styleText('dim', label)}`; } if (state === 'selected') { - const selectedCheckbox = isItem || selectableGroups ? color.green(S_CHECKBOX_SELECTED) : ''; - return `${spacingPrefix}${color.dim(prefix)}${selectedCheckbox} ${color.dim(label)}${ - option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' + const selectedCheckbox = + isItem || selectableGroups ? styleText('green', S_CHECKBOX_SELECTED) : ''; + return `${spacingPrefix}${styleText('dim', prefix)}${selectedCheckbox} ${styleText('dim', label)}${ + option.hint ? ` ${styleText('dim', `(${option.hint})`)}` : '' }`; } if (state === 'cancelled') { - return `${color.strikethrough(color.dim(label))}`; + return `${styleText(['strikethrough', 'dim'], label)}`; } if (state === 'active-selected') { - return `${spacingPrefix}${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${label}${ - option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' + return `${spacingPrefix}${styleText('dim', prefix)}${styleText('green', S_CHECKBOX_SELECTED)} ${label}${ + option.hint ? ` ${styleText('dim', `(${option.hint})`)}` : '' }`; } if (state === 'submitted') { - return `${color.dim(label)}`; + return `${styleText('dim', label)}`; } - const unselectedCheckbox = isItem || selectableGroups ? color.dim(S_CHECKBOX_INACTIVE) : ''; - return `${spacingPrefix}${color.dim(prefix)}${unselectedCheckbox} ${color.dim(label)}`; + const unselectedCheckbox = + isItem || selectableGroups ? styleText('dim', S_CHECKBOX_INACTIVE) : ''; + return `${spacingPrefix}${styleText('dim', prefix)}${unselectedCheckbox} ${styleText('dim', label)}`; }; const required = opts.required ?? true; @@ -90,16 +92,19 @@ export const groupMultiselect = (opts: GroupMultiSelectOptions) => selectableGroups, validate(selected: Value[] | undefined) { if (required && (selected === undefined || selected.length === 0)) - return `Please select at least one option.\n${color.reset( - color.dim( - `Press ${color.gray(color.bgWhite(color.inverse(' space ')))} to select, ${color.gray( - color.bgWhite(color.inverse(' enter ')) + return `Please select at least one option.\n${styleText( + 'reset', + styleText( + 'dim', + `Press ${styleText(['gray', 'bgWhite', 'inverse'], ' space ')} to select, ${styleText( + 'gray', + styleText(['bgWhite', 'inverse'], ' enter ') )} to submit` ) )}`; }, render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + const title = `${styleText('gray', S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; const value = this.value ?? []; switch (this.state) { @@ -108,26 +113,26 @@ export const groupMultiselect = (opts: GroupMultiSelectOptions) => .filter(({ value: optionValue }) => value.includes(optionValue)) .map((option) => opt(option, 'submitted')); const optionsText = - selectedOptions.length === 0 ? '' : ` ${selectedOptions.join(color.dim(', '))}`; - return `${title}${color.gray(S_BAR)}${optionsText}`; + selectedOptions.length === 0 ? '' : ` ${selectedOptions.join(styleText('dim', ', '))}`; + return `${title}${styleText('gray', S_BAR)}${optionsText}`; } case 'cancel': { const label = this.options .filter(({ value: optionValue }) => value.includes(optionValue)) .map((option) => opt(option, 'cancelled')) - .join(color.dim(', ')); - return `${title}${color.gray(S_BAR)} ${ - label.trim() ? `${label}\n${color.gray(S_BAR)}` : '' + .join(styleText('dim', ', ')); + return `${title}${styleText('gray', S_BAR)} ${ + label.trim() ? `${label}\n${styleText('gray', S_BAR)}` : '' }`; } case 'error': { const footer = this.error .split('\n') .map((ln, i) => - i === 0 ? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}` : ` ${ln}` + i === 0 ? `${styleText('yellow', S_BAR_END)} ${styleText('yellow', ln)}` : ` ${ln}` ) .join('\n'); - return `${title}${color.yellow(S_BAR)} ${this.options + return `${title}${styleText('yellow', S_BAR)} ${this.options .map((option, i, options) => { const selected = value.includes(option.value) || @@ -148,7 +153,7 @@ export const groupMultiselect = (opts: GroupMultiSelectOptions) => } return opt(option, active ? 'active' : 'inactive', options); }) - .join(`\n${color.yellow(S_BAR)} `)}\n${footer}\n`; + .join(`\n${styleText('yellow', S_BAR)} `)}\n${footer}\n`; } default: { const optionsText = this.options @@ -178,9 +183,9 @@ export const groupMultiselect = (opts: GroupMultiSelectOptions) => const prefix = i !== 0 && !optionText.startsWith('\n') ? ' ' : ''; return `${prefix}${optionText}`; }) - .join(`\n${color.cyan(S_BAR)}`); + .join(`\n${styleText('cyan', S_BAR)}`); const optionsPrefix = optionsText.startsWith('\n') ? '' : ' '; - return `${title}${color.cyan(S_BAR)}${optionsPrefix}${optionsText}\n${color.cyan(S_BAR_END)}\n`; + return `${title}${styleText('cyan', S_BAR)}${optionsPrefix}${optionsText}\n${styleText('cyan', S_BAR_END)}\n`; } } }, diff --git a/packages/prompts/src/limit-options.ts b/packages/prompts/src/limit-options.ts index 11d8f6d1..dcc10467 100644 --- a/packages/prompts/src/limit-options.ts +++ b/packages/prompts/src/limit-options.ts @@ -1,7 +1,7 @@ import type { Writable } from 'node:stream'; +import { styleText } from 'node:util'; import { getColumns, getRows } from '@clack/core'; import { wrapAnsi } from 'fast-wrap-ansi'; -import color from 'picocolors'; import type { CommonOptions } from './common.js'; export interface LimitOptionsParams extends CommonOptions { @@ -41,7 +41,7 @@ export const limitOptions = (params: LimitOptionsParams): stri const rowPadding = params.rowPadding ?? 4; const maxWidth = columns - columnPadding; const rows = getRows(output); - const overflowFormat = color.dim('...'); + const overflowFormat = styleText('dim', '...'); const paramMaxItems = params.maxItems ?? Number.POSITIVE_INFINITY; const outputMaxItems = Math.max(rows - rowPadding, 0); diff --git a/packages/prompts/src/log.ts b/packages/prompts/src/log.ts index ff4e00cd..431fc0a8 100644 --- a/packages/prompts/src/log.ts +++ b/packages/prompts/src/log.ts @@ -1,4 +1,4 @@ -import color from 'picocolors'; +import { styleText } from 'node:util'; import { type CommonOptions, S_BAR, @@ -19,8 +19,8 @@ export const log = { message: ( message: string | string[] = [], { - symbol = color.gray(S_BAR), - secondarySymbol = color.gray(S_BAR), + symbol = styleText('gray', S_BAR), + secondarySymbol = styleText('gray', S_BAR), output = process.stdout, spacing = 1, }: LogMessageOptions = {} @@ -48,22 +48,22 @@ export const log = { output.write(`${parts.join('\n')}\n`); }, info: (message: string, opts?: LogMessageOptions) => { - log.message(message, { ...opts, symbol: color.blue(S_INFO) }); + log.message(message, { ...opts, symbol: styleText('blue', S_INFO) }); }, success: (message: string, opts?: LogMessageOptions) => { - log.message(message, { ...opts, symbol: color.green(S_SUCCESS) }); + log.message(message, { ...opts, symbol: styleText('green', S_SUCCESS) }); }, step: (message: string, opts?: LogMessageOptions) => { - log.message(message, { ...opts, symbol: color.green(S_STEP_SUBMIT) }); + log.message(message, { ...opts, symbol: styleText('green', S_STEP_SUBMIT) }); }, warn: (message: string, opts?: LogMessageOptions) => { - log.message(message, { ...opts, symbol: color.yellow(S_WARN) }); + log.message(message, { ...opts, symbol: styleText('yellow', S_WARN) }); }, /** alias for `log.warn()`. */ warning: (message: string, opts?: LogMessageOptions) => { log.warn(message, opts); }, error: (message: string, opts?: LogMessageOptions) => { - log.message(message, { ...opts, symbol: color.red(S_ERROR) }); + log.message(message, { ...opts, symbol: styleText('red', S_ERROR) }); }, }; diff --git a/packages/prompts/src/messages.ts b/packages/prompts/src/messages.ts index db96d46e..aff5f422 100644 --- a/packages/prompts/src/messages.ts +++ b/packages/prompts/src/messages.ts @@ -1,18 +1,18 @@ import type { Writable } from 'node:stream'; -import color from 'picocolors'; +import { styleText } from 'node:util'; import { type CommonOptions, S_BAR, S_BAR_END, S_BAR_START } from './common.js'; export const cancel = (message = '', opts?: CommonOptions) => { const output: Writable = opts?.output ?? process.stdout; - output.write(`${color.gray(S_BAR_END)} ${color.red(message)}\n\n`); + output.write(`${styleText('gray', S_BAR_END)} ${styleText('red', message)}\n\n`); }; export const intro = (title = '', opts?: CommonOptions) => { const output: Writable = opts?.output ?? process.stdout; - output.write(`${color.gray(S_BAR_START)} ${title}\n`); + output.write(`${styleText('gray', S_BAR_START)} ${title}\n`); }; export const outro = (message = '', opts?: CommonOptions) => { const output: Writable = opts?.output ?? process.stdout; - output.write(`${color.gray(S_BAR)}\n${color.gray(S_BAR_END)} ${message}\n\n`); + output.write(`${styleText('gray', S_BAR)}\n${styleText('gray', S_BAR_END)} ${message}\n\n`); }; diff --git a/packages/prompts/src/multi-select.ts b/packages/prompts/src/multi-select.ts index 75ca9045..d70c6887 100644 --- a/packages/prompts/src/multi-select.ts +++ b/packages/prompts/src/multi-select.ts @@ -1,5 +1,5 @@ +import { styleText } from 'node:util'; import { MultiSelectPrompt } from '@clack/core'; -import color from 'picocolors'; import { type CommonOptions, S_BAR, @@ -34,32 +34,32 @@ export const multiselect = (opts: MultiSelectOptions) => { ) => { const label = option.label ?? String(option.value); if (state === 'disabled') { - return `${color.gray(S_CHECKBOX_INACTIVE)} ${color.gray(label)}${ - option.hint ? ` ${color.dim(`(${option.hint ?? 'disabled'})`)}` : '' + return `${styleText('gray', S_CHECKBOX_INACTIVE)} ${styleText('gray', label)}${ + option.hint ? ` ${styleText('dim', `(${option.hint ?? 'disabled'})`)}` : '' }`; } if (state === 'active') { - return `${color.cyan(S_CHECKBOX_ACTIVE)} ${label}${ - option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' + return `${styleText('cyan', S_CHECKBOX_ACTIVE)} ${label}${ + option.hint ? ` ${styleText('dim', `(${option.hint})`)}` : '' }`; } if (state === 'selected') { - return `${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}${ - option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' + return `${styleText('green', S_CHECKBOX_SELECTED)} ${styleText('dim', label)}${ + option.hint ? ` ${styleText('dim', `(${option.hint})`)}` : '' }`; } if (state === 'cancelled') { - return `${color.strikethrough(color.dim(label))}`; + return `${styleText('strikethrough', styleText('dim', label))}`; } if (state === 'active-selected') { - return `${color.green(S_CHECKBOX_SELECTED)} ${label}${ - option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' + return `${styleText('green', S_CHECKBOX_SELECTED)} ${label}${ + option.hint ? ` ${styleText('dim', `(${option.hint})`)}` : '' }`; } if (state === 'submitted') { - return `${color.dim(label)}`; + return `${styleText('dim', label)}`; } - return `${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`; + return `${styleText('dim', S_CHECKBOX_INACTIVE)} ${styleText('dim', label)}`; }; const required = opts.required ?? true; @@ -73,16 +73,19 @@ export const multiselect = (opts: MultiSelectOptions) => { cursorAt: opts.cursorAt, validate(selected: Value[] | undefined) { if (required && (selected === undefined || selected.length === 0)) - return `Please select at least one option.\n${color.reset( - color.dim( - `Press ${color.gray(color.bgWhite(color.inverse(' space ')))} to select, ${color.gray( - color.bgWhite(color.inverse(' enter ')) + return `Please select at least one option.\n${styleText( + 'reset', + styleText( + 'dim', + `Press ${styleText(['gray', 'bgWhite', 'inverse'], ' space ')} to select, ${styleText( + 'gray', + styleText('bgWhite', styleText('inverse', ' enter ')) )} to submit` ) )}`; }, render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + const title = `${styleText('gray', S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; const value = this.value ?? []; const styleOption = (option: Option, active: boolean) => { @@ -101,28 +104,28 @@ export const multiselect = (opts: MultiSelectOptions) => { switch (this.state) { case 'submit': { - return `${title}${color.gray(S_BAR)} ${ + return `${title}${styleText('gray', S_BAR)} ${ this.options .filter(({ value: optionValue }) => value.includes(optionValue)) .map((option) => opt(option, 'submitted')) - .join(color.dim(', ')) || color.dim('none') + .join(styleText('dim', ', ')) || styleText('dim', 'none') }`; } case 'cancel': { const label = this.options .filter(({ value: optionValue }) => value.includes(optionValue)) .map((option) => opt(option, 'cancelled')) - .join(color.dim(', ')); - return `${title}${color.gray(S_BAR)}${ - label.trim() ? ` ${label}\n${color.gray(S_BAR)}` : '' + .join(styleText('dim', ', ')); + return `${title}${styleText('gray', S_BAR)}${ + label.trim() ? ` ${label}\n${styleText('gray', S_BAR)}` : '' }`; } case 'error': { - const prefix = `${color.yellow(S_BAR)} `; + const prefix = `${styleText('yellow', S_BAR)} `; const footer = this.error .split('\n') .map((ln, i) => - i === 0 ? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}` : ` ${ln}` + i === 0 ? `${styleText('yellow', S_BAR_END)} ${styleText('yellow', ln)}` : ` ${ln}` ) .join('\n'); return `${title}${prefix}${limitOptions({ @@ -135,7 +138,7 @@ export const multiselect = (opts: MultiSelectOptions) => { }).join(`\n${prefix}`)}\n${footer}\n`; } default: { - const prefix = `${color.cyan(S_BAR)} `; + const prefix = `${styleText('cyan', S_BAR)} `; return `${title}${prefix}${limitOptions({ output: opts.output, options: this.options, @@ -143,7 +146,7 @@ export const multiselect = (opts: MultiSelectOptions) => { maxItems: opts.maxItems, columnPadding: prefix.length, style: styleOption, - }).join(`\n${prefix}`)}\n${color.cyan(S_BAR_END)}\n`; + }).join(`\n${prefix}`)}\n${styleText('cyan', S_BAR_END)}\n`; } } }, diff --git a/packages/prompts/src/note.ts b/packages/prompts/src/note.ts index bd80d07b..50f7b0e9 100644 --- a/packages/prompts/src/note.ts +++ b/packages/prompts/src/note.ts @@ -1,9 +1,9 @@ import process from 'node:process'; import type { Writable } from 'node:stream'; +import { styleText } from 'node:util'; import { getColumns } from '@clack/core'; import stringWidth from 'fast-string-width'; import { type Options as WrapAnsiOptions, wrapAnsi } from 'fast-wrap-ansi'; -import color from 'picocolors'; import { type CommonOptions, S_BAR, @@ -19,7 +19,7 @@ export interface NoteOptions extends CommonOptions { format?: FormatFn; } -const defaultNoteFormatter = (line: string): string => color.dim(line); +const defaultNoteFormatter = (line: string): string => styleText('dim', line); const wrapWithFormat = (message: string, width: number, format: FormatFn): string => { const opts: WrapAnsiOptions = { @@ -49,12 +49,14 @@ export const note = (message = '', title = '', opts?: NoteOptions) => { ) + 2; const msg = lines .map( - (ln) => `${color.gray(S_BAR)} ${ln}${' '.repeat(len - stringWidth(ln))}${color.gray(S_BAR)}` + (ln) => + `${styleText('gray', S_BAR)} ${ln}${' '.repeat(len - stringWidth(ln))}${styleText('gray', S_BAR)}` ) .join('\n'); output.write( - `${color.gray(S_BAR)}\n${color.green(S_STEP_SUBMIT)} ${color.reset(title)} ${color.gray( + `${styleText('gray', S_BAR)}\n${styleText('green', S_STEP_SUBMIT)} ${styleText('reset', title)} ${styleText( + 'gray', S_BAR_H.repeat(Math.max(len - titleLen - 1, 1)) + S_CORNER_TOP_RIGHT - )}\n${msg}\n${color.gray(S_CONNECT_LEFT + S_BAR_H.repeat(len + 2) + S_CORNER_BOTTOM_RIGHT)}\n` + )}\n${msg}\n${styleText('gray', S_CONNECT_LEFT + S_BAR_H.repeat(len + 2) + S_CORNER_BOTTOM_RIGHT)}\n` ); }; diff --git a/packages/prompts/src/password.ts b/packages/prompts/src/password.ts index 8010960b..a3c4a3ca 100644 --- a/packages/prompts/src/password.ts +++ b/packages/prompts/src/password.ts @@ -1,5 +1,5 @@ +import { styleText } from 'node:util'; import { PasswordPrompt } from '@clack/core'; -import color from 'picocolors'; import { type CommonOptions, S_BAR, S_BAR_END, S_PASSWORD_MASK, symbol } from './common.js'; export interface PasswordOptions extends CommonOptions { @@ -16,7 +16,7 @@ export const password = (opts: PasswordOptions) => { input: opts.input, output: opts.output, render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + const title = `${styleText('gray', S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; const userInput = this.userInputWithCursor; const masked = this.masked; @@ -26,22 +26,25 @@ export const password = (opts: PasswordOptions) => { if (opts.clearOnError) { this.clear(); } - return `${title.trim()}\n${color.yellow(S_BAR)}${maskedText}\n${color.yellow( + return `${title.trim()}\n${styleText('yellow', S_BAR)}${maskedText}\n${styleText( + 'yellow', S_BAR_END - )} ${color.yellow(this.error)}\n`; + )} ${styleText('yellow', this.error)}\n`; } case 'submit': { - const maskedText = masked ? ` ${color.dim(masked)}` : ''; - return `${title}${color.gray(S_BAR)}${maskedText}`; + const maskedText = masked ? ` ${styleText('dim', masked)}` : ''; + return `${title}${styleText('gray', S_BAR)}${maskedText}`; } case 'cancel': { - const maskedText = masked ? ` ${color.strikethrough(color.dim(masked))}` : ''; - return `${title}${color.gray(S_BAR)}${maskedText}${ - masked ? `\n${color.gray(S_BAR)}` : '' + const maskedText = masked + ? ` ${styleText(['strikethrough', 'dim'], masked)}` + : ''; + return `${title}${styleText('gray', S_BAR)}${maskedText}${ + masked ? `\n${styleText('gray', S_BAR)}` : '' }`; } default: - return `${title}${color.cyan(S_BAR)} ${userInput}\n${color.cyan(S_BAR_END)}\n`; + return `${title}${styleText('cyan', S_BAR)} ${userInput}\n${styleText('cyan', S_BAR_END)}\n`; } }, }).prompt() as Promise; diff --git a/packages/prompts/src/progress-bar.ts b/packages/prompts/src/progress-bar.ts index 8801833c..2a22de7b 100644 --- a/packages/prompts/src/progress-bar.ts +++ b/packages/prompts/src/progress-bar.ts @@ -1,5 +1,5 @@ +import { styleText } from 'node:util'; import type { State } from '@clack/core'; -import color from 'picocolors'; import { unicodeOr } from './common.js'; import { type SpinnerOptions, type SpinnerResult, spinner } from './spinner.js'; @@ -36,19 +36,19 @@ export function progress({ switch (state) { case 'initial': case 'active': - return color.magenta; + return (text: string) => styleText('magenta', text); case 'error': case 'cancel': - return color.red; + return (text: string) => styleText('red', text); case 'submit': - return color.green; + return (text: string) => styleText('green', text); default: - return color.magenta; + return (text: string) => styleText('magenta', text); } }; const drawProgress = (state: State, msg: string) => { const active = Math.floor((value / max) * size); - return `${activeStyle(state)(S_PROGRESS_CHAR[style].repeat(active))}${color.dim(S_PROGRESS_CHAR[style].repeat(size - active))} ${msg}`; + return `${activeStyle(state)(S_PROGRESS_CHAR[style].repeat(active))}${styleText('dim', S_PROGRESS_CHAR[style].repeat(size - active))} ${msg}`; }; const start = (msg = '') => { diff --git a/packages/prompts/src/select-key.ts b/packages/prompts/src/select-key.ts index f5bbbf69..93fe899a 100644 --- a/packages/prompts/src/select-key.ts +++ b/packages/prompts/src/select-key.ts @@ -1,5 +1,5 @@ +import { styleText } from 'node:util'; import { SelectKeyPrompt } from '@clack/core'; -import color from 'picocolors'; import { S_BAR, S_BAR_END, symbol } from './common.js'; import type { Option, SelectOptions } from './select.js'; @@ -10,18 +10,18 @@ export const selectKey = (opts: SelectOptions) => { ) => { const label = option.label ?? String(option.value); if (state === 'selected') { - return `${color.dim(label)}`; + return `${styleText('dim', label)}`; } if (state === 'cancelled') { - return `${color.strikethrough(color.dim(label))}`; + return `${styleText(['strikethrough', 'dim'], label)}`; } if (state === 'active') { - return `${color.bgCyan(color.gray(` ${option.value} `))} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : '' + return `${styleText(['bgCyan', 'gray'], ` ${option.value} `)} ${label} ${ + option.hint ? styleText('dim', `(${option.hint})`) : '' }`; } - return `${color.gray(color.bgWhite(color.inverse(` ${option.value} `)))} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : '' + return `${styleText(['gray', 'bgWhite', 'inverse'], ` ${option.value} `)} ${label} ${ + option.hint ? styleText('dim', `(${option.hint})`) : '' }`; }; @@ -32,22 +32,23 @@ export const selectKey = (opts: SelectOptions) => { output: opts.output, initialValue: opts.initialValue, render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + const title = `${styleText('gray', S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; switch (this.state) { case 'submit': - return `${title}${color.gray(S_BAR)} ${opt( + return `${title}${styleText('gray', S_BAR)} ${opt( this.options.find((opt) => opt.value === this.value) ?? opts.options[0], 'selected' )}`; case 'cancel': - return `${title}${color.gray(S_BAR)} ${opt(this.options[0], 'cancelled')}\n${color.gray( + return `${title}${styleText('gray', S_BAR)} ${opt(this.options[0], 'cancelled')}\n${styleText( + 'gray', S_BAR )}`; default: { - return `${title}${color.cyan(S_BAR)} ${this.options + return `${title}${styleText('cyan', S_BAR)} ${this.options .map((option, i) => opt(option, i === this.cursor ? 'active' : 'inactive')) - .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; + .join(`\n${styleText('cyan', S_BAR)} `)}\n${styleText('cyan', S_BAR_END)}\n`; } } }, diff --git a/packages/prompts/src/select.ts b/packages/prompts/src/select.ts index b091161c..996f33a9 100644 --- a/packages/prompts/src/select.ts +++ b/packages/prompts/src/select.ts @@ -1,5 +1,5 @@ +import { styleText } from 'node:util'; import { SelectPrompt } from '@clack/core'; -import color from 'picocolors'; import { type CommonOptions, S_BAR, @@ -79,19 +79,19 @@ export const select = (opts: SelectOptions) => { const label = option.label ?? String(option.value); switch (state) { case 'disabled': - return `${color.gray(S_RADIO_INACTIVE)} ${color.gray(label)}${ - option.hint ? ` ${color.dim(`(${option.hint ?? 'disabled'})`)}` : '' + return `${styleText('gray', S_RADIO_INACTIVE)} ${styleText('gray', label)}${ + option.hint ? ` ${styleText('dim', `(${option.hint ?? 'disabled'})`)}` : '' }`; case 'selected': - return `${color.dim(label)}`; + return `${styleText('dim', label)}`; case 'active': - return `${color.green(S_RADIO_ACTIVE)} ${label}${ - option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' + return `${styleText('green', S_RADIO_ACTIVE)} ${label}${ + option.hint ? ` ${styleText('dim', `(${option.hint})`)}` : '' }`; case 'cancelled': - return `${color.strikethrough(color.dim(label))}`; + return `${styleText(['strikethrough', 'dim'], label)}`; default: - return `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}`; + return `${styleText('dim', S_RADIO_INACTIVE)} ${styleText('dim', label)}`; } }; @@ -102,18 +102,18 @@ export const select = (opts: SelectOptions) => { output: opts.output, initialValue: opts.initialValue, render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + const title = `${styleText('gray', S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; switch (this.state) { case 'submit': - return `${title}${color.gray(S_BAR)} ${opt(this.options[this.cursor], 'selected')}`; + return `${title}${styleText('gray', S_BAR)} ${opt(this.options[this.cursor], 'selected')}`; case 'cancel': - return `${title}${color.gray(S_BAR)} ${opt( + return `${title}${styleText('gray', S_BAR)} ${opt( this.options[this.cursor], 'cancelled' - )}\n${color.gray(S_BAR)}`; + )}\n${styleText('gray', S_BAR)}`; default: { - const prefix = `${color.cyan(S_BAR)} `; + const prefix = `${styleText('cyan', S_BAR)} `; return `${title}${prefix}${limitOptions({ output: opts.output, cursor: this.cursor, @@ -122,7 +122,7 @@ export const select = (opts: SelectOptions) => { columnPadding: prefix.length, style: (item, active) => opt(item, item.disabled ? 'disabled' : active ? 'active' : 'inactive'), - }).join(`\n${prefix}`)}\n${color.cyan(S_BAR_END)}\n`; + }).join(`\n${prefix}`)}\n${styleText('cyan', S_BAR_END)}\n`; } } }, diff --git a/packages/prompts/src/spinner.ts b/packages/prompts/src/spinner.ts index 498a1f56..a972c799 100644 --- a/packages/prompts/src/spinner.ts +++ b/packages/prompts/src/spinner.ts @@ -1,6 +1,6 @@ +import { styleText } from 'node:util'; import { block, getColumns, settings } from '@clack/core'; import { wrapAnsi } from 'fast-wrap-ansi'; -import color from 'picocolors'; import { cursor, erase } from 'sisteransi'; import { type CommonOptions, @@ -29,7 +29,7 @@ export interface SpinnerResult { readonly isCancelled: boolean; } -const defaultStyleFn: SpinnerOptions['styleFrame'] = color.magenta; +const defaultStyleFn: SpinnerOptions['styleFrame'] = (frame) => styleText('magenta', frame); export const spinner = ({ indicator = 'dots', @@ -129,7 +129,7 @@ export const spinner = ({ unblock = block({ output }); _message = removeTrailingDots(msg); _origin = performance.now(); - output.write(`${color.gray(S_BAR)}\n`); + output.write(`${styleText('gray', S_BAR)}\n`); let frameIndex = 0; let indicatorTimer = 0; registerHooks(); @@ -170,10 +170,10 @@ export const spinner = ({ clearPrevMessage(); const step = code === 0 - ? color.green(S_STEP_SUBMIT) + ? styleText('green', S_STEP_SUBMIT) : code === 1 - ? color.red(S_STEP_CANCEL) - : color.red(S_STEP_ERROR); + ? styleText('red', S_STEP_CANCEL) + : styleText('red', S_STEP_ERROR); _message = msg ?? _message; if (indicator === 'timer') { output.write(`${step} ${_message} ${formatTimer(_origin)}\n`); diff --git a/packages/prompts/src/stream.ts b/packages/prompts/src/stream.ts index 19e79c11..4ff3db5e 100644 --- a/packages/prompts/src/stream.ts +++ b/packages/prompts/src/stream.ts @@ -1,9 +1,8 @@ -import { stripVTControlCharacters as strip } from 'node:util'; -import color from 'picocolors'; +import { stripVTControlCharacters as strip, styleText } from 'node:util'; import { S_BAR, S_ERROR, S_INFO, S_STEP_SUBMIT, S_SUCCESS, S_WARN } from './common.js'; import type { LogMessageOptions } from './log.js'; -const prefix = `${color.gray(S_BAR)} `; +const prefix = `${styleText('gray', S_BAR)} `; // TODO (43081j): this currently doesn't support custom `output` writables // because we rely on `columns` existing (i.e. `process.stdout.columns). @@ -13,9 +12,9 @@ const prefix = `${color.gray(S_BAR)} `; export const stream = { message: async ( iterable: Iterable | AsyncIterable, - { symbol = color.gray(S_BAR) }: LogMessageOptions = {} + { symbol = styleText('gray', S_BAR) }: LogMessageOptions = {} ) => { - process.stdout.write(`${color.gray(S_BAR)}\n${symbol} `); + process.stdout.write(`${styleText('gray', S_BAR)}\n${symbol} `); let lineWidth = 3; for await (let chunk of iterable) { chunk = chunk.replace(/\n/g, `\n${prefix}`); @@ -34,22 +33,22 @@ export const stream = { process.stdout.write('\n'); }, info: (iterable: Iterable | AsyncIterable) => { - return stream.message(iterable, { symbol: color.blue(S_INFO) }); + return stream.message(iterable, { symbol: styleText('blue', S_INFO) }); }, success: (iterable: Iterable | AsyncIterable) => { - return stream.message(iterable, { symbol: color.green(S_SUCCESS) }); + return stream.message(iterable, { symbol: styleText('green', S_SUCCESS) }); }, step: (iterable: Iterable | AsyncIterable) => { - return stream.message(iterable, { symbol: color.green(S_STEP_SUBMIT) }); + return stream.message(iterable, { symbol: styleText('green', S_STEP_SUBMIT) }); }, warn: (iterable: Iterable | AsyncIterable) => { - return stream.message(iterable, { symbol: color.yellow(S_WARN) }); + return stream.message(iterable, { symbol: styleText('yellow', S_WARN) }); }, /** alias for `log.warn()`. */ warning: (iterable: Iterable | AsyncIterable) => { return stream.warn(iterable); }, error: (iterable: Iterable | AsyncIterable) => { - return stream.message(iterable, { symbol: color.red(S_ERROR) }); + return stream.message(iterable, { symbol: styleText('red', S_ERROR) }); }, }; diff --git a/packages/prompts/src/task-log.ts b/packages/prompts/src/task-log.ts index a484499f..6b7d78c6 100644 --- a/packages/prompts/src/task-log.ts +++ b/packages/prompts/src/task-log.ts @@ -1,6 +1,6 @@ import type { Writable } from 'node:stream'; +import { styleText } from 'node:util'; import { getColumns } from '@clack/core'; -import color from 'picocolors'; import { erase } from 'sisteransi'; import { type CommonOptions, @@ -42,14 +42,14 @@ interface BufferEntry { export const taskLog = (opts: TaskLogOptions) => { const output: Writable = opts.output ?? process.stdout; const columns = getColumns(output); - const secondarySymbol = color.gray(S_BAR); + const secondarySymbol = styleText('gray', S_BAR); const spacing = opts.spacing ?? 1; const barSize = 3; const retainLog = opts.retainLog === true; const isTTY = !isCIFn() && isTTYFn(output); output.write(`${secondarySymbol}\n`); - output.write(`${color.green(S_STEP_SUBMIT)} ${opts.title}\n`); + output.write(`${styleText('green', S_STEP_SUBMIT)} ${opts.title}\n`); for (let i = 0; i < spacing; i++) { output.write(`${secondarySymbol}\n`); } @@ -103,19 +103,25 @@ export const taskLog = (opts: TaskLogOptions) => { const printBuffer = (buffer: BufferEntry, messageSpacing?: number, full?: boolean): void => { const messages = full ? `${buffer.full}\n${buffer.value}` : buffer.value; if (buffer.header !== undefined && buffer.header !== '') { - log.message(buffer.header.split('\n').map(color.bold), { + log.message( + buffer.header.split('\n').map((line) => styleText('bold', line)), + { + output, + secondarySymbol, + symbol: secondarySymbol, + spacing: 0, + } + ); + } + log.message( + messages.split('\n').map((line) => styleText('dim', line)), + { output, secondarySymbol, symbol: secondarySymbol, - spacing: 0, - }); - } - log.message(messages.split('\n').map(color.dim), { - output, - secondarySymbol, - symbol: secondarySymbol, - spacing: messageSpacing ?? spacing, - }); + spacing: messageSpacing ?? spacing, + } + ); }; const renderBuffer = (): void => { for (const buffer of buffers) { diff --git a/packages/prompts/src/text.ts b/packages/prompts/src/text.ts index 244f7c8d..0a362e80 100644 --- a/packages/prompts/src/text.ts +++ b/packages/prompts/src/text.ts @@ -1,5 +1,5 @@ +import { styleText } from 'node:util'; import { TextPrompt } from '@clack/core'; -import color from 'picocolors'; import { type CommonOptions, S_BAR, S_BAR_END, symbol } from './common.js'; export interface TextOptions extends CommonOptions { @@ -20,30 +20,31 @@ export const text = (opts: TextOptions) => { signal: opts.signal, input: opts.input, render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + const title = `${styleText('gray', S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; const placeholder = opts.placeholder - ? color.inverse(opts.placeholder[0]) + color.dim(opts.placeholder.slice(1)) - : color.inverse(color.hidden('_')); + ? styleText('inverse', opts.placeholder[0]) + styleText('dim', opts.placeholder.slice(1)) + : styleText(['inverse', 'hidden'], '_'); const userInput = !this.userInput ? placeholder : this.userInputWithCursor; const value = this.value ?? ''; switch (this.state) { case 'error': { - const errorText = this.error ? ` ${color.yellow(this.error)}` : ''; - return `${title.trim()}\n${color.yellow(S_BAR)} ${userInput}\n${color.yellow( + const errorText = this.error ? ` ${styleText('yellow', this.error)}` : ''; + return `${title.trim()}\n${styleText('yellow', S_BAR)} ${userInput}\n${styleText( + 'yellow', S_BAR_END )}${errorText}\n`; } case 'submit': { - const valueText = value ? ` ${color.dim(value)}` : ''; - return `${title}${color.gray(S_BAR)}${valueText}`; + const valueText = value ? ` ${styleText('dim', value)}` : ''; + return `${title}${styleText('gray', S_BAR)}${valueText}`; } case 'cancel': { - const valueText = value ? ` ${color.strikethrough(color.dim(value))}` : ''; - return `${title}${color.gray(S_BAR)}${valueText}${value.trim() ? `\n${color.gray(S_BAR)}` : ''}`; + const valueText = value ? ` ${styleText(['strikethrough', 'dim'], value)}` : ''; + return `${title}${styleText('gray', S_BAR)}${valueText}${value.trim() ? `\n${styleText('gray', S_BAR)}` : ''}`; } default: - return `${title}${color.cyan(S_BAR)} ${userInput}\n${color.cyan(S_BAR_END)}\n`; + return `${title}${styleText('cyan', S_BAR)} ${userInput}\n${styleText('cyan', S_BAR_END)}\n`; } }, }).prompt() as Promise; diff --git a/packages/prompts/test/box.test.ts b/packages/prompts/test/box.test.ts index 31105984..f3c82532 100644 --- a/packages/prompts/test/box.test.ts +++ b/packages/prompts/test/box.test.ts @@ -1,4 +1,4 @@ -import colors from 'picocolors'; +import { styleText } from 'node:util'; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; import * as prompts from '../src/index.js'; import { MockReadable, MockWritable } from './test-utils.js'; @@ -229,7 +229,7 @@ describe.each(['true', 'false'])('box (isCI = %s)', (isCI) => { input, output, width: 'auto', - formatBorder: colors.red, + formatBorder: (text: string) => styleText('red', text), }); expect(output.buffer).toMatchSnapshot(); diff --git a/packages/prompts/test/limit-options.test.ts b/packages/prompts/test/limit-options.test.ts index f48f4a7a..aaee854c 100644 --- a/packages/prompts/test/limit-options.test.ts +++ b/packages/prompts/test/limit-options.test.ts @@ -1,4 +1,4 @@ -import color from 'picocolors'; +import { styleText } from 'node:util'; import { beforeEach, describe, expect, test } from 'vitest'; import { type LimitOptionsParams, limitOptions } from '../src/index.js'; import { MockWritable } from './test-utils.js'; @@ -39,7 +39,7 @@ describe('limitOptions', () => { ]; options.maxItems = 3; const result = limitOptions(options); - expect(result).toEqual(['Item 1', 'Item 2', 'Item 3', 'Item 4', color.dim('...')]); + expect(result).toEqual(['Item 1', 'Item 2', 'Item 3', 'Item 4', styleText('dim', '...')]); }); test('returns sliding window when cursor moves down', async () => { @@ -59,7 +59,13 @@ describe('limitOptions', () => { options.maxItems = 5; options.cursor = 6; const result = limitOptions(options); - expect(result).toEqual([color.dim('...'), 'Item 6', 'Item 7', 'Item 8', color.dim('...')]); + expect(result).toEqual([ + styleText('dim', '...'), + 'Item 6', + 'Item 7', + 'Item 8', + styleText('dim', '...'), + ]); }); test('returns sliding window near end of list', async () => { @@ -78,7 +84,7 @@ describe('limitOptions', () => { options.maxItems = 5; options.cursor = 8; const result = limitOptions(options); - expect(result).toEqual([color.dim('...'), 'Item 7', 'Item 8', 'Item 9', 'Item 10']); + expect(result).toEqual([styleText('dim', '...'), 'Item 7', 'Item 8', 'Item 9', 'Item 10']); }); test('handles empty options list', async () => { @@ -103,7 +109,7 @@ describe('limitOptions', () => { output.rows = 7; options.maxItems = 10; const result = limitOptions(options); - expect(result).toEqual(['Item 1', 'Item 2', color.dim('...')]); + expect(result).toEqual(['Item 1', 'Item 2', styleText('dim', '...')]); }); test('handle multi-line item clamping (start)', async () => { @@ -138,7 +144,7 @@ describe('limitOptions', () => { 'Item 6', 'Item 7', 'Item 8', - color.dim('...'), + styleText('dim', '...'), ]); }); @@ -164,7 +170,7 @@ describe('limitOptions', () => { options.cursor = 7; const result = limitOptions(options); expect(result).toEqual([ - color.dim('...'), + styleText('dim', '...'), 'Item 2', 'Item 3', 'Item 4', @@ -175,7 +181,7 @@ describe('limitOptions', () => { 'Item 6', 'Item 7', 'Item 8', - color.dim('...'), + styleText('dim', '...'), ]); }); @@ -201,7 +207,7 @@ describe('limitOptions', () => { options.cursor = 9; const result = limitOptions(options); expect(result).toEqual([ - color.dim('...'), + styleText('dim', '...'), 'Item 4', 'Item 5', 'Item 6', diff --git a/packages/prompts/test/note.test.ts b/packages/prompts/test/note.test.ts index ac0df4fe..887448ed 100644 --- a/packages/prompts/test/note.test.ts +++ b/packages/prompts/test/note.test.ts @@ -1,4 +1,4 @@ -import colors from 'picocolors'; +import { styleText } from 'node:util'; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; import * as prompts from '../src/index.js'; import { MockReadable, MockWritable } from './test-utils.js'; @@ -56,7 +56,7 @@ describe.each(['true', 'false'])('note (isCI = %s)', (isCI) => { test('formatter which adds colors works', () => { prompts.note('line 0\nline 1\nline 2', 'title', { - format: (line) => colors.red(line), + format: (line) => styleText('red', line), input, output, }); @@ -79,7 +79,7 @@ describe.each(['true', 'false'])('note (isCI = %s)', (isCI) => { const message = `${'test string '.repeat(32)}\n`.repeat(4).trim(); output.columns = 75; prompts.note(message, 'title', { - format: (line) => colors.red(`* ${colors.cyan(line)} *`), + format: (line) => styleText('red', `* ${styleText('cyan', line)} *`), input, output, }); @@ -102,7 +102,7 @@ describe.each(['true', 'false'])('note (isCI = %s)', (isCI) => { const messages = ['이게 첫 번째 줄이에요', 'これは次の行です']; output.columns = 10; prompts.note(messages.join('\n'), '这是标题', { - format: (line) => colors.red(`* ${colors.cyan(line)} *`), + format: (line) => styleText('red', `* ${styleText('cyan', line)} *`), input, output, }); diff --git a/packages/prompts/test/spinner.test.ts b/packages/prompts/test/spinner.test.ts index 63ecc533..66ef5794 100644 --- a/packages/prompts/test/spinner.test.ts +++ b/packages/prompts/test/spinner.test.ts @@ -1,6 +1,6 @@ import { EventEmitter } from 'node:stream'; +import { styleText } from 'node:util'; import { getColumns } from '@clack/core'; -import color from 'picocolors'; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; import * as prompts from '../src/index.js'; import { MockWritable } from './test-utils.js'; @@ -236,7 +236,7 @@ describe.each(['true', 'false'])('spinner (isCI = %s)', (isCI) => { }); test('custom frame style', () => { - const result = prompts.spinner({ output, styleFrame: color.red }); + const result = prompts.spinner({ output, styleFrame: (text) => styleText('red', text) }); result.start(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c19acf1..5d1d962b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,9 +60,6 @@ importers: packages/core: dependencies: - picocolors: - specifier: ^1.0.0 - version: 1.1.1 sisteransi: specifier: ^1.0.5 version: 1.0.5 @@ -79,9 +76,6 @@ importers: '@clack/core': specifier: workspace:* version: link:../core - picocolors: - specifier: ^1.0.0 - version: 1.1.1 sisteransi: specifier: ^1.0.5 version: 1.0.5