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
JavaScript
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