How do you get the current terminal cursor position? - node.js

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();

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.

nodejs readline's cursor behavior and position

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();

Dynamic Slash Command Options List via Database Query?

Background:
I am building a discord bot that operates as a Dungeons & Dragons DM of sorts. We want to store game data in a database and during the execution of certain commands, query data from said database for use in the game.
All of the connections between our Discord server, our VPS, and the VPS' backend are functional and we are now implementing slash commands since traditional ! commands are being removed from support in April.
We are running into problems making the slash commands though. We want to set them up to be as efficient as possible which means no hard-coded choices for options. We want to build those choice lists via data from the database.
The problem we are running into is that we can't figure out the proper way to implement the fetch to the database within the SlashCommandBuilder.
Here is what we currently have:
const {SlashCommandBuilder} = require('#discordjs/builders');
const fetch = require('node-fetch');
const {REST} = require('#discordjs/rest');
const test = require('../commonFunctions/test.js');
var options = async function getOptions(){
let x = await test.getClasses();
console.log(x);
return ['test','test2'];
}
module.exports = {
data: new SlashCommandBuilder()
.setName('get-test-data')
.setDescription('Return Class and Race data from database')
.addStringOption(option =>{
option.setName('class')
.setDescription('Select a class for your character')
.setRequired(true)
for(let op of options()){
//option.addChoice(op,op);
}
return option
}
),
async execute(interaction){
},
};
This code produces the following error when start the npm for our bot on our server:
options is not a function or its return value is not iterable
I thought that maybe the function wasn't properly defined, so I replaced the contents of it with just a simple array return and the npm started without errors and the values I had passed showed up in the server.
This leads me to think that the function call in the modules.exports block is immediatly attempting to get the return value of the function and as the function is async, it isn't yet ready and is either returning undefined or a promise or something else not iteratable.
Is there a proper way to implement the code as shown? Or is this way too complex for discord.js to handle?
Is there a proper way to implement the idea at all? Like creating a json object that contains the option data which is built and saved to a file at some point prior to this command being registered and then having the code above just pull in that file for the option choices?
Alright, I found a way. Ian Malcom would be proud (LMAO).
Here is what I had to do for those with a similar issues:
I had to basically re-write our entire application. It sucks, I know, but it works so who cares?
When you run your index file for your npm, make sure that you do the following things.
Note: you can structure this however you want, this is just how I set up my js files.
Setup a function that will setup the data you need, it needs to be an async function as does everything downstream from this point on relating to the creation and registration of the slash commands.
Create a js file to act as your application setup "module". "Module" because we're faking a real module by just using the module.exports method. No package.jsons needed.
In the setup file, you will need two requires. The first is a, as of yet, non-existent data manager file; we'll do that next. The second is a require for node:fs.
Create an async function in your setup file called setup and add it to your module.exports like so:
module.exports = { setup }
In your async setup function or in a function that it calls, make a call to the function in your still as of yet non-existent data manager file. Use await so that the application doesn't proceed until something is returned. Here is what mine looks like, note that I am writing my data to a file to read in later because of my use case, you may or may not have to do the same for yours:
async function setup(){
console.log('test');
//build option choice lists
let listsBuilt = await buildChoiceLists();
if (listsBuilt){
return true;
} else {
return false;
}
}
async function buildChoiceLists(){
let classListBuilt = await buildClassList();
return true;
}
async function buildClassList(){
let classData = await classDataManager.getClassData();
console.log(classData);
classList = classData;
await writeFiles();
return true;
}
async function writeFiles(){
fs.writeFileSync('./CommandData/classList.json', JSON.stringify(classList));
}
Before we finish off this file, if you want to store anything as a property in this file and then get it later on, you can do so. In order for the data to return properly though, you will need to define a getter function in your exports. Here is an example:
var classList;
module.exports={
getClassList: () => classList,
setup
};
So, with everything above you should have something that looks like this:
const classDataManager = require('./DataManagers/ClassData.js')
const fs = require('node:fs');
var classList;
async function setup(){
console.log('test');
//build option choice lists
let listsBuilt = await buildChoiceLists();
if (listsBuilt){
return true;
} else {
return false;
}
}
async function buildChoiceLists(){
let classListBuilt = await buildClassList();
return true;
}
async function buildClassList(){
let classData = await classDataManager.getClassData();
console.log(classData);
classList = classData;
await writeFiles();
return true;
}
async function writeFiles(){
fs.writeFileSync('./CommandData/classList.json', JSON.stringify(classList));
}
module.exports={
getClassList: () => classList,
setup
};
Next that pesky non-existent DataManager file. For mine, each data type will have its own, but you might want to just combine them all into a single .js file for yours.
Same with the folder name, I called mine DataManagers, if you're combining them all into one, you could just call the file DataManager and leave it in the same folder as your appSetup.js file.
For the data manager file all we really need is a function to get our data and then return it in the format we want it to be in. I am using node-fetch. If you are using some other module for data requests, write your code as needed.
Instead of explaining everything, here is the contents of my file, not much has to be explained here:
const fetch = require('node-fetch');
async function getClassData(){
return new Promise((resolve) => {
let data = "action=GetTestData";
fetch('http://xxx.xxx.xxx.xx/backend/characterHandler.php', {
method: 'post',
headers: { 'Content-Type':'application/x-www-form-urlencoded'},
body: data
}).then(response => {
response.json().then(res => {
let status = res.status;
let clsData = res.classes;
let rcData = res.races;
if (status == "Success"){
let text = '';
let classes = [];
let races = [];
if (Object.keys(clsData).length > 0){
for (let key of Object.keys(clsData)){
let cls = clsData[key];
classes.push({
"name": key,
"code": key.toLowerCase()
});
}
}
if (Object.keys(rcData).length > 0){
for (let key of Object.keys(rcData)){
let rc = rcData[key];
races.push({
"name": key,
"desc": rc.Desc
});
}
}
resolve(classes);
}
});
});
});
}
module.exports = {
getClassData
};
This file contacts our backend php and requests data from it. It queries the data then returns it. Then we format it into an JSON structure for use later on with option choices for the slash command.
Once all of your appSetup and data manager files are complete, we still need to create the commands and register them with the server. So, in your index file add something similar to the following:
async function getCommands(){
let cmds = await comCreator.appSetup();
console.log(cmds);
client.commands = cmds;
}
getCommands();
This should go at or near the top of your index.js file. Note that comCreator refers to a file we haven't created yet; you can name this require const whatever you wish. That's it for this file.
Now, the "comCreator" file. I named mine deploy-commands.js, but you can name it whatever. Once again, here is the full file contents. I will explain anything that needs to be explained after:
const {Collection} = require('discord.js');
const {REST} = require('#discordjs/rest');
const {Routes} = require('discord-api-types/v9');
const app = require('./appSetup.js');
const fs = require('node:fs');
const config = require('./config.json');
async function appSetup(){
console.log('test2');
let setupDone = await app.setup();
console.log(setupDone);
console.log(app.getClassList());
return new Promise((resolve) => {
const cmds = [];
const cmdFiles = fs.readdirSync('./commands').filter(f => f.endsWith('.js'));
for (let file of cmdFiles){
let cmd = require('./commands/' + file);
console.log(file + ' added to commands!');
cmds.push(cmd.data.toJSON());
}
const rest = new REST({version: '9'}).setToken(config.token);
rest.put(Routes.applicationGuildCommands(config.clientId, config.guildId), {body: cmds})
.then(() => console.log('Successfully registered application commands.'))
.catch(console.error);
let commands = new Collection();
for (let file of cmdFiles){
let cmd = require('./commands/' + file);
commands.set(cmd.data.name, cmd);
}
resolve(commands);
});
}
module.exports = {
appSetup
};
Most of this is boiler plate for slash command creation though I did combine the creation and registering of the commands into the same process. As you can see, we are grabbing our command files, processing them into a collection, registering that collection, and then resolving the promise with that variable.
You might have noticed that property, was used to then set the client commands in the index.js file.
Config just contains your connection details for your discord server app.
Finally, how I accessed the data we wrote for the SlashCommandBuilder:
data: new SlashCommandBuilder()
.setName('get-test-data')
.setDescription('Return Class and Race data from database')
.addStringOption(option =>{
option.setName('class')
.setDescription('Select a class for your character')
.setRequired(true)
let ops = [];
let data = fs.readFileSync('./CommandData/classList.json','utf-8');
ops = JSON.parse(data);
console.log('test data class options: ' + ops);
for(let op of ops){
option.addChoice(op.name,op.code);
}
return option
}
),
Hopefully this helps someone in the future!

New Window Positioning in Electron

I need to know how to open new windows which is offset a little by the current window position (first window will be opened in the center)
My codes are as follows:
// index.js
const {app,BrowserWindow,Menu,MenuItem,dialog}=require('electron');
function window_open(path){
win = new BrowserWindow({show: false})
win.loadURL(path);
win.once('ready-to-show', () => {win.show()})
}
let win
app.on('ready',event=>{
'use strict';
window_open(`file://${__dirname}/index.html`)
});
This opens the initial window in the center. I am also passing this function in the new window command (cmd+n)
{
label: 'File',
submenu: [
{label: 'New Window', accelerator: 'CmdOrCtrl+N', click: () => (
window_open(`file://${__dirname}/index.html`))
},
The code works fine, except that every window is positioned the same, in the center. I would like each new windows to be offset a little.
What's the best way to achieve this?
I learned that I need these two things:
BrowserWindow.getFocusedWindow()
win.getPosition()
Combining with #pergy's response, I got the following code which finds the focused window if there is any and offsets a new window from its position, otherwise creates a new window in the center:
let win = null;
function window_open(path) {
const opts = { show: false };
if (BrowserWindow.getFocusedWindow()) {
current_win = BrowserWindow.getFocusedWindow();
const pos = current_win.getPosition();
Object.assign(opts, {
x: pos[0] + 22,
y: pos[1] + 22,
});
};
win = new BrowserWindow(opts);
win.loadURL(path);
win.once('ready-to-show', () => { win.show() });
};
app.once('ready', event => {
window_open(`file://${__dirname}/index.html`);
});
This does what I asked for in my original question, so I have decided to post this. However, I do feel that it is slow in spawning the new windows, so I won't mark this as an answer to see if there are faster approaches to this.
Update:
I have found out that waiting on 'ready-to-show' is what makes it slow, as it waits for the ready state. I have accepted this as the answer as I feel that the speed issue is dependent to the content and not the browser. Feel free to add comments on this as I am still open ears.
You can define the window's position in constructor option x and y. The currently active window's position can be retrieved with getPosition(), so you can define offset for the new window from that.
See this dummy app for example:
const { app, BrowserWindow } = require('electron')
let win = null
app.once('ready', () => {
const openWindow = () => {
const opts = {
show: false
}
if (win) {
const pos = win.getPosition()
Object.assign(opts, {
x: pos[0] + 10,
y: pos[1] + 10
})
}
win = new BrowserWindow(opts)
win.loadURL('http://google.com')
let thisWin = win
win.once('ready-to-show', () => {
thisWin.show()
})
}
setInterval(openWindow, 5000)
})

Using Redis SCAN in NODE

I have Redis with a lot of keys in some format and I want to get keys that match some pattern and do some operations on them. I don't use KEYS method since it's not recommend in production. Using SCAN I'm wondering what is the best way to write it in code. I have to do something like a while loop but using promises, my current solution looks like this (code is simplified a little):
'use strict'
const Promise = require('bluebird');
const config = require('./config');
const client = require('./clinet');
let iterator = 0;
Promise.coroutine(function* () {
do {
iterator = yield clinet.scanAsync(iterator, 'myQuery', 'COUNT', config.scanChunkSize)
.then(data => {
let nextIterator = data[0];
let values = data[1];
//do some magic with values
return nextIterator;
})
} while (iterator !== '0');
})();
Is there a better way to do it that I'm missing?
I realize this is a really old question, but I found all of the other answers very unsatisfying. Here is yet another attempt to scan in a relatively clean way using async await (WITHOUT the use of yet another external dependency). You can easily modify this to continuously delete each set of found keys (you would want to tackle them in batches like this in case there are LOTS). Pushing them into an array just demonstrates one very basic thing you could do with them during this stage.
const redis = require('redis');
const { promisify } = require('util');
const client = redis.createClient({...opts});
const scan = promisify(client.scan).bind(client);
const scanAll = async (pattern) => {
const found = [];
let cursor = '0';
do {
const reply = await scan(cursor, 'MATCH', pattern);
cursor = reply[0];
found.push(...reply[1]);
} while (cursor !== '0');
return found;
}
You can use recursion to keep calling scan until done.
function scanAsync(cursor, pattern, returnSet){
return redisClient.scanAsync(cursor, "MATCH", pattern, "COUNT", "100").then(
function (reply) {
cursor = reply[0];
var keys = reply[1];
keys.forEach(function(key,i){
returnSet.add(key);
});
if( cursor === '0' ){
return Array.from(returnSet);
}else{
return scanAsync(cursor, pattern, returnSet)
}
});
}
Pass in a Set() to make sure keys aren't duplicated
myResults = new Set();
scanAsync('0', "NOC-*[^listen]*", myResults).map(
function( myResults ){ console.log( myResults); }
);
You can try this snippet to scan (1000) keys per iteration and 'delete`.
var cursor = '0';
function scan(pattern,callback){
redisClient.scan(cursor, 'MATCH',pattern,'COUNT', '1000', function(err, reply){
if(err){
throw err;
}
cursor = reply[0];
if(cursor === '0'){
return callback();
}else{
var keys = reply[1];
keys.forEach(function(key,i){
redisClient.del(key, function(deleteErr, deleteSuccess){
console.log(key);
});
});
return scan(pattern,callback);
}
});
}
scan(strkey,function(){
console.log('Scan Complete');
});
Nice option for node-redis module is to use scan iterators. Example:
const redis = require("redis");
const client = redis.createClient();
async function getKeys(pattern="*", count=10) {
const results = [];
const iteratorParams = {
MATCH: pattern,
COUNT: count
}
for await (const key of client.scanIterator(iteratorParams)) {
results.push(key);
}
return results;
}
(Of course you can also process your keys on the fly in for await loop without storing them in additional array if that's enough for you).
If you do not want to override scan parameters (MATCH/COUNT) you can just skip them and execute client.scanIterator() without parameter (defaults will be used then, MATCH="*", COUNT=10).
I think the node bindings for Redis are pushing too much responsibility to the caller here. So I created my own library for scanning as well, using generators in node:
const redis = require('redis')
const client = redis.createClient(…)
const generators = require('redis-async-gen')
const { keysMatching } = generators.using(client)
…
for await (const key of keysMatching('test*')) {
console.info(key)
}
It's the last bit that obviously is the thing that you should care about. Instead of having to carefully control an iterator yourself, all you need to do is use a for comprehension.
I wrote more about it here.
Go through this, it may help.
https://github.com/fritzy/node-redisscan
do not use the library as it, go through the code available at
https://github.com/fritzy/node-redisscan/blob/master/index.js

Resources