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.
168 lines • 7.37 kB
JavaScript
import chalk from 'chalk';
import { linearRegression, linearRegressionLine } from 'simple-statistics';
import stripAnsi from 'strip-ansi';
import ConsolePoly from '../polyfill/consolePoly.js';
import TimePoly from '../polyfill/timePoly.js';
/**
* A wrapper class for a cli-progress {@link SingleBar} that formats the output.
*/
class SingleBarFormatted {
constructor(multiBar, initialTotal, initialPayload) {
this.valueTimeBuffer = [];
this.lastEtaTime = 0;
this.lastEtaValue = 'infinity';
this.multiBar = multiBar;
this.singleBar = this.multiBar.create(initialTotal, 0, initialPayload, {
format: (options, params, payload) => {
const symbolAndName = `${SingleBarFormatted.getSymbolAndName(payload)} | `;
const progressWrapped = this.getProgress(options, params, payload)
.split('\n')
.map((line, idx) => {
// Wrapping is broken: https://github.com/npkgz/cli-progress/issues/142
let lineTrimmed = line;
const maxLineLength = ConsolePoly.consoleWidth() - stripAnsi(symbolAndName).length - 2;
if (line.length > maxLineLength) {
// TODO(cemmer): seems to trim the first line of multi-line statuses too short
lineTrimmed = `${line.slice(0, maxLineLength - 3)}...`;
}
if (idx === 0) {
return lineTrimmed;
}
return ' '.repeat(stripAnsi(symbolAndName).length) + lineTrimmed;
})
.join('\n\x1B[K');
this.lastOutput = `${symbolAndName}${progressWrapped}`.trim();
return (this.lastOutput
// cli-progress doesn't handle multi-line progress bars, collapse to one line. The multi-
// line message will get logged correctly when the progress bar is frozen & logged.
.replaceAll(/\n\S*\s+/g, ' '));
},
});
}
getSingleBar() {
return this.singleBar;
}
getLastOutput() {
return this.lastOutput;
}
static getSymbolAndName(payload) {
const symbol = chalk.bold(payload.symbol ?? '');
const name = payload.name ?? '';
const namePadded = `${name} ${'·'.repeat(SingleBarFormatted.MAX_NAME_LENGTH)}`.trim();
const symbolAndName = `${symbol} ${namePadded}`;
const excessLength = stripAnsi(symbolAndName).trimStart().length - SingleBarFormatted.MAX_NAME_LENGTH;
const nameTrimmed = namePadded.slice(0, namePadded.length - Math.max(excessLength, 0));
return `${symbol} ${nameTrimmed}`.trimStart();
}
getProgress(options, params, payload) {
if (payload.finishedMessage) {
return payload.finishedMessage;
}
let progress = SingleBarFormatted.getBar(options, params, payload);
if (!params.total) {
return progress;
}
progress += ` | ${params.value.toLocaleString()}/${params.total.toLocaleString()}`;
if (params.value > 0) {
const eta = this.calculateEta(params);
if (eta > 0) {
progress += ` | ETA: ${this.getEtaFormatted(eta)}`;
}
}
if (payload.waitingMessage) {
progress += ` | ${payload.waitingMessage}`;
}
return progress;
}
calculateEta(params) {
const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
const MAX_BUFFER_SIZE = clamp(Math.floor(params.total / 10), 25, 50);
this.valueTimeBuffer = [
...this.valueTimeBuffer.slice(1 - MAX_BUFFER_SIZE),
[params.value, Date.now()],
];
const doneTime = linearRegressionLine(linearRegression(this.valueTimeBuffer))(params.total);
if (Number.isNaN(doneTime)) {
// Vertical line
return -1;
}
const remaining = (doneTime - Date.now()) / 1000;
if (!Number.isFinite(remaining)) {
return -1;
}
return Math.max(remaining, 0);
}
static getBar(options, params, payload) {
const barSize = options.barsize ?? 0;
if (barSize <= 0) {
return '';
}
const clamp = (val, min, max) => Math.min(Math.max(val ?? 0, min), max);
const completeSize = Math.floor(clamp(params.progress, 0, 1) * barSize);
const inProgressSize = params.total > 0
? Math.ceil((clamp(payload.inProgress, 0, params.total) / params.total) * barSize)
: 0;
const incompleteSize = barSize - inProgressSize - completeSize;
return (SingleBarFormatted.BAR_COMPLETE_CHAR.repeat(Math.max(completeSize, 0)) +
SingleBarFormatted.BAR_IN_PROGRESS_CHAR.repeat(Math.max(inProgressSize, 0)) +
SingleBarFormatted.BAR_INCOMPLETE_CHAR.repeat(Math.max(incompleteSize, 0)));
}
getEtaFormatted(etaSeconds) {
// Rate limit how often the ETA can change
// Update only every 5s if the ETA is >60s
const elapsedMs = TimePoly.hrtimeMillis(this.lastEtaTime);
if (etaSeconds > 60 && elapsedMs < 5000) {
return this.lastEtaValue;
}
this.lastEtaTime = TimePoly.hrtimeMillis();
if (etaSeconds < 0) {
this.lastEtaValue = 'infinity';
return this.lastEtaValue;
}
const etaSecondsInt = Math.ceil(etaSeconds);
const secondsRounded = 5 * Math.round(etaSecondsInt / 5);
if (secondsRounded >= 86_400) {
this.lastEtaValue = SingleBarFormatted.getEtaFormattedDays(secondsRounded);
}
else if (secondsRounded >= 3600) {
this.lastEtaValue = SingleBarFormatted.getEtaFormattedHours(secondsRounded);
}
else if (secondsRounded >= 60) {
this.lastEtaValue = SingleBarFormatted.getEtaFormattedMinutes(secondsRounded);
}
else if (etaSecondsInt >= 10) {
this.lastEtaValue = `${secondsRounded}s`;
}
else {
this.lastEtaValue = `${etaSecondsInt}s`;
}
return this.lastEtaValue;
}
static getEtaFormattedDays(secondsRounded) {
const hours = Math.floor((secondsRounded % 86_400) / 3600);
if (hours > 0) {
return `${Math.floor(secondsRounded / 86_400)}d${hours}h`;
}
return `${Math.floor(secondsRounded / 86_400)}d`;
}
static getEtaFormattedHours(secondsRounded) {
const minutes = Math.floor((secondsRounded % 3600) / 60);
if (minutes > 0) {
return `${Math.floor(secondsRounded / 3600)}h${minutes}m`;
}
return `${Math.floor(secondsRounded / 3600)}h`;
}
static getEtaFormattedMinutes(secondsRounded) {
const seconds = secondsRounded % 60;
if (seconds > 0) {
return `${Math.floor(secondsRounded / 60)}m${seconds}s`;
}
return `${Math.floor(secondsRounded / 60)}m`;
}
}
SingleBarFormatted.MAX_NAME_LENGTH = 35;
SingleBarFormatted.BAR_COMPLETE_CHAR = '\u2588';
SingleBarFormatted.BAR_IN_PROGRESS_CHAR = '\u2592';
SingleBarFormatted.BAR_INCOMPLETE_CHAR = '\u2591';
export default SingleBarFormatted;
//# sourceMappingURL=singleBarFormatted.js.map