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