xprogress 0.18.0 → 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/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
+ }