workwatch
Version:
A Linux terminal program for honest worktime tracking and billing.
196 lines (191 loc) • 8.06 kB
JavaScript
/**
* This file is part of the WorkWatch, a Linux terminal program for honest
* worktime tracking and billing.
*
* Copyright (C) 2020-2025 by Artur Rutkowski
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* WorkWatch is beeing developped and maintained by Artur (locust) Rutkwoski
* <locust@mailbox.org>
*/
/**
* This file is defining a default command. Default command is a replacement
* of index number 1 in a process.argv array. Instead of storing the script name
* it is replaced with an underscore "_" marking the default command.
*
* It is responsible for loading configuration, data files and validate them.
*/
const {Config} = require("../config.js");
const {TimeMeasurement, Log} = require("../file");
// The default command object. It accepts a third parameter with a next
// command to run. It is necesarry for respective file loading.
function _(parameters, appData, nextCommand) {
this.loadConfiguration = () => {
return new Promise((resolve, reject) => {
const params = Object.keys(parameters);
// The --config-file option has the highest precedence.
if (params.includes("--config-file")) {
resolve(Config.load(parameters["--config-file"]));
} else if (!params.includes("--config-file") && params.length > 0) {
// If not --config-file the rest of config options have their priority.
let config;
Promise.resolve({})
.then(parametersToSet => {
// The config module accepts parameters as an object not the
// command-line options so that they have to be converted.
return Object.assign(
parametersToSet,
...params.map(param => {
const configParam = param.replace("--", "").split("-").map(
(paramPart, paramPartIndex) => (
paramPartIndex > 0
)? paramPart.replace(paramPart[0], paramPart[0].toUpperCase()) : paramPart
).join("");
if (parameters[param] !== null && parameters[param] !== undefined) {
return {[configParam]: parameters[param]};
} else {
const error = new Error(`The parameter ${param} can't be null or undefined.`);
error.code = "ERR_CLI_WRONG_PARAMETER_VALUE";
reject(error);
}
})
);
})
.then(parametersToSet => {
// Check if there are any parameters in order not to set empty
// values when error occured.
if (Object.keys(parametersToSet).length > 0) {
// Set configuration provided by command-line.
config = new Config();
return config.set(parametersToSet);
}
})
.then(configurationSuccess => resolve(config))
.catch(cliConfigError => reject(cliConfigError));
} else {
// If there are no command-line config options use predefined config
// file locations including WORKWATCH_CONFIG_FILE environment variable.
resolve(Config.load());
}
});
};
this.loadDataFiles = () => {
return new Promise((resolve, reject) => {
// Loading data files using configured data directory and file-specific
// filename configurations.
Promise.resolve(nextCommand)
.then(commandName => {
// Load files according to the command needs.
if (commandName === "start" || commandName === "reset") {
return TimeMeasurement.load(`${appData.config.dataDir}/${appData.config.measurementFilename}.json`);
} else {
return {};
}
})
.catch(timeMeasurementLoadError => {
if (timeMeasurementLoadError.code === "ERR_FILE_NO_MEASUREMENT") {
// When there is no measurement file due to beggining a new measurement
// instantiate empty measurement file.
return new TimeMeasurement("{}", `${appData.config.dataDir}/${appData.config.measurementFilename}.json`);
} else if (
(
timeMeasurementLoadError.code === "ERR_FILE_MEASUREMENT_TOO_LONG_MEASUREMENT"
|| timeMeasurementLoadError.code === "ERR_FILE_MEASUREMENT_PREVIOUS_MEASUREMENT_NOT_SAVED"
)
&& nextCommand === "reset"
) {
// Load the measurement file without validations because it will be reset.
return TimeMeasurement.load(`${appData.config.dataDir}/${appData.config.measurementFilename}.json`, false);
} else {
// Reject any other error.
reject(timeMeasurementLoadError);
}
})
.then(timeMeasurementObject => {
if (timeMeasurementObject instanceof TimeMeasurement) {
// Add measurement file to the appData for usage by another command.
appData.timeMeasurement = timeMeasurementObject;
// Don't load the log file when continuing a measurement.
if (nextCommand === "start") {
try {
// Check if the measurement file is saved which means
// the measurement continuation.
appData.timeMeasurement.time;
// If it is saved don't do anything.
resolve(true);
} catch (error) {
// Error generally means the empty file in this context, so
// handle is empty allowing the log file to be loaded.
}
}
}
// Proceed to load the worklog file.
return Log.load(`${appData.config.dataDir}/${appData.config.logFilename}.json`);
})
.catch(logLoadError => {
if (logLoadError.code === "ERR_FILE_NO_LOG") {
// Instantiate empty log when you have no worklog yet.
return new Log("[]", `${appData.config.dataDir}/${appData.config.logFilename}.json`);
} else {
reject(logLoadError);
}
})
.then(logObject => {
if (logObject instanceof Log) {
// Add to appData for the same reason as with measurement.
appData.log = logObject;
resolve(true);
}
});
});
};
this.run = () => {
return new Promise((resolve, reject) => {
// Start the command with loading configuration.
this.loadConfiguration()
.catch(loadConfigurationError => {
if (loadConfigurationError.code === "ERR_NO_CONFIG") {
// If no config has been found in predefined locations instantiate
// the default configuration defined in the config module.
return new Config();
} else {
reject(loadConfigurationError);
}
})
.then(configObject => {
if (configObject instanceof Config) {
// Add current configuration for other commands' usage. Here, the
// configuration content is added, not the config object.
appData.config = {};
Object.keys(configObject).forEach(
key => appData.config[key] = configObject[key]
);
// Verify if predefined values are existing and correct.
return configObject.verify();
}
})
.then(configVerifySuccess => {
if (typeof configVerifySuccess === "boolean" && configVerifySuccess) {
return this.loadDataFiles();
}
})
.then(dataFilesLoadSuccess => resolve(0))
.catch(configVerifyOrDataLoadError => {
reject(configVerifyOrDataLoadError);
});
});
};
}
module.exports = _;