easy-cli-framework
Version:
A framework for building CLI applications that are robust and easy to maintain. Supports theming, configuration files, interactive prompts, and more.
345 lines (323 loc) • 11 kB
text/typescript
/** @packageDocumentation Support for theming for command line applications, it includes support for verbosity, themed text display, spinners, and progress bars. */
import chalk, { Chalk } from 'chalk';
import { EasyCLILogger } from './logger';
import { ThemedTable, ThemedTableColumn } from './themed-table';
import { ThemedSpinner } from './themed-spinner';
import {
ThemedSimpleProgressBar,
ThemedSimpleProgressBarOptions,
} from './progress/simple-progress';
import {
ThemedStatusProgressBar,
ThemedStatusProgressBarOptions,
} from './progress';
/**
* Options for displaying a string
* @interface StringDisplayOptions
* @property {boolean} [bold] Whether to bold the string
* @property {boolean} [italic] Whether to italicize the string
* @property {boolean} [underline] Whether to underline the string
* @property {boolean} [strikethrough] Whether to strikethrough the string
* @property {string} [color] The color of the string (hex), support for other formats is planned
* @property {string} [bgColor] The background color of the string (hex), support for other formats is planned
*
* @example
* ```typescript
* // Bold Red Text
* const options: StringDisplayOptions = {
* bold: true,
* color: '#FF5555',
* };
*
* // Italicized Blue Text
* const options: StringDisplayOptions = {
* italic: true,
* color: '#5555FF',
* };
* ```
*/
export type StringDisplayOptions = {
bold?: boolean;
italic?: boolean;
underline?: boolean;
strikethrough?: boolean;
// TODO: Support different color formats
color?: string;
bgColor?: string;
};
/**
* Options for displaying a string. Can be a string, a StringDisplayOptions object, or an array of strings and StringDisplayOptions objects.
* @typedef DisplayOptions
*
* @type {string | StringDisplayOptions | (string | StringDisplayOptions)[]}
*/
export type DisplayOptions =
| NamedDisplayOptions
| StringDisplayOptions
| (NamedDisplayOptions | StringDisplayOptions)[];
/**
* Named display options
* @enum
*/
export type NamedDisplayOptions =
| 'log'
| 'error'
| 'warn'
| 'info'
| 'success'
| 'default'
| string;
/**
* A theme for the CLI, that allows for customizing the look and feel of the CLI, generating logs, tables, spinners, and progress bars.
*
* @class EasyCLITheme
*
* @property {number} verbosity The verbosity level of the theme
* @property {Record<NamedDisplayOptions, StringDisplayOptions>} namedDisplayOptions The named display options for the theme
* @property {EasyCLITheme} setVerbosity Sets the verbosity level of the theme
* @property {EasyCLITheme} setNamedDisplayOption Sets the named display options for the theme
* @property {EasyCLILogger} getLogger Gets a logger with the theme
* @property {ThemedTable} getTable Gets a table with the theme
* @property {ThemedSpinner} getSpinner Gets a spinner with the theme
* @property {ThemedSimpleProgressBar} getSimpleProgressBar Gets a simple progress bar with the theme
* @property {ThemedStatusProgressBar} getStatusProgressBar Gets a status progress bar with the theme
*
* @example
* ```typescript
* const theme = new EasyCLITheme(
* 0, // Set the verbosity level to 0
* {
* log: { color: '#F5F5F5' }, // Update the log color
* error: { color: '#FF5555', bold: true }, // Update the error color and make it bold
* custom: { color: '#55FF55' }, // Add a custom named display option
* }
* );
* const logger = theme.getLogger();
* logger.log('Hello, world!');
* ```
*/
export class EasyCLITheme {
private verbosity: number = 0;
private namedDisplayOptions: Record<
NamedDisplayOptions,
StringDisplayOptions
> = {
log: {},
error: { color: '#FF5555', bold: true },
warn: { color: '#FFFF55' },
info: { color: '#F5F5F5' },
success: { color: '#55FF55' },
default: {},
};
/**
* Creates an instance of EasyCLITheme.
*
* @param {number} [verbosity=0] The verbosity level of the theme
* @param {Record<NamedDisplayOptions, StringDisplayOptions>} [namedDisplayOptions] The named display options for the theme
*/
constructor(
verbosity: number = 0,
namedDisplayOptions?: Record<NamedDisplayOptions, StringDisplayOptions>
) {
this.verbosity = verbosity;
if (namedDisplayOptions) {
this.namedDisplayOptions = {
...this.namedDisplayOptions,
...namedDisplayOptions,
};
}
}
/**
* An internal method to merge display options, allowing for multiple display options to be combined
* ie. ['info', { bold: true }] => { color: '#F5F5F5', bold: true }
*
* @param {DisplayOptions} options The display options to merge
*
* @returns {StringDisplayOptions} A single display options object
*/
private mergeDisplayOptions(options: DisplayOptions): StringDisplayOptions {
// If it's a string, we can just return the named display options
if (typeof options === 'string') {
return {
...this.namedDisplayOptions.default,
...(this.namedDisplayOptions?.[options] ?? {}),
};
}
// If it's an object, we can just return the object
if (!Array.isArray(options)) {
return {
...this.namedDisplayOptions.default,
...options,
};
}
// Otherwise combine them in reverse order
return [this.namedDisplayOptions.default ?? {}, ...options.reverse()]
.map(option =>
typeof option === 'string'
? this.namedDisplayOptions?.[option] ?? {}
: option
)
.reduce(
(acc, option) => ({ ...acc, ...option }),
{} as StringDisplayOptions
);
}
/**
* Formats a string with the display options
*
* @param {string} string The string to format
* @param {DisplayOptions} options The display options to use
*
* @returns {string} The formatted string
*
* @example
* ```typescript
* const theme = new EasyCLITheme();
* const formatted = theme.formattedString('Hello, world!', ['info', { bold: true }]);
* console.log(formatted);
* ```
*/
formattedString(string: string, options: DisplayOptions): string {
let formatter: Chalk = chalk;
let formatOptions = this.mergeDisplayOptions(options);
if (formatOptions.bold) formatter = formatter.bold;
if (formatOptions.italic) formatter = formatter.italic;
if (formatOptions.underline) formatter = formatter.underline;
if (formatOptions.strikethrough) formatter = formatter.strikethrough;
if (formatOptions.color) formatter = formatter.hex(formatOptions.color);
if (formatOptions.bgColor)
formatter = formatter.bgHex(formatOptions.bgColor);
return formatter(string);
}
/**
* Sets the verbosity level of the theme
*
* @param {number} verbosity The verbosity level to set
*
* @returns {EasyCLITheme} The theme with the verbosity level, useful for optional chaining
*/
setVerbosity(verbosity: number): EasyCLITheme {
this.verbosity = verbosity;
return this;
}
/**
* Sets a named display options for the theme
*
* @param {NamedDisplayOptions} name The name of the display options
* @param {StringDisplayOptions} options The display options to set
*
* @returns {EasyCLITheme} The theme with the named display options set for use with optional chaining.
*/
setNamedDisplayOption(
name: NamedDisplayOptions,
options: StringDisplayOptions
): EasyCLITheme {
this.namedDisplayOptions[name] = options;
return this;
}
/**
* Gets a logger instance with the theme.
*
* @returns {EasyCLILogger} The logger with the theme
*/
getLogger() {
return new EasyCLILogger({ theme: this, verbosity: this.verbosity }); // TODO: Considering generating a singleton logger
}
/**
* Gets a themed table using this theme
* @template TItem The datatype for the items in the table.
*
* @param {ThemedTableColumn<TItem>[]} [columns=[]] The columns for the table
* @param {number} [totalWidth=120] The total width of the table
*
* @returns {ThemedTable<TItem>} The themed table instance
*
* @example
* ```typescript
* const theme = new EasyCLITheme();
* const table = theme.getTable([
* { name: 'Name', data: item => item.name },
* { name: 'Age', data: item => item.age },
* ]);
*
* table.render([
* { name: 'Alice', age: 25 },
* { name: 'Bob', age: 30 },
* ]);
* ```
*/
getTable<TItem extends Record<string, any> = any[]>(
columns: ThemedTableColumn<TItem>[] = [],
totalWidth: number = 120
): ThemedTable<TItem> {
return new ThemedTable<TItem>({ theme: this, columns, totalWidth }); // TODO: Add verbosity and other options
}
/**
* Gets a spinner using this theme
*
* @param {DisplayOptions} [format='default'] The format for the spinner
*
* @returns {ThemedSpinner} The themed spinner instance
*
* @example
* ```typescript
* const theme = new EasyCLITheme();
* const spinner = theme.getSpinner('default');
* spinner.start('Loading...');
* ```
*/
getSpinner(format: DisplayOptions = 'default') {
return new ThemedSpinner(this, format); // TODO: Add other options
}
/**
* Gets a simple progress bar using this theme
*
* @param {string} name The name of the progress bar
* @param {DisplayOptions} [format='default'] The format for the progress bar
* @param {ThemedSimpleProgressBarOptions} [options={}] The options for the progress bar
*
* @returns {ThemedSimpleProgressBar} The themed simple progress bar instance
* @example
* ```typescript
* const theme = new EasyCLITheme();
* const progressBar = theme.getSimpleProgressBar('progress', 'default', {
* showCurrentRecord: true,
* currentRecordDisplayOptions: 'info',
* });
* ```
*/
getSimpleProgressBar(
name: string,
format: DisplayOptions = 'default',
options: ThemedSimpleProgressBarOptions = {}
) {
return new ThemedSimpleProgressBar(this, name, format, options); // TODO: Add Other Options
}
/**
* Gets a status progress bar using this theme
*
* @param {string} name The name of the progress bar
* @param {DisplayOptions} [format='default'] The format for the progress bar
* @param {ThemedStatusProgressBarOptions} [options={}] The options for the progress bar
*
* @returns {ThemedStatusProgressBar} The themed status progress bar instance
* @example
* ```typescript
* const theme = new EasyCLITheme();
* const progressBar = theme.getStatusProgressBar('Task', 'Task in progress', {
* showCurrentRecord: true,
* });
* ```
*/
getStatusProgressBar(
name: string,
format: DisplayOptions = 'default',
options: ThemedStatusProgressBarOptions = {}
) {
return new ThemedStatusProgressBar(this, name, format, options); // TODO: Add Other Options
}
}
export * from './logger';
export * from './progress';
export * from './themed-table';
export * from './themed-spinner';