xprogress 0.17.2 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -11
- package/{typings/index.d.ts → index.d.ts} +0 -0
- package/index.js +784 -0
- package/package.json +14 -41
- package/dist/index.js +0 -1038
package/index.js
ADDED
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright (c) 2020 Miraculous Owonubi
|
|
3
|
+
* @author Miraculous Owonubi
|
|
4
|
+
* @license Apache-2.0
|
|
5
|
+
* @module progress2
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {format} from 'util';
|
|
9
|
+
import {openSync} from 'fs';
|
|
10
|
+
import tty from 'tty';
|
|
11
|
+
import EventEmitter from 'events';
|
|
12
|
+
|
|
13
|
+
import merge from 'lodash.merge';
|
|
14
|
+
import xbytes from 'xbytes';
|
|
15
|
+
import stringd from 'stringd';
|
|
16
|
+
import prettyMs from 'pretty-ms';
|
|
17
|
+
import padRatio from 'pad-ratio';
|
|
18
|
+
import speedometer from 'speedometer';
|
|
19
|
+
import progressStream from 'progress-stream';
|
|
20
|
+
import {raw as cStringdRaw} from 'stringd-colors';
|
|
21
|
+
|
|
22
|
+
const globOpts = {
|
|
23
|
+
bar: {
|
|
24
|
+
blank: '-',
|
|
25
|
+
filler: '#',
|
|
26
|
+
header: '',
|
|
27
|
+
colorize: !0,
|
|
28
|
+
separator: '',
|
|
29
|
+
pulsateSkip: 15,
|
|
30
|
+
pulsateLength: 15,
|
|
31
|
+
},
|
|
32
|
+
label: 'Loading',
|
|
33
|
+
clean: !1,
|
|
34
|
+
length: 40,
|
|
35
|
+
flipper: ['|', '/', '-', '\\'],
|
|
36
|
+
pulsate: !1,
|
|
37
|
+
template: '',
|
|
38
|
+
variables: {
|
|
39
|
+
tag: ({tag}) => (tag && typeof tag !== 'function' ? `${tag}\n` : ''),
|
|
40
|
+
...cStringdRaw,
|
|
41
|
+
'color:bar:empty': ':{color:close}',
|
|
42
|
+
'color:bar:header': ':{color(green)}',
|
|
43
|
+
'color:bar:filled': ':{bgcolor(green)}:{color(black)}',
|
|
44
|
+
'color:bar:separator': '',
|
|
45
|
+
},
|
|
46
|
+
forceFirst: !1,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const defaultOptions = {
|
|
50
|
+
...globOpts,
|
|
51
|
+
template: ':{tag}[:{bar}] :{flipper} :{label} :3{percentage}% [:{completed}/:{total}]',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const streamOpts = {
|
|
55
|
+
...globOpts,
|
|
56
|
+
pulsate: false,
|
|
57
|
+
progress: {time: 100, pulsate: false, infinite: !1, pulsateSkip: 15, pulsateLength: 15},
|
|
58
|
+
template: [
|
|
59
|
+
':{label}',
|
|
60
|
+
' |:{slot:bar}| [:3{slot:percentage}%] (:{slot:eta}) [:{speed}] [:{slot:size}/:{slot:size:total}]',
|
|
61
|
+
' [:{bar}] [:3{percentage}%] (:{eta}) [:{size}/:{size:total}]',
|
|
62
|
+
],
|
|
63
|
+
variables: {
|
|
64
|
+
...globOpts.variables,
|
|
65
|
+
eta: null,
|
|
66
|
+
size: null,
|
|
67
|
+
speed: null,
|
|
68
|
+
progress: null,
|
|
69
|
+
'eta:raw': null,
|
|
70
|
+
'slot:bar': null,
|
|
71
|
+
'slot:blank': null,
|
|
72
|
+
'slot:eta': null,
|
|
73
|
+
'slot:eta:raw': null,
|
|
74
|
+
'slot:filler': null,
|
|
75
|
+
'slot:header': null,
|
|
76
|
+
'slot:size': null,
|
|
77
|
+
'slot:total': null,
|
|
78
|
+
'slot:runtime': null,
|
|
79
|
+
'slot:runtime:raw': null,
|
|
80
|
+
'slot:percentage': null,
|
|
81
|
+
'slot:size:total': null,
|
|
82
|
+
},
|
|
83
|
+
stageOpts: {},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get a persistent tty output stream
|
|
88
|
+
* @returns {tty.WriteStream} The writable stream instance similar to `process.stdout`
|
|
89
|
+
*/
|
|
90
|
+
function getPersistentStdout() {
|
|
91
|
+
const self = getPersistentStdout;
|
|
92
|
+
self.stdout =
|
|
93
|
+
self.stdout && self.stdout.isTTY
|
|
94
|
+
? self.stdout
|
|
95
|
+
: process.stdout.isTTY
|
|
96
|
+
? process.stdout
|
|
97
|
+
: process.stderr.isTTY
|
|
98
|
+
? process.stderr
|
|
99
|
+
: new tty.WriteStream(openSync('/dev/tty', 'w'));
|
|
100
|
+
return self.stdout;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Parse a bar, returning a styled bar with a given percentage filled
|
|
105
|
+
* @param {_defaultOptions} opts The options for the bar being parsed
|
|
106
|
+
* @param {Number} fillable Maximum fillable slots
|
|
107
|
+
* @param {Number} percentage Percentage filled
|
|
108
|
+
* @param {Boolean} headers Whether or not to add headers to the bar
|
|
109
|
+
*/
|
|
110
|
+
function parseBar(opts, fillable, percentage, headers = !opts.pulsate) {
|
|
111
|
+
fillable = Math.round(fillable);
|
|
112
|
+
let filled = Math.round((Math.min(100, percentage) / 100) * fillable);
|
|
113
|
+
let empty = fillable - filled;
|
|
114
|
+
let {filler, blank, header} = opts.bar;
|
|
115
|
+
[filled, empty] = [filled, empty].map(Math.floor);
|
|
116
|
+
[filler, blank] = [filler, blank].map(content => (Array.isArray(content) || typeof content === 'string' ? content : ''));
|
|
117
|
+
return stringd(
|
|
118
|
+
[
|
|
119
|
+
`:{color:bar:filled}${filler.repeat(filled)}`,
|
|
120
|
+
`:{color:bar:header}${headers ? header : ''}`,
|
|
121
|
+
`:{color:bar:empty}${blank.repeat(empty)}`,
|
|
122
|
+
'',
|
|
123
|
+
].join(':{color:close}:{bgcolor:close}'),
|
|
124
|
+
opts.variables,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const xprettyMs = (
|
|
129
|
+
_ => sec =>
|
|
130
|
+
Number.isFinite(sec) ? _(sec) : '\u221e'
|
|
131
|
+
)(prettyMs);
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Create a pulsate bar
|
|
135
|
+
* @param {ProgressBar} bar The bar to be pulsated
|
|
136
|
+
* @param {Array} slots Pulsate slots to be used
|
|
137
|
+
* @param {number} skip Valuation for by how much to skip the bar
|
|
138
|
+
* @param {string} sep The separator to be used on the bar
|
|
139
|
+
*/
|
|
140
|
+
function pulsateBar(bar, slots, skip, sep = '') {
|
|
141
|
+
if (slots[0].level + slots[1].level >= 100) slots[0].level = 100 - slots[1].level;
|
|
142
|
+
const total = bar.length() - sep.length * 2;
|
|
143
|
+
const stack = [...slots, {level: 100 - (slots[0].level + slots[1].level), value: 0}].map(({level, value}) => ({
|
|
144
|
+
fillable: Math.round((level / 100) * total),
|
|
145
|
+
percentage: value,
|
|
146
|
+
}));
|
|
147
|
+
if (slots[0].level + slots[1].level === 100) slots[0].level = 0;
|
|
148
|
+
else slots[0].level += skip;
|
|
149
|
+
stack[stack.length - 1].fillable +=
|
|
150
|
+
total -
|
|
151
|
+
Math.min(
|
|
152
|
+
total,
|
|
153
|
+
stack.reduce((max, {fillable}) => max + fillable, 0),
|
|
154
|
+
);
|
|
155
|
+
return stack.map(({fillable, percentage}) => parseBar(bar.opts, fillable, percentage, false));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
class ProgressGen extends EventEmitter {}
|
|
159
|
+
|
|
160
|
+
export default class ProgressBar {
|
|
161
|
+
/**
|
|
162
|
+
* Build a progress bar
|
|
163
|
+
* @param {Number} total Max attainable value by the progressBar
|
|
164
|
+
* @param {((size) => Number|Number[])|Number|Number[]} arr Allocation of slots by value
|
|
165
|
+
* @param {_defaultOptions} opts Attachable options
|
|
166
|
+
*/
|
|
167
|
+
constructor(total, arr = [total], opts = {}) {
|
|
168
|
+
if (!(total && typeof total === 'number')) throw Error('<ProgressBar> must have a max integer value');
|
|
169
|
+
if (typeof arr === 'function') arr = arr(total);
|
|
170
|
+
if (typeof arr === 'object' && !Array.isArray(arr)) [arr, opts] = [[total], arr];
|
|
171
|
+
this.opts = {
|
|
172
|
+
...defaultOptions,
|
|
173
|
+
...opts,
|
|
174
|
+
bar: {...defaultOptions.bar, ...opts.bar},
|
|
175
|
+
variables: {...defaultOptions.variables, ...opts.variables},
|
|
176
|
+
};
|
|
177
|
+
this.cores = {
|
|
178
|
+
total,
|
|
179
|
+
label: this.opts.label,
|
|
180
|
+
append: [],
|
|
181
|
+
length: this.opts.length,
|
|
182
|
+
stdout: getPersistentStdout(),
|
|
183
|
+
pulsateSlots: [
|
|
184
|
+
[0, 0],
|
|
185
|
+
[this.opts.bar.pulsateLength, 100],
|
|
186
|
+
].map(([level, value]) => ({
|
|
187
|
+
level,
|
|
188
|
+
value,
|
|
189
|
+
})),
|
|
190
|
+
};
|
|
191
|
+
const self = this;
|
|
192
|
+
this.slots = padRatio(arr, total, false).map(max => ({
|
|
193
|
+
max,
|
|
194
|
+
done: 0,
|
|
195
|
+
get level() {
|
|
196
|
+
return (this.max / self.total()) * 100;
|
|
197
|
+
},
|
|
198
|
+
get percentage() {
|
|
199
|
+
return (this.done / this.max) * 100;
|
|
200
|
+
},
|
|
201
|
+
}));
|
|
202
|
+
delete this.opts.blot;
|
|
203
|
+
delete this.opts.label;
|
|
204
|
+
delete this.opts.append;
|
|
205
|
+
delete this.opts.length;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Label the progressbar while returning itself
|
|
210
|
+
* @param {String} label The string label
|
|
211
|
+
*/
|
|
212
|
+
label(label) {
|
|
213
|
+
return label ? ((this.cores.label = label), this) : this.cores.label;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Return or set the max length of the progressbar
|
|
218
|
+
* @param {number} [value] The value to set the progressBar length to
|
|
219
|
+
*/
|
|
220
|
+
length(value) {
|
|
221
|
+
const len = getPersistentStdout().columns;
|
|
222
|
+
const core = this.cores.length;
|
|
223
|
+
if (value && ['function', 'number'].includes(typeof value)) {
|
|
224
|
+
this.cores.length = value;
|
|
225
|
+
return this;
|
|
226
|
+
}
|
|
227
|
+
let ret;
|
|
228
|
+
if (typeof core === 'function') ret = core(len);
|
|
229
|
+
if (!ret && typeof core === 'number') ret = core < 0 ? len + core : (Math.min(core, 100) / 100) * len;
|
|
230
|
+
if (typeof ret !== 'number') throw new Error('length of the bar must be a valid number');
|
|
231
|
+
return Math.floor(ret);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Return or update the total level of the progressbar
|
|
236
|
+
* @param {number} [value] The number to be added to the total level
|
|
237
|
+
* @param {{}} [template] Template variable values to be included into core options
|
|
238
|
+
*/
|
|
239
|
+
total(value, template) {
|
|
240
|
+
if (value && Number.isFinite(value)) {
|
|
241
|
+
if (value < this.average().completed) throw new Error(`<value> must not be lower than already completed value`);
|
|
242
|
+
this.slots.map((v, r) => ((v.max = ((r = v.max), (v.max * value) / this.total())), (v.done = (v.done * v.max) / r)));
|
|
243
|
+
this.cores.total = Math.floor(value) ? value : 0;
|
|
244
|
+
Object.assign(this.opts.template, template);
|
|
245
|
+
return this;
|
|
246
|
+
}
|
|
247
|
+
return this.cores.total;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Update the progressbar slots with certain percentages
|
|
252
|
+
* - This will top up the current slots with the inputed values as opposed to `this.progress(levels)`
|
|
253
|
+
* - The progressbar would be automatically drawn if [template] is provided
|
|
254
|
+
* @param {Number|Number[]} levels Level(s) to update the slots with
|
|
255
|
+
* @param {{}} [template] Template variable values to use on the drawn progress bar
|
|
256
|
+
*/
|
|
257
|
+
tick(levels, template) {
|
|
258
|
+
levels =
|
|
259
|
+
typeof levels === 'number'
|
|
260
|
+
? Array(this.slots.length).fill(levels)
|
|
261
|
+
: Array.isArray(levels)
|
|
262
|
+
? levels
|
|
263
|
+
: Array(this.slots.length).fill(0);
|
|
264
|
+
let res;
|
|
265
|
+
return this.percentage(
|
|
266
|
+
this.slots.map((slot, index) => ((res = slot.percentage + Math.floor(levels[index])), res > 100 ? 100 : res)),
|
|
267
|
+
template,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Update the progressbar slots with certain values
|
|
273
|
+
* - This will top up the current slots with the inputed values as opposed to `this.progress(levels)`
|
|
274
|
+
* - The progressbar would be automatically drawn if [template] is provided
|
|
275
|
+
* @param {Number|Number[]} levels Level(s) to update the slots with
|
|
276
|
+
* @param {{}} [template] Template variable values to use on the drawn progress bar
|
|
277
|
+
*/
|
|
278
|
+
tickValue(levels, template) {
|
|
279
|
+
levels =
|
|
280
|
+
typeof levels === 'number'
|
|
281
|
+
? Array(this.slots.length).fill(levels)
|
|
282
|
+
: Array.isArray(levels)
|
|
283
|
+
? levels
|
|
284
|
+
: Array(this.slots.length).fill(0);
|
|
285
|
+
return this.value(
|
|
286
|
+
this.slots.map((slot, index) => slot.done + Math.floor(levels[index])),
|
|
287
|
+
template,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Update the progressbar to a percentage
|
|
293
|
+
* - The progressbar would be automatically drawn if [template] is provided
|
|
294
|
+
* @param {number|number[]} index The index at which to replace percentage or an array of slot percentages
|
|
295
|
+
* @param {number} [value] if (index::number) the percentage for the specified index
|
|
296
|
+
* @param {{}} [template] Template variable values to use on the drawn progress bar
|
|
297
|
+
* @example
|
|
298
|
+
* > this.percentage(50, {}) // Update the entire bar to 50%
|
|
299
|
+
* > this.percentage(1, 20, {}) // Set the percentage of the first slot to 20%
|
|
300
|
+
* > this.percentage([40,20,70], {}) // Set the percentage of the slots according to array specification
|
|
301
|
+
*/
|
|
302
|
+
percentage(index, value, template) {
|
|
303
|
+
if (this.isEnded) throw Error('This bar has been ended and is now immutable');
|
|
304
|
+
const [parseType, inferParse] = [
|
|
305
|
+
(input, msg) => {
|
|
306
|
+
if (!(Array.isArray(input) ? input : [input]).every(v => typeof v === 'number')) throw new Error(msg);
|
|
307
|
+
else return input;
|
|
308
|
+
},
|
|
309
|
+
(slots, val, i) => {
|
|
310
|
+
slots[i].done =
|
|
311
|
+
(parseType(
|
|
312
|
+
val <= 0 || Math.floor(val) > 100 || val > 100 ? Math.floor(val) : val,
|
|
313
|
+
`Percentage [${val}] must be in the range [0 < X < 100]`,
|
|
314
|
+
) /
|
|
315
|
+
100) *
|
|
316
|
+
slots[i].max || slots[i].done;
|
|
317
|
+
},
|
|
318
|
+
];
|
|
319
|
+
const bars = [this, ...this.cores.append.reduce((a, v) => (v.inherit && a.push(v.bar), a), [])];
|
|
320
|
+
if (arguments.length > 1 && value && typeof value !== 'object')
|
|
321
|
+
parseType(
|
|
322
|
+
[index, value <= 0 || Math.floor(value) > 100 || value > 100 ? Math.floor(value) : value],
|
|
323
|
+
`<index> and <value> must be of type \`number\`, <number> must be in the range [0 < X < 100]`,
|
|
324
|
+
),
|
|
325
|
+
bars.map(({slots}) => (slots[index].done = (value / slots[index].max) * 100 || slots[index].value));
|
|
326
|
+
else
|
|
327
|
+
(template = value),
|
|
328
|
+
bars.map(({slots}) =>
|
|
329
|
+
Array.isArray(index)
|
|
330
|
+
? index.map((val, i) => inferParse(slots, val, i))
|
|
331
|
+
: slots.map((_slot, i) => inferParse(slots, index, i)),
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
return !template ? this : this.draw(template);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
value(index, value, template) {
|
|
338
|
+
function inferParse(slots, val, i) {
|
|
339
|
+
if (val >= 0)
|
|
340
|
+
if (val <= slots[i].max) slots[i].done = val;
|
|
341
|
+
else throw new Error(`Slot index [${i}] is being updated with more than it can hold [${val} > ${slots[i].max}]`);
|
|
342
|
+
else throw new Error(`Slot index [${i}] is being updated with [${val}] which is less than 0`);
|
|
343
|
+
}
|
|
344
|
+
const bars = [this, ...this.cores.append.reduce((a, v) => (v.inherit && a.push(v.bar), a), [])];
|
|
345
|
+
if (arguments.length > 1 && value && typeof value !== 'object')
|
|
346
|
+
bars.map(({slots}) => (slots[index].done = value || slots[index].done));
|
|
347
|
+
else
|
|
348
|
+
(template = value),
|
|
349
|
+
bars.map(({slots}) =>
|
|
350
|
+
Array.isArray(index)
|
|
351
|
+
? index.map((val, i) => inferParse(slots, val, i))
|
|
352
|
+
: slots.map((_slot, i) => inferParse(slots, index, i)),
|
|
353
|
+
);
|
|
354
|
+
return !template ? this : this.draw(template);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Get an average round up of values in percentage and current progress compatred to the total
|
|
359
|
+
* @param {Number} [fixedPoint] The fixed point at which to approximate average values to
|
|
360
|
+
* @returns {{completed:number, remaining:number, percentage:number}}
|
|
361
|
+
*/
|
|
362
|
+
average(fixedPoint) {
|
|
363
|
+
let completed = this.slots.reduce((a, b) => a + b.done, 0);
|
|
364
|
+
let percentage = (completed / this.total()) * 100;
|
|
365
|
+
let remaining = this.total() - completed;
|
|
366
|
+
[percentage, completed, remaining] = [percentage, completed, remaining].map(value =>
|
|
367
|
+
fixedPoint ? parseFloat(value.toFixed(fixedPoint)) : value,
|
|
368
|
+
);
|
|
369
|
+
return {completed, remaining, percentage};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Draw the progressbar, apply template options to the template
|
|
374
|
+
* @param {String|Object} [template] The template to use on the drawn progress bar or an array of predrawn progressbar from `this.constructBar` like `this.oldBar`
|
|
375
|
+
*/
|
|
376
|
+
draw(template) {
|
|
377
|
+
const result = Array.isArray(template)
|
|
378
|
+
? template
|
|
379
|
+
: [
|
|
380
|
+
...this.constructBar(template).split('\n'),
|
|
381
|
+
...this.cores.append.map(block => block.bar.constructBar(block.inherit ? template : null)),
|
|
382
|
+
];
|
|
383
|
+
this.oldBar = result;
|
|
384
|
+
this.print(`bar${result.length ? `+${result.length - 1}` : ''}`, result.join('\n'));
|
|
385
|
+
this.hasBarredOnce = !0;
|
|
386
|
+
return this;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
#flipperCount = 0;
|
|
390
|
+
|
|
391
|
+
constructBar(template) {
|
|
392
|
+
const forcedFirst = [
|
|
393
|
+
parseBar(this.opts, this.length() - this.opts.bar.header.length, this.average().percentage, this.opts.bar.header),
|
|
394
|
+
];
|
|
395
|
+
const bars = !this.opts.pulsate
|
|
396
|
+
? !this.opts.forceFirst
|
|
397
|
+
? (() => {
|
|
398
|
+
const total = Math.max(
|
|
399
|
+
0,
|
|
400
|
+
this.length() +
|
|
401
|
+
[
|
|
402
|
+
[this.opts.bar.header, 0],
|
|
403
|
+
[this.opts.bar.separator, -1],
|
|
404
|
+
].reduce((a, [v, e]) => a - (v.length ? v.length * this.slots.length + e : 0), 0),
|
|
405
|
+
);
|
|
406
|
+
const slotting = this.slots.map(({level, percentage}) => ({
|
|
407
|
+
portion: Math.floor((level / 100) * total),
|
|
408
|
+
percentage,
|
|
409
|
+
}));
|
|
410
|
+
const slack = total - slotting.reduce((max, {portion}) => max + portion, 0);
|
|
411
|
+
slotting.slice(-1).pop().portion += slack;
|
|
412
|
+
const result = slotting.map(({portion, percentage}) => parseBar(this.opts, portion, percentage));
|
|
413
|
+
return result;
|
|
414
|
+
})()
|
|
415
|
+
: forcedFirst
|
|
416
|
+
: pulsateBar(this, this.cores.pulsateSlots, this.opts.bar.pulsateSkip, this.opts.bar.separator);
|
|
417
|
+
const templateString = Array.isArray(this.opts.template) ? this.opts.template.join('\n') : this.opts.template;
|
|
418
|
+
const average = this.average();
|
|
419
|
+
return this.parseString(templateString, {
|
|
420
|
+
bar: bars.join(`:{color:bar:separator}${this.opts.bar.separator}:{color:close}:{bgcolor:close}`),
|
|
421
|
+
'bar:complete': forcedFirst.join(''),
|
|
422
|
+
label: this.label(),
|
|
423
|
+
total: this.total(),
|
|
424
|
+
flipper: this.opts.flipper[(this.#flipperCount += 1) % this.opts.flipper.length],
|
|
425
|
+
...average,
|
|
426
|
+
percentage: average.percentage.toFixed(0),
|
|
427
|
+
...template,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
parseString(str, template) {
|
|
432
|
+
const variables = {...(template = {...this.opts.variables, ...template})};
|
|
433
|
+
Object.entries(this.opts.variables).forEach(
|
|
434
|
+
([spec, content]) => typeof content === 'function' && content !== template[spec] && (template[spec] = content(template)),
|
|
435
|
+
);
|
|
436
|
+
return stringd(stringd(str, template), variables);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Print a message after a bar `draw` interrupt
|
|
441
|
+
* @param {'bar'|'end'} type Type of bar print or the first part of the printer
|
|
442
|
+
* @param {any[]} content The contents to be formatted
|
|
443
|
+
*/
|
|
444
|
+
print(type, ...content) {
|
|
445
|
+
const self = this;
|
|
446
|
+
type = format(type);
|
|
447
|
+
if (!self.cores.stdout.isTTY) throw Error("Can't draw or print progressBar interrupts with piped output");
|
|
448
|
+
const cleanWrite = function cleanWrite(arr, dontClean, addons = 0, ending = false, normie = false) {
|
|
449
|
+
if (!dontClean) {
|
|
450
|
+
// check https://github.com/freeall/single-line-log/blob/515b3b99b699396c2ad5f937e4b490b6f9fbff0e/index.js#L1-L3
|
|
451
|
+
self.cores.stdout.moveCursor(0, -addons);
|
|
452
|
+
self.cores.stdout.cursorTo(0);
|
|
453
|
+
self.cores.stdout.clearScreenDown();
|
|
454
|
+
}
|
|
455
|
+
(normie ? process.stdout : self.cores.stdout).write(
|
|
456
|
+
`${dontClean && ending && addons ? '\n' : ''}${self
|
|
457
|
+
.parseString(format(...arr))
|
|
458
|
+
// eslint-disable-next-line no-control-regex
|
|
459
|
+
.replace(self.opts.bar.colorize ? '' : /\x1b\[\d+m/g, '')}`,
|
|
460
|
+
);
|
|
461
|
+
};
|
|
462
|
+
let addonPack;
|
|
463
|
+
const addons = this.hasBarredOnce && !this.justLogged ? this.oldBar.length - 1 : 0;
|
|
464
|
+
this.justLogged =
|
|
465
|
+
type === 'bar' && content.length === 1
|
|
466
|
+
? !!cleanWrite(content, this.justLogged, addons)
|
|
467
|
+
: (addonPack = type.match(/^bar\+(\d+)/)) !== null
|
|
468
|
+
? !!cleanWrite(content, this.justLogged, this.hasBarredOnce ? addonPack[1] : addons)
|
|
469
|
+
: type === 'end'
|
|
470
|
+
? !!cleanWrite(content, !this.opts.clean, addons, true, true)
|
|
471
|
+
: !cleanWrite(
|
|
472
|
+
[(type.startsWith(':') && `${type.slice(1)}`) || type, ...content, '\n'],
|
|
473
|
+
this.justLogged,
|
|
474
|
+
addons,
|
|
475
|
+
false,
|
|
476
|
+
true,
|
|
477
|
+
);
|
|
478
|
+
if (this.justLogged && this.hasBarredOnce) this.draw(this.oldBar);
|
|
479
|
+
return this;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* End the bar irrespective of progress, optionally with a message.
|
|
484
|
+
* @param {any[]} [message] The message to be printed to `stdout` right before ending the bar
|
|
485
|
+
*/
|
|
486
|
+
end(...message) {
|
|
487
|
+
if (!this.isEnded) {
|
|
488
|
+
if (message.length) this.print('end', ...message);
|
|
489
|
+
this.isEnded = !0;
|
|
490
|
+
}
|
|
491
|
+
return this;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Drain all slots in the progressbar to 0
|
|
496
|
+
*/
|
|
497
|
+
drain(slotID) {
|
|
498
|
+
if (slotID) this.slots[slotID].done = 0;
|
|
499
|
+
else this.slots.map(slot => ((slot.done = 0), slot));
|
|
500
|
+
return this;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Drop the chain, return void
|
|
505
|
+
*/
|
|
506
|
+
drop() {}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Append the specified bar after `this`
|
|
510
|
+
* @param {ProgressBar} bar The bar to be appended
|
|
511
|
+
* @param {Boolean} inherit Whether or not to inherit bar templates from `this`
|
|
512
|
+
*/
|
|
513
|
+
append(bar, inherit = !1) {
|
|
514
|
+
if (!ProgressBar.isBar(bar) && !bar.opts.template) throw Error('The Parameter <bar> is not a progressbar or a hanger');
|
|
515
|
+
this.cores.append.push({bar, inherit});
|
|
516
|
+
bar.cores.isKid = !0;
|
|
517
|
+
return this;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Check if the bar or a slot is complete
|
|
522
|
+
* @param {Number} [slot] The slot to be checked for completion
|
|
523
|
+
*/
|
|
524
|
+
isComplete(slot) {
|
|
525
|
+
if (slot && !this.slots[slot]) throw Error(`Value in <slot>:${slot} has no slot reference`);
|
|
526
|
+
return slot ? this.slots[slot].max === this.slots[slot].done : this.average().remaining === 0;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Find out the progressbar is appended to another
|
|
531
|
+
*/
|
|
532
|
+
get isChild() {
|
|
533
|
+
return !!this.cores.isKid;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Check if the progressbar is active.
|
|
538
|
+
* - Activity is determined when the progressbar is not complete
|
|
539
|
+
*/
|
|
540
|
+
get isActive() {
|
|
541
|
+
return !this.isComplete();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Check if the bar is fresh.
|
|
546
|
+
* - Equivalent of `this.isActive && !this.average().value`
|
|
547
|
+
*/
|
|
548
|
+
get isFresh() {
|
|
549
|
+
return this.isActive && !this.average().completed;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Check if the provided progressbar is an instance of `this`
|
|
554
|
+
* @param {ProgressBar} bar The progressbar to be checked
|
|
555
|
+
*/
|
|
556
|
+
static isBar(bar) {
|
|
557
|
+
return bar instanceof ProgressBar;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Check if the provided progressbar is an stream instance of `this`
|
|
562
|
+
* @param {ProgressBar} bar The progressbar to be checked
|
|
563
|
+
*/
|
|
564
|
+
static isBarStream(barStream) {
|
|
565
|
+
return (
|
|
566
|
+
!!barStream &&
|
|
567
|
+
ProgressBar.isBar(barStream.bar) &&
|
|
568
|
+
barStream instanceof EventEmitter &&
|
|
569
|
+
[barStream.read, barStream.write].every(slot => typeof slot === 'function')
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Check if the provided object is a stream progressbar generator
|
|
575
|
+
* @param {any} bar The progressbar to be checked
|
|
576
|
+
*/
|
|
577
|
+
static isBarGen(barStream) {
|
|
578
|
+
return (
|
|
579
|
+
!!barStream && ProgressBar.isBar(barStream.bar) && barStream instanceof EventEmitter && typeof barStream.next === 'function'
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Check if the provided object is related to any instances created by this script
|
|
585
|
+
* @param {any} bar The progressbar to be checked
|
|
586
|
+
*/
|
|
587
|
+
static isBarRelated(barStream) {
|
|
588
|
+
return ProgressBar.isBar(barStream) || ProgressBar.isBarStream(barStream) || ProgressBar.isBarGen(barStream);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Calculate slot levels by number of slots
|
|
593
|
+
* @param {number} len Each slot length, inferrable if ratio doesn't make 100 or pop-able if over 100
|
|
594
|
+
*/
|
|
595
|
+
static slotsByCount(len) {
|
|
596
|
+
return size => padRatio(Array(len).fill(size / len), size, false);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Calculate slot levels by size
|
|
601
|
+
* @param {number} size Maximum possible total size
|
|
602
|
+
* @param {number[]} percentages Each slot length, inferrable if ratio doesn't make 100 or pop-able if over 100
|
|
603
|
+
*/
|
|
604
|
+
static slotsByPercentage(percentages) {
|
|
605
|
+
return size =>
|
|
606
|
+
padRatio(
|
|
607
|
+
percentages.map(_size => (_size / 100) * size),
|
|
608
|
+
size,
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Create a streamified bar for use with generators
|
|
614
|
+
* @param {Number} total Total attainable value of bytes in <N>
|
|
615
|
+
* @param {Number|Number[]} slots Number of slots in <%>
|
|
616
|
+
* @param {_streamOpts} [opts] Options for the bar
|
|
617
|
+
* @param {(bar:ProgressBar,slotLevel:Number,template:{}) => void} [actor] The actor for every yield
|
|
618
|
+
*/
|
|
619
|
+
static stream(total, slots, opts, actor) {
|
|
620
|
+
// if (typeof slots === 'function') [slots, opts, actor] = [, {}, slots];
|
|
621
|
+
if (typeof opts === 'function') [opts, actor] = [{}, opts];
|
|
622
|
+
if ((typeof slots === 'object') & !Array.isArray(slots)) [slots, opts] = [, slots];
|
|
623
|
+
opts = {
|
|
624
|
+
...(total === Infinity
|
|
625
|
+
? {
|
|
626
|
+
pulsate: !0,
|
|
627
|
+
template: ':{tag}[:{bar}] [:{flipper}] :{label} (:{slot:runtime}) :{slot:size}',
|
|
628
|
+
}
|
|
629
|
+
: {}),
|
|
630
|
+
...opts,
|
|
631
|
+
...(slots === Infinity ? ((slots = total), {progress: {infinite: !0}}) : {}),
|
|
632
|
+
};
|
|
633
|
+
const progressBar = new ProgressBar(total, slots, opts);
|
|
634
|
+
return ProgressBar.streamify(progressBar, actor, opts);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Streamify a bar for use with generators
|
|
639
|
+
* @param {ProgressBar} bar The bar to be used
|
|
640
|
+
* @param {(bar:ProgressBar, slotLevel:Number, template:() => {completed:number, remaining:number, percentage:number}) => void} [actor] The actor for every yield
|
|
641
|
+
* @param {_streamOpts} [opts] Options for the bar
|
|
642
|
+
* @returns {{next(size: Number, opts: _streamOpts):NodeJS.WritableStream, bar:ProgressBar}} Returned function from `ProgressBar.streamify`
|
|
643
|
+
*/
|
|
644
|
+
static streamify(bar, actor, opts) {
|
|
645
|
+
if (typeof actor === 'object') [actor, opts] = [, actor];
|
|
646
|
+
bar.opts = merge({}, bar.opts, streamOpts, opts);
|
|
647
|
+
bar.opts.template = opts.template || streamOpts.template;
|
|
648
|
+
const [pulsateSlots, pulsateSkips] = [
|
|
649
|
+
[
|
|
650
|
+
[0, 0],
|
|
651
|
+
[bar.opts.progress.pulsateLength || bar.opts.bar.pulsateLength, 100],
|
|
652
|
+
].map(([level, value]) => ({
|
|
653
|
+
level,
|
|
654
|
+
value,
|
|
655
|
+
})),
|
|
656
|
+
bar.opts.progress.pulsateSkip || bar.opts.bar.pulsateSkip,
|
|
657
|
+
];
|
|
658
|
+
let defaultUnit;
|
|
659
|
+
const progressGen = new ProgressGen();
|
|
660
|
+
const buildBytesWith =
|
|
661
|
+
(bytes, byteOpts = {}) =>
|
|
662
|
+
(props, data) =>
|
|
663
|
+
xbytes.createRelativeSizer((data && (data.args[0] || data.matched.unit)) || defaultUnit, {
|
|
664
|
+
iec: data && 'iec' in data.matched ? data.matched.iec === 'true' : byteOpts.iec || !1,
|
|
665
|
+
bits: data && 'bits' in data.matched ? data.matched.bits === 'true' : byteOpts.bits || !1,
|
|
666
|
+
fixed: data && 'fixed' in data.matched ? parseInt(data.matched.fixed, 10) : byteOpts.fixed || 2,
|
|
667
|
+
short: data && 'short' in data.matched ? data.matched.short === 'true' : byteOpts.short || !0,
|
|
668
|
+
space: data && 'space' in data.matched ? data.matched.space === 'true' : byteOpts.space || !0,
|
|
669
|
+
sticky: data && 'sticky' in data.matched ? data.matched.sticky === 'true' : byteOpts.sticky || !1,
|
|
670
|
+
})(typeof bytes === 'function' ? bytes(props) : bytes);
|
|
671
|
+
const buildSpeedWith = speed => (_, data) =>
|
|
672
|
+
`${buildBytesWith(speed, {iec: !1, bits: !0, fixed: 2, short: !0, space: !1, sticky: !1})(_, data)}${
|
|
673
|
+
(data && data.matched.metric) || 'ps'
|
|
674
|
+
}`;
|
|
675
|
+
const totalSpeed = speedometer(5000);
|
|
676
|
+
const streamGenerator = bar.slotStreamify((slotIndex, total, infinite) => {
|
|
677
|
+
const max = !infinite ? Math.round((bar.slots[slotIndex].level / 100) * bar.total()) : Infinity;
|
|
678
|
+
total = typeof total === 'function' ? total(bar) : total || max;
|
|
679
|
+
if (!infinite && total > max)
|
|
680
|
+
throw Error(
|
|
681
|
+
`<size> slot must not be greater than maximum possible size for the slot [${max}], consider using infinite slots`,
|
|
682
|
+
);
|
|
683
|
+
const through = progressStream({length: total, ...bar.opts.progress})
|
|
684
|
+
.on('progress', progress => {
|
|
685
|
+
const speed = totalSpeed(progress.delta);
|
|
686
|
+
if (bar.isEnded) {
|
|
687
|
+
through.emit('error', Error('The <bar> being used has been ended'));
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
(actor || ((_bar, args, template) => _bar.value(...args).draw(template)))(
|
|
691
|
+
bar,
|
|
692
|
+
bar.opts.progress.infinite ? [progress.delta + bar.average().completed] : [slotIndex, progress.transferred],
|
|
693
|
+
{
|
|
694
|
+
eta: () => xprettyMs((1000 * bar.average().remaining) / speed),
|
|
695
|
+
size: buildBytesWith(() => bar.average().completed),
|
|
696
|
+
speed: buildSpeedWith(speed),
|
|
697
|
+
'speed:raw': speed,
|
|
698
|
+
'slot:speed': buildSpeedWith(progress.speed),
|
|
699
|
+
'slot:speed:raw': progress.speed,
|
|
700
|
+
progress,
|
|
701
|
+
'eta:raw': () => Math.round(bar.average().remaining / speed),
|
|
702
|
+
'slot:bar': () =>
|
|
703
|
+
bar.opts.progress.pulsate
|
|
704
|
+
? pulsateBar(bar, pulsateSlots, pulsateSkips, bar.opts.bar.separator).join('')
|
|
705
|
+
: (() => {
|
|
706
|
+
const header = bar.opts.bar['slot:header'] || bar.opts.bar.header;
|
|
707
|
+
const filler = bar.opts.bar['slot:filler'] || bar.opts.bar.filler;
|
|
708
|
+
const blank = bar.opts.bar['slot:blank'] || bar.opts.bar.blank;
|
|
709
|
+
return parseBar(
|
|
710
|
+
merge({}, bar.opts, {bar: {header, filler, blank}}),
|
|
711
|
+
bar.length() - (header || '').length,
|
|
712
|
+
progress.percentage,
|
|
713
|
+
);
|
|
714
|
+
})(),
|
|
715
|
+
'slot:eta': xprettyMs(1000 * progress.eta),
|
|
716
|
+
'slot:eta:raw': progress.eta,
|
|
717
|
+
'slot:size': buildBytesWith(progress.transferred),
|
|
718
|
+
'slot:size:raw': progress.transferred,
|
|
719
|
+
'size:total': buildBytesWith(() => bar.total()),
|
|
720
|
+
'slot:runtime': xprettyMs(1000 * progress.runtime),
|
|
721
|
+
'slot:runtime:raw': progress.runtime,
|
|
722
|
+
'slot:percentage': progress.percentage.toFixed(0),
|
|
723
|
+
'slot:size:total': buildBytesWith(progress.length),
|
|
724
|
+
'slot:size:total:raw': progress.length,
|
|
725
|
+
},
|
|
726
|
+
);
|
|
727
|
+
[through, progressGen].map(emitter => emitter.emit('tick', {progress, bar}));
|
|
728
|
+
})
|
|
729
|
+
.on('end', () => (bar.isComplete() ? progressGen.emit('complete', bar) : null, progressGen.emit('end', bar)))
|
|
730
|
+
.once('error', error => bar.end(`:{color(red)}[Bar Error]:{color:close} An Error occurred\n${error}`));
|
|
731
|
+
// through.emit = (tr => (...args) => (bar.print('tr>', args[0]), tr.call(through, ...args)))(through.emit)
|
|
732
|
+
return (through.bar = bar), through;
|
|
733
|
+
});
|
|
734
|
+
return Object.assign(progressGen, {
|
|
735
|
+
/**
|
|
736
|
+
* Get the next PassThrough instance
|
|
737
|
+
* @param {number} [size] Size for the next chunk (Omittable)
|
|
738
|
+
* @param {_streamOpts} [options] Bar options
|
|
739
|
+
* @returns {NodeJS.WritableStream} Returned function from `ProgressBar.streamify`
|
|
740
|
+
*/
|
|
741
|
+
next: (size, options) => streamGenerator.next([...(typeof size === 'number' ? [size, options] : [, size])]).value,
|
|
742
|
+
/**
|
|
743
|
+
* End the bar irrespective of progress, optionally with a message.
|
|
744
|
+
* @param {any[]} [message] The message to be printed to `stdout` right before ending the bar
|
|
745
|
+
* @returns {ProgressBar} The ProgressBar
|
|
746
|
+
*/
|
|
747
|
+
end: (...message) => bar.end(...message),
|
|
748
|
+
/**
|
|
749
|
+
* Print a message after a bar `draw` interrupt
|
|
750
|
+
* @param {any[]} message The message to printed
|
|
751
|
+
* @returns {ProgressBar} The ProgressBar
|
|
752
|
+
*/
|
|
753
|
+
print: (...message) => bar.print(...message),
|
|
754
|
+
/**
|
|
755
|
+
* Set the default unit
|
|
756
|
+
* @param {xbytes.AllUnitStacks} unit Preferred unit representation
|
|
757
|
+
* @returns {ProgressBar} The ProgressBar
|
|
758
|
+
*/
|
|
759
|
+
defaultUnit(unit) {
|
|
760
|
+
if (!xbytes.isUnit(unit)) throw new Error(`Invalid ByteString unit: ${unit}`);
|
|
761
|
+
defaultUnit = unit;
|
|
762
|
+
return this;
|
|
763
|
+
},
|
|
764
|
+
/**
|
|
765
|
+
* The ProgressBar Instance
|
|
766
|
+
* @type {ProgressBar} The ProgresBar
|
|
767
|
+
*/
|
|
768
|
+
bar: streamGenerator.next().value,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Prepare a raw generator for use
|
|
774
|
+
* @param {(slots:Number, total?:Number) => String} actor The performing function
|
|
775
|
+
* @returns {Generator} New ProgressBar Generator
|
|
776
|
+
* @yields The through instance or a cache model of the ProgressBar
|
|
777
|
+
*/
|
|
778
|
+
*slotStreamify(actor, args) {
|
|
779
|
+
for (let level = -1; (level += 1) <= (this.opts.progress.infinite ? Infinity : this.slots.length); )
|
|
780
|
+
args = yield !level
|
|
781
|
+
? this
|
|
782
|
+
: (merge(this.opts, args[1]), actor(Math.floor(level - 1), args[0], this.opts.progress.infinite));
|
|
783
|
+
}
|
|
784
|
+
}
|