Skip to content

Commit 0ef3dc1

Browse files
authored
feat: add select-key option (#59)
2 parents 57df900 + d74dd05 commit 0ef3dc1

File tree

6 files changed

+87
-1
lines changed

6 files changed

+87
-1
lines changed

.changeset/four-guests-give.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+
Adds a `selectKey` prompt type

examples/basic/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as p from '@clack/prompts';
2-
import color from 'picocolors';
32
import { setTimeout } from 'node:timers/promises';
3+
import color from 'picocolors';
44

55
async function main() {
66
console.clear();

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ export { default as PasswordPrompt } from './prompts/password';
44
export { default as Prompt, isCancel } from './prompts/prompt';
55
export type { State } from './prompts/prompt';
66
export { default as SelectPrompt } from './prompts/select';
7+
export { default as SelectKeyPrompt } from './prompts/select-key';
78
export { default as TextPrompt } from './prompts/text';
89
export { block } from './utils';

packages/core/src/prompts/prompt.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ export default class Prompt {
162162
if (char && (char.toLowerCase() === 'y' || char.toLowerCase() === 'n')) {
163163
this.emit('confirm', char.toLowerCase() === 'y');
164164
}
165+
if (char) {
166+
this.emit('key', char.toLowerCase());
167+
}
165168

166169
if (key?.name === 'return') {
167170
if (this.opts.validate) {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Prompt, { PromptOptions } from './prompt';
2+
3+
interface SelectKeyOptions<T extends { value: any }> extends PromptOptions<SelectKeyPrompt<T>> {
4+
options: T[];
5+
}
6+
export default class SelectKeyPrompt<T extends { value: any }> extends Prompt {
7+
options: T[];
8+
cursor: number = 0;
9+
10+
constructor(opts: SelectKeyOptions<T>) {
11+
super(opts, false);
12+
13+
this.options = opts.options;
14+
const keys = this.options.map(({ value: [initial] }) => initial?.toLowerCase());
15+
this.cursor = Math.max(keys.indexOf(opts.initialValue), 0);
16+
17+
this.on('key', (key) => {
18+
if (!keys.includes(key)) return;
19+
const value = this.options.find(({ value: [initial] }) => initial?.toLowerCase() === key);
20+
if (value) {
21+
this.value = value.value;
22+
this.state = 'submit';
23+
this.emit('submit');
24+
}
25+
});
26+
}
27+
}

packages/prompts/src/index.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
isCancel,
55
MultiSelectPrompt,
66
PasswordPrompt,
7+
SelectKeyPrompt,
78
SelectPrompt,
89
State,
910
TextPrompt,
@@ -216,6 +217,54 @@ export const select = <Options extends Option<Value>[], Value extends Primitive>
216217
}).prompt() as Promise<Options[number]['value'] | symbol>;
217218
};
218219

220+
export const selectKey = <Options extends Option<Value>[], Value extends string>(
221+
opts: SelectOptions<Options, Value>
222+
) => {
223+
const opt = (
224+
option: Options[number],
225+
state: 'inactive' | 'active' | 'selected' | 'cancelled' = 'inactive'
226+
) => {
227+
const label = option.label ?? String(option.value);
228+
if (state === 'selected') {
229+
return `${color.dim(label)}`;
230+
} else if (state === 'cancelled') {
231+
return `${color.strikethrough(color.dim(label))}`;
232+
} else if (state === 'active') {
233+
return `${color.bgCyan(color.gray(` ${option.value} `))} ${label} ${
234+
option.hint ? color.dim(`(${option.hint})`) : ''
235+
}`;
236+
}
237+
return `${color.gray(color.bgWhite(color.inverse(` ${option.value} `)))} ${label} ${
238+
option.hint ? color.dim(`(${option.hint})`) : ''
239+
}`;
240+
};
241+
242+
return new SelectKeyPrompt({
243+
options: opts.options,
244+
initialValue: opts.initialValue,
245+
render() {
246+
const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
247+
248+
switch (this.state) {
249+
case 'submit':
250+
return `${title}${color.gray(S_BAR)} ${opt(
251+
this.options.find((opt) => opt.value === this.value)!,
252+
'selected'
253+
)}`;
254+
case 'cancel':
255+
return `${title}${color.gray(S_BAR)} ${opt(this.options[0], 'cancelled')}\n${color.gray(
256+
S_BAR
257+
)}`;
258+
default: {
259+
return `${title}${color.cyan(S_BAR)} ${this.options
260+
.map((option, i) => opt(option, i === this.cursor ? 'active' : 'inactive'))
261+
.join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`;
262+
}
263+
}
264+
},
265+
}).prompt() as Promise<Options[number]['value'] | symbol>;
266+
};
267+
219268
export interface MultiSelectOptions<Options extends Option<Value>[], Value extends Primitive> {
220269
message: string;
221270
options: Options;

0 commit comments

Comments
 (0)