Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/huge-items-throw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@clack/prompts": minor
"@clack/core": minor
---

Replaces `picocolors` with Node.js built-in `styleText`.
1 change: 0 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@
"test": "vitest run"
},
"dependencies": {
"picocolors": "^1.0.0",
"sisteransi": "^1.0.5"
},
"devDependencies": {
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/prompts/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -71,14 +71,14 @@ export default class AutocompletePrompt<T extends OptionLike> 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[] {
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/prompts/password.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import color from 'picocolors';
import { styleText } from 'node:util';
import Prompt, { type PromptOptions } from './prompt.js';

interface PasswordOptions extends PromptOptions<string, PasswordPrompt> {
Expand All @@ -18,12 +18,12 @@ export default class PasswordPrompt extends Prompt<string> {
}
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();
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/prompts/text.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import color from 'picocolors';
import { styleText } from 'node:util';
import Prompt, { type PromptOptions } from './prompt.js';

interface TextOptions extends PromptOptions<string, TextPrompt> {
Expand All @@ -17,7 +17,7 @@ export default class TextPrompt extends Prompt<string> {
}
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;
Expand Down
12 changes: 8 additions & 4 deletions packages/core/test/prompts/password.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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'], '_')}`
);
});
});
});
4 changes: 2 additions & 2 deletions packages/core/test/prompts/text.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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', () => {
Expand Down
1 change: 0 additions & 1 deletion packages/prompts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
},
"dependencies": {
"@clack/core": "workspace:*",
"picocolors": "^1.0.0",
"sisteransi": "^1.0.5"
},
"devDependencies": {
Expand Down
88 changes: 49 additions & 39 deletions packages/prompts/src/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { styleText } from 'node:util';
import { AutocompletePrompt } from '@clack/core';
import color from 'picocolors';
import {
type CommonOptions,
S_BAR,
Expand Down Expand Up @@ -89,7 +89,7 @@ export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => {
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;
Expand All @@ -102,59 +102,64 @@ export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => {
// 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: {
// Display cursor position - show plain text in navigation mode
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}`;
}

// 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'})`
)
: '';

// 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
Expand All @@ -170,12 +175,12 @@ export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => {
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,
Expand All @@ -184,7 +189,7 @@ export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => {
// 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');
}
Expand Down Expand Up @@ -222,14 +227,16 @@ export const autocompleteMultiselect = <Value>(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
Expand All @@ -251,7 +258,7 @@ export const autocompleteMultiselect = <Value>(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;
Expand All @@ -261,43 +268,46 @@ export const autocompleteMultiselect = <Value>(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'})`
)
: '';

// 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({
Expand All @@ -312,12 +322,12 @@ export const autocompleteMultiselect = <Value>(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');
}
}
Expand Down
10 changes: 5 additions & 5 deletions packages/prompts/src/common.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
}
};

Expand Down
Loading
Loading