From faa9cbd29d0fe89c5919d20c81c64660a04eb6ac Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Fri, 20 Dec 2024 00:41:35 +0100 Subject: [PATCH] feat(prompt): configurable cancel strategy (#325) --- README.md | 10 ++++++- src/prompt.ts | 82 +++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index fcd86c2..762208a 100644 --- a/README.md +++ b/README.md @@ -82,10 +82,18 @@ Log to all reporters. Example: `consola.info('Message')` -#### `await prompt(message, { type })` +#### `await prompt(message, { type, cancel })` Show an input prompt. Type can either of `text`, `confirm`, `select` or `multiselect`. +If prompt is canceled by user (with Ctrol+C), default value will be resolved by default. This strategy can be configured by setting `{ cancel: "..." }` option: + +- `"default"` - Resolve the promise with the `default` value or `initial` value. +- `"undefined`" - Resolve the promise with `undefined`. +- `"null"` - Resolve the promise with `null`. +- `"symbol"` - Resolve the promise with a symbol `Symbol.for("cancel")`. +- `"reject"` - Reject the promise with an error. + See [examples/prompt.ts](./examples/prompt.ts) for usage examples. #### `addReporter(reporter)` diff --git a/src/prompt.ts b/src/prompt.ts index fc8d857..e24f21f 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -6,7 +6,24 @@ type SelectOption = { hint?: string; }; -export type TextPromptOptions = { +export const kCancel = Symbol.for("cancel"); + +export type PromptCommonOptions = { + /** + * Specify how to handle a cancelled prompt (e.g. by pressing Ctrl+C). + * + * Default strategy is `"default"`. + * + * - `"default"` - Resolve the promise with the `default` value or `initial` value. + * - `"undefined`" - Resolve the promise with `undefined`. + * - `"null"` - Resolve the promise with `null`. + * - `"symbol"` - Resolve the promise with a symbol `Symbol.for("cancel")`. + * - `"reject"` - Reject the promise with an error. + */ + cancel?: "reject" | "default" | "undefined" | "null" | "symbol"; +}; + +export type TextPromptOptions = PromptCommonOptions & { /** * Specifies the prompt type as text. * @optional @@ -33,7 +50,7 @@ export type TextPromptOptions = { initial?: string; }; -export type ConfirmPromptOptions = { +export type ConfirmPromptOptions = PromptCommonOptions & { /** * Specifies the prompt type as confirm. */ @@ -46,7 +63,7 @@ export type ConfirmPromptOptions = { initial?: boolean; }; -export type SelectPromptOptions = { +export type SelectPromptOptions = PromptCommonOptions & { /** * Specifies the prompt type as select. */ @@ -64,7 +81,7 @@ export type SelectPromptOptions = { options: (string | SelectOption)[]; }; -export type MultiSelectOptions = { +export type MultiSelectOptions = PromptCommonOptions & { /** * Specifies the prompt type as multiselect. */ @@ -108,6 +125,20 @@ type inferPromptReturnType = ? T["options"] : unknown; +type inferPromptCancalReturnType = T extends { + cancel: "reject"; +} + ? never + : T extends { cancel: "default" } + ? inferPromptReturnType + : T extends { cancel: "undefined" } + ? undefined + : T extends { cancel: "null" } + ? null + : T extends { cancel: "symbol" } + ? typeof kCancel + : inferPromptReturnType /* default */; + /** * Asynchronously prompts the user for input based on specified options. * Supports text, confirm, select and multi-select prompts. @@ -123,21 +154,54 @@ export async function prompt< >( message: string, opts: PromptOptions = {}, -): Promise> { +): Promise | inferPromptCancalReturnType> { + const handleCancel = (value: unknown) => { + if ( + typeof value !== "symbol" || + value.toString() !== "Symbol(clack:cancel)" + ) { + return value; + } + + switch (opts.cancel) { + case "reject": { + const error = new Error("Prompt cancelled."); + error.name = "ConsolaPromptCancelledError"; + if (Error.captureStackTrace) { + Error.captureStackTrace(error, prompt); + } + throw error; + } + case "undefined": { + return undefined; + } + case "null": { + return null; + } + case "symbol": { + return kCancel; + } + default: + case "default": { + return (opts as TextPromptOptions).default ?? opts.initial; + } + } + }; + if (!opts.type || opts.type === "text") { return (await text({ message, defaultValue: opts.default, placeholder: opts.placeholder, initialValue: opts.initial as string, - })) as any; + }).then(handleCancel)) as any; } if (opts.type === "confirm") { return (await confirm({ message, initialValue: opts.initial, - })) as any; + }).then(handleCancel)) as any; } if (opts.type === "select") { @@ -147,7 +211,7 @@ export async function prompt< typeof o === "string" ? { value: o, label: o } : o, ), initialValue: opts.initial, - })) as any; + }).then(handleCancel)) as any; } if (opts.type === "multiselect") { @@ -158,7 +222,7 @@ export async function prompt< ), required: opts.required, initialValues: opts.initial, - })) as any; + }).then(handleCancel)) as any; } throw new Error(`Unknown prompt type: ${opts.type}`);