commit 32c957255688c81f27cc935ed47738ef5a919244 Author: Anish Lakhwara Date: Fri Jun 16 19:56:17 2023 +1000 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..103bdb8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +data/* +.nbb/* diff --git a/README.md b/README.md new file mode 100644 index 0000000..7e2c341 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +##### uHabits Terminal + +A Tiny TUI that is compatible with [Loop Habits](). + +### TODO +- Parse date +- Add all entries to current date +- Read atoms from config +- Tests +- Types +- Colors for each habit +- Help menu +- Scroll bar for habits +- Save changes to file +- Add new habit +- Delete / Archive habit +- Make compatible with db +- Integrating Loop Habit "scores" +- Notifications +- Package with Nix so that it can run anywhere + +### Built With +- [nbb]() +- [nodejs]() +- [ink]() +- [reagent]() +- [parse-csv]() +- [nix]() diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..659e4c6 --- /dev/null +++ b/flake.lock @@ -0,0 +1,26 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1685351734, + "narHash": "sha256-0XzJKhXFmdkKU/y3vdQBxUjLOdMtNPBc5rNzjlVGBoo=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "a380c8b7b1f67ad2b34260061140494936c65307", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..129f21e --- /dev/null +++ b/flake.nix @@ -0,0 +1,41 @@ +{ + description = "nbb TUI Habits Tracker compatible with uHabits (android)"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs"; + }; + + outputs = { self, nixpkgs }: + let + pkgs = nixpkgs.legacyPackages.x86_64-linux; + in + { + + packages.x86_64-linux.nbb-habits = pkgs.stdenv.mkDerivation { + name = "nbb-habits"; + src = ./.; + # Need node_modules here + # Probably need some sort of wrapper of the bin too + installPhase = '' + mkdir -p $out + cp -r src/ $out/ + cp nbb.edn $out/ + ''; + }; + + defaultPackage.x86_64-linux = self.packages.x86_64-linux.nbb-habits; + devShell.x86_64-linux = pkgs.mkShell { + buildInputs = with pkgs; [ + jdk11 + nodejs + babashka + clojure + ]; + shellHook = '' + alias nbb='./node_modules/.bin/nbb' + alias run='nbb src/habits/index.cljs' + ''; + }; + }; +} + diff --git a/nbb.edn b/nbb.edn new file mode 100644 index 0000000..ccd9a31 --- /dev/null +++ b/nbb.edn @@ -0,0 +1 @@ +{:paths ["src"]} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3241bdf --- /dev/null +++ b/package-lock.json @@ -0,0 +1,565 @@ +{ + "name": "nbb-habits", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "csv-parse": "^5.4.0", + "ink": "^4.2.0", + "ink-focus": "^1.0.1", + "nbb": "^1.2.174" + } + }, + "node_modules/ansi-escapes": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", + "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", + "dependencies": { + "type-fest": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.11.0.tgz", + "integrity": "sha512-JaPw5U9ixP0XcpUbQoVSbxSDcK/K4nww20C3kjm9yE6cDRRhptU28AH60VWf9ltXmCrIfIbtt9J+2OUk2Uqiaw==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", + "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "dependencies": { + "convert-to-spaces": "^2.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/csv-parse": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.4.0.tgz", + "integrity": "sha512-JiQosUWiOFgp4hQn0an+SBoV9IKdqzhROM0iiN4LB7UpfJBlsSJlWl9nq4zGgxgMAzHJ6V4t29VAVD+3+2NJAg==" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/import-meta-resolve": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-2.2.2.tgz", + "integrity": "sha512-f8KcQ1D80V7RnqVm+/lirO9zkOxjGxhaTC1IPrBGd3MEfNgmNG67tSUO9gTi2F3Blr2Az6g1vocaxzkVnWl9MA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ink/-/ink-4.2.0.tgz", + "integrity": "sha512-q7SeFAEFMyKxTblyVI+CsxHzfiMMP9JUDG0cRmOKEAmJiYrtrDW1YYTv129RXqfn7fMKcVc4h/LbAJvqvZIuEQ==", + "dependencies": { + "ansi-escapes": "^6.0.0", + "auto-bind": "^5.0.1", + "chalk": "^5.2.0", + "cli-boxes": "^3.0.0", + "cli-cursor": "^4.0.0", + "cli-truncate": "^3.1.0", + "code-excerpt": "^4.0.0", + "indent-string": "^5.0.0", + "is-ci": "^3.0.1", + "is-lower-case": "^2.0.2", + "is-upper-case": "^2.0.2", + "lodash": "^4.17.21", + "patch-console": "^2.0.0", + "react-reconciler": "^0.29.0", + "scheduler": "^0.23.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^6.0.0", + "stack-utils": "^2.0.6", + "string-width": "^5.1.2", + "type-fest": "^0.12.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.1.0", + "ws": "^8.12.0", + "yoga-wasm-web": "~0.3.3" + }, + "engines": { + "node": ">=14.16" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "react": ">=18.0.0", + "react-devtools-core": "^4.19.1" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "node_modules/ink-focus": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ink-focus/-/ink-focus-1.0.1.tgz", + "integrity": "sha512-ddAI9GBpQWdkfklvb+JUUnPi1pVh4fKs75Wjwgi6qBlEpm9gJgF0r9QTNVw2CChjsHUDX1cJoCrlSUL5gdRQtw==", + "dependencies": { + "keypress": "^0.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ink": ">=3.0.0", + "react": ">=16.8.0" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-lower-case/-/is-lower-case-2.0.2.tgz", + "integrity": "sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/is-upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-upper-case/-/is-upper-case-2.0.2.tgz", + "integrity": "sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ==", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/keypress": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/keypress/-/keypress-0.2.1.tgz", + "integrity": "sha512-HjorDJFNhnM4SicvaUXac0X77NiskggxJdesG72+O5zBKpSqKFCrqmndKVqpu3pFqkla0St6uGk8Ju0sCurrmg==" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/nbb": { + "version": "1.2.174", + "resolved": "https://registry.npmjs.org/nbb/-/nbb-1.2.174.tgz", + "integrity": "sha512-Yw/hBgV4SHOxqrElHip+/7zKwtnxsgPqsOWgCRluMcoL0B1yB+bYx67yWH64E64REIk8u12D7LVwXGg9R/0b7g==", + "dependencies": { + "import-meta-resolve": "^2.1.0" + }, + "bin": { + "nbb": "cli.js", + "nbbun": "nbbun.js" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-console": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", + "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-reconciler": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.0.tgz", + "integrity": "sha512-wa0fGj7Zht1EYMRhKWwoo1H9GApxYLBuhoAuXN0TlltESAjDssB+Apf0T/DngVqaMyPypDmabL37vw/2aRM98Q==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/slice-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-6.0.0.tgz", + "integrity": "sha512-6bn4hRfkTvDfUoEQYkERg0BVF1D0vrX9HEkMl08uDiNWvVvjylLHvZFZWkDo6wjT8tUctbYl1nCOuE66ZTaUtA==", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz", + "integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==" + }, + "node_modules/type-fest": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.12.0.tgz", + "integrity": "sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yoga-wasm-web": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz", + "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5052a84 --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "csv-parse": "^5.4.0", + "ink": "^4.2.0", + "ink-focus": "^1.0.1", + "nbb": "^1.2.174" + } +} diff --git a/src/habits/components/table.cljs b/src/habits/components/table.cljs new file mode 100644 index 0000000..c1ef051 --- /dev/null +++ b/src/habits/components/table.cljs @@ -0,0 +1,8 @@ +(ns habits.components.table + (:require ["ink" :refer [Text Box Spacer]])) + +(defn row [] + [:> [Box + [Text "Hello"] + [Spacer] + [Text "World"]]]) diff --git a/src/habits/index.cljs b/src/habits/index.cljs new file mode 100644 index 0000000..34532a2 --- /dev/null +++ b/src/habits/index.cljs @@ -0,0 +1,194 @@ +(ns habits.index + (:require ["ink" :refer [render Box Text Spacer useInput useApp]] + ["csv-parse/sync" :as csv] + ["fs" :as fs] + [habits.components.table :as table] + [reagent.core :as r])) + +; Things This Project would benefit from +; Splitting this single file into smaller files +; Testing +; Types + Type checking +; A working LSP + +; Each habit is just a map with keys :checkmarks and :name +; We'll probably need :color and maybe a couple other things, order? +; :checkmarks is an array of maps with keys :date :result +#_(def habits-type [{:name str + :checkmarks [{:date str + :result int}]}]) + +; Config section +(defonce size (r/atom [5 3])) +(defonce scroll-index (r/atom (map dec @size))) ; Position within viewable frame defined by size +(defonce select-index (r/atom [0 0])) ; Position within all the data loaded +(defonce uhabits-csv (r/atom "./data/Loop Habits CSV 2023-05-30/Checkmarks.csv")) + +; Mock Data +(def checkmarks_data + [{:date "27/09/12" :result -1} + {:date "26/09/12" :result 2} + {:date "25/09/12" :result 1} + {:date "24/09/12" :result 0} + {:date "23/09/12" :result 0}]) + +(def meditate + {:name "meditate" + :checkmarks checkmarks_data}) + +(def exercise + {:name "exercise" + :checkmarks checkmarks_data}) + +; CSV Parsing +(defn uhabits-data [csv-file] + (js->clj (csv/parse (str (fs/readFileSync csv-file)) {:columns true}))) + +(defn parse-row [habits row] + (let [date (first row) + values (drop-last (rest row)) + checkmarks (->> values + (mapv (fn [v] {:result (js/parseInt v) :date date})))] + (for [[index habit] (map-indexed vector habits)] + (assoc habit :checkmarks (conj (:checkmarks habit) (nth checkmarks index)))))) + +(defn make-habits [csv] + (let [headers (drop-last (drop 1 (first csv))) + data (take 20 (rest csv)) + habits (mapv (fn [x] {:name x :checkmarks []}) headers)] + (vec (reduce parse-row habits data)))) + +(def my-data (make-habits (uhabits-data @uhabits-csv))) + +; Data manipulation +(defonce habits (r/atom my-data)) + +(defn get-dates [habits] + (let [dates (->> habits + (first) + (:checkmarks) + (mapv #(get % :date)))] + dates)) + +(defn toggle-habits [habits] + (let [selected @select-index + scrolled @scroll-index + pos (->> @size + (map #(- % 1)) + (map - scrolled)) + [i j] (map + pos selected) + habit (nth habits i) + checkmarks (:checkmarks habit) + checkmark (nth checkmarks j) + new-checkmark (if (<= (:result checkmark) 0) 1 0) + new-checkmarks (assoc checkmarks j {:date (:date checkmark) :result new-checkmark}) + new-habits (assoc habits i {:name (:name habit) + :checkmarks new-checkmarks})] + new-habits)) + +(defn filter-checkmarks [checkmarks] + (let [[_ date-size] @size + [_ y] @scroll-index + y-adj (if (>= y date-size) (- y (- date-size 1)) 0)] + (->> checkmarks + (drop y-adj) + (take date-size)))) + +(defn filter-habits [habits] + (let [[habit-size _] @size + [x _] @scroll-index + x-adj (if (>= x habit-size) (- x (- habit-size 1)) 0) + filtered-habits (->> habits + (drop x-adj) + (take habit-size) + (map #(assoc % :checkmarks (filter-checkmarks (:checkmarks %)))))] + filtered-habits)) + +(defn update-index [dir] + (let [[habit-size date-size] @size + habit-limit (- habit-size 1) + date-limit (- date-size 1) + [cur-x cur-y] @select-index] + (case dir + "down" (doall (swap! scroll-index (fn [[x y]] (if (>= cur-x habit-limit) [(inc x) y] [x y]))) + (swap! select-index (fn [[x y]] (if (>= x habit-limit) [habit-limit y] [(inc x) y])))) + "up" (doall (swap! scroll-index (fn [[x y]] (if (= cur-x 0) + (if (<= x habit-limit) + [habit-limit y] + [(dec x) y]) + [x y]))) + (swap! select-index (fn [[x y]] (if (<= x 0) [0 y] [(dec x) y])))) + "right" (doall + (swap! scroll-index (fn [[x y]] (if (>= cur-y date-limit) [x (inc y)] [x y]))) + (swap! select-index (fn [[x y]] (if (>= y date-limit) [x date-limit] [x (inc y)])))) + "left" (doall (swap! scroll-index (fn [[x y]] (if (= cur-y 0) + (if (<= y date-limit) + [x date-limit] + [x (dec y)]) + [x y]))) + (swap! select-index (fn [[x y]] (if (<= y 0) [x 0] [x (dec y)]))))))) + +; UI +(defn headers [dates] + [:> Box {:width "100%" :alignItems "center"} + [:> Spacer] + (for [date dates] + [:<> {:key (str "header:" date)} + [:> Box {:minWidth 10} + [:> Spacer]] + [:> Text date] + [:> Spacer]])]) + +(defn check-box [isChecked isSelected] + (let [mark (if (>= isChecked 1) "✔" "✗")] + (if isSelected + [:> Text {:color "yellow"} mark] + [:> Text mark]))) + +(defn row [checkmarks habit j] + [:> Box {:borderStyle "round" :width "100%" :paddingRight 2 :alignItems "center" :flexGrow 1} + (for [[index checkmark] (map-indexed vector checkmarks)] + (let [isSelected (= j index)] + [:<> {:key (str "row:" habit (:date checkmark))} + [:> Spacer] + (check-box (:result checkmark) isSelected) + [:> Spacer]]))]) + +(defn column [habits i j] + [:> Box {:borderStyle "round" :flexDirection "column" :columnGap 1} + (headers (get-dates habits)) + (for [[index habit] (map-indexed vector habits)] + (let [checkmarks (:checkmarks habit) + n (:name habit)] + [:> Box {:key (str "box:row:" n) :alignItems "flex-end"} + (if (= index i) + [:<> {:key (str "row:" n)} + [:> Box {:minWidth 10 :alignSelf "center"} + [:> Text {:color "yellow"} n]] + (row checkmarks n j)] + [:<> {:key (str "row:" n)} + [:> Box {:minWidth 10 :alignSelf "center"} + [:> Text n]] + (row checkmarks n false)]) + [:> Spacer]]))]) + +(defn selector [] + (let [[i j] @select-index + habits_ (filter-habits @habits) + exit (useApp)] + (useInput (fn [input k] + (if (= input "q") + (. exit exit)) + (if (= input " ") + (swap! habits toggle-habits)) + (if k.downArrow + (update-index "down")) + (if k.upArrow + (update-index "up")) + (if k.rightArrow + (update-index "right")) + (if k.leftArrow + (update-index "left")))) + (column habits_ i j))) + +(render (r/as-element [:f> selector]))