UNPKG

igir

Version:

🕹 A zero-setup ROM collection manager that sorts, filters, extracts or archives, patches, and reports on collections of any size on any OS.

292 lines (291 loc) • 10 kB
import chalk from "chalk"; import isUnicodeSupported from "is-unicode-supported"; import { linearRegression, linearRegressionLine } from "simple-statistics"; import IntlUtil from "../utils/intlUtil.js"; import TimeUtil from "../utils/timeUtil.js"; import ProgressBar, { ProgressBarSymbol } from "./progressBar.js"; const CHALK_PROGRESS_COMPLETE_DEFAULT = chalk.reset; const CHALK_PROGRESS_IN_PROGRESS = chalk.blackBright; const CHALK_PROGRESS_INCOMPLETE = chalk.ansi256(240); const IS_UNICODE_SUPPORTED = isUnicodeSupported(); const BAR_COMPLETE_CHAR = IS_UNICODE_SUPPORTED ? "\u25A0" : "\u25AC"; const BAR_IN_PROGRESS_CHAR = IS_UNICODE_SUPPORTED ? "\u25A0" : "\u25AC"; const BAR_INCOMPLETE_CHAR = IS_UNICODE_SUPPORTED ? "\u25A0" : "\u25AC"; const DEFAULT_ETA = "--:--:--"; const clamp = (val, min, max) => Math.min(Math.max(val ?? 0, min), max); class SingleBar extends ProgressBar { static BAR_SIZE = 30; multiBar; displayDelay; displayCreated; indentSize; symbol; name; showProgressNewline; progressBarSizeMultiplier; progressFormatter; completed; inProgress; total; finishedMessage; lastOutput; // Memoize the formatted `completed`/`total` values. `total` is constant for the life of the bar, // and `completed` repeats across the renders where it hasn't advanced yet, so a single-slot cache // per value avoids re-running the (potentially expensive) progress formatter every render. lastFormattedCompletedValue; lastFormattedCompleted = ""; lastFormattedTotalValue; lastFormattedTotal = ""; valueTimeBuffer = []; valueTimeBufferIndex = 0; valueTimeBufferSize = 0; lastEtaCalculatedTime = 0; lastEtaCalculated = 0; lastEtaFormatTime = 0; lastEtaFormatted = DEFAULT_ETA; constructor(multiBar, options) { super(); this.multiBar = multiBar; if (options?.displayDelay !== void 0) { this.displayDelay = options.displayDelay; this.displayCreated = TimeUtil.hrtimeMillis(); } this.indentSize = options?.indentSize ?? 0; this.symbol = options?.symbol; this.name = options?.name; this.showProgressNewline = options?.showProgressNewline ?? true; this.progressBarSizeMultiplier = options?.progressBarSizeMultiplier ?? 1; this.progressFormatter = options?.progressFormatter ?? ((progress) => IntlUtil.toLocaleString(progress)); this.completed = options?.completed ?? 0; this.inProgress = options?.inProgress ?? 0; this.total = options?.total ?? 0; this.finishedMessage = options?.finishedMessage; } /** * A child progress bar to this progress bar. */ addChildBar(options) { return this.multiBar.addSingleBar( { indentSize: this.indentSize + (this.symbol?.symbol ? 2 : 0), progressBarSizeMultiplier: this.progressBarSizeMultiplier / 2, showProgressNewline: false, ...options }, this ); } getIndentSize() { return this.indentSize; } getSymbol() { return this.symbol; } setSymbol(symbol) { if (this.symbol === symbol) { return; } this.symbol = symbol; this.multiBar.clearAndRender(); } getName() { return this.name; } setName(name) { if (this.name === name) { return; } this.name = name; this.multiBar.clearAndRender(); } /** * Reset the completed, in-progress, and total values of the progress bar. */ resetProgress(total) { if (this.displayDelay !== void 0) { this.displayCreated = TimeUtil.hrtimeMillis(); } this.completed = 0; this.inProgress = 0; this.total = total; this.valueTimeBuffer = []; this.valueTimeBufferIndex = 0; this.valueTimeBufferSize = 0; } /** * Increment the completed count by the given increment (default: 1). */ incrementCompleted(increment = 1) { this.completed += increment; this.inProgress = Math.max(this.inProgress - increment, 0); } setCompleted(completed) { this.completed = completed; } /** * Increment the in-progress count by the given increment (default: 1). */ incrementInProgress(increment = 1) { this.inProgress += increment; } setInProgress(inProgress) { this.inProgress = inProgress; } /** * Increment the total count by the given increment (default: 1). */ incrementTotal(increment = 1) { this.total += increment; } setTotal(total) { this.total = total; } /** * Set the completed value to the total, and store a finished message. */ finish(finishedMessage) { if (this.symbol?.symbol) { this.symbol = ProgressBarSymbol.DONE; } if (this.total > 0) { this.completed = this.total; } else { this.completed = 1; } this.inProgress = 0; this.finishedMessage = finishedMessage; } /** * Log this {@link SingleBar}'s last output and freeze it. */ freeze() { this.multiBar.freezeSingleBar(this); } /** * Delete this {@link SingleBar} from the {@link MultiBar}. */ delete() { this.multiBar.removeSingleBar(this); } /** * Return the formatted output of this progress bar. */ format() { if (this.displayDelay !== void 0 && (this.completed >= this.total || TimeUtil.hrtimeMillis(this.displayCreated) < this.displayDelay)) { return ""; } this.displayDelay = void 0; let output = " ".repeat(this.indentSize); if (this.symbol?.symbol) { output += this.symbol.color(`${this.symbol.symbol} `); } if (this.finishedMessage) { output += `${this.name} ${CHALK_PROGRESS_IN_PROGRESS("\xBB")} ${this.finishedMessage}`; this.lastOutput = output; return this.lastOutput; } if (!this.showProgressNewline) { output += `${this.getBar()} `; } if (this.name) { output += `${this.name} `; } if (this.showProgressNewline) { output += ` ${" ".repeat(this.indentSize + (this.symbol?.symbol ? 2 : 0))}${this.getBar()} `; } this.lastOutput = output.trimEnd(); return this.lastOutput; } getBar() { let bar = ""; const symbolColor = (this.indentSize === 0 ? this.symbol?.color : void 0) ?? CHALK_PROGRESS_COMPLETE_DEFAULT; const barSize = Math.floor(SingleBar.BAR_SIZE * this.progressBarSizeMultiplier) - this.indentSize - (this.symbol?.symbol ? 2 : 0); const completeSize = this.total > 0 ? Math.floor(clamp(this.completed / this.total, 0, 1) * barSize) : 0; bar += symbolColor(BAR_COMPLETE_CHAR.repeat(Math.max(completeSize, 0))); const inProgressSize = this.total > 0 ? Math.ceil(clamp(this.inProgress, 0, this.total) / this.total * barSize) : 0; bar += CHALK_PROGRESS_IN_PROGRESS(BAR_IN_PROGRESS_CHAR.repeat(Math.max(inProgressSize, 0))); const incompleteSize = barSize - inProgressSize - completeSize; bar += CHALK_PROGRESS_INCOMPLETE(BAR_INCOMPLETE_CHAR.repeat(Math.max(incompleteSize, 0))); bar += " "; if (this.completed !== this.lastFormattedCompletedValue) { this.lastFormattedCompletedValue = this.completed; this.lastFormattedCompleted = this.progressFormatter(this.completed); } const formattedCompleted = this.lastFormattedCompleted; if (this.total !== this.lastFormattedTotalValue) { this.lastFormattedTotalValue = this.total; this.lastFormattedTotal = this.progressFormatter(this.total); } const formattedTotal = this.lastFormattedTotal; const paddedCompleted = formattedCompleted.padStart( Math.max(formattedTotal.length, this.indentSize > 0 ? 7 : 0), " " ); const paddedTotal = formattedTotal.padStart(this.indentSize > 0 ? 7 : 0, " "); bar += `${symbolColor(paddedCompleted)}/${CHALK_PROGRESS_IN_PROGRESS(paddedTotal)} `; if (this.completed > 0 || this.indentSize > 0) { bar += CHALK_PROGRESS_INCOMPLETE(`[${this.getEtaFormatted()}]`); } return bar.trim(); } getEtaFormatted() { if (this.completed === 0) { return DEFAULT_ETA; } const etaSeconds = this.calculateEta(); const timeNow = TimeUtil.hrtimeMillis(); const elapsedMs = timeNow - this.lastEtaFormatTime; if (etaSeconds > 60 && elapsedMs < 5e3) { return this.lastEtaFormatted; } this.lastEtaFormatTime = timeNow; if (Math.floor(etaSeconds) < 0) { this.lastEtaFormatted = DEFAULT_ETA; return this.lastEtaFormatted; } const hours = Math.floor(etaSeconds / 3600); const minutes = Math.floor(etaSeconds % 3600 / 60); const seconds = Math.ceil(etaSeconds) % 60; this.lastEtaFormatted = `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`; return this.lastEtaFormatted; } calculateEta() { const timeNow = TimeUtil.hrtimeMillis(); const elapsedMs = timeNow - this.lastEtaCalculatedTime; if (elapsedMs < 50) { return this.lastEtaCalculated; } this.lastEtaCalculatedTime = timeNow; const MAX_BUFFER_SIZE = clamp(Math.floor(this.total / 10), 25, 50); if (this.valueTimeBuffer.length !== MAX_BUFFER_SIZE) { this.valueTimeBuffer = Array.from({ length: MAX_BUFFER_SIZE }); this.valueTimeBufferIndex = 0; this.valueTimeBufferSize = 0; } this.valueTimeBuffer[this.valueTimeBufferIndex] = [this.completed, Date.now()]; this.valueTimeBufferIndex = (this.valueTimeBufferIndex + 1) % MAX_BUFFER_SIZE; this.valueTimeBufferSize = Math.min(this.valueTimeBufferSize + 1, MAX_BUFFER_SIZE); const doneTime = linearRegressionLine( linearRegression( this.valueTimeBufferSize < MAX_BUFFER_SIZE ? this.valueTimeBuffer.slice(0, this.valueTimeBufferSize) : this.valueTimeBuffer ) )(this.total); if (Number.isNaN(doneTime)) { return -1; } const remaining = (doneTime - Date.now()) / 1e3; if (!Number.isFinite(remaining)) { return -1; } this.lastEtaCalculated = Math.max(remaining, 0); return this.lastEtaCalculated; } getLastOutput() { return this.lastOutput; } } export { SingleBar as default }; //# sourceMappingURL=singleBar.js.map