berlioz
Version:
Berlioz - cloud deployment and migration services
654 lines (580 loc) • 17.8 kB
JavaScript
'use strict';
/**
* Module dependencies.
*/
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var _ = require('lodash');
var inquirer = require('inquirer');
var EventEmitter = require('events').EventEmitter;
var chalk = require('chalk');
var util = require('./util');
var logUpdate = require('log-update');
var UI = function (_EventEmitter) {
_inherits(UI, _EventEmitter);
/**
* Sets intial variables and registers
* listeners. This is called once in a
* process thread regardless of how many
* instances of Vorpal have been generated.
*
* @api private
*/
function UI() {
_classCallCheck(this, UI);
var _this = _possibleConstructorReturn(this, (UI.__proto__ || Object.getPrototypeOf(UI)).call(this));
var self = _this;
// Attached vorpal instance. The UI can
// only attach to one instance of Vorpal
// at a time, and directs all events to that
// instance.
_this.parent = undefined;
// Hook to reference active inquirer prompt.
_this._activePrompt = undefined;
// Fail-safe to ensure there is no double
// prompt in odd situations.
_this._midPrompt = false;
// Handle for inquirer's prompt.
_this.inquirer = inquirer;
// prompt history from inquirer
_this.inquirerStdout = [];
// Whether a prompt is currently in cancel mode.
_this._cancelled = false;
// Middleware for piping stdout through.
_this._pipeFn = undefined;
// Custom function on sigint event.
_this._sigintCalled = false;
_this._sigintCount = 0;
_this._sigint = function () {
if (_this._sigintCount > 1) {
_this.parent.emit('vorpal_exit');
process.exit(0);
} else {
var text = _this.input();
if (!_this.parent) {
// If Vorpal isn't shown, just exit.
process.exit(0);
} else if (_this.parent.session.cancelCommands) {
// There are commands running if
// cancelCommands function is available.
_this.imprint();
_this.submit('');
_this._sigintCalled = false;
_this._sigintCount = 0;
_this.parent.session.emit('vorpal_command_cancel');
} else if (String(text).trim() !== '') {
_this.imprint();
_this.submit('');
_this._sigintCalled = false;
_this._sigintCount = 0;
} else {
_this._sigintCalled = false;
_this.delimiter(' ');
_this.submit('');
_this.log('(^C again to quit)');
}
}
};
process.stdin.on('keypress', function (letter, key) {
key = key || {};
if (key.ctrl === true && key.shift === false && key.meta === false && ['c', 'C'].indexOf(key.name) > -1) {
_this._sigintCount++;
if (_this._sigint !== undefined && !_this._sigintCalled) {
_this._sigintCalled = true;
_this._sigint.call(self.parent);
_this._sigintCalled = false;
}
} else {
_this._sigintCalled = false;
_this._sigintCount = 0;
}
});
// Extend the render function to steal the active prompt object,
// as inquirer doesn't expose it and we need it.
var prompts = ['input', 'checkbox', 'confirm', 'expand', 'list', 'password', 'rawlist'];
var _loop = function _loop(key) {
var promptType = prompts[key];
// Add method to Inquirer to get type of prompt.
inquirer.prompt.prompts[promptType].prototype.getType = function () {
return promptType;
};
// Hook in to steal Inquirer's keypress.
inquirer.prompt.prompts[promptType].prototype.onKeypress = function (e) {
// Inquirer seems to have a bug with release v0.10.1
// (not 0.10.0 though) that triggers keypresses for
// the previous prompt in addition to the current one.
// So if the prompt is answered, shut it up.
if (this.status && this.status === 'answered') {
return;
}
self._activePrompt = this;
self.parent.emit('client_keypress', e);
self._keypressHandler(e, this);
};
// Add hook to render method.
var render = inquirer.prompt.prompts[promptType].prototype.render;
inquirer.prompt.prompts[promptType].prototype.render = function () {
self._activePrompt = this;
return render.apply(this, arguments);
};
};
for (var key in prompts) {
_loop(key);
}
// Sigint handling - make it more graceful.
var onSigInt = function onSigInt() {
if (_.isFunction(_this._sigint) && !_this._sigintCalled) {
_this._sigintCalled = true;
_this._sigint.call(_this.parent);
}
};
process.on('SIGINT', onSigInt);
process.on('SIGTERM', onSigInt);
return _this;
}
/**
* Hook for sigint event.
*
* @param {Object} options
* @param {Function} cb
* @api public
*/
_createClass(UI, [{
key: 'sigint',
value: function sigint(fn) {
if (_.isFunction(fn)) {
this._sigint = fn;
} else {
throw new Error('vorpal.ui.sigint must be passed in a valid function.');
}
return this;
}
/**
* Creates an inquirer prompt on the TTY.
*
* @param {Object} options
* @param {Function} cb
* @api public
*/
}, {
key: 'prompt',
value: function prompt(options, cb) {
var _this2 = this;
var prompt = void 0;
options = options || {};
if (!this.parent) {
return prompt;
}
if (options.delimiter) {
this.setDelimiter(options.delimiter);
}
if (options.message) {
this.setDelimiter(options.message);
}
if (this._midPrompt) {
console.log('Prompt called when mid prompt...');
throw new Error('UI Prompt called when already mid prompt.');
}
this._midPrompt = true;
try {
prompt = inquirer.prompt(options, function (result) {
_this2.inquirerStdout = [];
_this2._midPrompt = false;
if (_this2._cancel === true) {
_this2._cancel = false;
} else {
cb(result);
}
});
// Temporary hack. We need to pull the active
// prompt from inquirer as soon as possible,
// however we can't just assign it sync, as
// the prompt isn't ready yet.
// I am trying to get inquirer extended to
// fire an event instead.
setTimeout(function () {
// this._activePrompt = prompt._activePrompt;
}, 100);
} catch (e) {
console.log('Vorpal Prompt error:', e);
}
return prompt;
}
/**
* Returns a boolean as to whether user
* is mid another pr ompt.
*
* @return {Boolean}
* @api public
*/
}, {
key: 'midPrompt',
value: function midPrompt() {
var mid = this._midPrompt === true && this.parent !== undefined;
return mid;
}
}, {
key: 'setDelimiter',
value: function setDelimiter(str) {
var self = this;
if (!this.parent) {
return;
}
str = String(str).trim() + ' ';
this._lastDelimiter = str;
inquirer.prompt.prompts.password.prototype.getQuestion = function () {
self._activePrompt = this;
return this.opt.message;
};
inquirer.prompt.prompts.input.prototype.getQuestion = function () {
self._activePrompt = this;
var message = this.opt.message;
if ((this.opt.default || this.opt.default === false) && this.status !== 'answered') {
message += chalk.dim('(' + this.opt.default + ') ');
}
self.inquirerStdout.push(message);
return message;
};
}
/**
* Event handler for keypresses - deals with command history
* and tabbed auto-completion.
*
* @param {Event} e
* @param {Prompt} prompt
* @api private
*/
}, {
key: '_keypressHandler',
value: function _keypressHandler(e, prompt) {
// Remove tab characters from user input.
prompt.rl.line = prompt.rl.line.replace(/\t+/, '');
// Mask passwords.
var line = prompt.getType() !== 'password' ? prompt.rl.line : '*'.repeat(prompt.rl.line.length);
// Re-write render function.
var width = prompt.rl.line.length;
var newWidth = prompt.rl.line.length;
var diff = newWidth - width;
prompt.rl.cursor += diff;
var cursor = 0;
var message = prompt.getQuestion();
var addition = prompt.status === 'answered' ? chalk.cyan(prompt.answer) : line;
message += addition;
prompt.screen.render(message, { cursor: cursor });
var key = (e.key || {}).name;
var value = prompt ? String(line) : undefined;
this.emit('vorpal_ui_keypress', { key: key, value: value, e: e });
}
/**
* Pauses active prompt, returning
* the value of what had been typed so far.
*
* @return {String} val
* @api public
*/
}, {
key: 'pause',
value: function pause() {
if (!this.parent) {
return false;
}
if (!this._activePrompt) {
return false;
}
if (!this._midPrompt) {
return false;
}
var val = this._lastDelimiter + this._activePrompt.rl.line;
this._midPrompt = false;
var rl = this._activePrompt.screen.rl;
var screen = this._activePrompt.screen;
rl.output.unmute();
screen.clean();
rl.output.write('');
return val;
}
/**
* Resumes active prompt, accepting
* a string, which will fill the prompt
* with that text and put the cursor at
* the end.
*
* @param {String} val
* @api public
*/
}, {
key: 'resume',
value: function resume(val) {
if (!this.parent) {
return this;
}
val = val || '';
if (!this._activePrompt) {
return this;
}
if (this._midPrompt) {
return this;
}
var rl = this._activePrompt.screen.rl;
rl.output.write(val);
this._midPrompt = true;
return this;
}
/**
* Cancels the active prompt, essentially
* but cutting out of the inquirer loop.
*
* @api public
*/
}, {
key: 'cancel',
value: function cancel() {
if (this.midPrompt()) {
this._cancel = true;
this.submit('');
this._midPrompt = false;
}
return this;
}
/**
* Attaches TTY prompt to a given Vorpal instance.
*
* @param {Vorpal} vorpal
* @return {UI}
* @api public
*/
}, {
key: 'attach',
value: function attach(vorpal) {
this.parent = vorpal;
this.refresh();
this.parent._prompt();
return this;
}
/**
* Detaches UI from a given Vorpal instance.
*
* @param {Vorpal} vorpal
* @return {UI}
* @api public
*/
}, {
key: 'detach',
value: function detach(vorpal) {
if (vorpal === this.parent) {
this.parent = undefined;
}
return this;
}
/**
* Receives and runs logging through
* a piped function is one is provided
* through ui.pipe(). Pauses any active
* prompts, logs the data and then if
* paused, resumes the prompt.
*
* @return {UI}
* @api public
*/
}, {
key: 'log',
value: function log() {
var args = util.fixArgsForApply(arguments);
args = _.isFunction(this._pipeFn) ? this._pipeFn(args) : args;
if (args === '') {
return this;
}
args = util.fixArgsForApply(args);
if (this.midPrompt()) {
var data = this.pause();
console.log.apply(console.log, args);
if (typeof data !== 'undefined' && data !== false) {
this.resume(data);
} else {
console.log('Log got back \'false\' as data. This shouldn\'t happen.', data);
}
} else {
console.log.apply(console.log, args);
}
return this;
}
/**
* Submits a given prompt.
*
* @param {String} value
* @return {UI}
* @api public
*/
}, {
key: 'submit',
value: function submit() {
if (this._activePrompt) {
// this._activePrompt.screen.onClose();
this._activePrompt.rl.emit('line');
// this._activePrompt.onEnd({isValid: true, value: value});
// to do - I don't know a good way to do this.
}
return this;
}
/**
* Does a literal, one-time write to the
* *current* prompt delimiter.
*
* @param {String} str
* @return {UI}
* @api public
*/
}, {
key: 'delimiter',
value: function delimiter(str) {
if (!this._activePrompt) {
return this;
}
var prompt = this._activePrompt;
if (str === undefined) {
return prompt.opt.message;
}
prompt.opt.message = str;
this.refresh();
return this;
}
/**
* Re-writes the input of an Inquirer prompt.
* If no string is passed, it gets the current
* input.
*
* @param {String} str
* @return {String}
* @api public
*/
}, {
key: 'input',
value: function input(str) {
if (!this._activePrompt) {
return undefined;
}
var prompt = this._activePrompt;
if (str === undefined) {
return prompt.rl.line;
}
var width = prompt.rl.line.length;
prompt.rl.line = str;
var newWidth = prompt.rl.line.length;
var diff = newWidth - width;
prompt.rl.cursor += diff;
var cursor = 0;
var message = prompt.getQuestion();
var addition = prompt.status === 'answered' ? chalk.cyan(prompt.answer) : prompt.rl.line;
message += addition;
prompt.screen.render(message, { cursor: cursor });
return this;
}
/**
* Logs the current delimiter and typed data.
*
* @return {UI}
* @api public
*/
}, {
key: 'imprint',
value: function imprint() {
if (!this.parent) {
return this;
}
var val = this._activePrompt.rl.line;
var delimiter = this._lastDelimiter || this.delimiter() || '';
this.log(delimiter + val);
return this;
}
/**
* Redraws the inquirer prompt with a new string.
*
* @param {String} str
* @return {UI}
* @api private
*/
}, {
key: 'refresh',
value: function refresh() {
if (!this.parent || !this._activePrompt) {
return this;
}
this._activePrompt.screen.clean();
this._activePrompt.render();
this._activePrompt.rl.output.write(this._activePrompt.rl.line);
return this;
}
/**
* Writes over existing logging.
*
* @param {String} str
* @return {UI}
* @api public
*/
}, {
key: 'redraw',
value: function redraw(str) {
logUpdate(str);
return this;
}
}]);
return UI;
}(EventEmitter);
/**
* Initialize singleton.
*/
var ui = new UI();
/**
* Clears logging from `ui.redraw`
* permanently.
*
* @return {UI}
* @api public
*/
ui.redraw.clear = function () {
logUpdate.clear();
return ui;
};
/**
* Prints logging from `ui.redraw`
* permanently.
*
* @return {UI}
* @api public
*/
ui.redraw.done = function () {
logUpdate.done();
ui.refresh();
return ui;
};
/**
* Expose `ui`.
*
* Modifying global? WTF?!? Yes. It is evil.
* However node.js prompts are also quite
* evil in a way. Nothing prevents dual prompts
* between applications in the same terminal,
* and inquirer doesn't catch or deal with this, so
* if you want to start two independent instances of
* vorpal, you need to know that prompt listeners
* have already been initiated, and that you can
* only attach the tty to one vorpal instance
* at a time.
* When you fire inqurier twice, you get a double-prompt,
* where every keypress fires twice and it's just a
* total mess. So forgive me.
*/
global.__vorpal = global.__vorpal || {};
global.__vorpal.ui = global.__vorpal.ui || {
exists: false,
exports: undefined
};
if (!global.__vorpal.ui.exists) {
global.__vorpal.ui.exists = true;
global.__vorpal.ui.exports = ui;
module.exports = exports = global.__vorpal.ui.exports;
} else {
module.exports = global.__vorpal.ui.exports;
}