UNPKG

12.5 kBJavaScriptView Raw
1'use strict';
2
3const ask = require('ask-nicely');
4const os = require('os');
5const util = require('util');
6
7const Table = require('cli-table');
8const stripAnsi = require('strip-ansi');
9
10const primitives = require('./primitives');
11const extras = require('./extras');
12
13const DEFAULT_CONFIG = {
14 defaultWidth: extras.defaultWidth,
15 indentation: ' ',
16 defaultIndentation: 1,
17 tableCharacters: extras.expandedTable,
18 spinnerFactory: extras.spriteSpinner,
19 spinnerInterval: 200,
20 stdout: process.stdout
21};
22
23const DESTROYERS = Symbol();
24const LOG = Symbol();
25const RAW = Symbol();
26const WIDTH = Symbol();
27
28class ErrorWithSolution extends ask.InputError {
29 /**
30 * A Error object which can also have a solution and/or inner Error.
31 *
32 * @param {string} message
33 * @param {string} [solution]
34 * @param {Error} [innerError]
35 * @param {string} [code]
36 */
37 constructor(message, solution, innerError, code) {
38 super(message, solution, code);
39 this.innerError = innerError;
40
41 // A workaround to make `instanceof ErrorWithSolution` work.
42 this.constructor = ErrorWithSolution;
43 this.__proto__ = ErrorWithSolution.prototype;
44 }
45}
46
47class ErrorWithInnerError extends ErrorWithSolution {
48 /**
49 * A Error object which can also have an inner Error.
50 *
51 * @param {string} message
52 * @param {Error} [innerError]
53 * @param {string} [code]
54 */
55 constructor(message, innerError, code) {
56 super(message, undefined, innerError, code);
57
58 // A workaround to make `instanceof ErrorWithInnerError` work.
59 this.constructor = ErrorWithInnerError;
60 this.__proto__ = ErrorWithInnerError.prototype;
61 }
62}
63
64class FdtResponse {
65 /**
66 *
67 * @param {Object} [colors]
68 * @param {Object} [config]
69 */
70 constructor(colors, config) {
71 // Write the color config to instance, and convert values to arrays if they weren't already.
72 this.colors = Object.assign({}, extras.defaultTheme, colors);
73 Object.keys(this.colors).forEach(colorName => {
74 if (!this.colors[colorName]) this.colors[colorName] = ['dim'];
75 else if (!Array.isArray(this.colors[colorName]))
76 this.colors[colorName] = [this.colors[colorName]];
77 });
78
79 // Write all other config to instance.
80 this.config = Object.assign({}, DEFAULT_CONFIG, config);
81
82 // If set to TRUE, the next log will overwrite the previous output.
83 this.needsClearing = false;
84
85 // The number of indents the next log will have.
86 this.indentationLevel = this.config.defaultIndentation;
87
88 // The maximum width of any line logged by FdtResponse.
89 this[WIDTH] = primitives.getTerminalWidth() || this.config.defaultWidth;
90
91 // A list of interval destroyers.
92 this[DESTROYERS] = [];
93
94 this.ErrorWithInnerError = ErrorWithInnerError;
95 this.ErrorWithSolution = ErrorWithSolution;
96 this.InputError = ask.InputError;
97 }
98
99 clearIfNeeded() {
100 if (this.needsClearing && typeof this.config.stdout.clearLine === 'function') {
101 this.config.stdout.clearLine();
102 this.config.stdout.cursorTo(0);
103 this.needsClearing = false;
104 }
105 }
106
107 [RAW](data) {
108 this.config.stdout.write(data);
109 }
110
111 [LOG](string, formattingOptions, skipLineBreak) {
112 this.clearIfNeeded();
113
114 this[RAW](
115 primitives.indentString(
116 primitives.formatString(string, formattingOptions),
117 primitives.getLeftIndentationString(this.config.indentation, this.indentationLevel),
118 this.config.indentation,
119 this[WIDTH]
120 ) + (skipLineBreak ? '' : os.EOL)
121 );
122 }
123
124 getWidth() {
125 return this[WIDTH];
126 }
127
128 setWrapping(width) {
129 if (width === true) this[WIDTH] = primitives.getTerminalWidth();
130 else if (width) this[WIDTH] = parseInt(width, 10);
131 else this[WIDTH] = Infinity;
132 }
133
134 indent() {
135 ++this.indentationLevel;
136 }
137 outdent() {
138 this.indentationLevel = Math.max(this.config.defaultIndentation, this.indentationLevel - 1);
139 }
140
141 /**
142 * A description of proceedings relevant to the task/whatever.
143 *
144 * @param {*} data
145 * @param {boolean} [skipLineBreak]
146 */
147 log(data, skipLineBreak) {
148 this[LOG](data, this.colors.log, skipLineBreak);
149 }
150
151 /**
152 * Indicates that something the user wanted happened.
153 *
154 * @param {*} data
155 * @param {boolean} [skipLineBreak]
156 */
157 success(data, skipLineBreak) {
158 this[LOG](data, this.colors.success, skipLineBreak);
159 }
160
161 /**
162 * Indicates the app is working on a concern/task/whatever.
163 *
164 * @param {*} data
165 * @param {boolean} [skipLineBreak]
166 */
167 caption(data, skipLineBreak) {
168 this.clearIfNeeded();
169 this.break();
170 this[LOG](data, this.colors.caption, skipLineBreak);
171 }
172
173 /**
174 * Something that is probably of interest (but not necessarily bad), if not important, for the user; exceptions, search results, urgent stuff.
175 *
176 * @param {*} data
177 * @param {boolean} [skipLineBreak]
178 */
179 notice(data, skipLineBreak) {
180 this[LOG](data, this.colors.notice, skipLineBreak);
181 }
182
183 /**
184 * Something messed up.
185 *
186 * @param {*} data
187 * @param {boolean} [skipLineBreak]
188 */
189 error(data, skipLineBreak) {
190 this[LOG](data, this.colors.error, skipLineBreak);
191 }
192
193 /**
194 * Information that the user might not even care about at that time.
195 *
196 * @param {*} data
197 * @param {boolean} [skipLineBreak]
198 */
199 debug(data, skipLineBreak) {
200 this[LOG](
201 data && typeof data === 'object'
202 ? util.inspect(data, { depth: 3, colors: false })
203 : data,
204 this.colors.debug,
205 skipLineBreak
206 );
207 }
208
209 definition(key, value, formattingName, skipLineBreak) {
210 this[LOG](key, this.colors.definitionKey);
211
212 this[RAW](
213 primitives.indentString(
214 primitives.formatString(
215 value,
216 formattingName ? this.colors[formattingName] : this.colors.definitionValue
217 ),
218 primitives.getLeftIndentationString(
219 this.config.indentation,
220 this.indentationLevel + 1
221 ),
222 this.config.indentation,
223 this[WIDTH]
224 ) + (skipLineBreak ? '' : os.EOL)
225 );
226 }
227
228 property(key, value, keySize, formattingName, skipLineBreak) {
229 keySize = keySize || 0;
230 const keyString = primitives.indentString(
231 primitives.formatString(
232 primitives.padString(key, keySize),
233 this.colors.propertyKey
234 ),
235 primitives.getLeftIndentationString(this.config.indentation, this.indentationLevel),
236 this.config.indentation,
237 this[WIDTH]
238 ),
239 seperatorString = ''; // used to pad the value of a property
240
241 this.clearIfNeeded();
242 this[RAW](
243 primitives
244 .indentString(
245 primitives.formatString(
246 value,
247 formattingName ? this.colors[formattingName] : this.colors.propertyValue
248 ),
249 seperatorString,
250 seperatorString,
251 this[WIDTH] -
252 (1 + this.indentationLevel) * this.config.indentation.length -
253 seperatorString.length -
254 keySize
255 )
256 .split('\n')
257 .map(
258 (line, i) =>
259 (i === 0
260 ? keyString
261 : primitives.fillString(
262 keySize +
263 (this.indentationLevel + 1) *
264 this.config.indentation.length +
265 1
266 )) + line
267 )
268 .join('\n') + (skipLineBreak ? '' : os.EOL)
269 );
270 }
271
272 properties(obj, formattingName) {
273 let maxLength = 0;
274 if (Array.isArray(obj)) {
275 obj.forEach(k => {
276 maxLength = Math.max((k[0] || '').length, maxLength);
277 });
278 obj.forEach(k => {
279 this.property(k[0], k[1], maxLength, k[2] || formattingName);
280 });
281 } else {
282 Object.keys(obj).forEach(k => {
283 maxLength = Math.max(k.length, maxLength);
284 });
285 Object.keys(obj).forEach(k => {
286 this.property(k, obj[k], maxLength, formattingName);
287 });
288 }
289 }
290
291 /**
292 * Information that should be outputted without formatting.
293 *
294 * @param {*} data
295 */
296 raw(data) {
297 this[RAW](data);
298 }
299
300 /**
301 * Whenever you need a little peace & quiet in your life, use break().
302 */
303 break() {
304 this[RAW](os.EOL);
305 }
306
307 /**
308 * Stop and remove any remaining spinners.
309 */
310 destroyAllSpinners() {
311 this[DESTROYERS].forEach(fn => fn());
312 }
313
314 /**
315 * Nice little ASCII animation that runs while the destroyer is not called.
316 *
317 * @param {string} message
318 * @param {boolean} [skipLineBreak]
319 *
320 * @return {function(Type, Type): Type} The destroyer.
321 */
322 spinner(message, skipLineBreak) {
323 const startTime = new Date().getTime(),
324 hasClearLine = typeof this.config.stdout.clearLine === 'function';
325
326 if (!hasClearLine) {
327 // When there is no support for clearing the line and moving the cursor, do nothing and
328 // just output the message and duration when done.
329 const destroySpinnerWithoutClearLine = () => {
330 const ms = new Date().getTime() - startTime;
331 this[LOG](`${message} (${ms}ms)`, this.colors.spinnerDone, skipLineBreak);
332
333 this[DESTROYERS].splice(
334 this[DESTROYERS].indexOf(destroySpinnerWithoutClearLine),
335 1
336 );
337 };
338
339 this[DESTROYERS].push(destroySpinnerWithoutClearLine);
340
341 return destroySpinnerWithoutClearLine;
342 }
343
344 const formattedMessageWithAnsi = primitives
345 .indentString(
346 primitives.formatString(message, this.colors.spinnerSpinning),
347 primitives.getLeftIndentationString(
348 this.config.indentation,
349 this.indentationLevel
350 ),
351 this.config.indentation,
352 this.getWidth()
353 )
354 .replace(new RegExp(`${this.config.indentation}$`), ''),
355 formattedMessageWithoutAnsi = stripAnsi(formattedMessageWithAnsi),
356 drawSpinner = this.config.spinnerFactory(
357 this,
358 message,
359 formattedMessageWithoutAnsi,
360 formattedMessageWithAnsi
361 ),
362 interval = setInterval(() => {
363 if (!this.needsClearing) {
364 // Redraw the message when a log message is outputted in between the spinner output
365 this[RAW](formattedMessageWithAnsi);
366 }
367 drawSpinner(null, !this.needsClearing);
368 this.needsClearing = true;
369 }, this.config.spinnerInterval),
370 destroySpinner = () => {
371 const ms = new Date().getTime() - startTime;
372
373 if (!this.needsClearing) {
374 // Redraw the message when a log message is outputted in between the spinner output
375 this[RAW](formattedMessageWithAnsi);
376 }
377
378 drawSpinner(`(${ms}ms)`, !this.needsClearing);
379 this[RAW](os.EOL);
380
381 clearInterval(interval);
382 this[DESTROYERS].splice(this[DESTROYERS].indexOf(destroySpinner), 1);
383 };
384
385 this[DESTROYERS].push(destroySpinner);
386
387 this[RAW](formattedMessageWithAnsi);
388 this.needsClearing = true;
389 drawSpinner();
390
391 return destroySpinner;
392 }
393
394 table(columnNames, content, expanded) {
395 const columnSizes = [],
396 totalWidth = Math.min(
397 this[WIDTH] - (this.indentationLevel + 1) * this.config.indentation.length,
398 800
399 ),
400 columnSeperator = ' ';
401
402 content = content.map(row =>
403 row.map((cell, colIndex) => {
404 cell = cell + '';
405 if (!columnSizes[colIndex]) columnSizes[colIndex] = columnNames[colIndex].length;
406
407 let cellLength = cell.length;
408 if (cell.includes('\n'))
409 cellLength = cell
410 .split('\n')
411 .reduce((max, line) => Math.max(max, line.length), 0);
412 if (cellLength > columnSizes[colIndex]) columnSizes[colIndex] = cellLength;
413
414 return cell.trim();
415 })
416 );
417
418 const totalContentAvailableWidth = totalWidth - columnNames.length * columnSeperator.length,
419 totalContentNativeWidth = columnSizes.reduce((total, size) => total + size, 0),
420 contentRelativeSizes =
421 totalContentNativeWidth <= totalContentAvailableWidth
422 ? columnSizes
423 : columnSizes.map(size =>
424 Math.ceil(totalContentAvailableWidth * (size / totalContentNativeWidth))
425 ),
426 table = new Table({
427 head: columnNames || [],
428 colWidths: contentRelativeSizes,
429 chars: !expanded ? extras.compactTable : this.config.tableCharacters,
430 style: {
431 'padding-left': 0,
432 'padding-right': 0,
433 'compact': !expanded,
434 'head': this.colors.tableHeader || [],
435 'border': this.colors.debug || []
436 }
437 });
438
439 content.forEach(cont =>
440 table.push(
441 cont.map((c, i) => {
442 return c.length > contentRelativeSizes[i]
443 ? primitives.wrap(c, contentRelativeSizes[i])
444 : c;
445 })
446 )
447 );
448
449 this.clearIfNeeded();
450 table
451 .toString()
452 .split('\n')
453 .map(
454 line =>
455 primitives.getLeftIndentationString(
456 this.config.indentation,
457 this.indentationLevel
458 ) + line
459 )
460 .forEach(line => {
461 this[RAW](line + os.EOL);
462 });
463 }
464
465 list(listItems, bulletCharacter) {
466 listItems.forEach((listItem, i) => {
467 this.listItem(listItem, bulletCharacter ? bulletCharacter : i + 1 + '.');
468 });
469 }
470
471 listItem(value, bulletCharacter, skipLineBreak) {
472 this[LOG](
473 primitives.formatString(bulletCharacter, this.colors.listItemBullet) +
474 ' ' +
475 primitives.formatString(value, this.colors.listItemValue, skipLineBreak)
476 );
477 }
478}
479
480module.exports = FdtResponse;