Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
branches: [main]
pull_request:

env:
FORCE_COLOR: "1"

permissions:
contents: read

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ All available spinners are part of [cli-spinners](https://github.com/sindresorhu

## Requirements

- [Node.js](https://nodejs.org/en/) v20 or higher
- [Node.js](https://nodejs.org/en/) v22 or higher

## Getting Started

Expand Down
29 changes: 7 additions & 22 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,15 @@
"version": "2.1.2",
"description": "Asynchronous CLI Spinner. Allow to create and manage simultaneous/multiple spinners at a time.",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.cjs",
"import": "./dist/index.js"
}
},
"type": "module",
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
"build": "tsc",
"prepublishOnly": "npm run build",
"test-only": "tsx --test test/Spinner.spec.ts",
"test-only": "tsx --test test/**.spec.ts",
"test": "npm run lint && npm run coverage",
"coverage": "c8 -r html npm run test-only",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
"lint": "eslint src test"
},
"repository": {
"type": "git",
Expand All @@ -46,24 +38,17 @@
},
"homepage": "https://github.com/TopCli/Spinner#readme",
"dependencies": {
"@topcli/wcwidth": "^1.0.1",
"ansi-regex": "^6.0.1",
"cli-cursor": "^5.0.0",
"cli-spinners": "^3.1.0",
"kleur": "^4.1.5",
"strip-ansi": "^7.1.0"
"cli-spinners": "^3.1.0"
},
"devDependencies": {
"@openally/config.eslint": "^2.0.0",
"@openally/config.typescript": "^1.0.3",
"@types/node": "^22.10.2",
"c8": "^10.1.2",
"tsup": "^8.1.0",
"tsx": "^4.16.2",
"typescript": "^5.5.3"
},
"engines": {
"node": ">=20"
},
"type": "module"
"node": ">=22"
}
}
51 changes: 39 additions & 12 deletions src/Spinner.class.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
// 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 stripAnsi from "strip-ansi";
import ansiRegex from "ansi-regex";
import wcwidth from "@topcli/wcwidth";
import kleur from "kleur";

// Import Internal Dependencies
import {
stringLength,
type Color
} from "./utils/index.js";

// VARS
let internalSpinnerCount = 0;

// CONSTANTS
const kDefaultSpinnerName = "dots" satisfies cliSpinners.SpinnerName;
const kAvailableColors = new Set<Color>(
Object.keys(inspect.colors) as Color[]
);

const kLogSymbols = process.platform !== "win32" || process.env.CI || process.env.TERM === "xterm-256color" ?
{ success: kleur.bold().green("✔"), error: kleur.bold().red("✖") } :
{ success: kleur.bold().green("√"), error: kleur.bold().red("×") };
{ success: styleText("green", "✔"), error: styleText("red", "✖") } :
{ success: styleText("green", "√"), error: styleText("red", "×") };

export interface ISpinnerOptions {
/**
Expand All @@ -32,7 +39,7 @@ export interface ISpinnerOptions {
*
* @default "white"
*/
color?: string;
color?: Color;
/**
* Do not log anything when disabled
*
Expand Down Expand Up @@ -73,12 +80,18 @@ export class Spinner extends EventEmitter {

const { name = kDefaultSpinnerName, color = null } = options;

this.#spinner = name in cliSpinners ? cliSpinners[name] : cliSpinners[kDefaultSpinnerName];
this.#spinner = name in cliSpinners ?
cliSpinners.default[name] :
cliSpinners.default[kDefaultSpinnerName];
if (color === null) {
this.#color = (str: string) => str;
}
else {
this.#color = color in kleur ? kleur[color] : kleur.white;
const colorArr = Array.isArray(color) ? color : [color];

this.#color = colorArr.every((color) => kAvailableColors.has(color)) ?
(str) => styleText(color, str) :
(str) => styleText("white", str);
}
}

Expand Down Expand Up @@ -135,9 +148,9 @@ export class Spinner extends EventEmitter {
}
count = regexArray.length;
}
count += regexArray!.reduce((prev, curr) => prev + wcwidth(curr), 0);
count += regexArray!.reduce((prev, curr) => prev + stringLength(curr), 0);

return wcwidth(stripAnsi(defaultRaw)) > terminalCol ?
return stringLength(defaultRaw) > terminalCol ?
`${defaultRaw.slice(0, terminalCol + count)}\x1B[0m` :
defaultRaw;
}
Expand All @@ -153,7 +166,7 @@ export class Spinner extends EventEmitter {
const line = this.#lineToRender(spinnerSymbol);
readline.clearLine(this.stream, 0);
this.stream.write(line);
readline.moveCursor(this.stream, -(wcwidth(line)), moveCursorPos);
readline.moveCursor(this.stream, -(stringLength(line)), moveCursorPos);
}

start(text?: string, options: IStartOptions = {}) {
Expand Down Expand Up @@ -210,3 +223,17 @@ export class Spinner extends EventEmitter {
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");
}
59 changes: 59 additions & 0 deletions src/utils/colors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
type ForegroundColors =
| "black"
| "blackBright"
| "blue"
| "blueBright"
| "cyan"
| "cyanBright"
| "gray"
| "green"
| "greenBright"
| "grey"
| "magenta"
| "magentaBright"
| "red"
| "redBright"
| "white"
| "whiteBright"
| "yellow"
| "yellowBright";

type BackgroundColors =
| "bgBlack"
| "bgBlackBright"
| "bgBlue"
| "bgBlueBright"
| "bgCyan"
| "bgCyanBright"
| "bgGray"
| "bgGreen"
| "bgGreenBright"
| "bgGrey"
| "bgMagenta"
| "bgMagentaBright"
| "bgRed"
| "bgRedBright"
| "bgWhite"
| "bgWhiteBright"
| "bgYellow"
| "bgYellowBright";

type Modifiers =
| "blink"
| "bold"
| "dim"
| "doubleunderline"
| "framed"
| "hidden"
| "inverse"
| "italic"
| "overlined"
| "reset"
| "strikethrough"
| "underline";

export type Color =
| ForegroundColors
| BackgroundColors
| Modifiers
| Array<ForegroundColors | BackgroundColors | Modifiers>;
2 changes: 2 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./stringLength.js";
export * from "./colors.js";
24 changes: 24 additions & 0 deletions src/utils/stringLength.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Import Node.js Dependencies
import {
stripVTControlCharacters
} from "node:util";

// CONSTANTS
const kLenSegmenter = new Intl.Segmenter();

export function stringLength(
string: string
): number {
if (string === "") {
return 0;
}

let length = 0;
for (const _ of kLenSegmenter.segment(
stripVTControlCharacters(string)
)) {
length++;
}

return length;
}
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"extends": "@openally/config.typescript",
"compilerOptions": {
"outDir": "dist",
"rootDir": "./",
"rootDir": "./src",
},
"include": ["src", "index.ts"],
"include": ["src"],
"exclude": ["node_modules", "dist"]
}