index.js
function CLIProgress(text, settings) {
	var progress = this;

	progress.settings = {
		text: '{{current}} / {{max}} [{{bar}}] {{percent}}%',
		current: 0,
		max: 100,
		stream: process.stderr,
		render: function(line) {
			progress.settings.stream.cursorTo(0);
			progress.settings.stream.clearLine(1);
			progress.settings.stream.write(line);
		},
		clear: function() {
			progress.settings.stream.clearLine(0);
			progress.settings.stream.cursorTo(0);
		},
		width: windowSize.width,
		completeChar: '=',
		incompleteChar: ' ',
		percent: function() {
			var pText = Math.round(progress.settings.current / progress.settings.max * 100).toString();
			var strLen = stringLength(pText);
			if (strLen > 3) return pText; // Already over max length
			return padStart(pText, 3, ' '); // Left pad with spaces
		},
		throttle: 50,
		throttleSync: false,

		startTime: Date.now(),
		etaFormat: 'auto',
		eta: function() {
			var remaining = progress.settings.current >= progress.settings.total ? 0 : (Date.now() - progress.settings.startTime) * (progress.settings.max / progress.settings.current - 1);

			if (isNaN(remaining) || !isFinite(remaining)) remaining = 0;

			// ETA formatting {{{
			switch (progress.settings.etaFormat) {
				case 'auto':
					var hour = Math.floor(remaining / (60 * 60 * 1000));
					remaining = remaining % (60*60*1000);
					var min = Math.floor(remaining / (60 * 1000));
					remaining = remaining % (60*1000);
					var sec = Math.floor(remaining / (1000));
					if (hour > 0) {
						return hour + ':' + padStart(min.toString(), 2, '0') + ':' + padStart(sec.toString(), 2, '0');
					} else if (min > 0) {
						return min + ':' + padStart(sec.toString(), 2, '0');
					} else {
						return (remaining / 1000).toFixed(1);
					}
					break;
				case 'H:M:s.S':
				case 'H:M:s':
					var hour = Math.floor(remaining / (60 * 60 * 1000));
					remaining = remaining % (60*60*1000);
					var min = Math.floor(remaining / (60 * 1000));
					remaining = remaining % (60*1000);
					var sec = Math.floor(remaining / (1000));
					remaining = remaining % 1000;
					var fraction = '.' + (remaining / 1000).toFixed(1).substr(2);
					return hour + ':' + padStart(min.toString(), 2, '0') + ':' + padStart(sec.toString(), 2, '0') + (progress.settings.etaFormat.endsWith('.S') ? fraction : '');
					break;
				case 'M:s.S':
				case 'M:s':
					var min = Math.floor(remaining / (60 * 1000));
					remaining = remaining % (60*1000);
					var sec = Math.floor(remaining / (1000));
					remaining = remaining % 1000;
					var fraction = '.' + (remaining / 1000).toFixed(1).substr(2);
					return min + ':' + padStart(sec.toString(), 2, '0') + (progress.settings.etaFormat.endsWith('.S') ? fraction : '');
					break;
				case 's':
				case 's.S':
					return (remaining / 1000).toFixed(progress.settings.etaFormat.endsWith('.S') ? 1 : 0);
			}
			// }}}
		},

		spinnerTheme: 'dots',
		spinnerFrame: 0,
		spinner: function() {
			var spinnerFrames = cliSpinners[progress.settings.spinnerTheme];
			if (!spinnerFrames) throw new Error('Spinner theme not found: "' + progress.settings.spinnerTheme + '"');
			if (++progress.settings.spinnerFrame >= spinnerFrames.frames.length) progress.settings.spinnerFrame = 0;
			return spinnerFrames.frames[progress.settings.spinnerFrame];
		},
	};

	// Map all color / style functions from chalk into the settings object (e.g. bold => chalk.bold closure) {{{
	Object.keys(colors.styles).forEach(function(style) {
		progress.settings[style] = function() {
			return function(text, render) {
				return colors[style](render(text));
			};
		};
	});
	// }}}

	/**
	* Return the output that will be sent to the output stream
	* return {string} The raw console-suitable output
	*/
	progress.format = function() {
		var text = mustache.render(progress.settings.text, progress.settings);
		// Rendering a bar? {{{
		if (text.indexOf('[[BAR') > -1) {
			var maxBarWidth = (windowSize.width || 80) - stringLength(text);
			if (!progress.settings.max) progress.settings.max = progress.settings.current > 0 ? progress.settings.current : 0;
			var barCompleteWidth = Math.round(progress.settings.current / progress.settings.max * maxBarWidth);
			if (barCompleteWidth == NaN || !barCompleteWidth) barCompleteWidth = 0;

			var completeBits = Array(barCompleteWidth).join(progress.settings.completeChar);
			var incompleteBits = maxBarWidth - barCompleteWidth > 0 ? Array(maxBarWidth - barCompleteWidth).join(progress.settings.incompleteChar) : '';

			text = text
				.replace(/\[\[BAR\]\]/g, completeBits + incompleteBits)
				.replace(/\[\[BAR.complete\]\]/ig, completeBits)
				.replace(/\[\[BAR.incomplete\]\]/ig, incompleteBits)
		}
		// }}}
		return text;
	};


	/**
	* Holder for the throttle timeout handle
	* This will be the setTimeout() response if we are throttling
	* @var {Object}
	*/
	progress.throttleHandle;


	/**
	* Update and re-render the progress bar
	* This is really just a shortcut for .set() + .render(progress.format())
	* @params {Object|number} Either set a number of properties or the 'current' value if passed a number
	* @return {Object} This object instance
	* @see set()
	*/
	progress.update = function(val) {
		progress.set(val);

		if (progress.settings.throttle && progress.settings.throttleSync) { // Employ sync throttling method (count from last update)
			if (!progress.lastUpdate || Date.now() >= progress.lastUpdate + progress.settings.throttle) {
				progress.updateNow(); // Only allow redraw if we're within the update window
			}
		} else if (progress.settings.throttle) { // Employ async throttling method (setTimeout)
			if (!progress.throttleHandle) progress.throttleHandle = setTimeout(progress.updateNow, progress.settings.throttle);
		} else { // Not using throttle anyway
			progress.updateNow();
		}

		return progress;
	};


	/**
	* The timestamp in milliseconds of the last update event
	* This is used when employing the synchronous throttling method
	* @var {date}
	*/
	progress.lastUpdate;


	/**
	* Actual updater
	* This is hidden behind the update throttler so its not called too many times in one cycle
	*/
	progress.updateNow = function() {
		progress.lastUpdate = Date.now();
		progress.settings.render(progress.format());
		clearTimeout(progress.throttleHandle);
		progress.throttleHandle = null;
	};


	/**
	* Remove the bar from the screen
	* @return {Object} This object instance
	*/
	progress.remove = function() {
		clearTimeout(progress.throttleHandle); // Release any throttled redraws that may be queued
		progress.throttleHandle = null;
		progress.settings.clear();
		return progress;
	};


	/**
	* Set one or more options or tokens
	* NOTE: Unlike .set() this function DOES NOT refresh the progress bar
	* @params {Object|number} Either set a number of properties or the 'current' value if passed a number
	* @return {Object} This object instance
	* @see set()
	*/
	progress.set = function(val) {
		if (!val) return;
		if (typeof val == 'number') {
			progress.settings.current = val;
		} else {
			for (var k in val) {
				progress.settings[k] = val[k];
			}
			if (val.text) { // Setting the formatting text?
				progress.settings.text = progress.settings.text.replace(/\{\{bar(.*?)\}\}/g, '[[BAR$1]]'); // Remove mustache stuff as we have to calculate the width post-render
				mustache.parse(progress.settings.text);
			}
		}
		return progress;
	};


	// Load initial settings {{{
	if (typeof text == 'string') {
		progress.set({text: text});
	} else {
		progress.set(text);
	}
	if (typeof settings == 'object') progress.set(settings);
	// }}}
}
function CLIProgress(text, settings) {
	var progress = this;

	progress.settings = {
		text: '{{current}} / {{max}} [{{bar}}] {{percent}}%',
		current: 0,
		max: 100,
		stream: process.stderr,
		render: function(line) {
			progress.settings.stream.cursorTo(0);
			progress.settings.stream.clearLine(1);
			progress.settings.stream.write(line);
		},
		clear: function() {
			progress.settings.stream.clearLine(0);
			progress.settings.stream.cursorTo(0);
		},
		width: windowSize.width,
		completeChar: '=',
		incompleteChar: ' ',
		percent: function() {
			var pText = Math.round(progress.settings.current / progress.settings.max * 100).toString();
			var strLen = stringLength(pText);
			if (strLen > 3) return pText; // Already over max length
			return padStart(pText, 3, ' '); // Left pad with spaces
		},
		throttle: 50,
		throttleSync: false,

		startTime: Date.now(),
		etaFormat: 'auto',
		eta: function() {
			var remaining = progress.settings.current >= progress.settings.total ? 0 : (Date.now() - progress.settings.startTime) * (progress.settings.max / progress.settings.current - 1);

			if (isNaN(remaining) || !isFinite(remaining)) remaining = 0;

			// ETA formatting {{{
			switch (progress.settings.etaFormat) {
				case 'auto':
					var hour = Math.floor(remaining / (60 * 60 * 1000));
					remaining = remaining % (60*60*1000);
					var min = Math.floor(remaining / (60 * 1000));
					remaining = remaining % (60*1000);
					var sec = Math.floor(remaining / (1000));
					if (hour > 0) {
						return hour + ':' + padStart(min.toString(), 2, '0') + ':' + padStart(sec.toString(), 2, '0');
					} else if (min > 0) {
						return min + ':' + padStart(sec.toString(), 2, '0');
					} else {
						return (remaining / 1000).toFixed(1);
					}
					break;
				case 'H:M:s.S':
				case 'H:M:s':
					var hour = Math.floor(remaining / (60 * 60 * 1000));
					remaining = remaining % (60*60*1000);
					var min = Math.floor(remaining / (60 * 1000));
					remaining = remaining % (60*1000);
					var sec = Math.floor(remaining / (1000));
					remaining = remaining % 1000;
					var fraction = '.' + (remaining / 1000).toFixed(1).substr(2);
					return hour + ':' + padStart(min.toString(), 2, '0') + ':' + padStart(sec.toString(), 2, '0') + (progress.settings.etaFormat.endsWith('.S') ? fraction : '');
					break;
				case 'M:s.S':
				case 'M:s':
					var min = Math.floor(remaining / (60 * 1000));
					remaining = remaining % (60*1000);
					var sec = Math.floor(remaining / (1000));
					remaining = remaining % 1000;
					var fraction = '.' + (remaining / 1000).toFixed(1).substr(2);
					return min + ':' + padStart(sec.toString(), 2, '0') + (progress.settings.etaFormat.endsWith('.S') ? fraction : '');
					break;
				case 's':
				case 's.S':
					return (remaining / 1000).toFixed(progress.settings.etaFormat.endsWith('.S') ? 1 : 0);
			}
			// }}}
		},

		spinnerTheme: 'dots',
		spinnerFrame: 0,
		spinner: function() {
			var spinnerFrames = cliSpinners[progress.settings.spinnerTheme];
			if (!spinnerFrames) throw new Error('Spinner theme not found: "' + progress.settings.spinnerTheme + '"');
			if (++progress.settings.spinnerFrame >= spinnerFrames.frames.length) progress.settings.spinnerFrame = 0;
			return spinnerFrames.frames[progress.settings.spinnerFrame];
		},
	};

	// Map all color / style functions from chalk into the settings object (e.g. bold => chalk.bold closure) {{{
	Object.keys(colors.styles).forEach(function(style) {
		progress.settings[style] = function() {
			return function(text, render) {
				return colors[style](render(text));
			};
		};
	});
	// }}}

	/**
	* Return the output that will be sent to the output stream
	* return {string} The raw console-suitable output
	*/
	progress.format = function() {
		var text = mustache.render(progress.settings.text, progress.settings);
		// Rendering a bar? {{{
		if (text.indexOf('[[BAR') > -1) {
			var maxBarWidth = (windowSize.width || 80) - stringLength(text);
			if (!progress.settings.max) progress.settings.max = progress.settings.current > 0 ? progress.settings.current : 0;
			var barCompleteWidth = Math.round(progress.settings.current / progress.settings.max * maxBarWidth);
			if (barCompleteWidth == NaN || !barCompleteWidth) barCompleteWidth = 0;

			var completeBits = Array(barCompleteWidth).join(progress.settings.completeChar);
			var incompleteBits = maxBarWidth - barCompleteWidth > 0 ? Array(maxBarWidth - barCompleteWidth).join(progress.settings.incompleteChar) : '';

			text = text
				.replace(/\[\[BAR\]\]/g, completeBits + incompleteBits)
				.replace(/\[\[BAR.complete\]\]/ig, completeBits)
				.replace(/\[\[BAR.incomplete\]\]/ig, incompleteBits)
		}
		// }}}
		return text;
	};


	/**
	* Holder for the throttle timeout handle
	* This will be the setTimeout() response if we are throttling
	* @var {Object}
	*/
	progress.throttleHandle;


	/**
	* Update and re-render the progress bar
	* This is really just a shortcut for .set() + .render(progress.format())
	* @params {Object|number} Either set a number of properties or the 'current' value if passed a number
	* @return {Object} This object instance
	* @see set()
	*/
	progress.update = function(val) {
		progress.set(val);

		if (progress.settings.throttle && progress.settings.throttleSync) { // Employ sync throttling method (count from last update)
			if (!progress.lastUpdate || Date.now() >= progress.lastUpdate + progress.settings.throttle) {
				progress.updateNow(); // Only allow redraw if we're within the update window
			}
		} else if (progress.settings.throttle) { // Employ async throttling method (setTimeout)
			if (!progress.throttleHandle) progress.throttleHandle = setTimeout(progress.updateNow, progress.settings.throttle);
		} else { // Not using throttle anyway
			progress.updateNow();
		}

		return progress;
	};


	/**
	* The timestamp in milliseconds of the last update event
	* This is used when employing the synchronous throttling method
	* @var {date}
	*/
	progress.lastUpdate;


	/**
	* Actual updater
	* This is hidden behind the update throttler so its not called too many times in one cycle
	*/
	progress.updateNow = function() {
		progress.lastUpdate = Date.now();
		progress.settings.render(progress.format());
		clearTimeout(progress.throttleHandle);
		progress.throttleHandle = null;
	};


	/**
	* Remove the bar from the screen
	* @return {Object} This object instance
	*/
	progress.remove = function() {
		clearTimeout(progress.throttleHandle); // Release any throttled redraws that may be queued
		progress.throttleHandle = null;
		progress.settings.clear();
		return progress;
	};


	/**
	* Set one or more options or tokens
	* NOTE: Unlike .set() this function DOES NOT refresh the progress bar
	* @params {Object|number} Either set a number of properties or the 'current' value if passed a number
	* @return {Object} This object instance
	* @see set()
	*/
	progress.set = function(val) {
		if (!val) return;
		if (typeof val == 'number') {
			progress.settings.current = val;
		} else {
			for (var k in val) {
				progress.settings[k] = val[k];
			}
			if (val.text) { // Setting the formatting text?
				progress.settings.text = progress.settings.text.replace(/\{\{bar(.*?)\}\}/g, '[[BAR$1]]'); // Remove mustache stuff as we have to calculate the width post-render
				mustache.parse(progress.settings.text);
			}
		}
		return progress;
	};


	// Load initial settings {{{
	if (typeof text == 'string') {
		progress.set({text: text});
	} else {
		progress.set(text);
	}
	if (typeof settings == 'object') progress.set(settings);
	// }}}
}
function CLIProgress ( text , settings ) { var progress = this ; progress . settings = { text : '{{current}} / {{max}} [{{bar}}] {{percent}}%' , current : 0 , max : 100 , stream : process . stderr , render : function ( line ) { progress . settings . stream . cursorTo ( 0 ) ; progress . settings . stream . clearLine ( 1 ) ; progress . settings . stream . write ( line ) ; } , clear : function ( ) { progress . settings . stream . clearLine ( 0 ) ; progress . settings . stream . cursorTo ( 0 ) ; } , width : windowSize . width , completeChar : '=' , incompleteChar : ' ' , percent : function ( ) { var pText = Math . round ( progress . settings . current / progress . settings . max * 100 ) . toString ( ) ; var strLen = stringLength ( pText ) ; if ( strLen > 3 ) return pText ; // Already over max length return padStart ( pText , 3 , ' ' ) ; // Left pad with spaces } , throttle : 50 , throttleSync : false , startTime : Date . now ( ) , etaFormat : 'auto' , eta : function ( ) { var remaining = progress . settings . current >= progress . settings . total ? 0 : ( Date . now ( ) - progress . settings . startTime ) * ( progress . settings . max / progress . settings . current - 1 ) ; if ( isNaN ( remaining ) || ! isFinite ( remaining ) ) remaining = 0 ; // ETA formatting {{{ switch ( progress . settings . etaFormat ) { case 'auto' : var hour = Math . floor ( remaining / ( 60 * 60 * 1000 ) ) ; remaining = remaining % ( 60 * 60 * 1000 ) ; var min = Math . floor ( remaining / ( 60 * 1000 ) ) ; remaining = remaining % ( 60 * 1000 ) ; var sec = Math . floor ( remaining / ( 1000 ) ) ; if ( hour > 0 ) { return hour + ':' + padStart ( min . toString ( ) , 2 , '0' ) + ':' + padStart ( sec . toString ( ) , 2 , '0' ) ; } else if ( min > 0 ) { return min + ':' + padStart ( sec . toString ( ) , 2 , '0' ) ; } else { return ( remaining / 1000 ) . toFixed ( 1 ) ; } break ; case 'H:M:s.S' : case 'H:M:s' : var hour = Math . floor ( remaining / ( 60 * 60 * 1000 ) ) ; remaining = remaining % ( 60 * 60 * 1000 ) ; var min = Math . floor ( remaining / ( 60 * 1000 ) ) ; remaining = remaining % ( 60 * 1000 ) ; var sec = Math . floor ( remaining / ( 1000 ) ) ; remaining = remaining % 1000 ; var fraction = '.' + ( remaining / 1000 ) . toFixed ( 1 ) . substr ( 2 ) ; return hour + ':' + padStart ( min . toString ( ) , 2 , '0' ) + ':' + padStart ( sec . toString ( ) , 2 , '0' ) + ( progress . settings . etaFormat . endsWith ( '.S' ) ? fraction : '' ) ; break ; case 'M:s.S' : case 'M:s' : var min = Math . floor ( remaining / ( 60 * 1000 ) ) ; remaining = remaining % ( 60 * 1000 ) ; var sec = Math . floor ( remaining / ( 1000 ) ) ; remaining = remaining % 1000 ; var fraction = '.' + ( remaining / 1000 ) . toFixed ( 1 ) . substr ( 2 ) ; return min + ':' + padStart ( sec . toString ( ) , 2 , '0' ) + ( progress . settings . etaFormat . endsWith ( '.S' ) ? fraction : '' ) ; break ; case 's' : case 's.S' : return ( remaining / 1000 ) . toFixed ( progress . settings . etaFormat . endsWith ( '.S' ) ? 1 : 0 ) ; } // }}} } , spinnerTheme : 'dots' , spinnerFrame : 0 , spinner : function ( ) { var spinnerFrames = cliSpinners [ progress . settings . spinnerTheme ] ; if ( ! spinnerFrames ) throw new Error ( 'Spinner theme not found: "' + progress . settings . spinnerTheme + '"' ) ; if ( ++ progress . settings . spinnerFrame >= spinnerFrames . frames . length ) progress . settings . spinnerFrame = 0 ; return spinnerFrames . frames [ progress . settings . spinnerFrame ] ; } , } ; // Map all color / style functions from chalk into the settings object (e.g. bold => chalk.bold closure) {{{ Object . keys ( colors . styles ) . forEach ( function ( style ) { progress . settings [ style ] = function ( ) { return function ( text , render ) { return colors [ style ] ( render ( text ) ) ; } ; } ; } ) ; // }}} /**
	* Return the output that will be sent to the output stream
	* return {string} The raw console-suitable output
	*/ progress . format = function ( ) { var text = mustache . render ( progress . settings . text , progress . settings ) ; // Rendering a bar? {{{ if ( text . indexOf ( '[[BAR' ) > - 1 ) { var maxBarWidth = ( windowSize . width || 80 ) - stringLength ( text ) ; if ( ! progress . settings . max ) progress . settings . max = progress . settings . current > 0 ? progress . settings . current : 0 ; var barCompleteWidth = Math . round ( progress . settings . current / progress . settings . max * maxBarWidth ) ; if ( barCompleteWidth == NaN || ! barCompleteWidth ) barCompleteWidth = 0 ; var completeBits = Array ( barCompleteWidth ) . join ( progress . settings . completeChar ) ; var incompleteBits = maxBarWidth - barCompleteWidth > 0 ? Array ( maxBarWidth - barCompleteWidth ) . join ( progress . settings . incompleteChar ) : '' ; text = text . replace ( / \[\[BAR\]\] / g , completeBits + incompleteBits ) . replace ( / \[\[BAR.complete\]\] / ig , completeBits ) . replace ( / \[\[BAR.incomplete\]\] / ig , incompleteBits ) } // }}} return text ; } ; /**
	* Holder for the throttle timeout handle
	* This will be the setTimeout() response if we are throttling
	* @var {Object}
	*/ progress . throttleHandle ; /**
	* Update and re-render the progress bar
	* This is really just a shortcut for .set() + .render(progress.format())
	* @params {Object|number} Either set a number of properties or the 'current' value if passed a number
	* @return {Object} This object instance
	* @see set()
	*/ progress . update = function ( val ) { progress . set ( val ) ; if ( progress . settings . throttle && progress . settings . throttleSync ) { // Employ sync throttling method (count from last update) if ( ! progress . lastUpdate || Date . now ( ) >= progress . lastUpdate + progress . settings . throttle ) { progress . updateNow ( ) ; // Only allow redraw if we're within the update window } } else if ( progress . settings . throttle ) { // Employ async throttling method (setTimeout) if ( ! progress . throttleHandle ) progress . throttleHandle = setTimeout ( progress . updateNow , progress . settings . throttle ) ; } else { // Not using throttle anyway progress . updateNow ( ) ; } return progress ; } ; /**
	* The timestamp in milliseconds of the last update event
	* This is used when employing the synchronous throttling method
	* @var {date}
	*/ progress . lastUpdate ; /**
	* Actual updater
	* This is hidden behind the update throttler so its not called too many times in one cycle
	*/ progress . updateNow = function ( ) { progress . lastUpdate = Date . now ( ) ; progress . settings . render ( progress . format ( ) ) ; clearTimeout ( progress . throttleHandle ) ; progress . throttleHandle = null ; } ; /**
	* Remove the bar from the screen
	* @return {Object} This object instance
	*/ progress . remove = function ( ) { clearTimeout ( progress . throttleHandle ) ; // Release any throttled redraws that may be queued progress . throttleHandle = null ; progress . settings . clear ( ) ; return progress ; } ; /**
	* Set one or more options or tokens
	* NOTE: Unlike .set() this function DOES NOT refresh the progress bar
	* @params {Object|number} Either set a number of properties or the 'current' value if passed a number
	* @return {Object} This object instance
	* @see set()
	*/ progress . set = function ( val ) { if ( ! val ) return ; if ( typeof val == 'number' ) { progress . settings . current = val ; } else { for ( var k in val ) { progress . settings [ k ] = val [ k ] ; } if ( val . text ) { // Setting the formatting text? progress . settings . text = progress . settings . text . replace ( / \{\{bar(.*?)\}\} / g , '[[BAR$1]]' ) ; // Remove mustache stuff as we have to calculate the width post-render mustache . parse ( progress . settings . text ) ; } } return progress ; } ; // Load initial settings {{{ if ( typeof text == 'string' ) { progress . set ( { text : text } ) ; } else { progress . set ( text ) ; } if ( typeof settings == 'object' ) progress . set ( settings ) ; // }}} }
}}}
}}}