From 1e838463da4566fa130a3d57becc143f3757aeab Mon Sep 17 00:00:00 2001 From: Rafa Mel Date: Fri, 5 Jul 2024 14:51:00 +0200 Subject: [PATCH] feat: confirm, prompt, and select take a callback to pass responses A series of stdio tasks (confirm, prompt, and select) take a Task returning callback instead of a number of Tasks in order to pass user responses BREAKING CHANGE: Tasks confirm, prompt, and select, have a different signature. Review the lastest documentation. --- src/tasks/creation/create.ts | 4 ++-- src/tasks/reflection/lift.ts | 8 +++----- src/tasks/stdio/confirm.ts | 18 ++++++++---------- src/tasks/stdio/prompt.ts | 24 +++++++++--------------- src/tasks/stdio/select.ts | 32 ++++++++++++++++---------------- 5 files changed, 38 insertions(+), 48 deletions(-) diff --git a/src/tasks/creation/create.ts b/src/tasks/creation/create.ts index d2ddc30..56cbcd6 100644 --- a/src/tasks/creation/create.ts +++ b/src/tasks/creation/create.ts @@ -10,10 +10,10 @@ import { run } from '../../utils/run'; * @returns Task */ export function create( - fn: UnaryFn> + callback: UnaryFn> ): Task.Async { return async (ctx: Context): Promise => { - const task = await fn(ctx); + const task = await callback(ctx); if (task) await run(ctx, task); }; } diff --git a/src/tasks/reflection/lift.ts b/src/tasks/reflection/lift.ts index 8d9ee95..af499eb 100644 --- a/src/tasks/reflection/lift.ts +++ b/src/tasks/reflection/lift.ts @@ -118,11 +118,9 @@ export function lift( if (opts.mode === 'fix') { return write(pkgPath, pkg, { exists: 'overwrite' }); } - return confirm( - { default: true, message: 'Continue?' }, - write(pkgPath, pkg, { exists: 'overwrite' }), - null - ); + return confirm({ default: true, message: 'Continue?' }, (confirmation) => { + return confirmation ? write(pkgPath, pkg, { exists: 'overwrite' }) : null; + }); }); } diff --git a/src/tasks/stdio/confirm.ts b/src/tasks/stdio/confirm.ts index f3a83f7..70d1568 100644 --- a/src/tasks/stdio/confirm.ts +++ b/src/tasks/stdio/confirm.ts @@ -1,8 +1,7 @@ -import { type Empty, TypeGuard } from 'type-core'; +import { type Empty, type MaybePromise, TypeGuard } from 'type-core'; import { shallow } from 'merge-strategies'; import type { Task } from '../../definitions'; -import { run } from '../../utils/run'; import { isInteractive } from '../../utils/is-interactive'; import { create } from '../creation/create'; import { prompt } from './prompt'; @@ -27,13 +26,13 @@ export interface ConfirmOptions { /** * Uses a context's stdio to prompt for confirmation. - * Executes a task in response to user confirmation or rejection. + * The `confirmation` will be passed to a `Task` + * returning `callback` as a boolean. * @returns Task */ export function confirm( options: ConfirmOptions | Empty, - yes: Task | Empty, - no: Task | Empty + callback: (confirmation: boolean) => MaybePromise ): Task.Async { return create(async (ctx) => { const opts = shallow( @@ -64,11 +63,10 @@ export function confirm( ); } }, - async (ctx) => { - const response = (ctx.args[0] || '').toLowerCase(); - const task = response[0] === 'y' ? yes : no; - if (!task) return; - await run({ ...ctx, args: ctx.args.slice(1) }, task); + (str) => { + const response = (str.at(0) || '').toLowerCase(); + const confirmation = response === 'y'; + return callback(confirmation); } ); }); diff --git a/src/tasks/stdio/prompt.ts b/src/tasks/stdio/prompt.ts index 05cb741..462d012 100644 --- a/src/tasks/stdio/prompt.ts +++ b/src/tasks/stdio/prompt.ts @@ -2,6 +2,7 @@ import { createInterface } from 'node:readline'; import { type Empty, + type MaybePromise, type NullaryFn, TypeGuard, type UnaryFn, @@ -18,7 +19,6 @@ import { isInteractive } from '../../utils/is-interactive'; import { isCancelled, onCancel } from '../../utils/cancellation'; import { style } from '../../utils/style'; import { run } from '../../utils/run'; -import { context } from '../creation/context'; import { create } from '../creation/create'; import { series } from '../aggregate/series'; import { raises } from '../exception/raises'; @@ -50,11 +50,14 @@ export interface PromptOptions { /** * Uses a context's stdio to prompt for a user response. - * The response will be prepended to the context arg array - * for `task`, when valid. + * When valid, the `response` will be passed to a `Task` + * returning `callback`. * @returns Task */ -export function prompt(options: PromptOptions | Empty, task: Task): Task.Async { +export function prompt( + options: PromptOptions | Empty, + callback: (response: string) => MaybePromise +): Task.Async { return create(async (ctx) => { const opts = shallow( { @@ -76,10 +79,7 @@ export function prompt(options: PromptOptions | Empty, task: Task): Task.Async { 'Non-interactive default:', style(opts.default, { bold: true }) ), - context( - (ctx) => ({ ...ctx, args: [opts.default || '', ...ctx.args] }), - task - ) + create(() => callback(opts.default || '')) ) : series( print(message), @@ -158,13 +158,7 @@ export function prompt(options: PromptOptions | Empty, task: Task): Task.Async { /* Response is valid */ const str = response; - return context( - (ctx) => ({ - ...ctx, - args: [str, ...ctx.args] - }), - task - ); + return create(() => callback(str)); }); } diff --git a/src/tasks/stdio/select.ts b/src/tasks/stdio/select.ts index 5c2878d..9bf7779 100644 --- a/src/tasks/stdio/select.ts +++ b/src/tasks/stdio/select.ts @@ -1,6 +1,6 @@ import { Transform } from 'node:stream'; -import { type Dictionary, type Empty, TypeGuard } from 'type-core'; +import type { Empty, MaybePromise } from 'type-core'; import { shallow } from 'merge-strategies'; import cliSelect from 'cli-select'; @@ -37,13 +37,14 @@ export interface SelectOptions { /** * Uses a context's stdio to prompt for input. - * Executes a task in response to user selection. - * Takes in a record of `tasks`. + * Offers `values` to select from and passes the + * user `selection` to a `Task` returning `callback`. * @returns Task */ export function select( options: SelectOptions | Empty, - tasks: Dictionary + values: string[], + callback: (selection: string) => MaybePromise ): Task.Async { return create(async (ctx) => { const opts = shallow( @@ -55,24 +56,22 @@ export function select( options || undefined ); - const names = Object.keys(tasks); - const fallback: number = TypeGuard.isString(opts.default) - ? names.indexOf(opts.default) - : -1; + const names = Object.keys(values); const message = getBadge('prompt') + ` ${opts.message}`; await run(ctx, print(message)); if (isCancelled(ctx)) return; if (!isInteractive(ctx)) { - return fallback >= 0 && opts.default + const value = opts.default; + return value ? series( log( 'info', 'Default selection [non-interactive]:', - style(opts.default, { bold: true }) + style(value, { bold: true }) ), - tasks[opts.default] + create(() => callback(value)) ) : raises( new Error( @@ -105,7 +104,7 @@ export function select( const response = await cliSelect({ cleanup: true, values: names, - ...(fallback >= 0 ? { defaultValue: fallback } : {}), + ...(opts.default ? { defaultValue: names.indexOf(opts.default) } : {}), selected: addPrefix(getBadge('selected'), ' '.repeat(3), 'print', ctx), unselected: addPrefix( getBadge('unselected'), @@ -144,7 +143,7 @@ export function select( if (response !== null) { return series( log('info', 'Select:', style(response, { bold: true })), - tasks[response] + create(() => callback(response)) ); } @@ -152,14 +151,15 @@ export function select( if (!didTimeout) throw new Error(`User cancellation`); // No response and timeout triggered with a default selection available - if (fallback >= 0 && opts.default) { + const value = opts.default; + if (value) { return series( log( 'info', 'Default selection [timeout]:', - style(opts.default, { bold: true }) + style(value, { bold: true }) ), - tasks[opts.default] + create(() => callback(value)) ); }