From 82e18eb286fc20bf36bb341a79f02171ef53305d Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 21:28:44 +0200 Subject: [PATCH 01/41] Use a shell script to fix running on older node versions --- bin/wtc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/wtc b/bin/wtc index 525bc83..fca2d82 100755 --- a/bin/wtc +++ b/bin/wtc @@ -1,2 +1,3 @@ -#!/usr/bin/env node -import '../dist/main.js'; +#!/bin/sh + +node '../dist/main.js'; From 73b671cab2f7c5bf68ace5308ffc8d32391717f0 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 21:29:12 +0200 Subject: [PATCH 02/41] 0.0.8 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8cd30d2..602ab20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "work-time-calculator", - "version": "0.0.7", + "version": "0.0.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "work-time-calculator", - "version": "0.0.7", + "version": "0.0.8", "license": "MIT", "dependencies": { "chalk": "^5.3.0", diff --git a/package.json b/package.json index 0114aee..3d7414e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "work-time-calculator", - "version": "0.0.7", + "version": "0.0.8", "description": "An interactive CLI tool to calculate work time", "license": "MIT", "repository": { From d2d2dda9a4097a2f4fdda2736a0a6cda78397668 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 21:33:55 +0200 Subject: [PATCH 03/41] Fix pwd in startup script --- bin/wtc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/wtc b/bin/wtc index fca2d82..7e0a682 100755 --- a/bin/wtc +++ b/bin/wtc @@ -1,3 +1,4 @@ #!/bin/sh -node '../dist/main.js'; +DIR="$(dirname "$(readlink -f "$0")")" +node "$DIR/../dist/main.js"; From 8f794076f859225aa55d07babbb29fdfe9ea36fd Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 21:34:16 +0200 Subject: [PATCH 04/41] 0.0.9 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 602ab20..c6f67a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "work-time-calculator", - "version": "0.0.8", + "version": "0.0.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "work-time-calculator", - "version": "0.0.8", + "version": "0.0.9", "license": "MIT", "dependencies": { "chalk": "^5.3.0", diff --git a/package.json b/package.json index 3d7414e..093d734 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "work-time-calculator", - "version": "0.0.8", + "version": "0.0.9", "description": "An interactive CLI tool to calculate work time", "license": "MIT", "repository": { From 1148ba5244291e6aa47046f8c31415c52b929ec0 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 21:48:08 +0200 Subject: [PATCH 05/41] Fix logic error in unlogged calculation --- src/main.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/main.ts b/src/main.ts index 563d872..853a76e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -104,13 +104,7 @@ const main = async () => { loggedAnswer = '00:00'; } const logged = parseDuration(loggedAnswer); - let unLogged: Duration | undefined = undefined; - - if (logged.asMinutes() === worked.asMinutes()) { - unLogged = worked.subtract(logged); - } else { - unLogged = worked.subtract(logged); - } + const unLogged = worked.subtract(logged); // Log result log(); From a929c8aec8ca00a633b1c7d3b0f78fcf14a659a6 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 21:48:30 +0200 Subject: [PATCH 06/41] 0.0.10 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c6f67a9..cdfff40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "work-time-calculator", - "version": "0.0.9", + "version": "0.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "work-time-calculator", - "version": "0.0.9", + "version": "0.0.10", "license": "MIT", "dependencies": { "chalk": "^5.3.0", diff --git a/package.json b/package.json index 093d734..10d1514 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "work-time-calculator", - "version": "0.0.9", + "version": "0.0.10", "description": "An interactive CLI tool to calculate work time", "license": "MIT", "repository": { From 38b1e14b5c5958deeab4638ca7bbbe22d55a5224 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 22:27:08 +0200 Subject: [PATCH 07/41] Fix overtime duration constructor --- src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 853a76e..a2129da 100644 --- a/src/main.ts +++ b/src/main.ts @@ -131,7 +131,7 @@ const main = async () => { log( 'You worked', chalk.green( - formatDuration(dayjs.duration({ minutes: Math.round(workLeftMinutes * -1) })), + formatDuration(dayjs.duration(Math.round(workLeftMinutes * -1), 'minutes')), 'overtime today', ), ); From d141fe1d3646b7bdb9c546cd7793c587b1075115 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Wed, 15 Nov 2023 18:14:39 +0200 Subject: [PATCH 08/41] Add --version and --help --- bin/wtc | 2 +- package-lock.json | 143 ++++++++++++++++++++++++++++++++++++++++++---- package.json | 4 +- src/main.ts | 8 ++- 4 files changed, 141 insertions(+), 16 deletions(-) diff --git a/bin/wtc b/bin/wtc index 7e0a682..da225ff 100755 --- a/bin/wtc +++ b/bin/wtc @@ -1,4 +1,4 @@ #!/bin/sh DIR="$(dirname "$(readlink -f "$0")")" -node "$DIR/../dist/main.js"; +node "$DIR/../dist/main.js" "$@" diff --git a/package-lock.json b/package-lock.json index cdfff40..e5cc049 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,13 +10,15 @@ "license": "MIT", "dependencies": { "chalk": "^5.3.0", - "dayjs": "^1.11.10" + "dayjs": "^1.11.10", + "yargs": "^17.7.2" }, "bin": { "wtc": "bin/wtc" }, "devDependencies": { "@types/node": "^20.9.0", + "@types/yargs": "^17.0.31", "@typescript-eslint/eslint-plugin": "^6.10.0", "eslint-config-prettier": "^9.0.0", "typescript": "^5.2.2" @@ -182,6 +184,21 @@ "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==", "dev": true }, + "node_modules/@types/yargs": { + "version": "17.0.31", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.31.tgz", + "integrity": "sha512-bocYSx4DI8TmdlvxqGpVNXOgCNR1Jj0gNPhhAY+iz1rgKDAaYrAYdFYnhDV1IFuiuVc9HkOwyDcFxaTElF3/wg==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.10.0.tgz", @@ -423,8 +440,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "peer": true, "engines": { "node": ">=8" } @@ -433,8 +448,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -512,12 +525,23 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -528,9 +552,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "peer": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/concat-map": { "version": "0.0.1", @@ -608,6 +630,19 @@ "node": ">=6.0.0" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -948,6 +983,14 @@ "dev": true, "peer": true }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1097,6 +1140,14 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1439,6 +1490,14 @@ } ] }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -1545,12 +1604,23 @@ "node": ">=8" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -1686,6 +1756,22 @@ "node": ">= 8" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -1693,12 +1779,45 @@ "dev": true, "peer": true }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 10d1514..61a1b2c 100644 --- a/package.json +++ b/package.json @@ -28,12 +28,14 @@ "author": "Marko Korhonen ", "devDependencies": { "@types/node": "^20.9.0", + "@types/yargs": "^17.0.31", "@typescript-eslint/eslint-plugin": "^6.10.0", "eslint-config-prettier": "^9.0.0", "typescript": "^5.2.2" }, "dependencies": { "chalk": "^5.3.0", - "dayjs": "^1.11.10" + "dayjs": "^1.11.10", + "yargs": "^17.7.2" } } diff --git a/src/main.ts b/src/main.ts index a2129da..e8a0c74 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,6 @@ import chalk from 'chalk'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; import dayjs, { Dayjs } from 'dayjs'; import * as readline from 'readline/promises'; import { formatDuration, formatTime, formatTimestamp, getHoursRoundedStr } from './format.js'; @@ -12,7 +14,7 @@ const defaultStartTime = '08:00'; const lunchBreakDuration = dayjs.duration({ minutes: 30 }); const defaultWorkDayDuration = dayjs.duration({ hours: 7, minutes: 30 }); -const main = async () => { +const ui = async () => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, @@ -141,4 +143,6 @@ const main = async () => { } }; -main(); +yargs(hideBin(process.argv)).usage('Work time calculator').alias('help', 'h').alias('version', 'v').argv; + +ui(); From ab51b4c11d4a9029dcb0e1ec5954dce0bf420996 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Wed, 22 Nov 2023 20:57:42 +0200 Subject: [PATCH 09/41] Add configuration file support --- README.adoc | 10 +++-- config.toml | 29 ++++++++++++ package-lock.json | 18 ++++++++ package.json | 2 + src/config.ts | 85 ++++++++++++++++++++++++++++++++++++ src/main.ts | 109 +++++++++++++++++++++++++++------------------- src/parse.ts | 2 +- 7 files changed, 204 insertions(+), 51 deletions(-) create mode 100644 config.toml create mode 100644 src/config.ts diff --git a/README.adoc b/README.adoc index 7ee78f9..b4b689a 100644 --- a/README.adoc +++ b/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. There probably exists other tools to do the same task (maybe even better) but I wanted something simple that fits for my -needs specifically. In time, I will probably make it more generic and -configurable. For now, the following assumptions are made. +needs specifically. -* 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 -* [ ] 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 diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..976c159 --- /dev/null +++ b/config.toml @@ -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 diff --git a/package-lock.json b/package-lock.json index e5cc049..04db5dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,10 @@ "version": "0.0.10", "license": "MIT", "dependencies": { + "@iarna/toml": "^2.2.5", "chalk": "^5.3.0", "dayjs": "^1.11.10", + "xdg-basedir": "^5.1.0", "yargs": "^17.7.2" }, "bin": { @@ -128,6 +130,11 @@ "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/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1779,6 +1786,17 @@ "dev": 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": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 61a1b2c..20204b9 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,10 @@ "typescript": "^5.2.2" }, "dependencies": { + "@iarna/toml": "^2.2.5", "chalk": "^5.3.0", "dayjs": "^1.11.10", + "xdg-basedir": "^5.1.0", "yargs": "^17.7.2" } } diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..224dad6 --- /dev/null +++ b/src/config.ts @@ -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 { + 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; diff --git a/src/main.ts b/src/main.ts index e8a0c74..83c92e2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,83 +6,97 @@ import * as readline from 'readline/promises'; import { formatDuration, formatTime, formatTimestamp, getHoursRoundedStr } from './format.js'; import duration, { Duration } from 'dayjs/plugin/duration.js'; import { parseDuration, parseTimestamp } from './parse.js'; +import getConfig from './config.js'; dayjs.extend(duration); 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 { defaults, askInput } = getConfig(); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); let startedAt: Dayjs | undefined = undefined; - const now = dayjs(); + let stoppedAt: Dayjs | undefined = undefined; + let stoppedWorking = false; try { // Get work day duration let workDayDuration: Duration | undefined = undefined; - const durationAnswer = await rl.question( - `How long is your work day today, excluding the lunch break? [${formatDuration( - defaultWorkDayDuration, - true, - )}] `, - ); - if (durationAnswer !== '') { - workDayDuration = parseDuration(durationAnswer); - if (workDayDuration.asMinutes() <= 0) { - error( - chalk.red( - `Failed to parse ${durationAnswer} to duration, using default work day duration ${defaultWorkDayDuration}`, - ), - ); - workDayDuration = undefined; + if (askInput.workDayLength) { + const durationAnswer = await rl.question( + `How long is your work day today, excluding the lunch break? [${formatDuration( + defaults.workDayDuration, + true, + )}] `, + ); + if (durationAnswer !== '') { + workDayDuration = parseDuration(durationAnswer); + if (workDayDuration.asMinutes() <= 0) { + error( + chalk.red( + `Failed to parse ${durationAnswer} to duration, using default work day duration ${formatDuration( + defaults.workDayDuration, + true, + )}`, + ), + ); + workDayDuration = undefined; + } } } if (!workDayDuration) { - workDayDuration = defaultWorkDayDuration; + workDayDuration = defaults.workDayDuration; } - // Calculate worked time - const startTimeAnswer = await rl.question(`What time did you start work today? [${defaultStartTime}] `); - if (startTimeAnswer !== '') { - startedAt = parseTimestamp(startTimeAnswer); - if (!startedAt.isValid()) { - error( - chalk.red( - `Failed to parse ${startTimeAnswer} to time, using default start time ${defaultStartTime}`, - ), - ); + if (askInput.startTime) { + const startTimeAnswer = await rl.question( + `What time did you start work today? [${formatTime(defaults.startTime)}] `, + ); + if (startTimeAnswer !== '') { + startedAt = parseTimestamp(startTimeAnswer); + if (!startedAt.isValid()) { + error( + chalk.red( + `Failed to parse ${startTimeAnswer} to time, using default start time ${formatTime( + defaults.startTime, + )}`, + ), + ); + } } } if (!startedAt?.isValid()) { - startedAt = parseTimestamp(defaultStartTime); + startedAt = defaults.startTime; } - let stoppedWorking = false; - let stoppedAt: Dayjs | undefined = undefined; - const stoppedAnswer = await rl.question( - `What time did you stop working (default is current time if you didn't stop yet)? [${formatTime(now)}] `, - ); + if (askInput.stopTime) { + const stoppedAnswer = await rl.question( + `What time did you stop working (default is current time if you didn't stop yet)? [${formatTime( + defaults.stopTime, + )}] `, + ); - if (stoppedAnswer === '') { - stoppedAt = now; - } else { - stoppedWorking = true; - stoppedAt = parseTimestamp(stoppedAnswer); - if (!stoppedAt.isValid()) { - error(`Failed to parse ${stoppedAnswer} to time, using current time`); - stoppedAt = dayjs(); + if (stoppedAnswer !== '') { + stoppedWorking = true; + stoppedAt = parseTimestamp(stoppedAnswer); + if (!stoppedAt.isValid()) { + error(`Failed to parse ${stoppedAnswer} to time, using current time`); + stoppedAt = dayjs(); + } } } + if (!stoppedAt) { + stoppedAt = defaults.stopTime; + } + if (stoppedAt.isSame(startedAt) || stoppedAt.isBefore(startedAt)) { error( chalk.red( @@ -96,8 +110,11 @@ const ui = async () => { let worked = dayjs.duration(stoppedAt.diff(startedAt)); - if ((await rl.question('Did you have a lunch break? [Y/n] ')).toLowerCase() !== 'n') { - worked = worked.subtract(lunchBreakDuration); + const hadLunch = + 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 diff --git a/src/parse.ts b/src/parse.ts index a3cab22..4abe2fd 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -5,7 +5,7 @@ import duration, { Duration } from 'dayjs/plugin/duration.js'; dayjs.extend(customParseFormat); 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 => { const [hours, minutes] = time.split(':').map(Number); From 312ac4e81983bccfe4eed35a60ce08520116c163 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Wed, 22 Nov 2023 21:06:10 +0200 Subject: [PATCH 10/41] Improve stop time print --- src/main.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main.ts b/src/main.ts index 83c92e2..2181c0b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -78,9 +78,7 @@ const ui = async () => { if (askInput.stopTime) { const stoppedAnswer = await rl.question( - `What time did you stop working (default is current time if you didn't stop yet)? [${formatTime( - defaults.stopTime, - )}] `, + `What time did you stop working? [${formatTime(defaults.stopTime)}] `, ); if (stoppedAnswer !== '') { From df334bc297f3fd52856a450584d8b6f5294717a6 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Wed, 22 Nov 2023 21:32:31 +0200 Subject: [PATCH 11/41] Bundle application with rollup --- bin/wtc | 2 +- package-lock.json | 510 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 6 +- rollup.config.js | 14 ++ 4 files changed, 529 insertions(+), 3 deletions(-) create mode 100644 rollup.config.js diff --git a/bin/wtc b/bin/wtc index da225ff..bf01370 100755 --- a/bin/wtc +++ b/bin/wtc @@ -1,4 +1,4 @@ #!/bin/sh DIR="$(dirname "$(readlink -f "$0")")" -node "$DIR/../dist/main.js" "$@" +node "$DIR/../dist/wtc.js" "$@" diff --git a/package-lock.json b/package-lock.json index 04db5dc..7a63fe2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,10 +19,14 @@ "wtc": "bin/wtc" }, "devDependencies": { + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^11.1.5", "@types/node": "^20.9.0", "@types/yargs": "^17.0.31", "@typescript-eslint/eslint-plugin": "^6.10.0", "eslint-config-prettier": "^9.0.0", + "rollup": "^4.5.1", + "tslib": "^2.6.2", "typescript": "^5.2.2" } }, @@ -135,6 +139,64 @@ "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", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -170,6 +232,238 @@ "node": ">= 8" } }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-typescript": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.5.tgz", + "integrity": "sha512-rnMHrGBB0IUEv69Q8/JGRD/n4/n6b3nfpufUu26axhUcboUzv/twfZU8fIBbTOphRAe0v8EyxzeDpKXqGHfyDA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0||^3.0.0||^4.0.0", + "tslib": "*", + "typescript": ">=3.7.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "tslib": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", + "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.5.1.tgz", + "integrity": "sha512-YaN43wTyEBaMqLDYeze+gQ4ZrW5RbTEGtT5o1GVDkhpdNcsLTnLRcLccvwy3E9wiDKWg9RIhuoy3JQKDRBfaZA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.5.1.tgz", + "integrity": "sha512-n1bX+LCGlQVuPlCofO0zOKe1b2XkFozAVRoczT+yxWZPGnkEAKTTYVOGZz8N4sKuBnKMxDbfhUsB1uwYdup/sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.5.1.tgz", + "integrity": "sha512-QqJBumdvfBqBBmyGHlKxje+iowZwrHna7pokj/Go3dV1PJekSKfmjKrjKQ/e6ESTGhkfPNLq3VXdYLAc+UtAQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.5.1.tgz", + "integrity": "sha512-RrkDNkR/P5AEQSPkxQPmd2ri8WTjSl0RYmuFOiEABkEY/FSg0a4riihWQGKDJ4LnV9gigWZlTMx2DtFGzUrYQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.5.1.tgz", + "integrity": "sha512-ZFPxvUZmE+fkB/8D9y/SWl/XaDzNSaxd1TJUSE27XAKlRpQ2VNce/86bGd9mEUgL3qrvjJ9XTGwoX0BrJkYK/A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.5.1.tgz", + "integrity": "sha512-FEuAjzVIld5WVhu+M2OewLmjmbXWd3q7Zcx+Rwy4QObQCqfblriDMMS7p7+pwgjZoo9BLkP3wa9uglQXzsB9ww==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.5.1.tgz", + "integrity": "sha512-f5Gs8WQixqGRtI0Iq/cMqvFYmgFzMinuJO24KRfnv7Ohi/HQclwrBCYkzQu1XfLEEt3DZyvveq9HWo4bLJf1Lw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.5.1.tgz", + "integrity": "sha512-CWPkPGrFfN2vj3mw+S7A/4ZaU3rTV7AkXUr08W9lNP+UzOvKLVf34tWCqrKrfwQ0NTk5GFqUr2XGpeR2p6R4gw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.5.1.tgz", + "integrity": "sha512-ZRETMFA0uVukUC9u31Ed1nx++29073goCxZtmZARwk5aF/ltuENaeTtRVsSQzFlzdd4J6L3qUm+EW8cbGt0CKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.5.1.tgz", + "integrity": "sha512-ihqfNJNb2XtoZMSCPeoo0cYMgU04ksyFIoOw5S0JUVbOhafLot+KD82vpKXOurE2+9o/awrqIxku9MRR9hozHQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.5.1.tgz", + "integrity": "sha512-zK9MRpC8946lQ9ypFn4gLpdwr5a01aQ/odiIJeL9EbgZDMgbZjjT/XzTqJvDfTmnE1kHdbG20sAeNlpc91/wbg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.5.1.tgz", + "integrity": "sha512-5I3Nz4Sb9TYOtkRwlH0ow+BhMH2vnh38tZ4J4mggE48M/YyJyp/0sPSxhw1UeS1+oBgQ8q7maFtSeKpeRJu41Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -408,7 +702,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -511,6 +804,12 @@ "node": ">=8" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -561,6 +860,12 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -851,6 +1156,12 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -990,6 +1301,29 @@ "dev": true, "peer": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1084,6 +1418,18 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -1138,6 +1484,18 @@ "dev": true, "peer": true }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1436,6 +1794,12 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -1497,6 +1861,15 @@ } ] }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -1505,6 +1878,23 @@ "node": ">=0.10.0" } }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -1541,6 +1931,34 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rollup": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.5.1.tgz", + "integrity": "sha512-0EQribZoPKpb5z1NW/QYm3XSR//Xr8BeEXU49Lc/mQmpmVVG5jPUVrpc2iptup/0WMrY9mzas0fxH+TjYvG2CA==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.5.1", + "@rollup/rollup-android-arm64": "4.5.1", + "@rollup/rollup-darwin-arm64": "4.5.1", + "@rollup/rollup-darwin-x64": "4.5.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.5.1", + "@rollup/rollup-linux-arm64-gnu": "4.5.1", + "@rollup/rollup-linux-arm64-musl": "4.5.1", + "@rollup/rollup-linux-x64-gnu": "4.5.1", + "@rollup/rollup-linux-x64-musl": "4.5.1", + "@rollup/rollup-win32-arm64-msvc": "4.5.1", + "@rollup/rollup-win32-ia32-msvc": "4.5.1", + "@rollup/rollup-win32-x64-msvc": "4.5.1", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -1564,6 +1982,26 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -1579,6 +2017,15 @@ "node": ">=10" } }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -1611,6 +2058,31 @@ "node": ">=8" } }, + "node_modules/smob": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz", + "integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -1661,6 +2133,36 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/terser": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", + "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -1692,6 +2194,12 @@ "typescript": ">=4.2.0" } }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 20204b9..e645206 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "wtc": "bin/wtc" }, "scripts": { - "build": "tsc" + "build": "rollup -c" }, "keywords": [ "work", @@ -27,10 +27,14 @@ ], "author": "Marko Korhonen ", "devDependencies": { + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^11.1.5", "@types/node": "^20.9.0", "@types/yargs": "^17.0.31", "@typescript-eslint/eslint-plugin": "^6.10.0", "eslint-config-prettier": "^9.0.0", + "rollup": "^4.5.1", + "tslib": "^2.6.2", "typescript": "^5.2.2" }, "dependencies": { diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..3d31556 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,14 @@ +import typescript from '@rollup/plugin-typescript'; +import terser from '@rollup/plugin-terser'; + +/** @type {import('rollup').RollupOptions} */ +const config = { + input: 'src/main.ts', + output: { + format: 'esm', + file: 'dist/wtc.js', + }, + plugins: [typescript(), terser()], +}; + +export default config; From 28fe8772b19bdabfddd82b72db368ecb6a8115a1 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Wed, 22 Nov 2023 21:33:48 +0200 Subject: [PATCH 12/41] Remove debug log --- src/config.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/config.ts b/src/config.ts index 224dad6..d7192a3 100644 --- a/src/config.ts +++ b/src/config.ts @@ -6,8 +6,6 @@ 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; @@ -57,7 +55,6 @@ const getConfig = (): Config => { 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; } From 32468159972e39aaa2355776acc9383d346dc55e Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Wed, 22 Nov 2023 21:36:26 +0200 Subject: [PATCH 13/41] Remove unneeded dependencies --- package-lock.json | 4 ++-- package.json | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7a63fe2..8ff0ae9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,8 +26,7 @@ "@typescript-eslint/eslint-plugin": "^6.10.0", "eslint-config-prettier": "^9.0.0", "rollup": "^4.5.1", - "tslib": "^2.6.2", - "typescript": "^5.2.2" + "tslib": "^2.6.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2231,6 +2230,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index e645206..53bce44 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,7 @@ "@typescript-eslint/eslint-plugin": "^6.10.0", "eslint-config-prettier": "^9.0.0", "rollup": "^4.5.1", - "tslib": "^2.6.2", - "typescript": "^5.2.2" + "tslib": "^2.6.2" }, "dependencies": { "@iarna/toml": "^2.2.5", From b88e19e3119802d65d9239749a04bbebd302f81b Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Wed, 22 Nov 2023 22:13:00 +0200 Subject: [PATCH 14/41] Split code more into modules --- src/input.ts | 148 ++++++++++++++++++++++++++++++++ src/main.ts | 160 +---------------------------------- src/output.ts | 32 +++++++ src/types/WtcPromptResult.ts | 14 +++ src/ui.ts | 9 ++ 5 files changed, 206 insertions(+), 157 deletions(-) create mode 100644 src/input.ts create mode 100644 src/output.ts create mode 100644 src/types/WtcPromptResult.ts create mode 100644 src/ui.ts diff --git a/src/input.ts b/src/input.ts new file mode 100644 index 0000000..4b250f1 --- /dev/null +++ b/src/input.ts @@ -0,0 +1,148 @@ +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 { WtcPromptResult } from './types/WtcPromptResult'; +import duration from 'dayjs/plugin/duration.js'; + +dayjs.extend(duration); + +const { error } = console; + +const input = async (): Promise => { + const { defaults, askInput } = getConfig(); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + let startedAt: Dayjs | undefined = undefined; + let stoppedAt: Dayjs | undefined = undefined; + let stoppedWorking = false; + + try { + // Get work day duration + let workDayDuration: Duration | undefined = undefined; + + if (askInput.workDayLength) { + const durationAnswer = await rl.question( + `How long is your work day today, excluding the lunch break? [${formatDuration( + defaults.workDayDuration, + true, + )}] `, + ); + if (durationAnswer !== '') { + workDayDuration = parseDuration(durationAnswer); + if (workDayDuration.asMinutes() <= 0) { + error( + chalk.red( + `Failed to parse ${durationAnswer} to duration, using default work day duration ${formatDuration( + defaults.workDayDuration, + true, + )}`, + ), + ); + workDayDuration = undefined; + } + } + } + + if (!workDayDuration) { + workDayDuration = defaults.workDayDuration; + } + + if (askInput.startTime) { + const startTimeAnswer = await rl.question( + `What time did you start work today? [${formatTime(defaults.startTime)}] `, + ); + if (startTimeAnswer !== '') { + startedAt = parseTimestamp(startTimeAnswer); + if (!startedAt.isValid()) { + error( + chalk.red( + `Failed to parse ${startTimeAnswer} to time, using default start time ${formatTime( + defaults.startTime, + )}`, + ), + ); + } + } + } + + if (!startedAt?.isValid()) { + startedAt = defaults.startTime; + } + + if (askInput.stopTime) { + const stoppedAnswer = await rl.question( + `What time did you stop working? [${formatTime(defaults.stopTime)}] `, + ); + + if (stoppedAnswer !== '') { + stoppedWorking = true; + stoppedAt = parseTimestamp(stoppedAnswer); + if (!stoppedAt.isValid()) { + error(`Failed to parse ${stoppedAnswer} to time, using current time`); + stoppedAt = dayjs(); + } + } + } + + if (!stoppedAt) { + stoppedAt = defaults.stopTime; + } + + if (stoppedAt.isSame(startedAt) || stoppedAt.isBefore(startedAt)) { + error( + chalk.red( + `Start time (${formatTime(startedAt)}) needs to be before stop time (${formatTime( + stoppedAt, + )}). Exiting`, + ), + ); + process.exit(1); + } + + let worked = dayjs.duration(stoppedAt.diff(startedAt)); + + const hadLunch = + 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 + let loggedAnswer = await rl.question('How many hours did you log already? [00:00] '); + if (loggedAnswer === '') { + loggedAnswer = '00:00'; + } + const logged = parseDuration(loggedAnswer); + const unLogged = worked.subtract(logged); + const workLeft = workDayDuration.subtract(worked); + let workLeftMinutes = workLeft.asMinutes(); + let workedOverTime: Duration | undefined; + + if (workLeftMinutes < 0) { + workedOverTime = dayjs.duration(Math.round(workLeftMinutes * -1), 'minutes'); + } + + return { + logged, + unLogged, + startedAt, + stoppedAt, + stoppedWorking, + hadLunch, + worked, + workLeft, + }; + } finally { + rl.close(); + } +}; + +export default input; diff --git a/src/main.ts b/src/main.ts index 2181c0b..f244ada 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,163 +1,9 @@ -import chalk from 'chalk'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; -import dayjs, { Dayjs } from 'dayjs'; -import * as readline from 'readline/promises'; -import { formatDuration, formatTime, formatTimestamp, getHoursRoundedStr } from './format.js'; -import duration, { Duration } from 'dayjs/plugin/duration.js'; -import { parseDuration, parseTimestamp } from './parse.js'; -import getConfig from './config.js'; - -dayjs.extend(duration); - -const { log, error } = console; - -const ui = async () => { - const { defaults, askInput } = getConfig(); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - let startedAt: Dayjs | undefined = undefined; - let stoppedAt: Dayjs | undefined = undefined; - let stoppedWorking = false; - - try { - // Get work day duration - let workDayDuration: Duration | undefined = undefined; - - if (askInput.workDayLength) { - const durationAnswer = await rl.question( - `How long is your work day today, excluding the lunch break? [${formatDuration( - defaults.workDayDuration, - true, - )}] `, - ); - if (durationAnswer !== '') { - workDayDuration = parseDuration(durationAnswer); - if (workDayDuration.asMinutes() <= 0) { - error( - chalk.red( - `Failed to parse ${durationAnswer} to duration, using default work day duration ${formatDuration( - defaults.workDayDuration, - true, - )}`, - ), - ); - workDayDuration = undefined; - } - } - } - - if (!workDayDuration) { - workDayDuration = defaults.workDayDuration; - } - - if (askInput.startTime) { - const startTimeAnswer = await rl.question( - `What time did you start work today? [${formatTime(defaults.startTime)}] `, - ); - if (startTimeAnswer !== '') { - startedAt = parseTimestamp(startTimeAnswer); - if (!startedAt.isValid()) { - error( - chalk.red( - `Failed to parse ${startTimeAnswer} to time, using default start time ${formatTime( - defaults.startTime, - )}`, - ), - ); - } - } - } - - if (!startedAt?.isValid()) { - startedAt = defaults.startTime; - } - - if (askInput.stopTime) { - const stoppedAnswer = await rl.question( - `What time did you stop working? [${formatTime(defaults.stopTime)}] `, - ); - - if (stoppedAnswer !== '') { - stoppedWorking = true; - stoppedAt = parseTimestamp(stoppedAnswer); - if (!stoppedAt.isValid()) { - error(`Failed to parse ${stoppedAnswer} to time, using current time`); - stoppedAt = dayjs(); - } - } - } - - if (!stoppedAt) { - stoppedAt = defaults.stopTime; - } - - if (stoppedAt.isSame(startedAt) || stoppedAt.isBefore(startedAt)) { - error( - chalk.red( - `Start time (${formatTime(startedAt)}) needs to be before stop time (${formatTime( - stoppedAt, - )}). Exiting`, - ), - ); - process.exit(1); - } - - let worked = dayjs.duration(stoppedAt.diff(startedAt)); - - const hadLunch = - 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 - let loggedAnswer = await rl.question('How many hours did you log already? [00:00] '); - if (loggedAnswer === '') { - loggedAnswer = '00:00'; - } - const logged = parseDuration(loggedAnswer); - const unLogged = worked.subtract(logged); - - // Log result - log(); - log('Started working at:', formatTimestamp(startedAt)); - log((stoppedWorking ? 'Stopped working' : 'Hours calculated') + ' at:', formatTimestamp(stoppedAt)); - log('Worked today:', chalk.green(formatDuration(worked)), chalk.yellow(getHoursRoundedStr(worked))); - - if (unLogged.asMinutes() == 0) { - log('Unlogged today:', chalk.green('none')); - } else if (unLogged.asMinutes() > 0) { - log('Unlogged today:', chalk.red(formatDuration(unLogged)), chalk.yellow(getHoursRoundedStr(unLogged))); - } else if (unLogged.asMinutes() < 0) { - log( - chalk.red(`You have logged ${formatDuration(unLogged)} more than you worked today!`), - chalk.yellow(getHoursRoundedStr(unLogged)), - ); - } - - const workLeft = workDayDuration.subtract(worked); - const workLeftMinutes = workLeft.asMinutes(); - if (workLeftMinutes > 0) { - log('You still have to work', chalk.green(formatDuration(workLeft)), 'more today'); - } else if (workLeft.asMinutes() < 0) { - log( - 'You worked', - chalk.green( - formatDuration(dayjs.duration(Math.round(workLeftMinutes * -1), 'minutes')), - 'overtime today', - ), - ); - } - } finally { - rl.close(); - } -}; +import ui from './ui.js'; +// 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; +// Run UI if help or version is not prompted ui(); diff --git a/src/output.ts b/src/output.ts new file mode 100644 index 0000000..a45fbdc --- /dev/null +++ b/src/output.ts @@ -0,0 +1,32 @@ +import chalk from 'chalk'; +import { formatDuration, formatTimestamp, getHoursRoundedStr } from './format'; +import { WtcPromptResult } from './types/WtcPromptResult'; + +const { log } = console; + +const output = (result: WtcPromptResult) => { + const { startedAt, stoppedAt, stoppedWorking, worked, unLogged, workLeft, workedOverTime } = result; + log(); + log('Started working at:', formatTimestamp(startedAt)); + log((stoppedWorking ? 'Stopped working' : 'Hours calculated') + ' at:', formatTimestamp(stoppedAt)); + log('Worked today:', chalk.green(formatDuration(worked)), chalk.yellow(getHoursRoundedStr(worked))); + + if (unLogged.asMinutes() == 0) { + log('Unlogged today:', chalk.green('none')); + } else if (unLogged.asMinutes() > 0) { + log('Unlogged today:', chalk.red(formatDuration(unLogged)), chalk.yellow(getHoursRoundedStr(unLogged))); + } else if (unLogged.asMinutes() < 0) { + log( + chalk.red(`You have logged ${formatDuration(unLogged)} more than you worked today!`), + chalk.yellow(getHoursRoundedStr(unLogged)), + ); + } + + if (workLeft.asMinutes() > 0) { + log('You still have to work', chalk.green(formatDuration(workLeft)), 'more today'); + } else if (workedOverTime) { + log('You worked', chalk.green(formatDuration(workedOverTime), 'overtime today')); + } +}; + +export default output; diff --git a/src/types/WtcPromptResult.ts b/src/types/WtcPromptResult.ts new file mode 100644 index 0000000..ed3ee0f --- /dev/null +++ b/src/types/WtcPromptResult.ts @@ -0,0 +1,14 @@ +import { Dayjs } from 'dayjs'; +import { Duration } from 'dayjs/plugin/duration'; + +export interface WtcPromptResult { + startedAt: Dayjs; + stoppedAt: Dayjs; + stoppedWorking: boolean; + logged: Duration; + unLogged: Duration; + hadLunch: boolean; + worked: Duration; + workLeft: Duration; + workedOverTime?: Duration; +} diff --git a/src/ui.ts b/src/ui.ts new file mode 100644 index 0000000..1cc4bc5 --- /dev/null +++ b/src/ui.ts @@ -0,0 +1,9 @@ +import input from './input.js'; +import output from './output.js'; + +const ui = async () => { + const result = await input(); + output(result); +}; + +export default ui; From 4b8c8be226bdfee3b98b5eb9335d0b6297436401 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 17:24:08 +0200 Subject: [PATCH 15/41] Add i18n support and translations for finnish language --- config.toml | 10 ++++ src/config.ts | 28 +++------- src/format.ts | 46 ++++++++++------ src/i18n.ts | 120 +++++++++++++++++++++++++++++++++++++++++ src/input.ts | 52 +++++++----------- src/output.ts | 35 +++++++----- src/types/Language.ts | 6 +++ src/types/WtcConfig.ts | 20 +++++++ src/ui.ts | 6 ++- 9 files changed, 239 insertions(+), 84 deletions(-) create mode 100644 src/i18n.ts create mode 100644 src/types/Language.ts create mode 100644 src/types/WtcConfig.ts diff --git a/config.toml b/config.toml index 976c159..895729b 100644 --- a/config.toml +++ b/config.toml @@ -5,15 +5,24 @@ # You can place your configuration file in $XDG_CONFIG_HOME/wtc/config.toml, # usually ~/.config/wtc/config.toml +# The language of the application. +# Currently supported languages are "en", "fi" +language = "en" + # 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" @@ -25,5 +34,6 @@ workDayDuration = true startTime = true stopTime = true logged = true + # It is assumed that you didn't have lunch if this is false hadLunch = true diff --git a/src/config.ts b/src/config.ts index d7192a3..8049b22 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,27 +2,11 @@ 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'; +import WtcConfig from './types/WtcConfig.js'; +import Language from './types/Language.js'; -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 { +interface RawConfig extends Omit { defaults: { workDayDuration: string; lunchBreakDuration: string; @@ -32,6 +16,7 @@ interface RawConfig extends Omit { } const defaultConfig: RawConfig = { + language: Language.en, defaults: { workDayDuration: '07:30', lunchBreakDuration: '00:30', @@ -47,9 +32,9 @@ const defaultConfig: RawConfig = { }, }; -const getConfig = (): Config => { +const getConfig = (): WtcConfig => { const configDir = xdgConfig || path.join(process.env.HOME ?? './', '.config'); - let configFilePath = path.join(configDir, 'wct', 'config.toml'); + let configFilePath = path.join(configDir, 'wtc', 'config.toml'); let configData: RawConfig; if (fs.existsSync(configFilePath)) { @@ -59,6 +44,7 @@ const getConfig = (): Config => { } return { + language: configData.language ?? defaultConfig.language, defaults: { workDayDuration: parseDuration( configData.defaults.workDayDuration ?? defaultConfig.defaults.workDayDuration, diff --git a/src/format.ts b/src/format.ts index 3be9bb4..c831e24 100644 --- a/src/format.ts +++ b/src/format.ts @@ -1,28 +1,44 @@ 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 = (duration: Duration, short?: boolean): string => { - if (duration.hours() === 0 && duration.minutes() === 0) { - return 'none'; - } +export const formatDuration = + (language: Language) => + (duration: Duration, short?: boolean): string => { + if (duration.hours() === 0 && duration.minutes() === 0) { + return 'none'; + } - const formatString = short - ? 'HH:mm' - : duration.hours() > 0 && duration.minutes() > 0 - ? `H [hour${duration.hours() > 1 ? 's' : ''} and] m [minute${duration.minutes() > 1 ? 's' : ''}]` - : duration.hours() > 0 - ? `H [hour${duration.hours() > 1 ? 's' : ''}]` - : `m [minute${duration.minutes() > 1 ? 's' : ''}]`; + let formatString; - return duration.format(formatString); -}; + if (short) { + formatString = 'HH:mm'; + } 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' : ''}]` + : duration.hours() > 0 + ? `H [tunti${duration.hours() > 1 ? 'a' : ''}]` + : `m [minutti${duration.minutes() > 1 ? 'a' : ''}]`; + } else { + formatString = + duration.hours() > 0 && duration.minutes() > 0 + ? `H [hour${duration.hours() > 1 ? 's' : ''} and] m [minute${duration.minutes() > 1 ? 's' : ''}]` + : duration.hours() > 0 + ? `H [hour${duration.hours() > 1 ? 's' : ''}]` + : `m [minute${duration.minutes() > 1 ? 's' : ''}]`; + } -export const getHoursRoundedStr = (duration: Duration) => - `(${getHoursRounded(duration)} as hours rounded to next even 15 minutes)`; + return duration.format(formatString); + }; + +export const getHoursRoundedStr = (language: Language) => (duration: Duration) => + `(${getHoursRounded(duration)} ${message(language)(MessageKey.hoursRounded)})`; const getHoursRounded = (duration: Duration) => { // Round up to the next multiple of 15 diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..833a729 --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,120 @@ +import Language from './types/Language'; + +export enum MessageKey { + promptWorkDayDuration, + promptStartTime, + promptStopTime, + parseTimeFailed, + startTimeBeforeStopTimeError, + promptLunchBreak, + promptLogged, + none, + startedWorking, + stoppedWorking, + workedToday, + loggedOver, + workedOvertime, + workLeft, + hoursCalculated, + klo, + unloggedToday, + hoursRounded, +} + +const messages: Record> = { + [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}]: ', + }, + [MessageKey.promptStartTime]: { + [Language.en]: 'What time did you start work today? [{0}]: ', + [Language.fi]: 'Mihin aikaan aloitit työskentelyn tänään? [{0}]: ', + }, + [MessageKey.promptStopTime]: { + [Language.en]: "What time did you stop working? If you didn't stop yet, leave this empty: ", + [Language.fi]: 'Mihin aikaan lopetit työskentelyn? Jos et lopettanut vielä, jätä tämä tyhjäksi: ', + }, + [MessageKey.parseTimeFailed]: { + [Language.en]: 'Failed to parse time "{0}", using default value "{1}"', + [Language.fi]: 'Ajan "{0}" parsiminen epäonnistui, käytetään oletusasetusta "{1}"', + }, + [MessageKey.startTimeBeforeStopTimeError]: { + [Language.en]: 'Start time ({0}) needs to be before stop time ({1}). Exiting', + [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]: ', + }, + [MessageKey.promptLogged]: { + [Language.en]: 'How many hours did you log already? [00:00] ', + [Language.fi]: 'Kuinka monta tuntia kirjasit jo? [00:00] ', + }, + [MessageKey.none]: { + [Language.en]: 'None', + [Language.fi]: 'Ei yhtään', + }, + [MessageKey.startedWorking]: { + [Language.en]: 'Started working:', + [Language.fi]: 'Aloitit työskentelyn:', + }, + [MessageKey.stoppedWorking]: { + [Language.en]: 'Stopped working', + [Language.fi]: 'Lopetit työskentelyn', + }, + [MessageKey.hoursCalculated]: { + [Language.en]: 'Hours calculated', + [Language.fi]: 'Tunnit laskettu', + }, + [MessageKey.workedToday]: { + [Language.en]: 'Worked today:', + [Language.fi]: 'Tänään työskennelty:', + }, + [MessageKey.workedOvertime]: { + [Language.en]: 'You worked {0} overtime today', + [Language.fi]: 'Olet tehnyt {0} ylitöitä tänään', + }, + [MessageKey.loggedOver]: { + [Language.en]: 'You have logged {0} more than you worked today!', + [Language.fi]: 'Olet kirjannut {0} enemmän kuin olet työskennellyt!', + }, + [MessageKey.workLeft]: { + [Language.en]: 'You still have to work {0} more today', + [Language.fi]: 'Sinun pitää työskennellä tänään vielä {0} lisää', + }, + [MessageKey.klo]: { + [Language.en]: 'at', + [Language.fi]: 'klo', + }, + [MessageKey.unloggedToday]: { + [Language.en]: 'Unlogged today:', + [Language.fi]: 'Kirjaamattomia tänään:', + }, + [MessageKey.hoursRounded]: { + [Language.en]: 'as hours rounded to next even 15 minutes', + [Language.fi]: 'tunteina pyöristettynä seuraavaan 15 minuuttiin', + }, +}; + +/** + * Get a function to fetch messages for a given language + * @param language The language to get the messages for + */ +export const message = + (language: Language) => + /** + * Get a message for a fiven key + * @param key The key of the message + */ + (key: keyof typeof messages, ...params: string[]) => { + let result = messages[key][language]; + if (!result) { + throw `Unknown language: ${language}`; + } + + // Replace parameters in the template + for (let i = 0; i < params.length; i++) { + result = result.replace(new RegExp(`\\{${i}\\}`, 'g'), params[i]); + } + return result; + }; diff --git a/src/input.ts b/src/input.ts index 4b250f1..e49e582 100644 --- a/src/input.ts +++ b/src/input.ts @@ -7,13 +7,17 @@ import { formatDuration, formatTime } from './format'; import dayjs, { 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); const { error } = console; -const input = async (): Promise => { - const { defaults, askInput } = getConfig(); +const input = async (config: WtcConfig): Promise => { + const msg = message(config.language); + const fmtDuration = formatDuration(config.language); + const { defaults, askInput } = config; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, @@ -29,20 +33,14 @@ const input = async (): Promise => { if (askInput.workDayLength) { const durationAnswer = await rl.question( - `How long is your work day today, excluding the lunch break? [${formatDuration( - defaults.workDayDuration, - true, - )}] `, + msg(MessageKey.promptWorkDayDuration, fmtDuration(defaults.workDayDuration, true)), ); if (durationAnswer !== '') { workDayDuration = parseDuration(durationAnswer); if (workDayDuration.asMinutes() <= 0) { error( chalk.red( - `Failed to parse ${durationAnswer} to duration, using default work day duration ${formatDuration( - defaults.workDayDuration, - true, - )}`, + msg(MessageKey.parseTimeFailed, durationAnswer, fmtDuration(defaults.workDayDuration)), ), ); workDayDuration = undefined; @@ -55,19 +53,11 @@ const input = async (): Promise => { } if (askInput.startTime) { - const startTimeAnswer = await rl.question( - `What time did you start work today? [${formatTime(defaults.startTime)}] `, - ); + const startTimeAnswer = await rl.question(msg(MessageKey.promptStartTime, formatTime(defaults.startTime))); if (startTimeAnswer !== '') { startedAt = parseTimestamp(startTimeAnswer); if (!startedAt.isValid()) { - error( - chalk.red( - `Failed to parse ${startTimeAnswer} to time, using default start time ${formatTime( - defaults.startTime, - )}`, - ), - ); + error(chalk.red(msg(MessageKey.parseTimeFailed, startTimeAnswer, formatTime(defaults.startTime)))); } } } @@ -77,16 +67,13 @@ const input = async (): Promise => { } if (askInput.stopTime) { - const stoppedAnswer = await rl.question( - `What time did you stop working? [${formatTime(defaults.stopTime)}] `, - ); + const stoppedAnswer = await rl.question(msg(MessageKey.promptStopTime, formatTime(defaults.stopTime))); if (stoppedAnswer !== '') { stoppedWorking = true; stoppedAt = parseTimestamp(stoppedAnswer); if (!stoppedAt.isValid()) { - error(`Failed to parse ${stoppedAnswer} to time, using current time`); - stoppedAt = dayjs(); + error(chalk.red(msg(MessageKey.parseTimeFailed, stoppedAnswer, formatTime(defaults.stopTime)))); } } } @@ -97,26 +84,25 @@ const input = async (): Promise => { if (stoppedAt.isSame(startedAt) || stoppedAt.isBefore(startedAt)) { error( - chalk.red( - `Start time (${formatTime(startedAt)}) needs to be before stop time (${formatTime( - stoppedAt, - )}). Exiting`, - ), + chalk.red(msg(MessageKey.startTimeBeforeStopTimeError, formatTime(startedAt), formatTime(stoppedAt))), ); process.exit(1); } let worked = dayjs.duration(stoppedAt.diff(startedAt)); - const hadLunch = - askInput.hadLunch && (await rl.question('Did you have a lunch break? [Y/n] ')).toLowerCase() !== 'n'; + let hadLunch = false; + if (askInput.hadLunch) { + const lunchAnswer = (await rl.question(msg(MessageKey.promptLunchBreak))).toLowerCase(); + hadLunch = lunchAnswer === 'y' || lunchAnswer === 'k'; + } if (hadLunch) { worked = worked.subtract(defaults.lunchBreakDuration); } // Calculate unlogged time - let loggedAnswer = await rl.question('How many hours did you log already? [00:00] '); + let loggedAnswer = await rl.question(msg(MessageKey.promptLogged)); if (loggedAnswer === '') { loggedAnswer = '00:00'; } diff --git a/src/output.ts b/src/output.ts index a45fbdc..5e98780 100644 --- a/src/output.ts +++ b/src/output.ts @@ -1,31 +1,40 @@ import chalk from 'chalk'; import { formatDuration, formatTimestamp, getHoursRoundedStr } from './format'; import { WtcPromptResult } from './types/WtcPromptResult'; +import { MessageKey, message } from './i18n.js'; +import WtcConfig from './types/WtcConfig'; const { log } = console; -const output = (result: WtcPromptResult) => { +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; log(); - log('Started working at:', formatTimestamp(startedAt)); - log((stoppedWorking ? 'Stopped working' : 'Hours calculated') + ' at:', formatTimestamp(stoppedAt)); - log('Worked today:', chalk.green(formatDuration(worked)), chalk.yellow(getHoursRoundedStr(worked))); + log(msg(MessageKey.startedWorking), formatTimestamp(startedAt)); + log( + (stoppedWorking ? msg(MessageKey.stoppedWorking) : msg(MessageKey.hoursCalculated)) + + ` ${msg(MessageKey.klo)}:`, + formatTimestamp(stoppedAt), + ); + log(msg(MessageKey.workedToday), chalk.green(fmtDuration(worked)), chalk.yellow(hoursRounded(worked))); - if (unLogged.asMinutes() == 0) { - log('Unlogged today:', chalk.green('none')); - } else if (unLogged.asMinutes() > 0) { - log('Unlogged today:', chalk.red(formatDuration(unLogged)), chalk.yellow(getHoursRoundedStr(unLogged))); - } else if (unLogged.asMinutes() < 0) { + const unLoggedMinutes = unLogged.asMinutes(); + if (unLoggedMinutes >= 0) { log( - chalk.red(`You have logged ${formatDuration(unLogged)} more than you worked today!`), - chalk.yellow(getHoursRoundedStr(unLogged)), + msg(MessageKey.unloggedToday), + unLoggedMinutes === 0 ? chalk.green(msg(MessageKey.none)) : chalk.red(fmtDuration(unLogged)), + chalk.yellow(hoursRounded(unLogged)), ); + } else if (unLoggedMinutes < 0) { + log(chalk.red(msg(MessageKey.loggedOver, fmtDuration(unLogged))), chalk.yellow(hoursRounded(unLogged))); } if (workLeft.asMinutes() > 0) { - log('You still have to work', chalk.green(formatDuration(workLeft)), 'more today'); + log(msg(MessageKey.workLeft, chalk.green(fmtDuration(workLeft)))); } else if (workedOverTime) { - log('You worked', chalk.green(formatDuration(workedOverTime), 'overtime today')); + log(msg(MessageKey.workedOvertime, chalk.green(fmtDuration(workedOverTime)))); } }; diff --git a/src/types/Language.ts b/src/types/Language.ts new file mode 100644 index 0000000..3dcd6d4 --- /dev/null +++ b/src/types/Language.ts @@ -0,0 +1,6 @@ +enum Language { + en = 'en', + fi = 'fi', +} + +export default Language; diff --git a/src/types/WtcConfig.ts b/src/types/WtcConfig.ts new file mode 100644 index 0000000..65f7b4a --- /dev/null +++ b/src/types/WtcConfig.ts @@ -0,0 +1,20 @@ +import { Dayjs } from 'dayjs'; +import { Duration } from 'dayjs/plugin/duration.js'; +import Language from './Language.js'; + +export default interface WtcConfig { + language: Language, + defaults: { + workDayDuration: Duration; + lunchBreakDuration: Duration; + startTime: Dayjs; + stopTime: Dayjs; + }; + askInput: { + workDayLength: boolean; + startTime: boolean; + stopTime: boolean; + logged: boolean; + hadLunch: boolean; + }; +} diff --git a/src/ui.ts b/src/ui.ts index 1cc4bc5..45d0285 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -1,9 +1,11 @@ +import getConfig from './config.js'; import input from './input.js'; import output from './output.js'; const ui = async () => { - const result = await input(); - output(result); + const config = getConfig(); + const result = await input(config); + output(result, config); }; export default ui; From cd891bf6a00d91ea3d88a7b2a302514d31a3b758 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 17:38:30 +0200 Subject: [PATCH 16/41] Add shebang to output file so a shell script is no longer needed --- Makefile | 1 + bin/wtc | 4 ---- package-lock.json | 42 ++++++++++++++++++++++++++++++++++++++++++ package.json | 3 ++- rollup.config.js | 5 +++-- 5 files changed, 48 insertions(+), 7 deletions(-) delete mode 100755 bin/wtc diff --git a/Makefile b/Makefile index 7ef3132..a1f0c68 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ node_modules: build: node_modules npm run build + chmod +x dist/wtc clean: rm -r dist node_modules diff --git a/bin/wtc b/bin/wtc deleted file mode 100755 index bf01370..0000000 --- a/bin/wtc +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -DIR="$(dirname "$(readlink -f "$0")")" -node "$DIR/../dist/wtc.js" "$@" diff --git a/package-lock.json b/package-lock.json index 8ff0ae9..4ab161d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@typescript-eslint/eslint-plugin": "^6.10.0", "eslint-config-prettier": "^9.0.0", "rollup": "^4.5.1", + "rollup-plugin-add-shebang": "^0.3.1", "tslib": "^2.6.2" } }, @@ -1643,6 +1644,15 @@ "node": ">=10" } }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -1958,6 +1968,31 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-add-shebang": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-add-shebang/-/rollup-plugin-add-shebang-0.3.1.tgz", + "integrity": "sha512-tKONSgKoVw9Om1cp1CnAlPQ9nsHBzu8fInKObX3zT5KZVoAJtslD1aBL84lJuKLeh+L28dB26CBBeYT+doTMLg==", + "dev": true, + "dependencies": { + "magic-string": "^0.25.3", + "rollup-pluginutils": "^2.8.1" + } + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/rollup-pluginutils/node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -2082,6 +2117,13 @@ "source-map": "^0.6.0" } }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", diff --git a/package.json b/package.json index 53bce44..7140b41 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "main": "src/main.ts", "type": "module", "bin": { - "wtc": "bin/wtc" + "wtc": "dist/wtc" }, "scripts": { "build": "rollup -c" @@ -34,6 +34,7 @@ "@typescript-eslint/eslint-plugin": "^6.10.0", "eslint-config-prettier": "^9.0.0", "rollup": "^4.5.1", + "rollup-plugin-add-shebang": "^0.3.1", "tslib": "^2.6.2" }, "dependencies": { diff --git a/rollup.config.js b/rollup.config.js index 3d31556..0e1e6ab 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,14 +1,15 @@ import typescript from '@rollup/plugin-typescript'; 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.js', + file: 'dist/wtc', }, - plugins: [typescript(), terser()], + plugins: [typescript(), terser(), shebang({ include: 'dist/wtc' })], }; export default config; From bb79ecdaa1e1930cdb611b6abfb531852771ac27 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 17:49:34 +0200 Subject: [PATCH 17/41] Fix missing overtime print --- src/input.ts | 6 +++--- src/output.ts | 6 +++--- src/types/WtcPromptResult.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/input.ts b/src/input.ts index e49e582..96d2450 100644 --- a/src/input.ts +++ b/src/input.ts @@ -1,6 +1,5 @@ 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'; @@ -110,10 +109,10 @@ const input = async (config: WtcConfig): Promise => { const unLogged = worked.subtract(logged); const workLeft = workDayDuration.subtract(worked); let workLeftMinutes = workLeft.asMinutes(); - let workedOverTime: Duration | undefined; + 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 +124,7 @@ const input = async (config: WtcConfig): Promise => { hadLunch, worked, workLeft, + workedOvertime, }; } finally { rl.close(); diff --git a/src/output.ts b/src/output.ts index 5e98780..0bcd8c7 100644 --- a/src/output.ts +++ b/src/output.ts @@ -10,7 +10,7 @@ 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 { startedAt, stoppedAt, stoppedWorking, worked, unLogged, workLeft, workedOvertime } = result; log(); log(msg(MessageKey.startedWorking), formatTimestamp(startedAt)); log( @@ -33,8 +33,8 @@ const output = (result: WtcPromptResult, config: WtcConfig) => { 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)))); } }; diff --git a/src/types/WtcPromptResult.ts b/src/types/WtcPromptResult.ts index ed3ee0f..9e0f269 100644 --- a/src/types/WtcPromptResult.ts +++ b/src/types/WtcPromptResult.ts @@ -10,5 +10,5 @@ export interface WtcPromptResult { hadLunch: boolean; worked: Duration; workLeft: Duration; - workedOverTime?: Duration; + workedOvertime?: Duration; } From aee473b93bd1a4364673b35821f2c4a8ea7f3419 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 17:55:42 +0200 Subject: [PATCH 18/41] Fix unlogged print --- src/output.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/output.ts b/src/output.ts index 0bcd8c7..62589b4 100644 --- a/src/output.ts +++ b/src/output.ts @@ -25,7 +25,7 @@ const output = (result: WtcPromptResult, config: WtcConfig) => { 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))); From b76700923d90a018e54599a175d65770f5491175 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 18:04:42 +0200 Subject: [PATCH 19/41] Add support for configurable timestamp format --- config.toml | 5 +++++ src/config.ts | 2 ++ src/format.ts | 2 -- src/output.ts | 14 ++++++++------ src/types/WtcConfig.ts | 1 + 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/config.toml b/config.toml index 895729b..aa2338a 100644 --- a/config.toml +++ b/config.toml @@ -9,6 +9,11 @@ # 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] diff --git a/src/config.ts b/src/config.ts index 8049b22..3623862 100644 --- a/src/config.ts +++ b/src/config.ts @@ -17,6 +17,7 @@ interface RawConfig extends Omit { const defaultConfig: RawConfig = { language: Language.en, + timestampFormat: 'YYYY-MM-DD HH:mm', defaults: { workDayDuration: '07:30', lunchBreakDuration: '00:30', @@ -45,6 +46,7 @@ const getConfig = (): WtcConfig => { return { language: configData.language ?? defaultConfig.language, + timestampFormat: configData.timestampFormat ?? defaultConfig.timestampFormat, defaults: { workDayDuration: parseDuration( configData.defaults.workDayDuration ?? defaultConfig.defaults.workDayDuration, diff --git a/src/format.ts b/src/format.ts index c831e24..1df3a19 100644 --- a/src/format.ts +++ b/src/format.ts @@ -3,8 +3,6 @@ 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 = diff --git a/src/output.ts b/src/output.ts index 62589b4..a74b944 100644 --- a/src/output.ts +++ b/src/output.ts @@ -1,5 +1,5 @@ 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'; @@ -7,16 +7,18 @@ import WtcConfig from './types/WtcConfig'; 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 {language, timestampFormat} = config; + const msg = message(language); + const fmtDuration = formatDuration(language); + const hoursRounded = getHoursRoundedStr(language); const { startedAt, stoppedAt, stoppedWorking, worked, unLogged, workLeft, workedOvertime } = 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))); diff --git a/src/types/WtcConfig.ts b/src/types/WtcConfig.ts index 65f7b4a..a0aa1bc 100644 --- a/src/types/WtcConfig.ts +++ b/src/types/WtcConfig.ts @@ -4,6 +4,7 @@ import Language from './Language.js'; export default interface WtcConfig { language: Language, + timestampFormat: string, defaults: { workDayDuration: Duration; lunchBreakDuration: Duration; From cc6b197d823ecbd80bd440ae66ac946a2e7d4301 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 18:28:42 +0200 Subject: [PATCH 20/41] Resolve rollup build warnings --- rollup.config.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/rollup.config.js b/rollup.config.js index 0e1e6ab..0aef700 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -10,6 +10,19 @@ const config = { file: 'dist/wtc', }, plugins: [typescript(), terser(), shebang({ include: 'dist/wtc' })], + external: [ + '@iarna/toml', + 'chalk', + 'dayjs', + 'dayjs/plugin/customParseFormat.js', + 'dayjs/plugin/duration.js', + 'fs', + 'path', + 'readline/promises', + 'xdg-basedir', + 'yargs', + 'yargs/helpers', + ], }; export default config; From 87737b01186934e58a467de71fa2e65fd18f6fd1 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 18:33:50 +0200 Subject: [PATCH 21/41] Fix finnish time format --- src/format.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/format.ts b/src/format.ts index 1df3a19..2f16062 100644 --- a/src/format.ts +++ b/src/format.ts @@ -19,10 +19,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 [minuttia]'; } else { formatString = duration.hours() > 0 && duration.minutes() > 0 From 29ded9426bc98fe083b390ff0726e995f1d40222 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 18:57:14 +0200 Subject: [PATCH 22/41] Add schema to config --- README.adoc | 2 +- config/config-schema.json | 55 +++++++++++++++++++++++++++++++ config.toml => config/config.toml | 8 +++-- 3 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 config/config-schema.json rename config.toml => config/config.toml (80%) diff --git a/README.adoc b/README.adoc index b4b689a..8b37bc7 100644 --- a/README.adoc +++ b/README.adoc @@ -56,7 +56,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 diff --git a/config/config-schema.json b/config/config-schema.json new file mode 100644 index 0000000..e001123 --- /dev/null +++ b/config/config-schema.json @@ -0,0 +1,55 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "language": { + "type": "string", + "enum": ["en", "fi"] + } + }, + "type": "object", + "properties": { + "language": { + "$ref": "#/definitions/language", + "description": "The language of the application. Currently supported languages are 'en', '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": { + "lunchBreakDuration": { + "type": "string", + "description": "Set as \"00:00\" if you don't have an unpaid lunch break or if you normally log your lunch break hours" + }, + "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" + } + }, + "required": ["lunchBreakDuration", "workDayDuration", "startTime"], + "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" }, + "hadLunch": { "type": "boolean", "description": "Assumed that you didn't have lunch if this is 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" +} diff --git a/config.toml b/config/config.toml similarity index 80% rename from config.toml rename to config/config.toml index aa2338a..034b261 100644 --- a/config.toml +++ b/config/config.toml @@ -1,9 +1,12 @@ +#: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. # The language of the application. # Currently supported languages are "en", "fi" @@ -17,7 +20,7 @@ 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 +# Set as "00:00" if you don't have an unpaid lunch break # or if you normally log your lunch break hours lunchBreakDuration = "00:30" @@ -25,7 +28,6 @@ lunchBreakDuration = "00:30" workDayDuration = "07:30" # The time you start working - startTime = "08:00" # The time you stop working. Can either be "now" or a time From 71e8352ecf923f84053f2fe77b9fe598e609b3da Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 19:13:39 +0200 Subject: [PATCH 23/41] Add output for lunch break duration and rework config lunch option --- config/config-schema.json | 8 ++++---- config/config.toml | 11 ++++------- src/config.ts | 12 ++++-------- src/i18n.ts | 5 +++++ src/input.ts | 12 ++++++------ src/output.ts | 10 +++++++--- src/types/WtcConfig.ts | 3 +-- 7 files changed, 31 insertions(+), 30 deletions(-) diff --git a/config/config-schema.json b/config/config-schema.json index e001123..5d9f2aa 100644 --- a/config/config-schema.json +++ b/config/config-schema.json @@ -8,6 +8,10 @@ }, "type": "object", "properties": { + "lunchBreakDuration": { + "type": "string", + "description": "Remove or set as \"00:00\" if you don't have an unpaid lunch break or if you normally log your lunch break hours" + }, "language": { "$ref": "#/definitions/language", "description": "The language of the application. Currently supported languages are 'en', 'fi'" @@ -19,10 +23,6 @@ "defaults": { "type": "object", "properties": { - "lunchBreakDuration": { - "type": "string", - "description": "Set as \"00:00\" if you don't have an unpaid lunch break or if you normally log your lunch break hours" - }, "workDayDuration": { "type": "string", "description": "Your work day duration" }, "startTime": { "type": "string", "description": "The time you start working" }, "stopTime": { diff --git a/config/config.toml b/config/config.toml index 034b261..d2f6235 100644 --- a/config/config.toml +++ b/config/config.toml @@ -8,6 +8,10 @@ # usually ~/.config/wtc/config.toml # For windows, I don't know. +# Remove or set as "00:00" if you don't have an unpaid lunch break +# or if you normally log your lunch break hours +lunchBreakDuration = "00:30" + # The language of the application. # Currently supported languages are "en", "fi" language = "en" @@ -20,10 +24,6 @@ timestampFormat = "YYYY-MM-DD HH:mm" # This section is for default values for inputs [defaults] -# Set as "00:00" 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" @@ -41,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 diff --git a/src/config.ts b/src/config.ts index 3623862..1275c06 100644 --- a/src/config.ts +++ b/src/config.ts @@ -6,10 +6,10 @@ import { parseDuration, parseTimestamp } from './parse.js'; import WtcConfig from './types/WtcConfig.js'; import Language from './types/Language.js'; -interface RawConfig extends Omit { +interface RawConfig extends Omit { + lunchBreakDuration: string; defaults: { workDayDuration: string; - lunchBreakDuration: string; startTime: string; stopTime: string; }; @@ -18,9 +18,9 @@ interface RawConfig extends Omit { const defaultConfig: RawConfig = { language: Language.en, timestampFormat: 'YYYY-MM-DD HH:mm', + lunchBreakDuration: '00:30', defaults: { workDayDuration: '07:30', - lunchBreakDuration: '00:30', startTime: '08:00', stopTime: 'now', }, @@ -29,7 +29,6 @@ const defaultConfig: RawConfig = { startTime: true, stopTime: true, logged: true, - hadLunch: true, }, }; @@ -47,13 +46,11 @@ const getConfig = (): WtcConfig => { return { language: configData.language ?? defaultConfig.language, timestampFormat: configData.timestampFormat ?? defaultConfig.timestampFormat, + lunchBreakDuration: parseDuration(configData.lunchBreakDuration), 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), }, @@ -62,7 +59,6 @@ const getConfig = (): WtcConfig => { 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, }, }; }; diff --git a/src/i18n.ts b/src/i18n.ts index 833a729..9339014 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -7,6 +7,7 @@ export enum MessageKey { parseTimeFailed, startTimeBeforeStopTimeError, promptLunchBreak, + unpaidLunch, promptLogged, none, startedWorking, @@ -46,6 +47,10 @@ const messages: Record> = { [Language.en]: 'Did you have a lunch break? [y/N]: ', [Language.fi]: 'Piditkö jo lounastauon? [k/E]: ', }, + [MessageKey.unpaidLunch]: { + [Language.en]: 'Unpaid lunch duration:', + [Language.fi]: 'Palkattoman lounaan pituus:', + }, [MessageKey.promptLogged]: { [Language.en]: 'How many hours did you log already? [00:00] ', [Language.fi]: 'Kuinka monta tuntia kirjasit jo? [00:00] ', diff --git a/src/input.ts b/src/input.ts index 96d2450..b7ffbf6 100644 --- a/src/input.ts +++ b/src/input.ts @@ -16,7 +16,7 @@ const { error } = console; const input = async (config: WtcConfig): Promise => { const msg = message(config.language); const fmtDuration = formatDuration(config.language); - const { defaults, askInput } = config; + const { defaults, askInput, lunchBreakDuration } = config; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, @@ -91,13 +91,13 @@ const input = async (config: WtcConfig): Promise => { let worked = dayjs.duration(stoppedAt.diff(startedAt)); let hadLunch = false; - if (askInput.hadLunch) { + if (lunchBreakDuration) { const lunchAnswer = (await rl.question(msg(MessageKey.promptLunchBreak))).toLowerCase(); - hadLunch = lunchAnswer === 'y' || lunchAnswer === 'k'; - } - if (hadLunch) { - worked = worked.subtract(defaults.lunchBreakDuration); + if (lunchAnswer === 'y' || lunchAnswer === 'k') { + hadLunch = true + worked = worked.subtract(lunchBreakDuration); + } } // Calculate unlogged time diff --git a/src/output.ts b/src/output.ts index a74b944..8329fd4 100644 --- a/src/output.ts +++ b/src/output.ts @@ -7,21 +7,25 @@ import WtcConfig from './types/WtcConfig'; const { log } = console; const output = (result: WtcPromptResult, config: WtcConfig) => { - const {language, timestampFormat} = config; + const { language, timestampFormat } = config; const msg = message(language); const fmtDuration = formatDuration(language); const hoursRounded = getHoursRoundedStr(language); - const { startedAt, stoppedAt, stoppedWorking, worked, unLogged, workLeft, workedOvertime } = result; + const { startedAt, stoppedAt, stoppedWorking, worked, unLogged, workLeft, workedOvertime, hadLunch } = result; log(); log(msg(MessageKey.startedWorking), startedAt.format(timestampFormat)); log( (stoppedWorking ? msg(MessageKey.stoppedWorking) : msg(MessageKey.hoursCalculated)) + ` ${msg(MessageKey.klo)}:`, - stoppedAt.format(timestampFormat) + 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.defaults.lunchBreakDuration))); + } + const unLoggedMinutes = unLogged.asMinutes(); if (unLoggedMinutes >= 0) { log( diff --git a/src/types/WtcConfig.ts b/src/types/WtcConfig.ts index a0aa1bc..5add086 100644 --- a/src/types/WtcConfig.ts +++ b/src/types/WtcConfig.ts @@ -5,9 +5,9 @@ import Language from './Language.js'; export default interface WtcConfig { language: Language, timestampFormat: string, + lunchBreakDuration?: Duration; defaults: { workDayDuration: Duration; - lunchBreakDuration: Duration; startTime: Dayjs; stopTime: Dayjs; }; @@ -16,6 +16,5 @@ export default interface WtcConfig { startTime: boolean; stopTime: boolean; logged: boolean; - hadLunch: boolean; }; } From 567bc7a5f2938a5f9e1ec3d57a59bc12c9e3fbbf Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 19:14:33 +0200 Subject: [PATCH 24/41] Config schema: Don't require any properties --- config/config-schema.json | 1 - 1 file changed, 1 deletion(-) diff --git a/config/config-schema.json b/config/config-schema.json index 5d9f2aa..356600a 100644 --- a/config/config-schema.json +++ b/config/config-schema.json @@ -30,7 +30,6 @@ "description": "The time you stop working. Can either be 'now' or a time" } }, - "required": ["lunchBreakDuration", "workDayDuration", "startTime"], "additionalProperties": false, "description": "Default values for inputs" }, From 2a9e94389b39aa79a7ef76dba88aa58f6dad8cdb Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 19:16:21 +0200 Subject: [PATCH 25/41] Config schema: fix language --- config/config-schema.json | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/config/config-schema.json b/config/config-schema.json index 356600a..ddb1276 100644 --- a/config/config-schema.json +++ b/config/config-schema.json @@ -1,11 +1,5 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "language": { - "type": "string", - "enum": ["en", "fi"] - } - }, "type": "object", "properties": { "lunchBreakDuration": { @@ -13,7 +7,8 @@ "description": "Remove or set as \"00:00\" if you don't have an unpaid lunch break or if you normally log your lunch break hours" }, "language": { - "$ref": "#/definitions/language", + "type": "string", + "enum": ["en", "fi"], "description": "The language of the application. Currently supported languages are 'en', 'fi'" }, "timestampFormat": { From 91a73624953501bcb67f46e137fad068403e4603 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 19:17:12 +0200 Subject: [PATCH 26/41] Update description --- config/config-schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config-schema.json b/config/config-schema.json index ddb1276..cdd2eda 100644 --- a/config/config-schema.json +++ b/config/config-schema.json @@ -9,7 +9,7 @@ "language": { "type": "string", "enum": ["en", "fi"], - "description": "The language of the application. Currently supported languages are 'en', 'fi'" + "description": "The language of the application. Currently supported languages are English (en) and Finnish (fi)" }, "timestampFormat": { "type": "string", From b08ab097ba0019f5d2c909e50d61032b0e8ee99d Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 19:51:20 +0200 Subject: [PATCH 27/41] Add config.defaults.hadLunch and rework lunch messages --- config/config-schema.json | 8 ++++---- config/config.toml | 4 ++-- src/config.ts | 11 +++++++---- src/format.ts | 3 ++- src/i18n.ts | 28 ++++++++++++++++++++++------ src/input.ts | 25 ++++++++++++++++++++----- src/output.ts | 2 +- src/types/WtcConfig.ts | 3 ++- 8 files changed, 60 insertions(+), 24 deletions(-) diff --git a/config/config-schema.json b/config/config-schema.json index cdd2eda..165e5f9 100644 --- a/config/config-schema.json +++ b/config/config-schema.json @@ -4,7 +4,7 @@ "properties": { "lunchBreakDuration": { "type": "string", - "description": "Remove or set as \"00:00\" if you don't have an unpaid lunch break or if you normally log your lunch break hours" + "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", @@ -23,7 +23,8 @@ "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" @@ -37,8 +38,7 @@ }, "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" }, - "hadLunch": { "type": "boolean", "description": "Assumed that you didn't have lunch if this is false" } + "logged": { "type": "boolean", "description": "Disable prompt for logged time if set to false" } }, "additionalProperties": false, "description": "Settings to disable prompts" diff --git a/config/config.toml b/config/config.toml index d2f6235..cc7ead1 100644 --- a/config/config.toml +++ b/config/config.toml @@ -8,9 +8,9 @@ # usually ~/.config/wtc/config.toml # For windows, I don't know. -# Remove or set as "00:00" if you don't have an unpaid lunch break +# Comment out or remove if you don't have an unpaid lunch break # or if you normally log your lunch break hours -lunchBreakDuration = "00:30" +unpaidLunchBreakDuration = "00:30" # The language of the application. # Currently supported languages are "en", "fi" diff --git a/src/config.ts b/src/config.ts index 1275c06..6055013 100644 --- a/src/config.ts +++ b/src/config.ts @@ -6,23 +6,25 @@ import { parseDuration, parseTimestamp } from './parse.js'; import WtcConfig from './types/WtcConfig.js'; import Language from './types/Language.js'; -interface RawConfig extends Omit { - lunchBreakDuration: string; +interface RawConfig extends Omit { + unpaidLunchBreakDuration: string; defaults: { workDayDuration: string; startTime: string; stopTime: string; + hadLunch: boolean; }; } const defaultConfig: RawConfig = { language: Language.en, timestampFormat: 'YYYY-MM-DD HH:mm', - lunchBreakDuration: '00:30', + unpaidLunchBreakDuration: '00:30', defaults: { workDayDuration: '07:30', startTime: '08:00', stopTime: 'now', + hadLunch: true, }, askInput: { workDayLength: true, @@ -46,13 +48,14 @@ const getConfig = (): WtcConfig => { return { language: configData.language ?? defaultConfig.language, timestampFormat: configData.timestampFormat ?? defaultConfig.timestampFormat, - lunchBreakDuration: parseDuration(configData.lunchBreakDuration), + unpaidLunchBreakDuration: !configData.unpaidLunchBreakDuration ? undefined : parseDuration(configData.unpaidLunchBreakDuration), defaults: { workDayDuration: parseDuration( configData.defaults.workDayDuration ?? defaultConfig.defaults.workDayDuration, ), startTime: parseTimestamp(configData.defaults.startTime ?? defaultConfig.defaults.startTime), stopTime: parseTimestamp(configData.defaults.stopTime ?? defaultConfig.defaults.stopTime), + hadLunch: configData.defaults.hadLunch ?? defaultConfig.defaults.hadLunch, }, askInput: { workDayLength: configData.askInput.workDayLength ?? defaultConfig.askInput.workDayLength, diff --git a/src/format.ts b/src/format.ts index 2f16062..38c6c69 100644 --- a/src/format.ts +++ b/src/format.ts @@ -7,7 +7,8 @@ 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'; } diff --git a/src/i18n.ts b/src/i18n.ts index 9339014..329206a 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -1,12 +1,16 @@ import Language from './types/Language'; + export enum MessageKey { promptWorkDayDuration, + excludingLunch, promptStartTime, promptStopTime, parseTimeFailed, startTimeBeforeStopTimeError, promptLunchBreak, + promptYesNoYes, + promptYesNoNo, unpaidLunch, promptLogged, none, @@ -24,8 +28,12 @@ export enum MessageKey { const messages: Record> = { [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}]: ', @@ -44,12 +52,20 @@ const messages: Record> = { [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 duration:', - [Language.fi]: 'Palkattoman lounaan pituus:', + [Language.en]: 'Unpaid lunch:', + [Language.fi]: 'Palkaton lounas:', }, [MessageKey.promptLogged]: { [Language.en]: 'How many hours did you log already? [00:00] ', diff --git a/src/input.ts b/src/input.ts index b7ffbf6..5a2303c 100644 --- a/src/input.ts +++ b/src/input.ts @@ -16,7 +16,7 @@ const { error } = console; const input = async (config: WtcConfig): Promise => { const msg = message(config.language); const fmtDuration = formatDuration(config.language); - const { defaults, askInput, lunchBreakDuration } = config; + const { defaults, askInput, unpaidLunchBreakDuration: lunchBreakDuration } = config; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, @@ -32,7 +32,11 @@ const input = async (config: WtcConfig): Promise => { if (askInput.workDayLength) { 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,10 +96,21 @@ const input = async (config: WtcConfig): Promise => { let hadLunch = false; if (lunchBreakDuration) { - const lunchAnswer = (await rl.question(msg(MessageKey.promptLunchBreak))).toLowerCase(); + const lunchAnswer = ( + await rl.question( + msg( + MessageKey.promptLunchBreak, + msg(config.defaults.hadLunch ? MessageKey.promptYesNoYes : MessageKey.promptYesNoNo), + ), + ) + ).toLowerCase(); - if (lunchAnswer === 'y' || lunchAnswer === 'k') { - hadLunch = true + if ( + lunchAnswer === 'y' || + lunchAnswer === 'k' || + (config.defaults.hadLunch && lunchAnswer !== 'n' && lunchAnswer !== 'e') + ) { + hadLunch = true; worked = worked.subtract(lunchBreakDuration); } } diff --git a/src/output.ts b/src/output.ts index 8329fd4..49eaa33 100644 --- a/src/output.ts +++ b/src/output.ts @@ -23,7 +23,7 @@ const output = (result: WtcPromptResult, config: WtcConfig) => { log(msg(MessageKey.workedToday), chalk.green(fmtDuration(worked)), chalk.yellow(hoursRounded(worked))); if (hadLunch) { - log(msg(MessageKey.unpaidLunch), chalk.green(fmtDuration(config.defaults.lunchBreakDuration))); + log(msg(MessageKey.unpaidLunch), chalk.green(fmtDuration(config.unpaidLunchBreakDuration))); } const unLoggedMinutes = unLogged.asMinutes(); diff --git a/src/types/WtcConfig.ts b/src/types/WtcConfig.ts index 5add086..c0b9c62 100644 --- a/src/types/WtcConfig.ts +++ b/src/types/WtcConfig.ts @@ -5,11 +5,12 @@ import Language from './Language.js'; export default interface WtcConfig { language: Language, timestampFormat: string, - lunchBreakDuration?: Duration; + unpaidLunchBreakDuration?: Duration; defaults: { workDayDuration: Duration; startTime: Dayjs; stopTime: Dayjs; + hadLunch: boolean; }; askInput: { workDayLength: boolean; From f693c90dec277b5eff5d7521ad1785b5f23a32fc Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 19:51:52 +0200 Subject: [PATCH 28/41] 1.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4ab161d..3bbb2b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "work-time-calculator", - "version": "0.0.10", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "work-time-calculator", - "version": "0.0.10", + "version": "1.0.0", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 7140b41..0171f14 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "work-time-calculator", - "version": "0.0.10", + "version": "1.0.0", "description": "An interactive CLI tool to calculate work time", "license": "MIT", "repository": { From 536aaa01b0b6085b12c842a6d1f314c3cb0468e8 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 19:57:46 +0200 Subject: [PATCH 29/41] Add update instructions --- README.adoc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.adoc b/README.adoc index 8b37bc7..326a621 100644 --- a/README.adoc +++ b/README.adoc @@ -35,6 +35,16 @@ After installation, you should be able to run the program with wtc ---- +== Update + +The easiest way to update is to first remove the program and then install again + +[,shell] +---- +npm r -g work-time-calculator +npm i -g work-time-calculator +---- + == Rationale Don't know if it's just me but calculating my working hours sometimes From 6bbfd257454de8d2066c2c9c3c8bd99239cd358d Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 20:24:35 +0200 Subject: [PATCH 30/41] Fix running on older nodejs versions The shebang does not work without extension --- Makefile | 1 - README.adoc | 8 +------- bin/wtc | 4 ++++ package.json | 3 +-- rollup.config.js | 5 ++--- 5 files changed, 8 insertions(+), 13 deletions(-) create mode 100755 bin/wtc diff --git a/Makefile b/Makefile index a1f0c68..7ef3132 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,6 @@ node_modules: build: node_modules npm run build - chmod +x dist/wtc clean: rm -r dist node_modules diff --git a/README.adoc b/README.adoc index 326a621..d012c0f 100644 --- a/README.adoc +++ b/README.adoc @@ -37,13 +37,7 @@ wtc == Update -The easiest way to update is to first remove the program and then install again - -[,shell] ----- -npm r -g work-time-calculator -npm i -g work-time-calculator ----- +To update, just run the install command again == Rationale diff --git a/bin/wtc b/bin/wtc new file mode 100755 index 0000000..bf01370 --- /dev/null +++ b/bin/wtc @@ -0,0 +1,4 @@ +#!/bin/sh + +DIR="$(dirname "$(readlink -f "$0")")" +node "$DIR/../dist/wtc.js" "$@" diff --git a/package.json b/package.json index 0171f14..a7461f9 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "main": "src/main.ts", "type": "module", "bin": { - "wtc": "dist/wtc" + "wtc": "bin/wtc" }, "scripts": { "build": "rollup -c" @@ -34,7 +34,6 @@ "@typescript-eslint/eslint-plugin": "^6.10.0", "eslint-config-prettier": "^9.0.0", "rollup": "^4.5.1", - "rollup-plugin-add-shebang": "^0.3.1", "tslib": "^2.6.2" }, "dependencies": { diff --git a/rollup.config.js b/rollup.config.js index 0aef700..c109771 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,15 +1,14 @@ import typescript from '@rollup/plugin-typescript'; 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.js', }, - plugins: [typescript(), terser(), shebang({ include: 'dist/wtc' })], + plugins: [typescript(), terser()], external: [ '@iarna/toml', 'chalk', From a2e53e32a616cc028f564b48e329bf772790a77d Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 20:25:46 +0200 Subject: [PATCH 31/41] 1.0.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3bbb2b4..534df9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "work-time-calculator", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "work-time-calculator", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index a7461f9..94dbbb5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "work-time-calculator", - "version": "1.0.0", + "version": "1.0.1", "description": "An interactive CLI tool to calculate work time", "license": "MIT", "repository": { From acb07d0cdc647d7a3cd615420c71fc6ae44627bc Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 20:31:05 +0200 Subject: [PATCH 32/41] Fix config schema --- config/config-schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config-schema.json b/config/config-schema.json index 165e5f9..281af39 100644 --- a/config/config-schema.json +++ b/config/config-schema.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { - "lunchBreakDuration": { + "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" }, From f8cf9bad7d02e26628d28b63f14bc2b6010df037 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 20:46:58 +0200 Subject: [PATCH 33/41] Fix crash on missing config sections --- src/config.ts | 32 +++++++++++++++++--------------- src/input.ts | 2 +- src/types/WtcConfig.ts | 2 +- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/config.ts b/src/config.ts index 6055013..fe6f0f8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -27,7 +27,7 @@ const defaultConfig: RawConfig = { hadLunch: true, }, askInput: { - workDayLength: true, + workDayDuration: true, startTime: true, stopTime: true, logged: true, @@ -38,30 +38,32 @@ const getConfig = (): WtcConfig => { const configDir = xdgConfig || path.join(process.env.HOME ?? './', '.config'); let configFilePath = path.join(configDir, 'wtc', 'config.toml'); - let configData: RawConfig; + let configData: Partial; if (fs.existsSync(configFilePath)) { configData = toml.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, - timestampFormat: configData.timestampFormat ?? defaultConfig.timestampFormat, - unpaidLunchBreakDuration: !configData.unpaidLunchBreakDuration ? undefined : parseDuration(configData.unpaidLunchBreakDuration), + language: language ?? defaultConfig.language, + timestampFormat: timestampFormat ?? defaultConfig.timestampFormat, + unpaidLunchBreakDuration: !unpaidLunchBreakDuration + ? undefined + : parseDuration(unpaidLunchBreakDuration), defaults: { - workDayDuration: parseDuration( - configData.defaults.workDayDuration ?? defaultConfig.defaults.workDayDuration, - ), - startTime: parseTimestamp(configData.defaults.startTime ?? defaultConfig.defaults.startTime), - stopTime: parseTimestamp(configData.defaults.stopTime ?? defaultConfig.defaults.stopTime), - hadLunch: configData.defaults.hadLunch ?? defaultConfig.defaults.hadLunch, + 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, + workDayDuration: askInput?.workDayDuration ?? defaultConfig.askInput.workDayDuration, + startTime: askInput?.startTime ?? defaultConfig.askInput.startTime, + stopTime: askInput?.stopTime ?? defaultConfig.askInput.stopTime, + logged: askInput?.logged ?? defaultConfig.askInput.logged, }, }; }; diff --git a/src/input.ts b/src/input.ts index 5a2303c..035ed48 100644 --- a/src/input.ts +++ b/src/input.ts @@ -30,7 +30,7 @@ const input = async (config: WtcConfig): Promise => { // Get work day duration let workDayDuration: Duration | undefined = undefined; - if (askInput.workDayLength) { + if (askInput.workDayDuration) { const durationAnswer = await rl.question( msg( MessageKey.promptWorkDayDuration, diff --git a/src/types/WtcConfig.ts b/src/types/WtcConfig.ts index c0b9c62..15d6382 100644 --- a/src/types/WtcConfig.ts +++ b/src/types/WtcConfig.ts @@ -13,7 +13,7 @@ export default interface WtcConfig { hadLunch: boolean; }; askInput: { - workDayLength: boolean; + workDayDuration: boolean; startTime: boolean; stopTime: boolean; logged: boolean; From e7a67ebfa0ac3db6636fc3fe94f5459456378796 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 20:53:37 +0200 Subject: [PATCH 34/41] Fix displaying overlogged time as negative --- src/output.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/output.ts b/src/output.ts index 49eaa33..b300000 100644 --- a/src/output.ts +++ b/src/output.ts @@ -3,8 +3,11 @@ import { formatDuration, getHoursRoundedStr } from './format'; import { WtcPromptResult } from './types/WtcPromptResult'; import { MessageKey, message } from './i18n.js'; import WtcConfig from './types/WtcConfig'; +import duration from 'dayjs/plugin/duration.js'; +import dayjs from 'dayjs'; const { log } = console; +dayjs.extend(duration); const output = (result: WtcPromptResult, config: WtcConfig) => { const { language, timestampFormat } = config; @@ -34,7 +37,8 @@ const output = (result: WtcPromptResult, config: WtcConfig) => { 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) { From 13b4c3a6402d13d32baa89fd1b07170bec214228 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 20:56:11 +0200 Subject: [PATCH 35/41] Fix typo --- src/format.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/format.ts b/src/format.ts index 38c6c69..a242f7b 100644 --- a/src/format.ts +++ b/src/format.ts @@ -23,7 +23,7 @@ export const formatDuration = ? `H [tunti${duration.hours() > 1 ? 'a' : ''} ja] m [minuuttia]` : duration.hours() > 0 ? `H [tunti${duration.hours() > 1 ? 'a' : ''}]` - : 'm [minuttia]'; + : 'm [minuuttia]'; } else { formatString = duration.hours() > 0 && duration.minutes() > 0 From 0ce99d923536bbbec9c4ce59e113c65e5228eae6 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Thu, 23 Nov 2023 20:56:40 +0200 Subject: [PATCH 36/41] 1.0.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 534df9f..56b462e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "work-time-calculator", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "work-time-calculator", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 94dbbb5..21d7b11 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "work-time-calculator", - "version": "1.0.1", + "version": "1.0.2", "description": "An interactive CLI tool to calculate work time", "license": "MIT", "repository": { From bb375e62ea5922f1ec271ae3262bb22e4be442be Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Fri, 24 Nov 2023 16:03:29 +0200 Subject: [PATCH 37/41] Update package-lock.json --- package-lock.json | 42 ------------------------------------------ 1 file changed, 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index 56b462e..971f2c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,6 @@ "@typescript-eslint/eslint-plugin": "^6.10.0", "eslint-config-prettier": "^9.0.0", "rollup": "^4.5.1", - "rollup-plugin-add-shebang": "^0.3.1", "tslib": "^2.6.2" } }, @@ -1644,15 +1643,6 @@ "node": ">=10" } }, - "node_modules/magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", - "dev": true, - "dependencies": { - "sourcemap-codec": "^1.4.8" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -1968,31 +1958,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/rollup-plugin-add-shebang": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/rollup-plugin-add-shebang/-/rollup-plugin-add-shebang-0.3.1.tgz", - "integrity": "sha512-tKONSgKoVw9Om1cp1CnAlPQ9nsHBzu8fInKObX3zT5KZVoAJtslD1aBL84lJuKLeh+L28dB26CBBeYT+doTMLg==", - "dev": true, - "dependencies": { - "magic-string": "^0.25.3", - "rollup-pluginutils": "^2.8.1" - } - }, - "node_modules/rollup-pluginutils": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", - "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", - "dev": true, - "dependencies": { - "estree-walker": "^0.6.1" - } - }, - "node_modules/rollup-pluginutils/node_modules/estree-walker": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", - "dev": true - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -2117,13 +2082,6 @@ "source-map": "^0.6.0" } }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "deprecated": "Please use @jridgewell/sourcemap-codec instead", - "dev": true - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", From e44fc052f20c908474205773d5503d0eb20db23b Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Fri, 24 Nov 2023 16:03:44 +0200 Subject: [PATCH 38/41] Add dayjs configurator --- src/dayjs.ts | 10 ++++++++++ src/format.ts | 2 +- src/input.ts | 9 +++------ src/output.ts | 4 +--- src/parse.ts | 7 +------ 5 files changed, 16 insertions(+), 16 deletions(-) create mode 100644 src/dayjs.ts diff --git a/src/dayjs.ts b/src/dayjs.ts new file mode 100644 index 0000000..526f1a2 --- /dev/null +++ b/src/dayjs.ts @@ -0,0 +1,10 @@ +import dayjs, {Dayjs} from 'dayjs'; +import duration, {Duration} from 'dayjs/plugin/duration.js'; +import customParseFormat from 'dayjs/plugin/customParseFormat.js'; + +dayjs.extend(duration); +dayjs.extend(customParseFormat); + +export default dayjs; + +export type {Dayjs, Duration}; diff --git a/src/format.ts b/src/format.ts index a242f7b..b097907 100644 --- a/src/format.ts +++ b/src/format.ts @@ -1,4 +1,4 @@ -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'; diff --git a/src/input.ts b/src/input.ts index 035ed48..afc847d 100644 --- a/src/input.ts +++ b/src/input.ts @@ -1,15 +1,12 @@ import chalk from 'chalk'; -import { Duration } from 'dayjs/plugin/duration'; 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 dayjs, { Duration } from './dayjs'; const { error } = console; @@ -123,7 +120,7 @@ const input = async (config: WtcConfig): Promise => { const logged = parseDuration(loggedAnswer); const unLogged = worked.subtract(logged); const workLeft = workDayDuration.subtract(worked); - let workLeftMinutes = workLeft.asMinutes(); + const workLeftMinutes = workLeft.asMinutes(); let workedOvertime: Duration | undefined; if (workLeftMinutes < 0) { diff --git a/src/output.ts b/src/output.ts index b300000..9d8de7b 100644 --- a/src/output.ts +++ b/src/output.ts @@ -3,11 +3,9 @@ import { formatDuration, getHoursRoundedStr } from './format'; import { WtcPromptResult } from './types/WtcPromptResult'; import { MessageKey, message } from './i18n.js'; import WtcConfig from './types/WtcConfig'; -import duration from 'dayjs/plugin/duration.js'; -import dayjs from 'dayjs'; +import dayjs from './dayjs'; const { log } = console; -dayjs.extend(duration); const output = (result: WtcPromptResult, config: WtcConfig) => { const { language, timestampFormat } = config; diff --git a/src/parse.ts b/src/parse.ts index 4abe2fd..7d723fb 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -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)); From 820a49efd822363d3123e43c00d7fd8c6413ab44 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Fri, 24 Nov 2023 16:06:57 +0200 Subject: [PATCH 39/41] Fix main directive in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 21d7b11..5a1e825 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,11 @@ "url": "https://git.korhonen.cc/FunctionalHacker/work-time-calculator/issues", "email": "wtc@functionalhacker.korhonen.cc" }, - "main": "src/main.ts", "type": "module", "bin": { "wtc": "bin/wtc" }, + "main": "dist/wtc.js", "scripts": { "build": "rollup -c" }, From 3239a7c611cde618ef9ab866b28dcace181c474f Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Fri, 24 Nov 2023 16:44:32 +0200 Subject: [PATCH 40/41] Bundle all files to single JavaScript file, add development mode --- Makefile | 15 +++-- bin/wtc | 2 +- package-lock.json | 147 ++++++++++++++++++++++++++++++++++++++++-- package.json | 9 ++- rollup.config.js | 27 -------- rollup.dev.config.js | 15 +++++ rollup.prod.config.js | 15 +++++ src/config.ts | 6 +- src/dayjs.ts | 6 +- 9 files changed, 194 insertions(+), 48 deletions(-) delete mode 100644 rollup.config.js create mode 100644 rollup.dev.config.js create mode 100644 rollup.prod.config.js diff --git a/Makefile b/Makefile index 7ef3132..c5a355b 100644 --- a/Makefile +++ b/Makefile @@ -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,13 +12,17 @@ help: node_modules: npm install -build: node_modules - npm run build +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." diff --git a/bin/wtc b/bin/wtc index bf01370..408a2ea 100755 --- a/bin/wtc +++ b/bin/wtc @@ -1,4 +1,4 @@ #!/bin/sh DIR="$(dirname "$(readlink -f "$0")")" -node "$DIR/../dist/wtc.js" "$@" +node "$DIR/../dist/wtc.mjs" "$@" diff --git a/package-lock.json b/package-lock.json index 971f2c0..a0226f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,9 @@ "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", @@ -26,6 +27,7 @@ "@typescript-eslint/eslint-plugin": "^6.10.0", "eslint-config-prettier": "^9.0.0", "rollup": "^4.5.1", + "rollup-plugin-add-shebang": "^0.3.1", "tslib": "^2.6.2" } }, @@ -133,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", @@ -231,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", @@ -478,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", @@ -809,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", @@ -916,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", @@ -941,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", @@ -1429,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", @@ -1483,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", @@ -1524,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", @@ -1643,6 +1729,15 @@ "node": ">=10" } }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -1958,6 +2053,31 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-add-shebang": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-add-shebang/-/rollup-plugin-add-shebang-0.3.1.tgz", + "integrity": "sha512-tKONSgKoVw9Om1cp1CnAlPQ9nsHBzu8fInKObX3zT5KZVoAJtslD1aBL84lJuKLeh+L28dB26CBBeYT+doTMLg==", + "dev": true, + "dependencies": { + "magic-string": "^0.25.3", + "rollup-pluginutils": "^2.8.1" + } + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/rollup-pluginutils/node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -2082,6 +2202,21 @@ "source-map": "^0.6.0" } }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "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", diff --git a/package.json b/package.json index 5a1e825..5a651e7 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,10 @@ "bin": { "wtc": "bin/wtc" }, - "main": "dist/wtc.js", + "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 ", "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", @@ -34,12 +36,13 @@ "@typescript-eslint/eslint-plugin": "^6.10.0", "eslint-config-prettier": "^9.0.0", "rollup": "^4.5.1", + "rollup-plugin-add-shebang": "^0.3.1", "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" } diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index c109771..0000000 --- a/rollup.config.js +++ /dev/null @@ -1,27 +0,0 @@ -import typescript from '@rollup/plugin-typescript'; -import terser from '@rollup/plugin-terser'; - -/** @type {import('rollup').RollupOptions} */ -const config = { - input: 'src/main.ts', - output: { - format: 'esm', - file: 'dist/wtc.js', - }, - plugins: [typescript(), terser()], - external: [ - '@iarna/toml', - 'chalk', - 'dayjs', - 'dayjs/plugin/customParseFormat.js', - 'dayjs/plugin/duration.js', - 'fs', - 'path', - 'readline/promises', - 'xdg-basedir', - 'yargs', - 'yargs/helpers', - ], -}; - -export default config; diff --git a/rollup.dev.config.js b/rollup.dev.config.js new file mode 100644 index 0000000..7d2e1fb --- /dev/null +++ b/rollup.dev.config.js @@ -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; diff --git a/rollup.prod.config.js b/rollup.prod.config.js new file mode 100644 index 0000000..258f57b --- /dev/null +++ b/rollup.prod.config.js @@ -0,0 +1,15 @@ +import typescript from '@rollup/plugin-typescript'; +import { nodeResolve } from '@rollup/plugin-node-resolve'; +import terser from '@rollup/plugin-terser'; + +/** @type {import('rollup').RollupOptions} */ +const config = { + input: 'src/main.ts', + output: { + format: 'esm', + file: 'dist/wtc.mjs', + }, + plugins: [typescript(), nodeResolve({ exportConditions: ['node'] }), terser()], +}; + +export default config; diff --git a/src/config.ts b/src/config.ts index fe6f0f8..02f288b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,7 +1,7 @@ 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'; @@ -36,11 +36,11 @@ const defaultConfig: RawConfig = { 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: Partial; 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; } diff --git a/src/dayjs.ts b/src/dayjs.ts index 526f1a2..7626024 100644 --- a/src/dayjs.ts +++ b/src/dayjs.ts @@ -1,6 +1,6 @@ -import dayjs, {Dayjs} from 'dayjs'; -import duration, {Duration} from 'dayjs/plugin/duration.js'; -import customParseFormat from 'dayjs/plugin/customParseFormat.js'; +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); From c97472f6f5aa1f1085f118cc5ca659bb64723d2f Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Mon, 27 Nov 2023 17:57:13 +0200 Subject: [PATCH 41/41] i18n CLI options --- src/i18n.ts | 11 ++++++++++- src/input.ts | 8 ++++---- src/main.ts | 35 ++++++++++++++++++++++++++++++++--- src/output.ts | 8 ++++---- src/types/WtcConfig.ts | 8 ++++++++ src/ui.ts | 8 ++------ src/update.ts | 7 +++++++ 7 files changed, 67 insertions(+), 18 deletions(-) create mode 100644 src/update.ts diff --git a/src/i18n.ts b/src/i18n.ts index 329206a..074e43b 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -1,7 +1,8 @@ import Language from './types/Language'; - export enum MessageKey { + cliHelp, + cliVersion, promptWorkDayDuration, excludingLunch, promptStartTime, @@ -27,6 +28,14 @@ export enum MessageKey { } const messages: Record> = { + [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{0}? [{1}]: ', [Language.fi]: 'Kuinka pitkä työpäiväsi on tänään{0}? [{1}]: ', diff --git a/src/input.ts b/src/input.ts index afc847d..386060f 100644 --- a/src/input.ts +++ b/src/input.ts @@ -4,14 +4,14 @@ import * as readline from 'readline/promises'; import { formatDuration, formatTime } from './format'; import { Dayjs } from 'dayjs'; import { WtcPromptResult } from './types/WtcPromptResult'; -import WtcConfig from './types/WtcConfig'; -import { MessageKey, message } from './i18n'; +import { WtcRuntimeConfig } from './types/WtcConfig'; +import { MessageKey } from './i18n'; import dayjs, { Duration } from './dayjs'; const { error } = console; -const input = async (config: WtcConfig): Promise => { - const msg = message(config.language); +const input = async (runtimeCfg: WtcRuntimeConfig): Promise => { + const { config, msg } = runtimeCfg; const fmtDuration = formatDuration(config.language); const { defaults, askInput, unpaidLunchBreakDuration: lunchBreakDuration } = config; const rl = readline.createInterface({ diff --git a/src/main.ts b/src/main.ts index f244ada..6e3b054 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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); diff --git a/src/output.ts b/src/output.ts index 9d8de7b..86e0619 100644 --- a/src/output.ts +++ b/src/output.ts @@ -1,15 +1,15 @@ import chalk from 'chalk'; 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 output = (result: WtcPromptResult, runtimeCfg: WtcRuntimeConfig) => { + const {config, msg} = runtimeCfg; const { language, timestampFormat } = config; - const msg = message(language); const fmtDuration = formatDuration(language); const hoursRounded = getHoursRoundedStr(language); const { startedAt, stoppedAt, stoppedWorking, worked, unLogged, workLeft, workedOvertime, hadLunch } = result; diff --git a/src/types/WtcConfig.ts b/src/types/WtcConfig.ts index 15d6382..7d822b8 100644 --- a/src/types/WtcConfig.ts +++ b/src/types/WtcConfig.ts @@ -1,6 +1,7 @@ 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, @@ -19,3 +20,10 @@ export default interface WtcConfig { logged: boolean; }; } + +/** Config and current language msg function together */ +export interface WtcRuntimeConfig { + config: WtcConfig; + msg: ReturnType; +} + diff --git a/src/ui.ts b/src/ui.ts index 45d0285..9d792fe 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -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; diff --git a/src/update.ts b/src/update.ts new file mode 100644 index 0000000..92ad114 --- /dev/null +++ b/src/update.ts @@ -0,0 +1,7 @@ +const { log } = console; + +const update = () => { + log('update'); +}; + +export default update;