1'use strict'
2var Plumbing = require('./plumbing.js')
3var hasUnicode = require('has-unicode')
4var hasColor = require('./has-color.js')
5var onExit = require('signal-exit').onExit
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) {
83    this._requestRedraw()
84  }
85}
86
87Gauge.prototype._computeTheme = function (theme) {
88  if (!theme) {
89    theme = {}
90  }
91  if (typeof theme === 'string') {
92    theme = this._themes.getTheme(theme)
93  } else if (
94    Object.keys(theme).length === 0 || theme.hasUnicode != null || theme.hasColor != null
95  ) {
96    var useUnicode = theme.hasUnicode == null ? hasUnicode() : theme.hasUnicode
97    var useColor = theme.hasColor == null ? hasColor : theme.hasColor
98    theme = this._themes.getDefault({
99      hasUnicode: useUnicode,
100      hasColor: useColor,
101      platform: theme.platform,
102    })
103  }
104  return theme
105}
106
107Gauge.prototype.setThemeset = function (themes) {
108  this._themes = themes
109  this.setTheme(this._theme)
110}
111
112Gauge.prototype.setTheme = function (theme) {
113  this._gauge.setTheme(this._computeTheme(theme))
114  if (this._showing) {
115    this._requestRedraw()
116  }
117  this._theme = theme
118}
119
120Gauge.prototype._requestRedraw = function () {
121  this._needsRedraw = true
122  if (!this._fixedFramerate) {
123    this._doRedraw()
124  }
125}
126
127Gauge.prototype.getWidth = function () {
128  return ((this._tty && this._tty.columns) || 80) - 1
129}
130
131Gauge.prototype.setWriteTo = function (writeTo, tty) {
132  var enabled = !this._disabled
133  if (enabled) {
134    this.disable()
135  }
136  this._writeTo = writeTo
137  this._tty = tty ||
138    (writeTo === process.stderr && process.stdout.isTTY && process.stdout) ||
139    (writeTo.isTTY && writeTo) ||
140    this._tty
141  if (this._gauge) {
142    this._gauge.setWidth(this.getWidth())
143  }
144  if (enabled) {
145    this.enable()
146  }
147}
148
149Gauge.prototype.enable = function () {
150  if (!this._disabled) {
151    return
152  }
153  this._disabled = false
154  if (this._tty) {
155    this._enableEvents()
156  }
157  if (this._showing) {
158    this.show()
159  }
160}
161
162Gauge.prototype.disable = function () {
163  if (this._disabled) {
164    return
165  }
166  if (this._showing) {
167    this._lastUpdateAt = null
168    this._showing = false
169    this._doRedraw()
170    this._showing = true
171  }
172  this._disabled = true
173  if (this._tty) {
174    this._disableEvents()
175  }
176}
177
178Gauge.prototype._enableEvents = function () {
179  if (this._cleanupOnExit) {
180    this._removeOnExit = onExit(callWith(this, this.disable))
181  }
182  this._tty.on('resize', this._$$handleSizeChange)
183  if (this._fixedFramerate) {
184    this.redrawTracker = setInterval(this._$$doRedraw, this._updateInterval)
185    if (this.redrawTracker.unref) {
186      this.redrawTracker.unref()
187    }
188  }
189}
190
191Gauge.prototype._disableEvents = function () {
192  this._tty.removeListener('resize', this._$$handleSizeChange)
193  if (this._fixedFramerate) {
194    clearInterval(this.redrawTracker)
195  }
196  if (this._removeOnExit) {
197    this._removeOnExit()
198  }
199}
200
201Gauge.prototype.hide = function (cb) {
202  if (this._disabled) {
203    return cb && process.nextTick(cb)
204  }
205  if (!this._showing) {
206    return cb && process.nextTick(cb)
207  }
208  this._showing = false
209  this._doRedraw()
210  cb && setImmediate(cb)
211}
212
213Gauge.prototype.show = function (section, completed) {
214  this._showing = true
215  if (typeof section === 'string') {
216    this._status.section = section
217  } else if (typeof section === 'object') {
218    var sectionKeys = Object.keys(section)
219    for (var ii = 0; ii < sectionKeys.length; ++ii) {
220      var key = sectionKeys[ii]
221      this._status[key] = section[key]
222    }
223  }
224  if (completed != null) {
225    this._status.completed = completed
226  }
227  if (this._disabled) {
228    return
229  }
230  this._requestRedraw()
231}
232
233Gauge.prototype.pulse = function (subsection) {
234  this._status.subsection = subsection || ''
235  this._status.spun++
236  if (this._disabled) {
237    return
238  }
239  if (!this._showing) {
240    return
241  }
242  this._requestRedraw()
243}
244
245Gauge.prototype._handleSizeChange = function () {
246  this._gauge.setWidth(this._tty.columns - 1)
247  this._requestRedraw()
248}
249
250Gauge.prototype._doRedraw = function () {
251  if (this._disabled || this._paused) {
252    return
253  }
254  if (!this._fixedFramerate) {
255    var now = Date.now()
256    if (this._lastUpdateAt && now - this._lastUpdateAt < this._updateInterval) {
257      return
258    }
259    this._lastUpdateAt = now
260  }
261  if (!this._showing && this._onScreen) {
262    this._onScreen = false
263    var result = this._gauge.hide()
264    if (this._hideCursor) {
265      result += this._gauge.showCursor()
266    }
267    return this._writeTo.write(result)
268  }
269  if (!this._showing && !this._onScreen) {
270    return
271  }
272  if (this._showing && !this._onScreen) {
273    this._onScreen = true
274    this._needsRedraw = true
275    if (this._hideCursor) {
276      this._writeTo.write(this._gauge.hideCursor())
277    }
278  }
279  if (!this._needsRedraw) {
280    return
281  }
282  if (!this._writeTo.write(this._gauge.show(this._status))) {
283    this._paused = true
284    this._writeTo.on('drain', callWith(this, function () {
285      this._paused = false
286      this._doRedraw()
287    }))
288  }
289}
290