svcorelib
Version:
Core library used in the projects of Sv443 and the Sv443 Network. Contains tons of miscellaneous QoL features.
266 lines (206 loc) • 7.38 kB
JavaScript
const { EventEmitter } = require("events");
const allOfType = require("../functions/allOfType");
const unused = require("../functions/unused");
const keypress = require("keypress");
const { NoStdinError } = require("./Errors");
const col = require("../objects/colors").fg;
/** @typedef {import("../types/SelectionMenu").SelectionMenuResult} SelectionMenuResult */
const inputCooldown = 35;
keypress(process.stdin);
// TODO: refactor this according to .d.ts
class SelectionMenu extends EventEmitter
{
/* Emits events:
* "submit" - called when user submits an option - (result: SelectionMenuResult) => {}
*/
constructor(title, settings)
{
super();
if(!process.stdin || !process.stdin.isTTY || typeof process.stdin.setRawMode != "function")
throw new NoStdinError(`The current terminal doesn't have a stdin stream or is not a compatible TTY terminal.`);
if(settings === undefined || typeof settings !== "object")
{
settings = {
overflow: true, // if the user scrolls past the end or beginning, should the SelectionMenu overflow to the other side?
cancelable: true, // whether or not the user can cancel the prompt with the Esc key
};
}
else
{
settings = {
overflow: (typeof settings.overflow === "boolean" ? settings.overflow : true),
cancelable: (typeof settings.cancelable === "boolean" ? settings.cancelable : true)
}
}
this.title = (typeof title === "string" || typeof title === "number") ? title.toString() : null;
this.settings = settings;
this.options = [];
this.optIndex = 0;
this.locale = {
escKey: "Esc",
cancel: "Cancel",
scroll: "Scroll",
returnKey: "Return",
select: "Select"
};
}
setOptions(options)
{
if(!Array.isArray(options) || (Array.isArray(options) && !allOfType(options, "string")))
throw new TypeError(`Parameter "options" is not an array of strings`);
this.options = options;
return true;
}
addOption(option)
{
if(typeof option != "string")
return "Parameter \"option\" is not of type string";
this.options.push(option);
return true;
}
onSubmit(callback_fn)
{
return new Promise((pRes, pRej) => {
this.promiseRes = pRes;
this.promiseRej = pRej;
if(typeof callback_fn == "function")
this.callbackFn = callback_fn;
});
}
open()
{
if(this.options.length == 0)
return "No options were set in the FolderPrompt. Use the methods \"setOptions()\" or \"addOption()\" to add some before opening the FolderPrompt.";
this.addListeners();
this.update();
}
/**
* @private
*/
update()
{
this.clearConsole();
let logTxt = [];
if(typeof this.title == "string")
logTxt.push(`${col.blue}${this.title}${col.rst}\n`);
this.options.forEach((opt, i) => {
logTxt.push(`${i == this.optIndex ? `${col.yellow}> ${opt}` : ` ${opt}`}${col.rst}`);
});
logTxt.push(`\n\n${this.settings.cancelable ? `[${this.locale.escKey}] ${this.locale.cancel} - ` : ""}[▲ ▼] ${this.locale.scroll} - [${this.locale.returnKey}] ${this.locale.select} `);
process.stdout.write(logTxt.join("\n"));
}
/**
*@private
*/
addListeners()
{
process.stdin.setRawMode(true);
process.stdin.on("keypress", (char, key) => {
unused(char);
if(this.onCooldown || !key)
return;
this.onCooldown = true;
setTimeout(() => {
this.onCooldown = false;
}, inputCooldown);
switch(key.name)
{
case "c": // exit process if CTRL+C is pressed
if(key.ctrl === true)
process.exit(0);
break;
case "space":
case "return":
// submit currently selected option
this.removeListeners();
this.execCallbacks();
break;
case "s":
case "down":
this.optIndex++;
if(this.settings.overflow && this.optIndex > (this.options.length - 1))
this.optIndex = 0;
else if(this.optIndex > (this.options.length - 1))
this.optIndex = (this.options.length - 1);
this.update();
break;
case "a":
case "up":
if(this.settings.overflow && this.optIndex == 0)
this.optIndex = (this.options.length - 1);
else if(this.optIndex == 0)
this.optIndex = 0;
else
this.optIndex--;
this.update();
break;
case "escape":
if(this.settings.cancelable)
{
this.removeListeners();
this.execCallbacks(true);
}
break;
}
});
process.stdin.resume();
}
/**
* @private
*/
execCallbacks(canceled)
{
if(typeof canceled != "boolean")
canceled = false;
let retObj = {
canceled: canceled,
option: {
index: this.optIndex,
description: this.options[this.optIndex]
}
};
this.clearConsole();
this.emit("submit", retObj);
this.callbackFn(retObj);
this.promiseRes(retObj);
}
/**
* @private
*/
removeListeners()
{
process.stdin.removeAllListeners(["keypress"]);
if(!process.stdin.isPaused())
process.stdin.pause();
return true;
}
close()
{
let rmRes = this.removeListeners();
let upRes = this.update();
this.clearConsole();
return (rmRes && upRes);
}
/**
* @private
*/
clearConsole()
{
process.stdout.clearLine();
process.stdout.cursorTo(0, 0);
process.stdout.write("\n");
try
{
if(console && console.clear && process.stdout && process.stdout.isTTY)
console.clear();
else if(console)
console.log("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n");
else process.stdout.write("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n");
}
catch(err)
{
return;
}
}
}
module.exports = SelectionMenu;