Skip to content

Commit

Permalink
feat: confirm, prompt, and select take a callback to pass responses
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rafamel committed Jul 5, 2024
1 parent d4bf25e commit 1e83846
Show file tree
Hide file tree
Showing 5 changed files with 38 additions and 48 deletions.
4 changes: 2 additions & 2 deletions src/tasks/creation/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import { run } from '../../utils/run';
* @returns Task
*/
export function create(
fn: UnaryFn<Context, MaybePromise<Task | Empty>>
callback: UnaryFn<Context, MaybePromise<Task | Empty>>
): Task.Async {
return async (ctx: Context): Promise<void> => {
const task = await fn(ctx);
const task = await callback(ctx);
if (task) await run(ctx, task);
};
}
8 changes: 3 additions & 5 deletions src/tasks/reflection/lift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
});
}

Expand Down
18 changes: 8 additions & 10 deletions src/tasks/stdio/confirm.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 | Empty>
): Task.Async {
return create(async (ctx) => {
const opts = shallow(
Expand Down Expand Up @@ -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);
}
);
});
Expand Down
24 changes: 9 additions & 15 deletions src/tasks/stdio/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createInterface } from 'node:readline';

import {
type Empty,
type MaybePromise,
type NullaryFn,
TypeGuard,
type UnaryFn,
Expand All @@ -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';
Expand Down Expand Up @@ -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 | Empty>
): Task.Async {
return create(async (ctx) => {
const opts = shallow(
{
Expand All @@ -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),
Expand Down Expand Up @@ -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));
});
}

Expand Down
32 changes: 16 additions & 16 deletions src/tasks/stdio/select.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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<Task | Empty>
values: string[],
callback: (selection: string) => MaybePromise<Task | Empty>
): Task.Async {
return create(async (ctx) => {
const opts = shallow(
Expand All @@ -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(
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -144,22 +143,23 @@ export function select(
if (response !== null) {
return series(
log('info', 'Select:', style(response, { bold: true })),
tasks[response]
create(() => callback(response))
);
}

// No response and no timeout triggered
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))
);
}

Expand Down

0 comments on commit 1e83846

Please sign in to comment.