Skip to content

Commit 8762a8d

Browse files
authored
v0.1.7
* use getter/setters for value * kinda working default * fix typo and add option for stdin and stdout * end of day folks * working defaults. Fix scales * I think it's working * default values in text prompts * semi working defaults for numbers * clean up * tab completion * fixed custom render for number prompt * Support floats * Add hint opt to select * clean up * done * better ux and support for initial values * Use getVal instead * better safe than sorry * update example * remove null in case value is falsy * Update documenation * more documentation * reset to initial if possible * code review fixes * a few rendering fixes and tab functionality * clean test code * fix default value in confirm * better logic
1 parent bb0ab79 commit 8762a8d

File tree

10 files changed

+235
-125
lines changed

10 files changed

+235
-125
lines changed

example.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const { prompt } = require('./lib');
88
type: 'text',
99
name: 'twitter',
1010
message: `What's your twitter handle?`,
11+
initial: 'terkelg',
1112
format: v => `@${v}`
1213
},
1314
{
@@ -66,6 +67,7 @@ const { prompt } = require('./lib');
6667
type: 'autocomplete',
6768
name: 'value',
6869
message: 'Pick your favorite actor',
70+
initial: 1,
6971
choices: [
7072
{ title: 'Cage' },
7173
{ title: 'Clooney', value: 'silver-fox' },

lib/elements/autocomplete.js

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,33 @@ const Prompt = require('./prompt');
66
const { cursor } = require('sisteransi');
77

88
// Get value, with fallback to title
9-
const getVal = (arr, i) => arr[i].value || arr[i].title || arr[i];
9+
const getVal = (arr, i) => arr[i] && (arr[i].value || arr[i].title || arr[i]);
1010

1111
/**
12-
* A command line prompt with autocompletion
12+
* TextPrompt Base Element
13+
* @param {Object} opts Options
14+
* @param {String} opts.message Message
15+
* @param {Array} opts.choices Array of auto-complete choices objects
16+
* @param {Function} [opts.suggest] Filter function. Defaults to sort by title
17+
* @param {Number} [opts.limit=10] Max number of results to show
18+
* @param {Number} [opts.cursor=0] Cursor start position
19+
* @param {String} [opts.style='default'] Render style
20+
* @param {String} [opts.fallback] Fallback message - initial to default value
21+
* @param {String} [opts.initial] Index of the default value
1322
*/
1423
class AutocompletePrompt extends Prompt {
15-
constructor({ message, suggest, choices, cursor = 0, limit = 10, style = 'default' }) {
16-
super();
17-
this.msg = message;
18-
this.suggest = suggest;
19-
this.choices = choices;
24+
constructor(opts={}) {
25+
super(opts);
26+
this.msg = opts.message;
27+
this.suggest = opts.suggest;
28+
this.choices = opts.choices;
29+
this.initial = opts.initial;
30+
this.cursor = opts.initial || opts.cursor || 0;
31+
this.fallback = opts.fallback || opts.initial !== void 0 ? `› ${getVal(this.choices, this.initial)}` : `› no matches found`;
2032
this.suggestions = [];
2133
this.input = '';
22-
this.limit = limit;
23-
this.cursor = cursor;
24-
this.transform = util.style.render(style);
34+
this.limit = opts.limit || 10;
35+
this.transform = util.style.render(opts.style);
2536
this.render = this.render.bind(this);
2637
this.complete = this.complete.bind(this);
2738
this.clear = util.clear('');
@@ -32,7 +43,7 @@ class AutocompletePrompt extends Prompt {
3243
moveCursor(i) {
3344
this.cursor = i;
3445
if (this.suggestions.length > 0) this.value = getVal(this.suggestions, i);
35-
else this.value = null;
46+
else this.value = this.initial !== void 0 ? getVal(this.choices, this.initial) : null;
3647
this.fire();
3748
}
3849

@@ -54,7 +65,7 @@ class AutocompletePrompt extends Prompt {
5465
reset() {
5566
this.input = '';
5667
this.complete(() => {
57-
this.moveCursor(0);
68+
this.moveCursor(this.initial !== void 0 ? this.initial : 0);
5869
this.render();
5970
});
6071
this.render();
@@ -130,10 +141,12 @@ class AutocompletePrompt extends Prompt {
130141
].join(' ');
131142

132143
if (!this.done) {
133-
for (let i = 0; i < this.suggestions.length; i++) {
134-
const s = this.suggestions[i];
135-
prompt += '\n' + (i === this.cursor ? color.cyan(s.title) : s.title);
136-
}
144+
let suggestions = this.suggestions.map((item, i) =>
145+
`\n${i === this.cursor ? color.cyan(item.title) : item.title}`);
146+
147+
prompt += suggestions.length ?
148+
suggestions.reduce((acc, line) => acc+line, '') :
149+
`\n${color.gray(this.fallback)}`;
137150
}
138151

139152
this.out.write(this.clear + prompt);

lib/elements/confirm.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ const { style, clear } = require('../util');
44
const { erase, cursor } = require('sisteransi');
55

66
/**
7-
* A CLI classic confirm prompt
7+
* ConfirmPrompt Base Element
8+
* @param {Object} opts Options
9+
* @param {String} opts.message Message
10+
* @param {Boolean} [opts.initial] Default value (true/false)
811
*/
912
class ConfirmPrompt extends Prompt {
10-
constructor({ message, initial = false }) {
11-
super();
12-
this.msg = message;
13-
this.value = initial;
14-
this.initialValue = initial;
13+
constructor(opts={}) {
14+
super(opts);
15+
this.msg = opts.message;
16+
this.value = opts.initial;
17+
this.initialValue = !!opts.initial;
1518
this.render(true);
1619
}
1720

@@ -30,6 +33,7 @@ class ConfirmPrompt extends Prompt {
3033
}
3134

3235
submit() {
36+
this.value = this.value || false;
3337
this.done = true;
3438
this.aborted = false;
3539
this.fire();

lib/elements/multiselect.js

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,22 @@ const { cursor } = require('sisteransi');
66
const { clear, figures, style } = require('../util');
77

88
/**
9-
* A prompt to select zero or more items.
9+
* MultiselectPrompt Base Element
10+
* @param {Object} opts Options
11+
* @param {String} opts.message Message
12+
* @param {Array} opts.choices Array of choice objects
13+
* @param {String} [opts.hint] Hint to display
14+
* @param {Number} [opts.cursor=0] Cursor start position
15+
* @param {Number} [opts.max] Max choices
1016
*/
1117
class MultiselectPrompt extends Prompt {
12-
constructor({ message, choices, max, hint, cursor = 0 }) {
13-
super();
14-
this.msg = message;
15-
this.hint = hint;
16-
this.cursor = cursor;
17-
this.hint = hint || '- Space to select. Return to submit';
18-
this.maxChoices = max;
19-
this.value = choices.map(v => {
20-
return Object.assign({ title: v.value, selected: false }, v);
21-
});
18+
constructor(opts={}) {
19+
super(opts);
20+
this.msg = opts.message;
21+
this.cursor = opts.cursor || 0;
22+
this.hint = opts.hint || '- Space to select. Return to submit';
23+
this.maxChoices = opts.max;
24+
this.value = opts.choices.map(v => Object.assign({ title: v.value, selected: false }, v));
2225
this.clear = clear('');
2326
this.render(true);
2427
}

lib/elements/number.js

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,78 @@
11
const color = require('clorox');
22
const Prompt = require('./prompt');
33
const { cursor, erase } = require('sisteransi');
4-
const { style: stl, clear } = require('../util');
4+
const { style, clear } = require('../util');
55

66
const isNumber = /[0-9]/;
7+
const isValidChar = /\.|-/;
78
const isDef = any => any !== undefined;
8-
const isNull = any => any === null;
9+
const round = (number, precision) => {
10+
let factor = Math.pow(10, precision);
11+
return Math.round(number * factor) / factor;
12+
}
913

14+
/**
15+
* NumberPrompt Base Element
16+
* @param {Object} opts Options
17+
* @param {String} opts.message Message
18+
* @param {String} [opts.style='default'] Render style
19+
* @param {Number} [opts.initial] Default value
20+
* @param {Number} [opts.max=+Infinity] Max value
21+
* @param {Number} [opts.min=-Infinity] Min value
22+
* @param {Boolean} [opts.float=false] Parse input as floats
23+
* @param {Number} [opts.round=2] Round floats to x decimals
24+
* @param {Number} [opts.increment=1] Number to increment by when using arrow-keys
25+
*/
1026
class NumberPrompt extends Prompt {
11-
constructor({ message, initial = '', min, max, style = 'default' }) {
12-
super();
13-
14-
this.msg = message;
15-
this.transform = stl.render(style);
16-
17-
this.min = isDef(min) ? min : -Infinity;
18-
this.max = isDef(max) ? max : Infinity;
19-
this.value = initial;
20-
27+
constructor(opts={}) {
28+
super(opts);
29+
this.transform = style.render(opts.style);
30+
this.msg = opts.message;
31+
this.initial = isDef(opts.initial) ? opts.initial : '';
32+
this.float = !!opts.float;
33+
this.round = opts.round || 2;
34+
this.inc = opts.increment || 1;
35+
this.min = isDef(opts.min) ? opts.min : -Infinity;
36+
this.max = isDef(opts.max) ? opts.max : Infinity;
37+
this.value = ''
2138
this.typed = '';
2239
this.lastHit = 0;
40+
this.render();
41+
}
2342

24-
this.initialValue = this.value;
43+
set value(v) {
44+
if (!v && v !== 0) {
45+
this.placeholder = true;
46+
this.rendered = color.gray(this.transform.render(`${this.initial}`));
47+
} else {
48+
this.placeholder = false;
49+
this.rendered = this.transform.render(`${round(v, this.round)}`);
50+
}
51+
this._value = round(v, this.round);
52+
this.fire();
53+
}
2554

26-
this.render();
55+
get value() {
56+
return this._value;
57+
}
58+
59+
parse(x) {
60+
return this.float ? parseFloat(x) : parseInt(x);
61+
}
62+
63+
valid(c) {
64+
return c === '-' || c === '.' && this.float || isNumber.test(c)
2765
}
2866

2967
reset() {
3068
this.typed = '';
31-
this.value = this.initialValue;
69+
this.value = '';
3270
this.fire();
3371
this.render();
3472
}
3573

3674
abort() {
75+
this.value = this.value || this.initial;
3776
this.done = this.aborted = true;
3877
this.fire();
3978
this.render();
@@ -42,6 +81,7 @@ class NumberPrompt extends Prompt {
4281
}
4382

4483
submit() {
84+
this.value = this.value || this.initial;
4585
this.done = true;
4686
this.aborted = false;
4787
this.fire();
@@ -53,54 +93,60 @@ class NumberPrompt extends Prompt {
5393
up() {
5494
this.typed = '';
5595
if (this.value >= this.max) return this.bell();
56-
this.value++;
96+
this.value += this.inc;
5797
this.fire();
5898
this.render();
5999
}
60100

61101
down() {
62102
this.typed = '';
63103
if (this.value <= this.min) return this.bell();
64-
this.value--;
104+
this.value -= this.inc;
65105
this.fire();
66106
this.render();
67107
}
68108

69109
delete() {
70110
let val = this.value.toString();
71111
if (val.length === 0) return this.bell();
72-
this.value = parseInt((val = val.slice(0, -1))) || '';
112+
this.value = this.parse((val = val.slice(0, -1))) || '';
113+
this.fire();
114+
this.render();
115+
}
116+
117+
next() {
118+
this.value = this.initial;
73119
this.fire();
74120
this.render();
75121
}
76122

77123
_(c, key) {
78-
if (!isNumber.test(c)) return this.bell();
124+
if (!this.valid(c)) return this.bell();
79125

80126
const now = Date.now();
81127
if (now - this.lastHit > 1000) this.typed = ''; // 1s elapsed
82128
this.typed += c;
83129
this.lastHit = now;
84130

85-
this.value = Math.min(parseInt(this.typed), this.max);
131+
if (c === '.') return this.fire();
132+
133+
this.value = Math.min(this.parse(this.typed), this.max);
86134
if (this.value > this.max) this.value = this.max;
87135
if (this.value < this.min) this.value = this.min;
88136
this.fire();
89137
this.render();
90138
}
91139

92140
render() {
93-
let value = this.transform.render(this.value !== null ? this.value : '');
94-
if (!this.done) value = color.cyan.underline(value);
95-
141+
let underline = !this.done || (!this.done && !this.placeholder);
96142
this.out.write(
97143
erase.line +
98144
cursor.to(0) +
99145
[
100-
stl.symbol(this.done, this.aborted),
146+
style.symbol(this.done, this.aborted),
101147
color.bold(this.msg),
102-
stl.delimiter(this.done),
103-
value
148+
style.delimiter(this.done),
149+
underline ? color.cyan.underline(this.rendered) : this.rendered
104150
].join(' ')
105151
);
106152
}

lib/elements/prompt.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,16 @@ const { beep, cursor } = require('sisteransi');
99
* Base prompt skeleton
1010
*/
1111
class Prompt extends EventEmitter {
12-
constructor() {
12+
constructor(opts={}) {
1313
super();
1414

15-
this.in = process.stdin;
16-
this.out = process.stdout;
15+
this.in = opts.in || process.stdin;
16+
this.out = opts.out || process.stdout;
1717

1818
const rl = readline.createInterface(this.in);
19-
readline.emitKeypressEvents(this.in, this.rl);
19+
readline.emitKeypressEvents(this.in, rl);
2020

21-
if (this.in.isTTY) {
22-
this.in.setRawMode(true);
23-
}
21+
if (this.in.isTTY) this.in.setRawMode(true);
2422

2523
const keypress = (str, key) => {
2624
let a = action(key);

lib/elements/select.js

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@ const { style, clear } = require('../util');
66
const { erase, cursor } = require('sisteransi');
77

88
/**
9-
* A prompt to an item from a list.
9+
* SelectPrompt Base Element
10+
* @param {Object} opts Options
11+
* @param {String} opts.message Message
12+
* @param {Array} opts.choices Array of choice objects
13+
* @param {String} [opts.hint] Hint to display
14+
* @param {Number} [opts.initial] Index of default value
1015
*/
1116
class SelectPrompt extends Prompt {
12-
constructor({ message, choices = [], cursor = 0, initial = 0 }) {
13-
super();
14-
15-
this.msg = message;
16-
this.cursor = initial || cursor;
17-
18-
this.values = choices;
19-
this.value = choices[initial].value;
20-
17+
constructor(opts={}) {
18+
super(opts);
19+
this.msg = opts.message;
20+
this.hint = opts.hint || '- Use arrow-keys. Return to submit.';
21+
this.cursor = opts.initial || 0;
22+
this.values = opts.choices || [];
23+
this.value = opts.choices[this.cursor].value;
2124
this.clear = clear('');
2225
this.render(true);
2326
}
@@ -92,7 +95,7 @@ class SelectPrompt extends Prompt {
9295
style.symbol(this.done, this.aborted),
9396
color.bold(this.msg),
9497
style.delimiter(false),
95-
this.done ? this.values[this.cursor].title : ''
98+
this.done ? this.values[this.cursor].title : color.gray(this.hint)
9699
].join(' ')
97100
);
98101

0 commit comments

Comments
 (0)