Skip to content

Commit

Permalink
feat(ui): add Enquirer as UI lib
Browse files Browse the repository at this point in the history
  • Loading branch information
jwx committed Feb 7, 2019
1 parent 1455a35 commit f05da1a
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 226 deletions.
2 changes: 1 addition & 1 deletion lib/commands/new/new-application.json
Expand Up @@ -79,7 +79,7 @@
"type": "input-text", "type": "input-text",
"id": 200, "id": 200,
"nextActivity": 300, "nextActivity": 300,
"question": "Please enter a name for your new project below.", "question": "Please enter a name for your new project:",
"stateProperty": "name", "stateProperty": "name",
"defaultValue": "aurelia-app" "defaultValue": "aurelia-app"
}, },
Expand Down
61 changes: 61 additions & 0 deletions lib/ui-enquirer.js
@@ -0,0 +1,61 @@
'use strict';
const os = require('os');
const { prompt } = require('enquirer');
const Enquirer = require('enquirer');
const colors = require("ansi-colors");
const transform = require('./colors/transform');
const createLines = require('./string').createLines;

module.exports = class {
constructor(ui) {
this.ui = ui;
}

async prompt(type, name, question, initial, options, autoSubmit) {
const choices = options && options[0].displayName ? this.convertOptions(options) : options;
const enquirer = new Enquirer();
if (autoSubmit) {
enquirer.answers[name] = initial;
}
const answers = await enquirer.prompt({
type: type,
name: name,
message: question,
initial: initial,
choices: choices,
styles: {
em: colors.cyan,
},
separator: ' ' + (autoSubmit ? transform(` <cyan>${initial}</cyan>`) : ''),
header: ' ',
footer: ' ',
choicesHeader: ' ',
onSubmit() {
if (type === 'multiselect' && this.selected.length === 0) {
this.enable(this.focused);
}
},
autofill: autoSubmit ? 'show' : false,
});
if (type === 'multiselect') {
return (options && options[0].displayName ? answers[name].map((option) => this.findValue(options, option)) : answers[name]) || [];
}
else {
return (options && options[0].displayName) || autoSubmit ? this.findValue(options, answers[name]) : answers[name];
}
}

convertOptions(options) {
return options.map((option, index, options) => {
return {
// name: option.value,
value: option.displayName,
message: `${index}. ${option.displayName}`,
hint: os.EOL + transform(`<gray>${createLines(option.description, ' ', this.ui.getWidth())}</gray>`),
}
});
}
findValue(options, displayName) {
return options.find((option) => option.displayName === displayName).value;
}
};
154 changes: 41 additions & 113 deletions lib/ui.js
Expand Up @@ -5,12 +5,14 @@ const fs = require('./file-system');
const transform = require('./colors/transform'); const transform = require('./colors/transform');
const createLines = require('./string').createLines; const createLines = require('./string').createLines;
const tty = require('tty'); const tty = require('tty');
const UIEnquirer = require('./ui-enquirer');


exports.UI = class {}; exports.UI = class { };


exports.ConsoleUI = class { exports.ConsoleUI = class {
constructor(cliOptions) { constructor(cliOptions) {
this.cliOptions = cliOptions; this.cliOptions = cliOptions;
this.uiEnquirer = new UIEnquirer(this);
} }


open() { open() {
Expand Down Expand Up @@ -46,62 +48,47 @@ exports.ConsoleUI = class {
return this.question(question, suggestion); return this.question(question, suggestion);
} }


question(text, optionsOrSuggestion) { async question(question, optionsOrSuggestion, defaultValue) {
return new Promise((resolve, reject) => { if (!optionsOrSuggestion || typeof optionsOrSuggestion === 'string') {
if (!optionsOrSuggestion || typeof optionsOrSuggestion === 'string') { const answer = await this.uiEnquirer.prompt(
this.open(); 'input',

'noNameNeeded',
let fullText = os.EOL + text + os.EOL + os.EOL; question,

optionsOrSuggestion,
if (optionsOrSuggestion) { );
fullText += '[' + optionsOrSuggestion + ']'; if (answer && answer.length) {
} return answer;

this.rl.question(fullText + '> ', answer => {
this.close();

answer = answer || optionsOrSuggestion;

if (answer) {
resolve(answer);
} else {
return this.question(text, optionsOrSuggestion).then(theAnswer => resolve(theAnswer));
}
});
} else { } else {
optionsOrSuggestion = optionsOrSuggestion.filter(x => includeOption(this.cliOptions, x)); return this.question(question, optionsOrSuggestion);

if (optionsOrSuggestion.length === 1) {
return resolve(optionsOrSuggestion[0]);
}

let defaultOption = optionsOrSuggestion[0];
let fullText = os.EOL + text + os.EOL
+ createOptionsText(this, optionsOrSuggestion) + os.EOL + '[' + defaultOption.displayName + ']' + '> ';

this.open();
this.rl.question(fullText, answer => {
this.close();
resolve(interpretAnswer(answer, optionsOrSuggestion));
});
} }
}); } else {
optionsOrSuggestion = optionsOrSuggestion.filter(x => includeOption(this.cliOptions, x));
let autoSubmit = false;
if (optionsOrSuggestion.length === 1) {
defaultValue = optionsOrSuggestion[0].displayName;
optionsOrSuggestion[0].name = optionsOrSuggestion[0].value.id;
autoSubmit = true;
}

return this.uiEnquirer.prompt(
'select',
'noNameNeeded',
question,
defaultValue,
optionsOrSuggestion,
autoSubmit,
);
}
} }


multiselect(question, options) { multiselect(question, options, defaultValue) {
return new Promise(resolve => { return this.uiEnquirer.prompt(
let info = 'Select one or more options separated by spaces'; 'multiselect',
let fullText = os.EOL + question + os.EOL 'noNameNeeded',
+ createOptionsText(this, options, true) + os.EOL + info + os.EOL + '> '; question,

defaultValue,
this.open(); options
this.rl.question(fullText, answer => { );
this.close();
let answers = answer.split(' ');
answers = answers.filter(x => x.length > 0);
resolve(interpretAnswers(answers, options));
});
});
} }


getWidth() { getWidth() {
Expand Down Expand Up @@ -137,65 +124,6 @@ function includeOption(cliOptions, option) {
return true; return true;
} }


function createOptionsText(ui, options, multi) {
let text = os.EOL;

for (let i = 0; i < options.length; ++i) {
text += `${i + 1}. ${options[i].displayName}`;

if (!multi && i === 0) {
text += ' (Default)';
}

text += os.EOL;

if (options[i].description) {
text += createLines(`<dim>${options[i].description}</dim>`, ' ', ui.getWidth());
text += os.EOL;
}
}

return transform(text);
}

function interpretAnswer(answer, options) {
if (!answer) {
return options[0];
}

let lowerCasedAnswer = answer.toLowerCase();
let found = options.find(x => x.displayName.toLowerCase().startsWith(lowerCasedAnswer));

if (found) {
return found;
}

let num = parseInt(answer, 10);
return options[num - 1] || options[0];
}

function interpretAnswers(answers, options) {
let foundAnswers = [];

for (let i = 0; i < answers.length; i++) {
let lowerCasedAnswer = answers[i].toLowerCase();
let found = options.find(x => x.displayName.toLowerCase().startsWith(lowerCasedAnswer));

if (found) {
foundAnswers.push(found);
continue;
}

let num = parseInt(answers[i], 10);

if (options[num - 1]) {
foundAnswers.push(options[num - 1]);
}
}

return foundAnswers;
}

function getWindowSize() { function getWindowSize() {
let width; let width;
let height; let height;
Expand All @@ -216,5 +144,5 @@ function getWindowSize() {
height = 100; height = 100;
} }


return {height: height, width: width}; return { height: height, width: width };
} }
15 changes: 4 additions & 11 deletions lib/workflow/activities/input-multiselect.js
Expand Up @@ -8,16 +8,9 @@ module.exports = class {
this.ui = ui; this.ui = ui;
} }


execute(context) { async execute(context) {
return this.ui.multiselect(this.question, this.options).then(answers => { const answers = await this.ui.multiselect(this.question, this.options);
context.state[this.stateProperty] = []; context.state[this.stateProperty] = answers.slice();

context.next(this.nextActivity);
for (let i = 0; i < answers.length; i++) {
let answer = answers[i];

context.state[this.stateProperty].push(answer.value);
}
context.next(this.nextActivity);
});
} }
}; };
10 changes: 4 additions & 6 deletions lib/workflow/activities/input-select.js
Expand Up @@ -8,17 +8,15 @@ module.exports = class {
this.ui = ui; this.ui = ui;
} }


execute(context) { async execute(context) {
let overrideProperty = this.stateProperty + 'Override'; let overrideProperty = this.stateProperty + 'Override';


if (overrideProperty in context.state) { if (overrideProperty in context.state) {
context.state[this.stateProperty] = context.state[overrideProperty]; context.state[this.stateProperty] = context.state[overrideProperty];
context.next(this.nextActivity);
} else { } else {
return this.ui.question(this.question, this.options).then(answer => { const answer = await this.ui.question(this.question, this.options, this.defaultValue);
context.state[this.stateProperty] = answer.value; context.state[this.stateProperty] = answer;
context.next(this.nextActivity);
});
} }
context.next(this.nextActivity);
} }
}; };
9 changes: 4 additions & 5 deletions lib/workflow/activities/input-text.js
Expand Up @@ -8,10 +8,9 @@ module.exports = class {
this.ui = ui; this.ui = ui;
} }


execute(context) { async execute(context) {
return this.ui.ensureAnswer(context.state[this.stateProperty], this.question, this.defaultValue).then(answer => { const answer = await this.ui.ensureAnswer(context.state[this.stateProperty], this.question, this.defaultValue);
context.state[this.stateProperty] = answer; context.state[this.stateProperty] = answer;
context.next(this.nextActivity); context.next(this.nextActivity);
});
} }
}; };
45 changes: 22 additions & 23 deletions lib/workflow/activities/project-create.js
Expand Up @@ -15,7 +15,7 @@ module.exports = class {
this.options = options; this.options = options;
} }


execute(context) { async execute(context) {
let model = { let model = {
name: context.state.name, name: context.state.name,
type: context.state.type, type: context.state.type,
Expand Down Expand Up @@ -59,28 +59,27 @@ module.exports = class {
this.ui.log(JSON.stringify(model, null, 2)); this.ui.log(JSON.stringify(model, null, 2));
} }


return this.ui.log(this.createProjectDescription(model)) if (context.state.defaultOrCustom !== 'custom') {
.then(() => this.projectConfirmation(project)) await this.ui.log(this.createProjectDescription(model));
.then(answer => { }
if (answer.value === 'yes') { const answer = await this.projectConfirmation(project);
let configurator = require(`../../commands/new/buildsystems/${model.bundler.id}`); if (answer === 'yes') {
configurator(project, this.options); const configurator = require(`../../commands/new/buildsystems/${model.bundler.id}`);

configurator(project, this.options);
return project.create(this.ui, this.options.hasFlag('here') ? undefined : process.cwd())
.then(() => this.ui.log('Project structure created and configured.' + os.EOL)) return project.create(this.ui, this.options.hasFlag('here') ? undefined : process.cwd())
.then(() => project.renderManualInstructions()) .then(() => this.ui.log('Project structure created and configured.' + os.EOL))
.then(() => context.next(this.nextActivity)); .then(() => project.renderManualInstructions())
} else if (answer.value === 'restart') { .then(() => context.next(this.nextActivity)).catch(e => {
return context.next(this.restartActivity); logger.error(`Failed to create the project due to an error: ${e.message}`);
} logger.info(e.stack);

});
return this.ui.log(os.EOL + 'Project creation aborted.') } else if (answer === 'restart') {
.then(() => context.next()); return context.next(this.restartActivity);
}) }
.catch(e => {
logger.error(`Failed to create the project due to an error: ${e.message}`); return this.ui.log(os.EOL + 'Project creation aborted.')
logger.info(e.stack); .then(() => context.next());
});
} }


createProjectDescription(model) { createProjectDescription(model) {
Expand Down

0 comments on commit f05da1a

Please sign in to comment.