@dilapidated-penguin/cubetimer
Version:
fast and lightweight CLI timer for speedcubing. Track your solves, get random scrambles, and analyze your times
533 lines • 23.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const chalk_1 = __importDefault(require("chalk"));
const commander_1 = require("commander");
const events_json_1 = require("./events.json");
const get_windows_1 = require("get-windows");
const nice_table_1 = require("nice-table");
const prompts_1 = require("@inquirer/prompts");
const nodeplotlib_1 = require("nodeplotlib");
const child_process_1 = require("child_process");
const storage = __importStar(require("./util/storage"));
const settingsUtil = __importStar(require("./util/settings"));
const path_1 = __importDefault(require("path"));
var Scrambow = require('scrambow').Scrambow;
const cfonts = require('cfonts');
const cli_title_json_1 = require("./cli-title.json");
const program = new commander_1.Command();
var saved_data = storage.loadData();
//main_window_id
let main_window_id = null;
//timer variables**********************************
let timer_running = false;
let startTime = null;
let space_been_pressed = false;
let new_scramble = false;
let solve_labelled = false;
const node_global_key_listener_1 = require("@futpib/node-global-key-listener");
const listener = new node_global_key_listener_1.GlobalKeyboardListener();
//*************************************************
console.log(cli_title_json_1.string);
function normalizeArg(arg) {
const aliases = {
fastest_solve: ['f', 'b', 'best', 'fast', 'fastest', 'fastest_time'],
slowest_solve: ['w', 's', 'worst', 'slow', 'slowest', 'slowest_timer'],
session_mean: ['m', 'mean', 'avg', 'average', 'session_mean'],
standard_deviation: ['dev', 'standard_deviation', 'std.dev', 'deviation', 'd'],
variance: ['var', 'v', 'variance', 'var.']
};
for (const [key, val] of Object.entries(aliases)) {
if ((key === arg) || (val.includes(arg))) {
return key;
}
}
return null;
}
program
.version("1.0.15")
.description("fast and lightweight CLI timer for speedcubing. Cstimer in the command line (in progress)");
program
.command('graph')
.argument('<property>', 'desired statistic to graph')
.description(`generate a graph of one of the below stats: \n
session_mean \n
standard_deviation \n
variance \n
fastest_solve \n
slowest_solve`)
.action((property) => {
const normalized_property = normalizeArg(property);
if (normalized_property !== null) {
const session_data = storage.loadStats().session_data;
if (session_data.size >= 0) {
const x_dates = Array.from(session_data.keys())
.map((ISO_date) => {
return new Date(ISO_date);
});
const y_data = x_dates.map((date) => {
return session_data.get(date.toISOString())[normalized_property];
});
const data = [
{
x: x_dates,
y: y_data,
type: 'scatter'
}
];
(0, nodeplotlib_1.plot)(data);
}
else {
console.log(`error: ` + chalk_1.default.red(`Session data.size === 0`));
}
}
else {
console.log(chalk_1.default.red(`${property}`) + ` is not a valid property. Below are the valid values`);
console.log("session_mean \n" +
"standard_deviation \n" +
"variance \n" +
"fastest_solve \n" +
"slowest_solve");
}
});
program
.command('start')
.argument('[event]', 'the event you wish to practice', '333')
.option('-f, --focusMode', 'Displays only the most important stats')
.option('-w --window', 'Opens a second command prompt window to display the informationa and stats related to the solve')
.description('Begin a session of practicing a certain event')
.action((event, options) => {
console.log(event);
if (event !== undefined) {
const normalized_event = event
.toLowerCase()
.trim();
if (validEvent(normalized_event)) {
startSession(normalized_event, options);
}
else {
console.log(chalk_1.default.red(`${event} is not a valid/supported event`));
}
}
else {
(0, prompts_1.select)({
message: 'Select an event',
choices: events_json_1.event_choices
})
.then((event_choice) => {
startSession(event_choice, options);
}).catch((error) => {
console.log(chalk_1.default.bgRed(`An error occurred`));
});
}
});
program
.command("settings")
.argument("[property]", "configure the cli to your liking")
.action((setting_to_change) => {
let current_settings = settingsUtil.loadSettings();
const settings_list = Object.keys(current_settings);
if (setting_to_change === undefined) {
console.log(chalk_1.default.green(`Configure any of the below to some new and preferred value`));
console.table(current_settings);
(0, prompts_1.select)({
message: "Select the setting you'd like to alter",
choices: settings_list
}).then((answer) => {
updateSetting(current_settings, answer);
});
}
else {
if (settings_list.indexOf(setting_to_change) !== -1) {
updateSetting(current_settings, setting_to_change);
}
else {
console.log(chalk_1.default.red('Invalid argument:' + chalk_1.default.white('The argument is not a setting to change')));
}
}
});
program
.command('show-session')
.action(() => {
const menu_length = settingsUtil.loadSettings().show_session_menu_length;
function newChoices(menu_page) {
const session_array = Array.from(storage.loadData().data.values());
let menu_choices = session_array
.map((session) => {
return {
name: session.date_formatted,
value: session.date
};
}).filter((v, index) => {
return (index >= menu_page * (menu_length)) && (index < ((menu_page + 1) * menu_length));
});
if (menu_page !== 0) {
menu_choices.unshift({
name: chalk_1.default.blue(`Back`),
value: 'back'
});
}
if (session_array[(menu_page + 1) * menu_length] !== undefined) {
menu_choices.push({
name: chalk_1.default.blue(`next`),
value: 'next'
});
}
(0, prompts_1.select)({
message: `Select the session you'd like to observe`,
choices: menu_choices
}).then((value) => {
switch (value) {
case 'back':
newChoices(menu_page - 1);
break;
case 'next':
newChoices(menu_page + 1);
break;
default:
const current_session_data = storage.loadData().data.get(value);
const current_session_stats = storage.loadStats().session_data.get(value);
let info_table = current_session_data.entries.map((instance, index) => {
const label = (instance.label === "DNF") ? chalk_1.default.red(instance.label) : instance.label;
return {
n: index + 1,
time: instance.time.toFixed(3),
label: label !== null && label !== void 0 ? label : chalk_1.default.green('OK'),
};
});
console.log(`\n`);
console.log((0, nice_table_1.createTable)(info_table, ['n', 'time', 'label']));
if (current_session_stats !== undefined) {
console.log(Object.keys(current_session_stats).map((key_name) => {
return `${key_name}: ${current_session_stats[key_name].toFixed(3)} ${chalk_1.default.green('s')}`;
})
.join(chalk_1.default.blue('\n')));
}
else {
console.log(current_session_stats);
}
break;
}
}).catch((err) => {
console.log(chalk_1.default.red(`An error has occurred:${err}`));
});
}
newChoices(0);
});
program.parse(process.argv);
function updateSetting(current_settings, property) {
const prompt = (typeof current_settings[property] === 'number') ? prompts_1.number : prompts_1.input;
prompt({
message: `Enter new value for ${property}`,
default: `${current_settings[property]}`
}).then((new_value) => {
current_settings[property] = new_value;
settingsUtil.saveSettings(current_settings);
console.log(chalk_1.default.green('settings updated!'));
console.table(current_settings);
});
}
function validEvent(event_to_check) {
return (events_json_1.events_list.indexOf(event_to_check) !== -1);
}
function startSession(event, options) {
main_window_id = (0, get_windows_1.activeWindowSync)().id;
console.clear();
const session = Date.now();
const session_date = new Date(session);
const session_date_ISO = session_date.toISOString();
cfonts.say(`session: ${session}`, {
font: 'tiny', // define the font face
align: 'center', // define text alignment
colors: ['magenta'],
background: 'transparent', // define the background color, you can also use `backgroundColor` here as key
letterSpacing: 1, // define letter spacing
});
const current_settings = settingsUtil.loadSettings();
saved_data.data.set(session_date_ISO, storage.newSessionLog(session_date, event));
saved_data.last_accessed_log = session_date_ISO;
storage.saveData(saved_data);
new_scramble = true;
listener.kill();
if (options.window || options.w) {
const scriptPath = path_1.default.join(__dirname, 'window.js');
const cmd = (0, child_process_1.spawn)('cmd.exe', ['/K', `start cmd /K node ${scriptPath} ${session_date.toISOString()}`], {
detached: true,
stdio: 'ignore',
windowsHide: false
});
cmd.unref(); // Allow the parent process to exit without waiting for this new process
cmd.on('error', (err) => console.error(`Process error: ${err.message}`));
}
newSolve(current_settings, event, session_date, options);
}
function newSolve(current_settings, event, session_date, option) {
const session_date_ISO = session_date.toISOString();
var scramble_generator = new Scrambow();
process.stdin.resume();
let scramble = scramble_generator
.setType(event)
.setLength(current_settings.scramble_length)
.get(1)[0]
.scramble_string;
process.stdout.write(`\x1b[2K\r`);
console.log(chalk_1.default.bold.red(`Scramble:`));
process.stdout.write("\x1b[2K");
console.log(stylizeScramble(scramble));
listener.addListener(function (e, down) {
process.stdout.write('\x1b[2K\r');
if ((0, get_windows_1.activeWindowSync)().id !== main_window_id) {
return;
}
if ((e.name === "D") && (e.state === "UP") && (!new_scramble)) {
const current_session = saved_data.data.get(session_date_ISO);
if (current_session.entries.length >= 1) {
current_session.entries.pop();
console.log(chalk_1.default.blue(`Last solve deleted`));
saved_data.data.set(session_date_ISO, current_session);
storage.saveData(saved_data);
}
else {
console.log(chalk_1.default.red(`There exist no entries in the current session to delete`));
}
return;
}
if ((e.name === "N") && (e.state === "UP")) {
if (!new_scramble) {
process.stdout.write('\x1b[2K');
listener.kill();
new_scramble = true;
solve_labelled = false;
newSolve(current_settings, event, session_date, option);
}
return;
}
if ((e.name === "E") && (e.state === "UP") && (!new_scramble)) {
if (!solve_labelled) {
solve_labelled = true;
const current_session = saved_data.data.get(session_date_ISO);
console.log(`\n \n`);
if (current_session.entries.length >= 1) {
(0, prompts_1.select)({
message: `Select the label for the previous solve`,
choices: [
'+3',
'DNF',
'OK'
]
}).then((answer) => {
current_session.entries.at(-1).label = answer;
saved_data.data.set(session_date_ISO, current_session);
storage.saveData(saved_data);
console.log(chalk_1.default.green(`Last solve labelled ${answer}`));
console.log(chalk_1.default.bold.magentaBright(`Whenever ready use the spacebar to start a new solve`));
}).catch((err) => {
console.log(chalk_1.default.red(`An error has occurred:${err}`));
});
}
else {
console.log(chalk_1.default.redBright(`There exist no entries in the current session to label`));
}
console.log(`\n \n`);
return;
}
else {
console.log(chalk_1.default.redBright(`The solve has already been labelled.`));
return;
}
}
if ((e.name === "SPACE") && (new_scramble)) {
if (!timer_running) {
if (e.state === "DOWN") {
if (!space_been_pressed) {
space_been_pressed = true;
process.stdout.write(chalk_1.default.bgRed('...') + `\n`);
}
else {
process.stdout.write("\b \b");
}
}
else {
if (space_been_pressed) {
space_been_pressed = false;
process.stdout.write("\x1b[F"); //move back up a line
process.stdout.write('\x1b[2K'); // Clear the line
console.log(chalk_1.default.bgGreenBright('SOLVE') +
'\n \n');
startTimer();
}
}
}
else {
if (e.state === "DOWN") {
const elapsedTime = stopTimer();
new_scramble = false;
const current_session = saved_data.data.get(session_date_ISO);
current_session.entries.push({
scramble: scramble,
time: elapsedTime,
label: null
});
const session_average = current_session
.entries
.reduce((acc, curr) => {
return acc += curr.time;
}, 0) / current_session.entries.length;
const best_time = current_session
.entries
.reduce((acc, curr) => {
if (acc < curr.time) {
return acc;
}
else {
return curr.time;
}
}, Infinity);
const worst_time = current_session
.entries
.reduce((acc, curr) => {
if (acc > curr.time) {
return acc;
}
else {
return curr.time;
}
}, -Infinity);
const variance = current_session
.entries
.reduce((acc, curr) => {
return acc += Math.pow((session_average - curr.time), 2);
}, 0) / current_session.entries.length;
const stats_data = storage.loadStats();
const current_stats = {
session_mean: session_average,
standard_deviation: Math.sqrt(variance),
variance: variance,
fastest_solve: best_time,
slowest_solve: worst_time
};
const current_Ao5 = storage.Ao5(current_session);
const current_Ao12 = storage.Ao12(current_session);
stats_data.session_data.set(session_date_ISO, current_stats);
stats_data.pb_Ao5 = (current_Ao5 < stats_data.pb_Ao5) ? current_Ao5 : stats_data.pb_Ao5;
stats_data.pb_Ao12 = (current_Ao12 < stats_data.pb_Ao12) ? current_Ao12 : stats_data.pb_Ao12;
storage.saveStats(stats_data);
saved_data.data.set(session_date_ISO, current_session);
storage.saveData(saved_data);
process.stdout.write("\b \b");
cfonts.say(`${elapsedTime.toFixed(2)}s`, {
font: 'block', // define the font face
align: 'center', // define text alignment
colors: ['white'],
background: 'transparent', // define the background color, you can also use `backgroundColor` here as key
letterSpacing: 1, // define letter spacing
});
process.stdout.write("\b \b");
console.log(chalk_1.default.bold(`Time: `) + elapsedTime.toFixed(4) + chalk_1.default.green('s') +
`\n`);
console.log(chalk_1.default.bold(`Ao5: `) + chalk_1.default.magenta(current_Ao5 !== null && current_Ao5 !== void 0 ? current_Ao5 : "--") + chalk_1.default.green(`s`));
console.log(chalk_1.default.bold(`Ao12: `) + chalk_1.default.magenta(current_Ao12 !== null && current_Ao12 !== void 0 ? current_Ao12 : "--") + chalk_1.default.green(`s`) +
`\n \n`);
if (!(option.focusMode || option.f) && !(option.w || option.window)) {
//solves
console.table((0, nice_table_1.createTable)(current_session.entries.map((instance) => {
var _a;
return {
time: instance.time.toFixed(3),
label: (_a = instance.label) !== null && _a !== void 0 ? _a : 'OK'
};
}), ['time', 'label']));
//stats
const titles = ['average', 'std. dev.', 'variance', 'fastest', 'slowest'];
const stats_string = Object.keys(current_stats)
.map((stat_name, index) => {
return `${titles[index]}: ${chalk_1.default.bold(current_stats[stat_name].toFixed(3))}`;
})
.join(chalk_1.default.blue(` | `));
console.log(stats_string + `\n`);
console.log(`\n`);
console.log(chalk_1.default.dim(`To label/delete the last solve simply use (`) +
chalk_1.default.italic.yellow(`e`) + chalk_1.default.dim(`/`) + chalk_1.default.italic.yellow(`d`) +
chalk_1.default.dim(`) respectively`));
console.log(chalk_1.default.dim(`Exit session mode using`), chalk_1.default.green(`Ctrl+C`) +
`\n`);
console.log(chalk_1.default.bold.magentaBright(`Whenever ready use the spacebar to start a new solve using`) + chalk_1.default.italic.yellow(` n`) +
`\n \n`);
}
//reset
timer_running = false;
startTime = null;
space_been_pressed = false;
}
}
return;
}
});
}
function stopTimer() {
if (!startTime)
return;
timer_running = false;
const endTime = process.hrtime(startTime);
return endTime[0] + endTime[1] / 1e9;
}
function startTimer() {
startTime = process.hrtime();
timer_running = true;
}
function stylizeScramble(scramble) {
const colorMap = {
'r': chalk_1.default.redBright,
'l': chalk_1.default.blueBright,
'u': chalk_1.default.cyanBright,
'd': chalk_1.default.greenBright,
"'": chalk_1.default.whiteBright,
'2': chalk_1.default.cyan,
'F': chalk_1.default.magenta.underline,
};
return scramble
.trim()
.split('')
.map(char => {
const stylize = colorMap[char] || chalk_1.default.magenta;
return stylize(char);
})
.join('');
}
//# sourceMappingURL=index.js.map