I am designing a console app in Node.js using readline to control the cursor positions and get user input. Here is a library I have written for the same:
// ReadLine.js
const readline = require("readline");
const readSync = require("readline-sync");
const inStream = process.stdin;
const outStream = process.stdout;
class ReadLine {
constructor() {
this.io = readline.createInterface({input: inStream, output: outStream});
}
setEvent(event, callback) {
this.io.on(event, callback);
};
exit() {
this.io.close();
}
// Move Cursor to Row,Col Position
moveTo(row, col) {
readline.cursorTo(process.stdout, col, row);
// this.io.write(`\x1b[${row};${col}H`);
};
clearRow(row) {
this.moveTo(row, 0);
this.io.write(`\x1b[0K`); // Clears from the cursor to the end of the row
};
// Write text at current cursor position
write(msg) {
this.io.write(msg);
};
// Write text at specific Row,Col Position
writeAt(row, col, msg) {
this.moveTo(row, col);
this.write(msg);
};
// Ask a question, wait for an input
ask(question, callback) {
this.io.question(question, callback);
// const answer = readSync.question(question);
// callback(answer);
};
}
module.exports = {ReadLine};
And here is a sample test for this library:
// ReadCli.js
console.clear();
const ReadLine = require("./ReadLine");
const io = new ReadLine.ReadLine();
io.setEvent("close", process.exit);
const Ask = () => {
const choices = ["Print Fibonacci", "Exit"];
let row = 0;
const col = 3;
choices.forEach(choice => io.writeAt(++row, col, `${row} ${choice}`));
io.moveTo(++row, col);
io.ask("Enter Your Choice: ", answer => {
const lastChoice = choices.length;
switch(Number.parseInt(answer)) {
case lastChoice:
io.clearRow(++row);
io.writeAt(row, col, "Goodbye");
io.exit();
break;
default:
io.writeAt(++row, col, "Wrong Choice Entered");
}
Ask();
});
};
Ask();
Although the code seems straightforward, the interface printing in the console is beyond my understanding. I cannot deduce how the cursor is moving around different readline methods. Here is a summary of my problems:
- With the current code, the choices are printed again even before the user has given any input, at the current position. Although the choices are re-printed on the same line of question, the cursor is positioned just after Enter Your Choice: <cursor_position>. I have no idea what's happening here. The choices should not be re-printed, and even after re-printing the cursor is not positioned at the end of printing, but at the end of the question:
The question is positioned at the specified row, but not at the specified column. I checked this on some other tests, readline.question prints the question at the current cursor row, but it always prints from the first column, not from the current cursor column.
If I uncomment this line
this.io.write(`\x1b[${row};${col}H`);
and comment this
// readline.cursorTo(process.stdout, col, row);
I got almost the desired output:
But the problem with readline.question still persists, it's still printing on the specific row, but not from the specified column, but from the first column only.
- None of the problems occur when I use readline-sync instead of readline:
const readSync = require("readline-sync");
// Ask a question, wait for an input
ask(question, callback) {
// this.io.question(question, callback);
const answer = readSync.question(question);
callback(answer);
};
But I can't see the user input. Cursor doesn't move when user provide any input and input is not shown on screen:
So I need to understand what's happening with these two lines in the context of printing and cursor positioning
readline.cursorTo(process.stdout, col, row);
readline.question(question, callback);
Thanks and sorry for such a long question.




After some investigation into the source code this is what I have discovered:
When you do an io.write without an endline character, the content will be stored by readline as it assumes you are still editing the same line. The subsequent io.question will trigger a prompt that pushes the content to the back and renders the message at the front of the line (I have no idea why). See Readline.line
The io.question always moves the internal cursor to x=0, See kRefreshLine
I was going to suggest appending endline on all io.write but this causes the last line of the message to be cleared during rerender, you can either continue to use the terminal escape code or use io.write with endline and manually rerender the last line of message on rerender.
As for the question's column position, the only thing you can do is manually prepending the space in front of the question prompt.