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
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"type": "input-text",
"id": 200,
"nextActivity": 300,
"question": "Please enter a name for your new project below.",
"question": "Please enter a name for your new project:",
"stateProperty": "name",
"defaultValue": "aurelia-app"
},
Expand Down
61 changes: 61 additions & 0 deletions lib/ui-enquirer.js
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ const fs = require('./file-system');
const transform = require('./colors/transform');
const createLines = require('./string').createLines;
const tty = require('tty');
const UIEnquirer = require('./ui-enquirer');

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

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

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

question(text, optionsOrSuggestion) {
return new Promise((resolve, reject) => {
if (!optionsOrSuggestion || typeof optionsOrSuggestion === 'string') {
this.open();

let fullText = os.EOL + text + os.EOL + os.EOL;

if (optionsOrSuggestion) {
fullText += '[' + optionsOrSuggestion + ']';
}

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

answer = answer || optionsOrSuggestion;

if (answer) {
resolve(answer);
} else {
return this.question(text, optionsOrSuggestion).then(theAnswer => resolve(theAnswer));
}
});
async question(question, optionsOrSuggestion, defaultValue) {
if (!optionsOrSuggestion || typeof optionsOrSuggestion === 'string') {
const answer = await this.uiEnquirer.prompt(
'input',
'noNameNeeded',
question,
optionsOrSuggestion,
);
if (answer && answer.length) {
return answer;
} else {
optionsOrSuggestion = optionsOrSuggestion.filter(x => includeOption(this.cliOptions, x));

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));
});
return this.question(question, 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) {
return new Promise(resolve => {
let info = 'Select one or more options separated by spaces';
let fullText = os.EOL + question + os.EOL
+ createOptionsText(this, options, true) + os.EOL + info + os.EOL + '> ';

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

getWidth() {
Expand Down Expand Up @@ -137,65 +124,6 @@ function includeOption(cliOptions, option) {
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() {
let width;
let height;
Expand All @@ -216,5 +144,5 @@ function getWindowSize() {
height = 100;
}

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

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

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

context.state[this.stateProperty].push(answer.value);
}
context.next(this.nextActivity);
});
async execute(context) {
const answers = await this.ui.multiselect(this.question, this.options);
context.state[this.stateProperty] = answers.slice();
context.next(this.nextActivity);
}
};
10 changes: 4 additions & 6 deletions lib/workflow/activities/input-select.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,15 @@ module.exports = class {
this.ui = ui;
}

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

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

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

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

return this.ui.log(this.createProjectDescription(model))
.then(() => this.projectConfirmation(project))
.then(answer => {
if (answer.value === 'yes') {
let 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))
.then(() => project.renderManualInstructions())
.then(() => context.next(this.nextActivity));
} else if (answer.value === 'restart') {
return context.next(this.restartActivity);
}

return this.ui.log(os.EOL + 'Project creation aborted.')
.then(() => context.next());
})
.catch(e => {
logger.error(`Failed to create the project due to an error: ${e.message}`);
logger.info(e.stack);
});
if (context.state.defaultOrCustom !== 'custom') {
await this.ui.log(this.createProjectDescription(model));
}
const answer = await this.projectConfirmation(project);
if (answer === 'yes') {
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))
.then(() => project.renderManualInstructions())
.then(() => context.next(this.nextActivity)).catch(e => {
logger.error(`Failed to create the project due to an error: ${e.message}`);
logger.info(e.stack);
});
} else if (answer === 'restart') {
return context.next(this.restartActivity);
}

return this.ui.log(os.EOL + 'Project creation aborted.')
.then(() => context.next());
}

createProjectDescription(model) {
Expand Down
Loading

0 comments on commit f05da1a

Please sign in to comment.