@@ -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 });