Terminal UI

Terminal UI

Ace terminal UI is powered by the @poppinss/cliui package. The package exports helpers to display logs, render tables, render animated tasks UI, and much more.

The terminal UI primitives are built with testing in mind. When writing tests, you may turn on the raw mode to disable colors and formatting and collect all logs in memory to write assertions against them.

See also: Testing Ace commands

Displaying log messages

You may display log messages using the CLI logger. Following is the list of available log methods.

import { BaseCommand } from '@adonisjs/core/ace'
export default class GreetCommand extends BaseCommand {
async run() {
this.logger.debug('Something just happened')
this.logger.info('This is an info message')
this.logger.success('Account created')
this.logger.warning('Running out of disk space')
// Writes to stderr
this.logger.error(new Error('Unable to write. Disk full'))
this.logger.fatal(new Error('Unable to write. Disk full'))
}
}

Adding prefix and suffix

Using the options object, you may define the prefix and suffix for the log message. The prefix and suffix are displayed with lower opacity.

this.logger.info('installing packages', {
suffix: 'npm i --production'
})
this.logger.info('installing packages', {
prefix: process.pid
})

Loading animation

A log message with loading animation appends animated three dots (...) to the message. You may update the log message using the animation.update method and stop the animation using the animation.stop method.

const animation = this.logger.await('installing packages', {
suffix: 'npm i'
})
animation.start()
// Update the message
animation.update('unpacking packages', {
suffix: undefined
})
// Stop animation
animation.stop()

Logger actions

Logger actions can display the state of action with consistent styling and colors.

You may create an action using the logger.action method. The method accepts the action title as the first parameter.

const createFile = this.logger.action('creating config/auth.ts')
try {
await performTasks()
createFile.displayDuration().succeeded()
} catch (error) {
createFile.failed(error)
}

You can mark an action as either succeeded, failed, or skipped.

action.succeeded()
action.skipped('Skip reason')
action.failed(new Error())

Formatting text with ANSI colors

Ace uses kleur for formatting text with ANSI colors. Using the this.colors property, you can access kleur's chained API.

this.colors.red('[ERROR]')
this.colors.bgGreen().white(' CREATED ')

Rendering tables

A table can be created using the this.ui.table method. The method returns an instance of the Table class that you can use to define the table head and rows.

import { BaseCommand } from '@adonisjs/core/ace'
export default class GreetCommand extends BaseCommand {
async run() {
const table = this.ui.table()
table
.head([
'Migration',
'Duration',
'Status',
])
.row([
'1590591892626_tenants.ts',
'2ms',
'DONE'
])
.row([
'1590595949171_entities.ts',
'2ms',
'DONE'
])
.render()
}
}

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

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

Right-align columns

You may right-align the columns by defining them as objects and using the hAlign property. Make sure to also right-align the header column.

table
.head([
'Migration',
'Batch'
{
content: 'Status',
hAlign: 'right'
},
])
table.row([
'1590595949171_entities.ts',
'2',
{
content: this.colors.green('DONE'),
hAlign: 'right'
}
])

Full-width rendering

By default, tables are rendered with width auto, taking only the space required by the contents of each column.

However, you may render tables at full-width (taking all the terminal space) using the 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().render()

You may 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)

Printing stickers

Stickers allow you to render content inside a box with a border. They are helpful when you want to draw the user's attention to an essential piece of information.

You can create an instance of a sticker using the this.ui.sticker method.

import { BaseCommand } from '@adonisjs/core/ace'
export default class GreetCommand extends BaseCommand {
async run() {
const sticker = this.ui.sticker()
sticker
.add('Started HTTP server')
.add('')
.add(`Local address: ${this.colors.cyan('http://localhost:3333')}`)
.add(`Network address: ${this.colors.cyan('http://192.168.1.2:3333')}`)
.render()
}
}

If you want to display a set of instructions inside a box, you can use the this.ui.instructions method. Each line in the instructions output will be prefixed with an arrow sign >.

Rendering tasks

The tasks widget provides an excellent UI for sharing the progress of multiple time-taking tasks. The widget has the following two rendering modes:

  • In minimal mode, the UI for the currently running task is expanded to show one log line at a time.
  • In verbose mode, each log statement is rendered in its line. The verbose renderer must be used for debugging tasks.

You can create an instance of the tasks widget using the this.ui.tasks method.

import { BaseCommand } from '@adonisjs/core/ace'
export default class GreetCommand extends BaseCommand {
async run() {
const tasks = this.ui.tasks()
// ... rest of the code to follow
}
}

Individual tasks are added using the tasks.add method. The method accepts the task title as the first parameter and the implementation callback as the second parameter.

You must return the status of the task from the callback. All return values are considered success messages until you wrap them inside the task.error method or throw an exception inside the callback.

import { BaseCommand } from '@adonisjs/core/ace'
export default class GreetCommand extends BaseCommand {
async run() {
const tasks = this.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, it is recommended to call the task.update method.

The update method displays the latest log message using the minimal renderer and logs all messages 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'
})

Switching to the verbose renderer

You may switch to a verbose renderer when creating the tasks widget. You might consider allowing the command's user to pass a flag to turn on the verbose mode.

import { BaseCommand, flags } from '@adonisjs/core/ace'
export default class GreetCommand extends BaseCommand {
@flags.boolean()
declare verbose: boolean
async run() {
const tasks = this.ui.tasks({
verbose: this.verbose
})
}
}