From 691626fe6fed14e382f64a7f19454d6e1b0b6e4b Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Fri, 17 Feb 2023 06:41:03 -0600 Subject: [PATCH 1/9] feat: add select-key option --- examples/basic/index.ts | 8 +++++ packages/core/src/index.ts | 1 + packages/core/src/prompts/prompt.ts | 3 ++ packages/core/src/prompts/select-key.ts | 26 +++++++++++++++ packages/prompts/src/index.ts | 42 +++++++++++++++++++++++++ 5 files changed, 80 insertions(+) create mode 100644 packages/core/src/prompts/select-key.ts diff --git a/examples/basic/index.ts b/examples/basic/index.ts index 09e1aa1..dccd786 100644 --- a/examples/basic/index.ts +++ b/examples/basic/index.ts @@ -11,6 +11,14 @@ async function main() { const project = await p.group( { + continue: () => p.selectKey({ + message: 'Use this commit message?', + options: [ + { value: 'y', label: 'yes' }, + { value: 'n', label: 'no' }, + { value: 'r', label: 'regenerate' }, + ] + }), path: () => p.text({ message: 'Where should we create your project?', diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index cfeaf80..747e27c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,5 +4,6 @@ export { default as PasswordPrompt } from './prompts/password'; export { default as Prompt, isCancel } from './prompts/prompt'; export type { State } from './prompts/prompt'; export { default as SelectPrompt } from './prompts/select'; +export { default as SelectKeyPrompt } from './prompts/select-key'; export { default as TextPrompt } from './prompts/text'; export { block } from './utils'; diff --git a/packages/core/src/prompts/prompt.ts b/packages/core/src/prompts/prompt.ts index 0b6fe79..1efd83c 100644 --- a/packages/core/src/prompts/prompt.ts +++ b/packages/core/src/prompts/prompt.ts @@ -162,6 +162,9 @@ export default class Prompt { if (char && (char.toLowerCase() === 'y' || char.toLowerCase() === 'n')) { this.emit('confirm', char.toLowerCase() === 'y'); } + if (char) { + this.emit('key', char.toLowerCase()); + } if (key?.name === 'return') { if (this.opts.validate) { diff --git a/packages/core/src/prompts/select-key.ts b/packages/core/src/prompts/select-key.ts new file mode 100644 index 0000000..6eaec55 --- /dev/null +++ b/packages/core/src/prompts/select-key.ts @@ -0,0 +1,26 @@ +import Prompt, { PromptOptions } from './prompt'; + +interface SelectKeyOptions extends PromptOptions> { + options: T[]; +} +export default class SelectKeyPrompt extends Prompt { + options: T[]; + cursor: number = 0; + + constructor(opts: SelectKeyOptions) { + super(opts, false); + + this.options = opts.options; + const keys = opts.options.map(v => v.value[0]); + + this.on('key', (key) => { + if (!keys.includes(key)) return; + const value = opts.options.find(v => v.value[0] === key); + if (value) { + this.value = value.value; + this.state = 'submit'; + this.emit('submit'); + } + }); + } +} diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index eca23f0..4da8d9f 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -216,6 +216,48 @@ export const select = [], Value extends Primitive> }).prompt() as Promise; }; +export const selectKey = [], Value extends string>( + opts: SelectOptions +) => { + const opt = ( + option: Options[number], + state: 'inactive' | 'selected' | 'cancelled' = 'inactive' + ) => { + const label = option.label ?? String(option.value); + if (state === 'selected') { + return `${color.dim(label)}`; + } else if (state === 'cancelled') { + return `${color.strikethrough(color.dim(label))}`; + } + return `${color.gray(color.bgWhite(color.inverse(` ${option.value} `)))} ${label}`; + }; + + return new SelectKeyPrompt({ + options: opts.options, + initialValue: opts.initialValue, + render() { + const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + + switch (this.state) { + case 'submit': + return `${title}${color.gray(S_BAR)} ${opt( + this.options.find((opt) => opt.value === this.value)!, + 'selected' + )}`; + case 'cancel': + return `${title}${color.gray(S_BAR)} ${opt(this.options[0], 'cancelled')}\n${color.gray( + S_BAR + )}`; + default: { + return `${title}${color.cyan(S_BAR)} ${this.options + .map((option, i) => opt(option)) + .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; + } + } + }, + }).prompt() as Promise; +}; + export interface MultiSelectOptions[], Value extends Primitive> { message: string; options: Options; -- 2.51.2 From 3961e3f8b581e8d01004b2553451734efb403714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20L=C3=B6fgren?= <516549+ulken@users.noreply.github.com> Date: Tue, 21 Feb 2023 23:43:09 +0100 Subject: [PATCH 2/9] add missing import after rebase --- packages/prompts/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index 4da8d9f..c669356 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -4,6 +4,7 @@ import { isCancel, MultiSelectPrompt, PasswordPrompt, + SelectKeyPrompt, SelectPrompt, State, TextPrompt, -- 2.51.2 From fbccaba8eead7802ddd862028f84b6c32a50d42d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20L=C3=B6fgren?= <516549+ulken@users.noreply.github.com> Date: Tue, 21 Feb 2023 23:46:53 +0100 Subject: [PATCH 3/9] add hint --- examples/basic/index.ts | 19 ++++++++++--------- packages/prompts/src/index.ts | 4 +++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/examples/basic/index.ts b/examples/basic/index.ts index dccd786..a8f968c 100644 --- a/examples/basic/index.ts +++ b/examples/basic/index.ts @@ -1,6 +1,6 @@ import * as p from '@clack/prompts'; -import color from 'picocolors'; import { setTimeout } from 'node:timers/promises'; +import color from 'picocolors'; async function main() { console.clear(); @@ -11,14 +11,15 @@ async function main() { const project = await p.group( { - continue: () => p.selectKey({ - message: 'Use this commit message?', - options: [ - { value: 'y', label: 'yes' }, - { value: 'n', label: 'no' }, - { value: 'r', label: 'regenerate' }, - ] - }), + continue: () => + p.selectKey({ + message: 'Use this commit message?', + options: [ + { value: 'y', label: 'yes', hint: 'default' }, + { value: 'n', label: 'no' }, + { value: 'r', label: 'regenerate' }, + ], + }), path: () => p.text({ message: 'Where should we create your project?', diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index c669356..2c39168 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -230,7 +230,9 @@ export const selectKey = [], Value extends string> } else if (state === 'cancelled') { return `${color.strikethrough(color.dim(label))}`; } - return `${color.gray(color.bgWhite(color.inverse(` ${option.value} `)))} ${label}`; + return `${color.gray(color.bgWhite(color.inverse(` ${option.value} `)))} ${label} ${ + option.hint ? color.dim(`(${option.hint})`) : '' + }`; }; return new SelectKeyPrompt({ -- 2.51.2 From 1239d154bb9b8fb8c418df1e5db99ad4e0bcf83f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20L=C3=B6fgren?= <516549+ulken@users.noreply.github.com> Date: Tue, 21 Feb 2023 23:57:34 +0100 Subject: [PATCH 4/9] add active style for default selected --- packages/core/src/prompts/select-key.ts | 5 +++-- packages/prompts/src/index.ts | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/core/src/prompts/select-key.ts b/packages/core/src/prompts/select-key.ts index 6eaec55..f71768b 100644 --- a/packages/core/src/prompts/select-key.ts +++ b/packages/core/src/prompts/select-key.ts @@ -11,11 +11,12 @@ export default class SelectKeyPrompt extends Prompt { super(opts, false); this.options = opts.options; - const keys = opts.options.map(v => v.value[0]); + const keys = opts.options.map(({ value: [initial] }) => initial); + this.cursor = Math.max(keys.indexOf(opts.initialValue), 0); this.on('key', (key) => { if (!keys.includes(key)) return; - const value = opts.options.find(v => v.value[0] === key); + const value = opts.options.find(({ value: [initial] }) => initial === key); if (value) { this.value = value.value; this.state = 'submit'; diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index 2c39168..1e6bf0f 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -222,13 +222,17 @@ export const selectKey = [], Value extends string> ) => { const opt = ( option: Options[number], - state: 'inactive' | 'selected' | 'cancelled' = 'inactive' + state: 'inactive' | 'active' | 'selected' | 'cancelled' = 'inactive' ) => { const label = option.label ?? String(option.value); if (state === 'selected') { return `${color.dim(label)}`; } else if (state === 'cancelled') { return `${color.strikethrough(color.dim(label))}`; + } else if (state === 'active') { + return `${color.bgCyan(color.gray(` ${option.value} `))} ${label} ${ + option.hint ? color.dim(`(${option.hint})`) : '' + }`; } return `${color.gray(color.bgWhite(color.inverse(` ${option.value} `)))} ${label} ${ option.hint ? color.dim(`(${option.hint})`) : '' @@ -253,7 +257,7 @@ export const selectKey = [], Value extends string> )}`; default: { return `${title}${color.cyan(S_BAR)} ${this.options - .map((option, i) => opt(option)) + .map((option, i) => opt(option, i === this.cursor ? 'active' : 'inactive')) .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; } } -- 2.51.2 From 88a4988a774e39ba7c52c0a4b8f5a9dd39f4019c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20L=C3=B6fgren?= <516549+ulken@users.noreply.github.com> Date: Wed, 22 Feb 2023 00:12:33 +0100 Subject: [PATCH 5/9] submit default on enter --- packages/core/src/prompts/select-key.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/core/src/prompts/select-key.ts b/packages/core/src/prompts/select-key.ts index f71768b..1de8d0d 100644 --- a/packages/core/src/prompts/select-key.ts +++ b/packages/core/src/prompts/select-key.ts @@ -11,17 +11,21 @@ export default class SelectKeyPrompt extends Prompt { super(opts, false); this.options = opts.options; - const keys = opts.options.map(({ value: [initial] }) => initial); + const keys = this.options.map(({ value: [initial] }) => initial); this.cursor = Math.max(keys.indexOf(opts.initialValue), 0); this.on('key', (key) => { if (!keys.includes(key)) return; - const value = opts.options.find(({ value: [initial] }) => initial === key); + const value = this.options.find(({ value: [initial] }) => initial === key); if (value) { this.value = value.value; this.state = 'submit'; this.emit('submit'); } }); + + this.once('finalize', () => { + this.value = this.options[this.cursor].value; + }); } } -- 2.51.2 From a82276cc2c7592a308fcbf36d9fcc55a7326ed51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20L=C3=B6fgren?= <516549+ulken@users.noreply.github.com> Date: Wed, 22 Feb 2023 00:17:26 +0100 Subject: [PATCH 6/9] case-insensitive checks --- packages/core/src/prompts/select-key.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/prompts/select-key.ts b/packages/core/src/prompts/select-key.ts index 1de8d0d..b9b1a2b 100644 --- a/packages/core/src/prompts/select-key.ts +++ b/packages/core/src/prompts/select-key.ts @@ -11,12 +11,12 @@ export default class SelectKeyPrompt extends Prompt { super(opts, false); this.options = opts.options; - const keys = this.options.map(({ value: [initial] }) => initial); + const keys = this.options.map(({ value: [initial] }) => initial?.toLowerCase()); this.cursor = Math.max(keys.indexOf(opts.initialValue), 0); this.on('key', (key) => { if (!keys.includes(key)) return; - const value = this.options.find(({ value: [initial] }) => initial === key); + const value = this.options.find(({ value: [initial] }) => initial?.toLowerCase() === key); if (value) { this.value = value.value; this.state = 'submit'; -- 2.51.2 From 4a6729ca1f3db215082f86dd2cb3f5fc5623da0f Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Tue, 21 Feb 2023 21:21:22 -0600 Subject: [PATCH 7/9] fix value logic --- packages/core/src/prompts/select-key.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/core/src/prompts/select-key.ts b/packages/core/src/prompts/select-key.ts index b9b1a2b..daacfbb 100644 --- a/packages/core/src/prompts/select-key.ts +++ b/packages/core/src/prompts/select-key.ts @@ -23,9 +23,5 @@ export default class SelectKeyPrompt extends Prompt { this.emit('submit'); } }); - - this.once('finalize', () => { - this.value = this.options[this.cursor].value; - }); } } -- 2.51.2 From d51b12eba165680cbebff38de6d271f7f765accc Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Tue, 21 Feb 2023 21:21:30 -0600 Subject: [PATCH 8/9] remove select-key from demo --- examples/basic/index.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/examples/basic/index.ts b/examples/basic/index.ts index a8f968c..7ce1134 100644 --- a/examples/basic/index.ts +++ b/examples/basic/index.ts @@ -11,15 +11,6 @@ async function main() { const project = await p.group( { - continue: () => - p.selectKey({ - message: 'Use this commit message?', - options: [ - { value: 'y', label: 'yes', hint: 'default' }, - { value: 'n', label: 'no' }, - { value: 'r', label: 'regenerate' }, - ], - }), path: () => p.text({ message: 'Where should we create your project?', -- 2.51.2 From d74dd05cfc180fd8a0887b1f62ba20f0b2588fc2 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Tue, 21 Feb 2023 21:23:20 -0600 Subject: [PATCH 9/9] add changeset --- .changeset/four-guests-give.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/four-guests-give.md diff --git a/.changeset/four-guests-give.md b/.changeset/four-guests-give.md new file mode 100644 index 0000000..f3a8fb8 --- /dev/null +++ b/.changeset/four-guests-give.md @@ -0,0 +1,6 @@ +--- +'@clack/prompts': minor +'@clack/core': minor +--- + +Adds a `selectKey` prompt type -- 2.51.2