Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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', styleText('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', styleText('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
5 changes: 3 additions & 2 deletions packages/core/src/prompts/text.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import color from 'picocolors';
// 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 +18,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', styleText('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', styleText('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', styleText('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