UNPKG

@okwesil/type-test

Version:

Simple typing test program for the command line

223 lines (185 loc) 7.64 kB
#!/usr/bin/env node 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; }