expand.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. /**
  2. * `rawlist` type prompt
  3. */
  4. import chalk from 'chalk';
  5. import { map, takeUntil } from 'rxjs';
  6. import Base from './base.js';
  7. import Separator from '../objects/separator.js';
  8. import observe from '../utils/events.js';
  9. import Paginator from '../utils/paginator.js';
  10. export default class ExpandPrompt extends Base {
  11. constructor(questions, rl, answers) {
  12. super(questions, rl, answers);
  13. if (!this.opt.choices) {
  14. this.throwParamError('choices');
  15. }
  16. this.validateChoices(this.opt.choices);
  17. // Add the default `help` (/expand) option
  18. this.opt.choices.push({
  19. key: 'h',
  20. name: 'Help, list all options',
  21. value: 'help',
  22. });
  23. this.opt.validate = (choice) => {
  24. if (choice == null) {
  25. return 'Please enter a valid command';
  26. }
  27. return choice !== 'help';
  28. };
  29. // Setup the default string (capitalize the default key)
  30. this.opt.default = this.generateChoicesString(this.opt.choices, this.opt.default);
  31. this.paginator = new Paginator(this.screen);
  32. }
  33. /**
  34. * Start the Inquiry session
  35. * @param {Function} cb Callback when prompt is done
  36. * @return {this}
  37. */
  38. _run(cb) {
  39. this.done = cb;
  40. // Save user answer and update prompt to show selected option.
  41. const events = observe(this.rl);
  42. const validation = this.handleSubmitEvents(
  43. events.line.pipe(map(this.getCurrentValue.bind(this)))
  44. );
  45. validation.success.forEach(this.onSubmit.bind(this));
  46. validation.error.forEach(this.onError.bind(this));
  47. this.keypressObs = events.keypress
  48. .pipe(takeUntil(validation.success))
  49. .forEach(this.onKeypress.bind(this));
  50. // Init the prompt
  51. this.render();
  52. return this;
  53. }
  54. /**
  55. * Render the prompt to screen
  56. * @return {ExpandPrompt} self
  57. */
  58. render(error, hint) {
  59. let message = this.getQuestion();
  60. let bottomContent = '';
  61. if (this.status === 'answered') {
  62. message += chalk.cyan(this.answer);
  63. } else if (this.status === 'expanded') {
  64. const choicesStr = renderChoices(this.opt.choices, this.selectedKey);
  65. message += this.paginator.paginate(choicesStr, this.selectedKey, this.opt.pageSize);
  66. message += '\n Answer: ';
  67. }
  68. message += this.rl.line;
  69. if (error) {
  70. bottomContent = chalk.red('>> ') + error;
  71. }
  72. if (hint) {
  73. bottomContent = chalk.cyan('>> ') + hint;
  74. }
  75. this.screen.render(message, bottomContent);
  76. }
  77. getCurrentValue(input) {
  78. if (!input) {
  79. input = this.rawDefault;
  80. }
  81. const selected = this.opt.choices.where({ key: input.toLowerCase().trim() })[0];
  82. if (!selected) {
  83. return null;
  84. }
  85. return selected.value;
  86. }
  87. /**
  88. * Generate the prompt choices string
  89. * @return {String} Choices string
  90. */
  91. getChoices() {
  92. let output = '';
  93. this.opt.choices.forEach((choice) => {
  94. output += '\n ';
  95. if (choice.type === 'separator') {
  96. output += ' ' + choice;
  97. return;
  98. }
  99. let choiceStr = choice.key + ') ' + choice.name;
  100. if (this.selectedKey === choice.key) {
  101. choiceStr = chalk.cyan(choiceStr);
  102. }
  103. output += choiceStr;
  104. });
  105. return output;
  106. }
  107. onError(state) {
  108. if (state.value === 'help') {
  109. this.selectedKey = '';
  110. this.status = 'expanded';
  111. this.render();
  112. return;
  113. }
  114. this.render(state.isValid);
  115. }
  116. /**
  117. * When user press `enter` key
  118. */
  119. onSubmit(state) {
  120. this.status = 'answered';
  121. const choice = this.opt.choices.where({ value: state.value })[0];
  122. this.answer = choice.short || choice.name;
  123. // Re-render prompt
  124. this.render();
  125. this.screen.done();
  126. this.done(state.value);
  127. }
  128. /**
  129. * When user press a key
  130. */
  131. onKeypress() {
  132. this.selectedKey = this.rl.line.toLowerCase();
  133. const selected = this.opt.choices.where({ key: this.selectedKey })[0];
  134. if (this.status === 'expanded') {
  135. this.render();
  136. } else {
  137. this.render(null, selected ? selected.name : null);
  138. }
  139. }
  140. /**
  141. * Validate the choices
  142. * @param {Array} choices
  143. */
  144. validateChoices(choices) {
  145. let formatError;
  146. const errors = [];
  147. const keymap = {};
  148. choices.filter(Separator.exclude).forEach((choice) => {
  149. if (!choice.key || choice.key.length !== 1) {
  150. formatError = true;
  151. }
  152. choice.key = String(choice.key).toLowerCase();
  153. if (keymap[choice.key]) {
  154. errors.push(choice.key);
  155. }
  156. keymap[choice.key] = true;
  157. });
  158. if (formatError) {
  159. throw new Error(
  160. 'Format error: `key` param must be a single letter and is required.'
  161. );
  162. }
  163. if (keymap.h) {
  164. throw new Error(
  165. 'Reserved key error: `key` param cannot be `h` - this value is reserved.'
  166. );
  167. }
  168. if (errors.length) {
  169. throw new Error(
  170. 'Duplicate key error: `key` param must be unique. Duplicates: ' +
  171. [...new Set(errors)].join(',')
  172. );
  173. }
  174. }
  175. /**
  176. * Generate a string out of the choices keys
  177. * @param {Array} choices
  178. * @param {Number|String} default - the choice index or name to capitalize
  179. * @return {String} The rendered choices key string
  180. */
  181. generateChoicesString(choices, defaultChoice) {
  182. let defIndex = choices.realLength - 1;
  183. if (typeof defaultChoice === 'number' && this.opt.choices.getChoice(defaultChoice)) {
  184. defIndex = defaultChoice;
  185. } else if (typeof defaultChoice === 'string') {
  186. const index = choices.realChoices.findIndex(({ value }) => value === defaultChoice);
  187. defIndex = index === -1 ? defIndex : index;
  188. }
  189. const defStr = this.opt.choices.pluck('key');
  190. this.rawDefault = defStr[defIndex];
  191. defStr[defIndex] = String(defStr[defIndex]).toUpperCase();
  192. return defStr.join('');
  193. }
  194. }
  195. /**
  196. * Function for rendering checkbox choices
  197. * @param {String} pointer Selected key
  198. * @return {String} Rendered content
  199. */
  200. function renderChoices(choices, pointer) {
  201. let output = '';
  202. choices.forEach((choice) => {
  203. output += '\n ';
  204. if (choice.type === 'separator') {
  205. output += ' ' + choice;
  206. return;
  207. }
  208. let choiceStr = choice.key + ') ' + choice.name;
  209. if (pointer === choice.key) {
  210. choiceStr = chalk.cyan(choiceStr);
  211. }
  212. output += choiceStr;
  213. });
  214. return output;
  215. }