@poppinss/cliui
Version:
Opinionated UI KIT for Command Line apps
2,008 lines (2,007 loc) • 47.6 kB
JavaScript
import { t as TERMINAL_SIZE } from "./helpers-DBWrFADB.js";
import supportsColor from "supports-color";
import colors, { default as poppinssColors } from "@poppinss/colors";
import CliTable from "cli-table3";
import stringWidth from "string-width";
import logUpdate from "log-update";
import prettyHrtime from "pretty-hrtime";
import boxes from "cli-boxes";
//#region src/icons.ts
const { platform } = process;
/**
* A collection of platform specific icons
*/
const icons = platform === "win32" && !process.env.WT_SESSION ? {
tick: "√",
cross: "×",
bullet: "*",
nodejs: "♦",
pointer: ">",
info: "i",
warning: "‼",
squareSmallFilled: "[█]",
borderVertical: "|"
} : {
tick: "✔",
cross: "✖",
bullet: "●",
nodejs: "⬢",
pointer: "❯",
info: "ℹ",
warning: "⚠",
squareSmallFilled: "◼",
borderVertical: "│"
};
//#endregion
//#region src/colors.ts
/**
* Returns the colors instance based upon the environment.
*
* - The "raw" option returns the colors instance that prefix the color
* transformations as raw text
* - The "silent" option returns the colors instance that performs no
* color transformations
*/
function useColors(options = {}) {
if (options.raw) return colors.raw();
if (options.silent) return colors.silent();
return colors.ansi();
}
//#endregion
//#region src/renderers/console.ts
/**
* Renders messages to the "stdout" and "stderr"
*/
var ConsoleRenderer = class {
getLogs() {
return [];
}
flushLogs() {}
log(message) {
console.log(message);
}
/**
* Log message by overwriting the existing one
*/
logUpdate(message) {
logUpdate(message);
}
/**
* Persist the last logged message
*/
logUpdatePersist() {
logUpdate.done();
}
/**
* Log error
*/
logError(message) {
console.error(message);
}
};
//#endregion
//#region src/table.ts
/**
* Exposes the API to represent a table
*/
var Table = class {
#state = {
head: [],
rows: []
};
/**
* Size of the largest row for a given
* column
*/
#columnSizes = [];
/**
* The renderer to use to output logs
*/
#renderer;
/**
* Logger configuration options
*/
#options;
/**
* The colors reference
*/
#colors;
/**
* Whether or not to render full width
*/
#renderFullWidth = false;
/**
* The column index that should take remaining
* width.
*/
#fluidColumnIndex = 0;
/**
* Padding for columns
*/
#padding = 2;
constructor(options = {}) {
this.#options = {
raw: options.raw === void 0 ? false : options.raw,
chars: options.chars
};
}
/**
* Tracking the column size and keeping on the largest
* one by tracking the content size
*/
#storeColumnSize(columns) {
columns.forEach((column, index) => {
const size = stringWidth(column);
const existingSize = this.#columnSizes[index];
if (!existingSize || existingSize < size) this.#columnSizes[index] = size;
});
}
/**
* Computes the col widths based when in fullwidth mode
*/
#computeColumnsWidth() {
/**
* Do not compute columns size, when rendering in full-width
*/
if (!this.#renderFullWidth) return;
/**
* The terminal columns
*/
let columns = TERMINAL_SIZE - (this.#columnSizes.length + 1);
this.#state.colWidths = this.#state.colWidths || [];
this.#columnSizes.forEach((column, index) => {
/**
* The column width will be the size of the biggest
* text + padding left + padding right
*/
this.#state.colWidths[index] = this.#state.colWidths[index] || column + this.#padding * 2;
/**
* Compute remaining columns
*/
columns = columns - this.#state.colWidths[index];
});
/**
* If there are remaining columns, then assign them
* to the fluid column.
*/
if (columns) {
const index = this.#fluidColumnIndex > this.#columnSizes.length - 1 ? 0 : this.#fluidColumnIndex;
this.#state.colWidths[index] = this.#state.colWidths[index] + columns;
}
}
/**
* Returns the renderer for rendering the messages
*/
getRenderer() {
if (!this.#renderer) this.#renderer = new ConsoleRenderer();
return this.#renderer;
}
/**
* Define a custom renderer. Logs to "stdout" and "stderr"
* by default
*/
useRenderer(renderer) {
this.#renderer = renderer;
return this;
}
/**
* Returns the colors implementation in use
*/
getColors() {
if (!this.#colors) this.#colors = useColors();
return this.#colors;
}
/**
* Define a custom colors implementation
*/
useColors(color) {
this.#colors = color;
return this;
}
/**
* Define table head
*/
head(headColumns) {
this.#state.head = headColumns;
this.#storeColumnSize(headColumns.map((column) => typeof column === "string" ? column : column.content));
return this;
}
/**
* Add a new table row
*/
row(row) {
this.#state.rows.push(row);
if (Array.isArray(row)) this.#storeColumnSize(row.map((cell) => typeof cell === "string" ? cell : cell.content));
return this;
}
/**
* Define custom column widths
*/
columnWidths(widths) {
this.#state.colWidths = widths;
return this;
}
/**
* Toggle whether or render in full width or not
*/
fullWidth(renderFullWidth = true) {
this.#renderFullWidth = renderFullWidth;
return this;
}
/**
* Define the column index that should take
* will remaining width when rendering in
* full-width
*/
fluidColumnIndex(index) {
this.#fluidColumnIndex = index;
return this;
}
/**
* Render table
*/
render() {
if (this.#options.raw) {
this.getRenderer().log(this.#state.head.map((col) => typeof col === "string" ? col : col.content).join("|"));
this.#state.rows.forEach((row) => {
const content = Array.isArray(row) ? row.map((cell) => typeof cell === "string" ? cell : cell.content) : Object.keys(row);
this.getRenderer().log(content.join("|"));
});
return;
}
this.#computeColumnsWidth();
/**
* Types of "cli-table3" are out of the sync from the
* implementation
*/
const cliTable = new CliTable({
head: this.#state.head,
style: {
"head": [],
"border": ["dim"],
"padding-left": 2,
"padding-right": 2
},
wordWrap: true,
...this.#state.colWidths ? { colWidths: this.#state.colWidths } : {},
chars: this.#options.chars
});
this.#state.rows.forEach((row) => cliTable.push(row));
this.getRenderer().log(cliTable.toString());
}
};
//#endregion
//#region src/steps.ts
/**
* Steps class is used to display a series of sequential steps
* with counters, titles, content, and a visual left border
* connecting them.
*
* ```ts
* const steps = ui.steps()
*
* steps.add('Install dependencies', 'Run npm install to get started')
* steps.add('Configure app', 'Create a .env file with your settings')
* steps.add('Start server', 'Use npm start to launch the application')
*
* steps.render()
* ```
*/
var Steps = class {
/**
* Collection of steps to display
*/
#steps = [];
/**
* The renderer to use for rendering output
*/
#renderer;
/**
* Colors instance for styling output
*/
#colors;
/**
* Options for the steps
*/
#options;
constructor(options = {}) {
this.#options = { raw: options.raw === void 0 ? false : options.raw };
}
/**
* Returns the renderer to use for output.
* Defaults to ConsoleRenderer if not explicitly set
*/
getRenderer() {
if (!this.#renderer) this.#renderer = new ConsoleRenderer();
return this.#renderer;
}
/**
* Define a custom renderer to use
*/
useRenderer(renderer) {
this.#renderer = renderer;
return this;
}
/**
* Returns the colors instance
*/
getColors() {
if (!this.#colors) this.#colors = useColors();
return this.#colors;
}
/**
* Define a custom colors instance
*/
useColors(colors) {
this.#colors = colors;
return this;
}
/**
* Add a new step to the collection
*
* @param title - The step title/heading
* @param content - Optional content/description for the step (supports ANSI formatting)
*/
add(title, content) {
this.#steps.push({
title,
content
});
return this;
}
/**
* Prepare the formatted output without rendering.
* Useful for testing or custom output handling.
*/
prepare() {
const colors = this.getColors();
const lines = [];
const stepCount = this.#steps.length;
this.#steps.forEach((step, index) => {
const stepNumber = index + 1;
const isLast = stepNumber === stepCount;
const counter = colors.cyan(`${stepNumber}.`);
const title = colors.bold(step.title);
lines.push(`${counter} ${title}`);
if (step.content) step.content.split("\n").forEach((line) => {
if (this.#options.raw) lines.push(line);
else lines.push(` ${line}`);
});
if (!isLast && !this.#options.raw) lines.push("");
});
return lines.join("\n");
}
/**
* Render the steps to the configured renderer
*/
render() {
const output = this.prepare();
this.getRenderer().log(output);
}
};
//#endregion
//#region src/logger/action.ts
/**
* Exposes the API to print actions in one of the following three states
*
* - failed
* - succeeded
* - skipped
*/
var Action = class {
#startTime;
/**
* Action options
*/
#options;
/**
* Action message
*/
#message;
/**
* Reference to the colors implementation
*/
#colors;
/**
* The renderer to use for writing to the console
*/
#renderer;
/**
* Whether or not to display duration of the action
*/
#displayDuration = false;
constructor(message, options = {}) {
this.#message = message;
this.#startTime = process.hrtime();
this.#options = { dim: options.dim === void 0 ? false : options.dim };
}
/**
* Format label
*/
#formatLabel(label, color) {
label = this.getColors()[color](`${label.toUpperCase()}:`);
if (this.#options.dim) return this.getColors().dim(label);
return label;
}
/**
* Format message
*/
#formatMessage(message) {
if (this.#options.dim) return this.getColors().dim(message);
return message;
}
/**
* Format the suffix
*/
#formatSuffix(message) {
message = `(${message})`;
return this.getColors().dim(message);
}
/**
* Format error
*/
#formatError(error) {
return `\n ${(typeof error === "string" ? error : error.stack || error.message).split("\n").map((line) => {
if (this.#options.dim) line = this.getColors().dim(line);
return ` ${this.getColors().red(line)}`;
}).join("\n")}`;
}
/**
* Returns the renderer for rendering the messages
*/
getRenderer() {
if (!this.#renderer) this.#renderer = new ConsoleRenderer();
return this.#renderer;
}
/**
* Define a custom renderer.
*/
useRenderer(renderer) {
this.#renderer = renderer;
return this;
}
/**
* Returns the colors implementation in use
*/
getColors() {
if (!this.#colors) this.#colors = useColors();
return this.#colors;
}
/**
* Define a custom colors implementation
*/
useColors(color) {
this.#colors = color;
return this;
}
/**
* Toggle whether to display duration for completed
* tasks or not.
*/
displayDuration(displayDuration = true) {
this.#displayDuration = displayDuration;
return this;
}
/**
* Prepares the message to mark action as successful
*/
prepareSucceeded() {
let logMessage = `${this.#formatLabel("done", "green")} ${this.#formatMessage(this.#message)}`;
if (this.#displayDuration) logMessage = `${logMessage} ${this.#formatSuffix(prettyHrtime(process.hrtime(this.#startTime)))}`;
return logMessage;
}
/**
* Mark action as successful
*/
succeeded() {
this.getRenderer().log(this.prepareSucceeded());
}
/**
* Prepares the message to mark action as skipped
*/
prepareSkipped(skipReason) {
let logMessage = `${this.#formatLabel("skipped", "cyan")} ${this.#formatMessage(this.#message)}`;
if (skipReason) logMessage = `${logMessage} ${this.#formatSuffix(skipReason)}`;
return logMessage;
}
/**
* Mark action as skipped. An optional skip reason can be
* supplied
*/
skipped(skipReason) {
this.getRenderer().log(this.prepareSkipped(skipReason));
}
/**
* Prepares the message to mark action as failed
*/
prepareFailed(error) {
return `${this.#formatLabel("failed", "red")} ${this.#formatMessage(this.#message)} ${this.#formatError(error)}`;
}
/**
* Mark action as failed. An error message is required
*/
failed(error) {
this.getRenderer().logError(this.prepareFailed(error));
}
};
//#endregion
//#region src/logger/spinner.ts
/**
* Textual spinner to print a message with dotted progress
* bar.
*/
var Spinner = class {
#animator = {
frames: [
". ",
".. ",
"...",
" ..",
" .",
" "
],
interval: 200,
index: 0,
getFrame() {
return this.frames[this.index];
},
advance() {
this.index = this.index + 1 === this.frames.length ? 0 : this.index + 1;
return this.index;
}
};
/**
* The state of the spinner
*/
#state = "idle";
/**
* Spinner message
*/
#message;
/**
* The renderer to use for writing to the console
*/
#renderer;
/**
* Custom method to handle animation result
*/
#spinnerWriter;
constructor(message) {
this.#message = message;
}
/**
* Loop over the message and animate the spinner
*/
#animate() {
if (this.#state !== "running") return;
/**
* Do not write when in silent mode
*/
if (this.#message.silent) return;
const frame = this.#animator.getFrame();
if (this.#spinnerWriter) this.#spinnerWriter(`${this.#message.render()} ${frame}`);
else this.getRenderer().logUpdate(`${this.#message.render()} ${frame}`);
setTimeout(() => {
this.#animator.advance();
this.#animate();
}, this.#animator.interval);
}
/**
* Returns the renderer for rendering the messages
*/
getRenderer() {
if (!this.#renderer) this.#renderer = new ConsoleRenderer();
return this.#renderer;
}
/**
* Define the custom renderer
*/
useRenderer(renderer) {
this.#renderer = renderer;
return this;
}
/**
* Star the spinner
*/
start() {
this.#state = "running";
this.#animate();
return this;
}
/**
* Update spinner
*/
update(text, options) {
if (this.#state !== "running") return this;
Object.assign(this.#message, {
text,
...options
});
return this;
}
/**
* Stop spinner
*/
stop() {
this.#state = "stopped";
this.#animator.index = 0;
if (!this.#spinnerWriter && !this.#message.silent) {
this.getRenderer().logUpdate(`${this.#message.render()} ${this.#animator.frames[2]}`);
this.getRenderer().logUpdatePersist();
}
}
/**
* Tap into spinner to manually write the
* output.
*/
tap(callback) {
this.#spinnerWriter = callback;
return this;
}
};
//#endregion
//#region src/renderers/memory.ts
/**
* Keeps log messages within memory. Useful for testing
*/
var MemoryRenderer = class {
#logs = [];
getLogs() {
return this.#logs;
}
flushLogs() {
this.#logs = [];
}
/**
* Log message
*/
log(message) {
this.#logs.push({
message,
stream: "stdout"
});
}
/**
* For memory renderer the logUpdate is similar to log
*/
logUpdate(message) {
this.log(message);
}
/**
* Its a noop
*/
logUpdatePersist() {}
/**
* Log message as error
*/
logError(message) {
this.#logs.push({
message,
stream: "stderr"
});
}
};
//#endregion
//#region src/logger/main.ts
/**
* CLI logger to log messages to the console. The output is consistently
* formatted.
*/
var Logger = class {
/**
* Logger configuration options
*/
#options;
/**
* Reference to the colors implementation
*/
#colors;
/**
* The renderer to use to output logs
*/
#renderer;
getLogs() {
return this.getRenderer().getLogs();
}
flushLogs() {
this.getRenderer().flushLogs();
}
constructor(options = {}) {
const dimOutput = options.dim === void 0 ? false : options.dim;
this.#options = {
dim: dimOutput,
dimLabels: options.dimLabels === void 0 ? dimOutput : options.dimLabels
};
}
/**
* Color the logger label
*/
#colorizeLabel(color, text) {
text = this.getColors()[color](text);
if (this.#options.dimLabels) return `[ ${this.getColors().dim(text)} ]`;
return `[ ${text} ]`;
}
/**
* Returns the label for a given logging type
*/
#getLabel(type) {
switch (type) {
case "success": return this.#colorizeLabel("green", type);
case "error":
case "fatal": return this.#colorizeLabel("red", type);
case "warning": return this.#colorizeLabel("yellow", "warn");
case "info": return this.#colorizeLabel("blue", type);
case "debug": return this.#colorizeLabel("cyan", type);
case "await": return this.#colorizeLabel("cyan", "wait");
}
}
/**
* Appends the suffix to the message
*/
#addSuffix(message, suffix) {
if (!suffix) return message;
return `${message} ${this.getColors().dim().yellow(`(${suffix})`)}`;
}
/**
* Appends duration to the message
*/
#addDuration(message, duration) {
if (!duration) return message;
return `${message} ${this.getColors().dim(`(${prettyHrtime(process.hrtime(duration))})`)}`;
}
/**
* Prepends the prefix to the message. We do not DIM the prefix, since
* gray doesn't have much brightness already
*/
#addPrefix(message, prefix) {
if (!prefix) return message;
prefix = prefix.replace(/%time%/, (/* @__PURE__ */ new Date()).toISOString());
return `${this.getColors().dim(`[${prefix}]`)} ${message}`;
}
/**
* Prepends the prefix to the message
*/
#prefixLabel(message, label) {
return `${label} ${message}`;
}
/**
* Decorate message string
*/
#decorateMessage(message) {
if (this.#options.dim) return this.getColors().dim(message);
return message;
}
/**
* Decorate error stack
*/
#formatStack(stack) {
if (!stack) return "";
return `\n${stack.split("\n").splice(1).map((line) => {
if (this.#options.dim) line = this.getColors().dim(line);
return ` ${this.getColors().red(line)}`;
}).join("\n")}`;
}
/**
* Returns the renderer for rendering the messages
*/
getRenderer() {
if (!this.#renderer) this.#renderer = new ConsoleRenderer();
return this.#renderer;
}
/**
* Define a custom renderer to output logos
*/
useRenderer(renderer) {
this.#renderer = renderer;
return this;
}
/**
* Returns the colors implementation in use
*/
getColors() {
if (!this.#colors) this.#colors = useColors();
return this.#colors;
}
/**
* Define a custom colors implementation
*/
useColors(color) {
this.#colors = color;
return this;
}
/**
* Log message
*/
log(message) {
this.getRenderer().log(message);
}
/**
* Log message by updating the existing line
*/
logUpdate(message) {
this.getRenderer().logUpdate(message);
}
/**
* Persist log line written using the `logUpdate`
* method.
*/
logUpdatePersist() {
this.getRenderer().logUpdatePersist();
}
/**
* Log error message using the renderer. It is similar to `console.error`
* but uses the underlying renderer instead
*/
logError(message) {
this.getRenderer().logError(message);
}
/**
* Prepares the success message
*/
prepareSuccess(message, options) {
message = this.#decorateMessage(message);
message = this.#prefixLabel(message, this.#getLabel("success"));
message = this.#addPrefix(message, options?.prefix);
message = this.#addSuffix(message, options?.suffix);
message = this.#addDuration(message, options?.startTime);
return message;
}
/**
* Log success message
*/
success(message, options) {
this.log(this.prepareSuccess(message, options));
}
/**
* Prepares the error message
*/
prepareError(message, options) {
message = typeof message === "string" ? message : message.message;
message = this.#decorateMessage(message);
message = this.#prefixLabel(message, this.#getLabel("error"));
message = this.#addPrefix(message, options?.prefix);
message = this.#addSuffix(message, options?.suffix);
message = this.#addDuration(message, options?.startTime);
return message;
}
/**
* Log error message
*/
error(message, options) {
this.logError(this.prepareError(message, options));
}
/**
* Prepares the fatal message
*/
prepareFatal(message, options) {
const stack = this.#formatStack(typeof message === "string" ? void 0 : message.stack);
message = typeof message === "string" ? message : message.message;
message = this.#decorateMessage(message);
message = this.#prefixLabel(message, this.#getLabel("error"));
message = this.#addPrefix(message, options?.prefix);
message = this.#addSuffix(message, options?.suffix);
message = this.#addDuration(message, options?.startTime);
return `${message}${stack}`;
}
/**
* Log fatal message
*/
fatal(message, options) {
this.logError(this.prepareFatal(message, options));
}
/**
* Prepares the warning message
*/
prepareWarning(message, options) {
message = this.#decorateMessage(message);
message = this.#prefixLabel(message, this.#getLabel("warning"));
message = this.#addPrefix(message, options?.prefix);
message = this.#addSuffix(message, options?.suffix);
message = this.#addDuration(message, options?.startTime);
return message;
}
/**
* Log warning message
*/
warning(message, options) {
this.log(this.prepareWarning(message, options));
}
/**
* Prepares the info message
*/
prepareInfo(message, options) {
message = this.#decorateMessage(message);
message = this.#prefixLabel(message, this.#getLabel("info"));
message = this.#addPrefix(message, options?.prefix);
message = this.#addSuffix(message, options?.suffix);
message = this.#addDuration(message, options?.startTime);
return message;
}
/**
* Log info message
*/
info(message, options) {
this.log(this.prepareInfo(message, options));
}
/**
* Prepares the debug message
*/
prepareDebug(message, options) {
message = this.#decorateMessage(message);
message = this.#prefixLabel(message, this.#getLabel("debug"));
message = this.#addPrefix(message, options?.prefix);
message = this.#addSuffix(message, options?.suffix);
message = this.#addDuration(message, options?.startTime);
return message;
}
/**
* Log debug message
*/
debug(message, options) {
this.log(this.prepareDebug(message, options));
}
/**
* Log a message with a spinner
*/
await(text, options) {
return new Spinner({
logger: this,
text,
...options,
render() {
let decorated = this.logger.#decorateMessage(this.text);
decorated = this.logger.#prefixLabel(decorated, this.logger.#getLabel("await"));
decorated = this.logger.#addPrefix(decorated, this.prefix);
decorated = this.logger.#addSuffix(decorated, this.suffix);
return decorated;
}
}).useRenderer(this.getRenderer());
}
/**
* Initiates a new action
*/
action(title) {
return new Action(title, { dim: this.#options.dim }).useColors(this.getColors()).useRenderer(this.getRenderer());
}
/**
* Create a new child instance of self
*/
child(options) {
return new this.constructor(options).useColors(this.getColors()).useRenderer(this.getRenderer());
}
/**
* Create a dummy logger that silently discards all output.
* Useful when output must be suppressed, for example
* inside a tasks-manager callback where the task widget
* owns the terminal.
*/
dummy() {
return new this.constructor().useColors(this.getColors()).useRenderer(new MemoryRenderer());
}
};
//#endregion
//#region src/instructions.ts
/**
* The box styling used by the instructions
*/
const BOX = boxes.round;
/**
* The API to render instructions wrapped inside a box
*/
var Instructions = class {
#state = { content: [] };
/**
* Renderer to use for rendering instructions
*/
#renderer;
/**
* Length of the widest line inside instructions content
*/
#widestLineLength = 0;
/**
* Number of white spaces on the left of the box
*/
#leftPadding = 4;
/**
* Number of white spaces on the right of the box
*/
#rightPadding = 8;
/**
* Number of empty lines at the top
*/
#paddingTop = 1;
/**
* Number of empty lines at the bottom
*/
#paddingBottom = 1;
/**
* Reference to the colors
*/
#colors;
/**
* Options
*/
#options;
/**
* Draws the border
*/
#drawBorder = (border, colors) => {
return colors.dim(border);
};
constructor(options = {}) {
this.#options = {
icons: options.icons === void 0 ? true : options.icons,
raw: options.raw === void 0 ? false : options.raw
};
}
/**
* Returns the length of the horizontal line
*/
#getHorizontalLength() {
return this.#widestLineLength + this.#leftPadding + this.#rightPadding;
}
/**
* Repeats text for given number of times
*/
#repeat(text, times) {
return new Array(times + 1).join(text);
}
/**
* Wraps content inside the left and right vertical lines
*/
#wrapInVerticalLines(content, leftWhitespace, rightWhitespace) {
return `${this.#drawBorder(BOX.left, this.getColors())}${leftWhitespace}${content}${rightWhitespace}${this.#drawBorder(BOX.right, this.getColors())}`;
}
/**
* Returns the top line for the box
*/
#getTopLine() {
const horizontalLength = this.#getHorizontalLength();
const horizontalLine = this.#repeat(this.#drawBorder(BOX.top, this.getColors()), horizontalLength);
return `${this.#drawBorder(BOX.topLeft, this.getColors())}${horizontalLine}${this.#drawBorder(BOX.topRight, this.getColors())}`;
}
/**
* Returns the bottom line for the box
*/
#getBottomLine() {
const horizontalLength = this.#getHorizontalLength();
const horizontalLine = this.#repeat(this.#drawBorder(BOX.bottom, this.getColors()), horizontalLength);
return `${this.#drawBorder(BOX.bottomLeft, this.getColors())}${horizontalLine}${this.#drawBorder(BOX.bottomRight, this.getColors())}`;
}
/**
* Returns the heading border bottom
*/
#getHeadingBorderBottom() {
const horizontalLength = this.#getHorizontalLength();
const horizontalLine = this.#repeat(this.#drawBorder(boxes.single.top, this.getColors()), horizontalLength);
return this.#wrapInVerticalLines(horizontalLine, "", "");
}
/**
* Decorates the instruction line by wrapping it inside the box
* lines
*/
#getContentLine(line) {
const leftWhitespace = this.#repeat(" ", this.#leftPadding);
const rightWhitespace = this.#repeat(" ", this.#widestLineLength - line.width + this.#rightPadding);
return this.#wrapInVerticalLines(line.text, leftWhitespace, rightWhitespace);
}
/**
* Returns the heading line by applying padding
*/
#getHeading() {
if (!this.#state.heading) return;
return this.#getContentLine(this.#state.heading);
}
/**
* Returns the formatted body
*/
#getBody() {
if (!this.#state.content || !this.#state.content.length) return;
const top = new Array(this.#paddingTop).fill("").map(this.#getEmptyLineNode);
const bottom = new Array(this.#paddingBottom).fill("").map(this.#getEmptyLineNode);
return top.concat(this.#state.content).concat(bottom).map((line) => this.#getContentLine(line)).join("\n");
}
/**
* Returns node for a empty line
*/
#getEmptyLineNode() {
return {
text: "",
width: 0
};
}
/**
* Returns the renderer for rendering the messages
*/
getRenderer() {
if (!this.#renderer) this.#renderer = new ConsoleRenderer();
return this.#renderer;
}
/**
* Define a custom renderer. Logs to "stdout" and "stderr"
* by default
*/
useRenderer(renderer) {
this.#renderer = renderer;
return this;
}
/**
* Returns the colors implementation in use
*/
getColors() {
if (!this.#colors) this.#colors = useColors();
return this.#colors;
}
/**
* Define a custom colors implementation
*/
useColors(color) {
this.#colors = color;
return this;
}
/**
* Draw the instructions box in fullscreen
*/
fullScreen() {
this.#widestLineLength = TERMINAL_SIZE - (this.#leftPadding + this.#rightPadding) - 2;
return this;
}
/**
* Attach a callback to self draw the borders
*/
drawBorder(callback) {
this.#drawBorder = callback;
return this;
}
/**
* Define heading for instructions
*/
heading(text) {
const width = stringWidth(text);
if (width > this.#widestLineLength) this.#widestLineLength = width;
this.#state.heading = {
text,
width
};
return this;
}
/**
* Add new instruction. Each instruction is rendered
* in a new line inside a box
*/
add(text) {
text = this.#options.icons ? `${this.getColors().dim(icons.pointer)} ${text}` : `${text}`;
const width = stringWidth(text);
if (width > this.#widestLineLength) this.#widestLineLength = width;
this.#state.content.push({
text,
width
});
return this;
}
prepare() {
/**
* Render content as it is in raw mode
*/
if (this.#options.raw) {
let output = [];
if (this.#state.heading) output.push(this.#state.heading.text);
output = output.concat(this.#state.content.map(({ text }) => text));
return output.join("\n");
}
const top = this.#getTopLine();
const heading = this.#getHeading();
const headingBorderBottom = this.#getHeadingBorderBottom();
const body = this.#getBody();
const bottom = this.#getBottomLine();
let output = `${top}\n`;
/**
* Draw heading if it exists
*/
if (heading) output = `${output}${heading}`;
/**
* Draw the border between the heading and the body if
* both exists
*/
if (heading && body) output = `${output}\n${headingBorderBottom}\n`;
/**
* Draw body if it exists
*/
if (body) output = `${output}${body}`;
return `${output}\n${bottom}`;
}
/**
* Render instructions
*/
render() {
this.getRenderer().log(this.prepare());
}
};
//#endregion
//#region src/tasks/task.ts
/**
* Task exposes a very simple API to create tasks with states, along with a
* listener to listen for the task state updates.
*
* The task itself has does not render anything to the console. The task
* renderers does that.
*/
var Task = class {
#startTime;
/**
* Last logged line for the task
*/
#lastLogLine;
/**
* Define one or more update listeners
*/
#updateListeners = [];
/**
* Duration of the task. Updated after the task is over
*/
#duration;
/**
* Message set after completing the task. Can be an error or the
* a success message
*/
#completionMessage;
/**
* Task current state
*/
#state = "idle";
constructor(title) {
this.title = title;
}
#notifyListeners() {
for (let listener of this.#updateListeners) listener(this);
}
/**
* Access the task state
*/
getState() {
return this.#state;
}
/**
* Get the time spent in running the task
*/
getDuration() {
return this.#duration || null;
}
/**
* Get error occurred while running the task
*/
getError() {
return this.#completionMessage || null;
}
/**
* Get task completion success message
*/
getSuccessMessage() {
return typeof this.#completionMessage === "string" ? this.#completionMessage : null;
}
/**
* Last logged line for the task
*/
getLastLoggedLine() {
return this.#lastLogLine || null;
}
/**
* Bind a listener to listen to the state updates of the task
*/
onUpdate(listener) {
this.#updateListeners.push(listener);
return this;
}
/**
* Start the task
*/
start() {
this.#state = "running";
this.#startTime = process.hrtime();
this.#notifyListeners();
return this;
}
/**
* Update task with log messages. Based upon the renderer
* in use, it may only display one line at a time.
*/
update(message) {
this.#lastLogLine = message;
this.#notifyListeners();
return this;
}
/**
* Mark task as completed
*/
markAsSucceeded(message) {
this.#state = "succeeded";
this.#duration = prettyHrtime(process.hrtime(this.#startTime));
this.#completionMessage = message;
this.#notifyListeners();
return this;
}
/**
* Mark task as failed
*/
markAsFailed(error) {
this.#state = "failed";
this.#duration = prettyHrtime(process.hrtime(this.#startTime));
this.#completionMessage = error;
this.#notifyListeners();
return this;
}
};
//#endregion
//#region src/tasks/renderers/verbose.ts
/**
* Verbose renderer shows a detailed output of the tasks and the
* messages logged by a given task
*/
var VerboseRenderer = class {
/**
* Reference to the colors implementation
*/
#colors;
/**
* The renderer to use to output logs
*/
#renderer;
/**
* List of registered tasks
*/
#registeredTasks = [];
#notifiedTasks = /* @__PURE__ */ new Set();
constructor() {}
/**
* Format error
*/
#formatError(error) {
if (typeof error === "string") return `${this.#getAnsiIcon("│", "dim")}${this.getColors().red(error)}`;
if (!error.stack) return `${this.#getAnsiIcon("│", "dim")}${this.getColors().red(error.message)}`;
return `${error.stack.split("\n").map((line) => `${this.#getAnsiIcon("│", "dim")} ${this.getColors().red(line)}`).join("\n")}`;
}
/**
* Returns the ansi icon back when icons are enabled
* or an empty string
*/
#getAnsiIcon(icon, color) {
return this.getColors()[color](`${icon} `);
}
/**
* Renders message for a running task
*/
#renderRunningTask(task) {
if (this.#notifiedTasks.has(task.title)) {
const lastLoggedLine = task.getLastLoggedLine();
if (lastLoggedLine) this.getRenderer().log(`${this.#getAnsiIcon("│", "dim")}${lastLoggedLine}`);
return;
}
this.getRenderer().log(`${this.#getAnsiIcon("┌", "dim")}${task.title}`);
this.#notifiedTasks.add(task.title);
}
/**
* Renders message for a succeeded task
*/
#renderSucceededTask(task) {
const successMessage = task.getSuccessMessage();
const icon = this.#getAnsiIcon("└", "dim");
const status = this.getColors().green(successMessage || "Completed");
const duration = this.getColors().dim(`(${task.getDuration()})`);
this.getRenderer().log(`${icon}${status} ${duration}`);
}
/**
* Renders message for a failed task
*/
#renderFailedTask(task) {
const error = task.getError();
if (error) this.getRenderer().logError(this.#formatError(error));
const icon = this.#getAnsiIcon("└", "dim");
const status = this.getColors().red("Failed");
const duration = this.getColors().dim(`(${task.getDuration()})`);
this.getRenderer().logError(`${icon}${status} ${duration}`);
}
/**
* Renders a given task
*/
#renderTask(task) {
switch (task.getState()) {
case "running": return this.#renderRunningTask(task);
case "succeeded": return this.#renderSucceededTask(task);
case "failed": return this.#renderFailedTask(task);
}
}
/**
* Renders all tasks
*/
#renderTasks() {
this.#registeredTasks.forEach((task) => this.#renderTask(task));
}
/**
* Returns the renderer for rendering the messages
*/
getRenderer() {
if (!this.#renderer) this.#renderer = new ConsoleRenderer();
return this.#renderer;
}
/**
* Define a custom renderer. Logs to "stdout" and "stderr"
* by default
*/
useRenderer(renderer) {
this.#renderer = renderer;
return this;
}
/**
* Returns the colors implementation in use
*/
getColors() {
if (!this.#colors) this.#colors = useColors();
return this.#colors;
}
/**
* Define a custom colors implementation
*/
useColors(color) {
this.#colors = color;
return this;
}
/**
* Register tasks to render
*/
tasks(tasks) {
this.#registeredTasks = tasks;
return this;
}
/**
* Render all tasks
*/
render() {
this.#registeredTasks.forEach((task) => task.onUpdate(($task) => this.#renderTask($task)));
this.#renderTasks();
}
};
//#endregion
//#region src/tasks/renderers/minimal.ts
/**
* As the name suggests, render tasks in minimal UI for better viewing
* experience.
*/
var MinimalRenderer = class {
/**
* Renderer options
*/
#options;
/**
* Reference to the colors implementation
*/
#colors;
/**
* The renderer to use to output logs
*/
#renderer;
/**
* List of registered tasks
*/
#registeredTasks = [];
constructor(options) {
this.#options = { icons: options.icons === void 0 ? true : options.icons };
}
/**
* Format error
*/
#formatError(error) {
let message = typeof error === "string" ? error : error.message;
message = this.getColors().red(message);
return `\n ${message.split("\n").map((line) => `${line}`).join("\n")}`;
}
/**
* Returns the pointer icon, if icons are not disabled.
*/
#getPointerIcon(color) {
const icon = this.#options.icons ? `${icons.pointer} ` : "";
if (!icon) return icon;
return this.getColors()[color](icon);
}
/**
* Returns the display string for an idle task
*/
#renderIdleTask(task) {
return `${this.#getPointerIcon("dim")}${this.getColors().dim(task.title)}`;
}
/**
* Returns the display string for a running task
*/
#renderRunningTask(task) {
const lastLogLine = task.getLastLoggedLine();
return `${this.#options.icons ? `${icons.pointer} ${task.title}` : task.title}\n ${lastLogLine || ""}`;
}
/**
* Returns the display string for a failed task
*/
#renderFailedTask(task) {
const pointer = this.#getPointerIcon("red");
const duration = this.getColors().dim(`(${task.getDuration()})`);
let message = `${pointer}${task.title} ${duration}`;
const error = task.getError();
if (!error) return `${message}\n`;
message = `${message}${this.#formatError(error)}`;
return message;
}
/**
* Returns the display string for a succeeded task
*/
#renderSucceededTask(task) {
const pointer = this.#getPointerIcon("green");
const duration = this.getColors().dim(`(${task.getDuration()})`);
let message = `${pointer}${task.title} ${duration}`;
const successMessage = task.getSuccessMessage();
if (!successMessage) return `${message}\n`;
message = `${message}\n ${this.getColors().green(successMessage)}`;
return message;
}
/**
* Renders a given task
*/
#renderTask(task) {
switch (task.getState()) {
case "idle": return this.#renderIdleTask(task);
case "running": return this.#renderRunningTask(task);
case "succeeded": return this.#renderSucceededTask(task);
case "failed": return this.#renderFailedTask(task);
}
}
/**
* Renders all tasks
*/
#renderTasks() {
const lastTaskState = this.#registeredTasks[this.#registeredTasks.length - 1].getState();
this.getRenderer().logUpdate(this.#registeredTasks.map((task) => this.#renderTask(task)).join("\n"));
if (lastTaskState === "succeeded" || lastTaskState === "failed") this.getRenderer().logUpdatePersist();
}
/**
* Returns the renderer for rendering the messages
*/
getRenderer() {
if (!this.#renderer) this.#renderer = new ConsoleRenderer();
return this.#renderer;
}
/**
* Define a custom renderer. Logs to "stdout" and "stderr"
* by default
*/
useRenderer(renderer) {
this.#renderer = renderer;
return this;
}
/**
* Returns the colors implementation in use
*/
getColors() {
if (!this.#colors) this.#colors = useColors();
return this.#colors;
}
/**
* Define a custom colors implementation
*/
useColors(color) {
this.#colors = color;
return this;
}
/**
* Register tasks to render
*/
tasks(tasks) {
this.#registeredTasks = tasks;
return this;
}
/**
* Render all tasks
*/
render() {
this.#registeredTasks.forEach((task) => {
task.onUpdate(() => this.#renderTasks());
});
this.#renderTasks();
}
};
//#endregion
//#region src/tasks/renderers/raw.ts
/**
* Raw renderer shows a detailed output of the tasks without using any
* ansi characters
*/
var RawRenderer = class {
/**
* Reference to the colors implementation
*/
#colors;
/**
* The renderer to use to output logs
*/
#renderer;
/**
* List of registered tasks
*/
#registeredTasks = [];
#notifiedTasks = /* @__PURE__ */ new Set();
constructor() {}
/**
* Format error
*/
#formatError(error) {
if (typeof error === "string") return `${this.getColors().red(error)}`;
if (!error.stack) return `${this.getColors().red(error.message)}`;
return `${error.stack.split("\n").map((line) => ` ${this.getColors().red(line)}`).join("\n")}`;
}
/**
* Renders message for a running task
*/
#renderRunningTask(task) {
if (this.#notifiedTasks.has(task.title)) {
const lastLoggedLine = task.getLastLoggedLine();
if (lastLoggedLine) this.getRenderer().log(lastLoggedLine);
return;
}
this.getRenderer().log(`${task.title}\n${new Array(task.title.length + 1).join("-")}`);
this.#notifiedTasks.add(task.title);
}
/**
* Renders message for a succeeded task
*/
#renderSucceededTask(task) {
const successMessage = task.getSuccessMessage();
const status = this.getColors().green(successMessage || "completed");
const duration = this.getColors().dim(`(${task.getDuration()})`);
this.getRenderer().log(`${status} ${duration}\n`);
}
/**
* Renders message for a failed task
*/
#renderFailedTask(task) {
const error = task.getError();
if (error) this.getRenderer().logError(this.#formatError(error));
const status = this.getColors().red("failed");
const duration = this.getColors().dim(`(${task.getDuration()})`);
this.getRenderer().logError(`${status} ${duration}\n`);
}
/**
* Renders a given task
*/
#renderTask(task) {
switch (task.getState()) {
case "running": return this.#renderRunningTask(task);
case "succeeded": return this.#renderSucceededTask(task);
case "failed": return this.#renderFailedTask(task);
}
}
/**
* Renders all tasks
*/
#renderTasks() {
this.#registeredTasks.forEach((task) => this.#renderTask(task));
}
/**
* Returns the renderer for rendering the messages
*/
getRenderer() {
if (!this.#renderer) this.#renderer = new ConsoleRenderer();
return this.#renderer;
}
/**
* Define a custom renderer. Logs to "stdout" and "stderr"
* by default
*/
useRenderer(renderer) {
this.#renderer = renderer;
return this;
}
/**
* Returns the colors implementation in use
*/
getColors() {
if (!this.#colors) this.#colors = useColors();
return this.#colors;
}
/**
* Define a custom colors implementation
*/
useColors(color) {
this.#colors = color;
return this;
}
/**
* Register tasks to render
*/
tasks(tasks) {
this.#registeredTasks = tasks;
return this;
}
/**
* Render all tasks
*/
render() {
this.#registeredTasks.forEach((task) => task.onUpdate(($task) => this.#renderTask($task)));
this.#renderTasks();
}
};
//#endregion
//#region src/tasks/manager.ts
function TRANSFORM_ERROR(error) {
if (typeof error === "string") return {
isError: true,
message: error
};
return error;
}
/**
* Exposes the API to create a group of tasks and run them in sequence
*/
var TaskManager = class {
/**
* Last handled error
*/
error;
/**
* Options
*/
#options;
/**
* The renderer to use for rendering tasks. The verbose renderer is
* used When "raw" is set to true.
*/
#tasksRenderer;
/**
* A set of created tasks
*/
#tasks = [];
/**
* State of the tasks manager
*/
#state = "idle";
constructor(options = {}) {
this.#options = {
icons: options.icons === void 0 ? true : options.icons,
raw: options.raw === void 0 ? false : options.raw,
verbose: options.verbose === void 0 ? false : options.verbose
};
/**
* Using verbose renderer when in raw mode
*/
if (this.#options.raw) this.#tasksRenderer = new RawRenderer();
else if (this.#options.verbose) this.#tasksRenderer = new VerboseRenderer();
else
/**
* Otheriwse using the minimal renderer
*/
this.#tasksRenderer = new MinimalRenderer({ icons: this.#options.icons });
}
/**
* Run a given task. The underlying code assumes that tasks are
* executed in sequence.
*/
async #runTask(index) {
const task = this.#tasks[index];
if (!task) return;
/**
* Start the underlying task
*/
task.task.start();
/**
* Update task progress
*/
const update = (logMessage) => {
task.task.update(logMessage);
};
/**
* Invoke callback
*/
try {
const response = await task.callback({
error: TRANSFORM_ERROR,
update
});
if (typeof response === "string") {
task.task.markAsSucceeded(response);
await this.#runTask(index + 1);
} else {
this.#state = "failed";
task.task.markAsFailed(response);
}
} catch (error) {
this.#state = "failed";
this.error = error;
task.task.markAsFailed(error);
}
}
/**
* Access the task state
*/
getState() {
return this.#state;
}
/**
* Register a new task
*/
add(title, callback) {
this.#tasks.push({
task: new Task(title),
callback
});
return this;
}
/**
* Register a new task, when the "conditional = true"
*/
addIf(conditional, title, callback) {
if (conditional) this.add(title, callback);
return this;
}
/**
* Register a new task, when the "conditional = false"
*/
addUnless(conditional, title, callback) {
if (!conditional) this.add(title, callback);
return this;
}
/**
* Get access to registered tasks
*/
tasks() {
return this.#tasks.map(({ task }) => task);
}
/**
* Returns the renderer for rendering the messages
*/
getRenderer() {
return this.#tasksRenderer.getRenderer();
}
/**
* Define a custom renderer. Logs to "stdout" and "stderr"
* by default
*/
useRenderer(renderer) {
this.#tasksRenderer.useRenderer(renderer);
return this;
}
/**
* Define a custom colors implementation
*/
useColors(color) {
this.#tasksRenderer.useColors(color);
return this;
}
/**
* Run tasks
*/
async run() {
if (this.#state !== "idle") return;
this.#state = "running";
this.#tasksRenderer.tasks(this.tasks()).render();
await this.#runTask(0);
if (this.#state === "running") this.#state = "succeeded";
}
};
//#endregion
//#region index.ts
/**
* Create a new CLI UI instance.
*
* - The "raw" mode is tailored for testing
* - The "silent" mode should be used when the terminal does not support colors. We
* automatically perform the detection
*/
function cliui(options = {}) {
let mode = options.mode;
/**
* Use silent mode when not explicit mode is defined
*/
if (!mode && !supportsColor.stdout) mode = "silent";
/**
* Renderer to use
*/
let renderer = mode === "raw" ? new MemoryRenderer() : new ConsoleRenderer();
/**
* Colors instance in use
*/
let colors = useColors({
silent: mode === "silent",
raw: mode === "raw"
});
/**
* Logger
*/
const logger = new Logger();
logger.useRenderer(renderer);
logger.useColors(colors);
/**
* Render instructions inside a box
*/
const instructions = () => {
const instructionsInstance = new Instructions({
icons: true,
raw: mode === "raw"
});
instructionsInstance.useRenderer(renderer);
instructionsInstance.useColors(colors);
return instructionsInstance;
};
/**
* Similar to instructions. But without the `pointer` icon
*/
const sticker = () => {
const instructionsInstance = new Instructions({
icons: false,
raw: mode === "raw"
});
instructionsInstance.useRenderer(renderer);
instructionsInstance.useColors(colors);
return instructionsInstance;
};
/**
* Initiates a group of tasks
*/
const tasks = (tasksOptions) => {
const manager = new TaskManager({
raw: mode === "raw",
...tasksOptions
});
manager.useRenderer(renderer);
manager.useColors(colors);
return manager;
};
/**
* Instantiate a new table
*/
const table = (tableOptions) => {
const tableInstance = new Table({
raw: mode === "raw",
...tableOptions
});
tableInstance.useRenderer(renderer);
tableInstance.useColors(colors);
return tableInstance;
};
/**
* Instantiate a new steps display
*/
const steps = () => {
const stepsInstance = new Steps({ raw: mode === "raw" });
stepsInstance.useRenderer(renderer);
stepsInstance.useColors(colors);
return stepsInstance;
};
return {
colors,
logger,
table,
tasks,
steps,
icons,
sticker,
instructions,
switchMode(modeToUse) {
mode = modeToUse;
/**
* Use memory renderer in raw mode, otherwise switch to
* console renderer
*/
if (mode === "raw") this.useRenderer(new MemoryRenderer());
else this.useRenderer(new ConsoleRenderer());
this.useColors(useColors({
silent: mode === "silent",
raw: mode === "raw"
}));
},
useRenderer(rendererToUse) {
renderer = rendererToUse;
logger.useRenderer(renderer);
},
useColors(colorsToUse) {
colors = colorsToUse;
logger.useColors(colors);
this.colors = colors;
}
};
}
//#endregion
export { ConsoleRenderer, Instructions, Logger, MemoryRenderer, Steps, Table, TaskManager, cliui, poppinssColors as colors, icons };