array-test.js000066400000002123151121504500007167 0ustar00(function() { var assert, vows, util; vows = require('vows'); assert = require('assert'); util = require('../../lib/util'); vows.describe('Module core extension Array').addBatch({ 'Testing del': { topic: ['a', 'b', 'c'], 'element exists': { 'first element': function(topic) { return assert.deepEqual(util.array.del(topic, 'a'), ['b', 'c']); }, 'middle element': function(topic) { return assert.deepEqual(util.array.del(topic, 'b'), ['a', 'c']); }, 'last element': function(topic) { return assert.deepEqual(util.array.del(topic, 'c'), ['a', 'b']); } }, 'element does not exist': function(topic) { return assert.deepEqual(util.array.del(topic, 'd'), ['a', 'b', 'c']); } }, 'Testing utils': { topic: ['a', 'b', 'c'], 'first': function(topic) { return assert.equal(util.array.first(topic), 'a'); }, 'last': function(topic) { return assert.equal(util.array.last(topic), 'c'); } } })["export"](module); }).call(this); string-test.js000066400000005722151121504500007367 0ustar00(function() { var assert, vows, util; vows = require('vows'); assert = require('assert'); util = require('../../lib/util'); vows.describe('Module core extension String').addBatch({ 'Testing value': { topic: 'bullet', 'join the keys': function(topic) { return assert.equal(util.string.value(topic), 'bullet'); } }, 'Testing gsub': { topic: 'bullet', 'when no args': function(topic) { return assert.equal(util.string.gsub(topic), 'bullet'); }, 'when only 1 arg': function(topic) { return assert.equal(util.string.gsub(topic, /./), 'bullet'); }, 'when given proper args': function(topic) { return assert.equal(util.string.gsub(topic, /[aeiou]/, '*'), 'b*ll*t'); }, 'when replacement is a function': { 'with many groups': function(topic) { var str; str = util.string.gsub(topic, /([aeiou])(.)/, function($) { return "<" + $[1] + ">" + $[2]; }); return assert.equal(str, 'bllt'); }, 'with no groups': function(topic) { var str; str = util.string.gsub(topic, /[aeiou]/, function($) { return "<" + $[1] + ">"; }); return assert.equal(str, 'bllt'); } }, 'when replacement is special': { 'with many groups': function(topic) { return assert.equal(util.string.gsub(topic, /([aeiou])(.)/, '<$1>$2'), 'bllt'); }, 'with no groups': function(topic) { return assert.equal(util.string.gsub(topic, /[aeiou]/, '<$1>'), 'bllt'); } } }, 'Testing capitalize': { topic: 'employee salary', 'normal': function(topic) { return assert.equal(util.string.capitalize(topic), 'Employee Salary'); } }, 'Testing upcase': { topic: 'bullet', 'only first letter should be upcase': function(topic) { return assert.equal(util.string.upcase(topic), 'Bullet'); }, 'letter after underscore': function(topic) { return assert.equal(util.string.upcase('bullet_record'), 'Bullet_Record'); }, 'letter after slash': function(topic) { return assert.equal(util.string.upcase('bullet_record/errors'), 'Bullet_Record/Errors'); }, 'no letter after space': function(topic) { return assert.equal(util.string.upcase('employee salary'), 'Employee salary'); } }, 'Testing downcase': { topic: 'BULLET', 'only first letter should be downcase': function(topic) { return assert.equal(util.string.downcase(topic), 'bULLET'); }, 'letter after underscore': function(topic) { return assert.equal(util.string.downcase('BULLET_RECORD'), 'bULLET_rECORD'); }, 'letter after slash': function(topic) { return assert.equal(util.string.downcase('BULLET_RECORD/ERRORS'), 'bULLET_rECORD/eRRORS'); } } })["export"](module); }).call(this); events.js000066400000002626151121542170006415 0ustar00'use strict'; var { fromEvent } = require('rxjs'); var { filter, map, share, takeUntil } = require('rxjs/operators'); function normalizeKeypressEvents(value, key) { return { value: value, key: key || {} }; } module.exports = function (rl) { var keypress = fromEvent(rl.input, 'keypress', normalizeKeypressEvents) .pipe(takeUntil(fromEvent(rl, 'close'))) // Ignore `enter` key. On the readline, we only care about the `line` event. .pipe(filter(({ key }) => key.name !== 'enter' && key.name !== 'return')); return { line: fromEvent(rl, 'line'), keypress: keypress, normalizedUpKey: keypress.pipe( filter( ({ key }) => key.name === 'up' || key.name === 'k' || (key.name === 'p' && key.ctrl) ), share() ), normalizedDownKey: keypress.pipe( filter( ({ key }) => key.name === 'down' || key.name === 'j' || (key.name === 'n' && key.ctrl) ), share() ), numberKey: keypress.pipe( filter((e) => e.value && '123456789'.indexOf(e.value) >= 0), map((e) => Number(e.value)), share() ), spaceKey: keypress.pipe( filter(({ key }) => key && key.name === 'space'), share() ), aKey: keypress.pipe( filter(({ key }) => key && key.name === 'a'), share() ), iKey: keypress.pipe( filter(({ key }) => key && key.name === 'i'), share() ), }; }; incrementListIndex.js000066400000000741151121542170010715 0ustar00function incrementListIndex(current, dir, opt) { var len = opt.choices.realLength; var shouldLoop = 'loop' in opt ? Boolean(opt.loop) : true; if (dir === 'up') { if (current > 0) { return current - 1; } return shouldLoop ? len - 1 : current; } if (dir === 'down') { if (current < len - 1) { return current + 1; } return shouldLoop ? 0 : current; } throw new Error('dir must be up or down'); } module.exports = incrementListIndex; paginator.js000066400000004133151121542170007070 0ustar00'use strict'; var _ = { sum: require('lodash/sum'), flatten: require('lodash/flatten'), }; var chalk = require('chalk'); /** * The paginator returns a subset of the choices if the list is too long. */ class Paginator { constructor(screen, options = {}) { const { isInfinite = true } = options; this.lastIndex = 0; this.screen = screen; this.isInfinite = isInfinite; } paginate(output, active, pageSize) { pageSize = pageSize || 7; var lines = output.split('\n'); if (this.screen) { lines = this.screen.breakLines(lines); active = _.sum(lines.map((lineParts) => lineParts.length).splice(0, active)); lines = _.flatten(lines); } // Make sure there's enough lines to paginate if (lines.length <= pageSize) { return output; } const visibleLines = this.isInfinite ? this.getInfiniteLines(lines, active, pageSize) : this.getFiniteLines(lines, active, pageSize); this.lastIndex = active; return ( visibleLines.join('\n') + '\n' + chalk.dim('(Move up and down to reveal more choices)') ); } getInfiniteLines(lines, active, pageSize) { if (this.pointer === undefined) { this.pointer = 0; } var middleOfList = Math.floor(pageSize / 2); // Move the pointer only when the user go down and limit it to the middle of the list if ( this.pointer < middleOfList && this.lastIndex < active && active - this.lastIndex < pageSize ) { this.pointer = Math.min(middleOfList, this.pointer + active - this.lastIndex); } // Duplicate the lines so it give an infinite list look var infinite = _.flatten([lines, lines, lines]); var topIndex = Math.max(0, active + lines.length - this.pointer); return infinite.splice(topIndex, pageSize); } getFiniteLines(lines, active, pageSize) { var topIndex = active - pageSize / 2; if (topIndex < 0) { topIndex = 0; } else if (topIndex + pageSize > lines.length) { topIndex = lines.length - pageSize; } return lines.splice(topIndex, pageSize); } } module.exports = Paginator; readline.js000066400000002240151121542170006664 0ustar00'use strict'; var ansiEscapes = require('ansi-escapes'); /** * Move cursor left by `x` * @param {Readline} rl - Readline instance * @param {Number} x - How far to go left (default to 1) */ exports.left = function (rl, x) { rl.output.write(ansiEscapes.cursorBackward(x)); }; /** * Move cursor right by `x` * @param {Readline} rl - Readline instance * @param {Number} x - How far to go left (default to 1) */ exports.right = function (rl, x) { rl.output.write(ansiEscapes.cursorForward(x)); }; /** * Move cursor up by `x` * @param {Readline} rl - Readline instance * @param {Number} x - How far to go up (default to 1) */ exports.up = function (rl, x) { rl.output.write(ansiEscapes.cursorUp(x)); }; /** * Move cursor down by `x` * @param {Readline} rl - Readline instance * @param {Number} x - How far to go down (default to 1) */ exports.down = function (rl, x) { rl.output.write(ansiEscapes.cursorDown(x)); }; /** * Clear current line * @param {Readline} rl - Readline instance * @param {Number} len - number of line to delete */ exports.clearLine = function (rl, len) { rl.output.write(ansiEscapes.eraseLines(len)); }; screen-manager.js000066400000007523151121542170010001 0ustar00'use strict'; var _ = { last: require('lodash/last'), flatten: require('lodash/flatten'), }; var util = require('./readline'); var cliWidth = require('cli-width'); var stripAnsi = require('strip-ansi'); var stringWidth = require('string-width'); function height(content) { return content.split('\n').length; } function lastLine(content) { return _.last(content.split('\n')); } class ScreenManager { constructor(rl) { // These variables are keeping information to allow correct prompt re-rendering this.height = 0; this.extraLinesUnderPrompt = 0; this.rl = rl; } render(content, bottomContent) { this.rl.output.unmute(); this.clean(this.extraLinesUnderPrompt); /** * Write message to screen and setPrompt to control backspace */ var promptLine = lastLine(content); var rawPromptLine = stripAnsi(promptLine); // Remove the rl.line from our prompt. We can't rely on the content of // rl.line (mainly because of the password prompt), so just rely on it's // length. var prompt = rawPromptLine; if (this.rl.line.length) { prompt = prompt.slice(0, -this.rl.line.length); } this.rl.setPrompt(prompt); // SetPrompt will change cursor position, now we can get correct value var cursorPos = this.rl._getCursorPos(); var width = this.normalizedCliWidth(); content = this.forceLineReturn(content, width); if (bottomContent) { bottomContent = this.forceLineReturn(bottomContent, width); } // Manually insert an extra line if we're at the end of the line. // This prevent the cursor from appearing at the beginning of the // current line. if (rawPromptLine.length % width === 0) { content += '\n'; } var fullContent = content + (bottomContent ? '\n' + bottomContent : ''); this.rl.output.write(fullContent); /** * Re-adjust the cursor at the correct position. */ // We need to consider parts of the prompt under the cursor as part of the bottom // content in order to correctly cleanup and re-render. var promptLineUpDiff = Math.floor(rawPromptLine.length / width) - cursorPos.rows; var bottomContentHeight = promptLineUpDiff + (bottomContent ? height(bottomContent) : 0); if (bottomContentHeight > 0) { util.up(this.rl, bottomContentHeight); } // Reset cursor at the beginning of the line util.left(this.rl, stringWidth(lastLine(fullContent))); // Adjust cursor on the right if (cursorPos.cols > 0) { util.right(this.rl, cursorPos.cols); } /** * Set up state for next re-rendering */ this.extraLinesUnderPrompt = bottomContentHeight; this.height = height(fullContent); this.rl.output.mute(); } clean(extraLines) { if (extraLines > 0) { util.down(this.rl, extraLines); } util.clearLine(this.rl, this.height); } done() { this.rl.setPrompt(''); this.rl.output.unmute(); this.rl.output.write('\n'); } releaseCursor() { if (this.extraLinesUnderPrompt > 0) { util.down(this.rl, this.extraLinesUnderPrompt); } } normalizedCliWidth() { var width = cliWidth({ defaultWidth: 80, output: this.rl.output, }); return width; } breakLines(lines, width) { // Break lines who're longer than the cli width so we can normalize the natural line // returns behavior across terminals. width = width || this.normalizedCliWidth(); var regex = new RegExp('(?:(?:\\033[[0-9;]*m)*.?){1,' + width + '}', 'g'); return lines.map((line) => { var chunk = line.match(regex); // Last match is always empty chunk.pop(); return chunk || ''; }); } forceLineReturn(content, width) { width = width || this.normalizedCliWidth(); return _.flatten(this.breakLines(content.split('\n'), width)).join('\n'); } } module.exports = ScreenManager; utils.js000066400000001455151121542170006250 0ustar00'use strict'; var _ = { isFunction: require('lodash/isFunction'), }; var { from, of } = require('rxjs'); var runAsync = require('run-async'); /** * Resolve a question property value if it is passed as a function. * This method will overwrite the property on the question object with the received value. * @param {Object} question - Question object * @param {String} prop - Property to fetch name * @param {Object} answers - Answers object * @return {Rx.Observable} - Observable emitting once value is known */ exports.fetchAsyncQuestionProperty = function (question, prop, answers) { if (!_.isFunction(question[prop])) { return of(question); } return from( runAsync(question[prop])(answers).then((value) => { question[prop] = value; return question; }) ); };