Skip to content

Commit e3333fb

Browse files
dreyfus9243081j
andauthored
refactor(core, prompts): replace picocolors with styleText (#403)
Co-authored-by: James Garbutt <43081j@users.noreply.github.com>
1 parent 594c58a commit e3333fb

31 files changed

+319
-275
lines changed

.changeset/huge-items-throw.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@clack/prompts": minor
3+
"@clack/core": minor
4+
---
5+
6+
Replaces `picocolors` with Node.js built-in `styleText`.

packages/core/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@
5353
"test": "vitest run"
5454
},
5555
"dependencies": {
56-
"picocolors": "^1.0.0",
5756
"sisteransi": "^1.0.5"
5857
},
5958
"devDependencies": {

packages/core/src/prompts/autocomplete.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Key } from 'node:readline';
2-
import color from 'picocolors';
2+
import { styleText } from 'node:util';
33
import { findCursor } from '../utils/cursor.js';
44
import Prompt, { type PromptOptions } from './prompt.js';
55

@@ -73,14 +73,14 @@ export default class AutocompletePrompt<T extends OptionLike> extends Prompt<
7373

7474
get userInputWithCursor() {
7575
if (!this.userInput) {
76-
return color.inverse(color.hidden('_'));
76+
return styleText(['inverse', 'hidden'], '_');
7777
}
7878
if (this._cursor >= this.userInput.length) {
7979
return `${this.userInput}█`;
8080
}
8181
const s1 = this.userInput.slice(0, this._cursor);
8282
const [s2, ...s3] = this.userInput.slice(this._cursor);
83-
return `${s1}${color.inverse(s2)}${s3.join('')}`;
83+
return `${s1}${styleText('inverse', s2)}${s3.join('')}`;
8484
}
8585

8686
get options(): T[] {

packages/core/src/prompts/password.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import color from 'picocolors';
1+
import { styleText } from 'node:util';
22
import Prompt, { type PromptOptions } from './prompt.js';
33

44
export interface PasswordOptions extends PromptOptions<string, PasswordPrompt> {
@@ -18,12 +18,12 @@ export default class PasswordPrompt extends Prompt<string> {
1818
}
1919
const userInput = this.userInput;
2020
if (this.cursor >= userInput.length) {
21-
return `${this.masked}${color.inverse(color.hidden('_'))}`;
21+
return `${this.masked}${styleText(['inverse', 'hidden'], '_')}`;
2222
}
2323
const masked = this.masked;
2424
const s1 = masked.slice(0, this.cursor);
2525
const s2 = masked.slice(this.cursor);
26-
return `${s1}${color.inverse(s2[0])}${s2.slice(1)}`;
26+
return `${s1}${styleText('inverse', s2[0])}${s2.slice(1)}`;
2727
}
2828
clear() {
2929
this._clearUserInput();

packages/core/src/prompts/text.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import color from 'picocolors';
1+
import { styleText } from 'node:util';
22
import Prompt, { type PromptOptions } from './prompt.js';
33

44
export interface TextOptions extends PromptOptions<string, TextPrompt> {
@@ -17,7 +17,7 @@ export default class TextPrompt extends Prompt<string> {
1717
}
1818
const s1 = userInput.slice(0, this.cursor);
1919
const [s2, ...s3] = userInput.slice(this.cursor);
20-
return `${s1}${color.inverse(s2)}${s3.join('')}`;
20+
return `${s1}${styleText('inverse', s2)}${s3.join('')}`;
2121
}
2222
get cursor() {
2323
return this._cursor;

packages/core/test/prompts/password.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import color from 'picocolors';
1+
import { styleText } from 'node:util';
22
import { cursor } from 'sisteransi';
33
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
44
import { default as PasswordPrompt } from '../../src/prompts/password.js';
@@ -65,7 +65,9 @@ describe('PasswordPrompt', () => {
6565
});
6666
instance.prompt();
6767
input.emit('keypress', 'x', { name: 'x' });
68-
expect(instance.userInputWithCursor).to.equal(`•${color.inverse(color.hidden('_'))}`);
68+
expect(instance.userInputWithCursor).to.equal(
69+
`•${styleText(['inverse', 'hidden'], '_')}`
70+
);
6971
});
7072

7173
test('renders cursor inside value', () => {
@@ -80,7 +82,7 @@ describe('PasswordPrompt', () => {
8082
input.emit('keypress', 'z', { name: 'z' });
8183
input.emit('keypress', 'left', { name: 'left' });
8284
input.emit('keypress', 'left', { name: 'left' });
83-
expect(instance.userInputWithCursor).to.equal(`•${color.inverse('•')}•`);
85+
expect(instance.userInputWithCursor).to.equal(`•${styleText('inverse', '•')}•`);
8486
});
8587

8688
test('renders custom mask', () => {
@@ -92,7 +94,9 @@ describe('PasswordPrompt', () => {
9294
});
9395
instance.prompt();
9496
input.emit('keypress', 'x', { name: 'x' });
95-
expect(instance.userInputWithCursor).to.equal(`X${color.inverse(color.hidden('_'))}`);
97+
expect(instance.userInputWithCursor).to.equal(
98+
`X${styleText(['inverse', 'hidden'], '_')}`
99+
);
96100
});
97101
});
98102
});

packages/core/test/prompts/text.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import color from 'picocolors';
1+
import { styleText } from 'node:util';
22
import { cursor } from 'sisteransi';
33
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
44
import { default as TextPrompt } from '../../src/prompts/text.js';
@@ -93,7 +93,7 @@ describe('TextPrompt', () => {
9393
input.emit('keypress', keys[i], { name: keys[i] });
9494
}
9595
input.emit('keypress', 'left', { name: 'left' });
96-
expect(instance.userInputWithCursor).to.equal(`fo${color.inverse('o')}`);
96+
expect(instance.userInputWithCursor).to.equal(`fo${styleText('inverse', 'o')}`);
9797
});
9898

9999
test('shows cursor at end if beyond value', () => {

packages/prompts/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@
5454
},
5555
"dependencies": {
5656
"@clack/core": "workspace:*",
57-
"picocolors": "^1.0.0",
5857
"sisteransi": "^1.0.5"
5958
},
6059
"devDependencies": {

packages/prompts/src/autocomplete.ts

Lines changed: 52 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import { styleText } from 'node:util';
12
import { AutocompletePrompt, settings } from '@clack/core';
2-
import color from 'picocolors';
33
import {
44
type CommonOptions,
55
S_BAR,
@@ -98,7 +98,7 @@ export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => {
9898
const hasGuide = opts.withGuide ?? settings.withGuide;
9999
// Title and message display
100100
const headings = hasGuide
101-
? [`${color.gray(S_BAR)}`, `${symbol(this.state)} ${opts.message}`]
101+
? [`${styleText('gray', S_BAR)}`, `${symbol(this.state)} ${opts.message}`]
102102
: [`${symbol(this.state)} ${opts.message}`];
103103
const userInput = this.userInput;
104104
const options = this.options;
@@ -107,14 +107,16 @@ export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => {
107107
const opt = (option: Option<Value>, state: 'inactive' | 'active' | 'disabled') => {
108108
const label = getLabel(option);
109109
const hint =
110-
option.hint && option.value === this.focusedValue ? color.dim(` (${option.hint})`) : '';
110+
option.hint && option.value === this.focusedValue
111+
? styleText('dim', ` (${option.hint})`)
112+
: '';
111113
switch (state) {
112114
case 'active':
113-
return `${color.green(S_RADIO_ACTIVE)} ${label}${hint}`;
115+
return `${styleText('green', S_RADIO_ACTIVE)} ${label}${hint}`;
114116
case 'inactive':
115-
return `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}`;
117+
return `${styleText('dim', S_RADIO_INACTIVE)} ${styleText('dim', label)}`;
116118
case 'disabled':
117-
return `${color.gray(S_RADIO_INACTIVE)} ${color.strikethrough(color.gray(label))}`;
119+
return `${styleText('gray', S_RADIO_INACTIVE)} ${styleText(['strikethrough', 'gray'], label)}`;
118120
}
119121
};
120122

@@ -124,61 +126,64 @@ export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => {
124126
// Show selected value
125127
const selected = getSelectedOptions(this.selectedValues, options);
126128
const label =
127-
selected.length > 0 ? ` ${color.dim(selected.map(getLabel).join(', '))}` : '';
128-
const submitPrefix = hasGuide ? color.gray(S_BAR) : '';
129+
selected.length > 0 ? ` ${styleText('dim', selected.map(getLabel).join(', '))}` : '';
130+
const submitPrefix = hasGuide ? styleText('gray', S_BAR) : '';
129131
return `${headings.join('\n')}\n${submitPrefix}${label}`;
130132
}
131133

132134
case 'cancel': {
133-
const userInputText = userInput ? ` ${color.strikethrough(color.dim(userInput))}` : '';
134-
const cancelPrefix = hasGuide ? color.gray(S_BAR) : '';
135+
const userInputText = userInput
136+
? ` ${styleText(['strikethrough', 'dim'], userInput)}`
137+
: '';
138+
const cancelPrefix = hasGuide ? styleText('gray', S_BAR) : '';
135139
return `${headings.join('\n')}\n${cancelPrefix}${userInputText}`;
136140
}
137141

138142
default: {
139-
const barColor = this.state === 'error' ? color.yellow : color.cyan;
140-
const guidePrefix = hasGuide ? `${barColor(S_BAR)} ` : '';
141-
const guidePrefixEnd = hasGuide ? barColor(S_BAR_END) : '';
143+
const barStyle = this.state === 'error' ? 'yellow' : 'cyan';
144+
const guidePrefix = hasGuide ? `${styleText(barStyle, S_BAR)} ` : '';
145+
const guidePrefixEnd = hasGuide ? styleText(barStyle, S_BAR_END) : '';
142146
// Display cursor position - show plain text in navigation mode
143147
let searchText = '';
144148
if (this.isNavigating || showPlaceholder) {
145149
const searchTextValue = showPlaceholder ? placeholder : userInput;
146-
searchText = searchTextValue !== '' ? ` ${color.dim(searchTextValue)}` : '';
150+
searchText = searchTextValue !== '' ? ` ${styleText('dim', searchTextValue)}` : '';
147151
} else {
148152
searchText = ` ${this.userInputWithCursor}`;
149153
}
150154

151155
// Show match count if filtered
152156
const matches =
153157
this.filteredOptions.length !== options.length
154-
? color.dim(
158+
? styleText(
159+
'dim',
155160
` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? '' : 'es'})`
156161
)
157162
: '';
158163

159164
// No matches message
160165
const noResults =
161166
this.filteredOptions.length === 0 && userInput
162-
? [`${guidePrefix}${color.yellow('No matches found')}`]
167+
? [`${guidePrefix}${styleText('yellow', 'No matches found')}`]
163168
: [];
164169

165170
const validationError =
166-
this.state === 'error' ? [`${guidePrefix}${color.yellow(this.error)}`] : [];
171+
this.state === 'error' ? [`${guidePrefix}${styleText('yellow', this.error)}`] : [];
167172

168173
if (hasGuide) {
169174
headings.push(`${guidePrefix.trimEnd()}`);
170175
}
171176
headings.push(
172-
`${guidePrefix}${color.dim('Search:')}${searchText}${matches}`,
177+
`${guidePrefix}${styleText('dim', 'Search:')}${searchText}${matches}`,
173178
...noResults,
174179
...validationError
175180
);
176181

177182
// Show instructions
178183
const instructions = [
179-
`${color.dim('↑/↓')} to select`,
180-
`${color.dim('Enter:')} confirm`,
181-
`${color.dim('Type:')} to search`,
184+
`${styleText('dim', '↑/↓')} to select`,
185+
`${styleText('dim', 'Enter:')} confirm`,
186+
`${styleText('dim', 'Type:')} to search`,
182187
];
183188

184189
const footers = [`${guidePrefix}${instructions.join(' • ')}`, guidePrefixEnd];
@@ -243,17 +248,19 @@ export const autocompleteMultiselect = <Value>(opts: AutocompleteMultiSelectOpti
243248
const label = option.label ?? String(option.value ?? '');
244249
const hint =
245250
option.hint && focusedValue !== undefined && option.value === focusedValue
246-
? color.dim(` (${option.hint})`)
251+
? styleText('dim', ` (${option.hint})`)
247252
: '';
248-
const checkbox = isSelected ? color.green(S_CHECKBOX_SELECTED) : color.dim(S_CHECKBOX_INACTIVE);
253+
const checkbox = isSelected
254+
? styleText('green', S_CHECKBOX_SELECTED)
255+
: styleText('dim', S_CHECKBOX_INACTIVE);
249256

250257
if (option.disabled) {
251-
return `${color.gray(S_CHECKBOX_INACTIVE)} ${color.strikethrough(color.gray(label))}`;
258+
return `${styleText('gray', S_CHECKBOX_INACTIVE)} ${styleText(['strikethrough', 'gray'], label)}`;
252259
}
253260
if (active) {
254261
return `${checkbox} ${label}${hint}`;
255262
}
256-
return `${checkbox} ${color.dim(label)}`;
263+
return `${checkbox} ${styleText('dim', label)}`;
257264
};
258265

259266
// Create text prompt which we'll use as foundation
@@ -277,7 +284,7 @@ export const autocompleteMultiselect = <Value>(opts: AutocompleteMultiSelectOpti
277284
output: opts.output,
278285
render() {
279286
// Title and symbol
280-
const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
287+
const title = `${styleText('gray', S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
281288

282289
// Selection counter
283290
const userInput = this.userInput;
@@ -287,55 +294,58 @@ export const autocompleteMultiselect = <Value>(opts: AutocompleteMultiSelectOpti
287294
// Search input display
288295
const searchText =
289296
this.isNavigating || showPlaceholder
290-
? color.dim(showPlaceholder ? placeholder : userInput) // Just show plain text when in navigation mode
297+
? styleText('dim', showPlaceholder ? placeholder : userInput) // Just show plain text when in navigation mode
291298
: this.userInputWithCursor;
292299

293300
const options = this.options;
294301

295302
const matches =
296303
this.filteredOptions.length !== options.length
297-
? color.dim(
304+
? styleText(
305+
'dim',
298306
` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? '' : 'es'})`
299307
)
300308
: '';
301309

302310
// Render prompt state
303311
switch (this.state) {
304312
case 'submit': {
305-
return `${title}${color.gray(S_BAR)} ${color.dim(`${this.selectedValues.length} items selected`)}`;
313+
return `${title}${styleText('gray', S_BAR)} ${styleText('dim', `${this.selectedValues.length} items selected`)}`;
306314
}
307315
case 'cancel': {
308-
return `${title}${color.gray(S_BAR)} ${color.strikethrough(color.dim(userInput))}`;
316+
return `${title}${styleText('gray', S_BAR)} ${styleText(['strikethrough', 'dim'], userInput)}`;
309317
}
310318
default: {
311-
const barColor = this.state === 'error' ? color.yellow : color.cyan;
319+
const barStyle = this.state === 'error' ? 'yellow' : 'cyan';
312320
// Instructions
313321
const instructions = [
314-
`${color.dim('↑/↓')} to navigate`,
315-
`${color.dim(this.isNavigating ? 'Space/Tab:' : 'Tab:')} select`,
316-
`${color.dim('Enter:')} confirm`,
317-
`${color.dim('Type:')} to search`,
322+
`${styleText('dim', '↑/↓')} to navigate`,
323+
`${styleText('dim', this.isNavigating ? 'Space/Tab:' : 'Tab:')} select`,
324+
`${styleText('dim', 'Enter:')} confirm`,
325+
`${styleText('dim', 'Type:')} to search`,
318326
];
319327

320328
// No results message
321329
const noResults =
322330
this.filteredOptions.length === 0 && userInput
323-
? [`${barColor(S_BAR)} ${color.yellow('No matches found')}`]
331+
? [`${styleText(barStyle, S_BAR)} ${styleText('yellow', 'No matches found')}`]
324332
: [];
325333

326334
const errorMessage =
327-
this.state === 'error' ? [`${barColor(S_BAR)} ${color.yellow(this.error)}`] : [];
335+
this.state === 'error'
336+
? [`${styleText(barStyle, S_BAR)} ${styleText('yellow', this.error)}`]
337+
: [];
328338

329339
// Calculate header and footer line counts for rowPadding
330340
const headerLines = [
331-
...`${title}${barColor(S_BAR)}`.split('\n'),
332-
`${barColor(S_BAR)} ${color.dim('Search:')} ${searchText}${matches}`,
341+
...`${title}${styleText(barStyle, S_BAR)}`.split('\n'),
342+
`${styleText(barStyle, S_BAR)} ${styleText('dim', 'Search:')} ${searchText}${matches}`,
333343
...noResults,
334344
...errorMessage,
335345
];
336346
const footerLines = [
337-
`${barColor(S_BAR)} ${instructions.join(' • ')}`,
338-
`${barColor(S_BAR_END)}`,
347+
`${styleText(barStyle, S_BAR)} ${instructions.join(' • ')}`,
348+
styleText(barStyle, S_BAR_END),
339349
];
340350

341351
// Get limited options for display
@@ -352,7 +362,7 @@ export const autocompleteMultiselect = <Value>(opts: AutocompleteMultiSelectOpti
352362
// Build the prompt display
353363
return [
354364
...headerLines,
355-
...displayOptions.map((option) => `${barColor(S_BAR)} ${option}`),
365+
...displayOptions.map((option) => `${styleText(barStyle, S_BAR)} ${option}`),
356366
...footerLines,
357367
].join('\n');
358368
}

0 commit comments

Comments
 (0)