├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── dijo.1 ├── flake.lock ├── flake.nix ├── notes.txt ├── readme.md ├── rustfmt.toml └── src ├── app ├── cursor.rs ├── impl_self.rs ├── impl_view.rs ├── message.rs └── mod.rs ├── command.rs ├── habit ├── bit.rs ├── count.rs ├── float.rs ├── mod.rs ├── prelude.rs └── traits.rs ├── keybinds.rs ├── main.rs ├── theme.rs ├── utils.rs └── views.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | liberapay: nerdypepper 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | target-branch: master 9 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | rustfmt: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - run: rustup component add rustfmt 14 | - run: cargo fmt -- --check 15 | 16 | build-linux: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v1 22 | # cache the build assets so they dont recompile every time. 23 | - name: Cache Rust dependencies 24 | uses: actions/cache@v1.0.1 25 | with: 26 | path: target 27 | key: ${{ runner.OS }}-build-${{ hashFiles('**/Cargo.lock') }} 28 | restore-keys: | 29 | ${{ runner.OS }}-build- 30 | - name: Install latest rust toolchain 31 | uses: actions-rs/toolchain@v1 32 | with: 33 | toolchain: beta 34 | default: true 35 | override: true 36 | - name: Install system dependencies 37 | run: | 38 | sudo apt-get update \ 39 | && sudo apt-get install -y \ 40 | libdbus-1-dev libncurses5-dev libncursesw5-dev 41 | - name: Build 42 | run: cargo build --release && strip target/release/dijo 43 | 44 | - name: Upload binaries to release 45 | uses: svenstaro/upload-release-action@v1-release 46 | with: 47 | repo_token: ${{ secrets.GITHUB_TOKEN }} 48 | file: target/release/dijo 49 | asset_name: dijo-x86_64-linux 50 | tag: ${{ github.ref }} 51 | overwrite: true 52 | 53 | build-apple: 54 | runs-on: macos-latest 55 | 56 | steps: 57 | - name: Checkout 58 | uses: actions/checkout@v1 59 | - name: Cache Rust dependencies 60 | uses: actions/cache@v1.0.1 61 | with: 62 | path: target 63 | key: ${{ runner.OS }}-build-${{ hashFiles('**/Cargo.lock') }} 64 | restore-keys: | 65 | ${{ runner.OS }}-build- 66 | - name: Install latest rust toolchain 67 | uses: actions-rs/toolchain@v1 68 | with: 69 | toolchain: beta 70 | target: x86_64-apple-darwin 71 | default: true 72 | override: true 73 | 74 | - name: Build for mac 75 | run: cargo build --release && strip target/release/dijo 76 | 77 | - name: Upload binaries to release 78 | uses: svenstaro/upload-release-action@v1-release 79 | with: 80 | repo_token: ${{ secrets.GITHUB_TOKEN }} 81 | file: target/release/dijo 82 | asset_name: dijo-x86_64-apple 83 | tag: ${{ github.ref }} 84 | overwrite: true 85 | 86 | build-windows: 87 | runs-on: windows-latest 88 | 89 | steps: 90 | - name: Checkout 91 | uses: actions/checkout@v1 92 | - name: Cache Rust dependencies 93 | uses: actions/cache@v1.0.1 94 | with: 95 | path: target 96 | key: ${{ runner.OS }}-build-${{ hashFiles('**/Cargo.lock') }} 97 | restore-keys: | 98 | ${{ runner.OS }}-build- 99 | - name: Install latest rust toolchain 100 | uses: actions-rs/toolchain@v1 101 | with: 102 | toolchain: beta 103 | target: x86_64-pc-windows-msvc 104 | default: true 105 | override: true 106 | 107 | - name: Build for windows 108 | shell: bash 109 | run: cargo build --release --no-default-features --features "crossterm-backend" 110 | 111 | - name: Upload binaries to release 112 | uses: svenstaro/upload-release-action@v1-release 113 | with: 114 | repo_token: ${{ secrets.GITHUB_TOKEN }} 115 | file: target/release/dijo.exe 116 | asset_name: dijo-x86_64-windows.exe 117 | tag: ${{ github.ref }} 118 | overwrite: true 119 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | # ignore serialized files 9 | *.json 10 | 11 | # ignore logs 12 | *.txt 13 | 14 | # ignore session 15 | 16 | *.vim 17 | .envrc 18 | .direnv 19 | result 20 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.7.6" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" 10 | dependencies = [ 11 | "getrandom 0.2.3", 12 | "once_cell", 13 | "version_check", 14 | ] 15 | 16 | [[package]] 17 | name = "ansi_term" 18 | version = "0.11.0" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 21 | dependencies = [ 22 | "winapi 0.3.9", 23 | ] 24 | 25 | [[package]] 26 | name = "arrayref" 27 | version = "0.3.6" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" 30 | 31 | [[package]] 32 | name = "arrayvec" 33 | version = "0.5.2" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" 36 | 37 | [[package]] 38 | name = "atty" 39 | version = "0.2.14" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 42 | dependencies = [ 43 | "hermit-abi", 44 | "libc", 45 | "winapi 0.3.9", 46 | ] 47 | 48 | [[package]] 49 | name = "autocfg" 50 | version = "1.0.1" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 53 | 54 | [[package]] 55 | name = "base64" 56 | version = "0.13.0" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 59 | 60 | [[package]] 61 | name = "bitflags" 62 | version = "1.3.2" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 65 | 66 | [[package]] 67 | name = "blake2b_simd" 68 | version = "0.5.11" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" 71 | dependencies = [ 72 | "arrayref", 73 | "arrayvec", 74 | "constant_time_eq", 75 | ] 76 | 77 | [[package]] 78 | name = "cfg-if" 79 | version = "0.1.10" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 82 | 83 | [[package]] 84 | name = "cfg-if" 85 | version = "1.0.0" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 88 | 89 | [[package]] 90 | name = "chrono" 91 | version = "0.4.19" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 94 | dependencies = [ 95 | "libc", 96 | "num-integer", 97 | "num-traits", 98 | "serde", 99 | "time 0.1.43", 100 | "winapi 0.3.9", 101 | ] 102 | 103 | [[package]] 104 | name = "clap" 105 | version = "2.33.3" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 108 | dependencies = [ 109 | "ansi_term", 110 | "atty", 111 | "bitflags", 112 | "strsim 0.8.0", 113 | "textwrap", 114 | "unicode-width", 115 | "vec_map", 116 | ] 117 | 118 | [[package]] 119 | name = "constant_time_eq" 120 | version = "0.1.5" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" 123 | 124 | [[package]] 125 | name = "crossbeam-channel" 126 | version = "0.5.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" 129 | dependencies = [ 130 | "cfg-if 1.0.0", 131 | "crossbeam-utils", 132 | ] 133 | 134 | [[package]] 135 | name = "crossbeam-utils" 136 | version = "0.8.1" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d" 139 | dependencies = [ 140 | "autocfg", 141 | "cfg-if 1.0.0", 142 | "lazy_static", 143 | ] 144 | 145 | [[package]] 146 | name = "crossterm" 147 | version = "0.22.1" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c" 150 | dependencies = [ 151 | "bitflags", 152 | "crossterm_winapi", 153 | "libc", 154 | "mio 0.7.7", 155 | "parking_lot", 156 | "signal-hook", 157 | "signal-hook-mio", 158 | "winapi 0.3.9", 159 | ] 160 | 161 | [[package]] 162 | name = "crossterm_winapi" 163 | version = "0.9.0" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" 166 | dependencies = [ 167 | "winapi 0.3.9", 168 | ] 169 | 170 | [[package]] 171 | name = "ctor" 172 | version = "0.1.17" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "373c88d9506e2e9230f6107701b7d8425f4cb3f6df108ec3042a26e936666da5" 175 | dependencies = [ 176 | "quote", 177 | "syn", 178 | ] 179 | 180 | [[package]] 181 | name = "cursive" 182 | version = "0.17.0" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "ca536d245342f6c005e7547ab640e444a3dc2fc0319a92124c8c1cbff025e775" 185 | dependencies = [ 186 | "ahash", 187 | "cfg-if 1.0.0", 188 | "crossbeam-channel", 189 | "crossterm", 190 | "cursive_core", 191 | "lazy_static", 192 | "libc", 193 | "log", 194 | "signal-hook", 195 | "termion", 196 | "unicode-segmentation", 197 | "unicode-width", 198 | ] 199 | 200 | [[package]] 201 | name = "cursive_core" 202 | version = "0.3.1" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "e27fbda42833e46148ff28db338f6189a4407e4a38ba1f4105e2f623089e66a0" 205 | dependencies = [ 206 | "ahash", 207 | "crossbeam-channel", 208 | "enum-map", 209 | "enumset", 210 | "lazy_static", 211 | "log", 212 | "num", 213 | "owning_ref", 214 | "time 0.3.5", 215 | "unicode-segmentation", 216 | "unicode-width", 217 | "xi-unicode", 218 | ] 219 | 220 | [[package]] 221 | name = "darling" 222 | version = "0.12.4" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c" 225 | dependencies = [ 226 | "darling_core", 227 | "darling_macro", 228 | ] 229 | 230 | [[package]] 231 | name = "darling_core" 232 | version = "0.12.4" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36" 235 | dependencies = [ 236 | "fnv", 237 | "ident_case", 238 | "proc-macro2", 239 | "quote", 240 | "strsim 0.10.0", 241 | "syn", 242 | ] 243 | 244 | [[package]] 245 | name = "darling_macro" 246 | version = "0.12.4" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a" 249 | dependencies = [ 250 | "darling_core", 251 | "quote", 252 | "syn", 253 | ] 254 | 255 | [[package]] 256 | name = "dijo" 257 | version = "0.2.7" 258 | dependencies = [ 259 | "chrono", 260 | "clap", 261 | "cursive", 262 | "directories", 263 | "erased-serde", 264 | "lazy_static", 265 | "notify", 266 | "serde", 267 | "serde_json", 268 | "syn", 269 | "toml", 270 | "typetag", 271 | ] 272 | 273 | [[package]] 274 | name = "directories" 275 | version = "3.0.1" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "f8fed639d60b58d0f53498ab13d26f621fd77569cc6edb031f4cc36a2ad9da0f" 278 | dependencies = [ 279 | "dirs-sys", 280 | ] 281 | 282 | [[package]] 283 | name = "dirs-sys" 284 | version = "0.3.5" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" 287 | dependencies = [ 288 | "libc", 289 | "redox_users", 290 | "winapi 0.3.9", 291 | ] 292 | 293 | [[package]] 294 | name = "enum-map" 295 | version = "2.0.0" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "9ec3484df47a85c121b9d8fbf265ca7eedc26a5c4c341db7cf800876201c766f" 298 | dependencies = [ 299 | "enum-map-derive", 300 | ] 301 | 302 | [[package]] 303 | name = "enum-map-derive" 304 | version = "0.7.0" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "8182c0d26a908f001a23adc388fcef7fde884fbaf668874126cd5a3c13ca299e" 307 | dependencies = [ 308 | "proc-macro2", 309 | "quote", 310 | "syn", 311 | ] 312 | 313 | [[package]] 314 | name = "enumset" 315 | version = "1.0.6" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "fbd795df6708a599abf1ee10eacc72efd052b7a5f70fdf0715e4d5151a6db9c3" 318 | dependencies = [ 319 | "enumset_derive", 320 | ] 321 | 322 | [[package]] 323 | name = "enumset_derive" 324 | version = "0.5.4" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "e19c52f9ec503c8a68dc04daf71a04b07e690c32ab1a8b68e33897f255269d47" 327 | dependencies = [ 328 | "darling", 329 | "proc-macro2", 330 | "quote", 331 | "syn", 332 | ] 333 | 334 | [[package]] 335 | name = "erased-serde" 336 | version = "0.3.13" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "0465971a8cc1fa2455c8465aaa377131e1f1cf4983280f474a13e68793aa770c" 339 | dependencies = [ 340 | "serde", 341 | ] 342 | 343 | [[package]] 344 | name = "filetime" 345 | version = "0.2.13" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "0c122a393ea57648015bf06fbd3d372378992e86b9ff5a7a497b076a28c79efe" 348 | dependencies = [ 349 | "cfg-if 1.0.0", 350 | "libc", 351 | "redox_syscall", 352 | "winapi 0.3.9", 353 | ] 354 | 355 | [[package]] 356 | name = "fnv" 357 | version = "1.0.7" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 360 | 361 | [[package]] 362 | name = "fsevent" 363 | version = "0.4.0" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6" 366 | dependencies = [ 367 | "bitflags", 368 | "fsevent-sys", 369 | ] 370 | 371 | [[package]] 372 | name = "fsevent-sys" 373 | version = "2.0.1" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0" 376 | dependencies = [ 377 | "libc", 378 | ] 379 | 380 | [[package]] 381 | name = "fuchsia-zircon" 382 | version = "0.3.3" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" 385 | dependencies = [ 386 | "bitflags", 387 | "fuchsia-zircon-sys", 388 | ] 389 | 390 | [[package]] 391 | name = "fuchsia-zircon-sys" 392 | version = "0.3.3" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" 395 | 396 | [[package]] 397 | name = "getrandom" 398 | version = "0.1.16" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" 401 | dependencies = [ 402 | "cfg-if 1.0.0", 403 | "libc", 404 | "wasi 0.9.0+wasi-snapshot-preview1", 405 | ] 406 | 407 | [[package]] 408 | name = "getrandom" 409 | version = "0.2.3" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" 412 | dependencies = [ 413 | "cfg-if 1.0.0", 414 | "libc", 415 | "wasi 0.10.1+wasi-snapshot-preview1", 416 | ] 417 | 418 | [[package]] 419 | name = "ghost" 420 | version = "0.1.2" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "1a5bcf1bbeab73aa4cf2fde60a846858dc036163c7c33bec309f8d17de785479" 423 | dependencies = [ 424 | "proc-macro2", 425 | "quote", 426 | "syn", 427 | ] 428 | 429 | [[package]] 430 | name = "hermit-abi" 431 | version = "0.1.17" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" 434 | dependencies = [ 435 | "libc", 436 | ] 437 | 438 | [[package]] 439 | name = "ident_case" 440 | version = "1.0.1" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 443 | 444 | [[package]] 445 | name = "inotify" 446 | version = "0.7.1" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f" 449 | dependencies = [ 450 | "bitflags", 451 | "inotify-sys", 452 | "libc", 453 | ] 454 | 455 | [[package]] 456 | name = "inotify-sys" 457 | version = "0.1.4" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "c4563555856585ab3180a5bf0b2f9f8d301a728462afffc8195b3f5394229c55" 460 | dependencies = [ 461 | "libc", 462 | ] 463 | 464 | [[package]] 465 | name = "instant" 466 | version = "0.1.9" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" 469 | dependencies = [ 470 | "cfg-if 1.0.0", 471 | ] 472 | 473 | [[package]] 474 | name = "inventory" 475 | version = "0.1.10" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "0f0f7efb804ec95e33db9ad49e4252f049e37e8b0a4652e3cd61f7999f2eff7f" 478 | dependencies = [ 479 | "ctor", 480 | "ghost", 481 | "inventory-impl", 482 | ] 483 | 484 | [[package]] 485 | name = "inventory-impl" 486 | version = "0.1.10" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "75c094e94816723ab936484666968f5b58060492e880f3c8d00489a1e244fa51" 489 | dependencies = [ 490 | "proc-macro2", 491 | "quote", 492 | "syn", 493 | ] 494 | 495 | [[package]] 496 | name = "iovec" 497 | version = "0.1.4" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" 500 | dependencies = [ 501 | "libc", 502 | ] 503 | 504 | [[package]] 505 | name = "itoa" 506 | version = "0.4.7" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" 509 | 510 | [[package]] 511 | name = "kernel32-sys" 512 | version = "0.2.2" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 515 | dependencies = [ 516 | "winapi 0.2.8", 517 | "winapi-build", 518 | ] 519 | 520 | [[package]] 521 | name = "lazy_static" 522 | version = "1.4.0" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 525 | 526 | [[package]] 527 | name = "lazycell" 528 | version = "1.3.0" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 531 | 532 | [[package]] 533 | name = "libc" 534 | version = "0.2.112" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" 537 | 538 | [[package]] 539 | name = "lock_api" 540 | version = "0.4.2" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" 543 | dependencies = [ 544 | "scopeguard", 545 | ] 546 | 547 | [[package]] 548 | name = "log" 549 | version = "0.4.11" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" 552 | dependencies = [ 553 | "cfg-if 0.1.10", 554 | ] 555 | 556 | [[package]] 557 | name = "mio" 558 | version = "0.6.23" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" 561 | dependencies = [ 562 | "cfg-if 0.1.10", 563 | "fuchsia-zircon", 564 | "fuchsia-zircon-sys", 565 | "iovec", 566 | "kernel32-sys", 567 | "libc", 568 | "log", 569 | "miow 0.2.2", 570 | "net2", 571 | "slab", 572 | "winapi 0.2.8", 573 | ] 574 | 575 | [[package]] 576 | name = "mio" 577 | version = "0.7.7" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "e50ae3f04d169fcc9bde0b547d1c205219b7157e07ded9c5aff03e0637cb3ed7" 580 | dependencies = [ 581 | "libc", 582 | "log", 583 | "miow 0.3.6", 584 | "ntapi", 585 | "winapi 0.3.9", 586 | ] 587 | 588 | [[package]] 589 | name = "mio-extras" 590 | version = "2.0.6" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" 593 | dependencies = [ 594 | "lazycell", 595 | "log", 596 | "mio 0.6.23", 597 | "slab", 598 | ] 599 | 600 | [[package]] 601 | name = "miow" 602 | version = "0.2.2" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" 605 | dependencies = [ 606 | "kernel32-sys", 607 | "net2", 608 | "winapi 0.2.8", 609 | "ws2_32-sys", 610 | ] 611 | 612 | [[package]] 613 | name = "miow" 614 | version = "0.3.6" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897" 617 | dependencies = [ 618 | "socket2", 619 | "winapi 0.3.9", 620 | ] 621 | 622 | [[package]] 623 | name = "net2" 624 | version = "0.2.37" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" 627 | dependencies = [ 628 | "cfg-if 0.1.10", 629 | "libc", 630 | "winapi 0.3.9", 631 | ] 632 | 633 | [[package]] 634 | name = "notify" 635 | version = "4.0.15" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "80ae4a7688d1fab81c5bf19c64fc8db920be8d519ce6336ed4e7efe024724dbd" 638 | dependencies = [ 639 | "bitflags", 640 | "filetime", 641 | "fsevent", 642 | "fsevent-sys", 643 | "inotify", 644 | "libc", 645 | "mio 0.6.23", 646 | "mio-extras", 647 | "walkdir", 648 | "winapi 0.3.9", 649 | ] 650 | 651 | [[package]] 652 | name = "ntapi" 653 | version = "0.3.6" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" 656 | dependencies = [ 657 | "winapi 0.3.9", 658 | ] 659 | 660 | [[package]] 661 | name = "num" 662 | version = "0.4.0" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" 665 | dependencies = [ 666 | "num-complex", 667 | "num-integer", 668 | "num-iter", 669 | "num-rational", 670 | "num-traits", 671 | ] 672 | 673 | [[package]] 674 | name = "num-complex" 675 | version = "0.4.0" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085" 678 | dependencies = [ 679 | "num-traits", 680 | ] 681 | 682 | [[package]] 683 | name = "num-integer" 684 | version = "0.1.44" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 687 | dependencies = [ 688 | "autocfg", 689 | "num-traits", 690 | ] 691 | 692 | [[package]] 693 | name = "num-iter" 694 | version = "0.1.42" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" 697 | dependencies = [ 698 | "autocfg", 699 | "num-integer", 700 | "num-traits", 701 | ] 702 | 703 | [[package]] 704 | name = "num-rational" 705 | version = "0.4.0" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" 708 | dependencies = [ 709 | "autocfg", 710 | "num-integer", 711 | "num-traits", 712 | ] 713 | 714 | [[package]] 715 | name = "num-traits" 716 | version = "0.2.14" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 719 | dependencies = [ 720 | "autocfg", 721 | ] 722 | 723 | [[package]] 724 | name = "numtoa" 725 | version = "0.1.0" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" 728 | 729 | [[package]] 730 | name = "once_cell" 731 | version = "1.9.0" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" 734 | 735 | [[package]] 736 | name = "owning_ref" 737 | version = "0.4.1" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce" 740 | dependencies = [ 741 | "stable_deref_trait", 742 | ] 743 | 744 | [[package]] 745 | name = "parking_lot" 746 | version = "0.11.1" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" 749 | dependencies = [ 750 | "instant", 751 | "lock_api", 752 | "parking_lot_core", 753 | ] 754 | 755 | [[package]] 756 | name = "parking_lot_core" 757 | version = "0.8.2" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "9ccb628cad4f84851442432c60ad8e1f607e29752d0bf072cbd0baf28aa34272" 760 | dependencies = [ 761 | "cfg-if 1.0.0", 762 | "instant", 763 | "libc", 764 | "redox_syscall", 765 | "smallvec", 766 | "winapi 0.3.9", 767 | ] 768 | 769 | [[package]] 770 | name = "proc-macro2" 771 | version = "1.0.24" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" 774 | dependencies = [ 775 | "unicode-xid", 776 | ] 777 | 778 | [[package]] 779 | name = "quote" 780 | version = "1.0.8" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" 783 | dependencies = [ 784 | "proc-macro2", 785 | ] 786 | 787 | [[package]] 788 | name = "redox_syscall" 789 | version = "0.1.57" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" 792 | 793 | [[package]] 794 | name = "redox_termios" 795 | version = "0.1.1" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" 798 | dependencies = [ 799 | "redox_syscall", 800 | ] 801 | 802 | [[package]] 803 | name = "redox_users" 804 | version = "0.3.5" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" 807 | dependencies = [ 808 | "getrandom 0.1.16", 809 | "redox_syscall", 810 | "rust-argon2", 811 | ] 812 | 813 | [[package]] 814 | name = "rust-argon2" 815 | version = "0.8.3" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" 818 | dependencies = [ 819 | "base64", 820 | "blake2b_simd", 821 | "constant_time_eq", 822 | "crossbeam-utils", 823 | ] 824 | 825 | [[package]] 826 | name = "ryu" 827 | version = "1.0.5" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 830 | 831 | [[package]] 832 | name = "same-file" 833 | version = "1.0.6" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 836 | dependencies = [ 837 | "winapi-util", 838 | ] 839 | 840 | [[package]] 841 | name = "scopeguard" 842 | version = "1.1.0" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 845 | 846 | [[package]] 847 | name = "serde" 848 | version = "1.0.118" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800" 851 | dependencies = [ 852 | "serde_derive", 853 | ] 854 | 855 | [[package]] 856 | name = "serde_derive" 857 | version = "1.0.118" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df" 860 | dependencies = [ 861 | "proc-macro2", 862 | "quote", 863 | "syn", 864 | ] 865 | 866 | [[package]] 867 | name = "serde_json" 868 | version = "1.0.61" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a" 871 | dependencies = [ 872 | "itoa", 873 | "ryu", 874 | "serde", 875 | ] 876 | 877 | [[package]] 878 | name = "signal-hook" 879 | version = "0.3.13" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" 882 | dependencies = [ 883 | "libc", 884 | "signal-hook-registry", 885 | ] 886 | 887 | [[package]] 888 | name = "signal-hook-mio" 889 | version = "0.2.1" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4" 892 | dependencies = [ 893 | "libc", 894 | "mio 0.7.7", 895 | "signal-hook", 896 | ] 897 | 898 | [[package]] 899 | name = "signal-hook-registry" 900 | version = "1.4.0" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 903 | dependencies = [ 904 | "libc", 905 | ] 906 | 907 | [[package]] 908 | name = "slab" 909 | version = "0.4.2" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" 912 | 913 | [[package]] 914 | name = "smallvec" 915 | version = "1.6.1" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" 918 | 919 | [[package]] 920 | name = "socket2" 921 | version = "0.3.19" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" 924 | dependencies = [ 925 | "cfg-if 1.0.0", 926 | "libc", 927 | "winapi 0.3.9", 928 | ] 929 | 930 | [[package]] 931 | name = "stable_deref_trait" 932 | version = "1.2.0" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 935 | 936 | [[package]] 937 | name = "strsim" 938 | version = "0.8.0" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 941 | 942 | [[package]] 943 | name = "strsim" 944 | version = "0.10.0" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 947 | 948 | [[package]] 949 | name = "syn" 950 | version = "1.0.57" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "4211ce9909eb971f111059df92c45640aad50a619cf55cd76476be803c4c68e6" 953 | dependencies = [ 954 | "proc-macro2", 955 | "quote", 956 | "unicode-xid", 957 | ] 958 | 959 | [[package]] 960 | name = "termion" 961 | version = "1.5.5" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "c22cec9d8978d906be5ac94bceb5a010d885c626c4c8855721a4dbd20e3ac905" 964 | dependencies = [ 965 | "libc", 966 | "numtoa", 967 | "redox_syscall", 968 | "redox_termios", 969 | ] 970 | 971 | [[package]] 972 | name = "textwrap" 973 | version = "0.11.0" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 976 | dependencies = [ 977 | "unicode-width", 978 | ] 979 | 980 | [[package]] 981 | name = "time" 982 | version = "0.1.43" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" 985 | dependencies = [ 986 | "libc", 987 | "winapi 0.3.9", 988 | ] 989 | 990 | [[package]] 991 | name = "time" 992 | version = "0.3.5" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad" 995 | dependencies = [ 996 | "itoa", 997 | "libc", 998 | ] 999 | 1000 | [[package]] 1001 | name = "toml" 1002 | version = "0.5.8" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" 1005 | dependencies = [ 1006 | "serde", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "typetag" 1011 | version = "0.1.6" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "83b97b107d25d29de6879ac4f676ac5bfea92bdd01f206e995794493f1fc2e32" 1014 | dependencies = [ 1015 | "erased-serde", 1016 | "inventory", 1017 | "lazy_static", 1018 | "serde", 1019 | "typetag-impl", 1020 | ] 1021 | 1022 | [[package]] 1023 | name = "typetag-impl" 1024 | version = "0.1.6" 1025 | source = "registry+https://github.com/rust-lang/crates.io-index" 1026 | checksum = "3f2466fc87b07b800a5060f89ba579d6882f7a03ac21363e4737764aaf9f99f9" 1027 | dependencies = [ 1028 | "proc-macro2", 1029 | "quote", 1030 | "syn", 1031 | ] 1032 | 1033 | [[package]] 1034 | name = "unicode-segmentation" 1035 | version = "1.7.1" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" 1038 | 1039 | [[package]] 1040 | name = "unicode-width" 1041 | version = "0.1.8" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 1044 | 1045 | [[package]] 1046 | name = "unicode-xid" 1047 | version = "0.2.1" 1048 | source = "registry+https://github.com/rust-lang/crates.io-index" 1049 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 1050 | 1051 | [[package]] 1052 | name = "vec_map" 1053 | version = "0.8.2" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 1056 | 1057 | [[package]] 1058 | name = "version_check" 1059 | version = "0.9.2" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" 1062 | 1063 | [[package]] 1064 | name = "walkdir" 1065 | version = "2.3.1" 1066 | source = "registry+https://github.com/rust-lang/crates.io-index" 1067 | checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" 1068 | dependencies = [ 1069 | "same-file", 1070 | "winapi 0.3.9", 1071 | "winapi-util", 1072 | ] 1073 | 1074 | [[package]] 1075 | name = "wasi" 1076 | version = "0.9.0+wasi-snapshot-preview1" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 1079 | 1080 | [[package]] 1081 | name = "wasi" 1082 | version = "0.10.1+wasi-snapshot-preview1" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "93c6c3420963c5c64bca373b25e77acb562081b9bb4dd5bb864187742186cea9" 1085 | 1086 | [[package]] 1087 | name = "winapi" 1088 | version = "0.2.8" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 1091 | 1092 | [[package]] 1093 | name = "winapi" 1094 | version = "0.3.9" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1097 | dependencies = [ 1098 | "winapi-i686-pc-windows-gnu", 1099 | "winapi-x86_64-pc-windows-gnu", 1100 | ] 1101 | 1102 | [[package]] 1103 | name = "winapi-build" 1104 | version = "0.1.1" 1105 | source = "registry+https://github.com/rust-lang/crates.io-index" 1106 | checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 1107 | 1108 | [[package]] 1109 | name = "winapi-i686-pc-windows-gnu" 1110 | version = "0.4.0" 1111 | source = "registry+https://github.com/rust-lang/crates.io-index" 1112 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1113 | 1114 | [[package]] 1115 | name = "winapi-util" 1116 | version = "0.1.5" 1117 | source = "registry+https://github.com/rust-lang/crates.io-index" 1118 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1119 | dependencies = [ 1120 | "winapi 0.3.9", 1121 | ] 1122 | 1123 | [[package]] 1124 | name = "winapi-x86_64-pc-windows-gnu" 1125 | version = "0.4.0" 1126 | source = "registry+https://github.com/rust-lang/crates.io-index" 1127 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1128 | 1129 | [[package]] 1130 | name = "ws2_32-sys" 1131 | version = "0.2.1" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" 1134 | dependencies = [ 1135 | "winapi 0.2.8", 1136 | "winapi-build", 1137 | ] 1138 | 1139 | [[package]] 1140 | name = "xi-unicode" 1141 | version = "0.3.0" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" 1144 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dijo" 3 | version = "0.2.7" 4 | authors = ["Akshay "] 5 | edition = "2018" 6 | description = "Scriptable, curses-based, digital habit tracker" 7 | homepage = "https://github.com/nerdypepper/dijo" 8 | repository = "https://github.com/nerdypepper/dijo" 9 | readme = './readme.md' 10 | keywords = ["tracker", "event-tracker", "tui", "journal"] 11 | categories = ["date-and-time", "command-line-interface"] 12 | license = "MIT" 13 | 14 | [dependencies] 15 | serde_json = "1.0" 16 | lazy_static = "1.4.0" 17 | erased-serde = "0.3" 18 | typetag = "0.1.4" 19 | directories = "3.0.1" 20 | clap = "2.33" 21 | notify = "4.0" 22 | toml = "0.5.6" 23 | syn = "=1.0.57" 24 | 25 | [dependencies.cursive] 26 | version = "0.17" 27 | default-features = false 28 | 29 | [dependencies.chrono] 30 | version = "0.4" 31 | features = ["serde"] 32 | 33 | [dependencies.serde] 34 | version = "1.0.103" 35 | features = ["derive"] 36 | 37 | [features] 38 | default = ["termion-backend"] 39 | termion-backend = ["cursive/termion-backend"] 40 | crossterm-backend = ["cursive/crossterm-backend"] 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Akshay Oppiliappan (nerdy@peppe.rs) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /dijo.1: -------------------------------------------------------------------------------- 1 | .TH DIJO 1 "January 26, 2021" dijo-0.2.7 2 | 3 | .SH NAME 4 | dijo \- digital journal 5 | 6 | .SH SYNOPSIS 7 | .B dijo 8 | [\fBFLAGS\fR] 9 | [\fBOPTIONS\fR] 10 | 11 | .SH DESCRIPTION 12 | .B dijo 13 | is scriptable, curses-based and modal, much like \fIvim\fR, digital habit tracker 14 | 15 | .SH FLAGS 16 | .TP 17 | .BR \-h ", " \-\-help 18 | Prints help information 19 | .TP 20 | .BR \-l ", " \-\-list 21 | List dijo habits 22 | .TP 23 | .BR \-V ", " \-\-version 24 | Prints version information 25 | 26 | .SH OPTIONS 27 | .TP 28 | .BR \-c ", " \-\-command " " \fIcommand 29 | Run a dijo command 30 | 31 | .SH FEATURES 32 | .TP 33 | \(bu \fBvim like motions\fR 34 | Navigate with \fBhjkl\fR! 35 | .TP 36 | \(bu \fBdijo is modal\fR 37 | Different modes to view different stats! 38 | .TP 39 | \(bu \fBvim like command mode\fR 40 | Add with \fB:add\fR, delete with \fB:delete\fR and above all, quit with \fB:q!\fR. 41 | .TP 42 | \(bu \fBFully scriptable\fR 43 | You can configure it to track your \fIgit\fR commits! 44 | 45 | .SH GETTING STARTED 46 | .PP 47 | On running \fBdijo\fR at the command line, you are presented with a blank screen. This is the \fBDAY\fR mode, analogous to the \fBNORMAL\fR mode in Vim. In this mode, you may view stats for every day of the current month. 48 | .PP 49 | To start tracking habits, enter the command mode with \fB:.\fR Use the add command to add a habit to begin tracking. 50 | .TP 51 | Say, I would like to track the number of French lessons I took on Duolingo, and I aim to do 5 lessons each day: 52 | .nf 53 | \fB 54 | :add french 5 55 | | | 56 | `-----|----- habit name 57 | `---- goal (optional) 58 | \fR 59 | .fi 60 | .TP 61 | Go ahead and add a couple more habits the same way: 62 | .nf 63 | \fB 64 | :add lifting 1 <-- a yes/no kind of habit (represented by a dot grid) 65 | :add water <-- no goal provided 66 | \fR 67 | .fi 68 | .PP 69 | To track your progress for the day: 70 | .IP 71 | \(bu focus the habit you want to track, (the focused habit is highlighted in bright white). 72 | .IP 73 | \(bu Hit \fBEnter\fR on the keyboard to increase the value, or \fBBackspace\fR to reduce it. 74 | .IP 75 | \(bu Once you reach your daily goal, the day is marked in green and the habit name is struck through with a line. 76 | .PP 77 | Check your weekly progress for a given habit by pressing \fBv\fR on the keyboard, this is \fBWEEK\fR mode, and press \fB\fR to go back to \fBDAY\fR mode. \fB\fR is a shortcut to display weekly progress for every habit. The current mode is indicated on the status line. 78 | .PP 79 | Review your progress for previous months by pressing \fB[\fR on the keyboard, sift through months with \fB[\fR and \fB]\fR. 80 | 81 | .SH CUSTOMIZATION 82 | .PP 83 | \fBdijo (>= v0.2.7)\fR, can be configured via a configuration file. After its first run, \fBdijo\fR creates a configuration file. \fBdijo\fR must be restarted for changes in the configuration file to take effect. 84 | .PP 85 | This file is saved in different directories based on your operating system: 86 | 87 | .nf 88 | .IP 89 | \(bu GNU/Linux: \fB$XDG_CONFIG_HOME/dijo/config.toml\fR 90 | .IP 91 | \(bu MacOS: \fB$HOME/Library/Application Support/rs.nerdypepper.dijo/config.toml\fR 92 | .IP 93 | \(bu Win10: \fB{FOLDERID_RoamingAppData}\[rs]nerdypepper\[rs]dijo\[rs]config\[rs]config.toml\fR 94 | .fi 95 | 96 | .PP 97 | The default config file (created on first run) looks something like this, \fBconfig.toml\fR: 98 | .IP 99 | .nf 100 | \fB 101 | [look] 102 | true_chr = \[dq]\[pc]\[dq] 103 | false_chr = \[dq]\[pc]\[dq] 104 | future_chr = \[dq]\[pc]\[dq] 105 | 106 | [colors] 107 | reached = \[dq]cyan\[dq] 108 | todo = \[dq]magenta\[dq] 109 | inactive = \[dq]light black\[dq] 110 | \fR 111 | .fi 112 | 113 | .SS Look 114 | .PP 115 | Variables in this section define the characters \fBdijo\fR uses in \fBDAY\fR mode to represent days of the month. Every value in this section must span exactly 1 character in length. 116 | 117 | .IP 118 | \(bu \fBtrue_chr\fR: The character to use in a Bit habit when the goal is \fBreached\fR 119 | .IP 120 | \(bu \fBfalse_chr\fR: The character to use in a Bit habit when the goal is \fBnot reached\fR 121 | .IP 122 | \(bu \fBfuture_chr\fR: The character to use in a Bit habit when the day is \fBuntracked\fR. 123 | 124 | .SS Colors 125 | .PP 126 | Variables in this section define the colors \fBdijo\fR uses in all modes: 127 | 128 | .IP 129 | \(bu \fBreached\fR: The color to use when the goal is \fBreached\fR for the day. This is also the color used in \fBWEEK\fR mode to fill the progress bar. 130 | .IP 131 | \(bu \fBtodo\fR: The color to use when the goal is \fByet to be reached\fR. 132 | .IP 133 | \(bu \fBinactive\fR: The color to use for \fBunfocused habits and untracked days\fR. 134 | 135 | .PP 136 | The values in this section are strings, and may be populated by one of the following: 137 | .IP 138 | .nf 139 | \fB 140 | black light black 141 | red light red 142 | green light green 143 | yellow light yellow 144 | blue light blue 145 | magenta light magenta 146 | cyan light cyan 147 | white light white 148 | 149 | default -- uses your terminal\[aq]s background color 150 | #123456 -- any color in hex (supported only on true color terminals) 151 | \fR 152 | .fi 153 | 154 | .PP 155 | \fBNote\fR: These values have to be quoted (ex.: \fBreached = \[dq]black\[dq]\fR) 156 | 157 | .SH AUTO HABITS 158 | .PP 159 | \fBdijo\fR supports auto-trackable habits, that is, habits that can be updated via scripts. Add an auto-habit to, say, track your git commits: 160 | .IP 161 | .nf 162 | \fB 163 | :add-auto commits 5 164 | \fR 165 | .fi 166 | .PP 167 | You can control \fBdijo\fR externally by calling it with the 168 | \fB-c\fR flag: 169 | .IP 170 | .nf 171 | \fB 172 | dijo -c \[dq]track-up commits\[dq] # a +1 on today\[aq]s count 173 | dijo -c \[dq]track-down commits\[dq] # a -1 on today\[aq]s count 174 | \fR 175 | .fi 176 | .PP 177 | Firstly, point \fBgit\fR to your hooks directory 178 | (\fB\[ti]/.hooks\fR in this case): 179 | .IP 180 | .nf 181 | \fB 182 | # contents of \[ti]/.gitconfig 183 | 184 | [core] 185 | hooksPath = \[dq]/home/\fIusername\fB/.hooks\[dq] 186 | \fR 187 | .fi 188 | .PP 189 | Create a file called \fBpost-commit\fR in the \fB\[ti]/.hooks\fR directory, with the following contents (you should run \fBmkdir \[ti]/.hooks\fR if it doesn\[cq]t exist): 190 | .IP 191 | .nf 192 | \fB 193 | #! /usr/bin/env bash 194 | 195 | dijo -c \[dq]track-up commits\[dq] 196 | \fR 197 | .fi 198 | .IP 199 | .nf 200 | \fB 201 | # make the post-commit script an executable 202 | 203 | chmod +x \[ti]/.hooks/post-commit 204 | \fR 205 | .fi 206 | .PP 207 | Voil\[`a]! Every time you make a commit, \fBdijo\fR will automatically track it under the \fBcommits\fR habit. 208 | 209 | .SH MODES 210 | .PP 211 | Any habit in the interactive program can exist in one of two modes: 212 | .IP 213 | \(bu \fBDAY\fR: the default mode 214 | .IP 215 | \(bu \fBWEEK\fR: can be toggled on an off with \fBv\fR 216 | .SS \fBDAY\fR mode 217 | 218 | .PP 219 | \fBDAY\fR mode is the default mode for every habit. It shows you every day of the current month. In this mode, counting-type habits display their counts for each day of the month. Bit-type habits (yes/no) display their bits in the form of a \fB\[pc]\fR (U+00B7 Middle Dot). Days whose goals have been reached are displayed in cyan and those that haven\[cq]t been reached are displayed in magenta. Days that haven\[cq]t been tracked are displayed in \[lq]light black\[rq]. 220 | 221 | .SS \fBWEEK\fR mode 222 | .PP 223 | \fBWEEK\fR mode can be triggered for a single habit via \fBv\fR, 224 | and for every habit via \fBV\fR. \fBWEEK\fR mode will show you a summary of your progress for every week of the month. The current week\[cq]s percentage is indicated in white, other weeks are colored in \[lq]light black\[rq]. The progress for a given week is calculated as follows: 225 | .IP "1." 3 226 | if the goal is reached for a day of the week, the day contributes 227 | exactly 14.28% (100/7) towards that week\[cq]s progress. 228 | .IP "2." 3 229 | if the goal is not reached for a day of the week, the day contributes 230 | \fBprogress / goal * 100\fR towards that week\[cq]s progress. 231 | .PP 232 | That means, to achieve a 100% for a week, you have to reach your goal 233 | \fIevery single day of the week\fR. 234 | 235 | .SS The Command mode 236 | .PP 237 | The command mode is different from the other modes in that it is a \[lq]control\[rq] mode rather than an \[lq]observe\[rq] mode. One can enter the command mode by hitting \fB:\fR on the keyboard. You will notice a \fB:\fR pop up at the bottom of the screen. You may now begin typing a command, once you are done, press \fBEnter\fR on the keyboard to execute it. 238 | .PP 239 | The command mode may also be accessed without running the program interactively, by starting \fBdijo\fR with the \fB-c\fR flag and passing a string to it as a command: 240 | .IP 241 | .nf 242 | \fB 243 | $ dijo -c \[dq]track-up commits\[dq] 244 | \fR 245 | .fi 246 | .PP 247 | You can hit \fBTab\fR on the keyboard inside Command mode to 248 | trigger completions. For example: 249 | .IP 250 | .nf 251 | \fB 252 | :d 253 | # completes to 254 | :delete 255 | 256 | :delete fr 257 | # completes to 258 | :delete french 259 | \fR 260 | .fi 261 | 262 | .SH COMMANDS 263 | .SS Keybinds 264 | .PP 265 | These are key binds you can use at \fBDAY\fR or \fBWEEK\fR modes. Some of them are dependent on the currently focused habit and some are global. Those that are dependent on the currently focused habit are marked with 266 | a \fB[f]\fR. 267 | .IP \(bu 2 268 | Motions: 269 | .RS 2 270 | .nf 271 | .IP \(bu 2 272 | \fBh\fR - move one cell to the left (aliases: \fB\fR, \fB\fR) 273 | .IP \(bu 2 274 | \fBj\fR - move one cell down (aliases: \fB\fR) 275 | .IP \(bu 2 276 | \fBk\fR - move one cell up (alases: \fB\fR) 277 | .IP \(bu 2 278 | \fBl\fR - move one cell to the right (aliases: \fB\fR, 279 | \fB\fR) 280 | .fi 281 | .RE 282 | .IP \(bu 2 283 | Editing 284 | .RS 2 285 | .nf 286 | .IP \(bu 2 287 | \fB\fR - increment the currently focused habit (aliases: \fBn\fR) \fB[f]\fR 288 | .IP \(bu 2 289 | \fB\fR - decrement the currently focused habit (aliases: \fBp\fR) \fB[f]\fR 290 | .fi 291 | .RE 292 | .IP \(bu 2 293 | Modes 294 | .RS 2 295 | .IP \(bu 2 296 | \fBv\fR - enter \fBWEEK\fR mode for currently focused habit 297 | \fB[f]\fR 298 | .IP \(bu 2 299 | \fBV\fR - enter \fBWEEK\fR mode for all habits 300 | .IP \(bu 2 301 | \fB\fR - return to \fBDAY\fR mode 302 | .RE 303 | .IP \(bu 2 304 | Time Travel 305 | .RS 2 306 | .IP \(bu 2 307 | \fB[\fR - shift view port back by one month 308 | .IP \(bu 2 309 | \fB]\fR - shift view port forward by one month 310 | .IP \(bu 2 311 | \fB}\fR - return to present 312 | .RE 313 | .IP \(bu 2 314 | Control 315 | .RS 2 316 | .IP \(bu 2 317 | \fB\fR - quit without saving (subject to change) 318 | .RE 319 | .SS Commandline 320 | .PP 321 | Enter the command mode with \fB:\fR. Type out a command and press \fB\fR on the keyboard to execute it. Most commands have `aliases', or short forms. Command mode supports auto-complete also. 322 | .IP \(bu 2 323 | Help: show command syntax 324 | .RS 2 325 | .IP \(bu 2 326 | Inputs: optional command or alias 327 | .IP \(bu 2 328 | Usage: \fBhelp [command]\fR 329 | .IP \(bu 2 330 | Example: \fB:help aa\fR 331 | .IP \(bu 2 332 | Aliases: \fBh\fR 333 | .RE 334 | .IP \(bu 2 335 | Add: add a habit 336 | .RS 2 337 | .IP \(bu 2 338 | Inputs: name of habit, optional daily goal 339 | .IP \(bu 2 340 | Usage: \fBadd [goal]\fR 341 | .IP \(bu 2 342 | Example: \fB:add french 5\fR 343 | .IP \(bu 2 344 | Aliases: \fBa\fR 345 | .RE 346 | .IP \(bu 2 347 | Add Auto: add an auto-trackable habit 348 | .RS 2 349 | .IP \(bu 2 350 | Inputs: name of habit, optional daily goal 351 | .IP \(bu 2 352 | Usage: \fBadd-auto [goal]\fR 353 | .IP \(bu 2 354 | Example: \fB:add-auto commits 5\fR 355 | .IP \(bu 2 356 | Aliases: \fBaa\fR 357 | .RE 358 | .IP \(bu 2 359 | Delete: delete a habit 360 | .RS 2 361 | .IP \(bu 2 362 | Inputs: name of habit to delete 363 | .IP \(bu 2 364 | Usage: \fBdelete \fR 365 | .IP \(bu 2 366 | Example: \fB:delete water\fR 367 | .IP \(bu 2 368 | Aliases: \fBd\fR 369 | .RE 370 | .IP \(bu 2 371 | Month motions: stats from the past 372 | .RS 2 373 | .IP \(bu 2 374 | Inputs: None 375 | .IP \(bu 2 376 | Usage: \fBmonth-prev\fR, \fBmonth-next\fR 377 | .IP \(bu 2 378 | Example: \fB:month-prev\fR 379 | .IP \(bu 2 380 | Aliases: \fBmprev\fR, \fBmnext\fR 381 | .RE 382 | .IP \(bu 2 383 | Tracking: For use only with \fBdijo --command\fR, works only on 384 | auto-habits 385 | .RS 2 386 | .IP \(bu 2 387 | Inputs: name of habit to track up/down 388 | .IP \(bu 2 389 | Usage: \fBtrack-up \fR, 390 | \fBtrack-down \fR 391 | .IP \(bu 2 392 | Example: \fB:track-up commits\fR 393 | .IP \(bu 2 394 | Aliases: \fBtup\fR, \fBtdown\fR 395 | .RE 396 | .IP \(bu 2 397 | Write: write progress to disk 398 | .RS 2 399 | .IP \(bu 2 400 | Inputs: None 401 | .IP \(bu 2 402 | Usage: \fBwrite\fR 403 | .IP \(bu 2 404 | Example: \fB:write\fR 405 | .IP \(bu 2 406 | Aliases: \fBw\fR 407 | .RE 408 | .IP \(bu 2 409 | Quit: save and quit 410 | .RS 2 411 | .IP \(bu 2 412 | Inputs: None 413 | .IP \(bu 2 414 | Usage: \fBquit\fR 415 | .IP \(bu 2 416 | Example: \fB:quit\fR 417 | .IP \(bu 2 418 | Aliases: \fBq\fR 419 | .RE 420 | 421 | .SH INTERNALS 422 | .PP 423 | This document delves into the internals of \fBdijo\fR. 424 | .SS Files 425 | .PP 426 | After its first run, \fBdijo\fR creates three files on your file system, one to record habit data, \fBhabit_record.json\fR, one to record auto-habit data, \fBhabit_record[auto].json\fR, and one to store the default configuration in, \fBconfig.toml\fR. Data is saved in a human-readable format: JSON (a lot of work went into this). The config file is stored in TOML. 427 | 428 | .SS Data files: 429 | .PP 430 | Making changes to these files while \fBdijo\fR is running, is not recommended (\fBdijo\fR will overwrite your changes on save). 431 | .nf 432 | .IP \(bu 2 433 | GNU/Linux: \fB$XDG_DATA_HOME/dijo/*.json\fR 434 | .IP \(bu 2 435 | MacOS: \fB$HOME/Library/Application Support/rs.nerdypepper.dijo/*.json\fR 436 | .IP \(bu 2 437 | Win10: \fB{FOLDERID_RoamingAppData}\[rs]nerdypepper\[rs]dijo\[rs]data\[rs]*.json\fR 438 | .fi 439 | 440 | .SS Config files: 441 | .PP 442 | You can read more about configuring \fBdijo\fR at the Customization page. 443 | .nf 444 | .IP \(bu 2 445 | GNU/Linux: \fB$XDG_CONFIG_HOME/dijo/config.toml\fR 446 | .IP \(bu 2 447 | MacOS: \fB$HOME/Library/Application Support/rs.nerdypepper.dijo/config.toml\fR 448 | .IP \(bu 2 449 | Win10: \fB{FOLDERID_RoamingAppData}\[rs]nerdypepper\[rs]dijo\[rs]config\[rs]config.toml\fR 450 | .fi 451 | .PP 452 | \fBdijo\fR will not run on your computer if it can\[cq]t find your home directory. 453 | 454 | .SS Data format 455 | .PP 456 | The general structure of a habit is as follows: 457 | .IP 458 | .nf 459 | \fB 460 | type :: String, 461 | name :: String, 462 | goal :: HabitType, 463 | auto :: bool, 464 | stats :: Map 465 | \fR 466 | .fi 467 | .PP 468 | \fBHabitType\fR is the type of data to be tracked and it is a \fBbool\fR for bit-type habits and a \fBu32\fR for counting habits. 469 | 470 | .SS File watchers 471 | .PP 472 | \fBdijo\fR sets up a file watcher (in the interactive mode), and watches \fBhabit_record[auto].json\fR for changes. When changes are made to this file via \fBdijo -c\fR or equivalent, the interactive mode receives an update. However, the change is not reflected immediately because of \fBdijo\fR\[cq]s event based redrawing. You may force a redraw, and thereby update auto habits by pressing any button on the keyboard. 473 | 474 | .SH AUTHOR 475 | Akshay 476 | 477 | .SH SEE ALSO 478 | .PP 479 | \fIvi\fR(1), \fIvim\fR(1), \fInvim\fR(1), \fIjq\fR(1), \fIgit\fR(1) 480 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "mozillapkgs": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1603906276, 7 | "narHash": "sha256-RsNPnEKd7BcogwkqhaV5kI/HuNC4flH/OQCC/4W5y/8=", 8 | "owner": "mozilla", 9 | "repo": "nixpkgs-mozilla", 10 | "rev": "8c007b60731c07dd7a052cce508de3bb1ae849b4", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "mozilla", 15 | "repo": "nixpkgs-mozilla", 16 | "type": "github" 17 | } 18 | }, 19 | "naersk": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | }, 23 | "locked": { 24 | "lastModified": 1610392286, 25 | "narHash": "sha256-3wFl5y+4YZO4SgRYK8WE7JIS3p0sxbgrGaQ6RMw+d98=", 26 | "owner": "nmattia", 27 | "repo": "naersk", 28 | "rev": "d7bfbad3304fd768c0f93a4c3b50976275e6d4be", 29 | "type": "github" 30 | }, 31 | "original": { 32 | "owner": "nmattia", 33 | "repo": "naersk", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs": { 38 | "locked": { 39 | "lastModified": 1610850544, 40 | "narHash": "sha256-6GnsJuulJNdSrZNP98rRTRX/zJbxdC7m3qaH6WwsOuY=", 41 | "owner": "NixOS", 42 | "repo": "nixpkgs", 43 | "rev": "2fbc36f3d891c86ae34dc0414bc78e74e8911218", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "id": "nixpkgs", 48 | "type": "indirect" 49 | } 50 | }, 51 | "nixpkgs_2": { 52 | "locked": { 53 | "lastModified": 1610850544, 54 | "narHash": "sha256-6GnsJuulJNdSrZNP98rRTRX/zJbxdC7m3qaH6WwsOuY=", 55 | "owner": "NixOS", 56 | "repo": "nixpkgs", 57 | "rev": "2fbc36f3d891c86ae34dc0414bc78e74e8911218", 58 | "type": "github" 59 | }, 60 | "original": { 61 | "id": "nixpkgs", 62 | "type": "indirect" 63 | } 64 | }, 65 | "root": { 66 | "inputs": { 67 | "mozillapkgs": "mozillapkgs", 68 | "naersk": "naersk", 69 | "nixpkgs": "nixpkgs_2", 70 | "utils": "utils" 71 | } 72 | }, 73 | "utils": { 74 | "locked": { 75 | "lastModified": 1610051610, 76 | "narHash": "sha256-U9rPz/usA1/Aohhk7Cmc2gBrEEKRzcW4nwPWMPwja4Y=", 77 | "owner": "numtide", 78 | "repo": "flake-utils", 79 | "rev": "3982c9903e93927c2164caa727cd3f6a0e6d14cc", 80 | "type": "github" 81 | }, 82 | "original": { 83 | "owner": "numtide", 84 | "repo": "flake-utils", 85 | "type": "github" 86 | } 87 | } 88 | }, 89 | "root": "root", 90 | "version": 7 91 | } 92 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | utils.url = "github:numtide/flake-utils"; 4 | naersk.url = "github:nmattia/naersk"; 5 | mozillapkgs = { 6 | url = "github:mozilla/nixpkgs-mozilla"; 7 | flake = false; 8 | }; 9 | }; 10 | 11 | outputs = { self, nixpkgs, utils, naersk, mozillapkgs }: 12 | utils.lib.eachDefaultSystem (system: let 13 | pkgs = nixpkgs.legacyPackages."${system}"; 14 | 15 | # Get a specific rust version 16 | mozilla = pkgs.callPackage (mozillapkgs + "/package-set.nix") {}; 17 | rust = (mozilla.rustChannelOf { 18 | date = "2020-12-23"; 19 | channel = "nightly"; 20 | sha256 = "LbKHsCOFXWpg/SEyACfzZuWjKbkXdH6EJKOPSGoO01E="; # set zeros after modifying channel or date 21 | }).rust; 22 | rust-src = (mozilla.rustChannelOf { 23 | date = "2020-12-23"; 24 | channel = "nightly"; 25 | sha256 = "LbKHsCOFXWpg/SEyACfzZuWjKbkXdH6EJKOPSGoO01E="; # set zeros after modifying channel or date 26 | }).rust-src; 27 | 28 | naersk-lib = naersk.lib."${system}".override { 29 | cargo = rust; 30 | rustc = rust; 31 | }; 32 | in rec { 33 | packages.my-project = naersk-lib.buildPackage { 34 | pname = "dijo"; 35 | version = "0.2.7"; 36 | root = ./.; 37 | }; 38 | defaultPackage = packages.my-project; 39 | apps.my-project = utils.lib.mkApp { 40 | drv = packages.my-project; 41 | }; 42 | defaultApp = apps.my-project; 43 | devShell = pkgs.mkShell { 44 | nativeBuildInputs = [ 45 | rust 46 | rust-src 47 | pkgs.rust-analyzer 48 | pkgs.cargo 49 | pkgs.openssl 50 | pkgs.ncurses 51 | ]; 52 | shellHook = '' 53 | export RUST_SRC_PATH="${rust-src}/lib/rustlib/src/rust/library" 54 | ''; 55 | }; 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /notes.txt: -------------------------------------------------------------------------------- 1 | habit: 2 | `-type: bit/count 3 | `-stats: 4 | `-year: 5 | `-month: 6 | `-bit: 7 | | `-dates - array 8 | | 9 | `-count: 10 | `-dates - k,v pairs 11 | 12 | habit: 13 | `-type: bit/count 14 | `-stats: k,v (dates, bit/count) 15 | 16 | Cycle habit type: 17 | - n states 18 | - cycles through states on prev next events 19 | - represent by symbol/char 20 | - ser to usize? 21 | 22 | Modes: 23 | - day mode - shows all days of 1 month 24 | * sift months on prev/next 25 | - week mode? 26 | * aggregate stats for 1 week 27 | * show 4 weeks per view 28 | * bar graph for count and bit 29 | 30 | Command mode: 31 | - add command 32 | * add 33 | * add --type [--goal ] 34 | * interactive add command via questionnaire? 35 | - edit command? 36 | * edit 37 | * edit --goal 38 | * edit --type 39 | * interactive edit command via questionnaire? 40 | - delete command 41 | * delete 42 | * delete _ (deletes focused?) 43 | - chronological nav: 44 | * month-prev mprev 45 | * month-next mnext 46 | 47 | Interface: 48 | - move view port if focused view goes outside bounds 49 | - tab completion for command mode? requires lex table 50 | - move command window to bottom, styling 51 | - prefix command window with `:` 52 | 53 | Undo-tree: 54 | - store app states in memory 55 | - should store diffs? or entire state? 56 | - ideal undo depth limit? 57 | 58 | Auto-trackable habits 59 | - allow editing these habits via cli 60 | - can track commits, crons 61 | - disallow editing these habits via curses 62 | - storage 63 | * will be mutex with non-auto habits 64 | * serialize and save separately each other? [imp] 65 | 66 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | ### About 6 | 7 | `dijo` is a habit tracker. It is curses-based, it runs in 8 | your terminal. `dijo` is scriptable, hook it up [with 9 | external 10 | programs](https://github.com/NerdyPepper/dijo/wiki/Auto-Habits) 11 | to track events without moving a finger. `dijo` is modal, 12 | much like a certain text editor. 13 | 14 | ### Features 15 | 16 | - **vim like motions**: navigate `dijo` with `hjkl`! 17 | - **`dijo` is modal**: different modes to view different 18 | stats! 19 | - **vim like command mode**: add with `:add`, delete with 20 | `:delete` and above all, quit with `:q`!. 21 | - **fully scriptable**: [configure `dijo` to 22 | track your `git` commits](https://github.com/NerdyPepper/dijo/wiki/Auto-Habits)! 23 | 24 | ### Install 25 | 26 | To get the latest release of `dijo`, prefer installing it 27 | via `cargo`. Unofficial packages exist for some package 28 | managers as well. You can also browse the 29 | [Releases](https://github.com/NerdyPepper/dijo/releases) 30 | page for prebuilt binaries. 31 | 32 | #### Cargo 33 | 34 | ```shell 35 | # dijo requires rustc >= v1.42 36 | $ rustup update 37 | 38 | $ cargo install dijo 39 | ``` 40 | If you aren't familiar with `cargo` or Rust, read the [complete 41 | installation](https://github.com/NerdyPepper/dijo/wiki/Install) 42 | guide. 43 | 44 | #### Nix 45 | 46 | `dijo` on nixpkgs (maintained by [@Infinisil](https://github.com/Infinisil)): 47 | 48 | ``` 49 | $ nix-env -f channel:nixpkgs-unstable -iA dijo 50 | ``` 51 | 52 | #### Snap 53 | 54 | `dijo` on sanpstore (maintained by [@purveshpatel511](https://github.com/purveshpatel511)): 55 | 56 | ``` 57 | $ sudo snap install dijo 58 | ``` 59 | 60 | 61 | #### Arch Linux 62 | 63 | Install [`dijo-bin`](https://aur.archlinux.org/packages/dijo-bin/) or [`dijo-git`](https://aur.archlinux.org/packages/dijo-git) from the AUR. 64 | 65 | #### Windows 66 | 67 | ``` 68 | # the default termion backend dosen't run on windows yet 69 | $ cargo install --no-default-features --features "crossterm-backend" 70 | ``` 71 | 72 | ### Usage 73 | 74 | `dijo` has a [detailed 75 | wiki](https://github.com/NerdyPepper/dijo/wiki/), here are 76 | some good places to start out: 77 | 78 | - [Getting started](https://github.com/NerdyPepper/dijo/wiki/Getting-Started) 79 | - [Automatically tracking habits](https://github.com/NerdyPepper/dijo/wiki/Auto-Habits) 80 | - [Command reference](https://github.com/NerdyPepper/dijo/wiki/Commands) 81 | 82 | ### Gallery 83 | 84 | Day mode, shows days of the current month: 85 | 86 | ![day.png](https://u.peppe.rs/qI.png) 87 | 88 | Week mode, shows weekly summary for the weeks of the month: 89 | 90 | ![weekly.png](https://u.peppe.rs/HZ.png) 91 | 92 | [![Awesome Humane Tech](https://raw.githubusercontent.com/humanetech-community/awesome-humane-tech/main/humane-tech-badge.svg?sanitize=true)](https://github.com/humanetech-community/awesome-humane-tech) 93 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | hard_tabs = false 3 | tab_spaces = 4 4 | newline_style = "Auto" 5 | use_small_heuristics = "Default" 6 | reorder_imports = true 7 | reorder_modules = true 8 | remove_nested_parens = true 9 | fn_args_layout = "Tall" 10 | edition = "2018" 11 | merge_derives = true 12 | use_try_shorthand = false 13 | use_field_init_shorthand = false 14 | force_explicit_abi = true 15 | 16 | -------------------------------------------------------------------------------- /src/app/cursor.rs: -------------------------------------------------------------------------------- 1 | use chrono::{Duration, Local, NaiveDate}; 2 | use cursive::direction::Absolute; 3 | 4 | #[derive(Debug, Copy, Clone)] 5 | pub struct Cursor(pub NaiveDate); 6 | 7 | impl std::default::Default for Cursor { 8 | fn default() -> Self { 9 | Cursor::new() 10 | } 11 | } 12 | 13 | impl Cursor { 14 | pub fn new() -> Self { 15 | Cursor { 16 | 0: Local::now().naive_local().date(), 17 | } 18 | } 19 | pub fn small_seek(&mut self, d: Absolute) { 20 | let today = Local::now().naive_local().date(); 21 | let cursor = self.0; 22 | match d { 23 | Absolute::Right => { 24 | // forward by 1 day 25 | let next = cursor.succ_opt().unwrap_or(cursor); 26 | if next <= today { 27 | self.0 = next; 28 | } 29 | } 30 | Absolute::Left => { 31 | // backward by 1 day 32 | // assumes an infinite past 33 | self.0 = cursor.pred_opt().unwrap_or(cursor); 34 | } 35 | Absolute::Down => { 36 | // forward by 1 week 37 | let next = cursor.checked_add_signed(Duration::weeks(1)).unwrap(); 38 | if next <= today { 39 | self.0 = next; 40 | } 41 | } 42 | Absolute::Up => { 43 | // backward by 1 week 44 | // assumes an infinite past 45 | let next = cursor.checked_sub_signed(Duration::weeks(1)).unwrap(); 46 | self.0 = next; 47 | } 48 | Absolute::None => {} 49 | } 50 | } 51 | fn long_seek(&mut self, offset: Duration) { 52 | let cursor = self.0; 53 | let today = Local::now().naive_local().date(); 54 | let next = cursor.checked_add_signed(offset).unwrap_or(cursor); 55 | 56 | if next <= today { 57 | self.0 = next; 58 | } else { 59 | self.0 = today; 60 | } 61 | } 62 | pub fn month_forward(&mut self) { 63 | self.long_seek(Duration::weeks(4)); 64 | } 65 | pub fn month_backward(&mut self) { 66 | self.long_seek(Duration::weeks(-4)); 67 | } 68 | pub fn reset(&mut self) { 69 | self.0 = Local::now().naive_local().date(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app/impl_self.rs: -------------------------------------------------------------------------------- 1 | use std::default::Default; 2 | use std::f64; 3 | use std::fs::{File, OpenOptions}; 4 | use std::io::prelude::*; 5 | use std::path::PathBuf; 6 | use std::sync::mpsc::channel; 7 | use std::time::Duration; 8 | 9 | use chrono::{Local, NaiveDate}; 10 | use cursive::direction::Absolute; 11 | use cursive::Vec2; 12 | use notify::{watcher, RecursiveMode, Watcher}; 13 | 14 | use crate::command::{Command, CommandLineError, GoalKind}; 15 | use crate::habit::{Bit, Count, Float, HabitWrapper, TrackEvent, ViewMode}; 16 | use crate::utils::{self, GRID_WIDTH, VIEW_HEIGHT, VIEW_WIDTH}; 17 | 18 | use crate::app::{App, Cursor, Message, MessageKind, StatusLine}; 19 | 20 | impl App { 21 | pub fn new() -> Self { 22 | let (tx, rx) = channel(); 23 | let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap(); 24 | watcher.watch(utils::auto_habit_file(), RecursiveMode::Recursive); 25 | return App { 26 | habits: vec![], 27 | focus: 0, 28 | _file_watcher: watcher, 29 | file_event_recv: rx, 30 | cursor: Cursor::new(), 31 | message: Message::startup(), 32 | }; 33 | } 34 | 35 | pub fn add_habit(&mut self, h: Box) { 36 | self.habits.push(h); 37 | } 38 | 39 | pub fn list_habits(&self) -> Vec { 40 | self.habits.iter().map(|x| x.name()).collect::>() 41 | } 42 | 43 | pub fn delete_by_name(&mut self, name: &str) { 44 | let old_len = self.habits.len(); 45 | self.habits.retain(|h| h.name() != name); 46 | if old_len == self.habits.len() { 47 | self.message 48 | .set_message(format!("Could not delete habit `{}`", name)) 49 | } 50 | } 51 | 52 | pub fn get_mode(&self) -> ViewMode { 53 | if self.habits.is_empty() { 54 | return ViewMode::Day; 55 | } 56 | return self.habits[self.focus].inner_data_ref().view_mode(); 57 | } 58 | 59 | pub fn set_mode(&mut self, mode: ViewMode) { 60 | if !self.habits.is_empty() { 61 | self.habits[self.focus] 62 | .inner_data_mut_ref() 63 | .set_view_mode(mode); 64 | } 65 | } 66 | 67 | pub fn sift_backward(&mut self) { 68 | self.cursor.month_backward(); 69 | for v in self.habits.iter_mut() { 70 | v.inner_data_mut_ref().cursor.month_backward(); 71 | } 72 | } 73 | 74 | pub fn sift_forward(&mut self) { 75 | self.cursor.month_forward(); 76 | for v in self.habits.iter_mut() { 77 | v.inner_data_mut_ref().cursor.month_forward(); 78 | } 79 | } 80 | 81 | pub fn reset_cursor(&mut self) { 82 | self.cursor.reset(); 83 | for v in self.habits.iter_mut() { 84 | v.inner_data_mut_ref().cursor.reset(); 85 | } 86 | } 87 | 88 | pub fn move_cursor(&mut self, d: Absolute) { 89 | self.cursor.small_seek(d); 90 | for v in self.habits.iter_mut() { 91 | v.inner_data_mut_ref().move_cursor(d); 92 | } 93 | } 94 | 95 | pub fn set_focus(&mut self, d: Absolute) { 96 | match d { 97 | Absolute::Right => { 98 | if self.focus != self.habits.len() - 1 { 99 | self.focus += 1; 100 | } 101 | } 102 | Absolute::Left => { 103 | if self.focus != 0 { 104 | self.focus -= 1; 105 | } 106 | } 107 | Absolute::Down => { 108 | if self.focus + GRID_WIDTH < self.habits.len() - 1 { 109 | self.focus += GRID_WIDTH; 110 | } else { 111 | self.focus = self.habits.len() - 1; 112 | } 113 | } 114 | Absolute::Up => { 115 | if self.focus as isize - GRID_WIDTH as isize >= 0 { 116 | self.focus -= GRID_WIDTH; 117 | } else { 118 | self.focus = 0; 119 | } 120 | } 121 | Absolute::None => {} 122 | } 123 | } 124 | 125 | pub fn clear_message(&mut self) { 126 | self.message.clear(); 127 | } 128 | 129 | pub fn status(&self) -> StatusLine { 130 | let today = chrono::Local::now().naive_local().date(); 131 | let remaining = self.habits.iter().map(|h| h.remaining(today)).sum::(); 132 | let total = self.habits.iter().map(|h| h.goal()).sum::(); 133 | let completed = total - remaining; 134 | 135 | let timestamp = if self.cursor.0 == today { 136 | format!("{}", Local::now().naive_local().date().format("%d/%b/%y"),) 137 | } else { 138 | let since = NaiveDate::signed_duration_since(today, self.cursor.0).num_days(); 139 | let plural = if since == 1 { "" } else { "s" }; 140 | format!("{} ({} day{} ago)", self.cursor.0, since, plural) 141 | }; 142 | 143 | StatusLine { 144 | 0: format!( 145 | "Today: {} completed, {} remaining --{}--", 146 | completed, 147 | remaining, 148 | self.get_mode() 149 | ), 150 | 1: timestamp, 151 | } 152 | } 153 | 154 | pub fn max_size(&self) -> Vec2 { 155 | let width = GRID_WIDTH * VIEW_WIDTH; 156 | let height = { 157 | if !self.habits.is_empty() { 158 | (VIEW_HEIGHT as f64 * (self.habits.len() as f64 / GRID_WIDTH as f64).ceil()) 159 | as usize 160 | } else { 161 | 0 162 | } 163 | }; 164 | Vec2::new(width, height + 2) 165 | } 166 | 167 | pub fn load_state() -> Self { 168 | let (regular_f, auto_f) = (utils::habit_file(), utils::auto_habit_file()); 169 | let read_from_file = |file: PathBuf| -> Vec> { 170 | if let Ok(ref mut f) = File::open(file) { 171 | let mut j = String::new(); 172 | f.read_to_string(&mut j); 173 | return serde_json::from_str(&j).unwrap(); 174 | } else { 175 | return Vec::new(); 176 | } 177 | }; 178 | 179 | let mut regular = read_from_file(regular_f); 180 | let auto = read_from_file(auto_f); 181 | regular.extend(auto); 182 | return App { 183 | habits: regular, 184 | ..Default::default() 185 | }; 186 | } 187 | 188 | // this function does IO 189 | // TODO: convert this into non-blocking async function 190 | pub fn save_state(&self) { 191 | let (regular, auto): (Vec<_>, Vec<_>) = self.habits.iter().partition(|&x| !x.is_auto()); 192 | let (regular_f, auto_f) = (utils::habit_file(), utils::auto_habit_file()); 193 | 194 | let write_to_file = |data: Vec<&Box>, file: PathBuf| { 195 | let j = serde_json::to_string_pretty(&data).unwrap(); 196 | match OpenOptions::new() 197 | .write(true) 198 | .create(true) 199 | .truncate(true) 200 | .open(file) 201 | { 202 | Ok(ref mut f) => f.write_all(j.as_bytes()).unwrap(), 203 | Err(_) => panic!("Unable to write!"), 204 | }; 205 | }; 206 | 207 | write_to_file(regular, regular_f); 208 | write_to_file(auto, auto_f); 209 | } 210 | 211 | pub fn parse_command(&mut self, result: Result) { 212 | let mut _track = |name: &str, event: TrackEvent| { 213 | let target_habit = self 214 | .habits 215 | .iter_mut() 216 | .find(|x| x.name() == name && x.is_auto()); 217 | if let Some(h) = target_habit { 218 | h.modify(Local::now().naive_local().date(), event); 219 | } 220 | }; 221 | match result { 222 | Ok(c) => match c { 223 | Command::Add(name, goal, auto) => { 224 | if let Some(_) = self.habits.iter().find(|x| x.name() == name) { 225 | self.message.set_kind(MessageKind::Error); 226 | self.message 227 | .set_message(format!("Habit `{}` already exist", &name)); 228 | return; 229 | } 230 | match goal { 231 | Some(GoalKind::Bit) => { 232 | self.add_habit(Box::new(Bit::new(name, auto))); 233 | } 234 | Some(GoalKind::Count(v)) => { 235 | self.add_habit(Box::new(Count::new(name, v, auto))); 236 | } 237 | Some(GoalKind::Float(v, p)) => { 238 | self.message.set_kind(MessageKind::Error); 239 | self.message.set_message(format!("Added floating habit")); 240 | self.add_habit(Box::new(Float::new(name, v, p, auto))); 241 | } 242 | _ => { 243 | self.add_habit(Box::new(Count::new(name, 0, auto))); 244 | } 245 | } 246 | } 247 | Command::Delete(name) => { 248 | self.delete_by_name(&name); 249 | self.focus = 0; 250 | } 251 | Command::TrackUp(name) => { 252 | _track(&name, TrackEvent::Increment); 253 | } 254 | Command::TrackDown(name) => { 255 | _track(&name, TrackEvent::Decrement); 256 | } 257 | Command::Help(input) => { 258 | if let Some(topic) = input.as_ref().map(String::as_ref) { 259 | self.message.set_message( 260 | match topic { 261 | "a" | "add" => "add [goal] (alias: a)", 262 | "aa" | "add-auto" => "add-auto [goal] (alias: aa)", 263 | "d" | "delete" => "delete (alias: d)", 264 | "mprev" | "month-prev" => "month-prev (alias: mprev)", 265 | "mnext" | "month-next" => "month-next (alias: mnext)", 266 | "tup" | "track-up" => "track-up (alias: tup)", 267 | "tdown" | "track-down" => "track-down (alias: tdown)", 268 | "q" | "quit" => "quit dijo", 269 | "w" | "write" => "write current state to disk (alias: w)", 270 | "h"|"?" | "help" => "help [|commands|keys] (aliases: h, ?)", 271 | "cmds" | "commands" => "add, add-auto, delete, month-{prev,next}, track-{up,down}, help, quit", 272 | "keys" => "TODO", // TODO (view?) 273 | "wq" => "write current state to disk and quit dijo", 274 | _ => "unknown command or help topic.", 275 | } 276 | ) 277 | } else { 278 | // TODO (view?) 279 | self.message.set_message("help |commands|keys") 280 | } 281 | } 282 | Command::Quit | Command::Write | Command::WriteAndQuit => self.save_state(), 283 | Command::MonthNext => self.sift_forward(), 284 | Command::MonthPrev => self.sift_backward(), 285 | Command::Blank => {} 286 | }, 287 | Err(e) => { 288 | self.message.set_message(e.to_string()); 289 | self.message.set_kind(MessageKind::Error); 290 | } 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/app/impl_view.rs: -------------------------------------------------------------------------------- 1 | use std::f64; 2 | use std::fs::File; 3 | use std::io::prelude::*; 4 | use std::path::PathBuf; 5 | 6 | use cursive::direction::{Absolute, Direction}; 7 | use cursive::event::{Event, EventResult, Key}; 8 | use cursive::theme::Color; 9 | use cursive::view::{CannotFocus, View}; 10 | use cursive::{Printer, Vec2}; 11 | use notify::DebouncedEvent; 12 | 13 | use crate::app::{App, MessageKind}; 14 | use crate::habit::{HabitWrapper, ViewMode}; 15 | use crate::utils::{self, GRID_WIDTH, VIEW_HEIGHT, VIEW_WIDTH}; 16 | 17 | impl View for App { 18 | fn draw(&self, printer: &Printer) { 19 | let mut offset = Vec2::zero(); 20 | for (idx, habit) in self.habits.iter().enumerate() { 21 | if idx >= GRID_WIDTH && idx % GRID_WIDTH == 0 { 22 | offset = offset.map_y(|y| y + VIEW_HEIGHT).map_x(|_| 0); 23 | } 24 | habit.draw(&printer.offset(offset).focused(self.focus == idx)); 25 | offset = offset.map_x(|x| x + VIEW_WIDTH + 2); 26 | } 27 | 28 | offset = offset.map_x(|_| 0).map_y(|_| self.max_size().y - 2); 29 | 30 | let status = self.status(); 31 | printer.print(offset, &status.0); // left status 32 | 33 | let full = self.max_size().x; 34 | offset = offset.map_x(|_| full - status.1.len()); 35 | printer.print(offset, &status.1); // right status 36 | 37 | offset = offset.map_x(|_| 0).map_y(|_| self.max_size().y - 1); 38 | printer.with_style(Color::from(self.message.kind()), |p| { 39 | p.print(offset, self.message.contents()) 40 | }); 41 | } 42 | 43 | fn required_size(&mut self, _: Vec2) -> Vec2 { 44 | let width = GRID_WIDTH * (VIEW_WIDTH + 2); 45 | let height = { 46 | if self.habits.len() > 0 { 47 | (VIEW_HEIGHT as f64 * (self.habits.len() as f64 / GRID_WIDTH as f64).ceil()) 48 | as usize 49 | } else { 50 | 0 51 | } 52 | }; 53 | Vec2::new(width, height + 2) 54 | } 55 | 56 | fn take_focus(&mut self, _: Direction) -> Result { 57 | Err(CannotFocus) 58 | } 59 | 60 | fn on_event(&mut self, e: Event) -> EventResult { 61 | match self.file_event_recv.try_recv() { 62 | Ok(DebouncedEvent::Write(_)) => { 63 | let read_from_file = |file: PathBuf| -> Vec> { 64 | if let Ok(ref mut f) = File::open(file) { 65 | let mut j = String::new(); 66 | f.read_to_string(&mut j); 67 | return serde_json::from_str(&j).unwrap(); 68 | } else { 69 | return Vec::new(); 70 | } 71 | }; 72 | let auto = read_from_file(utils::auto_habit_file()); 73 | self.habits.retain(|x| !x.is_auto()); 74 | self.habits.extend(auto); 75 | } 76 | _ => {} 77 | }; 78 | if self.habits.is_empty() { 79 | return EventResult::Ignored; 80 | } 81 | match e { 82 | Event::Key(Key::Right) | Event::Key(Key::Tab) | Event::Char('l') => { 83 | self.set_focus(Absolute::Right); 84 | return EventResult::Consumed(None); 85 | } 86 | Event::Key(Key::Left) | Event::Shift(Key::Tab) | Event::Char('h') => { 87 | self.set_focus(Absolute::Left); 88 | return EventResult::Consumed(None); 89 | } 90 | Event::Key(Key::Up) | Event::Char('k') => { 91 | self.set_focus(Absolute::Up); 92 | return EventResult::Consumed(None); 93 | } 94 | Event::Key(Key::Down) | Event::Char('j') => { 95 | self.set_focus(Absolute::Down); 96 | return EventResult::Consumed(None); 97 | } 98 | 99 | Event::Char('K') => { 100 | self.move_cursor(Absolute::Up); 101 | return EventResult::Consumed(None); 102 | } 103 | Event::Char('H') => { 104 | self.move_cursor(Absolute::Left); 105 | return EventResult::Consumed(None); 106 | } 107 | Event::Char('J') => { 108 | self.move_cursor(Absolute::Down); 109 | return EventResult::Consumed(None); 110 | } 111 | Event::Char('L') => { 112 | self.move_cursor(Absolute::Right); 113 | return EventResult::Consumed(None); 114 | } 115 | 116 | Event::Char('v') => { 117 | if self.habits.is_empty() { 118 | return EventResult::Consumed(None); 119 | } 120 | if self.habits[self.focus].inner_data_ref().view_mode() == ViewMode::Week { 121 | self.set_mode(ViewMode::Day) 122 | } else { 123 | self.set_mode(ViewMode::Week) 124 | } 125 | return EventResult::Consumed(None); 126 | } 127 | Event::Char('V') => { 128 | for habit in self.habits.iter_mut() { 129 | habit.inner_data_mut_ref().set_view_mode(ViewMode::Week); 130 | } 131 | return EventResult::Consumed(None); 132 | } 133 | Event::Key(Key::Esc) => { 134 | for habit in self.habits.iter_mut() { 135 | habit.inner_data_mut_ref().set_view_mode(ViewMode::Day); 136 | } 137 | self.reset_cursor(); 138 | return EventResult::Consumed(None); 139 | } 140 | 141 | /* We want sifting to be an app level function, 142 | * that later trickles down into each habit 143 | * */ 144 | Event::Char(']') => { 145 | self.sift_forward(); 146 | return EventResult::Consumed(None); 147 | } 148 | Event::Char('[') => { 149 | self.sift_backward(); 150 | return EventResult::Consumed(None); 151 | } 152 | Event::Char('}') => { 153 | self.reset_cursor(); 154 | return EventResult::Consumed(None); 155 | } 156 | Event::CtrlChar('l') => { 157 | self.message.clear(); 158 | self.message.set_kind(MessageKind::Info); 159 | return EventResult::Consumed(None); 160 | } 161 | 162 | /* Every keybind that is not caught by App trickles 163 | * down to the focused habit. 164 | * */ 165 | _ => { 166 | if self.habits.is_empty() { 167 | return EventResult::Ignored; 168 | } 169 | self.habits[self.focus].on_event(e) 170 | } 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/app/message.rs: -------------------------------------------------------------------------------- 1 | use cursive::theme::{BaseColor, Color}; 2 | 3 | #[derive(Debug, Clone, Copy)] 4 | pub enum MessageKind { 5 | Error, 6 | Info, 7 | Hint, 8 | } 9 | 10 | impl From for Color { 11 | fn from(item: MessageKind) -> Self { 12 | match item { 13 | MessageKind::Error => Color::Dark(BaseColor::Red), 14 | MessageKind::Info => Color::Dark(BaseColor::Yellow), 15 | MessageKind::Hint => Color::Dark(BaseColor::White), 16 | } 17 | } 18 | } 19 | 20 | impl From for Message 21 | where 22 | T: AsRef, 23 | { 24 | fn from(item: T) -> Self { 25 | return Message { 26 | msg: item.as_ref().to_string(), 27 | kind: MessageKind::Info, 28 | }; 29 | } 30 | } 31 | 32 | pub struct Message { 33 | msg: String, 34 | kind: MessageKind, 35 | } 36 | 37 | impl Message { 38 | pub fn startup() -> Self { 39 | "Type :add to get started, Ctrl-L to dismiss".into() 40 | } 41 | pub fn contents(&self) -> &str { 42 | &self.msg 43 | } 44 | pub fn kind(&self) -> MessageKind { 45 | self.kind 46 | } 47 | pub fn set_kind(&mut self, k: MessageKind) { 48 | self.kind = k; 49 | } 50 | pub fn set_message>(&mut self, m: S) { 51 | self.msg = m.as_ref().into(); 52 | } 53 | pub fn clear(&mut self) { 54 | self.msg.clear() 55 | } 56 | } 57 | 58 | impl std::default::Default for Message { 59 | fn default() -> Self { 60 | Message { 61 | msg: String::new(), 62 | kind: MessageKind::Info, 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/app/mod.rs: -------------------------------------------------------------------------------- 1 | use std::default::Default; 2 | use std::sync::mpsc::Receiver; 3 | 4 | use notify::{DebouncedEvent, RecommendedWatcher}; 5 | 6 | use crate::habit::HabitWrapper; 7 | 8 | mod cursor; 9 | mod impl_self; 10 | mod impl_view; 11 | mod message; 12 | 13 | pub struct StatusLine(String, String); 14 | pub use cursor::Cursor; 15 | pub use message::{Message, MessageKind}; 16 | 17 | pub struct App { 18 | // holds app data 19 | habits: Vec>, 20 | 21 | _file_watcher: RecommendedWatcher, 22 | file_event_recv: Receiver, 23 | focus: usize, 24 | cursor: Cursor, 25 | message: Message, 26 | } 27 | 28 | impl Default for App { 29 | fn default() -> Self { 30 | App::new() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/command.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::str::FromStr; 3 | 4 | use cursive::event::{Event, EventResult, Key}; 5 | use cursive::theme::{BaseColor, Color, ColorStyle}; 6 | use cursive::view::Resizable; 7 | use cursive::views::{EditView, LinearLayout, OnEventView, TextView}; 8 | use cursive::Cursive; 9 | 10 | use crate::app::App; 11 | use crate::utils::{GRID_WIDTH, VIEW_WIDTH}; 12 | 13 | static COMMANDS: &'static [&'static str] = &[ 14 | "add", 15 | "add-auto", 16 | "delete", 17 | "track-up", 18 | "track-down", 19 | "month-prev", 20 | "month-next", 21 | "quit", 22 | "write", 23 | "help", 24 | "writeandquit", 25 | ]; 26 | 27 | fn get_command_completion(prefix: &str) -> Option { 28 | let first_match = COMMANDS.iter().filter(|&x| x.starts_with(prefix)).next(); 29 | return first_match.map(|&x| x.into()); 30 | } 31 | 32 | fn get_habit_completion(prefix: &str, habit_names: &[String]) -> Option { 33 | let first_match = habit_names.iter().filter(|&x| x.starts_with(prefix)).next(); 34 | return first_match.map(|x| x.into()); 35 | } 36 | 37 | pub fn open_command_window(s: &mut Cursive) { 38 | let habit_list: Vec = s 39 | .call_on_name("Main", |view: &mut App| { 40 | return view.list_habits(); 41 | }) 42 | .unwrap(); 43 | let style = ColorStyle::new(Color::Dark(BaseColor::Black), Color::Dark(BaseColor::White)); 44 | let command_window = OnEventView::new( 45 | EditView::new() 46 | .filler(" ") 47 | .on_submit(call_on_app) 48 | .style(style), 49 | ) 50 | .on_event_inner( 51 | Event::Key(Key::Tab), 52 | move |view: &mut EditView, _: &Event| { 53 | let contents = view.get_content(); 54 | if !contents.contains(" ") { 55 | let completion = get_command_completion(&*contents); 56 | if let Some(c) = completion { 57 | let cb = view.set_content(c); 58 | return Some(EventResult::Consumed(Some(cb))); 59 | }; 60 | return None; 61 | } else { 62 | let word = contents.split(' ').last().unwrap(); 63 | let completion = get_habit_completion(word, &habit_list); 64 | if let Some(c) = completion { 65 | let cb = view.set_content(format!("{}", contents) + &c[word.len()..]); 66 | return Some(EventResult::Consumed(Some(cb))); 67 | }; 68 | return None; 69 | } 70 | }, 71 | ) 72 | .fixed_width(VIEW_WIDTH * GRID_WIDTH); 73 | s.call_on_name("Frame", |view: &mut LinearLayout| { 74 | let mut commandline = LinearLayout::horizontal() 75 | .child(TextView::new(":")) 76 | .child(command_window); 77 | commandline.set_focus_index(1); 78 | view.add_child(commandline); 79 | view.set_focus_index(1); 80 | }); 81 | } 82 | 83 | fn call_on_app(s: &mut Cursive, input: &str) { 84 | // things to do after recieving the command 85 | // 1. parse the command 86 | // 2. clean existing command messages 87 | // 3. remove the command window 88 | // 4. handle quit command 89 | s.call_on_name("Main", |view: &mut App| { 90 | let cmd = Command::from_string(input); 91 | view.clear_message(); 92 | view.parse_command(cmd); 93 | }); 94 | s.call_on_name("Frame", |view: &mut LinearLayout| { 95 | view.set_focus_index(0); 96 | view.remove_child(view.get_focus_index()); 97 | }); 98 | 99 | // special command that requires access to 100 | // our main cursive object, has to be parsed again 101 | // here 102 | // TODO: fix this somehow 103 | match Command::from_string(input) { 104 | Ok(Command::Quit) | Ok(Command::WriteAndQuit) => s.quit(), 105 | _ => {} 106 | } 107 | } 108 | 109 | #[derive(Debug, PartialEq)] 110 | pub enum GoalKind { 111 | Count(u32), 112 | Bit, 113 | Float(u32, u8), 114 | Addiction(u32), 115 | } 116 | 117 | impl FromStr for GoalKind { 118 | type Err = CommandLineError; 119 | 120 | fn from_str(s: &str) -> Result { 121 | if let Some(n) = s.strip_prefix("<") { 122 | return n 123 | .parse::() 124 | .map_err(|_| CommandLineError::InvalidGoal(s.into())) 125 | .map(GoalKind::Addiction); 126 | } else if s.contains(".") { 127 | let value = s 128 | .chars() 129 | .filter(|x| x.is_digit(10)) 130 | .collect::() 131 | .parse::() 132 | .map_err(|_| CommandLineError::InvalidCommand(s.into()))?; 133 | let precision = s.chars().skip_while(|&x| x != '.').count() - 1; 134 | return Ok(GoalKind::Float(value, precision as u8)); 135 | } 136 | if let Ok(v) = s.parse::() { 137 | if v == 1 { 138 | return Ok(GoalKind::Bit); 139 | } else { 140 | return Ok(GoalKind::Count(v)); 141 | } 142 | } 143 | return Err(CommandLineError::InvalidCommand(s.into())); 144 | } 145 | } 146 | 147 | #[derive(PartialEq)] 148 | pub enum Command { 149 | Add(String, Option, bool), 150 | MonthPrev, 151 | MonthNext, 152 | Delete(String), 153 | TrackUp(String), 154 | TrackDown(String), 155 | Help(Option), 156 | Write, 157 | Quit, 158 | Blank, 159 | WriteAndQuit, 160 | } 161 | 162 | #[derive(Debug)] 163 | pub enum CommandLineError { 164 | InvalidCommand(String), // command name 165 | InvalidArg(u32), // position 166 | NotEnoughArgs(String, u32), // command name, required no. of args 167 | InvalidGoal(String), // goal expression 168 | } 169 | 170 | impl std::error::Error for CommandLineError {} 171 | 172 | impl fmt::Display for CommandLineError { 173 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 174 | match self { 175 | CommandLineError::InvalidCommand(s) => write!(f, "Invalid command: `{}`", s), 176 | CommandLineError::InvalidArg(p) => write!(f, "Invalid argument at position {}", p), 177 | CommandLineError::NotEnoughArgs(s, n) => { 178 | write!(f, "Command `{}` requires atleast {} argument(s)!", s, n) 179 | } 180 | CommandLineError::InvalidGoal(s) => write!(f, "Invalid goal expression: `{}`", s), 181 | } 182 | } 183 | } 184 | 185 | type Result = std::result::Result; 186 | 187 | impl Command { 188 | pub fn from_string>(input: P) -> Result { 189 | let mut strings: Vec<&str> = input.as_ref().trim().split(' ').collect(); 190 | if strings.is_empty() { 191 | return Ok(Command::Blank); 192 | } 193 | 194 | let first = strings.first().unwrap().to_string(); 195 | let mut args: Vec = strings.iter_mut().skip(1).map(|s| s.to_string()).collect(); 196 | let mut _add = |auto: bool, first: String| { 197 | if args.is_empty() { 198 | return Err(CommandLineError::NotEnoughArgs(first, 1)); 199 | } 200 | let goal = args.get(1).map(|x| GoalKind::from_str(x)).transpose()?; 201 | return Ok(Command::Add( 202 | args.get_mut(0).unwrap().to_string(), 203 | goal, 204 | auto, 205 | )); 206 | }; 207 | 208 | match first.as_ref() { 209 | "add" | "a" => _add(false, first), 210 | "add-auto" | "aa" => _add(true, first), 211 | "delete" | "d" => { 212 | if args.is_empty() { 213 | return Err(CommandLineError::NotEnoughArgs(first, 1)); 214 | } 215 | return Ok(Command::Delete(args[0].to_string())); 216 | } 217 | "track-up" | "tup" => { 218 | if args.is_empty() { 219 | return Err(CommandLineError::NotEnoughArgs(first, 1)); 220 | } 221 | return Ok(Command::TrackUp(args[0].to_string())); 222 | } 223 | "track-down" | "tdown" => { 224 | if args.is_empty() { 225 | return Err(CommandLineError::NotEnoughArgs(first, 1)); 226 | } 227 | return Ok(Command::TrackDown(args[0].to_string())); 228 | } 229 | "h" | "?" | "help" => { 230 | if args.is_empty() { 231 | return Ok(Command::Help(None)); 232 | } 233 | return Ok(Command::Help(Some(args[0].to_string()))); 234 | } 235 | "mprev" | "month-prev" => return Ok(Command::MonthPrev), 236 | "mnext" | "month-next" => return Ok(Command::MonthNext), 237 | "wq" | "writeandquit" => return Ok(Command::WriteAndQuit), 238 | "q" | "quit" => return Ok(Command::Quit), 239 | "w" | "write" => return Ok(Command::Write), 240 | "" => return Ok(Command::Blank), 241 | s => return Err(CommandLineError::InvalidCommand(s.into())), 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/habit/bit.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::default::Default; 3 | 4 | use chrono::NaiveDate; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::command::GoalKind; 8 | use crate::habit::prelude::default_auto; 9 | use crate::habit::traits::Habit; 10 | use crate::habit::{InnerData, TrackEvent}; 11 | use crate::CONFIGURATION; 12 | 13 | #[derive(Copy, Clone, Debug, Serialize, Deserialize)] 14 | pub struct CustomBool(bool); 15 | 16 | use std::fmt; 17 | impl fmt::Display for CustomBool { 18 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 19 | write!( 20 | f, 21 | "{:^3}", 22 | if self.0 { 23 | CONFIGURATION.look.true_chr 24 | } else { 25 | CONFIGURATION.look.false_chr 26 | } 27 | ) 28 | } 29 | } 30 | 31 | impl From for CustomBool { 32 | fn from(b: bool) -> Self { 33 | CustomBool(b) 34 | } 35 | } 36 | 37 | #[derive(Debug, Serialize, Deserialize)] 38 | pub struct Bit { 39 | name: String, 40 | stats: HashMap, 41 | goal: CustomBool, 42 | 43 | #[serde(default = "default_auto")] 44 | auto: bool, 45 | 46 | #[serde(skip)] 47 | inner_data: InnerData, 48 | } 49 | 50 | impl Bit { 51 | pub fn new(name: impl AsRef, auto: bool) -> Self { 52 | return Bit { 53 | name: name.as_ref().to_owned(), 54 | stats: HashMap::new(), 55 | goal: CustomBool(true), 56 | auto, 57 | inner_data: Default::default(), 58 | }; 59 | } 60 | } 61 | 62 | impl Habit for Bit { 63 | type HabitType = CustomBool; 64 | fn name(&self) -> String { 65 | return self.name.clone(); 66 | } 67 | fn set_name(&mut self, n: impl AsRef) { 68 | self.name = n.as_ref().to_owned(); 69 | } 70 | fn kind(&self) -> GoalKind { 71 | GoalKind::Bit 72 | } 73 | fn set_goal(&mut self, g: Self::HabitType) { 74 | self.goal = g; 75 | } 76 | fn get_by_date(&self, date: NaiveDate) -> Option<&Self::HabitType> { 77 | self.stats.get(&date) 78 | } 79 | fn insert_entry(&mut self, date: NaiveDate, val: Self::HabitType) { 80 | *self.stats.entry(date).or_insert(val) = val; 81 | } 82 | fn reached_goal(&self, date: NaiveDate) -> bool { 83 | if let Some(val) = self.stats.get(&date) { 84 | if val.0 >= self.goal.0 { 85 | return true; 86 | } 87 | } 88 | return false; 89 | } 90 | fn remaining(&self, date: NaiveDate) -> u32 { 91 | if let Some(val) = self.stats.get(&date) { 92 | if val.0 { 93 | return 0; 94 | } else { 95 | return 1; 96 | } 97 | } else { 98 | return 1; 99 | } 100 | } 101 | fn goal(&self) -> u32 { 102 | return 1; 103 | } 104 | fn modify(&mut self, date: NaiveDate, event: TrackEvent) { 105 | if let Some(val) = self.stats.get_mut(&date) { 106 | match event { 107 | TrackEvent::Increment => *val = (val.0 ^ true).into(), 108 | TrackEvent::Decrement => { 109 | if val.0 { 110 | *val = false.into(); 111 | } else { 112 | self.stats.remove(&date); 113 | } 114 | } 115 | } 116 | } else { 117 | if event == TrackEvent::Increment { 118 | self.insert_entry(date, CustomBool(true)); 119 | } 120 | } 121 | } 122 | fn inner_data_ref(&self) -> &InnerData { 123 | &self.inner_data 124 | } 125 | fn inner_data_mut_ref(&mut self) -> &mut InnerData { 126 | &mut self.inner_data 127 | } 128 | fn is_auto(&self) -> bool { 129 | self.auto 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/habit/count.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::default::Default; 3 | 4 | use chrono::NaiveDate; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::command::GoalKind; 8 | use crate::habit::prelude::default_auto; 9 | use crate::habit::traits::Habit; 10 | use crate::habit::{InnerData, TrackEvent}; 11 | 12 | #[derive(Debug, Serialize, Deserialize)] 13 | pub struct Count { 14 | name: String, 15 | stats: HashMap, 16 | goal: u32, 17 | 18 | #[serde(default = "default_auto")] 19 | auto: bool, 20 | 21 | #[serde(skip)] 22 | inner_data: InnerData, 23 | } 24 | 25 | impl Count { 26 | pub fn new(name: impl AsRef, goal: u32, auto: bool) -> Self { 27 | return Count { 28 | name: name.as_ref().to_owned(), 29 | stats: HashMap::new(), 30 | goal, 31 | auto, 32 | inner_data: Default::default(), 33 | }; 34 | } 35 | } 36 | 37 | impl Habit for Count { 38 | type HabitType = u32; 39 | 40 | fn name(&self) -> String { 41 | return self.name.clone(); 42 | } 43 | fn set_name(&mut self, n: impl AsRef) { 44 | self.name = n.as_ref().to_owned(); 45 | } 46 | fn kind(&self) -> GoalKind { 47 | GoalKind::Count(self.goal) 48 | } 49 | fn set_goal(&mut self, g: Self::HabitType) { 50 | self.goal = g; 51 | } 52 | fn get_by_date(&self, date: NaiveDate) -> Option<&Self::HabitType> { 53 | self.stats.get(&date) 54 | } 55 | fn insert_entry(&mut self, date: NaiveDate, val: Self::HabitType) { 56 | *self.stats.entry(date).or_insert(val) = val; 57 | } 58 | fn reached_goal(&self, date: NaiveDate) -> bool { 59 | if let Some(val) = self.stats.get(&date) { 60 | if val >= &self.goal { 61 | return true; 62 | } 63 | } 64 | return false; 65 | } 66 | fn remaining(&self, date: NaiveDate) -> u32 { 67 | if self.reached_goal(date) { 68 | return 0; 69 | } else { 70 | if let Some(val) = self.stats.get(&date) { 71 | return self.goal - val; 72 | } else { 73 | return self.goal; 74 | } 75 | } 76 | } 77 | fn goal(&self) -> u32 { 78 | return self.goal; 79 | } 80 | fn modify(&mut self, date: NaiveDate, event: TrackEvent) { 81 | if let Some(val) = self.stats.get_mut(&date) { 82 | match event { 83 | TrackEvent::Increment => *val += 1, 84 | TrackEvent::Decrement => { 85 | if *val > 0 { 86 | *val -= 1 87 | } else { 88 | self.stats.remove(&date); 89 | }; 90 | } 91 | } 92 | } else { 93 | match event { 94 | TrackEvent::Increment => self.insert_entry(date, 1), 95 | _ => {} 96 | }; 97 | } 98 | } 99 | fn inner_data_ref(&self) -> &InnerData { 100 | &self.inner_data 101 | } 102 | fn inner_data_mut_ref(&mut self) -> &mut InnerData { 103 | &mut self.inner_data 104 | } 105 | fn is_auto(&self) -> bool { 106 | self.auto 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/habit/float.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::{Eq, Ord, PartialEq}; 2 | use std::collections::HashMap; 3 | use std::default::Default; 4 | use std::fmt; 5 | use std::ops::{Add, Sub}; 6 | 7 | use chrono::NaiveDate; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | use crate::command::GoalKind; 11 | use crate::habit::prelude::default_auto; 12 | use crate::habit::traits::Habit; 13 | use crate::habit::{InnerData, TrackEvent}; 14 | 15 | #[derive(Copy, Clone, Debug, Ord, Eq, PartialEq, PartialOrd, Serialize, Deserialize)] 16 | pub struct FloatData { 17 | value: u32, 18 | precision: u8, 19 | } 20 | 21 | impl FloatData { 22 | pub fn add(self, v: u32) -> Self { 23 | let f = FloatData { 24 | value: v, 25 | precision: self.precision, 26 | }; 27 | self + f 28 | } 29 | pub fn sub(self, v: u32) -> Self { 30 | let f = FloatData { 31 | value: v, 32 | precision: self.precision, 33 | }; 34 | self - f 35 | } 36 | pub fn zero() -> Self { 37 | FloatData { 38 | value: 0, 39 | precision: 0, 40 | } 41 | } 42 | } 43 | 44 | impl fmt::Display for FloatData { 45 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 46 | let characteristic = self.value / (10 * self.precision as u32); 47 | let mantissa = self.value % (10 * self.precision as u32); 48 | let s = if characteristic == 0 { 49 | format!(".{}", mantissa) 50 | } else if mantissa == 0 { 51 | format!("{}", characteristic) 52 | } else { 53 | format!("{}.{}", characteristic, mantissa) 54 | }; 55 | write!(f, "{:^3}", s) 56 | } 57 | } 58 | 59 | impl Add for FloatData { 60 | type Output = Self; 61 | fn add(self, other: Self) -> Self { 62 | Self { 63 | value: self.value + other.value, 64 | precision: self.precision, 65 | } 66 | } 67 | } 68 | 69 | impl Sub for FloatData { 70 | type Output = Self; 71 | 72 | fn sub(self, other: Self) -> Self { 73 | Self { 74 | value: self.value.saturating_sub(other.value), 75 | precision: self.precision, 76 | } 77 | } 78 | } 79 | 80 | #[derive(Debug, Serialize, Deserialize)] 81 | pub struct Float { 82 | name: String, 83 | stats: HashMap, 84 | goal: FloatData, 85 | precision: u8, 86 | #[serde(default = "default_auto")] 87 | auto: bool, 88 | 89 | #[serde(skip)] 90 | inner_data: InnerData, 91 | } 92 | 93 | impl Float { 94 | pub fn new(name: impl AsRef, goal: u32, precision: u8, auto: bool) -> Self { 95 | return Float { 96 | name: name.as_ref().to_owned(), 97 | stats: HashMap::new(), 98 | goal: FloatData { 99 | value: goal, 100 | precision, 101 | }, 102 | precision, 103 | auto, 104 | inner_data: Default::default(), 105 | }; 106 | } 107 | } 108 | 109 | impl Habit for Float { 110 | type HabitType = FloatData; 111 | 112 | fn name(&self) -> String { 113 | return self.name.clone(); 114 | } 115 | fn set_name(&mut self, n: impl AsRef) { 116 | self.name = n.as_ref().to_owned(); 117 | } 118 | fn kind(&self) -> GoalKind { 119 | GoalKind::Float(self.goal.value, self.goal.precision) 120 | } 121 | fn set_goal(&mut self, g: Self::HabitType) { 122 | self.goal = g; 123 | } 124 | fn get_by_date(&self, date: NaiveDate) -> Option<&Self::HabitType> { 125 | self.stats.get(&date) 126 | } 127 | fn insert_entry(&mut self, date: NaiveDate, val: Self::HabitType) { 128 | *self.stats.entry(date).or_insert(val) = val; 129 | } 130 | fn reached_goal(&self, date: NaiveDate) -> bool { 131 | if let Some(val) = self.stats.get(&date) { 132 | if val >= &self.goal { 133 | return true; 134 | } 135 | } 136 | return false; 137 | } 138 | fn remaining(&self, date: NaiveDate) -> u32 { 139 | if self.reached_goal(date) { 140 | return 0; 141 | } else { 142 | if let Some(&val) = self.stats.get(&date) { 143 | return (self.goal - val).value; 144 | } else { 145 | return self.goal.value; 146 | } 147 | } 148 | } 149 | fn goal(&self) -> u32 { 150 | return self.goal.value; 151 | } 152 | fn modify(&mut self, date: NaiveDate, event: TrackEvent) { 153 | if let Some(val) = self.stats.get_mut(&date) { 154 | match event { 155 | TrackEvent::Increment => *val = val.add(1), 156 | TrackEvent::Decrement => { 157 | if *val > FloatData::zero() { 158 | *val = val.sub(1); 159 | } else { 160 | self.stats.remove(&date); 161 | }; 162 | } 163 | } 164 | } else { 165 | match event { 166 | TrackEvent::Increment => self.insert_entry( 167 | date, 168 | FloatData { 169 | value: 1, 170 | precision: self.precision, 171 | }, 172 | ), 173 | _ => {} 174 | }; 175 | } 176 | } 177 | fn inner_data_ref(&self) -> &InnerData { 178 | &self.inner_data 179 | } 180 | fn inner_data_mut_ref(&mut self) -> &mut InnerData { 181 | &mut self.inner_data 182 | } 183 | fn is_auto(&self) -> bool { 184 | self.auto 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/habit/mod.rs: -------------------------------------------------------------------------------- 1 | use std::default::Default; 2 | 3 | mod traits; 4 | pub use traits::{Habit, HabitWrapper}; 5 | 6 | mod count; 7 | pub use count::Count; 8 | 9 | mod bit; 10 | pub use bit::Bit; 11 | 12 | mod float; 13 | pub use float::Float; 14 | 15 | mod prelude; 16 | pub use prelude::{TrackEvent, ViewMode}; 17 | 18 | use crate::app::Cursor; 19 | 20 | use cursive::direction::Absolute; 21 | 22 | #[derive(Debug, Default)] 23 | pub struct InnerData { 24 | pub cursor: Cursor, 25 | pub view_mode: ViewMode, 26 | } 27 | 28 | impl InnerData { 29 | pub fn move_cursor(&mut self, d: Absolute) { 30 | self.cursor.small_seek(d); 31 | } 32 | pub fn cursor(&self) -> Cursor { 33 | self.cursor 34 | } 35 | pub fn set_view_mode(&mut self, mode: ViewMode) { 36 | self.view_mode = mode; 37 | } 38 | pub fn view_mode(&self) -> ViewMode { 39 | self.view_mode 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/habit/prelude.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::default; 3 | use std::fmt; 4 | 5 | #[derive(Debug, PartialEq)] 6 | pub enum TrackEvent { 7 | Increment, 8 | Decrement, 9 | } 10 | 11 | #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] 12 | pub enum ViewMode { 13 | Day, 14 | Week, 15 | Month, 16 | Year, 17 | } 18 | 19 | impl default::Default for ViewMode { 20 | fn default() -> Self { 21 | ViewMode::Day 22 | } 23 | } 24 | 25 | impl fmt::Display for ViewMode { 26 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 27 | match self { 28 | ViewMode::Day => write!(f, "DAY"), 29 | ViewMode::Week => write!(f, "WEEK"), 30 | ViewMode::Month => write!(f, "MONTH"), 31 | ViewMode::Year => write!(f, "YEAR"), 32 | } 33 | } 34 | } 35 | 36 | pub fn default_auto() -> bool { 37 | false 38 | } 39 | -------------------------------------------------------------------------------- /src/habit/traits.rs: -------------------------------------------------------------------------------- 1 | use chrono::NaiveDate; 2 | use cursive::direction::Direction; 3 | use cursive::event::{Event, EventResult}; 4 | use cursive::view::CannotFocus; 5 | use cursive::{Printer, Vec2}; 6 | use typetag; 7 | 8 | use crate::command::GoalKind; 9 | use crate::habit::{Bit, Count, Float, InnerData, TrackEvent}; 10 | use crate::views::ShadowView; 11 | 12 | pub trait Habit { 13 | type HabitType; 14 | 15 | fn get_by_date(&self, date: NaiveDate) -> Option<&Self::HabitType>; 16 | fn goal(&self) -> u32; 17 | fn insert_entry(&mut self, date: NaiveDate, val: Self::HabitType); 18 | fn modify(&mut self, date: NaiveDate, event: TrackEvent); 19 | fn name(&self) -> String; 20 | fn reached_goal(&self, date: NaiveDate) -> bool; 21 | fn remaining(&self, date: NaiveDate) -> u32; 22 | fn set_goal(&mut self, goal: Self::HabitType); 23 | fn set_name(&mut self, name: impl AsRef); 24 | fn kind(&self) -> GoalKind; 25 | 26 | fn inner_data_ref(&self) -> &InnerData; 27 | fn inner_data_mut_ref(&mut self) -> &mut InnerData; 28 | 29 | fn is_auto(&self) -> bool; 30 | } 31 | 32 | #[typetag::serde(tag = "type")] 33 | pub trait HabitWrapper: erased_serde::Serialize { 34 | fn draw(&self, printer: &Printer); 35 | fn goal(&self) -> u32; 36 | fn kind(&self) -> GoalKind; 37 | fn modify(&mut self, date: NaiveDate, event: TrackEvent); 38 | fn name(&self) -> String; 39 | fn on_event(&mut self, event: Event) -> EventResult; 40 | fn remaining(&self, date: NaiveDate) -> u32; 41 | fn required_size(&mut self, _: Vec2) -> Vec2; 42 | fn take_focus(&mut self, _: Direction) -> Result; 43 | 44 | fn inner_data_ref(&self) -> &InnerData; 45 | fn inner_data_mut_ref(&mut self) -> &mut InnerData; 46 | 47 | fn is_auto(&self) -> bool; 48 | } 49 | 50 | macro_rules! auto_habit_impl { 51 | ($struct_name:ident) => { 52 | #[typetag::serde] 53 | impl HabitWrapper for $struct_name { 54 | // ShadowView 55 | fn draw(&self, printer: &Printer) { 56 | ShadowView::draw(self, printer) 57 | } 58 | fn on_event(&mut self, event: Event) -> EventResult { 59 | ShadowView::on_event(self, event) 60 | } 61 | fn required_size(&mut self, x: Vec2) -> Vec2 { 62 | ShadowView::required_size(self, x) 63 | } 64 | fn take_focus(&mut self, d: Direction) -> Result { 65 | ShadowView::take_focus(self, d) 66 | } 67 | 68 | // Habit 69 | fn remaining(&self, date: NaiveDate) -> u32 { 70 | Habit::remaining(self, date) 71 | } 72 | fn goal(&self) -> u32 { 73 | Habit::goal(self) 74 | } 75 | fn kind(&self) -> GoalKind { 76 | Habit::kind(self) 77 | } 78 | fn modify(&mut self, date: NaiveDate, event: TrackEvent) { 79 | Habit::modify(self, date, event); 80 | } 81 | fn name(&self) -> String { 82 | Habit::name(self) 83 | } 84 | fn inner_data_ref(&self) -> &InnerData { 85 | Habit::inner_data_ref(self) 86 | } 87 | fn inner_data_mut_ref(&mut self) -> &mut InnerData { 88 | Habit::inner_data_mut_ref(self) 89 | } 90 | fn is_auto(&self) -> bool { 91 | Habit::is_auto(self) 92 | } 93 | } 94 | }; 95 | } 96 | 97 | macro_rules! generate_implementations { 98 | ($($x:ident),*) => ( 99 | $( 100 | auto_habit_impl!($x); 101 | )* 102 | ); 103 | } 104 | 105 | generate_implementations!(Count, Bit, Float); 106 | -------------------------------------------------------------------------------- /src/keybinds.rs: -------------------------------------------------------------------------------- 1 | use std::convert::From; 2 | 3 | use cursive::event::Event as CursiveEvent; 4 | use serde::ser; 5 | use serde::{self, Deserialize, Serialize, Serializer}; 6 | 7 | #[derive(Debug, PartialEq)] 8 | struct Event(CursiveEvent); 9 | 10 | macro_rules! event { 11 | ($thing:expr) => { 12 | Event { 0: $thing }; 13 | }; 14 | } 15 | 16 | impl From for Event 17 | where 18 | T: AsRef, 19 | { 20 | fn from(key: T) -> Self { 21 | let key = key.as_ref(); 22 | if key.len() == 1 { 23 | // single key 24 | return event!(CursiveEvent::Char(key.chars().nth(0).unwrap())); 25 | } else if (key.starts_with("c-") || key.starts_with("C-")) && key.len() == 3 { 26 | // ctrl-key 27 | return event!(CursiveEvent::CtrlChar(key.chars().nth(2).unwrap())); 28 | } else { 29 | panic!( 30 | r"Invalid keybind in configuration! 31 | (I intend to handle this error gracefully in the near future)" 32 | ); 33 | } 34 | } 35 | } 36 | 37 | enum Bind { 38 | Char(char), 39 | CtrlChar(char), 40 | AltChar(char), 41 | } 42 | 43 | impl Serialize for Bind { 44 | fn serialize(&self, serializer: S) -> Result 45 | where 46 | S: Serializer, 47 | { 48 | match self { 49 | Bind::Char(c) => serializer.serialize_newtype_variant("bind", 0, "regular", &c), 50 | Bind::CtrlChar(c) => serializer.serialize_newtype_variant("bind", 0, "ctrl", &c), 51 | Bind::AltChar(c) => serializer.serialize_newtype_variant("bind", 0, "alt", &c), 52 | } 53 | } 54 | } 55 | 56 | impl Deserialize for Bind { 57 | fn deserialize(deserializer: D) -> Result 58 | where 59 | D: Deserializer<'de>, 60 | { 61 | eprintln!("hell = {:#?}", hell); 62 | } 63 | } 64 | 65 | impl From for CursiveEvent { 66 | fn from(key: Bind) -> Self { 67 | match key { 68 | Bind::Char(c) => CursiveEvent::Char(c), 69 | Bind::CtrlChar(c) => CursiveEvent::Char(c), 70 | Bind::AltChar(c) => CursiveEvent::AltChar(c), 71 | } 72 | } 73 | } 74 | 75 | #[derive(Serialize, Deserialize)] 76 | pub struct KeyBinds { 77 | grid: Movement, 78 | cursor: Movement, 79 | week_mode: Bind, 80 | global_week_mode: Bind, 81 | } 82 | 83 | #[derive(Serialize, Deserialize)] 84 | pub struct Movement { 85 | up: Bind, 86 | down: Bind, 87 | left: Bind, 88 | right: Bind, 89 | } 90 | 91 | impl Movement { 92 | pub fn new(left: char, down: char, up: char, right: char) -> Self { 93 | return Movement { 94 | up: Bind::Char(up), 95 | down: Bind::Char(down), 96 | left: Bind::Char(left), 97 | right: Bind::Char(right), 98 | }; 99 | } 100 | } 101 | 102 | impl std::default::Default for KeyBinds { 103 | fn default() -> Self { 104 | let grid = Movement::new('h', 'j', 'k', 'l'); 105 | let cursor = Movement::new('H', 'J', 'K', 'L'); 106 | return KeyBinds { 107 | grid, 108 | cursor, 109 | week_mode: Bind::Char('v'), 110 | global_week_mode: Bind::Char('V'), 111 | }; 112 | } 113 | } 114 | 115 | #[cfg(test)] 116 | mod tests { 117 | use super::*; 118 | 119 | #[test] 120 | fn normal_keybind() { 121 | let bind = "X"; 122 | let expected = CursiveEvent::Char('X'); 123 | assert_eq!(Event::from(bind), event!(expected)); 124 | } 125 | 126 | #[test] 127 | fn control_keybind() { 128 | let bind = "C-x"; 129 | let expected = CursiveEvent::CtrlChar('x'); 130 | assert_eq!(Event::from(bind), event!(expected)); 131 | } 132 | 133 | #[test] 134 | fn lower_case_control_keybind() { 135 | let bind = "c-x"; 136 | let expected = CursiveEvent::CtrlChar('x'); 137 | assert_eq!(Event::from(bind), event!(expected)); 138 | } 139 | 140 | #[test] 141 | #[should_panic] 142 | fn very_long_and_wrong_keybind() { 143 | let bind = "alksdjfalkjdf"; 144 | Event::from(bind); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_must_use)] 2 | 3 | mod app; 4 | mod command; 5 | mod habit; 6 | mod theme; 7 | mod utils; 8 | mod views; 9 | 10 | use crate::app::App; 11 | use crate::command::{open_command_window, Command}; 12 | use crate::utils::{load_configuration_file, AppConfig}; 13 | 14 | use clap::{App as ClapApp, Arg}; 15 | 16 | #[cfg(any(feature = "termion-backend", feature = "default"))] 17 | use cursive::termion; 18 | 19 | #[cfg(feature = "crossterm-backend")] 20 | use cursive::crossterm; 21 | 22 | use cursive::views::{LinearLayout, NamedView}; 23 | use lazy_static::lazy_static; 24 | 25 | lazy_static! { 26 | pub static ref CONFIGURATION: AppConfig = load_configuration_file(); 27 | } 28 | 29 | fn main() { 30 | let matches = ClapApp::new(env!("CARGO_PKG_NAME")) 31 | .version(env!("CARGO_PKG_VERSION")) 32 | .author(env!("CARGO_PKG_AUTHORS")) 33 | .about(env!("CARGO_PKG_DESCRIPTION")) 34 | .arg( 35 | Arg::with_name("command") 36 | .short("c") 37 | .long("command") 38 | .takes_value(true) 39 | .value_name("CMD") 40 | .help("run a dijo command"), 41 | ) 42 | .arg( 43 | Arg::with_name("list") 44 | .short("l") 45 | .long("list") 46 | .takes_value(false) 47 | .help("list dijo habits") 48 | .conflicts_with("command"), 49 | ) 50 | .get_matches(); 51 | if let Some(c) = matches.value_of("command") { 52 | let command = Command::from_string(c); 53 | match command { 54 | Ok(Command::TrackUp(_)) | Ok(Command::TrackDown(_)) => { 55 | let mut app = App::load_state(); 56 | app.parse_command(command); 57 | app.save_state(); 58 | } 59 | Err(e) => { 60 | eprintln!("{}", e); 61 | } 62 | _ => eprintln!( 63 | "Commands other than `track-up` and `track-down` are currently not supported!" 64 | ), 65 | } 66 | } else if matches.is_present("list") { 67 | for h in App::load_state().list_habits() { 68 | println!("{}", h); 69 | } 70 | } else { 71 | #[cfg(any(feature = "termion-backend", feature = "default"))] 72 | let mut s = termion(); 73 | 74 | #[cfg(feature = "crossterm-backend")] 75 | let mut s = crossterm(); 76 | 77 | let app = App::load_state(); 78 | let layout = NamedView::new( 79 | "Frame", 80 | LinearLayout::vertical().child(NamedView::new("Main", app)), 81 | ); 82 | s.add_layer(layout); 83 | s.add_global_callback(':', |s| open_command_window(s)); 84 | 85 | s.set_theme(theme::theme_gen()); 86 | s.run(); 87 | 88 | s.call_on_name("Main", |app: &mut App| app.save_state()); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/theme.rs: -------------------------------------------------------------------------------- 1 | use cursive::theme::Color::{self, *}; 2 | use cursive::theme::PaletteColor::*; 3 | use cursive::theme::{BorderStyle, Palette, Theme}; 4 | 5 | pub fn pallete_gen() -> Palette { 6 | let mut p = Palette::default(); 7 | p[Background] = TerminalDefault; 8 | p[Shadow] = TerminalDefault; 9 | p[View] = TerminalDefault; 10 | p[Primary] = TerminalDefault; 11 | p[Secondary] = TerminalDefault; 12 | p[Tertiary] = TerminalDefault; 13 | p[TitlePrimary] = TerminalDefault; 14 | p[Highlight] = TerminalDefault; 15 | p[HighlightInactive] = TerminalDefault; 16 | 17 | return p; 18 | } 19 | 20 | pub fn theme_gen() -> Theme { 21 | let mut t = Theme::default(); 22 | t.shadow = false; 23 | t.borders = BorderStyle::None; 24 | t.palette = pallete_gen(); 25 | return t; 26 | } 27 | 28 | pub fn cursor_bg() -> Color { 29 | Light(cursive::theme::BaseColor::Black) 30 | } 31 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use cursive::theme::{BaseColor, Color}; 2 | use directories::ProjectDirs; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use std; 6 | use std::default::Default; 7 | use std::fs::{self, File, OpenOptions}; 8 | use std::io::{Read, Write}; 9 | use std::path::PathBuf; 10 | 11 | pub const VIEW_WIDTH: usize = 25; 12 | pub const VIEW_HEIGHT: usize = 8; 13 | pub const GRID_WIDTH: usize = 3; 14 | 15 | #[derive(Serialize, Deserialize)] 16 | pub struct Characters { 17 | #[serde(default = "base_char")] 18 | pub true_chr: char, 19 | #[serde(default = "base_char")] 20 | pub false_chr: char, 21 | #[serde(default = "base_char")] 22 | pub future_chr: char, 23 | } 24 | 25 | fn base_char() -> char { 26 | '·' 27 | } 28 | 29 | impl Default for Characters { 30 | fn default() -> Self { 31 | Characters { 32 | true_chr: '·', 33 | false_chr: '·', 34 | future_chr: '·', 35 | } 36 | } 37 | } 38 | 39 | #[derive(Serialize, Deserialize)] 40 | pub struct Colors { 41 | #[serde(default = "cyan")] 42 | pub reached: String, 43 | #[serde(default = "magenta")] 44 | pub todo: String, 45 | #[serde(default = "light_black")] 46 | pub inactive: String, 47 | } 48 | 49 | fn cyan() -> String { 50 | "cyan".into() 51 | } 52 | fn magenta() -> String { 53 | "magenta".into() 54 | } 55 | fn light_black() -> String { 56 | "light black".into() 57 | } 58 | 59 | impl Default for Colors { 60 | fn default() -> Self { 61 | Colors { 62 | reached: cyan(), 63 | todo: magenta(), 64 | inactive: light_black(), 65 | } 66 | } 67 | } 68 | 69 | #[derive(Serialize, Deserialize)] 70 | pub struct AppConfig { 71 | #[serde(default)] 72 | pub look: Characters, 73 | 74 | #[serde(default)] 75 | pub colors: Colors, 76 | } 77 | 78 | impl Default for AppConfig { 79 | fn default() -> Self { 80 | AppConfig { 81 | look: Default::default(), 82 | colors: Default::default(), 83 | } 84 | } 85 | } 86 | 87 | impl AppConfig { 88 | // TODO: implement string parsing from config.json 89 | pub fn reached_color(&self) -> Color { 90 | return Color::parse(&self.colors.reached).unwrap_or(Color::Dark(BaseColor::Cyan)); 91 | } 92 | pub fn todo_color(&self) -> Color { 93 | return Color::parse(&self.colors.todo).unwrap_or(Color::Dark(BaseColor::Magenta)); 94 | } 95 | pub fn inactive_color(&self) -> Color { 96 | return Color::parse(&self.colors.inactive).unwrap_or(Color::Light(BaseColor::Black)); 97 | } 98 | } 99 | 100 | pub fn load_configuration_file() -> AppConfig { 101 | let cf = config_file(); 102 | if let Ok(ref mut f) = File::open(&cf) { 103 | let mut j = String::new(); 104 | f.read_to_string(&mut j); 105 | return toml::from_str(&j).unwrap_or_else(|e| panic!("Invalid config file: `{}`", e)); 106 | } else { 107 | if let Ok(dc) = toml::to_string(&AppConfig::default()) { 108 | match OpenOptions::new().create(true).write(true).open(&cf) { 109 | Ok(ref mut file) => file.write(dc.as_bytes()).unwrap(), 110 | Err(_) => panic!("Unable to write config file to disk!"), 111 | }; 112 | } 113 | return Default::default(); 114 | } 115 | } 116 | 117 | fn project_dirs() -> ProjectDirs { 118 | ProjectDirs::from("rs", "nerdypepper", "dijo") 119 | .unwrap_or_else(|| panic!("Invalid home directory!")) 120 | } 121 | 122 | pub fn config_file() -> PathBuf { 123 | let proj_dirs = project_dirs(); 124 | let mut data_file = PathBuf::from(proj_dirs.config_dir()); 125 | fs::create_dir_all(&data_file); 126 | data_file.push("config.toml"); 127 | return data_file; 128 | } 129 | 130 | pub fn habit_file() -> PathBuf { 131 | let proj_dirs = project_dirs(); 132 | let mut data_file = PathBuf::from(proj_dirs.data_dir()); 133 | fs::create_dir_all(&data_file); 134 | data_file.push("habit_record.json"); 135 | return data_file; 136 | } 137 | 138 | pub fn auto_habit_file() -> PathBuf { 139 | let proj_dirs = project_dirs(); 140 | let mut data_file = PathBuf::from(proj_dirs.data_dir()); 141 | fs::create_dir_all(&data_file); 142 | data_file.push("habit_record[auto].json"); 143 | return data_file; 144 | } 145 | -------------------------------------------------------------------------------- /src/views.rs: -------------------------------------------------------------------------------- 1 | use cursive::direction::Direction; 2 | use cursive::event::{Event, EventResult, Key}; 3 | use cursive::theme::{ColorStyle, Effect, Style}; 4 | use cursive::view::{CannotFocus, View}; 5 | use cursive::{Printer, Vec2}; 6 | 7 | use chrono::prelude::*; 8 | use chrono::{Local, NaiveDate}; 9 | 10 | use crate::habit::{Bit, Count, Float, Habit, TrackEvent, ViewMode}; 11 | use crate::theme::cursor_bg; 12 | use crate::utils::VIEW_WIDTH; 13 | 14 | use crate::CONFIGURATION; 15 | 16 | pub trait ShadowView { 17 | fn draw(&self, printer: &Printer); 18 | fn required_size(&mut self, _: Vec2) -> Vec2; 19 | fn take_focus(&mut self, _: Direction) -> Result; 20 | fn on_event(&mut self, e: Event) -> EventResult; 21 | } 22 | 23 | // the only way to not rewrite each View implementation for trait 24 | // objects of Habit is to rewrite the View trait itself. 25 | impl ShadowView for T 26 | where 27 | T: Habit, 28 | T::HabitType: std::fmt::Display, 29 | { 30 | fn draw(&self, printer: &Printer) { 31 | // let now = if self.view_month_offset() == 0 { 32 | // Local::today() 33 | // } else { 34 | // Local::today() 35 | // .checked_sub_signed(Duration::weeks(4 * self.view_month_offset() as i64)) 36 | // .unwrap() 37 | // }; 38 | let now = self.inner_data_ref().cursor().0; 39 | let is_today = now == Local::now().naive_local().date(); 40 | let year = now.year(); 41 | let month = now.month(); 42 | 43 | let goal_reached_style = Style::from(CONFIGURATION.reached_color()); 44 | let future_style = Style::from(CONFIGURATION.inactive_color()); 45 | 46 | let strikethrough = Style::from(Effect::Strikethrough); 47 | 48 | let goal_status = is_today && self.reached_goal(Local::now().naive_local().date()); 49 | 50 | printer.with_style( 51 | Style::merge(&[ 52 | if goal_status { 53 | strikethrough 54 | } else { 55 | Style::none() 56 | }, 57 | if !printer.focused { 58 | future_style 59 | } else { 60 | Style::none() 61 | }, 62 | ]), 63 | |p| { 64 | p.print( 65 | (0, 0), 66 | &format!(" {:.width$} ", self.name(), width = VIEW_WIDTH - 6), 67 | ); 68 | }, 69 | ); 70 | 71 | let draw_week = |printer: &Printer| { 72 | let days = (1..31) 73 | .map(|i| NaiveDate::from_ymd_opt(year, month, i)) 74 | .flatten() // dates 28-31 may not exist, ignore them if they don't 75 | .collect::>(); 76 | for (week, line_nr) in days.chunks(7).zip(2..) { 77 | let weekly_goal = self.goal() * week.len() as u32; 78 | let is_this_week = week.contains(&Local::now().naive_local().date()); 79 | let remaining = week.iter().map(|&i| self.remaining(i)).sum::(); 80 | let completions = weekly_goal - remaining; 81 | let full = VIEW_WIDTH - 8; 82 | let bars_to_fill = if weekly_goal > 0 { 83 | (completions * full as u32) / weekly_goal 84 | } else { 85 | 0 86 | }; 87 | let percentage = if weekly_goal > 0 { 88 | (completions as f64 * 100.) / weekly_goal as f64 89 | } else { 90 | 0.0 91 | }; 92 | printer.with_style(future_style, |p| { 93 | p.print((4, line_nr), &"─".repeat(full)); 94 | }); 95 | printer.with_style(goal_reached_style, |p| { 96 | p.print((4, line_nr), &"─".repeat(bars_to_fill as usize)); 97 | }); 98 | printer.with_style( 99 | if is_this_week { 100 | Style::none() 101 | } else { 102 | future_style 103 | }, 104 | |p| { 105 | p.print((0, line_nr), &format!("{:2.0}% ", percentage)); 106 | }, 107 | ); 108 | } 109 | }; 110 | 111 | let draw_day = |printer: &Printer| { 112 | let mut i = 0; 113 | while let Some(d) = NaiveDate::from_ymd_opt(year, month, i + 1) { 114 | let mut day_style = Style::none(); 115 | let mut fs = future_style; 116 | let grs = ColorStyle::front(CONFIGURATION.reached_color()); 117 | let ts = ColorStyle::front(CONFIGURATION.todo_color()); 118 | let cs = ColorStyle::back(cursor_bg()); 119 | 120 | if self.reached_goal(d) { 121 | day_style = day_style.combine(Style::from(grs)); 122 | } else { 123 | day_style = day_style.combine(Style::from(ts)); 124 | } 125 | if d == now && printer.focused { 126 | day_style = day_style.combine(cs); 127 | fs = fs.combine(cs); 128 | } 129 | let coords: Vec2 = ((i % 7) * 3, i / 7 + 2).into(); 130 | if let Some(c) = self.get_by_date(d) { 131 | printer.with_style(day_style, |p| { 132 | p.print(coords, &format!("{:^3}", c)); 133 | }); 134 | } else { 135 | printer.with_style(fs, |p| { 136 | p.print(coords, &format!("{:^3}", CONFIGURATION.look.future_chr)); 137 | }); 138 | } 139 | i += 1; 140 | } 141 | }; 142 | 143 | match self.inner_data_ref().view_mode() { 144 | ViewMode::Day => draw_day(printer), 145 | ViewMode::Week => draw_week(printer), 146 | _ => draw_day(printer), 147 | }; 148 | } 149 | 150 | fn required_size(&mut self, _: Vec2) -> Vec2 { 151 | (25, 6).into() 152 | } 153 | 154 | fn take_focus(&mut self, _: Direction) -> Result { 155 | Ok(EventResult::consumed()) 156 | } 157 | 158 | fn on_event(&mut self, e: Event) -> EventResult { 159 | let now = self.inner_data_mut_ref().cursor().0; 160 | if self.is_auto() { 161 | return EventResult::Ignored; 162 | } 163 | match e { 164 | Event::Key(Key::Enter) | Event::Char('n') => { 165 | self.modify(now, TrackEvent::Increment); 166 | return EventResult::Consumed(None); 167 | } 168 | Event::Key(Key::Backspace) | Event::Char('p') => { 169 | self.modify(now, TrackEvent::Decrement); 170 | return EventResult::Consumed(None); 171 | } 172 | _ => return EventResult::Ignored, 173 | } 174 | } 175 | } 176 | 177 | macro_rules! auto_view_impl { 178 | ($struct_name:ident) => { 179 | impl View for $struct_name { 180 | fn draw(&self, printer: &Printer) { 181 | ShadowView::draw(self, printer); 182 | } 183 | fn required_size(&mut self, x: Vec2) -> Vec2 { 184 | ShadowView::required_size(self, x) 185 | } 186 | fn take_focus(&mut self, d: Direction) -> Result { 187 | ShadowView::take_focus(self, d) 188 | } 189 | fn on_event(&mut self, e: Event) -> EventResult { 190 | ShadowView::on_event(self, e) 191 | } 192 | } 193 | }; 194 | } 195 | 196 | macro_rules! generate_view_impls { 197 | ($($x:ident),*) => ( 198 | $( 199 | auto_view_impl!($x); 200 | )* 201 | ); 202 | } 203 | 204 | generate_view_impls!(Count, Bit, Float); 205 | --------------------------------------------------------------------------------