Add configuration file support

This commit is contained in:
Marko Korhonen 2023-11-22 20:57:42 +02:00
parent d141fe1d36
commit ab51b4c11d
Signed by: FunctionalHacker
GPG key ID: A7F78BCB859CD890
7 changed files with 204 additions and 51 deletions

View file

@ -52,12 +52,14 @@ To alleviate my pains, I included the following features
This is a highly opinionated tool I built for my specific needs. This is a highly opinionated tool I built for my specific needs.
There probably exists other tools to do the same task There probably exists other tools to do the same task
(maybe even better) but I wanted something simple that fits for my (maybe even better) but I wanted something simple that fits for my
needs specifically. In time, I will probably make it more generic and needs specifically.
configurable. For now, the following assumptions are made.
* You have an unpaid 30 minute lunch break == Configuration file
See the https://git.korhonen.cc/FunctionalHacker/work-time-calculator/src/branch/main/config.toml[default configuration file]
for more information on how to override configurations.
== TODO == TODO
* [ ] Configuration file for default settings and altering behaviour in interactive mode * [x] Configuration file for default settings and altering behaviour in interactive mode
* [ ] Non-interactive mode with CLI arguments parsing * [ ] Non-interactive mode with CLI arguments parsing

29
config.toml Normal file
View file

@ -0,0 +1,29 @@
# Work Time Calculator configuration file
# This is the default configuration.
# You can only partially override the config,
# any missing values will use the defaults described here.
# You can place your configuration file in $XDG_CONFIG_HOME/wtc/config.toml,
# usually ~/.config/wtc/config.toml
# This section is for default values for inputs
[defaults]
# Leave empty if you don't have an unpaid lunch break
# or if you normally log your lunch break hours
lunchBreakDuration = "00:30"
# Your work day duration
workDayDuration = "07:30"
# The time you start working
startTime = "08:00"
# The time you stop working. Can either be "now" or a time
stopTime = "now"
# This section can be used to disable prompts for each
# of the questions. The default value will be used automatically
# if the setting is set to false
[askInput]
workDayDuration = true
startTime = true
stopTime = true
logged = true
# It is assumed that you didn't have lunch if this is false
hadLunch = true

18
package-lock.json generated
View file

@ -9,8 +9,10 @@
"version": "0.0.10", "version": "0.0.10",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iarna/toml": "^2.2.5",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"xdg-basedir": "^5.1.0",
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },
"bin": { "bin": {
@ -128,6 +130,11 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"node_modules/@iarna/toml": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz",
"integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -1779,6 +1786,17 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"node_modules/xdg-basedir": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz",
"integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View file

@ -34,8 +34,10 @@
"typescript": "^5.2.2" "typescript": "^5.2.2"
}, },
"dependencies": { "dependencies": {
"@iarna/toml": "^2.2.5",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"xdg-basedir": "^5.1.0",
"yargs": "^17.7.2" "yargs": "^17.7.2"
} }
} }

85
src/config.ts Normal file
View file

@ -0,0 +1,85 @@
import fs from 'fs';
import path from 'path';
import { xdgConfig } from 'xdg-basedir';
import toml from '@iarna/toml';
import { Dayjs } from 'dayjs';
import { Duration } from 'dayjs/plugin/duration.js';
import { parseDuration, parseTimestamp } from './parse.js';
const { debug } = console;
interface Config {
defaults: {
workDayDuration: Duration;
lunchBreakDuration: Duration;
startTime: Dayjs;
stopTime: Dayjs;
};
askInput: {
workDayLength: boolean;
startTime: boolean;
stopTime: boolean;
logged: boolean;
hadLunch: boolean;
};
}
interface RawConfig extends Omit<Config, 'defaults'> {
defaults: {
workDayDuration: string;
lunchBreakDuration: string;
startTime: string;
stopTime: string;
};
}
const defaultConfig: RawConfig = {
defaults: {
workDayDuration: '07:30',
lunchBreakDuration: '00:30',
startTime: '08:00',
stopTime: 'now',
},
askInput: {
workDayLength: true,
startTime: true,
stopTime: true,
logged: true,
hadLunch: true,
},
};
const getConfig = (): Config => {
const configDir = xdgConfig || path.join(process.env.HOME ?? './', '.config');
let configFilePath = path.join(configDir, 'wct', 'config.toml');
let configData: RawConfig;
if (fs.existsSync(configFilePath)) {
configData = toml.parse(fs.readFileSync(configFilePath, 'utf8')) as unknown as RawConfig;
} else {
debug('Configuration file does not exist, loading defaults');
configData = defaultConfig;
}
return {
defaults: {
workDayDuration: parseDuration(
configData.defaults.workDayDuration ?? defaultConfig.defaults.workDayDuration,
),
lunchBreakDuration: parseDuration(
configData.defaults.lunchBreakDuration ?? defaultConfig.defaults.workDayDuration,
),
startTime: parseTimestamp(configData.defaults.startTime ?? defaultConfig.defaults.startTime),
stopTime: parseTimestamp(configData.defaults.stopTime ?? defaultConfig.defaults.stopTime),
},
askInput: {
workDayLength: configData.askInput.workDayLength ?? defaultConfig.askInput.workDayLength,
startTime: configData.askInput.startTime ?? defaultConfig.askInput.startTime,
stopTime: configData.askInput.stopTime ?? defaultConfig.askInput.stopTime,
logged: configData.askInput.logged ?? defaultConfig.askInput.logged,
hadLunch: configData.askInput.hadLunch ?? defaultConfig.askInput.hadLunch,
},
};
};
export default getConfig;

View file

@ -6,30 +6,31 @@ import * as readline from 'readline/promises';
import { formatDuration, formatTime, formatTimestamp, getHoursRoundedStr } from './format.js'; import { formatDuration, formatTime, formatTimestamp, getHoursRoundedStr } from './format.js';
import duration, { Duration } from 'dayjs/plugin/duration.js'; import duration, { Duration } from 'dayjs/plugin/duration.js';
import { parseDuration, parseTimestamp } from './parse.js'; import { parseDuration, parseTimestamp } from './parse.js';
import getConfig from './config.js';
dayjs.extend(duration); dayjs.extend(duration);
const { log, error } = console; const { log, error } = console;
const defaultStartTime = '08:00';
const lunchBreakDuration = dayjs.duration({ minutes: 30 });
const defaultWorkDayDuration = dayjs.duration({ hours: 7, minutes: 30 });
const ui = async () => { const ui = async () => {
const { defaults, askInput } = getConfig();
const rl = readline.createInterface({ const rl = readline.createInterface({
input: process.stdin, input: process.stdin,
output: process.stdout, output: process.stdout,
}); });
let startedAt: Dayjs | undefined = undefined; let startedAt: Dayjs | undefined = undefined;
const now = dayjs(); let stoppedAt: Dayjs | undefined = undefined;
let stoppedWorking = false;
try { try {
// Get work day duration // Get work day duration
let workDayDuration: Duration | undefined = undefined; let workDayDuration: Duration | undefined = undefined;
if (askInput.workDayLength) {
const durationAnswer = await rl.question( const durationAnswer = await rl.question(
`How long is your work day today, excluding the lunch break? [${formatDuration( `How long is your work day today, excluding the lunch break? [${formatDuration(
defaultWorkDayDuration, defaults.workDayDuration,
true, true,
)}] `, )}] `,
); );
@ -38,43 +39,51 @@ const ui = async () => {
if (workDayDuration.asMinutes() <= 0) { if (workDayDuration.asMinutes() <= 0) {
error( error(
chalk.red( chalk.red(
`Failed to parse ${durationAnswer} to duration, using default work day duration ${defaultWorkDayDuration}`, `Failed to parse ${durationAnswer} to duration, using default work day duration ${formatDuration(
defaults.workDayDuration,
true,
)}`,
), ),
); );
workDayDuration = undefined; workDayDuration = undefined;
} }
} }
if (!workDayDuration) {
workDayDuration = defaultWorkDayDuration;
} }
// Calculate worked time if (!workDayDuration) {
const startTimeAnswer = await rl.question(`What time did you start work today? [${defaultStartTime}] `); workDayDuration = defaults.workDayDuration;
}
if (askInput.startTime) {
const startTimeAnswer = await rl.question(
`What time did you start work today? [${formatTime(defaults.startTime)}] `,
);
if (startTimeAnswer !== '') { if (startTimeAnswer !== '') {
startedAt = parseTimestamp(startTimeAnswer); startedAt = parseTimestamp(startTimeAnswer);
if (!startedAt.isValid()) { if (!startedAt.isValid()) {
error( error(
chalk.red( chalk.red(
`Failed to parse ${startTimeAnswer} to time, using default start time ${defaultStartTime}`, `Failed to parse ${startTimeAnswer} to time, using default start time ${formatTime(
defaults.startTime,
)}`,
), ),
); );
} }
} }
if (!startedAt?.isValid()) {
startedAt = parseTimestamp(defaultStartTime);
} }
let stoppedWorking = false; if (!startedAt?.isValid()) {
let stoppedAt: Dayjs | undefined = undefined; startedAt = defaults.startTime;
}
if (askInput.stopTime) {
const stoppedAnswer = await rl.question( const stoppedAnswer = await rl.question(
`What time did you stop working (default is current time if you didn't stop yet)? [${formatTime(now)}] `, `What time did you stop working (default is current time if you didn't stop yet)? [${formatTime(
defaults.stopTime,
)}] `,
); );
if (stoppedAnswer === '') { if (stoppedAnswer !== '') {
stoppedAt = now;
} else {
stoppedWorking = true; stoppedWorking = true;
stoppedAt = parseTimestamp(stoppedAnswer); stoppedAt = parseTimestamp(stoppedAnswer);
if (!stoppedAt.isValid()) { if (!stoppedAt.isValid()) {
@ -82,6 +91,11 @@ const ui = async () => {
stoppedAt = dayjs(); stoppedAt = dayjs();
} }
} }
}
if (!stoppedAt) {
stoppedAt = defaults.stopTime;
}
if (stoppedAt.isSame(startedAt) || stoppedAt.isBefore(startedAt)) { if (stoppedAt.isSame(startedAt) || stoppedAt.isBefore(startedAt)) {
error( error(
@ -96,8 +110,11 @@ const ui = async () => {
let worked = dayjs.duration(stoppedAt.diff(startedAt)); let worked = dayjs.duration(stoppedAt.diff(startedAt));
if ((await rl.question('Did you have a lunch break? [Y/n] ')).toLowerCase() !== 'n') { const hadLunch =
worked = worked.subtract(lunchBreakDuration); askInput.hadLunch && (await rl.question('Did you have a lunch break? [Y/n] ')).toLowerCase() !== 'n';
if (hadLunch) {
worked = worked.subtract(defaults.lunchBreakDuration);
} }
// Calculate unlogged time // Calculate unlogged time

View file

@ -5,7 +5,7 @@ import duration, { Duration } from 'dayjs/plugin/duration.js';
dayjs.extend(customParseFormat); dayjs.extend(customParseFormat);
dayjs.extend(duration); dayjs.extend(duration);
export const parseTimestamp = (time: string): Dayjs => dayjs(time, 'HH:mm', true); export const parseTimestamp = (time: string): Dayjs => (time === 'now' ? dayjs() : dayjs(time, 'HH:mm', true));
export const parseDuration = (time: string): Duration => { export const parseDuration = (time: string): Duration => {
const [hours, minutes] = time.split(':').map(Number); const [hours, minutes] = time.split(':').map(Number);