@bearz/ansi
Version:
The ansi module provides color detection, writing ansi, codes, and an ansi writer.
467 lines (466 loc) • 14.2 kB
JavaScript
/**
* The `writer` module provides a simple and flexible way to write messages to the console with ANSI styles.
* It allows you to write messages with different log levels, such as `info`, `debug`, `warn`, and `error`,
* styled messages, log commands, and more.
*
* @module
*/
import { AnsiSettings } from "./settings.js";
import {
blue,
brightBlack,
cyan,
gray,
green,
magenta,
red,
reset,
rgb24,
yellow,
} from "./styles.js";
import { get, has } from "@bearz/env";
import { sprintf } from "@bearz/fmt/printf";
import { AnsiLogLevels, AnsiModes } from "./enums.js";
import { globals, WINDOWS } from "./globals.js";
import { stdout } from "@bearz/process/streams";
const groupSymbol =
"\x1b[38;2;60;0;255m❯\x1b[39m\x1b[38;2;90;0;255m❯\x1b[39m\x1b[38;2;121;0;255m❯\x1b[39m\x1b[38;2;151;0;255m❯\x1b[39m\x1b[38;2;182;0;255m❯\x1b[39m";
const EOL = WINDOWS ? "\r\n" : "\n";
let args = [];
if (typeof globals.Deno !== "undefined") {
args = globals.Deno.args;
} else if (typeof globals.process !== "undefined") {
args = globals.process.argv.slice(2);
}
function write(message) {
stdout.writeSync(new TextEncoder().encode(message));
}
function handleStack(stack) {
stack = stack ?? "";
const index = stack.indexOf("\n");
if (index === -1) {
return stack;
}
return stack.substring(index + 1);
}
export function handleArguments(args) {
let msg = undefined;
let stack = undefined;
switch (args.length) {
case 0:
return { msg, stack };
case 1: {
if (args[0] instanceof Error) {
const e = args[0];
msg = e.message;
stack = handleStack(e.stack);
} else {
msg = args[0];
}
return { msg, stack };
}
case 2: {
if (args[0] instanceof Error) {
const e = args[0];
const message = args[1];
msg = message;
stack = handleStack(e.stack);
} else {
const message = args[0];
const splat = Array.from(args).slice(1);
msg = sprintf(message, ...splat);
}
return { msg, stack };
}
default: {
if (args[0] instanceof Error) {
const e = args[0];
const message = args[1];
const splat = Array.from(args).slice(2);
msg = sprintf(message, ...splat);
stack = handleStack(e.stack);
} else {
const message = args[0];
const splat = Array.from(args).slice(1);
msg = sprintf(message, ...splat);
}
return { msg, stack };
}
}
}
/**
* The default implementation of the ANSI writer.
*/
export class DefaultAnsiWriter {
#interactive;
#level;
#write = write;
/**
* Creates a new instance of DefaultAnsiWriter.
* @param level The log level.
* @param secretMasker The secret masker.
*/
constructor(level, write) {
this.#level = level ?? AnsiLogLevels.Debug;
if (write) {
this.#write = write;
}
}
/**
* Gets or sets the log level.
*/
get level() {
return this.#level;
}
set level(value) {
this.#level = value;
}
/**
* Determines if the log level is enabled.
* @param level The log level.
* @returns `true` if the log level is enabled, `false` otherwise.
*/
enabled(level) {
return this.#level >= level;
}
/**
* Determines if the current environment is interactive.
*/
get interactive() {
if (this.#interactive !== undefined) {
return this.#interactive;
}
if (get("CI") === "true") {
this.#interactive = false;
return false;
}
const isCi = [
"CI",
"GITHUB_ACTIONS",
"GITLAB_CI",
"CIRCLECI",
"BITBUCKET_BUILD_NUMBER",
"TF_BUILD",
"JENKINS_URL",
].some((o) => has(o));
if (isCi) {
this.#interactive = false;
return false;
}
if (get("DEBIAN_FRONTEND") === "noninteractive") {
this.#interactive = false;
}
if (args.includes("-NonInteractive") || args.includes("--non-interactive")) {
this.#interactive = false;
}
this.#interactive = true;
return this.#interactive;
}
/**
* Gets the ANSI settings.
*/
get settings() {
return AnsiSettings.current;
}
progress(name, value) {
this.write(`${name}: ${green(value.toString().padStart(2))}% \r`);
return this;
}
/**
* Writes a command to the output.
* @param command The executable.
* @param args The arguments passed to the command.
* @returns The writer.
*/
command(command, args) {
if (this.level === AnsiLogLevels.None) {
return this;
}
if (this.settings.mode >= AnsiModes.EightBit) {
this.write(cyan("❱ $ "));
this.write(rgb24(`${command}`, 0xff8700));
if (args && args.length > 0) {
for (const value of args) {
if (value.startsWith("-") || value.startsWith("/")) {
this.write(" ").write(cyan(`${value}`));
continue;
}
if (value.includes(" ") || value.includes("\n") || value.includes("\t")) {
if (!value.includes("'")) {
this.write(" ").write(magenta(`'${value}'`));
} else {
this.write(" ").write(magenta(`"${value}"`));
}
continue;
}
this.write(` ${value}`);
}
}
this.writeLine();
return this;
}
this.write(`❱ $ ${command}`);
if (args && args.length > 0) {
for (const value of args) {
if (value.startsWith("-") || value.startsWith("/")) {
this.write(` ${value}`);
continue;
}
if (value.includes(" ") || value.includes("\n") || value.includes("\t")) {
if (!value.includes("'")) {
this.write(` '${value}'`);
} else {
this.write(` "${value}"`);
}
continue;
}
this.write(` ${value}`);
}
}
this.writeLine();
return this;
}
trace() {
if (this.#level < AnsiLogLevels.Trace) {
return this;
}
const { msg, stack } = handleArguments(arguments);
if (this.settings.mode !== AnsiModes.None) {
if (this.settings.mode >= AnsiModes.EightBit) {
this.write(rgb24("❱ [TRACE]: ", 0x626262));
} else {
this.write(brightBlack("❱ [TRACE]: "));
}
this.writeLine(msg);
if (stack) {
this.writeLine(stack);
}
return this;
}
this.writeLine(`❱ [TRACE]: ${msg}`);
if (stack) {
this.writeLine(stack);
}
return this;
}
debug() {
if (this.#level < AnsiLogLevels.Debug) {
return this;
}
const { msg, stack } = handleArguments(arguments);
if (this.settings.mode !== AnsiModes.None) {
if (this.settings.mode >= AnsiModes.EightBit) {
this.write(rgb24("❱ [DEBUG]: ", 0x808080));
} else {
this.write(gray("❱ [DEBUG]: "));
}
this.writeLine(msg);
if (stack) {
this.writeLine(stack);
}
return this;
}
this.writeLine(`❱ [DEBUG]: ${msg}`);
if (stack) {
this.writeLine(stack);
}
return this;
}
warn() {
if (this.#level < AnsiLogLevels.Warning) {
return this;
}
const { msg, stack } = handleArguments(arguments);
if (this.settings.mode !== AnsiModes.None) {
if (this.settings.mode >= AnsiModes.EightBit) {
this.write(rgb24("❱ [WARN]: ", 0xff8700));
} else {
this.write(yellow("❱ [WARN]: "));
}
this.writeLine(msg);
if (stack) {
this.writeLine(stack);
}
return this;
}
this.writeLine(`❱ [WARN]: ${msg}`);
if (stack) {
this.writeLine(stack);
}
return this;
}
error() {
if (this.#level < AnsiLogLevels.Error) {
return this;
}
const { msg, stack } = handleArguments(arguments);
if (this.settings.mode !== AnsiModes.None) {
if (this.settings.mode >= AnsiModes.EightBit) {
this.write(rgb24("❱ [ERROR]: ", 0xff0000));
} else {
this.write(red("❱ [ERROR]: "));
}
this.writeLine(msg);
if (stack) {
this.writeLine(stack);
}
return this;
}
this.writeLine(`❱ [ERROR]: ${msg}`);
if (stack) {
this.writeLine(stack);
}
return this;
}
/**
* Writes a success message to the output.
* @param message The message to write.
* @param args The message arguments.
* @returns The writer.
*/
success(message, ...args) {
switch (arguments.length) {
case 0:
return this;
case 1:
{
if (this.settings.mode !== AnsiModes.None) {
this.writeLine(green(`${message}`));
} else {
this.writeLine(`${message}`);
}
}
return this;
default: {
if (this.settings.mode !== AnsiModes.None) {
this.writeLine(green(`${sprintf(message, ...args)}`));
} else {
this.writeLine(`${sprintf(message, ...args)}`);
}
return this;
}
}
}
info() {
if (this.#level < AnsiLogLevels.Information) {
return this;
}
const { msg, stack } = handleArguments(arguments);
if (this.settings.mode !== AnsiModes.None) {
if (this.settings.mode >= AnsiModes.EightBit) {
this.write(rgb24("❱ [INFO]: ", 0x00afff));
} else {
this.write(cyan("❱ [INFO]: "));
}
this.writeLine(msg);
if (stack) {
this.writeLine(stack);
}
return this;
}
this.writeLine(`❱ [INFO]: ${msg}`);
if (stack) {
this.writeLine(stack);
}
return this;
}
/**
* Writes a styled message to the output.
*
* @param message The message to style.
* @param styles The styles to apply.
* @example
* ```typescript
* import { writer, bold, green, bgBlue } from "@bearz/ansi";
*
* writer.style("Hello, World!", bold, green, bgBlue);
* ```
*/
style(message, ...styles) {
this.write(styles.reduce((acc, style) => style(acc), message));
}
/**
* Writes a styled message as a new line to the output.
* @param message The message to style.
* @param styles The styles to apply.
*/
styleLine(message, ...styles) {
this.writeLine(styles.reduce((acc, style) => style(acc), message));
}
/**
* Writes a message to the output.
* @param message The message to write.
* @param args The message arguments.
* @returns the writer.
*/
write(message, ...args) {
if (message === undefined) {
return this;
}
switch (arguments.length) {
case 0:
return this;
case 1:
this.#write(message);
break;
default:
{
const formatted = sprintf(message, ...args);
this.#write(formatted);
}
break;
}
return this;
}
/**
* Writes a message as a new line to the output.
* @param message The message to write.
* @param args The message arguments.
* @returns the writer.
*/
writeLine(message, ...args) {
switch (arguments.length) {
case 0:
this.#write(EOL);
break;
case 1:
this.#write(`${message}${EOL}`);
break;
default:
{
const formatted = sprintf(`${message}${EOL}`, ...args);
this.#write(formatted);
}
break;
}
return this;
}
/**
* Starts a new group that groups a set of messages.
* @param name The group name.
* @returns the writer.
*/
startGroup(name) {
if (this.settings.mode !== AnsiModes.None) {
if (this.settings.mode === AnsiModes.TwentyFourBit) {
this.write(groupSymbol).write(reset(" "));
this.writeLine(magenta(`${name}`));
return this;
}
this.write(blue(`❯❯❯❯❯`)).write(" ");
this.writeLine(magenta(`${name}`));
return this;
}
this.writeLine(`❯❯❯❯❯ ${name}`);
return this;
}
/**
* Ends the current group.
* @returns the writer.
*/
endGroup() {
this.writeLine();
return this;
}
}
export const writer = new DefaultAnsiWriter();