Add configuration file support
This commit is contained in:
parent
d141fe1d36
commit
ab51b4c11d
7 changed files with 204 additions and 51 deletions
10
README.adoc
10
README.adoc
|
@ -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
29
config.toml
Normal 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
18
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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
85
src/config.ts
Normal 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;
|
63
src/main.ts
63
src/main.ts
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue