UNPKG

vrem

Version:

An open-source automatic time-tracker

288 lines (287 loc) 12.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.reportCommand = exports.getReport = void 0; const data_utils_1 = require("./data_utils"); const colors_1 = __importDefault(require("./colors")); const utils_1 = require("./utils"); function formRawProgramReportFromEntries(logEntries) { const report = {}; let previousEntry = null; for (const entry of logEntries) { if (!previousEntry) { if (entry.path || entry.type === data_utils_1.programLogTypes.idle) previousEntry = entry; continue; } if (entry.type === data_utils_1.programLogTypes.begin) { previousEntry = null; } else if (entry.type === data_utils_1.programLogTypes.end || entry.type === data_utils_1.programLogTypes.idle || entry.path !== previousEntry.path) { const time = entry.timestamp - previousEntry.timestamp; if (time > 0) { // just in case if the time on the machine was changed for some reason. const path = (previousEntry.type === data_utils_1.programLogTypes.idle) ? 'idle' : previousEntry.path; if (!report[path]) { report[path] = { time: time, description: previousEntry.description, }; } else { report[path].time += time; } } if (entry.path || entry.type === data_utils_1.programLogTypes.idle) { previousEntry = entry; } } else { // do nothing } } return report; } /** * If there are entries with the same description they should be either united * or their descriptions should be amended with a name of the executable file. */ function normalizeRawReport(report) { const idleEntry = report.idle; delete report.idle; const idleTime = idleEntry ? idleEntry.time : 0; const byDescription = {}; for (const [filePath, data] of Object.entries(report)) { const key = data.description || (0, utils_1.getDescriptionByPath)(filePath); const entry = { ...data, description: key, path: filePath }; if (byDescription[key]) { byDescription[key].push(entry); } else { byDescription[key] = [entry]; } } const allEntries = []; for (const array of Object.values(byDescription)) { if (array.length > 1) { const byExeName = {}; for (const entry of array) { const exeName = (0, utils_1.getDescriptionByPath)(entry.path); if (byExeName[exeName]) { byExeName[exeName].push(entry); } else { byExeName[exeName] = [entry]; } } const byExeNameEntries = Object.entries(byExeName); const uniteEntries = (entries, descriptionAmendment = '') => entries.reduce((aggregate, entry) => { aggregate.time += entry.time; aggregate.path.push(entry.path); return aggregate; }, { path: [], description: entries[0].description + descriptionAmendment, time: 0, }); if (byExeNameEntries.length === 1) { // it means there are several entries, but all have the same exeName; allEntries.push(uniteEntries(byExeNameEntries[0][1])); // same description, but multiple paths } else { for (const [exeName, entries] of byExeNameEntries) { if (entries.length === 1) { // one path, but amended description allEntries.push({ ...entries[0], description: entries[0].description + ` (${exeName})` }); } else { allEntries.push(uniteEntries(entries, ` (${exeName})`)); // many paths with amended description } } } } else { allEntries.push(array[0]); } } return { idleTime: idleTime, entries: allEntries.sort((a, b) => -a.time + b.time), }; } const dateToIsoDateString = date => new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().split('T')[0]; function prettyPrintProgramReport(normalizedReport) { let result = '\n'; if (!normalizedReport.entries.length) { result += `${colors_1.default.yellow('There is no data for the requested period.')}\n`; return console.info(result); } const totalActiveTime = normalizedReport.entries.reduce((sum, entry) => sum + entry.time, 0); for (const data of normalizedReport.entries) { const timeString = (0, utils_1.makeDurationString)(data.time); const percent = Math.round(data.time / totalActiveTime * 10000) / 100; result += `${(`(${percent}%) `.padEnd(9) + timeString).padEnd(20)} - ${colors_1.default.green(data.description)}\n`; } result += `\n${colors_1.default.brightGreen('Total active time')}: ${(0, utils_1.makeDurationString)(totalActiveTime)}\n`; result += `${colors_1.default.yellow('Idle time')}: ${(0, utils_1.makeDurationString)(normalizedReport.idleTime)}\n`; result += `${colors_1.default.cyan('Total time')}: ${(0, utils_1.makeDurationString)(normalizedReport.idleTime + totalActiveTime)}\n`; console.info(result); } function isoDateStringToDate(string) { const regex = /^\d{4}-\d{2}-\d{2}$/; if (!regex.test(string)) { console.error(colors_1.default.red(`Incorrect date format! You should provide dates in format yyyy-mm-dd. Got: ${string}\n`)); throw new Error(); } const dateObject = new Date(string); dateObject.setTime(dateObject.getTime() + dateObject.getTimezoneOffset() * 60000); return dateObject; } function processDates(date, from, to) { const dateObj = date && isoDateStringToDate(date) || new Date(); const fromObj = from && isoDateStringToDate(from) || new Date(dateObj.getTime()); const toObj = to && isoDateStringToDate(to) || new Date((dateObj.getTime())); fromObj.setHours(0, 0, 0, 0); toObj.setHours(23, 59, 59, 999); return [fromObj, toObj]; } function _getProgramReport(from, to) { const sortedLogEntries = (0, data_utils_1.getProgramLogEntriesForDates)(from, to); if (sortedLogEntries.length) { // We should take the current program into account and add an artificial "end" entry at the end of array const endEntry = (0, data_utils_1.getEndEntryForTheCurrentProgram)(sortedLogEntries[sortedLogEntries.length - 1]); if (endEntry) { sortedLogEntries.push(endEntry); } } const rawReport = formRawProgramReportFromEntries(sortedLogEntries); return normalizeRawReport(rawReport); } function getReport(fromString, toString) { const dates = processDates(undefined, fromString, toString); return { programReport: _getProgramReport(...dates), taskReport: getTaskReport(...dates), }; } exports.getReport = getReport; function makeHeader(string = '*******') { const decoration = '*'.repeat(string.length); return `${decoration}\n${string.toUpperCase()}\n${decoration}`; } function getPeriodClause(fromDate, toDate) { const fromDateString = dateToIsoDateString(fromDate); const toDateString = dateToIsoDateString(toDate); if (fromDateString === toDateString) { return ` on the date ${fromDateString}`; } else { return ` in the period from ${fromDateString} to ${toDateString}`; } } function getTaskReport(fromDate, toDate) { const taskLogs = (0, data_utils_1.getTaskLogEntriesForDates)(fromDate, toDate); const programLogs = (0, data_utils_1.getProgramLogEntriesForDates)(fromDate, toDate); const unprocessedTasks = taskLogs.map(log => ({ ...log, autoEntries: [{ type: data_utils_1.programLogTypes.begin, timestamp: log.startTime }] })); const processedTasks = []; let previousEntry = null; for (const logEntry of programLogs) { const processedIndices = []; for (let i = 0; i < unprocessedTasks.length; i++) { const taskEntry = unprocessedTasks[i]; if (taskEntry.endTime < logEntry.timestamp) { // "begin" means that the tracker was stopped, so we cannot say that the previous program // was in focus till the very end of the task. if (logEntry.type !== data_utils_1.programLogTypes.begin) { taskEntry.autoEntries.push({ type: data_utils_1.programLogTypes.end, timestamp: taskEntry.endTime, }); } processedIndices.push(i); continue; } if (taskEntry.startTime <= logEntry.timestamp) { if (taskEntry.autoEntries.length === 1 && previousEntry) { taskEntry.autoEntries.push({ ...previousEntry, timestamp: taskEntry.startTime, }); } taskEntry.autoEntries.push(logEntry); } } processedIndices.forEach(index => { const [task] = unprocessedTasks.splice(index, 1); processedTasks.push(task); }); if (logEntry.type === data_utils_1.programLogTypes.program || logEntry.type === data_utils_1.programLogTypes.idle) { previousEntry = logEntry; } else { previousEntry = null; } } processedTasks.push(...unprocessedTasks); // in case if there is a current task const tasks = processedTasks.reduce((obj, entry) => { if (obj[entry.name]) { obj[entry.name].autoEntries.push(...entry.autoEntries); obj[entry.name].time += entry.endTime - entry.startTime; obj[entry.name].endTime = entry.endTime; obj[entry.name].current = entry.current; } else { obj[entry.name] = { //taskEntries: [entry], name: entry.name, time: entry.endTime - entry.startTime, autoEntries: [...entry.autoEntries], startTime: entry.startTime, endTime: entry.endTime, current: entry.current, }; } return obj; }, {}); return Object.values(tasks) .sort((a, b) => -a.time + b.time) .map(task => { const result = { ...task, programReport: normalizeRawReport(formRawProgramReportFromEntries(task.autoEntries)), }; delete result.autoEntries; return result; }); } function prettyPrintTaskReport(taskReport) { for (const task of taskReport) { console.info(colors_1.default.cyan(`Task "${colors_1.default.brightGreen(task.name)}" took ${colors_1.default.white((0, utils_1.makeDurationString)(task.time))}\n` + `Began: ${colors_1.default.white((0, utils_1.makeTimeStringWithDate)(new Date(task.startTime)))}\n` + (task.current ? `And is running. Current time: ${colors_1.default.white((0, utils_1.makeTimeStringWithDate)(new Date()))}` : `Ended: ${colors_1.default.white((0, utils_1.makeTimeStringWithDate)(new Date(task.endTime)))}`) + `\nThe programs used within this task:`)); prettyPrintProgramReport(task.programReport); console.info('-'.repeat(25) + '\n'); } } function reportCommand(date, { from, to }) { const [fromDate, toDate] = processDates(date, from, to); const periodClause = getPeriodClause(fromDate, toDate); console.info(colors_1.default.cyan(makeHeader('GENERAL REPORT ON USED PROGRAMS' + periodClause))); prettyPrintProgramReport(_getProgramReport(fromDate, toDate)); const taskReport = getTaskReport(fromDate, toDate); if (taskReport.length) { console.info(colors_1.default.cyan(makeHeader('REPORTS FOR EACH TASK' + periodClause) + '\n')); prettyPrintTaskReport(taskReport); } } exports.reportCommand = reportCommand;