From 67cd548b79a32befc4c768feb2133cc6b8ffa6b5 Mon Sep 17 00:00:00 2001 From: Nick Heiner Date: Thu, 7 Nov 2019 16:47:16 -0800 Subject: [PATCH 01/18] Add ability to set hotkeys for multiselect --- lib/elements/multiselect.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/elements/multiselect.js b/lib/elements/multiselect.js index afa2ca23..06af6be0 100644 --- a/lib/elements/multiselect.js +++ b/lib/elements/multiselect.js @@ -30,6 +30,7 @@ class MultiselectPrompt extends Prompt { this.showMinError = false; this.maxChoices = opts.max; this.instructions = opts.instructions; + this.hotkeys = opts.hotkeys || {}; this.optionsPerPage = opts.optionsPerPage || 10; this.value = opts.choices.map((ch, idx) => { if (typeof ch === 'string') @@ -155,6 +156,9 @@ class MultiselectPrompt extends Prompt { this.handleSpaceToggle(); } else if (c === 'a') { this.toggleAll(); + } else if (Object.keys(this.hotkeys).includes(c)) { + this.hotkeys[c].handle(); + this.abort(); } else { return this.bell(); } @@ -165,10 +169,20 @@ class MultiselectPrompt extends Prompt { if (typeof this.instructions === 'string') { return this.instructions; } + + let hotkeyInstructions = Object.entries(this.hotkeys) + .reduce((instructions, [hotkeyChar, hotkeyDescriptor]) => + instructions += ` ${hotkeyChar}: ${hotkeyDescriptor.instruction}`, '') + + if (hotkeyInstructions) { + hotkeyInstructions += '\n'; + } + return '\nInstructions:\n' + ` ${figures.arrowUp}/${figures.arrowDown}: Highlight option\n` + ` ${figures.arrowLeft}/${figures.arrowRight}/[space]: Toggle selection\n` + (this.maxChoices === undefined ? ` a: Toggle all\n` : '') + + hotkeyInstructions + ` enter/return: Complete answer`; } return ''; From ff65bd4e149bd82e9202e7b3ae71130d12201039 Mon Sep 17 00:00:00 2001 From: Nick Heiner Date: Mon, 11 Nov 2019 11:24:59 -0800 Subject: [PATCH 02/18] with great effort, and deep, deep concentration, a man can achieve --- test/fixtures/multiselect.js | 23 +++++++++++++++++ test/multiselect.js | 49 ++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100755 test/fixtures/multiselect.js create mode 100644 test/multiselect.js diff --git a/test/fixtures/multiselect.js b/test/fixtures/multiselect.js new file mode 100755 index 00000000..a4fdd307 --- /dev/null +++ b/test/fixtures/multiselect.js @@ -0,0 +1,23 @@ +const prompts = require('../..'); + +prompts([ + { + type: 'multiselect', + name: 'color', + message: 'Pick colors', + choices: [ + { title: 'Red', value: '#ff0000' }, + { title: 'Green', value: '#00ff00' }, + { title: 'Blue', value: '#0000ff' } + ], + } +]).then(response => console.log(response)); + +// process.stdin.pipe(process.stdout); + +// const readline = require('readline'); + +// const rl = readline.createInterface(process.stdin); +// readline.emitKeypressEvents(process.stdin, rl); + +// process.stdin.on('keypress', (str, key) => console.log({str, key})); \ No newline at end of file diff --git a/test/multiselect.js b/test/multiselect.js new file mode 100644 index 00000000..993829c4 --- /dev/null +++ b/test/multiselect.js @@ -0,0 +1,49 @@ +const child_process = require('child_process'); +const multiselectFixture = require.resolve('./fixtures/multiselect'); +const readline = require('readline'); +const {Readable} = require('stream'); + +const test = require('tape'); + +const awaitOutput = (pipe, output) => new Promise((resolve) => { + pipe.on('data', data => { + if (data.toString().includes(output)) { + resolve(); + } + }) +}); + +class EmptyStream extends Readable { + _read() { + return ''; + } +} + +test('multiselect', t => { + const {stdout, stdin} = child_process.spawn('node', [multiselectFixture]); + stdout.pipe(process.stdout); + process.stdin.pipe(stdin); + + const readlineInstance = readline.createInterface({output: stdin, input: stdin, terminal: true }); + readlineInstance.write('a\n'); + + // awaitOutput(stdout, 'Blue').then(() => { + + + // console.log('writing'); + // // process.stdin.write('a\n'); + // // readlineInstance.write(undefined, {name: 'a', ctrl: true}); + // // readlineInstance.write('a\n'); + // readlineInstance.write(undefined, {name: 'enter'}); + // readlineInstance.write('\n'); + + // t.end(); + // }) +}) + + +// const {stdout, stdin} = child_process.spawn('node', [multiselectFixture]); +// stdout.pipe(process.stdout); + +// const readlineInstance = readline.createInterface({output: stdin, input: stdin, terminal: true }); +// readlineInstance.write('af'); \ No newline at end of file From f06026a10263419c352041a2132a60c0aa5c8122 Mon Sep 17 00:00:00 2001 From: Nick Heiner Date: Mon, 11 Nov 2019 13:16:25 -0800 Subject: [PATCH 03/18] Update travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 813ea903..e735877f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,5 +3,6 @@ script: - npm run build - npm test node_js: + - "stable" - "8" - "6" From f0d6f9baf61b71de4cbbd069404994c8576a1ab8 Mon Sep 17 00:00:00 2001 From: Nick Heiner Date: Mon, 11 Nov 2019 13:49:25 -0800 Subject: [PATCH 04/18] Making the test better --- test/fixtures/multiselect.js | 11 +---- test/multiselect.js | 84 +++++++++++++++++++++++------------- 2 files changed, 54 insertions(+), 41 deletions(-) diff --git a/test/fixtures/multiselect.js b/test/fixtures/multiselect.js index a4fdd307..fa4e4e50 100755 --- a/test/fixtures/multiselect.js +++ b/test/fixtures/multiselect.js @@ -11,13 +11,4 @@ prompts([ { title: 'Blue', value: '#0000ff' } ], } -]).then(response => console.log(response)); - -// process.stdin.pipe(process.stdout); - -// const readline = require('readline'); - -// const rl = readline.createInterface(process.stdin); -// readline.emitKeypressEvents(process.stdin, rl); - -// process.stdin.on('keypress', (str, key) => console.log({str, key})); \ No newline at end of file +]).then(response => console.error(JSON.stringify(response, null, 2))); diff --git a/test/multiselect.js b/test/multiselect.js index 993829c4..144af4e7 100644 --- a/test/multiselect.js +++ b/test/multiselect.js @@ -1,49 +1,71 @@ const child_process = require('child_process'); const multiselectFixture = require.resolve('./fixtures/multiselect'); const readline = require('readline'); -const {Readable} = require('stream'); const test = require('tape'); const awaitOutput = (pipe, output) => new Promise((resolve) => { - pipe.on('data', data => { + const handleData = data => { if (data.toString().includes(output)) { + pipe.removeListener('data', handleData); resolve(); } - }) + }; + + pipe.on('data', handleData); }); -class EmptyStream extends Readable { - _read() { - return ''; - } -} +const spawn = (inputs) => { + const childProcess = child_process.spawn('node', [multiselectFixture]); + const {stdout, stderr, stdin} = childProcess; + // stdout.pipe(process.stdout); + // process.stdin.pipe(stdin); -test('multiselect', t => { - const {stdout, stdin} = child_process.spawn('node', [multiselectFixture]); - stdout.pipe(process.stdout); - process.stdin.pipe(stdin); + childProcess.on('exit', () => { + console.log('exit'); + }) + childProcess.on('error', (err) => { + console.log('error', err); + }) const readlineInstance = readline.createInterface({output: stdin, input: stdin, terminal: true }); - readlineInstance.write('a\n'); - - // awaitOutput(stdout, 'Blue').then(() => { - - - // console.log('writing'); - // // process.stdin.write('a\n'); - // // readlineInstance.write(undefined, {name: 'a', ctrl: true}); - // // readlineInstance.write('a\n'); - // readlineInstance.write(undefined, {name: 'enter'}); - // readlineInstance.write('\n'); - - // t.end(); - // }) -}) + return new Promise((resolve, reject) => { + try { + stderr.on('data', data => { + childProcess.kill(); + readlineInstance.close(); + stdout.unpipe(process.stdout); + // I hope the JSON output always comes as a single data event. + resolve(JSON.parse(data.toString())); + }) + + awaitOutput(stdout, 'Blue').then(() => { + inputs.forEach(input => { + if (typeof input === 'string') { + readlineInstance.write(input); + } else if (typeof input === 'object') { + readlineInstance.write(undefined, input); + } else { + throw new Error( + 'Argument error: every element of spawn()s input array should be a string or an object, ' + + `but "${input}" was passed.`); + } + }) + }, reject); + } catch (e) { + reject(e); + } + }) -// const {stdout, stdin} = child_process.spawn('node', [multiselectFixture]); -// stdout.pipe(process.stdout); +} -// const readlineInstance = readline.createInterface({output: stdin, input: stdin, terminal: true }); -// readlineInstance.write('af'); \ No newline at end of file +test('multiselect', t => { + t.plan(1); + spawn(['a', {name: 'enter'}]).then(response => { + t.deepEqual(response, { + color: ['#ff0000', '#00ff00', '#0000ff'] + }, 'pressing "a" selects all options'); + t.end(); + }); +}) From 760b3083b11e417ae7cef5a12e6e7721265279b7 Mon Sep 17 00:00:00 2001 From: Nick Heiner Date: Mon, 11 Nov 2019 14:01:29 -0800 Subject: [PATCH 05/18] Adding a failing test for having a hotkey that selects multiple answers --- test/fixtures/multiselect.js | 8 +++++ test/multiselect.js | 67 +++++++++++++++++++++++------------- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/test/fixtures/multiselect.js b/test/fixtures/multiselect.js index fa4e4e50..bc200bd2 100755 --- a/test/fixtures/multiselect.js +++ b/test/fixtures/multiselect.js @@ -10,5 +10,13 @@ prompts([ { title: 'Green', value: '#00ff00' }, { title: 'Blue', value: '#0000ff' } ], + hotkeys: { + r: { + handle() { + return {answers: ['Red', 'Green']}; + }, + instruction: 'Choose Red and Green' + } + } } ]).then(response => console.error(JSON.stringify(response, null, 2))); diff --git a/test/multiselect.js b/test/multiselect.js index 144af4e7..59a9cc73 100644 --- a/test/multiselect.js +++ b/test/multiselect.js @@ -5,14 +5,16 @@ const readline = require('readline'); const test = require('tape'); const awaitOutput = (pipe, output) => new Promise((resolve) => { - const handleData = data => { - if (data.toString().includes(output)) { - pipe.removeListener('data', handleData); - resolve(); + let allDataFromPipe = ''; + const handleDataFromPipe = dataFromPipe => { + allDataFromPipe += dataFromPipe; + if (allDataFromPipe.toString().includes(output)) { + pipe.removeListener('data', handleDataFromPipe); + resolve(allDataFromPipe); } }; - pipe.on('data', handleData); + pipe.on('data', handleDataFromPipe); }); const spawn = (inputs) => { @@ -21,6 +23,8 @@ const spawn = (inputs) => { // stdout.pipe(process.stdout); // process.stdin.pipe(stdin); + // TODO: see if we can remove some of this stream closing. + childProcess.on('exit', () => { console.log('exit'); }) @@ -30,7 +34,7 @@ const spawn = (inputs) => { const readlineInstance = readline.createInterface({output: stdin, input: stdin, terminal: true }); - return new Promise((resolve, reject) => { + const stderrPromise = new Promise((resolve, reject) => { try { stderr.on('data', data => { childProcess.kill(); @@ -39,33 +43,50 @@ const spawn = (inputs) => { // I hope the JSON output always comes as a single data event. resolve(JSON.parse(data.toString())); }) - - awaitOutput(stdout, 'Blue').then(() => { - inputs.forEach(input => { - if (typeof input === 'string') { - readlineInstance.write(input); - } else if (typeof input === 'object') { - readlineInstance.write(undefined, input); - } else { - throw new Error( - 'Argument error: every element of spawn()s input array should be a string or an object, ' + - `but "${input}" was passed.`); - } - }) - }, reject); } catch (e) { reject(e); } - }) + }) + const stdoutPromise = awaitOutput(stdout, 'Blue').then(allDataFromPipe => { + inputs.forEach(input => { + if (typeof input === 'string') { + readlineInstance.write(input); + } else if (typeof input === 'object') { + readlineInstance.write(undefined, input); + } else { + throw new Error( + 'Argument error: every element of spawn()s input array should be a string or an object, ' + + `but "${input}" was passed.`); + } + }) + return allDataFromPipe; + }); + + return Promise.all([stderrPromise, stdoutPromise]).then(([childProcessResponse, childProcessStdout]) => ({ + response: childProcessResponse, + stdout: childProcessStdout + })); } -test('multiselect', t => { +test('multiselect "a"', t => { t.plan(1); - spawn(['a', {name: 'enter'}]).then(response => { + spawn(['a', {name: 'enter'}]).then(({response}) => { t.deepEqual(response, { color: ['#ff0000', '#00ff00', '#0000ff'] }, 'pressing "a" selects all options'); t.end(); }); }) + +test('multiselect hotkey that selects multiple answers', t => { + t.plan(2); + spawn(['r', {name: 'enter'}]).then(({response, stdout}) => { + t.ok(stdout.includes('r: Choose Red and Green'), `"${stdout}" includes hotkey instructions`); + + t.deepEqual(response, { + color: ['#ff0000', '#00ff00'] + }, 'pressing hotkey selects the right answers'); + t.end(); + }); +}) From 626e4c5f862dd6e7f3b6f5807947615a2fb4c9e4 Mon Sep 17 00:00:00 2001 From: Nick Heiner Date: Mon, 11 Nov 2019 14:18:04 -0800 Subject: [PATCH 06/18] Make the test pass --- lib/elements/multiselect.js | 16 +++++++++++++--- test/fixtures/multiselect.js | 2 +- test/multiselect.js | 19 +++++-------------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/lib/elements/multiselect.js b/lib/elements/multiselect.js index 06af6be0..370afb16 100644 --- a/lib/elements/multiselect.js +++ b/lib/elements/multiselect.js @@ -151,14 +151,24 @@ class MultiselectPrompt extends Prompt { this.render(); } - _(c, key) { + _(c) { if (c === ' ') { this.handleSpaceToggle(); } else if (c === 'a') { this.toggleAll(); } else if (Object.keys(this.hotkeys).includes(c)) { - this.hotkeys[c].handle(); - this.abort(); + const hotkeyResponse = this.hotkeys[c].handle() || {}; + this.value.forEach(value => { + const answerForValue = hotkeyResponse.answers[value.title]; + if (answerForValue !== undefined) { + value.selected = answerForValue; + this.render(); + } + }); + + if (hotkeyResponse.command === 'abort') { + this.abort(); + } } else { return this.bell(); } diff --git a/test/fixtures/multiselect.js b/test/fixtures/multiselect.js index bc200bd2..ae526c8e 100755 --- a/test/fixtures/multiselect.js +++ b/test/fixtures/multiselect.js @@ -13,7 +13,7 @@ prompts([ hotkeys: { r: { handle() { - return {answers: ['Red', 'Green']}; + return {answers: {Red: true, Green: true}}; }, instruction: 'Choose Red and Green' } diff --git a/test/multiselect.js b/test/multiselect.js index 59a9cc73..b21eb038 100644 --- a/test/multiselect.js +++ b/test/multiselect.js @@ -6,15 +6,12 @@ const test = require('tape'); const awaitOutput = (pipe, output) => new Promise((resolve) => { let allDataFromPipe = ''; - const handleDataFromPipe = dataFromPipe => { + pipe.on('data', dataFromPipe => { allDataFromPipe += dataFromPipe; if (allDataFromPipe.toString().includes(output)) { - pipe.removeListener('data', handleDataFromPipe); resolve(allDataFromPipe); } - }; - - pipe.on('data', handleDataFromPipe); + }); }); const spawn = (inputs) => { @@ -23,14 +20,9 @@ const spawn = (inputs) => { // stdout.pipe(process.stdout); // process.stdin.pipe(stdin); - // TODO: see if we can remove some of this stream closing. - - childProcess.on('exit', () => { - console.log('exit'); - }) childProcess.on('error', (err) => { - console.log('error', err); - }) + console.log('Spawn error:', err); + }); const readlineInstance = readline.createInterface({output: stdin, input: stdin, terminal: true }); @@ -38,7 +30,6 @@ const spawn = (inputs) => { try { stderr.on('data', data => { childProcess.kill(); - readlineInstance.close(); stdout.unpipe(process.stdout); // I hope the JSON output always comes as a single data event. resolve(JSON.parse(data.toString())); @@ -82,7 +73,7 @@ test('multiselect "a"', t => { test('multiselect hotkey that selects multiple answers', t => { t.plan(2); spawn(['r', {name: 'enter'}]).then(({response, stdout}) => { - t.ok(stdout.includes('r: Choose Red and Green'), `"${stdout}" includes hotkey instructions`); + t.ok(stdout.includes('r: Choose Red and Green'), `Stdout includes hotkey instructions`); t.deepEqual(response, { color: ['#ff0000', '#00ff00'] From 58d570f395ccd67e5cde3384403a95d573ccb70e Mon Sep 17 00:00:00 2001 From: Nick Heiner Date: Mon, 11 Nov 2019 14:23:42 -0800 Subject: [PATCH 07/18] Adding ability to abort --- lib/elements/multiselect.js | 22 ++++++++++------------ test/fixtures/multiselect.js | 6 ++++++ test/multiselect.js | 12 ++++++++++++ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/lib/elements/multiselect.js b/lib/elements/multiselect.js index 370afb16..9b5b0c7e 100644 --- a/lib/elements/multiselect.js +++ b/lib/elements/multiselect.js @@ -158,13 +158,15 @@ class MultiselectPrompt extends Prompt { this.toggleAll(); } else if (Object.keys(this.hotkeys).includes(c)) { const hotkeyResponse = this.hotkeys[c].handle() || {}; - this.value.forEach(value => { - const answerForValue = hotkeyResponse.answers[value.title]; - if (answerForValue !== undefined) { - value.selected = answerForValue; - this.render(); - } - }); + if (hotkeyResponse.answers) { + this.value.forEach(value => { + const answerForValue = hotkeyResponse.answers[value.title]; + if (answerForValue !== undefined) { + value.selected = answerForValue; + this.render(); + } + }); + } if (hotkeyResponse.command === 'abort') { this.abort(); @@ -182,11 +184,7 @@ class MultiselectPrompt extends Prompt { let hotkeyInstructions = Object.entries(this.hotkeys) .reduce((instructions, [hotkeyChar, hotkeyDescriptor]) => - instructions += ` ${hotkeyChar}: ${hotkeyDescriptor.instruction}`, '') - - if (hotkeyInstructions) { - hotkeyInstructions += '\n'; - } + instructions += ` ${hotkeyChar}: ${hotkeyDescriptor.instruction}\n`, '') return '\nInstructions:\n' + ` ${figures.arrowUp}/${figures.arrowDown}: Highlight option\n` diff --git a/test/fixtures/multiselect.js b/test/fixtures/multiselect.js index ae526c8e..6283ef25 100755 --- a/test/fixtures/multiselect.js +++ b/test/fixtures/multiselect.js @@ -16,6 +16,12 @@ prompts([ return {answers: {Red: true, Green: true}}; }, instruction: 'Choose Red and Green' + }, + d: { + handle() { + return {command: 'abort'}; + }, + instruction: 'Abort' } } } diff --git a/test/multiselect.js b/test/multiselect.js index b21eb038..cc5bbef1 100644 --- a/test/multiselect.js +++ b/test/multiselect.js @@ -4,6 +4,8 @@ const readline = require('readline'); const test = require('tape'); +// Running these tests will make your terminal cursor disappear. Sorry. Run `reset` to fix it. + const awaitOutput = (pipe, output) => new Promise((resolve) => { let allDataFromPipe = ''; pipe.on('data', dataFromPipe => { @@ -81,3 +83,13 @@ test('multiselect hotkey that selects multiple answers', t => { t.end(); }); }) + +test('multiselect hotkey that aborts', t => { + t.plan(2); + spawn(['d']).then(({response, stdout}) => { + t.ok(stdout.includes('d: Abort'), `Stdout includes hotkey instructions`); + + t.deepEqual(response, {}, 'pressing hotkey aborts the process'); + t.end(); + }); +}) From d08aff44f40d9c32c3f270c0f7377145db076df6 Mon Sep 17 00:00:00 2001 From: Nick Heiner Date: Mon, 11 Nov 2019 14:28:25 -0800 Subject: [PATCH 08/18] Add a test for having a hotkey submit an answer --- lib/elements/multiselect.js | 5 +++-- test/fixtures/multiselect.js | 8 +++++++- test/multiselect.js | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/elements/multiselect.js b/lib/elements/multiselect.js index 9b5b0c7e..e9fbeeb1 100644 --- a/lib/elements/multiselect.js +++ b/lib/elements/multiselect.js @@ -168,9 +168,10 @@ class MultiselectPrompt extends Prompt { }); } - if (hotkeyResponse.command === 'abort') { - this.abort(); + if (['abort', 'submit'].includes(hotkeyResponse.command)) { + this[hotkeyResponse.command](); } + } else { return this.bell(); } diff --git a/test/fixtures/multiselect.js b/test/fixtures/multiselect.js index 6283ef25..f28fd387 100755 --- a/test/fixtures/multiselect.js +++ b/test/fixtures/multiselect.js @@ -21,7 +21,13 @@ prompts([ handle() { return {command: 'abort'}; }, - instruction: 'Abort' + instruction: 'Abort' + }, + e: { + handle() { + return {answers: {Green: true, Blue: true}, command: 'submit'} + }, + instruction: 'Select Green and Blue, and move on the to the next question' } } } diff --git a/test/multiselect.js b/test/multiselect.js index cc5bbef1..cabfbcda 100644 --- a/test/multiselect.js +++ b/test/multiselect.js @@ -93,3 +93,18 @@ test('multiselect hotkey that aborts', t => { t.end(); }); }) + +test('multiselect hotkey that chooses answers and submits', t => { + t.plan(2); + spawn(['e']).then(({response, stdout}) => { + t.ok( + stdout.includes('e: Select Green and Blue, and move on the to the next question'), + `Stdout includes hotkey instructions` + ); + + t.deepEqual(response, { + color: ['#00ff00', '#0000ff'] + }, 'pressing hotkey chooses answers and submits'); + t.end(); + }); +}) From 59f9c2765586313cc8c5823b4ac3bf30468e6d9a Mon Sep 17 00:00:00 2001 From: Nick Heiner Date: Mon, 11 Nov 2019 14:34:12 -0800 Subject: [PATCH 09/18] Add a test for deselecting an answer --- test/fixtures/multiselect.js | 6 ++++++ test/multiselect.js | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/test/fixtures/multiselect.js b/test/fixtures/multiselect.js index f28fd387..dfdd5d95 100755 --- a/test/fixtures/multiselect.js +++ b/test/fixtures/multiselect.js @@ -28,6 +28,12 @@ prompts([ return {answers: {Green: true, Blue: true}, command: 'submit'} }, instruction: 'Select Green and Blue, and move on the to the next question' + }, + f: { + handle() { + return {answers: {Red: false, Blue: true}, command: 'submit'} + }, + instruction: 'Pay respect. Also, enable Blue and disable Red.' } } } diff --git a/test/multiselect.js b/test/multiselect.js index cabfbcda..c584dffc 100644 --- a/test/multiselect.js +++ b/test/multiselect.js @@ -108,3 +108,18 @@ test('multiselect hotkey that chooses answers and submits', t => { t.end(); }); }) + +test('multiselect hotkey that chooses answers and submits', t => { + t.plan(2); + spawn(['a', 'f']).then(({response, stdout}) => { + t.ok( + stdout.includes('f: Pay respect. Also, enable Blue and disable Red.'), + `Stdout includes hotkey instructions` + ); + + t.deepEqual(response, { + color: ['#00ff00', '#0000ff'] + }, 'pressing hotkey chooses answers and submits'); + t.end(); + }); +}) From aaaa5db71e55d192900f10843ceae6fac2734cea Mon Sep 17 00:00:00 2001 From: Nick Heiner Date: Mon, 11 Nov 2019 14:36:30 -0800 Subject: [PATCH 10/18] README cleanup --- readme.md | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/readme.md b/readme.md index 2f6c5b6a..bebfa0dc 100755 --- a/readme.md +++ b/readme.md @@ -1,5 +1,5 @@

- Prompts + Prompts

❯ Prompts

@@ -36,7 +36,7 @@ * **Unified**: consistent experience across all [prompts](#-types). -![split](https://github.com/terkelg/prompts/raw/master/media/split.png) +![split](./media/split.png) ## ❯ Install @@ -47,11 +47,11 @@ $ npm install --save prompts > This package supports Node 6 and above -![split](https://github.com/terkelg/prompts/raw/master/media/split.png) +![split](./media/split.png) ## ❯ Usage -example prompt +example prompt ```js const prompts = require('prompts'); @@ -71,7 +71,7 @@ const prompts = require('prompts'); > See [`example.js`](https://github.com/terkelg/prompts/blob/master/example.js) for more options. -![split](https://github.com/terkelg/prompts/raw/master/media/split.png) +![split](./media/split.png) ## ❯ Examples @@ -155,7 +155,7 @@ const questions = [ ``` -![split](https://github.com/terkelg/prompts/raw/master/media/split.png) +![split](./media/split.png) ## ❯ API @@ -299,7 +299,7 @@ prompts.inject([ '@terkelg', ['#ff0000', '#0000ff'] ]); })(); ``` -![split](https://github.com/terkelg/prompts/raw/master/media/split.png) +![split](./media/split.png) ## ❯ Prompt Objects @@ -418,7 +418,7 @@ The function signature is `(state)` where `state` is an object with a snapshot o The state object has two properties `value` and `aborted`. E.g `{ value: 'This is ', aborted: false }` -![split](https://github.com/terkelg/prompts/raw/master/media/split.png) +![split](./media/split.png) ## ❯ Types @@ -444,7 +444,7 @@ The state object has two properties `value` and `aborted`. E.g `{ value: 'This i Hit tab to autocomplete to `initial` value when provided. #### Example -text prompt +text prompt ```js { @@ -475,7 +475,7 @@ Hit tab to autocomplete to `initial` value when provided. This prompt is a similar to a prompt of type `'text'` with `style` set to `'password'`. #### Example -password prompt +password prompt ```js { @@ -506,7 +506,7 @@ This prompt is working like `sudo` where the input is invisible. This prompt is a similar to a prompt of type `'text'` with style set to `'invisible'`. #### Example -invisible prompt +invisible prompt ```js { @@ -536,7 +536,7 @@ This prompt is a similar to a prompt of type `'text'` with style set to `'invisi You can type in numbers and use up/down to increase/decrease the value. Only numbers are allowed as input. Hit tab to autocomplete to `initial` value when provided. #### Example -number prompt +number prompt ```js { @@ -576,7 +576,7 @@ You can type in numbers and use up/down to increase/decrea Hit y or n to confirm/reject. #### Example -confirm prompt +confirm prompt ```js { @@ -617,7 +617,7 @@ string separated by `separator`. } ``` -list prompt +list prompt | Param | Type | Description | @@ -639,7 +639,7 @@ string separated by `separator`. Use tab or arrow keys/tab/space to switch between options. #### Example -toggle prompt +toggle prompt ```js { @@ -673,7 +673,7 @@ Use tab or arrow keys/tab/space to switch betwe Use up/down to navigate. Use tab to cycle the list. #### Example -select prompt +select prompt ```js { @@ -714,7 +714,7 @@ Use space to toggle select/unselect and up/down By default this prompt returns an `array` containing the **values** of the selected items - not their display title. #### Example -multiselect prompt +multiselect prompt ```js { @@ -763,7 +763,7 @@ The default suggests function is sorting based on the `title` property of the ch You can overwrite how choices are being filtered by passing your own suggest function. #### Example -auto complete prompt +auto complete prompt ```js { @@ -810,7 +810,7 @@ const suggestByTitle = (input, choices) => Use left/right/tab to navigate. Use up/down to change date. #### Example -date prompt +date prompt ```js { @@ -835,7 +835,7 @@ Use left/right/tab to navigate. Use up**Formatting**: See full list of formatting options in the [wiki](https://github.com/terkelg/prompts/wiki/Date-Time-Formatting) -![split](https://github.com/terkelg/prompts/raw/master/media/split.png) +![split](./media/split.png) **↑ back to:** [Prompt types](#-types) From 94bcfc1e894f98b3df9e5351d0bc507841f370f8 Mon Sep 17 00:00:00 2001 From: Nick Heiner Date: Mon, 11 Nov 2019 14:44:00 -0800 Subject: [PATCH 11/18] README --- readme.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/readme.md b/readme.md index bebfa0dc..d782f33f 100755 --- a/readme.md +++ b/readme.md @@ -727,6 +727,17 @@ By default this prompt returns an `array` containing the **values** of the selec { title: 'Blue', value: '#0000ff', selected: true } ], max: 2, + hotkeys: { + d: { + handle() { + return { + answer: {Red: true, Green: false}, + command: 'submit' + } + }, + instruction: 'Enable red, disable green, and submit the answer.' + } + } hint: '- Space to select. Return to submit' } ``` @@ -745,10 +756,33 @@ By default this prompt returns an `array` containing the **values** of the selec | warn | `string` | Message to display when selecting a disabled option | | 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` | +| hotkeys | `{[hotkeyCharacter]: hotkeyDescriptor}` | | This is one of the few prompts that don't take a initial value. If you want to predefine selected values, give the choice object an `selected` property of `true`. +The type of `hotkeyDescriptor` is: + +```ts +{ + handle: () => { + // Pass "submit" to have this question be submitted when the user presses the hotkey. + // Pass "abort" to have the question be aborted (as if the user had hit control+c). + command?: 'submit' | 'abort', + + answers: { + // The key in this object is the `title` field in the `choices` array passed above. + // The value is whether this choice will be selected after the hotkey is pressed. + // Pass true to select the choice. False deselects the choice. Undefined leaves the choice as-is. + [answerTitle]: boolean + } + }, + instruction: string +} +``` + +See the [multiselect test fixture](./test/fixtures/multiselect.js) for more examples of using hotkeys. + **↑ back to:** [Prompt types](#-types) *** From 63561e0862eadea7b817ed91e82c29717a655287 Mon Sep 17 00:00:00 2001 From: Nick Heiner Date: Mon, 11 Nov 2019 14:45:49 -0800 Subject: [PATCH 12/18] update README --- readme.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index d782f33f..f9db2e85 100755 --- a/readme.md +++ b/readme.md @@ -756,7 +756,7 @@ By default this prompt returns an `array` containing the **values** of the selec | warn | `string` | Message to display when selecting a disabled option | | 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` | -| hotkeys | `{[hotkeyCharacter]: hotkeyDescriptor}` | | +| hotkeys | `{[hotkeyCharacter]: hotkeyDescriptor}` | Define hotkeys that can select multiple answers at once. | This is one of the few prompts that don't take a initial value. If you want to predefine selected values, give the choice object an `selected` property of `true`. @@ -765,7 +765,9 @@ The type of `hotkeyDescriptor` is: ```ts { + // You can do whatever you want in this function (e.g. trigger a side effect). You can also customize the choices object. handle: () => { + // Pass "submit" to have this question be submitted when the user presses the hotkey. // Pass "abort" to have the question be aborted (as if the user had hit control+c). command?: 'submit' | 'abort', From 3725cab989d3eff8dfd927eb29a9b4574daf4e24 Mon Sep 17 00:00:00 2001 From: Nick Heiner Date: Mon, 11 Nov 2019 14:50:36 -0800 Subject: [PATCH 13/18] Clean up tests --- test/multiselect.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/multiselect.js b/test/multiselect.js index c584dffc..9d6a61f3 100644 --- a/test/multiselect.js +++ b/test/multiselect.js @@ -19,8 +19,13 @@ const awaitOutput = (pipe, output) => new Promise((resolve) => { const spawn = (inputs) => { const childProcess = child_process.spawn('node', [multiselectFixture]); const {stdout, stderr, stdin} = childProcess; - // stdout.pipe(process.stdout); - // process.stdin.pipe(stdin); + + // Pass this env var to get some visibility into what the subproc is doing, and also kinda break the tests. + // But maybe it'll help you. :) + if (process.env.DEBUG) { + stdout.pipe(process.stdout); + process.stdin.pipe(stdin); + } childProcess.on('error', (err) => { console.log('Spawn error:', err); From 2cc9ee5e3092fc0d6be624bb2bd66b281b27cd80 Mon Sep 17 00:00:00 2001 From: Nick Heiner Date: Mon, 11 Nov 2019 14:53:54 -0800 Subject: [PATCH 14/18] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ca0c1f2b..10226e34 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prompts", - "version": "2.2.1", + "version": "2.3.0", "description": "Lightweight, beautiful and user-friendly prompts", "license": "MIT", "repository": "terkelg/prompts", From 126c66fdf4cf50e1cfc9c4277400eec818a43e8d Mon Sep 17 00:00:00 2001 From: Nick Heiner Date: Wed, 13 Nov 2019 09:49:09 -0800 Subject: [PATCH 15/18] Embrace async/await --- .travis.yml | 1 - package.json | 2 +- test/multiselect.js | 93 ++++++++++++++++++++++----------------------- 3 files changed, 46 insertions(+), 50 deletions(-) diff --git a/.travis.yml b/.travis.yml index e735877f..dd6f39d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,4 +5,3 @@ script: node_js: - "stable" - "8" - - "6" diff --git a/package.json b/package.json index 10226e34..f1e24cb8 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,6 @@ "tape": "^4.11.0" }, "engines": { - "node": ">= 6" + "node": ">= 7" } } diff --git a/test/multiselect.js b/test/multiselect.js index 9d6a61f3..a1fe3d59 100644 --- a/test/multiselect.js +++ b/test/multiselect.js @@ -16,7 +16,7 @@ const awaitOutput = (pipe, output) => new Promise((resolve) => { }); }); -const spawn = (inputs) => { +const spawn = async (inputs) => { const childProcess = child_process.spawn('node', [multiselectFixture]); const {stdout, stderr, stdin} = childProcess; @@ -61,70 +61,67 @@ const spawn = (inputs) => { return allDataFromPipe; }); - return Promise.all([stderrPromise, stdoutPromise]).then(([childProcessResponse, childProcessStdout]) => ({ + + const [childProcessResponse, childProcessStdout] = await Promise.all([stderrPromise, stdoutPromise]); + return { response: childProcessResponse, stdout: childProcessStdout - })); + }; } -test('multiselect "a"', t => { +test('multiselect "a"', async t => { t.plan(1); - spawn(['a', {name: 'enter'}]).then(({response}) => { - t.deepEqual(response, { - color: ['#ff0000', '#00ff00', '#0000ff'] - }, 'pressing "a" selects all options'); - t.end(); - }); + const {response} = await spawn(['a', {name: 'enter'}]) + t.deepEqual(response, { + color: ['#ff0000', '#00ff00', '#0000ff'] + }, 'pressing "a" selects all options'); + t.end(); }) -test('multiselect hotkey that selects multiple answers', t => { +test('multiselect hotkey that selects multiple answers', async t => { t.plan(2); - spawn(['r', {name: 'enter'}]).then(({response, stdout}) => { - t.ok(stdout.includes('r: Choose Red and Green'), `Stdout includes hotkey instructions`); + const {response, stdout} = await spawn(['r', {name: 'enter'}]) + t.ok(stdout.includes('r: Choose Red and Green'), `Stdout includes hotkey instructions`); - t.deepEqual(response, { - color: ['#ff0000', '#00ff00'] - }, 'pressing hotkey selects the right answers'); - t.end(); - }); + t.deepEqual(response, { + color: ['#ff0000', '#00ff00'] + }, 'pressing hotkey selects the right answers'); + t.end(); }) -test('multiselect hotkey that aborts', t => { +test('multiselect hotkey that aborts', async t => { t.plan(2); - spawn(['d']).then(({response, stdout}) => { - t.ok(stdout.includes('d: Abort'), `Stdout includes hotkey instructions`); + const {response, stdout} = await spawn(['d']) + t.ok(stdout.includes('d: Abort'), `Stdout includes hotkey instructions`); - t.deepEqual(response, {}, 'pressing hotkey aborts the process'); - t.end(); - }); + t.deepEqual(response, {}, 'pressing hotkey aborts the process'); + t.end(); }) -test('multiselect hotkey that chooses answers and submits', t => { +test('multiselect hotkey that chooses answers and submits', async t => { t.plan(2); - spawn(['e']).then(({response, stdout}) => { - t.ok( - stdout.includes('e: Select Green and Blue, and move on the to the next question'), - `Stdout includes hotkey instructions` - ); - - t.deepEqual(response, { - color: ['#00ff00', '#0000ff'] - }, 'pressing hotkey chooses answers and submits'); - t.end(); - }); + const {response, stdout} = await spawn(['e']) + t.ok( + stdout.includes('e: Select Green and Blue, and move on the to the next question'), + `Stdout includes hotkey instructions` + ); + + t.deepEqual(response, { + color: ['#00ff00', '#0000ff'] + }, 'pressing hotkey chooses answers and submits'); + t.end(); }) -test('multiselect hotkey that chooses answers and submits', t => { +test('multiselect hotkey that chooses answers and submits', async t => { t.plan(2); - spawn(['a', 'f']).then(({response, stdout}) => { - t.ok( - stdout.includes('f: Pay respect. Also, enable Blue and disable Red.'), - `Stdout includes hotkey instructions` - ); - - t.deepEqual(response, { - color: ['#00ff00', '#0000ff'] - }, 'pressing hotkey chooses answers and submits'); - t.end(); - }); + const {response, stdout} = await spawn(['a', 'f']) + t.ok( + stdout.includes('f: Pay respect. Also, enable Blue and disable Red.'), + `Stdout includes hotkey instructions` + ); + + t.deepEqual(response, { + color: ['#00ff00', '#0000ff'] + }, 'pressing hotkey chooses answers and submits'); + t.end(); }) From a831315527b26e8981eb8abfc0284536ac5d1702 Mon Sep 17 00:00:00 2001 From: Nick Heiner Date: Sat, 16 Nov 2019 16:34:45 -0800 Subject: [PATCH 16/18] Revert unwanted changes --- .travis.yml | 1 + package.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index dd6f39d6..e735877f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,3 +5,4 @@ script: node_js: - "stable" - "8" + - "6" diff --git a/package.json b/package.json index f1e24cb8..ca0c1f2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prompts", - "version": "2.3.0", + "version": "2.2.1", "description": "Lightweight, beautiful and user-friendly prompts", "license": "MIT", "repository": "terkelg/prompts", @@ -48,6 +48,6 @@ "tape": "^4.11.0" }, "engines": { - "node": ">= 7" + "node": ">= 6" } } From 27ed003020e9e13c35d5348ef1e6d8cbc4b9bcc1 Mon Sep 17 00:00:00 2001 From: Nick Heiner Date: Sat, 16 Nov 2019 16:36:48 -0800 Subject: [PATCH 17/18] Revert "Embrace async/await" This reverts commit 126c66fdf4cf50e1cfc9c4277400eec818a43e8d. --- test/multiselect.js | 93 +++++++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/test/multiselect.js b/test/multiselect.js index a1fe3d59..9d6a61f3 100644 --- a/test/multiselect.js +++ b/test/multiselect.js @@ -16,7 +16,7 @@ const awaitOutput = (pipe, output) => new Promise((resolve) => { }); }); -const spawn = async (inputs) => { +const spawn = (inputs) => { const childProcess = child_process.spawn('node', [multiselectFixture]); const {stdout, stderr, stdin} = childProcess; @@ -61,67 +61,70 @@ const spawn = async (inputs) => { return allDataFromPipe; }); - - const [childProcessResponse, childProcessStdout] = await Promise.all([stderrPromise, stdoutPromise]); - return { + return Promise.all([stderrPromise, stdoutPromise]).then(([childProcessResponse, childProcessStdout]) => ({ response: childProcessResponse, stdout: childProcessStdout - }; + })); } -test('multiselect "a"', async t => { +test('multiselect "a"', t => { t.plan(1); - const {response} = await spawn(['a', {name: 'enter'}]) - t.deepEqual(response, { - color: ['#ff0000', '#00ff00', '#0000ff'] - }, 'pressing "a" selects all options'); - t.end(); + spawn(['a', {name: 'enter'}]).then(({response}) => { + t.deepEqual(response, { + color: ['#ff0000', '#00ff00', '#0000ff'] + }, 'pressing "a" selects all options'); + t.end(); + }); }) -test('multiselect hotkey that selects multiple answers', async t => { +test('multiselect hotkey that selects multiple answers', t => { t.plan(2); - const {response, stdout} = await spawn(['r', {name: 'enter'}]) - t.ok(stdout.includes('r: Choose Red and Green'), `Stdout includes hotkey instructions`); + spawn(['r', {name: 'enter'}]).then(({response, stdout}) => { + t.ok(stdout.includes('r: Choose Red and Green'), `Stdout includes hotkey instructions`); - t.deepEqual(response, { - color: ['#ff0000', '#00ff00'] - }, 'pressing hotkey selects the right answers'); - t.end(); + t.deepEqual(response, { + color: ['#ff0000', '#00ff00'] + }, 'pressing hotkey selects the right answers'); + t.end(); + }); }) -test('multiselect hotkey that aborts', async t => { +test('multiselect hotkey that aborts', t => { t.plan(2); - const {response, stdout} = await spawn(['d']) - t.ok(stdout.includes('d: Abort'), `Stdout includes hotkey instructions`); + spawn(['d']).then(({response, stdout}) => { + t.ok(stdout.includes('d: Abort'), `Stdout includes hotkey instructions`); - t.deepEqual(response, {}, 'pressing hotkey aborts the process'); - t.end(); + t.deepEqual(response, {}, 'pressing hotkey aborts the process'); + t.end(); + }); }) -test('multiselect hotkey that chooses answers and submits', async t => { +test('multiselect hotkey that chooses answers and submits', t => { t.plan(2); - const {response, stdout} = await spawn(['e']) - t.ok( - stdout.includes('e: Select Green and Blue, and move on the to the next question'), - `Stdout includes hotkey instructions` - ); - - t.deepEqual(response, { - color: ['#00ff00', '#0000ff'] - }, 'pressing hotkey chooses answers and submits'); - t.end(); + spawn(['e']).then(({response, stdout}) => { + t.ok( + stdout.includes('e: Select Green and Blue, and move on the to the next question'), + `Stdout includes hotkey instructions` + ); + + t.deepEqual(response, { + color: ['#00ff00', '#0000ff'] + }, 'pressing hotkey chooses answers and submits'); + t.end(); + }); }) -test('multiselect hotkey that chooses answers and submits', async t => { +test('multiselect hotkey that chooses answers and submits', t => { t.plan(2); - const {response, stdout} = await spawn(['a', 'f']) - t.ok( - stdout.includes('f: Pay respect. Also, enable Blue and disable Red.'), - `Stdout includes hotkey instructions` - ); - - t.deepEqual(response, { - color: ['#00ff00', '#0000ff'] - }, 'pressing hotkey chooses answers and submits'); - t.end(); + spawn(['a', 'f']).then(({response, stdout}) => { + t.ok( + stdout.includes('f: Pay respect. Also, enable Blue and disable Red.'), + `Stdout includes hotkey instructions` + ); + + t.deepEqual(response, { + color: ['#00ff00', '#0000ff'] + }, 'pressing hotkey chooses answers and submits'); + t.end(); + }); }) From eb38dc6b930d9980946630a3f484a08daa3c55cd Mon Sep 17 00:00:00 2001 From: Nick Heiner Date: Sat, 16 Nov 2019 16:41:34 -0800 Subject: [PATCH 18/18] Support node 6 --- lib/elements/multiselect.js | 3 ++- package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/elements/multiselect.js b/lib/elements/multiselect.js index e9fbeeb1..1f87082d 100644 --- a/lib/elements/multiselect.js +++ b/lib/elements/multiselect.js @@ -183,7 +183,8 @@ class MultiselectPrompt extends Prompt { return this.instructions; } - let hotkeyInstructions = Object.entries(this.hotkeys) + let hotkeyInstructions = Object.keys(this.hotkeys) + .map(key => [key, this.hotkeys[key]]) .reduce((instructions, [hotkeyChar, hotkeyDescriptor]) => instructions += ` ${hotkeyChar}: ${hotkeyDescriptor.instruction}\n`, '') diff --git a/package.json b/package.json index ca0c1f2b..c0c67c01 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "start": "node lib/index.js", "build": "babel lib -d dist", "prepublishOnly": "npm run build", - "test": "tape test/*.js | tap-spec" + "test": "npm run build && tape test/*.js | tap-spec" }, "keywords": [ "ui",