Compare commits

...

25 commits
v0.1.0 ... main

Author SHA1 Message Date
c97472f6f5
i18n CLI options 2023-11-27 17:57:13 +02:00
3239a7c611
Bundle all files to single JavaScript file, add development mode 2023-11-24 16:45:23 +02:00
820a49efd8
Fix main directive in package.json 2023-11-24 16:06:57 +02:00
e44fc052f2
Add dayjs configurator 2023-11-24 16:03:44 +02:00
bb375e62ea
Update package-lock.json 2023-11-24 16:03:29 +02:00
0ce99d9235
1.0.2 2023-11-23 20:56:40 +02:00
13b4c3a640
Fix typo 2023-11-23 20:56:11 +02:00
e7a67ebfa0
Fix displaying overlogged time as negative 2023-11-23 20:53:37 +02:00
f8cf9bad7d
Fix crash on missing config sections 2023-11-23 20:46:58 +02:00
acb07d0cdc
Fix config schema 2023-11-23 20:31:05 +02:00
a2e53e32a6
1.0.1 2023-11-23 20:25:46 +02:00
6bbfd25745
Fix running on older nodejs versions
The shebang does not work without extension
2023-11-23 20:24:35 +02:00
536aaa01b0
Add update instructions 2023-11-23 19:57:46 +02:00
f693c90dec
1.0.0 2023-11-23 19:51:52 +02:00
b08ab097ba
Add config.defaults.hadLunch and rework lunch messages 2023-11-23 19:51:20 +02:00
91a7362495
Update description 2023-11-23 19:17:12 +02:00
2a9e94389b
Config schema: fix language 2023-11-23 19:16:21 +02:00
567bc7a5f2
Config schema: Don't require any properties 2023-11-23 19:14:33 +02:00
71e8352ecf
Add output for lunch break duration and rework config lunch option 2023-11-23 19:13:39 +02:00
29ded9426b
Add schema to config 2023-11-23 18:57:14 +02:00
87737b0118
Fix finnish time format 2023-11-23 18:33:50 +02:00
cc6b197d82
Resolve rollup build warnings 2023-11-23 18:28:42 +02:00
b76700923d
Add support for configurable timestamp format 2023-11-23 18:04:42 +02:00
aee473b93b
Fix unlogged print 2023-11-23 17:55:42 +02:00
bb79ecdaa1
Fix missing overtime print 2023-11-23 17:49:34 +02:00
21 changed files with 393 additions and 120 deletions

View file

@ -1,9 +1,10 @@
.PHONY: help build clean update-npmjs-readme release publish
.PHONY: help prod dev clean update-npmjs-readme release publish
help:
@echo "Available targets:"
@echo " - help: Show this help message"
@echo " - build: Build the project"
@echo " - prod: Build the project in production mode"
@echo " - dev: Build the project in development mode"
@echo " - clean: Remove build artifacts"
@echo " - release: Create a new release version"
@echo " - publish: Publish the new version created with the release target"
@ -11,14 +12,17 @@ help:
node_modules:
npm install
build: node_modules
npm run build
chmod +x dist/wtc
prod: node_modules
npm run prod
dev: node_modules
npm run dev
chmod +x dist/wtc-dev.mjs
clean:
rm -r dist node_modules
release: build
release: prod
@read -p "Enter version bump (patch, minor, major): " bump && \
version=$$(npm version $$bump | grep -oP "(?<=v)[^']+") && \
echo "Version $$version created. Run 'make publish' to push the changes and publish the package."

View file

@ -35,6 +35,10 @@ After installation, you should be able to run the program with
wtc
----
== Update
To update, just run the install command again
== Rationale
Don't know if it's just me but calculating my working hours sometimes
@ -56,7 +60,7 @@ needs specifically.
== Configuration file
See the https://git.korhonen.cc/FunctionalHacker/work-time-calculator/src/branch/main/config.toml[default configuration file]
See the https://git.korhonen.cc/FunctionalHacker/work-time-calculator/src/branch/main/config/config.toml[default configuration file]
for more information on how to override configurations.
== TODO

4
bin/wtc Executable file
View file

@ -0,0 +1,4 @@
#!/bin/sh
DIR="$(dirname "$(readlink -f "$0")")"
node "$DIR/../dist/wtc.mjs" "$@"

49
config/config-schema.json Normal file
View file

@ -0,0 +1,49 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"unpaidLunchBreakDuration": {
"type": "string",
"description": "Comment out or remove if you don't have an unpaid lunch break or if you normally log your lunch break hours"
},
"language": {
"type": "string",
"enum": ["en", "fi"],
"description": "The language of the application. Currently supported languages are English (en) and Finnish (fi)"
},
"timestampFormat": {
"type": "string",
"description": "Time format used to display timestamps (started, stopped, etc.). Refer to the dayjs documentation on how to set this https://day.js.org/docs/en/display/format"
},
"defaults": {
"type": "object",
"properties": {
"workDayDuration": { "type": "string", "description": "Your work day duration" },
"startTime": { "type": "string", "description": "The time you start working" },
"stopTime": {
"type": ["string", "null"],
"description": "The time you stop working. Can either be 'now' or a time"
},
"hadLunch": { "type": "boolean", "description": "Wether you had lunch already or not" }
},
"additionalProperties": false,
"description": "Default values for inputs"
},
"askInput": {
"type": "object",
"properties": {
"workDayDuration": {
"type": "boolean",
"description": "Disable prompt for work day duration if set to false"
},
"startTime": { "type": "boolean", "description": "Disable prompt for start time if set to false" },
"stopTime": { "type": "boolean", "description": "Disable prompt for stop time if set to false" },
"logged": { "type": "boolean", "description": "Disable prompt for logged time if set to false" }
},
"additionalProperties": false,
"description": "Settings to disable prompts"
}
},
"additionalProperties": false,
"description": "Work Time Calculator configuration file. Configuration file location: $XDG_CONFIG_HOME/wtc/config.toml, usually ~/.config/wtc/config.toml"
}

View file

@ -1,26 +1,33 @@
#:schema https://git.korhonen.cc/FunctionalHacker/work-time-calculator/raw/branch/main/config/config-schema.json
# 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,
# On Unix/Linux you can place your configuration file in $XDG_CONFIG_HOME/wtc/config.toml,
# usually ~/.config/wtc/config.toml
# For windows, I don't know.
# Comment out or remove if you don't have an unpaid lunch break
# or if you normally log your lunch break hours
unpaidLunchBreakDuration = "00:30"
# The language of the application.
# Currently supported languages are "en", "fi"
language = "en"
# Time format used to display timestamps (started, stopped etc.)
# Refer to the dayjs documentation on how to set this https://day.js.org/docs/en/display/format
# For example, the finnish format would be "MM.DD.YYYY [kello] HH.mm"
timestampFormat = "YYYY-MM-DD HH:mm"
# 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
@ -34,6 +41,3 @@ workDayDuration = true
startTime = true
stopTime = true
logged = true
# It is assumed that you didn't have lunch if this is false
hadLunch = true

109
package-lock.json generated
View file

@ -1,17 +1,17 @@
{
"name": "work-time-calculator",
"version": "0.0.10",
"version": "1.0.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "work-time-calculator",
"version": "0.0.10",
"version": "1.0.2",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^2.2.5",
"chalk": "^5.3.0",
"dayjs": "^1.11.10",
"iarna-toml-esm": "^3.0.5",
"xdg-basedir": "^5.1.0",
"yargs": "^17.7.2"
},
@ -19,6 +19,7 @@
"wtc": "bin/wtc"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.5",
"@types/node": "^20.9.0",
@ -134,11 +135,6 @@
"dev": 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/@jridgewell/gen-mapping": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
@ -232,6 +228,31 @@
"node": ">= 8"
}
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "15.2.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
"integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==",
"dev": true,
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",
"deepmerge": "^4.2.2",
"is-builtin-module": "^3.2.1",
"is-module": "^1.0.0",
"resolve": "^1.22.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.78.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-terser": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
@ -479,6 +500,12 @@
"undici-types": "~5.26.4"
}
},
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"dev": true
},
"node_modules/@types/semver": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz",
@ -810,6 +837,18 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
"node_modules/builtin-modules": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
"dev": true,
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -917,6 +956,15 @@
"dev": true,
"peer": true
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -942,6 +990,14 @@
"node": ">=6.0.0"
}
},
"node_modules/emitter-component": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.2.tgz",
"integrity": "sha512-QdXO3nXOzZB4pAjM0n6ZE+R9/+kPpECA/XSELIcc54NeYVnBqIk+4DFiBgK+8QbV3mdvTG6nedl7dTYgO+5wDw==",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@ -1430,6 +1486,14 @@
"node": ">= 0.4"
}
},
"node_modules/iarna-toml-esm": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/iarna-toml-esm/-/iarna-toml-esm-3.0.5.tgz",
"integrity": "sha512-CgeDbPohnFG827UoRaCqKxJ8idiIDZDWlcHf5hUReQnZ8jHnNnhN4QJFiY12fKvr0LvuDuKAimqQfrmQnacbtw==",
"dependencies": {
"stream": "^0.0.2"
}
},
"node_modules/ignore": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
@ -1484,6 +1548,21 @@
"dev": true,
"peer": true
},
"node_modules/is-builtin-module": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
"integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
"dev": true,
"dependencies": {
"builtin-modules": "^3.3.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-core-module": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
@ -1525,6 +1604,12 @@
"node": ">=0.10.0"
}
},
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"dev": true
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@ -2124,6 +2209,14 @@
"deprecated": "Please use @jridgewell/sourcemap-codec instead",
"dev": true
},
"node_modules/stream": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stream/-/stream-0.0.2.tgz",
"integrity": "sha512-gCq3NDI2P35B2n6t76YJuOp7d6cN/C7Rt0577l91wllh0sY9ZBuw9KaSGqH/b0hzn3CWWJbpbW0W0WvQ1H/Q7g==",
"dependencies": {
"emitter-component": "^1.1.1"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",

View file

@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "work-time-calculator",
"version": "0.0.10",
"version": "1.0.2",
"description": "An interactive CLI tool to calculate work time",
"license": "MIT",
"repository": {
@ -12,13 +12,14 @@
"url": "https://git.korhonen.cc/FunctionalHacker/work-time-calculator/issues",
"email": "wtc@functionalhacker.korhonen.cc"
},
"main": "src/main.ts",
"type": "module",
"bin": {
"wtc": "dist/wtc"
"wtc": "bin/wtc"
},
"main": "dist/wtc.mjs",
"scripts": {
"build": "rollup -c"
"prod": "rollup -c ./rollup.prod.config.js",
"dev": "rollup -c ./rollup.dev.config.js"
},
"keywords": [
"work",
@ -27,6 +28,7 @@
],
"author": "Marko Korhonen <marko@korhonen.cc>",
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.5",
"@types/node": "^20.9.0",
@ -38,9 +40,9 @@
"tslib": "^2.6.2"
},
"dependencies": {
"@iarna/toml": "^2.2.5",
"chalk": "^5.3.0",
"dayjs": "^1.11.10",
"iarna-toml-esm": "^3.0.5",
"xdg-basedir": "^5.1.0",
"yargs": "^17.7.2"
}

15
rollup.dev.config.js Normal file
View file

@ -0,0 +1,15 @@
import typescript from '@rollup/plugin-typescript';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import shebang from 'rollup-plugin-add-shebang';
/** @type {import('rollup').RollupOptions} */
const config = {
input: 'src/main.ts',
output: {
format: 'esm',
file: 'dist/wtc-dev.mjs',
},
plugins: [typescript(), nodeResolve({ exportConditions: ['node'] }), shebang({ include: 'dist/wtc-dev.mjs' })],
};
export default config;

View file

@ -1,15 +1,15 @@
import typescript from '@rollup/plugin-typescript';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import terser from '@rollup/plugin-terser';
import shebang from 'rollup-plugin-add-shebang';
/** @type {import('rollup').RollupOptions} */
const config = {
input: 'src/main.ts',
output: {
format: 'esm',
file: 'dist/wtc',
file: 'dist/wtc.mjs',
},
plugins: [typescript(), terser(), shebang({ include: 'dist/wtc' })],
plugins: [typescript(), nodeResolve({ exportConditions: ['node'] }), terser()],
};
export default config;

View file

@ -1,66 +1,69 @@
import fs from 'fs';
import path from 'path';
import { xdgConfig } from 'xdg-basedir';
import toml from '@iarna/toml';
import {parse} from 'iarna-toml-esm';
import { parseDuration, parseTimestamp } from './parse.js';
import WtcConfig from './types/WtcConfig.js';
import Language from './types/Language.js';
interface RawConfig extends Omit<WtcConfig, 'defaults'> {
interface RawConfig extends Omit<WtcConfig, 'unpaidLunchBreakDuration' | 'defaults'> {
unpaidLunchBreakDuration: string;
defaults: {
workDayDuration: string;
lunchBreakDuration: string;
startTime: string;
stopTime: string;
hadLunch: boolean;
};
}
const defaultConfig: RawConfig = {
language: Language.en,
timestampFormat: 'YYYY-MM-DD HH:mm',
unpaidLunchBreakDuration: '00:30',
defaults: {
workDayDuration: '07:30',
lunchBreakDuration: '00:30',
startTime: '08:00',
stopTime: 'now',
hadLunch: true,
},
askInput: {
workDayLength: true,
workDayDuration: true,
startTime: true,
stopTime: true,
logged: true,
hadLunch: true,
},
};
const getConfig = (): WtcConfig => {
const configDir = xdgConfig || path.join(process.env.HOME ?? './', '.config');
let configFilePath = path.join(configDir, 'wtc', 'config.toml');
const configFilePath = path.join(configDir, 'wtc', 'config.toml');
let configData: RawConfig;
let configData: Partial<RawConfig>;
if (fs.existsSync(configFilePath)) {
configData = toml.parse(fs.readFileSync(configFilePath, 'utf8')) as unknown as RawConfig;
configData = parse(fs.readFileSync(configFilePath, 'utf8')) as unknown as RawConfig;
} else {
configData = defaultConfig;
}
const { language, timestampFormat, unpaidLunchBreakDuration, defaults, askInput } = configData;
return {
language: configData.language ?? defaultConfig.language,
language: language ?? defaultConfig.language,
timestampFormat: timestampFormat ?? defaultConfig.timestampFormat,
unpaidLunchBreakDuration: !unpaidLunchBreakDuration
? undefined
: parseDuration(unpaidLunchBreakDuration),
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),
workDayDuration: parseDuration(defaults?.workDayDuration ?? defaultConfig.defaults.workDayDuration),
startTime: parseTimestamp(defaults?.startTime ?? defaultConfig.defaults.startTime),
stopTime: parseTimestamp(defaults?.stopTime ?? defaultConfig.defaults.stopTime),
hadLunch: defaults?.hadLunch ?? defaultConfig.defaults.hadLunch,
},
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,
workDayDuration: askInput?.workDayDuration ?? defaultConfig.askInput.workDayDuration,
startTime: askInput?.startTime ?? defaultConfig.askInput.startTime,
stopTime: askInput?.stopTime ?? defaultConfig.askInput.stopTime,
logged: askInput?.logged ?? defaultConfig.askInput.logged,
},
};
};

10
src/dayjs.ts Normal file
View file

@ -0,0 +1,10 @@
import dayjs, {Dayjs} from 'dayjs/esm';
import duration, {Duration} from 'dayjs/esm/plugin/duration';
import customParseFormat from 'dayjs/esm/plugin/customParseFormat';
dayjs.extend(duration);
dayjs.extend(customParseFormat);
export default dayjs;
export type {Dayjs, Duration};

View file

@ -1,15 +1,14 @@
import dayjs, { Dayjs } from 'dayjs';
import dayjs, { Dayjs } from './dayjs';
import { Duration } from 'dayjs/plugin/duration.js';
import Language from './types/Language';
import { MessageKey, message } from './i18n';
export const formatTimestamp = (timestamp: Dayjs): string => timestamp.format('YYYY-MM-DD HH:mm');
export const formatTime = (time: Dayjs): string => time.format('HH:mm');
export const formatDuration =
(language: Language) =>
(duration: Duration, short?: boolean): string => {
(duration?: Duration, short?: boolean): string => {
duration = duration ?? dayjs.duration(0, 'minutes');
if (duration.hours() === 0 && duration.minutes() === 0) {
return 'none';
}
@ -21,10 +20,10 @@ export const formatDuration =
} else if (language === Language.fi) {
formatString =
duration.hours() > 0 && duration.minutes() > 0
? `H [tunti${duration.hours() > 1 ? 'a' : ''} ja] m [minuutti${duration.minutes() > 1 ? 'a' : ''}]`
? `H [tunti${duration.hours() > 1 ? 'a' : ''} ja] m [minuuttia]`
: duration.hours() > 0
? `H [tunti${duration.hours() > 1 ? 'a' : ''}]`
: `m [minutti${duration.minutes() > 1 ? 'a' : ''}]`;
: 'm [minuuttia]';
} else {
formatString =
duration.hours() > 0 && duration.minutes() > 0

View file

@ -1,12 +1,18 @@
import Language from './types/Language';
export enum MessageKey {
cliHelp,
cliVersion,
promptWorkDayDuration,
excludingLunch,
promptStartTime,
promptStopTime,
parseTimeFailed,
startTimeBeforeStopTimeError,
promptLunchBreak,
promptYesNoYes,
promptYesNoNo,
unpaidLunch,
promptLogged,
none,
startedWorking,
@ -22,9 +28,21 @@ export enum MessageKey {
}
const messages: Record<MessageKey, Record<Language, string>> = {
[MessageKey.cliHelp]: {
[Language.en]: 'Show this help',
[Language.fi]: 'Näytä tämä ohje',
},
[MessageKey.cliVersion]: {
[Language.en]: 'Show program version',
[Language.fi]: 'Näytä ohjelman versio',
},
[MessageKey.promptWorkDayDuration]: {
[Language.en]: 'How long is your work day today, excluding the lunch break? [{0}]: ',
[Language.fi]: 'Kuinka pitkä työpäiväsi on tänään, poisluettuna lounastauko? [{0}]: ',
[Language.en]: 'How long is your work day today{0}? [{1}]: ',
[Language.fi]: 'Kuinka pitkä työpäiväsi on tänään{0}? [{1}]: ',
},
[MessageKey.excludingLunch]: {
[Language.en]: ', excluding the lunch break',
[Language.fi]: ', poisluettuna lounastauko',
},
[MessageKey.promptStartTime]: {
[Language.en]: 'What time did you start work today? [{0}]: ',
@ -43,8 +61,20 @@ const messages: Record<MessageKey, Record<Language, string>> = {
[Language.fi]: 'Aloitusaika ({0}) pitää olla ennen lopetusaikaa ({1}). Ohjelma sammuu',
},
[MessageKey.promptLunchBreak]: {
[Language.en]: 'Did you have a lunch break? [y/N]: ',
[Language.fi]: 'Piditkö jo lounastauon? [k/E]: ',
[Language.en]: 'Did you have a lunch break? [{0}]: ',
[Language.fi]: 'Piditkö jo lounastauon? [{0}]: ',
},
[MessageKey.promptYesNoYes]: {
[Language.en]: 'Y/n',
[Language.fi]: 'K/e',
},
[MessageKey.promptYesNoNo]: {
[Language.en]: 'y/N',
[Language.fi]: 'k/E',
},
[MessageKey.unpaidLunch]: {
[Language.en]: 'Unpaid lunch:',
[Language.fi]: 'Palkaton lounas:',
},
[MessageKey.promptLogged]: {
[Language.en]: 'How many hours did you log already? [00:00] ',

View file

@ -1,23 +1,19 @@
import chalk from 'chalk';
import { Duration } from 'dayjs/plugin/duration';
import getConfig from './config';
import { parseDuration, parseTimestamp } from './parse';
import * as readline from 'readline/promises';
import { formatDuration, formatTime } from './format';
import dayjs, { Dayjs } from 'dayjs';
import { Dayjs } from 'dayjs';
import { WtcPromptResult } from './types/WtcPromptResult';
import duration from 'dayjs/plugin/duration.js';
import WtcConfig from './types/WtcConfig';
import { MessageKey, message } from './i18n';
dayjs.extend(duration);
import { WtcRuntimeConfig } from './types/WtcConfig';
import { MessageKey } from './i18n';
import dayjs, { Duration } from './dayjs';
const { error } = console;
const input = async (config: WtcConfig): Promise<WtcPromptResult> => {
const msg = message(config.language);
const input = async (runtimeCfg: WtcRuntimeConfig): Promise<WtcPromptResult> => {
const { config, msg } = runtimeCfg;
const fmtDuration = formatDuration(config.language);
const { defaults, askInput } = config;
const { defaults, askInput, unpaidLunchBreakDuration: lunchBreakDuration } = config;
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
@ -31,9 +27,13 @@ const input = async (config: WtcConfig): Promise<WtcPromptResult> => {
// Get work day duration
let workDayDuration: Duration | undefined = undefined;
if (askInput.workDayLength) {
if (askInput.workDayDuration) {
const durationAnswer = await rl.question(
msg(MessageKey.promptWorkDayDuration, fmtDuration(defaults.workDayDuration, true)),
msg(
MessageKey.promptWorkDayDuration,
config.unpaidLunchBreakDuration ? msg(MessageKey.excludingLunch) : '',
fmtDuration(defaults.workDayDuration, true),
),
);
if (durationAnswer !== '') {
workDayDuration = parseDuration(durationAnswer);
@ -92,13 +92,24 @@ const input = async (config: WtcConfig): Promise<WtcPromptResult> => {
let worked = dayjs.duration(stoppedAt.diff(startedAt));
let hadLunch = false;
if (askInput.hadLunch) {
const lunchAnswer = (await rl.question(msg(MessageKey.promptLunchBreak))).toLowerCase();
hadLunch = lunchAnswer === 'y' || lunchAnswer === 'k';
}
if (lunchBreakDuration) {
const lunchAnswer = (
await rl.question(
msg(
MessageKey.promptLunchBreak,
msg(config.defaults.hadLunch ? MessageKey.promptYesNoYes : MessageKey.promptYesNoNo),
),
)
).toLowerCase();
if (hadLunch) {
worked = worked.subtract(defaults.lunchBreakDuration);
if (
lunchAnswer === 'y' ||
lunchAnswer === 'k' ||
(config.defaults.hadLunch && lunchAnswer !== 'n' && lunchAnswer !== 'e')
) {
hadLunch = true;
worked = worked.subtract(lunchBreakDuration);
}
}
// Calculate unlogged time
@ -109,11 +120,11 @@ const input = async (config: WtcConfig): Promise<WtcPromptResult> => {
const logged = parseDuration(loggedAnswer);
const unLogged = worked.subtract(logged);
const workLeft = workDayDuration.subtract(worked);
let workLeftMinutes = workLeft.asMinutes();
let workedOverTime: Duration | undefined;
const workLeftMinutes = workLeft.asMinutes();
let workedOvertime: Duration | undefined;
if (workLeftMinutes < 0) {
workedOverTime = dayjs.duration(Math.round(workLeftMinutes * -1), 'minutes');
workedOvertime = dayjs.duration(Math.round(workLeftMinutes * -1), 'minutes');
}
return {
@ -125,6 +136,7 @@ const input = async (config: WtcConfig): Promise<WtcPromptResult> => {
hadLunch,
worked,
workLeft,
workedOvertime,
};
} finally {
rl.close();

View file

@ -1,9 +1,38 @@
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import ui from './ui.js';
import update from './update.js';
import getConfig from './config.js';
import { MessageKey, message } from './i18n.js';
import { WtcRuntimeConfig } from './types/WtcConfig.js';
// Build runtime config
const config = getConfig();
const msg = message(config.language);
const runtimeConfig: WtcRuntimeConfig = {
config,
msg,
};
// Process args. Yargs will exit if it detects help or version
yargs(hideBin(process.argv)).usage('Work time calculator').alias('help', 'h').alias('version', 'v').argv;
const args = await yargs(hideBin(process.argv))
.usage('Work time calculator')
.alias('help', 'h')
.alias('version', 'v')
.options({
help: {
description: msg(MessageKey.cliHelp),
},
version: {
description: msg(MessageKey.cliVersion),
},
}).argv;
// Run UI if help or version is not prompted
ui();
// Run updater if requested
if (args.update) {
update();
process.exit(0);
}
// Run UI if no arguments
ui(runtimeConfig);

View file

@ -1,40 +1,48 @@
import chalk from 'chalk';
import { formatDuration, formatTimestamp, getHoursRoundedStr } from './format';
import { formatDuration, getHoursRoundedStr } from './format';
import { WtcPromptResult } from './types/WtcPromptResult';
import { MessageKey, message } from './i18n.js';
import WtcConfig from './types/WtcConfig';
import { MessageKey } from './i18n.js';
import { WtcRuntimeConfig } from './types/WtcConfig';
import dayjs from './dayjs';
const { log } = console;
const output = (result: WtcPromptResult, config: WtcConfig) => {
const msg = message(config.language);
const fmtDuration = formatDuration(config.language);
const hoursRounded = getHoursRoundedStr(config.language);
const { startedAt, stoppedAt, stoppedWorking, worked, unLogged, workLeft, workedOverTime } = result;
const output = (result: WtcPromptResult, runtimeCfg: WtcRuntimeConfig) => {
const {config, msg} = runtimeCfg;
const { language, timestampFormat } = config;
const fmtDuration = formatDuration(language);
const hoursRounded = getHoursRoundedStr(language);
const { startedAt, stoppedAt, stoppedWorking, worked, unLogged, workLeft, workedOvertime, hadLunch } = result;
log();
log(msg(MessageKey.startedWorking), formatTimestamp(startedAt));
log(msg(MessageKey.startedWorking), startedAt.format(timestampFormat));
log(
(stoppedWorking ? msg(MessageKey.stoppedWorking) : msg(MessageKey.hoursCalculated)) +
` ${msg(MessageKey.klo)}:`,
formatTimestamp(stoppedAt),
stoppedAt.format(timestampFormat),
);
log(msg(MessageKey.workedToday), chalk.green(fmtDuration(worked)), chalk.yellow(hoursRounded(worked)));
if (hadLunch) {
log(msg(MessageKey.unpaidLunch), chalk.green(fmtDuration(config.unpaidLunchBreakDuration)));
}
const unLoggedMinutes = unLogged.asMinutes();
if (unLoggedMinutes >= 0) {
log(
msg(MessageKey.unloggedToday),
unLoggedMinutes === 0 ? chalk.green(msg(MessageKey.none)) : chalk.red(fmtDuration(unLogged)),
chalk.yellow(hoursRounded(unLogged)),
unLoggedMinutes === 0 ? '' : chalk.yellow(hoursRounded(unLogged)),
);
} else if (unLoggedMinutes < 0) {
log(chalk.red(msg(MessageKey.loggedOver, fmtDuration(unLogged))), chalk.yellow(hoursRounded(unLogged)));
const overLogged = dayjs.duration(Math.abs(unLogged.asMilliseconds()), 'milliseconds');
log(chalk.red(msg(MessageKey.loggedOver, fmtDuration(overLogged))));
}
if (workLeft.asMinutes() > 0) {
log(msg(MessageKey.workLeft, chalk.green(fmtDuration(workLeft))));
} else if (workedOverTime) {
log(msg(MessageKey.workedOvertime, chalk.green(fmtDuration(workedOverTime))));
} else if (workedOvertime) {
log(msg(MessageKey.workedOvertime, chalk.green(fmtDuration(workedOvertime))));
}
};

View file

@ -1,9 +1,4 @@
import dayjs, { Dayjs } from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat.js';
import duration, { Duration } from 'dayjs/plugin/duration.js';
dayjs.extend(customParseFormat);
dayjs.extend(duration);
import dayjs, { Dayjs, Duration } from './dayjs';
export const parseTimestamp = (time: string): Dayjs => (time === 'now' ? dayjs() : dayjs(time, 'HH:mm', true));

View file

@ -1,20 +1,29 @@
import { Dayjs } from 'dayjs';
import { Duration } from 'dayjs/plugin/duration.js';
import Language from './Language.js';
import { message } from '../i18n.js';
export default interface WtcConfig {
language: Language,
timestampFormat: string,
unpaidLunchBreakDuration?: Duration;
defaults: {
workDayDuration: Duration;
lunchBreakDuration: Duration;
startTime: Dayjs;
stopTime: Dayjs;
hadLunch: boolean;
};
askInput: {
workDayLength: boolean;
workDayDuration: boolean;
startTime: boolean;
stopTime: boolean;
logged: boolean;
hadLunch: boolean;
};
}
/** Config and current language msg function together */
export interface WtcRuntimeConfig {
config: WtcConfig;
msg: ReturnType<typeof message>;
}

View file

@ -10,5 +10,5 @@ export interface WtcPromptResult {
hadLunch: boolean;
worked: Duration;
workLeft: Duration;
workedOverTime?: Duration;
workedOvertime?: Duration;
}

View file

@ -1,11 +1,7 @@
import getConfig from './config.js';
import input from './input.js';
import output from './output.js';
import { WtcRuntimeConfig } from './types/WtcConfig.js';
const ui = async () => {
const config = getConfig();
const result = await input(config);
output(result, config);
};
const ui = async (config: WtcRuntimeConfig) => output(await input(config), config);
export default ui;

7
src/update.ts Normal file
View file

@ -0,0 +1,7 @@
const { log } = console;
const update = () => {
log('update');
};
export default update;