@@ -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);
7+ * The escape (excluding \x1b) to move the cursur right one.
9+ * This is what the right arrow key translates to in raw mode.
11+const cursorRight = "[C";
13+ * The escape (excluding \x1b) to move the cursur left one.
15+ * This is what the left arrow key translates to in raw mode.
17+const cursorLeft = "[D";
20+ * The ASCII character sent when the tty is in raw mode and backspace is pressed.
25+ * The ASCII characcter sent when the tty is in raw mode and Ctrl+C is pressed.
27+const endOfText = "\x03";
29+ * The ASCII character sent when the tty is in raw mode and Ctrl+D is pressed.
31+const endOfTransmission = "\x04";
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;
73−const delChar = "\x7f";
100+/**
74−const escapeClearLineRight = "\x1b[K";
101+ * Handles ANSI escapes from stdin.
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
106+function handleArrowKey() {
107+ if (!readByteSync()) {
111+ let str = buf.toString("binary");
117+ if (!readByteSync()) {
121+ str += buf.toString("binary");
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.
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();
146+ * Writes a string at an index of another string, appending as needed.
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.
153+function insertAt(str, other, index) {
154+ return [str.slice(0, index) + other + str.slice(index), index + other.length];
158+ * The ANSI escape code used to clear the contents of the current line to the right
161+const escapeClearLineRight = "\x1b[K";
164+ * The escape used to move the cursor to a specific position in-line.
166+ * @param {number} columnNum The position (starting at 1) in the current line which the cursor should be moved to.
169+function escapeMoveCursorToColumn(columnNum) {
170+ return `\x1b[${columnNum}G`;
174+ * Sets the current line to our promt + string w/ the cursor at the right index.
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
180+function displayPromptAndStr(prompt, current, index) {
182+ // reset cursor position
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 +
192+ escapeMoveCursorToColumn(prompt.length + index + 1)
89 * Synchronously reads from stdin until `\n` or `\r`
197 * Synchronously reads from stdin until `\n` or `\r`
@@ -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 = "";
98− writeOutput(prompt);
208+ writeOutput(prompt);
100 for (;;) {
211 for (;;) {
212+ displayPromptAndStr(prompt, str, index);
101 const didRead = readByteSync();
213 const didRead = readByteSync();
103 if (!didRead) {
215 if (!didRead) {
@@ -111,14 +223,31 @@ function question(prompt) {
111 writeTTYOutput("\r\n");
223 writeTTYOutput("\r\n");
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) {
238+ // otherwise, the escape wasn't a left or right arrow key,
239+ // meaning we got an escaped version of the code.
241+ [str, index] = insertAt(str, ret, index);
243+ } else if (isTTY && char === del) {
246+ // remove the character at the old index
247+ str = str.slice(0, index) + str.slice(index + 1);
120− writeTTYOutput(buf);
250+ [str, index] = insertAt(str, char, index);