This repository has been archived by the owner on Feb 11, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 54
use readline for prompt() via magic #96
Open
wbourne0
wants to merge
2
commits into
master
Choose a base branch
from
wb-use-readline-for-sync-input
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,34 +1,19 @@ | ||
const { readSync, writeSync, openSync } = require("fs"); | ||
const { isatty } = require("tty"); | ||
const { readSync, writeSync, openSync } = require('fs'); | ||
const { isatty } = require('tty'); | ||
const { Readable } = require('stream'); | ||
const { createInterface } = require('readline'); | ||
|
||
const buf = Buffer.alloc(1); | ||
const isTTY = isatty(process.stdin.fd); | ||
|
||
/** | ||
* The escape (excluding \x1b) to move the cursur right one. | ||
* | ||
* This is what the right arrow key translates to in raw mode. | ||
*/ | ||
const cursorRight = "[C"; | ||
/** | ||
* The escape (excluding \x1b) to move the cursur left one. | ||
* | ||
* This is what the left arrow key translates to in raw mode. | ||
*/ | ||
const cursorLeft = "[D"; | ||
|
||
/** | ||
* The ASCII character sent when the tty is in raw mode and backspace is pressed. | ||
*/ | ||
const del = "\x7f"; | ||
|
||
/** | ||
* The ASCII characcter sent when the tty is in raw mode and Ctrl+C is pressed. | ||
* The ASCII character sent when the tty is in raw mode and Ctrl+C is pressed. | ||
*/ | ||
const endOfText = "\x03"; | ||
const endOfText = '\x03'; | ||
/** | ||
* The ASCII character sent when the tty is in raw mode and Ctrl+D is pressed. | ||
*/ | ||
const endOfTransmission = "\x04"; | ||
const endOfTransmission = '\x04'; | ||
|
||
/** | ||
* Reads a single byte from stdin to buf. | ||
|
@@ -47,8 +32,37 @@ function readByteSync() { | |
const stdinFd = isTTY | ||
? // We can't just use process.stdin.fd here since node has some getter shenanigans | ||
// which cause sync reads to throw | ||
openSync("/dev/tty", "r") | ||
: openSync("/dev/stdin", "r"); | ||
openSync('/dev/tty', 'r') | ||
: openSync('/dev/stdin', 'r'); | ||
|
||
class SyncReadable extends Readable { | ||
constructor(fd) { | ||
super(); | ||
|
||
this.fd = fd; | ||
} | ||
|
||
_read() {} | ||
|
||
readNext() { | ||
readByteSync(); | ||
|
||
this.push( | ||
// copy the buffer to be safe | ||
Buffer.concat([buf]) | ||
); | ||
} | ||
} | ||
|
||
const rd = new SyncReadable(stdinFd); | ||
// prime reader | ||
rd.read(); | ||
|
||
const rl = createInterface({ | ||
input: rd, | ||
output: process.stdout, | ||
terminal: isTTY, | ||
}); | ||
|
||
/** | ||
* Writes output to stdout. | ||
|
@@ -97,39 +111,6 @@ function ensureRawMode(cb) { | |
return ret; | ||
} | ||
|
||
/** | ||
* Handles ANSI escapes from stdin. | ||
* | ||
* @return {string | -1 | 1} String if the escape isn't a left or right arrow . | ||
* Otherwise, -1 on left arrow and 1 on right arrow | ||
*/ | ||
function handleArrowKey() { | ||
if (!readByteSync()) { | ||
return "^"; | ||
} | ||
|
||
let str = buf.toString("binary"); | ||
|
||
if (str !== "[") { | ||
return `^${str}`; | ||
} | ||
|
||
if (!readByteSync()) { | ||
return `^${str}`; | ||
} | ||
|
||
str += buf.toString("binary"); | ||
|
||
switch (str) { | ||
case cursorRight: | ||
return 1; | ||
case cursorLeft: | ||
return -1; | ||
default: | ||
return `^${str}`; | ||
} | ||
} | ||
|
||
/** | ||
* Checks to see if the input character is what we get in raw mode for | ||
* Ctrl+C or Ctrl+D, and if so sends the proc SIGINT> | ||
|
@@ -142,115 +123,22 @@ function checkForSigs(char) { | |
} | ||
} | ||
|
||
/** | ||
* Writes a string at an index of another string, appending as needed. | ||
* | ||
* @param {string} str The base string | ||
* @param {string} other The new string which is being written | ||
* @param {number} index The index at which the new string should start at. | ||
* @return {string} str with other written at index. | ||
*/ | ||
function insertAt(str, other, index) { | ||
return [str.slice(0, index) + other + str.slice(index), index + other.length]; | ||
} | ||
|
||
/** | ||
* The ANSI escape code used to clear the contents of the current line to the right | ||
* of the cursor. | ||
*/ | ||
const escapeClearLineRight = "\x1b[K"; | ||
|
||
/** | ||
* The escape used to move the cursor to a specific position in-line. | ||
* | ||
* @param {number} columnNum The position (starting at 1) in the current line which the cursor should be moved to. | ||
* | ||
*/ | ||
function escapeMoveCursorToColumn(columnNum) { | ||
return `\x1b[${columnNum}G`; | ||
} | ||
|
||
/** | ||
* Sets the current line to our promt + string w/ the cursor at the right index. | ||
* | ||
* @param {string} prompt The question's prompt | ||
* @param {string} current The current string (what the user has input so far) | ||
* @param {number} index The index that | ||
*/ | ||
function displayPromptAndStr(prompt, current, index) { | ||
writeTTYOutput( | ||
// reset cursor position | ||
"\r" + | ||
// clear the rest of the line | ||
// EL (Erase in Line ): in this case, as no number is speciifed, | ||
// erases everything to the right of the cursor. | ||
escapeClearLineRight + | ||
// write the prompt | ||
prompt + | ||
// write the string | ||
current + | ||
escapeMoveCursorToColumn(prompt.length + index + 1) | ||
); | ||
} | ||
|
||
/** | ||
* Synchronously reads from stdin until `\n` or `\r` | ||
* | ||
* @param {string} prompt The prompt to be displayed | ||
* @return {string} The input read (excluding newlines) | ||
*/ | ||
function question(prompt) { | ||
return ensureRawMode(() => { | ||
let str = ""; | ||
let index = 0; | ||
let result = null; | ||
|
||
if (!isTTY) { | ||
writeOutput(prompt); | ||
} | ||
|
||
for (;;) { | ||
displayPromptAndStr(prompt, str, index); | ||
const didRead = readByteSync(); | ||
|
||
if (!didRead) { | ||
return str; | ||
} | ||
|
||
const char = buf.toString("binary"); | ||
checkForSigs(char); | ||
|
||
if (char === "\n" || char === "\r") { | ||
writeTTYOutput("\r\n"); | ||
|
||
return str; | ||
} else if (isTTY && char === "\x1b") { | ||
const ret = handleArrowKey(); | ||
rl.question(prompt, (d) => { | ||
result = d; | ||
}); | ||
|
||
// if ret is a number, its the difference for the index | ||
if (typeof ret === "number") { | ||
// Only move the cursor if it will be in a valid position. | ||
const newIndex = index + ret; | ||
// the index can be equal to the strs length, if that's the case we're appending to the string. | ||
if (newIndex >= 0 && newIndex <= str.length) { | ||
index = newIndex; | ||
} | ||
while (result == null) rd.readNext(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this If it is intentional, maybe leave a comment explaining it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is intentional. It's what's called a "nullish" check. it's somewhat unstandard but handy to know.
|
||
|
||
// otherwise, the escape wasn't a left or right arrow key, | ||
// meaning we got an escaped version of the code. | ||
} else { | ||
[str, index] = insertAt(str, ret, index); | ||
} | ||
} else if (isTTY && char === del) { | ||
if (index > 0) { | ||
index--; | ||
// remove the character at the old index | ||
str = str.slice(0, index) + str.slice(index + 1); | ||
} | ||
} else { | ||
[str, index] = insertAt(str, char, index); | ||
} | ||
} | ||
}); | ||
return result; | ||
} | ||
|
||
/** | ||
|
@@ -263,22 +151,22 @@ function question(prompt) { | |
*/ | ||
function keyInYNStrict(prompt) { | ||
return ensureRawMode(() => { | ||
writeOutput(`${prompt == null ? "Are you sure?" : prompt} [y/n]: `); | ||
writeOutput(`${prompt == null ? 'Are you sure?' : prompt} [y/n]: `); | ||
|
||
for (;;) { | ||
const didRead = readByteSync(); | ||
|
||
if (!didRead) { | ||
throw new Error("Unexpected EOF / end of input. Expected y/n."); | ||
throw new Error('Unexpected EOF / end of input. Expected y/n.'); | ||
} | ||
|
||
const char = buf.toString("binary"); | ||
const char = buf.toString('binary'); | ||
checkForSigs(char); | ||
|
||
if (char.match(/[yn]/i)) { | ||
writeTTYOutput(`${char}\r\n`); | ||
|
||
return char === "y" || char === "Y"; | ||
return char === 'y' || char === 'Y'; | ||
} | ||
} | ||
}); | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yikes, this whole project has inconsistent quoting styles, so we get these painful prettier diffs. I opened #97 and #98 as one-click merge options for how to try to mitigate this if you want.