on master help sign in
[nodejs prompt/confirm] support left+right arrow keys (#74)
922f628AllAwesome497
prybar_assets/nodejs/input-sync.js+139−10
@@ -3,6 +3,33 @@ const { isatty } = require("tty");
3const buf = Buffer.alloc(1);
3const buf = Buffer.alloc(1);
4const isTTY = isatty(process.stdin.fd);
4const isTTY = isatty(process.stdin.fd);
5
5
6+/**
7+ * The escape (excluding \x1b) to move the cursur right one.
8+ *
9+ * This is what the right arrow key translates to in raw mode.
10+ */
11+const cursorRight = "[C";
12+/**
13+ * The escape (excluding \x1b) to move the cursur left one.
14+ *
15+ * This is what the left arrow key translates to in raw mode.
16+ */
17+const cursorLeft = "[D";
18+
19+/**
20+ * The ASCII character sent when the tty is in raw mode and backspace is pressed.
21+ */
22+const del = "\x7f";
23+
24+/**
25+ * The ASCII characcter sent when the tty is in raw mode and Ctrl+C is pressed.
26+ */
27+const endOfText = "\x03";
28+/**
29+ * The ASCII character sent when the tty is in raw mode and Ctrl+D is pressed.
30+ */
31+const endOfTransmission = "\x04";
32+
6/**
33/**
7 * Reads a single byte from stdin to buf.
34 * Reads a single byte from stdin to buf.
8 * @return {boolean} Whether a byte was read or not (false on EOF)
35 * @return {boolean} Whether a byte was read or not (false on EOF)
@@ -70,8 +97,38 @@ function ensureRawMode(cb) {
70 return ret;
97 return ret;
71}
98}
72
99
73−const delChar = "\x7f";
100+/**
74−const escapeClearLineRight = "\x1b[K";
101+ * Handles ANSI escapes from stdin.
102+ *
103+ * @return {string | -1 | 1} String if the escape isn't a left or right arrow .
104+ * Otherwise, -1 on left arrow and 1 on right arrow
105+ */
106+function handleArrowKey() {
107+ if (!readByteSync()) {
108+ return "^";
109+ }
110+
111+ let str = buf.toString("binary");
112+
113+ if (str !== "[") {
114+ return `^${str}`;
115+ }
116+
117+ if (!readByteSync()) {
118+ return `^${str}`;
119+ }
120+
121+ str += buf.toString("binary");
122+
123+ switch (str) {
124+ case cursorRight:
125+ return 1;
126+ case cursorLeft:
127+ return -1;
128+ default:
129+ return `^${str}`;
130+ }
131+}
75
132
76/**
133/**
77 * Checks to see if the input character is what we get in raw mode for
134 * Checks to see if the input character is what we get in raw mode for
@@ -80,11 +137,62 @@ const escapeClearLineRight = "\x1b[K";
80 * @param {string} char The character read.
137 * @param {string} char The character read.
81 */
138 */
82function checkForSigs(char) {
139function checkForSigs(char) {
83− if (isTTY && (char === "\x03" || char === "\x04")) {
140+ if (isTTY && (char === endOfText || char === endOfTransmission)) {
84 process.exit();
141 process.exit();
85 }
142 }
86}
143}
87
144
145+/**
146+ * Writes a string at an index of another string, appending as needed.
147+ *
148+ * @param {string} str The base string
149+ * @param {string} other The new string which is being written
150+ * @param {number} index The index at which the new string should start at.
151+ * @return {string} str with other written at index.
152+ */
153+function insertAt(str, other, index) {
154+ return [str.slice(0, index) + other + str.slice(index), index + other.length];
155+}
156+
157+/**
158+ * The ANSI escape code used to clear the contents of the current line to the right
159+ * of the cursor.
160+ */
161+const escapeClearLineRight = "\x1b[K";
162+
163+/**
164+ * The escape used to move the cursor to a specific position in-line.
165+ *
166+ * @param {number} columnNum The position (starting at 1) in the current line which the cursor should be moved to.
167+ *
168+ */
169+function escapeMoveCursorToColumn(columnNum) {
170+ return `\x1b[${columnNum}G`;
171+}
172+
173+/**
174+ * Sets the current line to our promt + string w/ the cursor at the right index.
175+ *
176+ * @param {string} prompt The question's prompt
177+ * @param {string} current The current string (what the user has input so far)
178+ * @param {number} index The index that
179+ */
180+function displayPromptAndStr(prompt, current, index) {
181+ writeTTYOutput(
182+ // reset cursor position
183+ "\r" +
184+ // clear the rest of the line
185+ // EL (Erase in Line ): in this case, as no number is speciifed,
186+ // erases everything to the right of the cursor.
187+ escapeClearLineRight +
188+ // write the prompt
189+ prompt +
190+ // write the string
191+ current +
192+ escapeMoveCursorToColumn(prompt.length + index + 1)
193+ );
194+}
195+
88/**
196/**
89 * Synchronously reads from stdin until `\n` or `\r`
197 * Synchronously reads from stdin until `\n` or `\r`
90 *
198 *
@@ -94,10 +202,14 @@ function checkForSigs(char) {
94function question(prompt) {
202function question(prompt) {
95 return ensureRawMode(() => {
203 return ensureRawMode(() => {
96 let str = "";
204 let str = "";
205+ let index = 0;
97
206
207+ if (!isTTY) {
98− writeOutput(prompt);
208+ writeOutput(prompt);
209+ }
99
210
100 for (;;) {
211 for (;;) {
212+ displayPromptAndStr(prompt, str, index);
101 const didRead = readByteSync();
213 const didRead = readByteSync();
102
214
103 if (!didRead) {
215 if (!didRead) {
@@ -111,14 +223,31 @@ function question(prompt) {
111 writeTTYOutput("\r\n");
223 writeTTYOutput("\r\n");
112
224
113 return str;
225 return str;
114− } else if (isTTY && char === delChar) {
226+ } else if (isTTY && char === "\x1b") {
115− if (str.length > 0) {
227+ const ret = handleArrowKey();
116− writeOutput("\b" + escapeClearLineRight);
228+
117− str = str.slice(0, str.length - 1);
229+ // if ret is a number, its the difference for the index
230+ if (typeof ret === "number") {
231+ // Only move the cursor if it will be in a valid position.
232+ const newIndex = index + ret;
233+ // the index can be equal to the strs length, if that's the case we're appending to the string.
234+ if (newIndex >= 0 && newIndex <= str.length) {
235+ index = newIndex;
236+ }
237+
238+ // otherwise, the escape wasn't a left or right arrow key,
239+ // meaning we got an escaped version of the code.
240+ } else {
241+ [str, index] = insertAt(str, ret, index);
242+ }
243+ } else if (isTTY && char === del) {
244+ if (index > 0) {
245+ index--;
246+ // remove the character at the old index
247+ str = str.slice(0, index) + str.slice(index + 1);
118 }
248 }
119 } else {
249 } else {
120− writeTTYOutput(buf);
250+ [str, index] = insertAt(str, char, index);
121− str += char;
122 }
251 }
123 }
252 }
124 });
253 });
tests/nodejs/prompt_tty.exp+19−7
@@ -8,16 +8,28 @@ expect -exact "\[1G\[0J--> \[5G"
8send -- "prompt('abc')\r"
8send -- "prompt('abc')\r"
9expect -exact "prompt('abc')"
9expect -exact "prompt('abc')"
10expect -exact "abc> "
10expect -exact "abc> "
11−send -- "helo"
11+send -- "makr"
12−expect -exact "helo"
12+expect -exact "makr"
13+
14+# left arrow key
15+send -- "\x1b\[D"
16+expect -exact "makr"
17+
13# delete
18# delete
14−send -- "\x7f"
19+send -- "\x7f"
15−expect -exact "\b\[K"
20+expect -exact "mar"
21+
22+# right arrow key
23+send -- "\x1b\[C"
24+expect -exact "mar"
25+
16−send -- "lo"
26+send -- "k"
17−expect -exact "lo"
27+expect -exact "mark"
28+
18send -- "\r"
29send -- "\r"
19expect -exact "\r\n"
30expect -exact "\r\n"
31+
20−expect -exact "'hello'"
32+expect -exact "'mark'"
21# end of transmission
33# end of transmission
22send -- "\x04"
34send -- "\x04"
23expect eof
35expect eof
tests/nodejs/prompt_tty_bounds.expnew file+39
@@ -0,0 +1,39 @@
1+#!/usr/bin/env -S expect -f
2+
3+set timeout -1
4+spawn ./prybar-nodejs -i -q
5+match_max 100000
6+expect -exact "\[1G\[0J--> \[5G"
7+
8+send -- "prompt('abc')\r"
9+expect -exact "prompt('abc')"
10+expect -exact "abc> "
11+
12+send -- "d"
13+expect -exact "abc> d"
14+
15+# right arrow key (index 1/1)
16+send -- "\x1b\[C"
17+
18+send -- "e"
19+expect -exact "abc> de"
20+
21+# left arrow key (index 2/3)
22+send -- "\x1b\[D"
23+
24+# delete / backspace (index 1/2)
25+send -- "\x7f"
26+expect -exact "abc> e"
27+
28+# delete / backspace (index: 0/1)
29+send -- "\x7f"
30+expect -exact "abc> e"
31+
32+# left arrow key (index 0/1)
33+send -- "\x1b\[D"
34+send -- "d"
35+expect -exact "abc> de"
36+
37+# end of transmission
38+send -- "\x04"
39+expect eof
tests/nodejs/prompt_tty_escapes.expnew file+34
@@ -0,0 +1,34 @@
1+#!/usr/bin/env -S expect -f
2+
3+set timeout -1
4+spawn ./prybar-nodejs -i -q
5+match_max 100000
6+expect -exact "\[1G\[0J--> \[5G"
7+
8+send -- "prompt('abc')\r"
9+expect -exact "prompt('abc')"
10+expect -exact "abc> "
11+send -- "ab"
12+
13+# left arrow key (index 2/2)
14+send -- "\x1b\[D"
15+expect -exact "abc> ab"
16+
17+# up arrow key (should be escaped) (index 1/2)
18+send -- "\x1b\[A"
19+expect -exact "abc> a^\[Ab"
20+
21+# right arrow key + a few deletes to clear the prompt (index 6/6)
22+send -- "\x1b\[C\x7f\x7f\x7f\x7f\x7f\x7f"
23+
24+# random escape that doesn't exist (should be escaped)
25+send -- "\x1bfake escape"
26+expect -exact "abc> ^fake escape"
27+
28+send -- "\r"
29+expect -exact "\r\n"
30+expect -exact "'^fake escape'"
31+
32+# end of transmission
33+send -- "\x04"
34+expect eof