@topcli/spinner
Version:
Asynchronous CLI Spinner. Allow to create and manage simultaneous/multiple spinners at a time.
166 lines • 5.54 kB
JavaScript
// Import Node.js Dependencies
import { EventEmitter } from "node:events";
import { performance } from "node:perf_hooks";
import { inspect, styleText } from "node:util";
import readline from "node:readline";
import * as TTY from "node:tty";
// Import Third-party Dependencies
import * as cliSpinners from "cli-spinners";
// Import Internal Dependencies
import { stringLength } from "./utils/index.js";
// VARS
let internalSpinnerCount = 0;
// CONSTANTS
const kDefaultSpinnerName = "dots";
const kAvailableColors = new Set(Object.keys(inspect.colors));
const kLogSymbols = process.platform !== "win32" || process.env.CI || process.env.TERM === "xterm-256color" ?
{ success: styleText("green", "✔"), error: styleText("red", "✖") } :
{ success: styleText("green", "√"), error: styleText("red", "×") };
export class Spinner extends EventEmitter {
static reset() {
internalSpinnerCount = 0;
}
stream = process.stdout;
#verbose = true;
#started = false;
#spinner;
#text = "";
#prefix = "";
#color;
#interval = null;
#frameIndex = 0;
#spinnerPos = 0;
#startTime;
constructor(options = {}) {
super();
this.#verbose = options.verbose ?? true;
if (!this.#verbose) {
return;
}
const { name = kDefaultSpinnerName, color = null } = options;
this.#spinner = name in cliSpinners ?
cliSpinners.default[name] :
cliSpinners.default[kDefaultSpinnerName];
if (color === null) {
this.#color = (str) => str;
}
else {
const colorArr = Array.isArray(color) ? color : [color];
this.#color = colorArr.every((color) => kAvailableColors.has(color)) ?
(str) => styleText(color, str) :
(str) => styleText("white", str);
}
}
get started() {
return this.#started;
}
get verbose() {
return this.#verbose;
}
get elapsedTime() {
return performance.now() - this.#startTime;
}
get startTime() {
return this.#startTime;
}
set text(value) {
if (typeof value == "string") {
this.#text = value.replaceAll(/\r?\n|\r/gm, "");
}
}
get text() {
return this.#text;
}
#getSpinnerFrame(spinnerSymbol) {
if (typeof spinnerSymbol === "string") {
return spinnerSymbol;
}
const { frames } = this.#spinner;
const frame = frames[this.#frameIndex];
this.#frameIndex = ++this.#frameIndex < frames.length ? this.#frameIndex : 0;
return this.#color(frame);
}
#lineToRender(spinnerSymbol) {
const terminalCol = this.stream.columns;
const defaultRaw = `${this.#getSpinnerFrame(spinnerSymbol)} ${this.#prefix}${this.text}`;
let regexArray;
let count = 0;
while (1) {
regexArray = defaultRaw
.slice(0, terminalCol + count)
.match(ansiRegex()) ?? [];
if (regexArray.length === count) {
break;
}
count = regexArray.length;
}
count += regexArray.reduce((prev, curr) => prev + stringLength(curr), 0);
return stringLength(defaultRaw) > terminalCol ?
`${defaultRaw.slice(0, terminalCol + count)}\x1B[0m` :
defaultRaw;
}
#renderLine(spinnerSymbol) {
if (!this.#verbose) {
return;
}
const moveCursorPos = internalSpinnerCount - this.#spinnerPos;
readline.moveCursor(this.stream, 0, -moveCursorPos);
const line = this.#lineToRender(spinnerSymbol);
readline.clearLine(this.stream, 0);
this.stream.write(line);
readline.moveCursor(this.stream, -(stringLength(line)), moveCursorPos);
}
start(text, options = {}) {
this.#started = true;
this.text = text;
if (typeof options.withPrefix === "string") {
this.#prefix = options.withPrefix;
}
this.emit("start");
this.#spinnerPos = internalSpinnerCount++;
this.#startTime = performance.now();
if (!this.#verbose) {
return this;
}
this.#frameIndex = 0;
this.stream.write(this.#lineToRender() + "\n");
this.#interval = setInterval(() => this.#renderLine(), this.#spinner.interval);
return this;
}
#stop(text) {
this.text = text;
this.#started = false;
if (this.#interval !== null) {
clearInterval(this.#interval);
}
}
succeed(text) {
if (this.#started) {
this.#stop(text);
this.#renderLine(kLogSymbols.success);
this.emit("succeed");
}
return this;
}
failed(text) {
if (this.#started) {
this.#stop(text);
this.#renderLine(kLogSymbols.error);
this.emit("failed");
}
return this;
}
}
/**
* @note code copy-pasted from https://github.com/chalk/ansi-regex#readme
*/
function ansiRegex() {
// Valid string terminator sequences are BEL, ESC\, and 0x9c
const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)";
const pattern = [
`[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?${ST})`,
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))"
].join("|");
return new RegExp(pattern, "g");
}
//# sourceMappingURL=Spinner.class.js.map