Skip to content

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.

bash
npm i @listr2/prompt-adapter-enquirer enquirer
bash
yarn add @listr2/prompt-adapter-enquirer enquirer
bash
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.

ts
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.

ts
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.

typescript
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 #676

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.

ts
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 #676

DANGER

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.

bash
npm i @listr2/prompt-adapter-inquirer @inquirer/prompts
bash
yarn add @listr2/prompt-adapter-inquirer @inquirer/prompts
bash
pnpm i @listr2/prompt-adapter-inquirer @inquirer/prompts
Single Prompt
ts
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.

ts
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.