From 793b6aece08734fc0d05d54dde29d74aadacb8b7 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 19:28:35 +0200 Subject: [PATCH 01/55] Use github flavored markdown for npmjs README --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 895d8a5..0971178 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ release: update-npmjs-readme: asciidoctor -b docbook -o target/README.xml README.adoc - pandoc -f docbook -t markdown_strict target/README.xml -o README.md + pandoc -f docbook -t gfm target/README.xml -o README.md publish: update-npmjs-readme @git push && \ From fe9e51a29739fc2051225ce46ff2203d3e690a22 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 19:35:28 +0200 Subject: [PATCH 02/55] Add npmignore Fixes built files not added to npmjs --- .gitignore | 2 +- .npmignore | 4 ++++ Makefile | 6 +++--- bin/wtc | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 .npmignore diff --git a/.gitignore b/.gitignore index ed256b9..736bf05 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ node_modules/ -target/ +dist/ README.md diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..ded40b7 --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +* +!bin/ +!dist/ +!README.md diff --git a/Makefile b/Makefile index 0971178..82b400e 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ build: node_modules npm run build clean: - rm -r target node_modules + rm -r dist node_modules release: @read -p "Enter version bump (patch, minor, major): " bump && \ @@ -23,8 +23,8 @@ release: echo "Version $$version created. Run 'make publish' to push the changes and publish the package." update-npmjs-readme: - asciidoctor -b docbook -o target/README.xml README.adoc - pandoc -f docbook -t gfm target/README.xml -o README.md + asciidoctor -b docbook -o dist/README.xml README.adoc + pandoc -f docbook -t gfm dist/README.xml -o README.md publish: update-npmjs-readme @git push && \ diff --git a/bin/wtc b/bin/wtc index d5bd086..525bc83 100755 --- a/bin/wtc +++ b/bin/wtc @@ -1,2 +1,2 @@ #!/usr/bin/env node -import '../target/main.js'; +import '../dist/main.js'; From 99c0b03941a978932078c805edd8847859f8a433 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 19:36:01 +0200 Subject: [PATCH 03/55] Build package before release --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 82b400e..9e0fba5 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ build: node_modules clean: rm -r dist node_modules -release: +release: build @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." From 4664b9154597ca93711180d4e0f3bd2e2c285e08 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 19:40:39 +0200 Subject: [PATCH 04/55] Fix tsc target dir --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index d09b535..da2bd3f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,7 @@ "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, - "outDir": "target" + "outDir": "dist" }, "include": ["src/**/*.ts"] } From 7c4e07b74c7ec1ba43ab679e54f50c983d76c75c Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 19:41:09 +0200 Subject: [PATCH 05/55] 0.0.4 --- 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 d736142..7dd98f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "work-time-calculator", - "version": "0.0.3", + "version": "0.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "work-time-calculator", - "version": "0.0.3", + "version": "0.0.4", "license": "MIT", "dependencies": { "chalk": "^5.3.0", diff --git a/package.json b/package.json index ebf3661..d525ffb 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.3", + "version": "0.0.4", "description": "An interactive CLI tool to calculate work time", "license": "MIT", "repository": { From 8f505d39a31027474a2739a9a983f1efa2af6560 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 19:44:43 +0200 Subject: [PATCH 06/55] npm publish: get OTP from pass --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 9e0fba5..7ef3132 100644 --- a/Makefile +++ b/Makefile @@ -29,4 +29,4 @@ update-npmjs-readme: publish: update-npmjs-readme @git push && \ git push --tags && \ - npm publish + npm publish --otp $$(pass otp services/npmjs.com) From e82cb22b93a6d84aadc45ae3179dd8685e890190 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 19:47:41 +0200 Subject: [PATCH 07/55] Fix npmignore --- .npmignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.npmignore b/.npmignore index ded40b7..a22abda 100644 --- a/.npmignore +++ b/.npmignore @@ -1,4 +1,4 @@ * -!bin/ -!dist/ +!bin/wtc +!dist/**/*.js !README.md From a315a1b3b261507d60b676eeeb78c577243f543b Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 19:48:09 +0200 Subject: [PATCH 08/55] 0.0.5 --- 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 7dd98f9..3484b54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "work-time-calculator", - "version": "0.0.4", + "version": "0.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "work-time-calculator", - "version": "0.0.4", + "version": "0.0.5", "license": "MIT", "dependencies": { "chalk": "^5.3.0", diff --git a/package.json b/package.json index d525ffb..c9291cf 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.4", + "version": "0.0.5", "description": "An interactive CLI tool to calculate work time", "license": "MIT", "repository": { From 8a119295d57c42b7ad2aa6ce74da183be4fa235e Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 19:58:06 +0200 Subject: [PATCH 09/55] Add image to README --- README.adoc | 2 ++ img/demo.png | Bin 0 -> 77074 bytes 2 files changed, 2 insertions(+) create mode 100644 img/demo.png diff --git a/README.adoc b/README.adoc index fc849c0..7974e28 100644 --- a/README.adoc +++ b/README.adoc @@ -2,6 +2,8 @@ An interactive CLI tool to calculate work time. +image::img/demo.png[] + == Install You can run this in your terminal diff --git a/img/demo.png b/img/demo.png new file mode 100644 index 0000000000000000000000000000000000000000..068b615f2f30a2a82186813874cf5cf27e9f1901 GIT binary patch literal 77074 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ℑ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

jJYlwfubv7#QIc#RH(Q;pwrDUZtlH$@jm6u90 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 YSZw0bmSiRtLsz`2a9uuI*4X#I0lx&s9smFU literal 0 HcmV?d00001 From 9b9666ca32acb5d830c3d181bc50064f2c1a773c Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 20:07:16 +0200 Subject: [PATCH 10/55] Add more rationale in the readme --- README.adoc | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/README.adoc b/README.adoc index 7974e28..7ee78f9 100644 --- a/README.adoc +++ b/README.adoc @@ -37,9 +37,23 @@ wtc == Rationale -This is a highly opinionated CLI tool I built for my specific needs. -In time, I will probable make it more generic and configurable. -For now, the following assumptions are made. +Don't know if it's just me but calculating my working hours sometimes +can get difficult. Especially if you have flexible hours and you end up +starting at a weird time, f.ex 08:15. This combined with the fact that +I have to log my hours to many different tasks, at the end of the day +calculating all this can get very confusing. + +To alleviate my pains, I included the following features + +* Asks wether you already had lunch or not and accommodates this in the calculation +* Asks the hours that you already logged and calculates unlogged hours +* Calculates how much under/overtime you worked + +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. * You have an unpaid 30 minute lunch break From 28d6c8d1201f7986af44c62c62f98335cac0bccd Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 21:03:09 +0200 Subject: [PATCH 11/55] Fix "unknown file extension" nodejs error --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index c9291cf..28bb709 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "url": "https://git.korhonen.cc/FunctionalHacker/work-time-calculator/issues", "email": "wtc@functionalhacker.korhonen.cc" }, - "type": "module", "main": "src/main.ts", "bin": { "wtc": "bin/wtc" From c8536e5f6e67209faeb968444dd06c32f4182d9f Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 21:03:34 +0200 Subject: [PATCH 12/55] 0.0.6 --- 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 3484b54..9b48b77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "work-time-calculator", - "version": "0.0.5", + "version": "0.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "work-time-calculator", - "version": "0.0.5", + "version": "0.0.6", "license": "MIT", "dependencies": { "chalk": "^5.3.0", diff --git a/package.json b/package.json index 28bb709..b18df6a 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.5", + "version": "0.0.6", "description": "An interactive CLI tool to calculate work time", "license": "MIT", "repository": { From 412ec3c887787802a4a338a38b88d00066f179a3 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 21:15:43 +0200 Subject: [PATCH 13/55] Revert "0.0.6" This reverts commit c8536e5f6e67209faeb968444dd06c32f4182d9f. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index b18df6a..4ad059e 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "email": "wtc@functionalhacker.korhonen.cc" }, "main": "src/main.ts", + "type": "module", "bin": { "wtc": "bin/wtc" }, From 6571a5daf8ac69b61a586da366d83ad08d050d09 Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 21:16:05 +0200 Subject: [PATCH 14/55] 0.0.7 --- 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 9b48b77..8cd30d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "work-time-calculator", - "version": "0.0.6", + "version": "0.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "work-time-calculator", - "version": "0.0.6", + "version": "0.0.7", "license": "MIT", "dependencies": { "chalk": "^5.3.0", diff --git a/package.json b/package.json index 4ad059e..0114aee 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.6", + "version": "0.0.7", "description": "An interactive CLI tool to calculate work time", "license": "MIT", "repository": { From 82e18eb286fc20bf36bb341a79f02171ef53305d Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 21:28:44 +0200 Subject: [PATCH 15/55] 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 16/55] 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 17/55] 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 18/55] 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 19/55] 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 20/55] 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 21/55] 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 22/55] 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 23/55] 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 24/55] 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 25/55] 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 26/55] 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 27/55] 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 28/55] 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 29/55] 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 30/55] 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 31/55] 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 32/55] 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 33/55] 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 34/55] 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 35/55] 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 36/55] 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 37/55] 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 38/55] 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 39/55] 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 40/55] 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 41/55] 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 42/55] 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 43/55] 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 44/55] 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 45/55] 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 46/55] 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 47/55] 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 48/55] 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 49/55] 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 50/55] 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 51/55] 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 52/55] 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 53/55] 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 54/55] 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 55/55] 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;