Skip to content

Repository files navigation

@poppinss/cliui

Opinionated UI KIT for Command Line apps

gh-workflow-imagetypescript-imagenpm-imagelicense-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'constui=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 ONconstui=cliui({raw: true})ui.logger.info('This is an info message')constlogs=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.

constui=cliui({raw: true})ui.logger.error('Something went wrong')constlogs=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'constui=cliui()// Writes to stdoutlogger.debug('Something just happened')logger.info('This is an info message')logger.success('Account created')logger.warning('Running out of disk space')// Writes to stderrlogger.error(newError('Unable to write. Disk full'))logger.fatal(newError('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.

constloader=logger.await('installing packages',{suffix: 'npm i'})// Start animationloader.start()// Update the messageloader.update('unpacking packages',{suffix: undefined})// Stop loaderloader.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.

constloader=logger.await('installing packages',{suffix: 'npm i',silent: true// 👈})// Prints nothingloader.start()// Prints nothingloader.update('unpacking packages',{suffix: undefined})// Prints nothingloader.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.

constdebugMessage=logger.prepareDebug('Something just happened')constinfoMessage=logger.prepareInfo('This is an info message')constsuccessMessage=logger.prepareSuccess('Account created')constwarningMessage=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.

constui=cliui({raw: true})ui.logger.info('Hello world')constlogs=ui.logger.getRenderer().getLogs()console.log(logs)

You can also flush all logs by calling flushLogs.

ui.logger.info('Hello world')constlogs=ui.logger.getRenderer().getLogs()console.log(logs.length);// 1ui.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(newError('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.

constui=cliui()consttable=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.

constui=cliui({raw: true})consttable=ui.table()table.head(['Migration','Duration','Status']).row(['1590591892626_tenants.ts','2ms','DONE']).render()constlogs=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 >.

constui=cliui()constinstructions=ui.instructions()instructions.add(`cd ${ui.colors.cyan('hello-world')}`).add(`Run ${ui.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 >.

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

Steps

The steps widget allows you to display a series of sequential steps with counters, titles, and optional content. Steps are visually connected with a vertical border.

constui=cliui()conststeps=ui.steps()steps.add('Install dependencies','Run npm install to get started').add('Configure app','Create a .env file with your settings').add('Start server','Use npm start to launch the application').render()

Each step is numbered automatically and can include:

  • A title (required): The main heading for the step
  • content (optional): Additional details or instructions for the step

The content supports multiline text and ANSI formatting:

steps.add('Configure environment',`Copy .env.example to .env\nUpdate API keys and credentials`)

You can also use the prepare() method to get the formatted output without rendering:

constoutput=steps.prepare()console.log(output)

Testing steps output

First, you must instantiate the cliui in raw mode to collect all logs messages within memory. In raw mode, the visual borders and indentation are omitted to make assertions easier.

constui=cliui({raw: true})conststeps=ui.steps()steps.add('Install dependencies','Run npm install').add('Start server','Run npm start').render()constlogs=steps.getRenderer().getLogs()console.log(logs)

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.

constui=cliui({raw: true})constinstructions=ui.instructions()instructions.add(`cd ${ui.colors.cyan('hello-world')}`).add(`Run ${ui.colors.cyan('node ace serve --watch')} to start the server`).render()constlogs=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.
constui=cliui()consttasks=ui.tasks()awaittasks.add('clone repo',async(task)=>{return'Completed'}).add('update package file',async(task)=>{returntask.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.

constsleep=()=>newPromise<void>((resolve)=>setTimeout(resolve,50))tasks.add('clone repo',async(task)=>{for(leti=0;i<=100;i=i+2){awaitsleep()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.

consttasks=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.

constui=cliui({raw: true})consttasks=ui.tasks()awaittasks.add('clone repo',async(task)=>{return'Completed'}).add('update package file',async(task)=>{returntask.error('Unable to update package file')}).add('install dependencies',async(task)=>{return'Installed'}).run()constlogs=tasks.getRenderer().getLogs()console.log(logs)

About

Opinionated UI KIT for Command Line apps

Resources

Code of conduct

Contributing

Security policy

Stars

155 stars

Watchers

5 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages