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