Skip to content

Commit

Permalink
feat(prompt): configurable cancel strategy (#325)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 authored Dec 19, 2024
1 parent e2aa5c9 commit faa9cbd
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 10 deletions.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand Down
82 changes: 73 additions & 9 deletions src/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -33,7 +50,7 @@ export type TextPromptOptions = {
initial?: string;
};

export type ConfirmPromptOptions = {
export type ConfirmPromptOptions = PromptCommonOptions & {
/**
* Specifies the prompt type as confirm.
*/
Expand All @@ -46,7 +63,7 @@ export type ConfirmPromptOptions = {
initial?: boolean;
};

export type SelectPromptOptions = {
export type SelectPromptOptions = PromptCommonOptions & {
/**
* Specifies the prompt type as select.
*/
Expand All @@ -64,7 +81,7 @@ export type SelectPromptOptions = {
options: (string | SelectOption)[];
};

export type MultiSelectOptions = {
export type MultiSelectOptions = PromptCommonOptions & {
/**
* Specifies the prompt type as multiselect.
*/
Expand Down Expand Up @@ -108,6 +125,20 @@ type inferPromptReturnType<T extends PromptOptions> =
? T["options"]
: unknown;

type inferPromptCancalReturnType<T extends PromptOptions> = T extends {
cancel: "reject";
}
? never
: T extends { cancel: "default" }
? inferPromptReturnType<T>
: T extends { cancel: "undefined" }
? undefined
: T extends { cancel: "null" }
? null
: T extends { cancel: "symbol" }
? typeof kCancel
: inferPromptReturnType<T> /* default */;

/**
* Asynchronously prompts the user for input based on specified options.
* Supports text, confirm, select and multi-select prompts.
Expand All @@ -123,21 +154,54 @@ export async function prompt<
>(
message: string,
opts: PromptOptions = {},
): Promise<inferPromptReturnType<T>> {
): Promise<inferPromptReturnType<T> | inferPromptCancalReturnType<T>> {
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") {
Expand All @@ -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") {
Expand All @@ -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}`);
Expand Down

0 comments on commit faa9cbd

Please sign in to comment.