UNPKG

@grandlinex/easy-cli

Version:

Cli lib to perform common tasks

117 lines (116 loc) 3.88 kB
import process from 'process'; function clamp(n, min, max) { return Math.min(max, Math.max(min, n)); } function pad2(n) { return n < 10 ? `0${n}` : `${n}`; } function formatDuration(ms) { const totalSec = Math.max(0, Math.floor(ms / 1000)); const minutes = Math.floor(totalSec / 60); const seconds = totalSec % 60; return `${pad2(minutes)}:${pad2(seconds)}`; } function stripAnsi(input) { // Remove ANSI escape sequences so padEnd uses visible text length. // eslint-disable-next-line no-control-regex return input.replace(/\x1b\[[0-9;]*m/g, ''); } function color(enabled, code, text) { if (!enabled) { return text; } return `\x1b[${code}m${text}\x1b[0m`; } export default class ProgressBar { constructor(end = 1) { this.maxLength = 0; this.info = null; this.width = 36; this.frameIndex = 0; this.frames = ['|', '/', '-', '\\']; this.unicodeFrames = [ '⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏', ]; this.colorEnabled = Boolean(process.stdout.isTTY); this.startMs = Date.now(); this.cur = 0; this.end = end; } reset() { this.startMs = Date.now(); this.cur = 0; this.maxLength = 0; this.frameIndex = 0; this.info = null; } setEnd(end) { this.end = end; this.update(); } setInfo(info) { this.info = info; } setState(cur, info) { this.cur = cur; if (info !== undefined) { this.info = info; } this.update(); } done() { this.cur = this.end; const line = this.renderLine(true); process.stdout.write(`${line}\r\n`); } renderLine(isDone) { const safeEnd = this.end <= 0 ? 1 : this.end; const curClamped = clamp(this.cur, 0, safeEnd); const ratio = clamp(curClamped / safeEnd, 0, 1); const perc = ratio * 100; const elapsedMs = Date.now() - this.startMs; const etaMs = ratio > 0 ? elapsedMs / ratio - elapsedMs : 0; const speed = elapsedMs > 0 ? curClamped / (elapsedMs / 1000) : 0; const filled = Math.round(this.width * ratio); const empty = this.width - filled; const fillChar = this.colorEnabled ? '█' : '#'; const emptyChar = this.colorEnabled ? '░' : '-'; const donePart = fillChar.repeat(filled); const todoPart = emptyChar.repeat(empty); const bar = this.colorEnabled ? `${color(true, '32', donePart)}${color(true, '90', todoPart)}` : `${donePart}${todoPart}`; const spinnerFrames = this.colorEnabled ? this.unicodeFrames : this.frames; const frame = isDone ? '✓' : spinnerFrames[this.frameIndex % spinnerFrames.length]; this.frameIndex += 1; const status = isDone ? color(this.colorEnabled, '32', 'Done') : color(this.colorEnabled, '36', 'Working'); const timeText = `${formatDuration(elapsedMs)}/${formatDuration(etaMs)}`; const infoText = this.info ? ` ${this.info}` : ''; let line = `${frame} ${status} [${bar}] ${perc.toFixed(1)}% (${timeText}) ${speed.toFixed(1)}/s${infoText}`; const visibleLen = stripAnsi(line).length; if (visibleLen > this.maxLength) { this.maxLength = visibleLen; } // Pad based on visible text length, not raw string length with ANSI. const padSpaces = Math.max(0, this.maxLength - visibleLen); line = `${line}${' '.repeat(padSpaces)}`; return line; } update() { const line = this.renderLine(false); process.stdout.write(`${line}\r`); } }