1'use strict'
2var Progress = require('are-we-there-yet')
3var Gauge = require('gauge')
4var EE = require('events').EventEmitter
5var log = exports = module.exports = new EE()
6var util = require('util')
7
8var setBlocking = require('set-blocking')
9var consoleControl = require('console-control-strings')
10
11setBlocking(true)
12var stream = process.stderr
13Object.defineProperty(log, 'stream', {
14  set: function (newStream) {
15    stream = newStream
16    if (this.gauge) {
17      this.gauge.setWriteTo(stream, stream)
18    }
19  },
20  get: function () {
21    return stream
22  },
23})
24
25// by default, decide based on tty-ness.
26var colorEnabled
27log.useColor = function () {
28  return colorEnabled != null ? colorEnabled : stream.isTTY
29}
30
31log.enableColor = function () {
32  colorEnabled = true
33  this.gauge.setTheme({ hasColor: colorEnabled, hasUnicode: unicodeEnabled })
34}
35log.disableColor = function () {
36  colorEnabled = false
37  this.gauge.setTheme({ hasColor: colorEnabled, hasUnicode: unicodeEnabled })
38}
39
40// default level
41log.level = 'info'
42
43log.gauge = new Gauge(stream, {
44  enabled: false, // no progress bars unless asked
45  theme: { hasColor: log.useColor() },
46  template: [
47    { type: 'progressbar', length: 20 },
48    { type: 'activityIndicator', kerning: 1, length: 1 },
49    { type: 'section', default: '' },
50    ':',
51    { type: 'logline', kerning: 1, default: '' },
52  ],
53})
54
55log.tracker = new Progress.TrackerGroup()
56
57// we track this separately as we may need to temporarily disable the
58// display of the status bar for our own loggy purposes.
59log.progressEnabled = log.gauge.isEnabled()
60
61var unicodeEnabled
62
63log.enableUnicode = function () {
64  unicodeEnabled = true
65  this.gauge.setTheme({ hasColor: this.useColor(), hasUnicode: unicodeEnabled })
66}
67
68log.disableUnicode = function () {
69  unicodeEnabled = false
70  this.gauge.setTheme({ hasColor: this.useColor(), hasUnicode: unicodeEnabled })
71}
72
73log.setGaugeThemeset = function (themes) {
74  this.gauge.setThemeset(themes)
75}
76
77log.setGaugeTemplate = function (template) {
78  this.gauge.setTemplate(template)
79}
80
81log.enableProgress = function () {
82  if (this.progressEnabled || this._paused) {
83    return
84  }
85
86  this.progressEnabled = true
87  this.tracker.on('change', this.showProgress)
88  this.gauge.enable()
89}
90
91log.disableProgress = function () {
92  if (!this.progressEnabled) {
93    return
94  }
95  this.progressEnabled = false
96  this.tracker.removeListener('change', this.showProgress)
97  this.gauge.disable()
98}
99
100var trackerConstructors = ['newGroup', 'newItem', 'newStream']
101
102var mixinLog = function (tracker) {
103  // mixin the public methods from log into the tracker
104  // (except: conflicts and one's we handle specially)
105  Object.keys(log).forEach(function (P) {
106    if (P[0] === '_') {
107      return
108    }
109
110    if (trackerConstructors.filter(function (C) {
111      return C === P
112    }).length) {
113      return
114    }
115
116    if (tracker[P]) {
117      return
118    }
119
120    if (typeof log[P] !== 'function') {
121      return
122    }
123
124    var func = log[P]
125    tracker[P] = function () {
126      return func.apply(log, arguments)
127    }
128  })
129  // if the new tracker is a group, make sure any subtrackers get
130  // mixed in too
131  if (tracker instanceof Progress.TrackerGroup) {
132    trackerConstructors.forEach(function (C) {
133      var func = tracker[C]
134      tracker[C] = function () {
135        return mixinLog(func.apply(tracker, arguments))
136      }
137    })
138  }
139  return tracker
140}
141
142// Add tracker constructors to the top level log object
143trackerConstructors.forEach(function (C) {
144  log[C] = function () {
145    return mixinLog(this.tracker[C].apply(this.tracker, arguments))
146  }
147})
148
149log.clearProgress = function (cb) {
150  if (!this.progressEnabled) {
151    return cb && process.nextTick(cb)
152  }
153
154  this.gauge.hide(cb)
155}
156
157log.showProgress = function (name, completed) {
158  if (!this.progressEnabled) {
159    return
160  }
161
162  var values = {}
163  if (name) {
164    values.section = name
165  }
166
167  var last = log.record[log.record.length - 1]
168  if (last) {
169    values.subsection = last.prefix
170    var disp = log.disp[last.level] || last.level
171    var logline = this._format(disp, log.style[last.level])
172    if (last.prefix) {
173      logline += ' ' + this._format(last.prefix, this.prefixStyle)
174    }
175
176    logline += ' ' + last.message.split(/\r?\n/)[0]
177    values.logline = logline
178  }
179  values.completed = completed || this.tracker.completed()
180  this.gauge.show(values)
181}.bind(log) // bind for use in tracker's on-change listener
182
183// temporarily stop emitting, but don't drop
184log.pause = function () {
185  this._paused = true
186  if (this.progressEnabled) {
187    this.gauge.disable()
188  }
189}
190
191log.resume = function () {
192  if (!this._paused) {
193    return
194  }
195
196  this._paused = false
197
198  var b = this._buffer
199  this._buffer = []
200  b.forEach(function (m) {
201    this.emitLog(m)
202  }, this)
203  if (this.progressEnabled) {
204    this.gauge.enable()
205  }
206}
207
208log._buffer = []
209
210var id = 0
211log.record = []
212log.maxRecordSize = 10000
213log.log = function (lvl, prefix, message) {
214  var l = this.levels[lvl]
215  if (l === undefined) {
216    return this.emit('error', new Error(util.format(
217      'Undefined log level: %j', lvl)))
218  }
219
220  var a = new Array(arguments.length - 2)
221  var stack = null
222  for (var i = 2; i < arguments.length; i++) {
223    var arg = a[i - 2] = arguments[i]
224
225    // resolve stack traces to a plain string.
226    if (typeof arg === 'object' && arg instanceof Error && arg.stack) {
227      Object.defineProperty(arg, 'stack', {
228        value: stack = arg.stack + '',
229        enumerable: true,
230        writable: true,
231      })
232    }
233  }
234  if (stack) {
235    a.unshift(stack + '\n')
236  }
237  message = util.format.apply(util, a)
238
239  var m = {
240    id: id++,
241    level: lvl,
242    prefix: String(prefix || ''),
243    message: message,
244    messageRaw: a,
245  }
246
247  this.emit('log', m)
248  this.emit('log.' + lvl, m)
249  if (m.prefix) {
250    this.emit(m.prefix, m)
251  }
252
253  this.record.push(m)
254  var mrs = this.maxRecordSize
255  var n = this.record.length - mrs
256  if (n > mrs / 10) {
257    var newSize = Math.floor(mrs * 0.9)
258    this.record = this.record.slice(-1 * newSize)
259  }
260
261  this.emitLog(m)
262}.bind(log)
263
264log.emitLog = function (m) {
265  if (this._paused) {
266    this._buffer.push(m)
267    return
268  }
269  if (this.progressEnabled) {
270    this.gauge.pulse(m.prefix)
271  }
272
273  var l = this.levels[m.level]
274  if (l === undefined) {
275    return
276  }
277
278  if (l < this.levels[this.level]) {
279    return
280  }
281
282  if (l > 0 && !isFinite(l)) {
283    return
284  }
285
286  // If 'disp' is null or undefined, use the lvl as a default
287  // Allows: '', 0 as valid disp
288  var disp = log.disp[m.level] != null ? log.disp[m.level] : m.level
289  this.clearProgress()
290  m.message.split(/\r?\n/).forEach(function (line) {
291    var heading = this.heading
292    if (heading) {
293      this.write(heading, this.headingStyle)
294      this.write(' ')
295    }
296    this.write(disp, log.style[m.level])
297    var p = m.prefix || ''
298    if (p) {
299      this.write(' ')
300    }
301
302    this.write(p, this.prefixStyle)
303    this.write(' ' + line + '\n')
304  }, this)
305  this.showProgress()
306}
307
308log._format = function (msg, style) {
309  if (!stream) {
310    return
311  }
312
313  var output = ''
314  if (this.useColor()) {
315    style = style || {}
316    var settings = []
317    if (style.fg) {
318      settings.push(style.fg)
319    }
320
321    if (style.bg) {
322      settings.push('bg' + style.bg[0].toUpperCase() + style.bg.slice(1))
323    }
324
325    if (style.bold) {
326      settings.push('bold')
327    }
328
329    if (style.underline) {
330      settings.push('underline')
331    }
332
333    if (style.inverse) {
334      settings.push('inverse')
335    }
336
337    if (settings.length) {
338      output += consoleControl.color(settings)
339    }
340
341    if (style.beep) {
342      output += consoleControl.beep()
343    }
344  }
345  output += msg
346  if (this.useColor()) {
347    output += consoleControl.color('reset')
348  }
349
350  return output
351}
352
353log.write = function (msg, style) {
354  if (!stream) {
355    return
356  }
357
358  stream.write(this._format(msg, style))
359}
360
361log.addLevel = function (lvl, n, style, disp) {
362  // If 'disp' is null or undefined, use the lvl as a default
363  if (disp == null) {
364    disp = lvl
365  }
366
367  this.levels[lvl] = n
368  this.style[lvl] = style
369  if (!this[lvl]) {
370    this[lvl] = function () {
371      var a = new Array(arguments.length + 1)
372      a[0] = lvl
373      for (var i = 0; i < arguments.length; i++) {
374        a[i + 1] = arguments[i]
375      }
376
377      return this.log.apply(this, a)
378    }.bind(this)
379  }
380  this.disp[lvl] = disp
381}
382
383log.prefixStyle = { fg: 'magenta' }
384log.headingStyle = { fg: 'white', bg: 'black' }
385
386log.style = {}
387log.levels = {}
388log.disp = {}
389log.addLevel('silly', -Infinity, { inverse: true }, 'sill')
390log.addLevel('verbose', 1000, { fg: 'cyan', bg: 'black' }, 'verb')
391log.addLevel('info', 2000, { fg: 'green' })
392log.addLevel('timing', 2500, { fg: 'green', bg: 'black' })
393log.addLevel('http', 3000, { fg: 'green', bg: 'black' })
394log.addLevel('notice', 3500, { fg: 'cyan', bg: 'black' })
395log.addLevel('warn', 4000, { fg: 'black', bg: 'yellow' }, 'WARN')
396log.addLevel('error', 5000, { fg: 'red', bg: 'black' }, 'ERR!')
397log.addLevel('silent', Infinity)
398
399// allow 'error' prefix
400log.on('error', function () {})
401