@okwesil/type-test
Version:
Simple typing test program for the command line
223 lines (185 loc) • 7.64 kB
JavaScript
import chalk from "chalk";
import { generateSampleText } from "./text.js";
import { VERSION, settingsMenu, save, loadSave, defaultSave, SAVE_FILE } from "./save.js";
import readline from "node:readline";
import { existsSync } from "fs";
import PromptSync from "prompt-sync";
export const toggleDebugMode = () => { debugMode = !debugMode };
export let debugMode = false;
const prompt = PromptSync();
const cursor = {
save: () => process.stdout.write("\x1b[s"),
restore: () => process.stdout.write("\x1b[u"),
hide: () => process.stdout.write("\x1b[?25l"),
show: () => process.stdout.write("\x1b[?25h"),
/**
* clears
* all text off screen,
* all text on screen.
*
* and resets cursor position
*/
clearTerminal: () => process.stdout.write("\x1b[2J\x1b[3J\x1b[H")
};
// opening message
if (!existsSync(SAVE_FILE)) {
defaultSave();
}
while (true) {
console.log(`${chalk.bold.green("Type-Test " + VERSION)} | Personal Best: ${chalk.bold.green(loadSave().personalBest.toString())}`);
console.log(`Hit ${chalk.bold.green("enter")} to start test or type ${chalk.bold.green("settings")} to change settings`);
const response = prompt();
switch (response) {
case "exit":
case "quit":
case null:
console.log(chalk.yellow("exiting"))
process.exit(0);
case "settings":
case "setting":
case "set":
case "s":
settingsMenu();
break;
}
const result =
await startTypeTest()
.catch(result => {
console.log(chalk.bold.red("You stopped the test"));
// so testsCompleted stays the same
save(undefined, undefined, undefined, loadSave().testsCompleted - 1);
return result;
});
const [wpm, correctCharactersTyped] = result;
save(undefined, undefined,loadSave().correctCharactersTyped + correctCharactersTyped, loadSave().testsCompleted + 1)
if (wpm > loadSave().personalBest) {
save(wpm);
console.log(chalk.bold.yellow("New Personal Best!"));
};
console.log("Your words per minute was " + chalk.bold.green(wpm.toString()));
console.log("-------------");
}
/**
*
* @returns {Promise<[number, number]>};
*/
function startTypeTest() {
// all time in milliseconds
let startTime;
let pausedTime = 0;
let whenStartedResizing = 0;
let resizing = false;
let wordsTyped = 0;
//current letter/index in the sample text
let currentLetter = 0;
let lastCurrentLetter = 0;
//set raw for better terminal control
process.stdin.setRawMode(true);
process.stdin.setEncoding("utf8");
//generate sample text and align cursor with start of sample text
const sampleText = generateSampleText(loadSave().textLength);
console.log(chalk.green("Start typing to begin"));
// text preview
cursor.save();
console.log(sampleText);
cursor.restore();
// return a promise that resolves when the user finishes typing
return new Promise((resolve, reject) => {
/**
*
* @param {string} key
* @returns {void}
*/
const onKeyPress = key => {
if (resizing == true) {
pausedTime += performance.now() - whenStartedResizing;
}
resizing = false;
const onResize = () => {
// if just started resizing
if (resizing == false) {
whenStartedResizing = performance.now();
}
resizing = true;
cursor.clearTerminal();
process.stdout.write(chalk.bold.red("Paused while resizing, Press any key to continue"));
}
if (currentLetter == 0) {
// once first key is pressed count that as the start time
startTime = performance.now();
process.stdout.on("resize", onResize);
}
//in seconds
const elapsedTime = (performance.now() - startTime - pausedTime) / 1000;
const wpm = Math.round((wordsTyped / elapsedTime) * 60);
// if Ctrl + C is pressed
if (key == "\u0003") {
process.stdin.setRawMode(false);
cursor.clearTerminal();
process.stdin.removeListener("data", onKeyPress);
process.stdout.removeAllListeners("resize");
reject([wpm, currentLetter]);
return;
}
lastCurrentLetter = currentLetter;
if (key == sampleText[currentLetter]) {
currentLetter++;
}
// hide the redrawing of text and cursor moving
let typingInfo = "WPM: " + chalk.green(wpm.toString());
if (debugMode) {
typingInfo += " | CL: " + currentLetter + " | LCL: " + lastCurrentLetter + " | WT: " + wordsTyped;
}
cursor.hide();
console.clear();
let content = "\x1b[H";
content += typingInfo + "\n";
content += markText(sampleText, currentLetter, chalk.bold.green, lastCurrentLetter == currentLetter);
// erases extra text
content += "\x1b[J";
cursor.show();
process.stdout.write(content);
// if they are the same then user typed an incorrect letter
if (currentLetter % 5 == 0 && lastCurrentLetter != currentLetter) wordsTyped++;
/**
* move the cursor to the correct position in the terminal based on its width
*
* however tall the typing info is + where the user is in the text
*/
readline.cursorTo(process.stdout, currentLetter % process.stdout.columns, Math.ceil(typingInfo.length / process.stdout.columns) + Math.floor(currentLetter / process.stdout.columns));
// after setup/render show the result to the user so they knows where they are
cursor.show();
// if reached the end of the sample text stop listening
if (currentLetter == sampleText.length) {
process.stdin.setRawMode(false);
cursor.clearTerminal();
//stop listening for inputs once text completed
process.stdin.removeListener("data", onKeyPress);
// stop checking for resizes
process.stdout.removeAllListeners("resize");
resolve([wpm, sampleText.length]);
}
}
process.stdin.on("data", onKeyPress);
})
}
/**
*
* @param {string} text
* @param {number} index
* @param {function()} markFunc
* @param {boolean} incorrect
* @returns
*/
function markText(text, index, markFunc, incorrect) {
let marked = "";
if (incorrect) {
marked = markFunc(text.slice(0, index));
marked += chalk.bgRed(text[index]);
} else {
marked = markFunc(text.slice(0, index));
}
let ouput = marked + text.slice(incorrect ? index + 1 : index);
return ouput;
}