@slashinfty/chronode
Version:
Command-line-based speedrunning timer
196 lines (184 loc) • 7.49 kB
JavaScript
// Import modules
import * as fs from 'fs';
import * as path from 'path';
import * as readline from 'node:readline';
import * as readlinePromises from 'node:readline/promises';
import { URL } from 'url';
import { fileURLToPath } from 'node:url';
import clear from 'console-cls';
import chalk from 'chalk';
import figlet from 'figlet';
// Import files
import { splits } from './src/Splits.js';
import { upload } from './src/SplitsIO.js';
import * as View from './src/Views.js';
// ESM __dirname
export const dirname = fileURLToPath(import.meta.url).replace('index.js', '');
// Readline
export const rl = readlinePromises.createInterface({
input: process.stdin,
output: process.stdout
});
export const status = {
"state": "splash",
"raceInfo": {}
};
// Read keypresses during process life
readline.emitKeypressEvents(rl.input);
rl.input.setRawMode(true);
// Handling keypresses
rl.input.on('keypress', async (str, key) => {
// To exit the program: ctrl+c or ctrl+d or esc
if ([`\x03`, `\x04`, `\x1B`].includes(key.sequence)) {
process.exit(1);
}
// Ignore keypresses
if (status.state === 'wait') {
return;
}
// Keys to accept...
// ...during plash screen
if (status.state === 'splash') {
if (key.name === 'n') {
View.create();
} else if (key.name === 'l') {
status.state = 'load-before';
clear();
console.log(`Press ${chalk.cyan('l')} for local file or ${chalk.cyan('s')} for splits.io`);
} else if (key.name === 'r') {
View.race();
} else if (key.name === 'h') {
View.help();
}
// ...during help screen
} else if (status.state === 'help') {
splash();
// ...when ready to load to the timer
} else if (status.state === 'ready') {
console.log(status.raceInfo);
if (Object.keys(status.raceInfo).length > 0) {
View.activeRace();
} else {
View.active();
}
// ...to determine how to load splits
} else if (status.state === 'load-before') {
if (key.name === 'l') {
View.load('local');
} else if (key.name === 's') {
View.load('splitsio');
}
// ...while the timer is active
} else if (status.state === 'timer') {
if (key.name === config.hotkeys.split) {
if (View.timer.timer.running === false && View.timer.timer.started === false) {
View.timer.start();
} else if ((View.timer.race === true && View.timer.lap < View.timer.segments.length - 1) || (View.timer.race === false && View.timer.lap < View.timer.segments.length)) {
View.timer.split();
if (View.timer.lap === View.timer.segments.length) {
status.state = 'timer-stop';
console.log(`\nPress...\n* ${chalk.cyan('r')} to reset the timer\n* ${chalk.cyan('g')} to save any new best segments\n* ${chalk.cyan('p')} to save the current run as a personal best\n* ${chalk.cyan('s')} to save the splits file locally\n* ${chalk.cyan('u')} to upload the splits file to splits.io\n* ${chalk.cyan('m')} to return to the main menu`);
}
}
} else if (key.name === config.hotkeys.undo) {
if (View.timer.timer.running === true && View.timer.lap !== 0) {
View.timer.undo();
}
} else if (key.name === config.hotkeys.skip) {
if (View.timer.timer.running === true && View.timer.lap < View.timer.segments.length - 1) {
View.timer.skip();
}
} else if (key.name === config.hotkeys.pause) {
View.timer.timer.pause();
} else if (key.name === config.hotkeys.reset) {
if (View.timer.timer.started) {
View.timer.timer.stop();
if (View.timer.segments.some(seg => seg.currSegment !== null && seg.currSegment < seg.bestSegment)) {
status.state = 'reset-check';
console.log(`\nDo you want to save your new best segments? Press ${chalk.cyan('y')} or ${chalk.cyan('n')}.`);
} else {
View.timer.reset();
}
}
} else if (key.name === config.hotkeys.quit) {
View.timer.timer.stop();
status.state = 'timer-stop';
console.log(`\nPress...\n* ${chalk.cyan('r')} to reset the timer\n* ${chalk.cyan('g')} to save any new best segments\n* ${chalk.cyan('p')} to save the current run as a personal best\n* ${chalk.cyan('s')} to save the splits file locally\n* ${chalk.cyan('u')} to upload the splits file to splits.io\n* ${chalk.cyan('m')} to return to the main menu`);
}
// ...when the timer is done
} else if (status.state === 'timer-stop') {
if (key.name === 'r') {
View.timer.reset();
status.state = 'timer';
} else if (key.name === 'g') {
View.timer.saveBests();
console.log('Best segments saved.')
} else if (key.name === 'p') {
View.timer.saveRun();
console.log('Run saved.')
} else if (key.name === 's') {
fs.writeFileSync(`${config.splitsPath}/${splits.fileName}.json`, JSON.stringify(splits, null, 4));
console.log('File saved.');
} else if (key.name === 'u') {
fs.writeFileSync(`${config.splitsPath}/${splits.fileName}.json`, JSON.stringify(splits, null, 4));
const resp = await upload();
console.log(resp);
} else if (key.name === 'm') {
splash();
}
// ...before reseting
} else if (status.state === 'reset-check') {
if (key.name === 'y') {
View.timer.saveBests();
View.timer.reset();
} else if (key.name === 'n') {
View.timer.reset();
}
}
});
// Default configuration file
const defaultConfig = {
"hotkeys": {
"split": "s",
"pause": "p",
"reset": "r",
"skip": "n",
"undo": "b",
"quit": "q"
},
"colors": {
"headers": "white",
"names": "white",
"times": "white",
"timer": "white",
"ahead": "green",
"behind": "red",
"best": "yellowBright"
},
"precision": {
"timer": "S.mm",
"splits": "M:SS",
"deltas": "S.m"
},
"splitsPath": path.resolve(dirname, 'splits')
}
// Check for config file
if (!fs.existsSync(path.resolve(dirname, 'config.json'))) {
fs.writeFileSync(path.resolve(dirname, 'config.json'), JSON.stringify(defaultConfig, null, 4));
}
export const config = JSON.parse(fs.readFileSync(path.resolve(dirname, 'config.json')));
// Check for splits folder
if (!fs.existsSync(path.resolve(dirname, 'splits')) && config.splitsPath === path.resolve(dirname, 'splits')) {
fs.mkdirSync(path.resolve(dirname, 'splits'));
}
// Splash screen
const splash = () => {
status.state = 'splash';
clear();
console.log(chalk.green(figlet.textSync('chronode', { font: "Speed" })));
console.log(`Version 0.0.6`);
console.log(`\nPress...\n* ${chalk.cyan('n')} to create new splits\n* ${chalk.cyan('l')} to load existing splits\n* ${chalk.cyan('r')} to connect to a race on racetime.gg\n* ${chalk.cyan('h')} for help`);
console.log(`\nYou can exit any time by pressing ${chalk.cyan('esc')}`);
}
splash();