1'use strict' 2var Plumbing = require('./plumbing.js') 3var hasUnicode = require('has-unicode') 4var hasColor = require('./has-color.js') 5var onExit = require('signal-exit') 6var defaultThemes = require('./themes') 7var setInterval = require('./set-interval.js') 8var process = require('./process.js') 9var setImmediate = require('./set-immediate') 10 11module.exports = Gauge 12 13function callWith (obj, method) { 14 return function () { 15 return method.call(obj) 16 } 17} 18 19function Gauge (arg1, arg2) { 20 var options, writeTo 21 if (arg1 && arg1.write) { 22 writeTo = arg1 23 options = arg2 || {} 24 } else if (arg2 && arg2.write) { 25 writeTo = arg2 26 options = arg1 || {} 27 } else { 28 writeTo = process.stderr 29 options = arg1 || arg2 || {} 30 } 31 32 this._status = { 33 spun: 0, 34 section: '', 35 subsection: '' 36 } 37 this._paused = false // are we paused for back pressure? 38 this._disabled = true // are all progress bar updates disabled? 39 this._showing = false // do we WANT the progress bar on screen 40 this._onScreen = false // IS the progress bar on screen 41 this._needsRedraw = false // should we print something at next tick? 42 this._hideCursor = options.hideCursor == null ? true : options.hideCursor 43 this._fixedFramerate = options.fixedFramerate == null 44 ? !(/^v0\.8\./.test(process.version)) 45 : options.fixedFramerate 46 this._lastUpdateAt = null 47 this._updateInterval = options.updateInterval == null ? 50 : options.updateInterval 48 49 this._themes = options.themes || defaultThemes 50 this._theme = options.theme 51 var theme = this._computeTheme(options.theme) 52 var template = options.template || [ 53 {type: 'progressbar', length: 20}, 54 {type: 'activityIndicator', kerning: 1, length: 1}, 55 {type: 'section', kerning: 1, default: ''}, 56 {type: 'subsection', kerning: 1, default: ''} 57 ] 58 this.setWriteTo(writeTo, options.tty) 59 var PlumbingClass = options.Plumbing || Plumbing 60 this._gauge = new PlumbingClass(theme, template, this.getWidth()) 61 62 this._$$doRedraw = callWith(this, this._doRedraw) 63 this._$$handleSizeChange = callWith(this, this._handleSizeChange) 64 65 this._cleanupOnExit = options.cleanupOnExit == null || options.cleanupOnExit 66 this._removeOnExit = null 67 68 if (options.enabled || (options.enabled == null && this._tty && this._tty.isTTY)) { 69 this.enable() 70 } else { 71 this.disable() 72 } 73} 74Gauge.prototype = {} 75 76Gauge.prototype.isEnabled = function () { 77 return !this._disabled 78} 79 80Gauge.prototype.setTemplate = function (template) { 81 this._gauge.setTemplate(template) 82 if (this._showing) this._requestRedraw() 83} 84 85Gauge.prototype._computeTheme = function (theme) { 86 if (!theme) theme = {} 87 if (typeof theme === 'string') { 88 theme = this._themes.getTheme(theme) 89 } else if (theme && (Object.keys(theme).length === 0 || theme.hasUnicode != null || theme.hasColor != null)) { 90 var useUnicode = theme.hasUnicode == null ? hasUnicode() : theme.hasUnicode 91 var useColor = theme.hasColor == null ? hasColor : theme.hasColor 92 theme = this._themes.getDefault({hasUnicode: useUnicode, hasColor: useColor, platform: theme.platform}) 93 } 94 return theme 95} 96 97Gauge.prototype.setThemeset = function (themes) { 98 this._themes = themes 99 this.setTheme(this._theme) 100} 101 102Gauge.prototype.setTheme = function (theme) { 103 this._gauge.setTheme(this._computeTheme(theme)) 104 if (this._showing) this._requestRedraw() 105 this._theme = theme 106} 107 108Gauge.prototype._requestRedraw = function () { 109 this._needsRedraw = true 110 if (!this._fixedFramerate) this._doRedraw() 111} 112 113Gauge.prototype.getWidth = function () { 114 return ((this._tty && this._tty.columns) || 80) - 1 115} 116 117Gauge.prototype.setWriteTo = function (writeTo, tty) { 118 var enabled = !this._disabled 119 if (enabled) this.disable() 120 this._writeTo = writeTo 121 this._tty = tty || 122 (writeTo === process.stderr && process.stdout.isTTY && process.stdout) || 123 (writeTo.isTTY && writeTo) || 124 this._tty 125 if (this._gauge) this._gauge.setWidth(this.getWidth()) 126 if (enabled) this.enable() 127} 128 129Gauge.prototype.enable = function () { 130 if (!this._disabled) return 131 this._disabled = false 132 if (this._tty) this._enableEvents() 133 if (this._showing) this.show() 134} 135 136Gauge.prototype.disable = function () { 137 if (this._disabled) return 138 if (this._showing) { 139 this._lastUpdateAt = null 140 this._showing = false 141 this._doRedraw() 142 this._showing = true 143 } 144 this._disabled = true 145 if (this._tty) this._disableEvents() 146} 147 148Gauge.prototype._enableEvents = function () { 149 if (this._cleanupOnExit) { 150 this._removeOnExit = onExit(callWith(this, this.disable)) 151 } 152 this._tty.on('resize', this._$$handleSizeChange) 153 if (this._fixedFramerate) { 154 this.redrawTracker = setInterval(this._$$doRedraw, this._updateInterval) 155 if (this.redrawTracker.unref) this.redrawTracker.unref() 156 } 157} 158 159Gauge.prototype._disableEvents = function () { 160 this._tty.removeListener('resize', this._$$handleSizeChange) 161 if (this._fixedFramerate) clearInterval(this.redrawTracker) 162 if (this._removeOnExit) this._removeOnExit() 163} 164 165Gauge.prototype.hide = function (cb) { 166 if (this._disabled) return cb && process.nextTick(cb) 167 if (!this._showing) return cb && process.nextTick(cb) 168 this._showing = false 169 this._doRedraw() 170 cb && setImmediate(cb) 171} 172 173Gauge.prototype.show = function (section, completed) { 174 this._showing = true 175 if (typeof section === 'string') { 176 this._status.section = section 177 } else if (typeof section === 'object') { 178 var sectionKeys = Object.keys(section) 179 for (var ii = 0; ii < sectionKeys.length; ++ii) { 180 var key = sectionKeys[ii] 181 this._status[key] = section[key] 182 } 183 } 184 if (completed != null) this._status.completed = completed 185 if (this._disabled) return 186 this._requestRedraw() 187} 188 189Gauge.prototype.pulse = function (subsection) { 190 this._status.subsection = subsection || '' 191 this._status.spun ++ 192 if (this._disabled) return 193 if (!this._showing) return 194 this._requestRedraw() 195} 196 197Gauge.prototype._handleSizeChange = function () { 198 this._gauge.setWidth(this._tty.columns - 1) 199 this._requestRedraw() 200} 201 202Gauge.prototype._doRedraw = function () { 203 if (this._disabled || this._paused) return 204 if (!this._fixedFramerate) { 205 var now = Date.now() 206 if (this._lastUpdateAt && now - this._lastUpdateAt < this._updateInterval) return 207 this._lastUpdateAt = now 208 } 209 if (!this._showing && this._onScreen) { 210 this._onScreen = false 211 var result = this._gauge.hide() 212 if (this._hideCursor) { 213 result += this._gauge.showCursor() 214 } 215 return this._writeTo.write(result) 216 } 217 if (!this._showing && !this._onScreen) return 218 if (this._showing && !this._onScreen) { 219 this._onScreen = true 220 this._needsRedraw = true 221 if (this._hideCursor) { 222 this._writeTo.write(this._gauge.hideCursor()) 223 } 224 } 225 if (!this._needsRedraw) return 226 if (!this._writeTo.write(this._gauge.show(this._status))) { 227 this._paused = true 228 this._writeTo.on('drain', callWith(this, function () { 229 this._paused = false 230 this._doRedraw() 231 })) 232 } 233} 234