Prompts
Prompts use adapters and optional peer dependencies to provide interactivity with the user. The problem that we have with this application is that we are utilizing a single console updater, therefore we cannot directly write to process.stdout
. This behavior requires a adapter in between to instead write to task.stdout
and control the ANSI escape sequences for clearing lines since we do not have a vt100
compatible interface through the console updater.
Since v7.0.0, for the ability to support multiple prompt providers, signature of the function task.prompt
has changed requiring a adapter first.
Adapters
enquirer
The input adapter uses the beautiful and not very well-maintained (xD) enquirer
.
DANGER
enquirer
is an optional peer dependency. Please install it first.
npm i @listr2/prompt-adapter-enquirer enquirer
yarn add @listr2/prompt-adapter-enquirer enquirer
pnpm i @listr2/prompt-adapter-enquirer enquirer
Inside a Task, the task.prompt
function gives you access to any enquirer
default prompt as well as ability to modify the underlying instance for using a custom enquirer
prompt.
To get input from the user you can assign the task a new prompt in an async function and write the response to the context.
WARNING
It is not advised to run prompts in concurrent tasks because multiple prompts will clash and overwrite each other's console output and when you do keyboard movements it will apply to them both.
This has been disabled to do in some renderers, but you are still able to do it with some renderers.
Example
You can find the related examples here.
Usage
To access the prompts just utilize the task.prompt
jumper function by passing in your enquirer
prompts as an argument.
INFO
Please note that I rewrote the types for the enquirer
and bundle them with this application.
So it is highly likely that it has some mistakes in it since I usually do not use all of them. I will merge the original types when the enquirer
fixes them with the pending merge request #235 , which can be tracked in issue , which will probably never happen!
Single Prompt
DANGER
I have done a little trick here where, whenever you have just one prompt, then you do not have to name your prompt as in enquirer
, it will be automatically named and then returned.
import { ListrEnquirerPromptAdapter } from '@listr2/prompt-adapter-enquirer'
import { Listr } from 'listr2'
interface Ctx {
input: boolean
}
const tasks = new Listr<Ctx>(
[
{
task: async (ctx, task): Promise<boolean> => ctx.input = await task.prompt(ListrEnquirerPromptAdapter).run<boolean>({ type: 'Toggle', message: 'Do you love me?' })
},
{
title: 'This task will get your input.',
task: async (ctx, task): Promise<void> => {
ctx.input = await task.prompt(ListrEnquirerPromptAdapter).run<boolean>({ type: 'Toggle', message: 'Do you love me?' })
// do something
if (ctx.input === false) {
throw new Error(':/')
}
}
}
],
{ concurrent: false }
)
const ctx = await tasks.run()
console.log(ctx)
Multiple Prompts
WARNING
If you want to pass in an array of prompts, be careful that you should name them, this is also enforced by Typescript as well. This is not true for single prompts, since they only return a single value, it will be direct gets past to the assigned variable.
import { ListrEnquirerPromptAdapter } from '@listr2/prompt-adapter-enquirer'
import { Listr } from 'listr2'
interface Ctx {
input?: {
first: boolean
second: boolean
}
}
const tasks = new Listr<Ctx>(
[
{
title: 'This task will get your input.',
task: async (ctx, task): Promise<void> => {
ctx.input = await task.prompt(ListrEnquirerPromptAdapter).run<{ first: boolean, second: boolean }>([
{
type: 'Toggle',
name: 'first',
message: 'Do you love me?'
},
{
type: 'Toggle',
name: 'second',
message: 'Do you love me?'
}
])
// do something
if (ctx.input.first === false) {
task.output = 'oh okay'
}
if (ctx.input.second === false) {
throw new Error('You did not had to tell me for the second time')
}
}
}
],
{ concurrent: false }
)
const ctx = await tasks.run()
console.log(ctx)
Use a Custom Prompt
You can either use a custom prompt out of the npm registry, or a custom-created one as long as it works with the enquirer
, it will work as expected. Instead of passing in the prompt name use the not-new-invoked class.
import Enquirer from 'enquirer'
import EditorPrompt from 'enquirer-editor'
import { Listr, ListrEnquirerPromptAdapter } from 'listr2'
const enquirer = new Enquirer()
enquirer.register('editor', Editor)
const tasks = new Listr<Ctx>(
[
{
title: 'Custom prompt',
task: async (ctx, task): Promise<void> => {
ctx.testInput = await task.prompt(ListrEnquirerPromptAdapter).run(
{
type: 'editor',
message: 'Write something in this enquirer custom prompt.',
initial: 'Start writing!',
validate: (response): boolean | string => {
return true
}
},
{ enquirer }
)
}
}
],
{ concurrent: false }
)
const ctx = await tasks.run()
console.log(ctx)
Cancel a Prompt
v7.0.0 #173 #676Since Task keeps track of the active prompt and this adapter exposes a cancel
method, you can cancel a prompt while it is still active.
import { ListrEnquirerPromptAdapter } from '@listr2/prompt-adapter-enquirer'
import { delay, Listr } from 'listr2'
interface Ctx {
input: boolean
}
const tasks = new Listr<Ctx>(
[
{
title: 'This task will get your input.',
task: async (ctx, task): Promise<void> => {
const prompt = task.prompt(ListrEnquirerPromptAdapter)
// Cancel the prompt after 5 seconds
void delay(5000).then(() => prompt.cancel())
ctx.input = await prompt.run({
type: 'Input',
message: 'Give me input before it disappears.'
})
}
}
],
{ concurrent: false }
)
const ctx = await tasks.run()
console.log(ctx)
inquirer
v7.0.0 #676DANGER
inquirer
is an optional peer dependency. Please install it first.
This library utilizes @inquirer/prompts
instead of the legacy implementation inquirer
.
Please also add the necessary prompt package for using a prompt from inquirer
, you can read more about it in their documentation.
npm i @listr2/prompt-adapter-inquirer @inquirer/prompts
yarn add @listr2/prompt-adapter-inquirer @inquirer/prompts
pnpm i @listr2/prompt-adapter-inquirer @inquirer/prompts
Single Prompt
import { input } from '@inquirer/prompts'
import { ListrInquirerPromptAdapter } from '@listr2/prompt-adapter-inquirer'
import { Listr } from 'listr2'
interface Ctx {
input: string
}
const tasks = new Listr<Ctx>(
[
{
task: async (ctx, task): Promise<string> => ctx.input = await task.prompt(ListrInquirerPromptAdapter).run(input, { message: 'Please tell me about yourself' })
}
],
{ concurrent: false }
)
const ctx = await tasks.run()
console.log(ctx)
Cancel a Prompt
Since Task keeps track of the active prompt and this adapter exposes a cancel
method, you can cancel a prompt while it is still active.
WARNING
inquirer
acts a little bit different while canceling the prompt, since it is a implemented in a CancellablePromise
kind of way and not exposing submit externally, whenever the promise is cancelled it will throw an error out from the promise.
import { input } from '@inquirer/prompts'
import { ListrInquirerPromptAdapter } from '@listr2/prompt-adapter-inquirer'
import { delay, Listr } from 'listr2'
interface Ctx {
input: string
}
const tasks = new Listr<Ctx>(
[
{
title: 'This task will get your input.',
task: async (ctx, task): Promise<void> => {
const prompt = task.prompt(ListrInquirerPromptAdapter)
// Cancel the prompt after 5 seconds
void delay(5000).then(() => prompt.cancel())
ctx.input = await prompt.run(input, {
message: 'Give me input before you lose your chance to do so.'
})
}
}
],
{ concurrent: false }
)
const ctx = await tasks.run()
console.log(ctx)
Renderer
Prompts, since their output passes through an internal WritableStream
as a process.stdout
will render multiple times in non-TTY renderers. It will work anyhow albeit it might not look great. Since prompts are not even intended for non-TTY terminals, this is a novelty.
DefaultRenderer
Prompts can either have a title or not, but they will always be rendered at the end of the current console output.