@poppinss/cliui
TypeScript icon, indicating that this package has built-in type declarations

6.4.1 • Public • Published

@poppinss/cliui

Opinionated UI KIT for Command Line apps

gh-workflow-image typescript-image npm-image license-image

Why this package exists?

CLI UI is an opinionated UI Kit to log messages, render tables, display spinners, and much more. Following are some of the reasons for creating this package.

  • First-class support for testing the UI kit output.
  • Using a standard set of design elements without allowing them to be configurable. Choosing consistency over configurability.

Basic usage

Install the package from the npm registry as follows:

npm i @poppinss/cliui

# yarn lovers
yarn add @poppinss/cliui

Import the cliui and create an instance of it.

import { cliui } from '@poppinss/cliui'
const ui = cliui()

ui.logger.info('This is an info message')
// [ info ] This is an info message

Now, let's say you are testing a command and want to assert that an info message is logged during the command's execution.

Usually, you will have to hack into the process.stdout stream and collect messages within memory (for assertion) and strip any colors (aka ansi sequences) from the output.

However, with this package, you can turn on the raw mode to collect the logger messages within memory and turn off all color transformations.

// RAW MODE ON
const ui = cliui({ raw: true })

ui.logger.info('This is an info message')

const logs = ui.logger.getRenderer().getLogs()
assert.deepEqual(logs, [
  {
    stream: 'stdout',
    message: '[ cyan(info) ] This is an info message'
  }
])

Similarly, you can assert that an error message is logged to stderr.

const ui = cliui({ raw: true })

ui.logger.error('Something went wrong')

const logs = ui.logger.getRenderer().getLogs()
assert.deepEqual(logs, [
  {
    stream: 'stderr',
    message: '[ red(error) ] Something went wrong'
  }
])

Logger

The logger displays all the log messages with consistent styling. Following are the available logging methods.

import { cliui } from '@poppinss/cliui'
const ui = cliui()

// Writes to stdout
logger.debug('Something just happened')
logger.info('This is an info message')
logger.success('Account created')
logger.warning('Running out of disk space')

// Writes to stderr
logger.error(new Error('Unable to write. Disk full'))
logger.fatal(new Error('Unable to write. Disk full'))

You can also define the prefix and suffix for the log message. The prefix and suffix are displayed with lower opacity (the dim color transformation is used).

logger.info('Install packages', { suffix: 'npm i --production' })

You can display current time as a prefix using the %time% keyword.

logger.info('Message with time prefix', { prefix: '%time%' })

Loading animation

You can display a log message with a loading animation using the logger.await method. The method accepts the initial message to display alongside an optional prefix or suffix.

const loader = logger.await('installing packages', { suffix: 'npm i' })

// Start animation
loader.start()

// Update the message
loader.update('unpacking packages', { suffix: undefined })

// Stop loader
loader.stop()

Spinner in silent mode

If you writing conditionals around the spinning animation to start it in certain conditions, then you might want to create the spinner in silent mode. In silent mode, the spinner will not output any logs.

const loader = logger.await('installing packages', {
  suffix: 'npm i',
  silent: true // 👈
})

// Prints nothing
loader.start()

// Prints nothing
loader.update('unpacking packages', { suffix: undefined })

// Prints nothing
loader.stop()

Preparing messages without writing them

You can also use the logger to just prepare the message (with colors and formatting) without writing it to the output stream. Just prefix the log message with prepare and it will return a string value.

const debugMessage = logger.prepareDebug('Something just happened')
const infoMessage = logger.prepareInfo('This is an info message')
const successMessage = logger.prepareSuccess('Account created')
const warningMessage = logger.prepareWarning('Running out of disk space')

Testing logger output

First, you must instantiate the cliui in raw mode to collect all logs messages within memory. And then you can access the logs using logger.getRenderer() to write assertions.

const ui = cliui({ raw: true })

ui.logger.info('Hello world')

const logs = ui.logger.getRenderer().getLogs()
console.log(logs)

You can also flush all logs by calling flushLogs.

ui.logger.info('Hello world')

const logs = ui.logger.getRenderer().getLogs()
console.log(logs.length); // 1
ui.logger.getRenderer().flushLogs();
console.log(logs.length); // 0

Logger actions

Logger actions are pre-styled logs to display the outcome of an action. For example, the action can be to create/update or delete a file.

You can create an action by calling the logger.action method and pass the message to display. Once, done perfoming the underlying operation, you can either mark the action as succeeded, skipped, or failed.

logger
  .action('Creating config/auth.ts')
  .displayDuration()
  .succeeded()

logger
  .action('Updating .tsconfig.json')
  .succeeded()

logger
  .action('Creating app/Models/User.ts')
  .skipped('File already exists')

logger
  .action('Creating server.ts')
  .failed(new Error('File already exists'))

Table

You can create a table using the ui.table method. Under the hood, we are using cli-table3 but only expose some of its configuration options for consistency.

const ui = cliui()
const table = ui.table()

table
  .head([
    'Migration',
    'Duration',
    'Status',
  ])
  .row([
    '1590591892626_tenants.ts',
    '2ms',
    'DONE'
  ])
  .row([
    '1590595949171_entities.ts',
    '2ms',
    'DONE'
  ])
  .render()

You can apply color transforms to any value when rendering the table. For example:

table.row([
  '1590595949171_entities.ts',
  '2',
  ui.colors.green('DONE')
])

Right-align columns

You can right-align the columns by defining them as objects and using the hAlign property. Also, make sure to align the header column right as well.

table
  .head([
    'Migration',
    'Batch'
    {
      content: 'Status',
      hAlign: 'right'
    },
  ])

table.row([
  '1590595949171_entities.ts',
  '2',
  {
    content: ui.colors.green('DONE'),
    hAlign: 'right'
  }
])

Render full width

You can render tables in full width (taking all the space of the terminal) by calling the table.fullWidth method. In full-width mode:

  • We will render all columns as per the size of the content.
  • Except for the first column, which takes all the available space.
table.fullWidth()

You can also change the column index for the fluid column (the one that takes all the space) by calling the table.fluidColumnIndex method.

table
  .fullWidth()
  .fluidColumnIndex(1)

Testing table output

First, you must instantiate the cliui in raw mode to collect all logs messages within memory. And then you can access the table output using logger.getRenderer() to write assertions.

const ui = cliui({ raw: true })
const table = ui.table()

table
  .head(['Migration','Duration', 'Status'])
  .row([ '1590591892626_tenants.ts', '2ms', 'DONE'])
  .render()

const logs = table.getRenderer().getLogs()
console.log(logs)

Instructions

The instructions widget allows you to render a box with steps. Each step gets prefixed with an arrow >.

const ui = cliui()
const instructions = ui.instructions()

instructions
  .add(`cd ${colors.cyan('hello-world')}`)
  .add(`Run ${colors.cyan('node ace serve --watch')} to start the server`)
  .render()

Sticker

The sticker widget is the same as the instructions widget. But it does not prefix each line with an arrow >.

const ui = cliui()
const sticker = ui.sticker()

sticker
  .add('Started HTTP server')
  .add('')
  .add(`Local address:    ${colors.cyan('http://localhost:3333')}`)
  .add(`Network address:  ${colors.cyan('http://192.168.1.2:3333')}`)
  .render()

Testing instructions and sticker output

First, you must instantiate the cliui in raw mode to collect all logs messages within memory. And then you can access the instructions/sticker output using logger.getRenderer() to write assertions.

const ui = cliui({ raw: true })
const instructions = ui.instructions()

instructions
  .add(`cd ${colors.cyan('hello-world')}`)
  .add(`Run ${colors.cyan('node ace serve --watch')} to start the server`)
  .render()

const logs = instructions.getRenderer().getLogs()
console.log(logs)

Tasks

The tasks widget allows rendering a list of tasks to perform. Each task has an associated async callback to perform the task, report its progress, and also mark it as succeeded or failed.

  • The return value of the callback function is used as the success message.
  • You can throw an Error to mark the task as failed. Or, call the task.error method to prepare an error from a string value.
const ui = cliui()
const tasks = ui.tasks()

await tasks
  .add('clone repo', async (task) => {
    return 'Completed'
  })
  .add('update package file', async (task) => {
    return task.error('Unable to update package file')
  })
  .add('install dependencies', async (task) => {
    return 'Installed'
  })
  .run()

Reporting task progress

Instead of writing the task progress messages to the console directly, you recommend you call the task.update method.

The method ensures to display of the latest log message only when using the minimal renderer and logs all messages when using the verbose renderer.

const sleep = () => new Promise<void>((resolve) => setTimeout(resolve, 50))

tasks
  .add('clone repo', async (task) => {
    for (let i = 0; i <= 100; i = i + 2) {
      await sleep()
      task.update(`Downloaded ${i}%`)
    }

    return 'Completed'
  })

Using verbose renderer

The verbose renderer displays all the log messages instead of just the latest one. Also, the task output is rendered differently from the minimal renderer. Please, check the example file for the same.

You can create the tasks instance with a verbose renderer as follows. The rest of the API is the same.

const tasks = ui.tasks({ verbose: true })

Testing tasks output

First, you must instantiate the cliui in raw mode to collect all logs messages within memory. And then you can access the tasks output using logger.getRenderer() to write assertions.

const ui = cliui({ raw: true })
const tasks = ui.tasks()

await tasks
  .add('clone repo', async (task) => {
    return 'Completed'
  })
  .add('update package file', async (task) => {
    return task.error('Unable to update package file')
  })
  .add('install dependencies', async (task) => {
    return 'Installed'
  })
  .run()

const logs = tasks.getRenderer().getLogs()
console.log(logs)

Readme

Keywords

Package Sidebar

Install

npm i @poppinss/cliui

Weekly Downloads

35,049

Version

6.4.1

License

MIT

Unpacked Size

182 kB

Total Files

28

Last publish

Collaborators

  • julien-r44
  • romainlanz
  • virk