diff --git a/lib/elements/autocomplete.js b/lib/elements/autocomplete.js index fecbff3e..8e06da21 100644 --- a/lib/elements/autocomplete.js +++ b/lib/elements/autocomplete.js @@ -23,6 +23,7 @@ const getIndex = (arr, valOrTitle) => { * @param {String} [opts.style='default'] Render style * @param {String} [opts.fallback] Fallback message - initial to default value * @param {String} [opts.initial] Index of the default value + * @param {Boolean} [opts.clearFirst] The first ESCAPE keypress will clear the input * @param {Stream} [opts.stdin] The Readable stream to listen to * @param {Stream} [opts.stdout] The Writable stream to write readline data to * @param {String} [opts.noMatches] The no matches found label @@ -39,6 +40,7 @@ class AutocompletePrompt extends Prompt { this.select = this.initial || opts.cursor || 0; this.i18n = { noMatches: opts.noMatches || 'no matches found' }; this.fallback = opts.fallback || this.initial; + this.clearFirst = opts.clearFirst || false; this.suggestions = []; this.input = ''; this.limit = opts.limit || 10; @@ -96,8 +98,22 @@ class AutocompletePrompt extends Prompt { this.render(); } + exit() { + if (this.clearFirst && this.input.length > 0) { + this.reset(); + } else { + this.done = this.exited = true; + this.aborted = false; + this.fire(); + this.render(); + this.out.write('\n'); + this.close(); + } + } + abort() { this.done = this.aborted = true; + this.exited = false; this.fire(); this.render(); this.out.write('\n'); @@ -106,7 +122,7 @@ class AutocompletePrompt extends Prompt { submit() { this.done = true; - this.aborted = false; + this.aborted = this.exited = false; this.fire(); this.render(); this.out.write('\n'); @@ -222,7 +238,7 @@ class AutocompletePrompt extends Prompt { let { startIndex, endIndex } = entriesToDisplay(this.select, this.choices.length, this.limit); this.outputText = [ - style.symbol(this.done, this.aborted), + style.symbol(this.done, this.aborted, this.exited), color.bold(this.msg), style.delimiter(this.completing), this.done && this.suggestions[this.select] diff --git a/lib/elements/confirm.js b/lib/elements/confirm.js index 12167cec..7a9173f1 100644 --- a/lib/elements/confirm.js +++ b/lib/elements/confirm.js @@ -34,6 +34,10 @@ class ConfirmPrompt extends Prompt { this.render(); } + exit() { + this.abort(); + } + abort() { this.done = this.aborted = true; this.fire(); diff --git a/lib/elements/date.js b/lib/elements/date.js index 75e3c6d5..71ff6082 100644 --- a/lib/elements/date.js +++ b/lib/elements/date.js @@ -101,6 +101,10 @@ class DatePrompt extends Prompt { this.render(); } + exit() { + this.abort(); + } + abort() { this.done = this.aborted = true; this.error = false; diff --git a/lib/elements/multiselect.js b/lib/elements/multiselect.js index a0b1fc9c..99b393fa 100644 --- a/lib/elements/multiselect.js +++ b/lib/elements/multiselect.js @@ -59,6 +59,10 @@ class MultiselectPrompt extends Prompt { return this.value.filter(v => v.selected); } + exit() { + this.abort(); + } + abort() { this.done = this.aborted = true; this.fire(); diff --git a/lib/elements/number.js b/lib/elements/number.js index a077dcf3..dc3efe9e 100644 --- a/lib/elements/number.js +++ b/lib/elements/number.js @@ -78,6 +78,10 @@ class NumberPrompt extends Prompt { this.render(); } + exit() { + this.abort(); + } + abort() { let x = this.value; this.value = x !== `` ? x : this.initial; diff --git a/lib/elements/prompt.js b/lib/elements/prompt.js index 3ed54b47..b7933300 100644 --- a/lib/elements/prompt.js +++ b/lib/elements/prompt.js @@ -19,7 +19,7 @@ class Prompt extends EventEmitter { this.in = opts.stdin || process.stdin; this.out = opts.stdout || process.stdout; this.onRender = (opts.onRender || (() => void 0)).bind(this); - const rl = readline.createInterface(this.in); + const rl = readline.createInterface({ input:this.in, escapeCodeTimeout:50 }); readline.emitKeypressEvents(this.in, rl); if (this.in.isTTY) this.in.setRawMode(true); @@ -40,7 +40,7 @@ class Prompt extends EventEmitter { this.in.removeListener('keypress', keypress); if (this.in.isTTY) this.in.setRawMode(false); rl.close(); - this.emit(this.aborted ? 'abort' : 'submit', this.value); + this.emit(this.aborted ? 'abort' : this.exited ? 'exit' : 'submit', this.value); this.closed = true; }; @@ -50,7 +50,8 @@ class Prompt extends EventEmitter { fire() { this.emit('state', { value: this.value, - aborted: !!this.aborted + aborted: !!this.aborted, + exited: !!this.exited }); } diff --git a/lib/elements/select.js b/lib/elements/select.js index b7d23cd6..6d6727f7 100644 --- a/lib/elements/select.js +++ b/lib/elements/select.js @@ -52,6 +52,10 @@ class SelectPrompt extends Prompt { this.render(); } + exit() { + this.abort(); + } + abort() { this.done = this.aborted = true; this.fire(); diff --git a/lib/elements/text.js b/lib/elements/text.js index f4f8e6c9..eee731b5 100644 --- a/lib/elements/text.js +++ b/lib/elements/text.js @@ -52,6 +52,10 @@ class TextPrompt extends Prompt { this.render(); } + exit() { + this.abort(); + } + abort() { this.value = this.value || this.initial; this.done = this.aborted = true; diff --git a/lib/elements/toggle.js b/lib/elements/toggle.js index 24e1aa2a..bad612ce 100644 --- a/lib/elements/toggle.js +++ b/lib/elements/toggle.js @@ -30,6 +30,10 @@ class TogglePrompt extends Prompt { this.render(); } + exit() { + this.abort(); + } + abort() { this.done = this.aborted = true; this.fire(); diff --git a/lib/prompts.js b/lib/prompts.js index df5d66bc..9f625564 100644 --- a/lib/prompts.js +++ b/lib/prompts.js @@ -8,8 +8,10 @@ function toPrompt(type, args, opts={}) { const p = new el[type](args); const onAbort = opts.onAbort || noop; const onSubmit = opts.onSubmit || noop; + const onExit = opts.onExit || noop; p.on('state', args.onState || noop); p.on('submit', x => res(onSubmit(x))); + p.on('exit', x => res(onExit(x))); p.on('abort', x => rej(onAbort(x))); }); } @@ -190,6 +192,7 @@ const byTitle = (input, choices) => Promise.resolve( * @param {number} [args.limit=10] Max number of results to show * @param {string} [args.style="default"] Render style ('default', 'password', 'invisible') * @param {String} [args.initial] Index of the default value + * @param {boolean} [opts.clearFirst] The first ESCAPE keypress will clear the input * @param {String} [args.fallback] Fallback message - defaults to initial value * @param {function} [args.onState] On state change callback * @param {Stream} [args.stdin] The Readable stream to listen to diff --git a/lib/util/action.js b/lib/util/action.js index 2be9bd39..fefbd947 100644 --- a/lib/util/action.js +++ b/lib/util/action.js @@ -1,7 +1,7 @@ 'use strict'; module.exports = (key, isSelect) => { - if (key.meta) return; + if (key.meta && key.name !== 'escape') return; if (key.ctrl) { if (key.name === 'a') return 'first'; @@ -21,7 +21,7 @@ module.exports = (key, isSelect) => { if (key.name === 'backspace') return 'delete'; if (key.name === 'delete') return 'deleteForward'; if (key.name === 'abort') return 'abort'; - if (key.name === 'escape') return 'abort'; + if (key.name === 'escape') return 'exit'; if (key.name === 'tab') return 'next'; if (key.name === 'pagedown') return 'nextPage'; if (key.name === 'pageup') return 'prevPage'; diff --git a/lib/util/style.js b/lib/util/style.js index 982ff81c..1851cc7d 100644 --- a/lib/util/style.js +++ b/lib/util/style.js @@ -16,11 +16,12 @@ const render = type => styles[type] || styles.default; const symbols = Object.freeze({ aborted: c.red(figures.cross), done: c.green(figures.tick), + exited: c.yellow(figures.cross), default: c.cyan('?') }); -const symbol = (done, aborted) => - aborted ? symbols.aborted : done ? symbols.done : symbols.default; +const symbol = (done, aborted, exited) => + aborted ? symbols.aborted : exited ? symbols.exited : done ? symbols.done : symbols.default; // between the question and the user's input. const delimiter = completing => diff --git a/readme.md b/readme.md index e13fb480..4a8b0654 100755 --- a/readme.md +++ b/readme.md @@ -799,9 +799,10 @@ You can overwrite how choices are being filtered by passing your own suggest fun | limit | `number` | Max number of results to show. Defaults to `10` | | style | `string` | Render style (`default`, `password`, `invisible`, `emoji`). Defaults to `'default'` | | initial | `string \| number` | Default initial value | +| clearFirst | `boolean` | The first ESCAPE keypress will clear the input | | fallback | `string` | Fallback message when no match is found. Defaults to `initial` value if provided | | onRender | `function` | On render callback. Keyword `this` refers to the current prompt | -| onState | `function` | On state change callback. Function signature is an `object` with two properties: `value` and `aborted` | +| onState | `function` | On state change callback. Function signature is an `object` with three properties: `value`, `aborted` and `exited` | Example on what a `suggest` function might look like: ```js