nodejs readline's cursor behavior and position - node.js

I have the following code
const readline = require('readline');
const {stdin, stderr} = process;
const rl = readline.createInterface({
output: stderr,
input: stdin
})
console.log(rl.getCursorPos());
let i = 5;
while (i > 0) {
console.log('fill up with logs');
i--
}
console.log(rl.getCursorPos())
and its output
{ cols: 2, rows: 0 }
fill up with logs
fill up with logs
fill up with logs
fill up with logs
{ cols: 2, rows: 0 }
as from above the last 'getCursorPos()' should return a new position since the stderr output has been filled up with logs, instead, it returns its default value, did I misunderstand the way it works?
or is there anyway I can get cursor position and save it multiple times using readline? I've looked through using asni escape, but it seems like it can only store one cursor position at a time.

The position returned by readline is relative to the current cursor location, not absolute on the screen.
I was able to create a promise to get this information using the answer below containing the escape sequence for requesting terminal cursor position:
How can I get position of cursor in terminal?
const getCursorPos = () => new Promise((resolve) => {
const termcodes = { cursorGetPosition: '\u001b[6n' };
process.stdin.setEncoding('utf8');
process.stdin.setRawMode(true);
const readfx = function () {
const buf = process.stdin.read();
const str = JSON.stringify(buf); // "\u001b[9;1R"
const regex = /\[(.*)/g;
const xy = regex.exec(str)[0].replace(/\[|R"/g, '').split(';');
const pos = { rows: xy[0], cols: xy[1] };
process.stdin.setRawMode(false);
resolve(pos);
}
process.stdin.once('readable', readfx);
process.stdout.write(termcodes.cursorGetPosition);
})
const AppMain = async function () {
process.stdout.write('HELLO');
const pos = await getCursorPos();
process.stdout.write("\n");
console.log({ pos });
}
AppMain();

Related

Unable to trigger if block in wordle clone

I followed PedroTech's guide for a Wordle Clone to practice React, which I really enjoy. I've mostly had no issues and have made some additions myself but am having a hell of a time getting my gameOver condition to trigger on a win (guessing the correct word). It triggers correctly on a loss (6 guesses without the right word). The issue would seem at first blush to be a casing mismatch between the correct word from the word bank and the current word being concatted in the for loop, but upon logging each I am getting identical strings. I have tried everything in my toolbelt at this point and am at a loss.
Here is where the correct word and states are set. The correct word is set from a lowercase bank using use effect and is logged upon comp mount. I know it is correct because I am getting my letter colors to fill correctly upon guess. I will post an image to give a better idea of what I'm looking at.
`function App() {
const [board, setBoard] = useState(emptyBoard);
const [wordSet, setWordSet] = useState(new Set());
const [currentGuess, setCurrentGuess] = useState({ attempt: 0, letterPosition: 0 });
const [gamesWon, setGamesWon] = useState(0);
const [soupInfo, setSoupInfo] = useState([]);
const [soupPic, setSoupPic] = useState(null);
const [userPersist, setUserPersist] = useState(false);
const [validWord, setValidWord] = useState('');
const [disabledLetters, setDisabledLetters] = useState([]);
const [gameOver, setGameOver] = useState({ gameOver: false, guessedWord: false })
const [correctWord, setCorrectWord] = useState('');
const [playerPosition, setPlayerPosition] = useState(1);
// let [currentWord, setCurrentWord] = useState('');
let [soupIndex, setSoupIndex] = useState([]);
useEffect(() => {
genWordSet().then((words) => {
setWordSet(words.wordSet);
setCorrectWord(words.todaysWord);
});
}, []);`
Here is an image of the correct word being recognized letter by letter:
soup-seeker
Here is my onEnter() function that recognizes keydown input of a 5-letter word. This is where the gameOver state should change upon correctWord and currentWord being matched:
const onEnter = () => {
if (currentGuess.letterPosition !== 5) return;
let currentWord = '';
for (let i = 0; i < 5; i++) {
// setCurrentWord(board[currentGuess.attempt][i].toString())
currentWord += board[currentGuess.attempt][i].toLowerCase();
}
console.log(currentWord);
console.log(correctWord);
console.log(typeof currentWord);
console.log(typeof correctWord);
// + `\r`
//not sure why this \r is appearing in the word set but can just concatenate
if (wordSet.has(currentWord.toLowerCase() + `\r`)) {
setCurrentGuess({ attempt: currentGuess.attempt + 1, letterPosition: 0 });
console.log(currentWord)
console.log(correctWord);
} else {
setValidWord('Not null');
} if (currentWord === correctWord) {
setGameOver({ gameOver: true, guessedWord: true });
return;
};
if (currentGuess.attempt === 5) {
setGameOver({ gameOver: true, guessedWord: false })
return;
};
//increase the array index w attempt; reset position in array to start for next guess
};
I have a tried lower casing the current word in the if condition but it doesn't seem to do anything, and they are both registering as a lowercase strings anyways.
Could anyone help me out? I'm pretty new to this so my guess is I'm missing something really obvious. Usually I try to solve things myself but this one has me really stumped.
Thank you.

How do you get the current terminal cursor position?

I can determine the terminal window size with process.stdout.columns, process.stdout.rows, and can recalculate my positioning with the resize event.
I am struggling to find out how to get my current cursor position for doing something like updating a specific location on my terminal, then returning to the previous location.
Does Node offer something like process.stdeout.x, process.stdeout.y to tell me where I currently am?
I realise I there are some Linux specific work arounds, but is there something offered by Node that allows for this functionality in a cross platform way?
The position returned by readline is relative to the current cursor location, not absolute on the screen.
I was able to create a promise to get this information using the answer below containing the escape sequence for requesting terminal cursor position:
How can I get position of cursor in terminal?
const getCursorPos = () => new Promise((resolve) => {
const termcodes = { cursorGetPosition: '\u001b[6n' };
process.stdin.setEncoding('utf8');
process.stdin.setRawMode(true);
const readfx = function () {
const buf = process.stdin.read();
const str = JSON.stringify(buf); // "\u001b[9;1R"
const regex = /\[(.*)/g;
const xy = regex.exec(str)[0].replace(/\[|R"/g, '').split(';');
const pos = { rows: xy[0], cols: xy[1] };
process.stdin.setRawMode(false);
resolve(pos);
}
process.stdin.once('readable', readfx);
process.stdout.write(termcodes.cursorGetPosition);
})
const AppMain = async function () {
process.stdout.write('HELLO');
const pos = await getCursorPos();
process.stdout.write("\n");
console.log({ pos });
}
AppMain();

Node.js: readline inside replace()

I have a string with the alphabet and certain letters are in parenthesis. It means these letters should be uppercase. But the user has to be asked first if he wants it.
The problem is, I have to call readline inside a loop and this doesn't work.
const readline = require('readline');
const text = 'a(b)cdefg(h)ijklmnopqrst(u)vwxyz';
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
text.replace(/\((\w)\)/g, function (match, p1) {
let answer;
rl.question(`Do you want to change ${p1}? [yes/no] `, input => {
answer = input;
});
return answer === "yes" ? p1.toUpperCase() : p1;
});
console.log("Result:", text);
It just prints out Do you want to change b? [yes/no] Result: a(b)cdefg(h)ijklmnopqrst(u)vwxyz.
How can this be solved?
As Nick has explained in the comments the main issue of your code it that it does not handle the async readline operation correctly - here's how I would do this:
use a promisifed version of readline to prevent nested callbacks
use a while loop to step through all regex matches and await the user input in order to convert the chars to upper case or not
Something like (still needs error handling, but should give you an idea):
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
function question(query) {
return new Promise(resolve => {
readline.question(query, resolve);
})
}
const specialCharsRegex = /\((\w)\)/g;
const text = 'a(b)cdefg(h)ijklmnopqrst(u)vwxyz';
(async () => {
let result = text;
let currMatch;
while ((currMatch = specialCharsRegex.exec(text)) !== null) {
const convertInput = await question(`Do you want to change ${currMatch[1]}? [yes/no] `);
result = result.replace(currMatch[0], v => {
return convertInput === "yes" ? currMatch[1].toUpperCase() : currMatch[1];
});
}
console.log("Result: ", result);
readline.close();
})();
The main issue with your code is that you're trying to do something asynchronous in a callback that is expected to be synchronous - that is, you're trying to read user input in the .replace() callback, but reading user input doesn't happen immediately, so the callback to .question() occurs sometime after your question has been asked and your replace callback has terminated/finished. Making rl.question() synchronous would fix this (Node.js synchronous prompt) and would allow you to prompt against the exact words you're replacing.
If you want to keep things asynchronous, you could first match the letters you want to replace using .matchAll(), that way you'll have the text/groups, and the indexes of the characters you want to change. Then, you can use a loop to iterate the matches, and prompt the user whether they want to change the group. For each group, you can update the text accordingly by updating the text at the specified index. You could use replace, but if your text has duplicates eg: "a(b)c(b)e", and you said "no" for the first (b) but yes for the second (b), then you'll either need to replace both occurrences, or the first match. By using the index, you can specify exactly which group to change:
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin, //or fileStream
output: process.stdout
});
const question = q => new Promise(res => rl.question(q, res));
let text = 'a(b)cdefg(h)ijklmnopqrst(u)vwxyz';
(async () => {
const matches = text.matchAll(/\((\w)\)/g);
for(const {0: match, 1: p1, index} of matches) {
const ans = await question(`Do you want to change ${p1}? [yes/no] `);
if(ans === "yes")
text = text.slice(0, index) + match.toUpperCase() + text.slice(index + match.length);
}
console.log(text);
})();

How remove lines afterwards from a specific line location in node.js in text file

I want to remove all the lines afterwards line no 7 How to do this using node.js and fs
const fs = require('fs');
Text file the (.txt)
Time Schedule for
Remind before
Channel to remind
Last edit
Last update
Reminders
1) fr 4:9 7:0 abcd 123
2) mo 5:0 7:0 123 ajk
You can use a Transform stream, to stop reading the file after the desired line count is reached, and write only the transformed content, which is the content until the nth line.
const fs = require('fs');
const { Transform } = require('stream');
const clipLines = maxLines => {
// Get new line character code [10, 13]
const NEWLINES = ['\n', '\r\n'].map(item => item.charCodeAt(0));
let sourceStream;
let linesRemaining = maxLines;
const transform = new Transform({
transform(chunk, encoding, callback) {
const chunks = [];
// Loop through the buffer
for(const char of chunk) {
chunks.push(char); // Move to the end if you don't want the ending newline
if(NEWLINES.includes(char) && --linesRemaining == 0) {
sourceStream.close(); // Close stream, we don't want to process any more data.
break;
}
}
callback(null, Buffer.from(chunks));
}
});
transform.on('pipe', source => {
sourceStream = source
if(maxLines <= 0)
source.close();
});
return transform;
}
fs.createReadStream('test.txt')
.pipe(clipLines(7))
.pipe(fs.createWriteStream('out.txt'))
Using streams, allow you to handle big files, without reaching memory limit.

Set rate using RxJS5

I have this code which just reads in data from a .csv file and converts it to json and logs the data:
const fs = require('fs');
const path = require('path');
const sd = path.resolve(__dirname + '/fixtures/SampleData.csv');
const strm = fs.createReadStream(sd).setEncoding('utf8');
const Rx = require('rxjs/Rx');
const csv2json = require('csv2json');
const dest = strm
.pipe(csv2json({
separator: ','
}));
dest.on('error', function(e){
console.error(e.stack || e);
})
const obs = Rx.Observable.fromEvent(dest, 'data')
.flatMap(d => Rx.Observable.timer(100).mapTo(d))
obs.subscribe(v => {
console.log(String(v));
})
What the code is doing is logging all the data after a 100 ms delay. I actually want to delay on each line of data and log each line after a small delay.
The above code doesn't achieve that - what is the best way to control the rate at which the data is logged?
Hypothesis: All the lines of data come in approximately at the same time, so all are delayed 100 ms, so they end up getting printed at pretty much the same time. I need to only start delaying the next line after the previous as been logged.
the following code seems to do the same thing as using the timer above:
const obs = Rx.Observable.fromEvent(dest, 'data')
.delay(100)
Hypothesis: All the lines of data come in approximately at the same
time, so all are delayed 100 ms, so they end up getting printed at
pretty much the same time. I need to only start delaying the next line
after the previous as been logged.
Your hypothesis is correct
Solution
Swap out the .flatMap() in your original solution with .concatMap()
Rx.Observable.from([1,2,3,4])
.mergeMap(i => Rx.Observable.timer(500).mapTo(i))
.subscribe(val => console.log('mergeMap value: ' + val));
Rx.Observable.from([1,2,3,4])
.concatMap(i => Rx.Observable.timer(500).mapTo(i))
.subscribe(val => console.log('concatMap value: ' + val));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.0.3/Rx.js"></script>
This will ensure that every emission completes before the next emission is subscribed to and starts delaying its value.
I couldn't find the functionality I needed in the RxJS library (although it might be there, I just couldn't find it, let me know if there is a better, more idiomatic, way).
So I wrote this, which seems to do the job:
const fs = require('fs');
const path = require('path');
const sd = path.resolve(__dirname + '/fixtures/SampleData.csv');
const strm = fs.createReadStream(sd).setEncoding('utf8');
const Rx = require('rxjs/Rx');
const csv2json = require('csv2json');
const p = Rx.Observable.prototype;
p.eachWait = function(timeout){
const source = this;
const values = [];
let flipped = true;
const onNext = function (sub){
flipped = false;
setTimeout(() => {
var c = values.pop();
if(c) sub.next(c);
if(values.length > 0){
onNext(sub);
}
else{
flipped = true;
}
}, timeout);
}
return Rx.Observable.create(sub => {
return source.subscribe(
function next(v){
values.unshift(v);
if(flipped){
onNext(sub);
}
},
sub.error.bind(sub),
sub.complete.bind(sub)
);
});
}
const dest = strm
.pipe(csv2json({
separator: ','
}));
dest.on('error', function(e){
console.error(e.stack || e);
});
const obs = Rx.Observable.fromEvent(dest, 'data')
.eachWait(1000)
obs.subscribe(v => {
console.log(String(v));
});
I assume this is as about as performant as you can make it - only one timer should be running at any given moment.

Resources