|
1 | 1 | import chalk from 'chalk';
|
| 2 | +import {InputCustomOptions} from 'inquirer'; |
2 | 3 |
|
3 |
| -import type {InputSetting, Prompter, Result, RuleEntry} from './types'; |
| 4 | +import type {InputSetting, RuleEntry, Result, ResultPart} from './types'; |
4 | 5 |
|
5 | 6 | import format from './format';
|
6 | 7 | import getForcedCaseFn from './get-forced-case-fn';
|
7 | 8 | import getForcedLeadingFn from './get-forced-leading-fn';
|
8 | 9 | import meta from './meta';
|
9 | 10 | import {
|
10 | 11 | enumRuleIsActive,
|
11 |
| - ruleIsNotApplicable, |
12 |
| - ruleIsApplicable, |
13 |
| - ruleIsActive, |
14 | 12 | getHasName,
|
15 | 13 | getMaxLength,
|
| 14 | + ruleIsActive, |
| 15 | + ruleIsApplicable, |
| 16 | + ruleIsNotApplicable, |
16 | 17 | } from './utils';
|
17 | 18 |
|
| 19 | +const EOL = '\n'; |
| 20 | + |
18 | 21 | /**
|
19 | 22 | * Get a cli prompt based on rule configuration
|
20 | 23 | * @param type type of the data to gather
|
21 |
| - * @param context rules to parse |
| 24 | + * @param rules |
| 25 | + * @param settings |
22 | 26 | * @return prompt instance
|
23 | 27 | */
|
24 | 28 | export default function getPrompt(
|
25 |
| - type: string, |
26 |
| - context: { |
27 |
| - rules?: RuleEntry[]; |
28 |
| - settings?: InputSetting; |
29 |
| - results?: Result; |
30 |
| - prompter?: () => Prompter; |
31 |
| - } = {} |
32 |
| -): Promise<string | undefined> { |
33 |
| - const {rules = [], settings = {}, results = {}, prompter} = context; |
34 |
| - |
35 |
| - if (typeof prompter !== 'function') { |
36 |
| - throw new TypeError('Missing prompter function in getPrompt context'); |
37 |
| - } |
38 |
| - |
39 |
| - const prompt = prompter(); |
40 |
| - |
41 |
| - if (typeof prompt.removeAllListeners !== 'function') { |
42 |
| - throw new TypeError( |
43 |
| - 'getPrompt: prompt.removeAllListeners is not a function' |
44 |
| - ); |
45 |
| - } |
46 |
| - |
47 |
| - if (typeof prompt.command !== 'function') { |
48 |
| - throw new TypeError('getPrompt: prompt.command is not a function'); |
49 |
| - } |
50 |
| - |
51 |
| - if (typeof prompt.catch !== 'function') { |
52 |
| - throw new TypeError('getPrompt: prompt.catch is not a function'); |
53 |
| - } |
54 |
| - |
55 |
| - if (typeof prompt.addListener !== 'function') { |
56 |
| - throw new TypeError('getPrompt: prompt.addListener is not a function'); |
57 |
| - } |
58 |
| - |
59 |
| - if (typeof prompt.log !== 'function') { |
60 |
| - throw new TypeError('getPrompt: prompt.log is not a function'); |
61 |
| - } |
62 |
| - |
63 |
| - if (typeof prompt.delimiter !== 'function') { |
64 |
| - throw new TypeError('getPrompt: prompt.delimiter is not a function'); |
65 |
| - } |
66 |
| - |
67 |
| - if (typeof prompt.show !== 'function') { |
68 |
| - throw new TypeError('getPrompt: prompt.show is not a function'); |
69 |
| - } |
70 |
| - |
71 |
| - const enumRule = rules.filter(getHasName('enum')).find(enumRuleIsActive); |
72 |
| - |
73 |
| - const emptyRule = rules.find(getHasName('empty')); |
74 |
| - |
75 |
| - const mustBeEmpty = |
76 |
| - emptyRule && ruleIsActive(emptyRule) && ruleIsApplicable(emptyRule); |
| 29 | + type: ResultPart, |
| 30 | + rules: RuleEntry[] = [], |
| 31 | + settings: InputSetting = {} |
| 32 | +): InputCustomOptions<Result> | null { |
| 33 | + const emptyRule = rules.filter(getHasName('empty')).find(ruleIsActive); |
77 | 34 |
|
78 |
| - const mayNotBeEmpty = |
79 |
| - emptyRule && ruleIsActive(emptyRule) && ruleIsNotApplicable(emptyRule); |
80 |
| - |
81 |
| - const mayBeEmpty = !mayNotBeEmpty; |
| 35 | + const mustBeEmpty = emptyRule ? ruleIsApplicable(emptyRule) : false; |
82 | 36 |
|
83 | 37 | if (mustBeEmpty) {
|
84 |
| - prompt.removeAllListeners('keypress'); |
85 |
| - prompt.removeAllListeners('client_prompt_submit'); |
86 |
| - prompt.ui.redraw.done(); |
87 |
| - return Promise.resolve(undefined); |
| 38 | + return null; |
88 | 39 | }
|
89 | 40 |
|
90 |
| - const caseRule = rules.find(getHasName('case')); |
91 |
| - |
92 |
| - const forceCaseFn = getForcedCaseFn(caseRule); |
| 41 | + const required = emptyRule ? ruleIsNotApplicable(emptyRule) : false; |
93 | 42 |
|
94 |
| - const leadingBlankRule = rules.find(getHasName('leading-blank')); |
95 |
| - |
96 |
| - const forceLeadingBlankFn = getForcedLeadingFn(leadingBlankRule); |
| 43 | + const forceCaseFn = getForcedCaseFn(rules.find(getHasName('case'))); |
| 44 | + const forceLeadingBlankFn = getForcedLeadingFn( |
| 45 | + rules.find(getHasName('leading-blank')) |
| 46 | + ); |
97 | 47 |
|
98 | 48 | const maxLengthRule = rules.find(getHasName('max-length'));
|
99 | 49 | const inputMaxLength = getMaxLength(maxLengthRule);
|
100 | 50 |
|
101 |
| - const headerLength = settings.header ? settings.header.length : Infinity; |
102 |
| - |
103 |
| - const remainingHeaderLength = headerLength |
104 |
| - ? headerLength - |
105 |
| - [ |
106 |
| - results.type, |
107 |
| - results.scope, |
108 |
| - results.scope ? '()' : '', |
109 |
| - results.type && results.scope ? ':' : '', |
110 |
| - results.subject, |
111 |
| - ].join('').length |
112 |
| - : Infinity; |
113 |
| - |
114 |
| - const maxLength = Math.min(inputMaxLength, remainingHeaderLength); |
115 |
| - |
116 |
| - return new Promise((resolve) => { |
117 |
| - // Add the defined enums as sub commands if applicable |
118 |
| - if (enumRule) { |
119 |
| - const [, [, , enums]] = enumRule; |
| 51 | + const enumRule = rules.filter(getHasName('enum')).find(enumRuleIsActive); |
120 | 52 |
|
121 |
| - enums.forEach((enumerable) => { |
| 53 | + const tabCompletion = enumRule |
| 54 | + ? enumRule[1][2].map((enumerable) => { |
122 | 55 | const enumSettings = (settings.enumerables || {})[enumerable] || {};
|
123 |
| - prompt |
124 |
| - .command(enumerable) |
125 |
| - .description(enumSettings.description || '') |
126 |
| - .action(() => { |
127 |
| - prompt.removeAllListeners(); |
128 |
| - prompt.ui.redraw.done(); |
129 |
| - return resolve(forceLeadingBlankFn(forceCaseFn(enumerable))); |
130 |
| - }); |
| 56 | + return { |
| 57 | + value: forceLeadingBlankFn(forceCaseFn(enumerable)), |
| 58 | + description: enumSettings.description || '', |
| 59 | + }; |
| 60 | + }) |
| 61 | + : []; |
| 62 | + |
| 63 | + const maxLength = (res: Result) => { |
| 64 | + let remainingHeaderLength = Infinity; |
| 65 | + if (settings.header && settings.header.length) { |
| 66 | + const header = format({ |
| 67 | + type: res.type, |
| 68 | + scope: res.scope, |
| 69 | + subject: res.subject, |
131 | 70 | });
|
132 |
| - } else { |
133 |
| - prompt.catch('[text...]').action((parameters) => { |
134 |
| - const {text = ''} = parameters; |
135 |
| - prompt.removeAllListeners(); |
136 |
| - prompt.ui.redraw.done(); |
137 |
| - return resolve(forceLeadingBlankFn(forceCaseFn(text.join(' ')))); |
138 |
| - }); |
139 |
| - } |
140 |
| - |
141 |
| - if (mayBeEmpty) { |
142 |
| - // Add an easy exit command |
143 |
| - prompt |
144 |
| - .command(':skip') |
145 |
| - .description('Skip the input if possible.') |
146 |
| - .action(() => { |
147 |
| - prompt.removeAllListeners(); |
148 |
| - prompt.ui.redraw.done(); |
149 |
| - resolve(''); |
150 |
| - }); |
| 71 | + remainingHeaderLength = settings.header.length - header.length; |
151 | 72 | }
|
152 |
| - |
153 |
| - // Handle empty input |
154 |
| - const onSubmit = (input: string) => { |
155 |
| - if (input.length > 0) { |
156 |
| - return; |
| 73 | + return Math.min(inputMaxLength, remainingHeaderLength); |
| 74 | + }; |
| 75 | + |
| 76 | + return { |
| 77 | + type: 'input-custom', |
| 78 | + name: type, |
| 79 | + message: `${type}:`, |
| 80 | + validate(input, answers) { |
| 81 | + if (input.length > maxLength(answers || {})) { |
| 82 | + return 'Input contains too many characters!'; |
157 | 83 | }
|
158 |
| - |
159 |
| - // Show help if enum is defined and input may not be empty |
160 |
| - if (mayNotBeEmpty) { |
161 |
| - prompt.ui.log(chalk.yellow(`⚠ ${chalk.bold(type)} may not be empty.`)); |
| 84 | + if (required && input.trim().length === 0) { |
| 85 | + // Show help if enum is defined and input may not be empty |
| 86 | + return `⚠ ${chalk.bold(type)} may not be empty.`; |
162 | 87 | }
|
163 | 88 |
|
164 |
| - if (mayBeEmpty) { |
165 |
| - prompt.ui.log( |
166 |
| - chalk.blue( |
167 |
| - `ℹ Enter ${chalk.bold(':skip')} to omit ${chalk.bold(type)}.` |
168 |
| - ) |
169 |
| - ); |
| 89 | + const tabValues = tabCompletion.map((item) => item.value); |
| 90 | + if ( |
| 91 | + input.length > 0 && |
| 92 | + tabValues.length > 0 && |
| 93 | + !tabValues.includes(input) |
| 94 | + ) { |
| 95 | + return `⚠ ${chalk.bold(type)} must be one of ${tabValues.join(', ')}.`; |
170 | 96 | }
|
171 |
| - |
172 |
| - if (enumRule) { |
173 |
| - prompt.exec('help'); |
| 97 | + return true; |
| 98 | + }, |
| 99 | + tabCompletion, |
| 100 | + log(answers?: Result) { |
| 101 | + let prefix = |
| 102 | + `${chalk.white('Please enter a')} ${chalk.bold(type)}: ${meta({ |
| 103 | + optional: !required, |
| 104 | + required: required, |
| 105 | + 'tab-completion': typeof enumRule !== 'undefined', |
| 106 | + header: typeof settings.header !== 'undefined', |
| 107 | + 'multi-line': settings.multiline, |
| 108 | + })}` + EOL; |
| 109 | + |
| 110 | + if (settings.description) { |
| 111 | + prefix += chalk.grey(`${settings.description}`) + EOL; |
174 | 112 | }
|
175 |
| - }; |
176 |
| - |
177 |
| - const drawRemaining = (length: number) => { |
178 |
| - if (length < Infinity) { |
179 |
| - const colors = [ |
180 |
| - { |
181 |
| - threshold: 5, |
182 |
| - color: chalk.red, |
183 |
| - }, |
184 |
| - { |
185 |
| - threshold: 10, |
186 |
| - color: chalk.yellow, |
187 |
| - }, |
188 |
| - { |
189 |
| - threshold: Infinity, |
190 |
| - color: chalk.grey, |
191 |
| - }, |
192 |
| - ]; |
193 |
| - |
194 |
| - const el = colors.find((item) => item.threshold >= length); |
195 |
| - const color = el ? el.color : chalk.grey; |
196 |
| - prompt.ui.redraw(color(`${length} characters left`)); |
| 113 | + if (answers) { |
| 114 | + prefix += EOL + `${format(answers, true)}` + EOL; |
197 | 115 | }
|
198 |
| - }; |
199 |
| - |
200 |
| - const onKey = (event: {value: string}) => { |
201 |
| - const sanitized = forceCaseFn(event.value); |
202 |
| - const cropped = sanitized.slice(0, maxLength); |
203 |
| - |
204 |
| - // We **could** do live editing, but there are some quirks to solve |
205 |
| - /* const live = merge({}, results, { |
206 |
| - [type]: cropped |
207 |
| - }); |
208 |
| - prompt.ui.redraw(`\n\n${format(live, true)}\n\n`); */ |
209 |
| - |
210 |
| - if (maxLength) { |
211 |
| - drawRemaining(maxLength - cropped.length); |
212 |
| - } |
213 |
| - prompt.ui.input(cropped); |
214 |
| - }; |
215 |
| - |
216 |
| - prompt.addListener('keypress', onKey); |
217 |
| - prompt.addListener('client_prompt_submit', onSubmit); |
218 |
| - |
219 |
| - prompt.log( |
220 |
| - `\n\nPlease enter a ${chalk.bold(type)}: ${meta({ |
221 |
| - optional: !mayNotBeEmpty, |
222 |
| - required: mayNotBeEmpty, |
223 |
| - 'tab-completion': typeof enumRule !== 'undefined', |
224 |
| - header: typeof settings.header !== 'undefined', |
225 |
| - 'multi-line': settings.multiline, |
226 |
| - })}` |
227 |
| - ); |
228 |
| - |
229 |
| - if (settings.description) { |
230 |
| - prompt.log(chalk.grey(`${settings.description}\n`)); |
231 |
| - } |
232 |
| - |
233 |
| - prompt.log(`\n\n${format(results, true)}\n\n`); |
234 |
| - |
235 |
| - drawRemaining(maxLength); |
236 |
| - |
237 |
| - prompt.delimiter(`❯ ${type}:`).show(); |
238 |
| - }); |
| 116 | + return prefix + EOL; |
| 117 | + }, |
| 118 | + maxLength, |
| 119 | + transformer(value: string) { |
| 120 | + return forceCaseFn(value); |
| 121 | + }, |
| 122 | + }; |
239 | 123 | }
|
0 commit comments