Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

This PR will fix the keypress event lost on windows #313

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,3 @@ script:
- npm test
node_js:
- "8"
- "6"
1 change: 1 addition & 0 deletions lib/elements/autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const getIndex = (arr, valOrTitle) => {
* @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 {Boolean} [opts.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
* @param {String} [opts.noMatches] The no matches found label
*/
class AutocompletePrompt extends Prompt {
Expand Down
1 change: 1 addition & 0 deletions lib/elements/autocompleteMultiselect.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const { clear, style, figures } = require('../util');
* @param {Number} [opts.cursor=0] Cursor start position
* @param {Stream} [opts.stdin] The Readable stream to listen to
* @param {Stream} [opts.stdout] The Writable stream to write readline data to
* @param {Boolean} [opts.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
*/
class AutocompleteMultiselectPrompt extends MultiselectPrompt {
constructor(opts={}) {
Expand Down
1 change: 1 addition & 0 deletions lib/elements/confirm.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const { erase, cursor } = require('sisteransi');
* @param {Boolean} [opts.initial] Default value (true/false)
* @param {Stream} [opts.stdin] The Readable stream to listen to
* @param {Stream} [opts.stdout] The Writable stream to write readline data to
* @param {Boolean} [opts.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
* @param {String} [opts.yes] The "Yes" label
* @param {String} [opts.yesOption] The "Yes" option when choosing between yes/no
* @param {String} [opts.no] The "No" label
Expand Down
1 change: 1 addition & 0 deletions lib/elements/date.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const dfltLocales = {
* @param {Function} [opts.validate] Function to validate the submitted value
* @param {Stream} [opts.stdin] The Readable stream to listen to
* @param {Stream} [opts.stdout] The Writable stream to write readline data to
* @param {Boolean} [opts.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
*/
class DatePrompt extends Prompt {
constructor(opts={}) {
Expand Down
1 change: 1 addition & 0 deletions lib/elements/multiselect.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const { clear, figures, style, wrap, entriesToDisplay } = require('../util');
* @param {Number} [opts.optionsPerPage=10] Max options to display at once
* @param {Stream} [opts.stdin] The Readable stream to listen to
* @param {Stream} [opts.stdout] The Writable stream to write readline data to
* @param {Boolean} [opts.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
*/
class MultiselectPrompt extends Prompt {
constructor(opts={}) {
Expand Down
1 change: 1 addition & 0 deletions lib/elements/number.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const round = (number, precision) => {
* @param {Function} [opts.validate] Validate function
* @param {Stream} [opts.stdin] The Readable stream to listen to
* @param {Stream} [opts.stdout] The Writable stream to write readline data to
* @param {Boolean} [opts.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
* @param {String} [opts.error] The invalid error label
*/
class NumberPrompt extends Prompt {
Expand Down
53 changes: 37 additions & 16 deletions lib/elements/prompt.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,62 @@ const color = require('kleur');
* Base prompt skeleton
* @param {Stream} [opts.stdin] The Readable stream to listen to
* @param {Stream} [opts.stdout] The Writable stream to write readline data to
* @param {Boolean} [opts.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
*/
class Prompt extends EventEmitter {
constructor(opts={}) {
constructor(opts = {}) {
super();

this.firstRender = true;
global._prompt = !!global._prompt ? global._prompt : { listener: [] };
this.removeInputListenerManually = opts.removeInputListenerManually || false;
this.in = opts.stdin || process.stdin;
this.out = opts.stdout || process.stdout;
this.onRender = (opts.onRender || (() => void 0)).bind(this);
const rl = readline.createInterface({ input:this.in, escapeCodeTimeout:50 });
readline.emitKeypressEvents(this.in, rl);

if (this.in.isTTY) this.in.setRawMode(true);
const isSelect = [ 'SelectPrompt', 'MultiselectPrompt' ].indexOf(this.constructor.name) > -1;
if (!this.removeInputListenerManually || !global._prompt.listening) {
global._prompt.rl = readline.createInterface({ input: this.in, escapeCodeTimeout: 50 });
readline.emitKeypressEvents(this.in, global._prompt.rl);
if (this.in.isTTY) this.in.setRawMode(true);
global._prompt.in = this.in;
global._prompt.kill = () => {
global._prompt.in.removeListener('keypress', keypress);
if (global._prompt.in.isTTY) global._prompt.in.setRawMode(false);
global._prompt.rl.close();
global._prompt.listening = false;
}
}

this._isSelect = ['SelectPrompt', 'MultiselectPrompt'].indexOf(this.constructor.name) > -1;
const keypress = (str, key) => {
let a = action(key, isSelect);
if (a === false) {
this._ && this._(str, key);
} else if (typeof this[a] === 'function') {
this[a](key);
} else {
this.bell();
if (global._prompt.listener.length) {
const __this = global._prompt.listener[0];
let a = action(key, __this._isSelect);
if (a === false) {
__this._ && __this._(str, key);
} else if (typeof __this[a] === 'function') {
__this[a](key);
} else {
__this.bell();
}
}
};

this.close = () => {
this.out.write(cursor.show);
this.in.removeListener('keypress', keypress);
if (this.in.isTTY) this.in.setRawMode(false);
rl.close();
global._prompt.listener.pop();
if (!this.removeInputListenerManually || !global._prompt.listening) {
global._prompt.kill();
}
this.emit(this.aborted ? 'abort' : this.exited ? 'exit' : 'submit', this.value);
this.closed = true;
};

this.in.on('keypress', keypress);
if (!this.removeInputListenerManually || !global._prompt.listening) {
this.in.on('keypress', keypress);
}
global._prompt.listener.push(this);
global._prompt.listening = this.removeInputListenerManually ? true : false;
}

fire() {
Expand Down
1 change: 1 addition & 0 deletions lib/elements/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const { cursor } = require('sisteransi');
* @param {Number} [opts.initial] Index of default value
* @param {Stream} [opts.stdin] The Readable stream to listen to
* @param {Stream} [opts.stdout] The Writable stream to write readline data to
* @param {Boolean} [opts.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
* @param {Number} [opts.optionsPerPage=10] Max options to display at once
*/
class SelectPrompt extends Prompt {
Expand Down
1 change: 1 addition & 0 deletions lib/elements/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const { style, clear, lines, figures } = require('../util');
* @param {Function} [opts.validate] Validate function
* @param {Stream} [opts.stdin] The Readable stream to listen to
* @param {Stream} [opts.stdout] The Writable stream to write readline data to
* @param {Boolean} [opts.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
* @param {String} [opts.error] The invalid error label
*/
class TextPrompt extends Prompt {
Expand Down
1 change: 1 addition & 0 deletions lib/elements/toggle.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const { cursor, erase } = require('sisteransi');
* @param {String} [opts.inactive='off'] Inactive label
* @param {Stream} [opts.stdin] The Readable stream to listen to
* @param {Stream} [opts.stdout] The Writable stream to write readline data to
* @param {Boolean} [opts.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
*/
class TogglePrompt extends Prompt {
constructor(opts={}) {
Expand Down
8 changes: 7 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,10 @@ function override(answers) {
prompt._override = Object.assign({}, answers);
}

module.exports = Object.assign(prompt, { prompt, prompts, inject, override });
function removeInputListener() {
if(global._prompt.kill) {
global._prompt.kill();
}
}

module.exports = Object.assign(prompt, { prompt, prompts, inject, override, removeInputListener });
11 changes: 11 additions & 0 deletions lib/prompts.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ function toPrompt(type, args, opts={}) {
* @param {function} [args.validate] Function to validate user input
* @param {Stream} [args.stdin] The Readable stream to listen to
* @param {Stream} [args.stdout] The Writable stream to write readline data to
* @param {boolean} [args.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
* @returns {Promise} Promise with user input
*/
$.text = args => toPrompt('TextPrompt', args);
Expand All @@ -37,6 +38,7 @@ $.text = args => toPrompt('TextPrompt', args);
* @param {function} [args.validate] Function to validate user input
* @param {Stream} [args.stdin] The Readable stream to listen to
* @param {Stream} [args.stdout] The Writable stream to write readline data to
* @param {boolean} [args.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
* @returns {Promise} Promise with user input
*/
$.password = args => {
Expand All @@ -52,6 +54,7 @@ $.password = args => {
* @param {function} [args.validate] Function to validate user input
* @param {Stream} [args.stdin] The Readable stream to listen to
* @param {Stream} [args.stdout] The Writable stream to write readline data to
* @param {boolean} [args.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
* @returns {Promise} Promise with user input
*/
$.invisible = args => {
Expand All @@ -73,6 +76,7 @@ $.invisible = args => {
* @param {function} [args.validate] Function to validate user input
* @param {Stream} [args.stdin] The Readable stream to listen to
* @param {Stream} [args.stdout] The Writable stream to write readline data to
* @param {boolean} [args.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
* @returns {Promise} Promise with user input
*/
$.number = args => toPrompt('NumberPrompt', args);
Expand All @@ -91,6 +95,7 @@ $.number = args => toPrompt('NumberPrompt', args);
* @param {function} [args.validate] Function to validate user input
* @param {Stream} [args.stdin] The Readable stream to listen to
* @param {Stream} [args.stdout] The Writable stream to write readline data to
* @param {boolean} [args.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
* @returns {Promise} Promise with user input
*/
$.date = args => toPrompt('DatePrompt', args);
Expand All @@ -102,6 +107,7 @@ $.date = args => toPrompt('DatePrompt', args);
* @param {function} [args.onState] On state change callback
* @param {Stream} [args.stdin] The Readable stream to listen to
* @param {Stream} [args.stdout] The Writable stream to write readline data to
* @param {boolean} [args.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
* @returns {Promise} Promise with user input
*/
$.confirm = args => toPrompt('ConfirmPrompt', args);
Expand All @@ -115,6 +121,7 @@ $.confirm = args => toPrompt('ConfirmPrompt', args);
* @param {function} [args.onState] On state change callback
* @param {Stream} [args.stdin] The Readable stream to listen to
* @param {Stream} [args.stdout] The Writable stream to write readline data to
* @param {boolean} [args.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
* @returns {Promise} Promise with user input, in form of an `Array`
*/
$.list = args => {
Expand All @@ -133,6 +140,7 @@ $.list = args => {
* @param {function} [args.onState] On state change callback
* @param {Stream} [args.stdin] The Readable stream to listen to
* @param {Stream} [args.stdout] The Writable stream to write readline data to
* @param {boolean} [args.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
* @returns {Promise} Promise with user input
*/
$.toggle = args => toPrompt('TogglePrompt', args);
Expand All @@ -146,6 +154,7 @@ $.toggle = args => toPrompt('TogglePrompt', args);
* @param {function} [args.onState] On state change callback
* @param {Stream} [args.stdin] The Readable stream to listen to
* @param {Stream} [args.stdout] The Writable stream to write readline data to
* @param {boolean} [args.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
* @returns {Promise} Promise with user input
*/
$.select = args => toPrompt('SelectPrompt', args);
Expand All @@ -160,6 +169,7 @@ $.select = args => toPrompt('SelectPrompt', args);
* @param {function} [args.onState] On state change callback
* @param {Stream} [args.stdin] The Readable stream to listen to
* @param {Stream} [args.stdout] The Writable stream to write readline data to
* @param {boolean} [args.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
* @returns {Promise} Promise with user input
*/
$.multiselect = args => {
Expand Down Expand Up @@ -197,6 +207,7 @@ const byTitle = (input, choices) => Promise.resolve(
* @param {function} [args.onState] On state change callback
* @param {Stream} [args.stdin] The Readable stream to listen to
* @param {Stream} [args.stdout] The Writable stream to write readline data to
* @param {boolean} [args.removeInputListenerManually] only removeInputListener() close the stdin listener. This option fixing the keypress event lost between close and reopen the input listener
* @returns {Promise} Promise with user input
*/
$.autocomplete = args => {
Expand Down
7 changes: 7 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ Almost all prompt objects have the following properties:
onState: Function
stdin: Readable
stdout: Writeable
removeInputListenerManually: boolean
}
```

Expand Down Expand Up @@ -426,6 +427,12 @@ Type: `Stream`
By default, prompts uses `process.stdin` for receiving input and `process.stdout` for writing output.
If you need to use different streams, for instance `process.stderr`, you can set these with the `stdin` and `stdout` properties.

### removeInputListenerManually

Type: `boolean`

By default, prompts closes `process.stdin` when the prompts end. In nodejs there is a [bug](https://github.com/nodejs/node/issues/38663) what this property fixing. If this property is set to `true` you need to call `removeInputListener()` manually. This means the prompts will not handle the key event when it's end. Only when it truly waiting for the keypress. *The solution*: prompts is not remove the keypress listener, and the new prompts will not lose any keystroke during the main program. **This error only occurs on Windows!**


![split](https://github.com/terkelg/prompts/raw/master/media/split.png)

Expand Down