on master help sign in
[nodejs prompt/confirm] support left+right arrow keys (#74)

* support left+right arrow keys * clean up a lil' * add some constants for magic escapes * fix left/right mixup

commit922f628219115fa4f210a558f2536cae49740881 authorAllAwesome497 <47748690+AllAwesome497@users.noreply.github.com> dateTue 5 Oct 2021, 19:42 (5 years ago) parente773937
4 files changed+231−17
prybar_assets/nodejs/input-sync.js+139−10 tests/nodejs/prompt_tty.exp+19−7 tests/nodejs/prompt_tty_bounds.exp+39 tests/nodejs/prompt_tty_escapes.exp+34
prybar_assets/nodejs/input-sync.js+139−10
@@ -3,6 +3,33 @@ const { isatty } = require("tty");
33const buf = Buffer.alloc(1);
44const isTTY = isatty(process.stdin.fd);
55
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+
633/**
734 * Reads a single byte from stdin to buf.
835 * @return {boolean} Whether a byte was read or not (false on EOF)
@@ -70,8 +97,38 @@ function ensureRawMode(cb) {
7097 return ret;
7198}
7299
73−const delChar = "\x7f";
74−const escapeClearLineRight = "\x1b[K";
100+/**
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+}
75132
76133/**
77134 * Checks to see if the input character is what we get in raw mode for
@@ -80,11 +137,62 @@ const escapeClearLineRight = "\x1b[K";
80137 * @param {string} char The character read.
81138 */
82139function checkForSigs(char) {
83− if (isTTY && (char === "\x03" || char === "\x04")) {
140+ if (isTTY && (char === endOfText || char === endOfTransmission)) {
84141 process.exit();
85142 }
86143}
87144
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+
88196/**
89197 * Synchronously reads from stdin until `\n` or `\r`
90198 *
@@ -94,10 +202,14 @@ function checkForSigs(char) {
94202function question(prompt) {
95203 return ensureRawMode(() => {
96204 let str = "";
205+ let index = 0;
97206
98− writeOutput(prompt);
207+ if (!isTTY) {
208+ writeOutput(prompt);
209+ }
99210
100211 for (;;) {
212+ displayPromptAndStr(prompt, str, index);
101213 const didRead = readByteSync();
102214
103215 if (!didRead) {
@@ -111,14 +223,31 @@ function question(prompt) {
111223 writeTTYOutput("\r\n");
112224
113225 return str;
114− } else if (isTTY && char === delChar) {
115− if (str.length > 0) {
116− writeOutput("\b" + escapeClearLineRight);
117− str = str.slice(0, str.length - 1);
226+ } else if (isTTY && char === "\x1b") {
227+ const ret = handleArrowKey();
228+
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);
118248 }
119249 } else {
120− writeTTYOutput(buf);
121− str += char;
250+ [str, index] = insertAt(str, char, index);
122251 }
123252 }
124253 });
tests/nodejs/prompt_tty.exp+19−7
@@ -8,16 +8,28 @@ expect -exact "\[1G\[0J--> \[5G"
88send -- "prompt('abc')\r"
99expect -exact "prompt('abc')"
1010expect -exact "abc> "
11−send -- "helo"
12−expect -exact "helo"
11+send -- "makr"
12+expect -exact "makr"
13+
14+# left arrow key
15+send -- "\x1b\[D"
16+expect -exact "makr"
17+
1318# delete
14−send -- "\x7f"
15−expect -exact "\b\[K"
16−send -- "lo"
17−expect -exact "lo"
19+send -- "\x7f"
20+expect -exact "mar"
21+
22+# right arrow key
23+send -- "\x1b\[C"
24+expect -exact "mar"
25+
26+send -- "k"
27+expect -exact "mark"
28+
1829send -- "\r"
1930expect -exact "\r\n"
20−expect -exact "'hello'"
31+
32+expect -exact "'mark'"
2133# end of transmission
2234send -- "\x04"
2335expect 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