├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── DOCUMENTATION.md ├── LICENSE.txt ├── README.md └── src ├── app.rs ├── cli ├── add.rs ├── cli_utils.rs ├── complete.rs ├── config.rs ├── delete.rs ├── formats.rs ├── ls.rs └── mod.rs ├── configuration.rs ├── day_of_week.rs ├── lib.rs ├── main.rs ├── repeat.rs ├── task.rs ├── task_form.rs ├── ui ├── all_tasks_page.rs ├── delete_task_page.rs ├── mod.rs └── task_page.rs └── utils.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "allocator-api2" 7 | version = "0.2.21" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 10 | 11 | [[package]] 12 | name = "android_system_properties" 13 | version = "0.1.5" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 16 | dependencies = [ 17 | "libc", 18 | ] 19 | 20 | [[package]] 21 | name = "anyhow" 22 | version = "1.0.69" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" 25 | 26 | [[package]] 27 | name = "autocfg" 28 | version = "1.1.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 31 | 32 | [[package]] 33 | name = "bitflags" 34 | version = "1.3.2" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 37 | 38 | [[package]] 39 | name = "bitflags" 40 | version = "2.9.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 43 | 44 | [[package]] 45 | name = "bumpalo" 46 | version = "3.12.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" 49 | 50 | [[package]] 51 | name = "cassowary" 52 | version = "0.3.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 55 | 56 | [[package]] 57 | name = "castaway" 58 | version = "0.2.3" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" 61 | dependencies = [ 62 | "rustversion", 63 | ] 64 | 65 | [[package]] 66 | name = "cc" 67 | version = "1.0.79" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 70 | 71 | [[package]] 72 | name = "cfg-if" 73 | version = "1.0.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 76 | 77 | [[package]] 78 | name = "chrono" 79 | version = "0.4.23" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" 82 | dependencies = [ 83 | "iana-time-zone", 84 | "js-sys", 85 | "num-integer", 86 | "num-traits", 87 | "time", 88 | "wasm-bindgen", 89 | "winapi", 90 | ] 91 | 92 | [[package]] 93 | name = "clap" 94 | version = "4.1.8" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "c3d7ae14b20b94cb02149ed21a86c423859cbe18dc7ed69845cace50e52b40a5" 97 | dependencies = [ 98 | "bitflags 1.3.2", 99 | "clap_derive", 100 | "clap_lex", 101 | "is-terminal", 102 | "once_cell", 103 | "strsim 0.10.0", 104 | "termcolor", 105 | ] 106 | 107 | [[package]] 108 | name = "clap_derive" 109 | version = "4.1.8" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "44bec8e5c9d09e439c4335b1af0abaab56dcf3b94999a936e1bb47b9134288f0" 112 | dependencies = [ 113 | "heck 0.4.1", 114 | "proc-macro-error", 115 | "proc-macro2", 116 | "quote", 117 | "syn 1.0.109", 118 | ] 119 | 120 | [[package]] 121 | name = "clap_lex" 122 | version = "0.3.2" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09" 125 | dependencies = [ 126 | "os_str_bytes", 127 | ] 128 | 129 | [[package]] 130 | name = "codespan-reporting" 131 | version = "0.11.1" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" 134 | dependencies = [ 135 | "termcolor", 136 | "unicode-width 0.1.10", 137 | ] 138 | 139 | [[package]] 140 | name = "compact_str" 141 | version = "0.8.1" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" 144 | dependencies = [ 145 | "castaway", 146 | "cfg-if", 147 | "itoa", 148 | "rustversion", 149 | "ryu", 150 | "static_assertions", 151 | ] 152 | 153 | [[package]] 154 | name = "convert_case" 155 | version = "0.7.1" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" 158 | dependencies = [ 159 | "unicode-segmentation", 160 | ] 161 | 162 | [[package]] 163 | name = "core-foundation-sys" 164 | version = "0.8.3" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 167 | 168 | [[package]] 169 | name = "crossterm" 170 | version = "0.28.1" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 173 | dependencies = [ 174 | "bitflags 2.9.0", 175 | "crossterm_winapi", 176 | "mio", 177 | "parking_lot", 178 | "rustix 0.38.44", 179 | "signal-hook", 180 | "signal-hook-mio", 181 | "winapi", 182 | ] 183 | 184 | [[package]] 185 | name = "crossterm" 186 | version = "0.29.0" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" 189 | dependencies = [ 190 | "bitflags 2.9.0", 191 | "crossterm_winapi", 192 | "derive_more", 193 | "document-features", 194 | "mio", 195 | "parking_lot", 196 | "rustix 1.0.7", 197 | "signal-hook", 198 | "signal-hook-mio", 199 | "winapi", 200 | ] 201 | 202 | [[package]] 203 | name = "crossterm_winapi" 204 | version = "0.9.1" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 207 | dependencies = [ 208 | "winapi", 209 | ] 210 | 211 | [[package]] 212 | name = "cxx" 213 | version = "1.0.91" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62" 216 | dependencies = [ 217 | "cc", 218 | "cxxbridge-flags", 219 | "cxxbridge-macro", 220 | "link-cplusplus", 221 | ] 222 | 223 | [[package]] 224 | name = "cxx-build" 225 | version = "1.0.91" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690" 228 | dependencies = [ 229 | "cc", 230 | "codespan-reporting", 231 | "once_cell", 232 | "proc-macro2", 233 | "quote", 234 | "scratch", 235 | "syn 1.0.109", 236 | ] 237 | 238 | [[package]] 239 | name = "cxxbridge-flags" 240 | version = "1.0.91" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf" 243 | 244 | [[package]] 245 | name = "cxxbridge-macro" 246 | version = "1.0.91" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" 249 | dependencies = [ 250 | "proc-macro2", 251 | "quote", 252 | "syn 1.0.109", 253 | ] 254 | 255 | [[package]] 256 | name = "darling" 257 | version = "0.20.11" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 260 | dependencies = [ 261 | "darling_core", 262 | "darling_macro", 263 | ] 264 | 265 | [[package]] 266 | name = "darling_core" 267 | version = "0.20.11" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 270 | dependencies = [ 271 | "fnv", 272 | "ident_case", 273 | "proc-macro2", 274 | "quote", 275 | "strsim 0.11.1", 276 | "syn 2.0.101", 277 | ] 278 | 279 | [[package]] 280 | name = "darling_macro" 281 | version = "0.20.11" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 284 | dependencies = [ 285 | "darling_core", 286 | "quote", 287 | "syn 2.0.101", 288 | ] 289 | 290 | [[package]] 291 | name = "derive_more" 292 | version = "2.0.1" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 295 | dependencies = [ 296 | "derive_more-impl", 297 | ] 298 | 299 | [[package]] 300 | name = "derive_more-impl" 301 | version = "2.0.1" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 304 | dependencies = [ 305 | "convert_case", 306 | "proc-macro2", 307 | "quote", 308 | "syn 2.0.101", 309 | ] 310 | 311 | [[package]] 312 | name = "dirs" 313 | version = "4.0.0" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" 316 | dependencies = [ 317 | "dirs-sys", 318 | ] 319 | 320 | [[package]] 321 | name = "dirs-sys" 322 | version = "0.3.7" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" 325 | dependencies = [ 326 | "libc", 327 | "redox_users", 328 | "winapi", 329 | ] 330 | 331 | [[package]] 332 | name = "document-features" 333 | version = "0.2.11" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" 336 | dependencies = [ 337 | "litrs", 338 | ] 339 | 340 | [[package]] 341 | name = "either" 342 | version = "1.8.1" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 345 | 346 | [[package]] 347 | name = "equivalent" 348 | version = "1.0.2" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 351 | 352 | [[package]] 353 | name = "errno" 354 | version = "0.2.8" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 357 | dependencies = [ 358 | "errno-dragonfly", 359 | "libc", 360 | "winapi", 361 | ] 362 | 363 | [[package]] 364 | name = "errno" 365 | version = "0.3.11" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 368 | dependencies = [ 369 | "libc", 370 | "windows-sys 0.59.0", 371 | ] 372 | 373 | [[package]] 374 | name = "errno-dragonfly" 375 | version = "0.1.2" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 378 | dependencies = [ 379 | "cc", 380 | "libc", 381 | ] 382 | 383 | [[package]] 384 | name = "fnv" 385 | version = "1.0.7" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 388 | 389 | [[package]] 390 | name = "foldhash" 391 | version = "0.1.5" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 394 | 395 | [[package]] 396 | name = "getrandom" 397 | version = "0.2.8" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" 400 | dependencies = [ 401 | "cfg-if", 402 | "libc", 403 | "wasi 0.11.0+wasi-snapshot-preview1", 404 | ] 405 | 406 | [[package]] 407 | name = "hashbrown" 408 | version = "0.15.3" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" 411 | dependencies = [ 412 | "allocator-api2", 413 | "equivalent", 414 | "foldhash", 415 | ] 416 | 417 | [[package]] 418 | name = "heck" 419 | version = "0.4.1" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 422 | 423 | [[package]] 424 | name = "heck" 425 | version = "0.5.0" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 428 | 429 | [[package]] 430 | name = "hermit-abi" 431 | version = "0.3.1" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 434 | 435 | [[package]] 436 | name = "iana-time-zone" 437 | version = "0.1.53" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" 440 | dependencies = [ 441 | "android_system_properties", 442 | "core-foundation-sys", 443 | "iana-time-zone-haiku", 444 | "js-sys", 445 | "wasm-bindgen", 446 | "winapi", 447 | ] 448 | 449 | [[package]] 450 | name = "iana-time-zone-haiku" 451 | version = "0.1.1" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" 454 | dependencies = [ 455 | "cxx", 456 | "cxx-build", 457 | ] 458 | 459 | [[package]] 460 | name = "ident_case" 461 | version = "1.0.1" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 464 | 465 | [[package]] 466 | name = "indoc" 467 | version = "2.0.6" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 470 | 471 | [[package]] 472 | name = "instability" 473 | version = "0.3.7" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" 476 | dependencies = [ 477 | "darling", 478 | "indoc", 479 | "proc-macro2", 480 | "quote", 481 | "syn 2.0.101", 482 | ] 483 | 484 | [[package]] 485 | name = "io-lifetimes" 486 | version = "1.0.5" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" 489 | dependencies = [ 490 | "libc", 491 | "windows-sys 0.45.0", 492 | ] 493 | 494 | [[package]] 495 | name = "is-terminal" 496 | version = "0.4.4" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" 499 | dependencies = [ 500 | "hermit-abi", 501 | "io-lifetimes", 502 | "rustix 0.36.9", 503 | "windows-sys 0.45.0", 504 | ] 505 | 506 | [[package]] 507 | name = "itertools" 508 | version = "0.10.5" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 511 | dependencies = [ 512 | "either", 513 | ] 514 | 515 | [[package]] 516 | name = "itertools" 517 | version = "0.13.0" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 520 | dependencies = [ 521 | "either", 522 | ] 523 | 524 | [[package]] 525 | name = "itoa" 526 | version = "1.0.6" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" 529 | 530 | [[package]] 531 | name = "js-sys" 532 | version = "0.3.61" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" 535 | dependencies = [ 536 | "wasm-bindgen", 537 | ] 538 | 539 | [[package]] 540 | name = "libc" 541 | version = "0.2.172" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 544 | 545 | [[package]] 546 | name = "link-cplusplus" 547 | version = "1.0.8" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" 550 | dependencies = [ 551 | "cc", 552 | ] 553 | 554 | [[package]] 555 | name = "linux-raw-sys" 556 | version = "0.1.4" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" 559 | 560 | [[package]] 561 | name = "linux-raw-sys" 562 | version = "0.4.15" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 565 | 566 | [[package]] 567 | name = "linux-raw-sys" 568 | version = "0.9.4" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 571 | 572 | [[package]] 573 | name = "litrs" 574 | version = "0.4.1" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" 577 | 578 | [[package]] 579 | name = "lock_api" 580 | version = "0.4.9" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" 583 | dependencies = [ 584 | "autocfg", 585 | "scopeguard", 586 | ] 587 | 588 | [[package]] 589 | name = "log" 590 | version = "0.4.17" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 593 | dependencies = [ 594 | "cfg-if", 595 | ] 596 | 597 | [[package]] 598 | name = "lru" 599 | version = "0.12.5" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 602 | dependencies = [ 603 | "hashbrown", 604 | ] 605 | 606 | [[package]] 607 | name = "mio" 608 | version = "1.0.3" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 611 | dependencies = [ 612 | "libc", 613 | "log", 614 | "wasi 0.11.0+wasi-snapshot-preview1", 615 | "windows-sys 0.52.0", 616 | ] 617 | 618 | [[package]] 619 | name = "num-integer" 620 | version = "0.1.45" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 623 | dependencies = [ 624 | "autocfg", 625 | "num-traits", 626 | ] 627 | 628 | [[package]] 629 | name = "num-traits" 630 | version = "0.2.15" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 633 | dependencies = [ 634 | "autocfg", 635 | ] 636 | 637 | [[package]] 638 | name = "once_cell" 639 | version = "1.17.1" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 642 | 643 | [[package]] 644 | name = "open" 645 | version = "4.0.0" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "bd61e3bf9d78956c72ee864bba52431f7f43994b21a17e9e72596a81bd61075b" 648 | dependencies = [ 649 | "pathdiff", 650 | ] 651 | 652 | [[package]] 653 | name = "os_str_bytes" 654 | version = "6.4.1" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" 657 | 658 | [[package]] 659 | name = "parking_lot" 660 | version = "0.12.1" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 663 | dependencies = [ 664 | "lock_api", 665 | "parking_lot_core", 666 | ] 667 | 668 | [[package]] 669 | name = "parking_lot_core" 670 | version = "0.9.7" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" 673 | dependencies = [ 674 | "cfg-if", 675 | "libc", 676 | "redox_syscall", 677 | "smallvec", 678 | "windows-sys 0.45.0", 679 | ] 680 | 681 | [[package]] 682 | name = "paste" 683 | version = "1.0.15" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 686 | 687 | [[package]] 688 | name = "pathdiff" 689 | version = "0.2.1" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" 692 | 693 | [[package]] 694 | name = "proc-macro-error" 695 | version = "1.0.4" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 698 | dependencies = [ 699 | "proc-macro-error-attr", 700 | "proc-macro2", 701 | "quote", 702 | "syn 1.0.109", 703 | "version_check", 704 | ] 705 | 706 | [[package]] 707 | name = "proc-macro-error-attr" 708 | version = "1.0.4" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 711 | dependencies = [ 712 | "proc-macro2", 713 | "quote", 714 | "version_check", 715 | ] 716 | 717 | [[package]] 718 | name = "proc-macro2" 719 | version = "1.0.95" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 722 | dependencies = [ 723 | "unicode-ident", 724 | ] 725 | 726 | [[package]] 727 | name = "quote" 728 | version = "1.0.40" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 731 | dependencies = [ 732 | "proc-macro2", 733 | ] 734 | 735 | [[package]] 736 | name = "ratatui" 737 | version = "0.29.0" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" 740 | dependencies = [ 741 | "bitflags 2.9.0", 742 | "cassowary", 743 | "compact_str", 744 | "crossterm 0.28.1", 745 | "indoc", 746 | "instability", 747 | "itertools 0.13.0", 748 | "lru", 749 | "paste", 750 | "strum", 751 | "unicode-segmentation", 752 | "unicode-truncate", 753 | "unicode-width 0.2.0", 754 | ] 755 | 756 | [[package]] 757 | name = "redox_syscall" 758 | version = "0.2.16" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 761 | dependencies = [ 762 | "bitflags 1.3.2", 763 | ] 764 | 765 | [[package]] 766 | name = "redox_users" 767 | version = "0.4.3" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 770 | dependencies = [ 771 | "getrandom", 772 | "redox_syscall", 773 | "thiserror", 774 | ] 775 | 776 | [[package]] 777 | name = "rustix" 778 | version = "0.36.9" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc" 781 | dependencies = [ 782 | "bitflags 1.3.2", 783 | "errno 0.2.8", 784 | "io-lifetimes", 785 | "libc", 786 | "linux-raw-sys 0.1.4", 787 | "windows-sys 0.45.0", 788 | ] 789 | 790 | [[package]] 791 | name = "rustix" 792 | version = "0.38.44" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 795 | dependencies = [ 796 | "bitflags 2.9.0", 797 | "errno 0.3.11", 798 | "libc", 799 | "linux-raw-sys 0.4.15", 800 | "windows-sys 0.59.0", 801 | ] 802 | 803 | [[package]] 804 | name = "rustix" 805 | version = "1.0.7" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 808 | dependencies = [ 809 | "bitflags 2.9.0", 810 | "errno 0.3.11", 811 | "libc", 812 | "linux-raw-sys 0.9.4", 813 | "windows-sys 0.59.0", 814 | ] 815 | 816 | [[package]] 817 | name = "rustversion" 818 | version = "1.0.20" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 821 | 822 | [[package]] 823 | name = "ryu" 824 | version = "1.0.13" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" 827 | 828 | [[package]] 829 | name = "scopeguard" 830 | version = "1.1.0" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 833 | 834 | [[package]] 835 | name = "scratch" 836 | version = "1.0.4" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "5d5e082f6ea090deaf0e6dd04b68360fd5cddb152af6ce8927c9d25db299f98c" 839 | 840 | [[package]] 841 | name = "serde" 842 | version = "1.0.152" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" 845 | dependencies = [ 846 | "serde_derive", 847 | ] 848 | 849 | [[package]] 850 | name = "serde_derive" 851 | version = "1.0.152" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" 854 | dependencies = [ 855 | "proc-macro2", 856 | "quote", 857 | "syn 1.0.109", 858 | ] 859 | 860 | [[package]] 861 | name = "serde_json" 862 | version = "1.0.93" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" 865 | dependencies = [ 866 | "itoa", 867 | "ryu", 868 | "serde", 869 | ] 870 | 871 | [[package]] 872 | name = "signal-hook" 873 | version = "0.3.18" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" 876 | dependencies = [ 877 | "libc", 878 | "signal-hook-registry", 879 | ] 880 | 881 | [[package]] 882 | name = "signal-hook-mio" 883 | version = "0.2.4" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 886 | dependencies = [ 887 | "libc", 888 | "mio", 889 | "signal-hook", 890 | ] 891 | 892 | [[package]] 893 | name = "signal-hook-registry" 894 | version = "1.4.1" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 897 | dependencies = [ 898 | "libc", 899 | ] 900 | 901 | [[package]] 902 | name = "smallvec" 903 | version = "1.10.0" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 906 | 907 | [[package]] 908 | name = "static_assertions" 909 | version = "1.1.0" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 912 | 913 | [[package]] 914 | name = "strsim" 915 | version = "0.10.0" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 918 | 919 | [[package]] 920 | name = "strsim" 921 | version = "0.11.1" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 924 | 925 | [[package]] 926 | name = "strum" 927 | version = "0.26.3" 928 | source = "registry+https://github.com/rust-lang/crates.io-index" 929 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 930 | dependencies = [ 931 | "strum_macros", 932 | ] 933 | 934 | [[package]] 935 | name = "strum_macros" 936 | version = "0.26.4" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 939 | dependencies = [ 940 | "heck 0.5.0", 941 | "proc-macro2", 942 | "quote", 943 | "rustversion", 944 | "syn 2.0.101", 945 | ] 946 | 947 | [[package]] 948 | name = "syn" 949 | version = "1.0.109" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 952 | dependencies = [ 953 | "proc-macro2", 954 | "quote", 955 | "unicode-ident", 956 | ] 957 | 958 | [[package]] 959 | name = "syn" 960 | version = "2.0.101" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 963 | dependencies = [ 964 | "proc-macro2", 965 | "quote", 966 | "unicode-ident", 967 | ] 968 | 969 | [[package]] 970 | name = "termcolor" 971 | version = "1.2.0" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" 974 | dependencies = [ 975 | "winapi-util", 976 | ] 977 | 978 | [[package]] 979 | name = "thiserror" 980 | version = "1.0.38" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" 983 | dependencies = [ 984 | "thiserror-impl", 985 | ] 986 | 987 | [[package]] 988 | name = "thiserror-impl" 989 | version = "1.0.38" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" 992 | dependencies = [ 993 | "proc-macro2", 994 | "quote", 995 | "syn 1.0.109", 996 | ] 997 | 998 | [[package]] 999 | name = "time" 1000 | version = "0.1.45" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" 1003 | dependencies = [ 1004 | "libc", 1005 | "wasi 0.10.0+wasi-snapshot-preview1", 1006 | "winapi", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "todui" 1011 | version = "0.1.4" 1012 | dependencies = [ 1013 | "anyhow", 1014 | "chrono", 1015 | "clap", 1016 | "crossterm 0.29.0", 1017 | "dirs", 1018 | "itertools 0.10.5", 1019 | "open", 1020 | "ratatui", 1021 | "serde", 1022 | "serde_json", 1023 | "unicode-width 0.1.10", 1024 | ] 1025 | 1026 | [[package]] 1027 | name = "unicode-ident" 1028 | version = "1.0.7" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "775c11906edafc97bc378816b94585fbd9a054eabaf86fdd0ced94af449efab7" 1031 | 1032 | [[package]] 1033 | name = "unicode-segmentation" 1034 | version = "1.10.1" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 1037 | 1038 | [[package]] 1039 | name = "unicode-truncate" 1040 | version = "1.1.0" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" 1043 | dependencies = [ 1044 | "itertools 0.13.0", 1045 | "unicode-segmentation", 1046 | "unicode-width 0.1.10", 1047 | ] 1048 | 1049 | [[package]] 1050 | name = "unicode-width" 1051 | version = "0.1.10" 1052 | source = "registry+https://github.com/rust-lang/crates.io-index" 1053 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 1054 | 1055 | [[package]] 1056 | name = "unicode-width" 1057 | version = "0.2.0" 1058 | source = "registry+https://github.com/rust-lang/crates.io-index" 1059 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1060 | 1061 | [[package]] 1062 | name = "version_check" 1063 | version = "0.9.4" 1064 | source = "registry+https://github.com/rust-lang/crates.io-index" 1065 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1066 | 1067 | [[package]] 1068 | name = "wasi" 1069 | version = "0.10.0+wasi-snapshot-preview1" 1070 | source = "registry+https://github.com/rust-lang/crates.io-index" 1071 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 1072 | 1073 | [[package]] 1074 | name = "wasi" 1075 | version = "0.11.0+wasi-snapshot-preview1" 1076 | source = "registry+https://github.com/rust-lang/crates.io-index" 1077 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1078 | 1079 | [[package]] 1080 | name = "wasm-bindgen" 1081 | version = "0.2.84" 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" 1083 | checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" 1084 | dependencies = [ 1085 | "cfg-if", 1086 | "wasm-bindgen-macro", 1087 | ] 1088 | 1089 | [[package]] 1090 | name = "wasm-bindgen-backend" 1091 | version = "0.2.84" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" 1094 | dependencies = [ 1095 | "bumpalo", 1096 | "log", 1097 | "once_cell", 1098 | "proc-macro2", 1099 | "quote", 1100 | "syn 1.0.109", 1101 | "wasm-bindgen-shared", 1102 | ] 1103 | 1104 | [[package]] 1105 | name = "wasm-bindgen-macro" 1106 | version = "0.2.84" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" 1109 | dependencies = [ 1110 | "quote", 1111 | "wasm-bindgen-macro-support", 1112 | ] 1113 | 1114 | [[package]] 1115 | name = "wasm-bindgen-macro-support" 1116 | version = "0.2.84" 1117 | source = "registry+https://github.com/rust-lang/crates.io-index" 1118 | checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" 1119 | dependencies = [ 1120 | "proc-macro2", 1121 | "quote", 1122 | "syn 1.0.109", 1123 | "wasm-bindgen-backend", 1124 | "wasm-bindgen-shared", 1125 | ] 1126 | 1127 | [[package]] 1128 | name = "wasm-bindgen-shared" 1129 | version = "0.2.84" 1130 | source = "registry+https://github.com/rust-lang/crates.io-index" 1131 | checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" 1132 | 1133 | [[package]] 1134 | name = "winapi" 1135 | version = "0.3.9" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1138 | dependencies = [ 1139 | "winapi-i686-pc-windows-gnu", 1140 | "winapi-x86_64-pc-windows-gnu", 1141 | ] 1142 | 1143 | [[package]] 1144 | name = "winapi-i686-pc-windows-gnu" 1145 | version = "0.4.0" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1148 | 1149 | [[package]] 1150 | name = "winapi-util" 1151 | version = "0.1.5" 1152 | source = "registry+https://github.com/rust-lang/crates.io-index" 1153 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1154 | dependencies = [ 1155 | "winapi", 1156 | ] 1157 | 1158 | [[package]] 1159 | name = "winapi-x86_64-pc-windows-gnu" 1160 | version = "0.4.0" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1163 | 1164 | [[package]] 1165 | name = "windows-sys" 1166 | version = "0.45.0" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 1169 | dependencies = [ 1170 | "windows-targets 0.42.1", 1171 | ] 1172 | 1173 | [[package]] 1174 | name = "windows-sys" 1175 | version = "0.52.0" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1178 | dependencies = [ 1179 | "windows-targets 0.52.6", 1180 | ] 1181 | 1182 | [[package]] 1183 | name = "windows-sys" 1184 | version = "0.59.0" 1185 | source = "registry+https://github.com/rust-lang/crates.io-index" 1186 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1187 | dependencies = [ 1188 | "windows-targets 0.52.6", 1189 | ] 1190 | 1191 | [[package]] 1192 | name = "windows-targets" 1193 | version = "0.42.1" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" 1196 | dependencies = [ 1197 | "windows_aarch64_gnullvm 0.42.1", 1198 | "windows_aarch64_msvc 0.42.1", 1199 | "windows_i686_gnu 0.42.1", 1200 | "windows_i686_msvc 0.42.1", 1201 | "windows_x86_64_gnu 0.42.1", 1202 | "windows_x86_64_gnullvm 0.42.1", 1203 | "windows_x86_64_msvc 0.42.1", 1204 | ] 1205 | 1206 | [[package]] 1207 | name = "windows-targets" 1208 | version = "0.52.6" 1209 | source = "registry+https://github.com/rust-lang/crates.io-index" 1210 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1211 | dependencies = [ 1212 | "windows_aarch64_gnullvm 0.52.6", 1213 | "windows_aarch64_msvc 0.52.6", 1214 | "windows_i686_gnu 0.52.6", 1215 | "windows_i686_gnullvm", 1216 | "windows_i686_msvc 0.52.6", 1217 | "windows_x86_64_gnu 0.52.6", 1218 | "windows_x86_64_gnullvm 0.52.6", 1219 | "windows_x86_64_msvc 0.52.6", 1220 | ] 1221 | 1222 | [[package]] 1223 | name = "windows_aarch64_gnullvm" 1224 | version = "0.42.1" 1225 | source = "registry+https://github.com/rust-lang/crates.io-index" 1226 | checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" 1227 | 1228 | [[package]] 1229 | name = "windows_aarch64_gnullvm" 1230 | version = "0.52.6" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1233 | 1234 | [[package]] 1235 | name = "windows_aarch64_msvc" 1236 | version = "0.42.1" 1237 | source = "registry+https://github.com/rust-lang/crates.io-index" 1238 | checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" 1239 | 1240 | [[package]] 1241 | name = "windows_aarch64_msvc" 1242 | version = "0.52.6" 1243 | source = "registry+https://github.com/rust-lang/crates.io-index" 1244 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1245 | 1246 | [[package]] 1247 | name = "windows_i686_gnu" 1248 | version = "0.42.1" 1249 | source = "registry+https://github.com/rust-lang/crates.io-index" 1250 | checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" 1251 | 1252 | [[package]] 1253 | name = "windows_i686_gnu" 1254 | version = "0.52.6" 1255 | source = "registry+https://github.com/rust-lang/crates.io-index" 1256 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1257 | 1258 | [[package]] 1259 | name = "windows_i686_gnullvm" 1260 | version = "0.52.6" 1261 | source = "registry+https://github.com/rust-lang/crates.io-index" 1262 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1263 | 1264 | [[package]] 1265 | name = "windows_i686_msvc" 1266 | version = "0.42.1" 1267 | source = "registry+https://github.com/rust-lang/crates.io-index" 1268 | checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" 1269 | 1270 | [[package]] 1271 | name = "windows_i686_msvc" 1272 | version = "0.52.6" 1273 | source = "registry+https://github.com/rust-lang/crates.io-index" 1274 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1275 | 1276 | [[package]] 1277 | name = "windows_x86_64_gnu" 1278 | version = "0.42.1" 1279 | source = "registry+https://github.com/rust-lang/crates.io-index" 1280 | checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" 1281 | 1282 | [[package]] 1283 | name = "windows_x86_64_gnu" 1284 | version = "0.52.6" 1285 | source = "registry+https://github.com/rust-lang/crates.io-index" 1286 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1287 | 1288 | [[package]] 1289 | name = "windows_x86_64_gnullvm" 1290 | version = "0.42.1" 1291 | source = "registry+https://github.com/rust-lang/crates.io-index" 1292 | checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" 1293 | 1294 | [[package]] 1295 | name = "windows_x86_64_gnullvm" 1296 | version = "0.52.6" 1297 | source = "registry+https://github.com/rust-lang/crates.io-index" 1298 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1299 | 1300 | [[package]] 1301 | name = "windows_x86_64_msvc" 1302 | version = "0.42.1" 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" 1304 | checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" 1305 | 1306 | [[package]] 1307 | name = "windows_x86_64_msvc" 1308 | version = "0.52.6" 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" 1310 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1311 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "todui" 3 | version = "0.1.4" 4 | edition = "2021" 5 | authors = ["Daniel M. "] 6 | description = "A CLI and TUI for your todos" 7 | repository = "https://github.com/danimelchor/todui" 8 | license = "MIT" 9 | 10 | [dependencies] 11 | tui = { package = "ratatui", version = "0.29.0" } 12 | crossterm = "0.29" 13 | chrono = "0.4.23" 14 | serde = { version = "1.0.152", features = ["derive"] } 15 | serde_json = "1.0.93" 16 | anyhow = "1.0.69" 17 | itertools = "0.10.5" 18 | unicode-width = "0.1.10" 19 | clap = { version = "4.1.8", features = ["derive"] } 20 | open = "4.0.0" 21 | dirs = "4.0.0" 22 | -------------------------------------------------------------------------------- /DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## Configuration 4 | 5 | ### Date formats 6 | 7 | Date formatting is done using the [Chrono](https://docs.rs/chrono/latest/chrono/) crate. The available formats can be found here: [strftime specifiers](https://docs.rs/chrono/latest/chrono/format/strftime/index.html). 8 | 9 | ### Icons 10 | 11 | If you are using [NerdFont](https://www.nerdfonts.com/), you can search for icons using [their tool](https://www.nerdfonts.com/cheat-sheet). 12 | 13 | ### Colors 14 | 15 | The available colors are rust's [TUI](https://docs.rs/tui/latest/tui/) styles. The list of colors is the following: 16 | 17 | - `Reset` 18 | - `Black` 19 | - `Red` 20 | - `Green` 21 | - `Yellow` 22 | - `Blue` 23 | - `Magenta` 24 | - `Cyan` 25 | - `Gray` 26 | - `DarkGray` 27 | - `LightRed` 28 | - `LightGreen` 29 | - `LightYellow` 30 | - `LightBlue` 31 | - `LightMagenta` 32 | - `LightCyan` 33 | - `White` 34 | - `Rgb(u8, u8, u8)` -> Example: `Rgb(255, 0, 0)` 35 | - `Indexed(u8)` -> Example: `Indexed(3)` 36 | 37 | ### Key Bindings 38 | 39 | The available keys correspond to rust's crate [Crossterm](https://docs.rs/crossterm/latest/crossterm/) keycodes. The list of all KeyCodes can be found here: [KeyCode enum](https://docs.rs/crossterm/latest/crossterm/event/enum.KeyCode.html). Not all KeyCodes have been implemented. Maybe I will add more in the future. 40 | 41 | The currently available list of KeyCodes is: 42 | 43 | - `Esc`: the escape key 44 | - `Backspace`: the backspace key 45 | - `Left`: the left arrow key 46 | - `Right`: the right arrow key 47 | - `Up`: the up arrow key 48 | - `Down`: the down arrow key 49 | - `Home`: the home key 50 | - `End`: the end key 51 | - `Delete`: the delete key 52 | - `Insert`: the insert key 53 | - `PageUp`: the page up key 54 | - `PageDown`: the page down key 55 | - `F1` to `F12`: the function keys 56 | - `Space`: the space key 57 | - `Tab`: the tab key 58 | - `Enter`: the enter key 59 | - `Backtab`: the backtab key 60 | - `Null`: the null key 61 | - `CapsLock`: the caps lock key 62 | - `ScrollLock`: the scroll lock key 63 | - `NumLock`: the num lock key 64 | - `PrintScreen`: the print screen key 65 | - `Pause`: the pause key 66 | - `Menu`: the menu key 67 | - `KeypadBegin`: the keypad begin key 68 | - Any character -> Example: `a`, `b`, `c`, `1`, `2`, `3`, `!`, `@`, `#` 69 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Daniel Melchor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # todui 2 | 3 | ## TUI 4 | 5 | https://user-images.githubusercontent.com/24496843/225743483-2cbbcdda-9957-4c01-b9c1-aed1fc0ecb19.mp4 6 | 7 | 8 | ## Features 9 | 10 | This app allows for almost anythig you would need when dealing with todos: 11 | - Create, edit, and delete tasks 12 | - Add links to tasks 13 | - Add due dates to tasks 14 | - Add repeating tasks 15 | - Add notes to tasks 16 | - Add tasks to groups (e.g. work, personal, etc.) 17 | 18 | ## How to use? 19 | 20 | You can run the TUI by executing `todui` anywhere in your terminal. To use the CLI, you can start by running `todui --help`: 21 | 22 | ``` 23 | $ todui --help 24 | A CLI and TUI for your todos 25 | 26 | Usage: todui 27 | 28 | Commands: 29 | ls Lists all the tasks 30 | add Adds a task to your todos 31 | delete Deletes a task from your todos 32 | complete Marks a task as complete or incomplete 33 | config Sets default configurations 34 | help Print this message or the help of the given subcommand(s) 35 | 36 | Options: 37 | -h, --help Print help 38 | -V, --version Print version 39 | ``` 40 | 41 | For example: 42 | 43 | ``` 44 | $ todui ls --format json --date-filter today 45 | [{"id":108,"name":"LF112 Homework","date":"2023-03-16T23:59:59-04:00","repeats":{"DaysOfWeek":["Sunday","Tuesday","Thursday"]},"group":"School","description":null,"url":"https://google.com","complete":false},{"id":114,"name":"LF112 Async Thursday","date":"2023-03-16T23:59:59-04:00","repeats":"Weekly","group":"School","description":null,"url":"https://google.com","complete":false},{"id":107,"name":"EN221 Recitation","date":"2023-03-16T23:59:59-04:00","repeats":{"DaysOfWeek":["Tuesday","Thursday"]},"group":"School","description":null,"url":"https://google.com","complete":false}] 46 | ``` 47 | 48 | ## Installation 49 | 50 | Use rusts package manger to install todui. 51 | 52 | ``` 53 | cargo install todui 54 | ``` 55 | 56 | ## Dependencies 57 | 58 | This tool doesn't have any mandatory dependencies. However, it looks much better if you install [Nerd Fonts](https://www.nerdfonts.com/) for better icons. If you don't want to do so, you can always use your own icons or change them for plain text, like `[ ]` for an incomplete task and `[x]` for a complete task. 59 | 60 | ## [Documentation](https://github.com/danimelchor/todui/blob/main/DOCUMENTATION.md) 61 | 62 | ## Config 63 | 64 | The config file can be found in: 65 | - Unix: `~/.config/todui/settings.json` 66 | - Windows: `C:\Users\\AppData\Roaming\todui\settings.json` 67 | 68 | There are some pre-built commands you can run to change the configuration. For example, you can change the keybindings to `vi` mode by running: 69 | 70 | ``` 71 | todui config --mode vi 72 | ``` 73 | 74 | You can also enable special icons by running: 75 | 76 | ``` 77 | todui config --icons special 78 | ``` 79 | 80 | For all the configuration options, run: 81 | 82 | ``` 83 | todui config help 84 | ``` 85 | 86 | Optionally, you can change the default configuration by editing the files directly. The default config is the following: 87 | 88 | ``` 89 | todui config --show 90 | ``` 91 | 92 | ```json 93 | { 94 | "date_formats": { 95 | "display_date_format": "%a %b %-d", 96 | "display_datetime_format": "%a %b %-d at %-H:%M", 97 | "input_date_format": "%d-%m-%Y", 98 | "input_date_hint": "DD-MM-YYYY", 99 | "input_datetime_format": "%d-%m-%Y %H:%M", 100 | "input_datetime_hint": "DD-MM-YYYY HH:MM" 101 | }, 102 | "show_complete": true, 103 | "current_group": null, 104 | "icons": { 105 | "complete": "[x]", 106 | "incomplete": "[ ]", 107 | "repeats": "[r]" 108 | }, 109 | "colors": { 110 | "primary_color": "LightGreen", 111 | "secondary_color": "LightYellow", 112 | "accent_color": "LightBlue" 113 | }, 114 | "keybindings": { 115 | "quit": "q", 116 | "down": "Down", 117 | "up": "Up", 118 | "complete_task": "Space", 119 | "toggle_completed_tasks": "h", 120 | "delete_task": "Delete", 121 | "new_task": "n", 122 | "edit_task": "e", 123 | "save_changes": "Enter", 124 | "enter_insert_mode": "i", 125 | "enter_normal_mode": "Esc", 126 | "go_back": "Esc", 127 | "open_link": "Enter", 128 | "next_group": "Right", 129 | "prev_group": "Left" 130 | } 131 | } 132 | ``` 133 | 134 | For more options, head to [the documentation](https://github.com/danimelchor/todui/blob/main/DOCUMENTATION.md) 135 | 136 | ## Key Bindings 137 | 138 | All key bindings can be modified in the config file. The defaults have been chosen to mimic vim movements as best as possible. Feel free to modify them to your liking! 139 | 140 | **List of tasks panel** 141 | 142 | | Key Bindings | Description | 143 | | -------- | ---------- | 144 | | `q` | Quits the application | 145 | | `Down` | Moves down one task | 146 | | `Up` | Moves up one task | 147 | | `Space` | Marks the task as completed | 148 | | `h` | Toggles hiding completed tasks | 149 | | `d` | Deletes the selected task forever| 150 | | `n` | Opens the new task page | 151 | | `e` | Focuses the task editing panel | 152 | | `Enter` | If the task has an associated link, it opens it in your preferred browser | 153 | | `Right` | Select next group | 154 | | `Left` | Select previous group | 155 | 156 | **Editing/new task panel** 157 | 158 | This panel has two modes (similar to vim). When you are in insert mode, you can modify the fields to edit or create a task. When you are in normal mode, you can move around the fields, save the tasks, go back, or quit. 159 | 160 | *Normal mode* 161 | 162 | | Key Bindings | Description | 163 | | -------- | ---------- | 164 | | `q` | Quit the application | 165 | | `Down` | Move down to the next field | 166 | | `Up` | Move up to the previous field | 167 | | `i` | Enter insert mode | 168 | | `Esc` | Go back to the list of tasks panel | 169 | | `Enter` | Save changes or add the new task | 170 | 171 | *Insert mode* 172 | 173 | | Key Bindings | Description | 174 | | -------- | ---------- | 175 | | `Esc` | Exit insert mode / go back to normal mode | 176 | 177 | ## Why the CLI? 178 | 179 | CLI access to your todos introduces a programmatic way to modify or display your todos in comfortable places. For developers, this might mean displaying your todos when you open your terminal, as notifications, or even or your menu bar. For me, the menu bar was what drove me to create this project. I have used the app Cron for a bit and loved being able to see my events for that day without opening anything. So I created my own SketchyBar widget to interact with my todos: 180 | 181 | https://user-images.githubusercontent.com/24496843/225197941-0dc04074-58d7-496e-a65d-692220fea809.mov 182 | 183 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{ 4 | configuration::{get_db_file, Settings}, 5 | task::Task, 6 | utils, 7 | }; 8 | 9 | pub type Id = usize; 10 | 11 | pub struct App { 12 | pub tasks: HashMap, 13 | pub settings: Settings, 14 | pub current_id: usize, 15 | } 16 | 17 | impl App { 18 | pub fn new(settings: Settings) -> App { 19 | let tasks: HashMap = utils::load_tasks(get_db_file()); 20 | let current_id = tasks.iter().map(|(&k, _)| k).max().unwrap_or(0); 21 | App { 22 | tasks, 23 | settings, 24 | current_id, 25 | } 26 | } 27 | 28 | pub fn get_task(&self, id: Id) -> Option<&Task> { 29 | self.tasks.get(&id) 30 | } 31 | 32 | pub fn save_state(&mut self) { 33 | utils::save_tasks(get_db_file(), self); 34 | } 35 | 36 | pub fn add_task(&mut self, mut t: Task) -> Id { 37 | let new_id = match t.id { 38 | Some(id) => { 39 | self.tasks.insert(id, t); 40 | self.save_state(); 41 | id 42 | } 43 | None => { 44 | let new_id = self.get_next_id(); 45 | t.id = Some(new_id); 46 | self.tasks.insert(new_id, t); 47 | new_id 48 | }, 49 | }; 50 | self.save_state(); 51 | new_id 52 | } 53 | 54 | pub fn delete_task(&mut self, id: usize) -> Option { 55 | self.tasks.remove(&id)?; 56 | self.save_state(); 57 | Some(id) 58 | } 59 | 60 | pub fn set_complete(&mut self, id: usize, complete: bool) -> Option { 61 | let new_task_id = if complete { 62 | let possible_new_task = self.tasks.get_mut(&id)?.set_complete(); 63 | if let Some(possible_new_task) = possible_new_task { 64 | self.delete_task(id); 65 | self.add_task(possible_new_task) 66 | } else { 67 | id 68 | } 69 | } else { 70 | self.tasks.get_mut(&id)?.set_incomplete(); 71 | id 72 | }; 73 | 74 | self.save_state(); 75 | Some(new_task_id) 76 | } 77 | 78 | pub fn toggle_complete_task(&mut self, id: usize) -> Option { 79 | let complete = self.tasks.get(&id)?.complete; 80 | self.set_complete(id, !complete) 81 | } 82 | 83 | fn get_next_id(&mut self) -> usize { 84 | self.current_id += 1; 85 | self.current_id 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/cli/add.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::Parser; 3 | 4 | use crate::app::App; 5 | use crate::cli::cli_utils; 6 | use crate::cli::formats::Format; 7 | use crate::task_form::TaskForm; 8 | 9 | #[derive(Parser)] 10 | pub struct Args { 11 | /// The name of the new task 12 | name: String, 13 | /// The date the task is due 14 | #[arg(long)] 15 | date: Option, 16 | /// How often the task repeats 17 | #[arg(long)] 18 | repeats: Option, 19 | /// The group the task belongs to 20 | #[arg(long)] 21 | group: Option, 22 | /// A description or url for your task 23 | #[arg(long)] 24 | description: Option, 25 | /// A url for your task 26 | #[arg(long)] 27 | url: Option, 28 | /// The format to display the new task with 29 | #[arg(long)] 30 | format: Option, 31 | } 32 | 33 | pub fn run(mut app: App, args: Args) -> Result<()> { 34 | let Args { 35 | name, 36 | format, 37 | date, 38 | repeats, 39 | group, 40 | description, 41 | url, 42 | } = args; 43 | 44 | let mut task_form = TaskForm { 45 | id: None, 46 | name, 47 | date: date.unwrap_or("".to_string()), 48 | repeats: repeats.unwrap_or("".to_string()), 49 | group: group.unwrap_or("".to_string()), 50 | description: description.unwrap_or("".to_string()), 51 | url: url.unwrap_or("".to_string()), 52 | }; 53 | 54 | let task = task_form.submit(&app.settings)?; 55 | let id = app.add_task(task); 56 | let task = app.get_task(id).unwrap(); 57 | cli_utils::print_task(task, format, &app.settings); 58 | 59 | Ok(()) 60 | } 61 | -------------------------------------------------------------------------------- /src/cli/cli_utils.rs: -------------------------------------------------------------------------------- 1 | use super::formats::Format; 2 | use crate::{configuration::Settings, task::Task, utils}; 3 | 4 | pub fn print_task(task: &Task, format: Option, settings: &Settings) { 5 | let tasks = vec![task]; 6 | print_tasks(tasks, format, true, true, settings); 7 | } 8 | 9 | pub fn print_tasks( 10 | tasks: Vec<&Task>, 11 | format: Option, 12 | show_descriptions: bool, 13 | show_urls: bool, 14 | settings: &Settings, 15 | ) { 16 | match format { 17 | Some(Format::Json) => println!("{}", serde_json::to_string(&tasks).expect("Failed to serialize tasks to JSON")), 18 | Some(Format::JsonPretty) => println!("{}", serde_json::to_string_pretty(&tasks).expect("Failed to serialize tasks to JSON")), 19 | _ => { 20 | let longest_name = tasks.iter().map(|t| t.name.len()).max().unwrap_or(0); 21 | let longest_date = tasks 22 | .iter() 23 | .map(|t| utils::date_to_display_str(&t.date, settings).len()) 24 | .max() 25 | .unwrap_or(0); 26 | let longest_repeat = tasks 27 | .iter() 28 | .map(|t| t.repeats.to_string().len()) 29 | .max() 30 | .unwrap_or(0); 31 | let longest_group = tasks 32 | .iter() 33 | .map(|t| t.group.as_deref().unwrap_or_default().len()) 34 | .max() 35 | .unwrap_or(0); 36 | 37 | // Print header 38 | print!("{:width$} ", "Name", width = longest_name + 10); 39 | print!("{:width$} ", "Date", width = longest_date); 40 | print!("{:width$}\t", "Repeats", width = longest_repeat); 41 | print!("{:width$}\t", "Group", width = longest_group); 42 | 43 | if show_descriptions { 44 | print!("Description "); 45 | } 46 | 47 | if show_urls { 48 | print!("Url "); 49 | } 50 | println!(); 51 | 52 | // Print tasks 53 | for task in tasks { 54 | let complete = task.complete; 55 | let x = settings.icons.get_complete_icon(complete); 56 | let name = task.name.clone(); 57 | let id = task.id.unwrap(); 58 | let name_id = format!("{} {} ({})", x, name, id); 59 | let width = longest_name + 10; 60 | print!("{:width$} ", name_id, width = width); 61 | 62 | let date = utils::date_to_display_str(&task.date, settings); 63 | print!("{:width$} ", date, width = longest_date); 64 | 65 | let repeats = &task.repeats; 66 | print!("{:width$}\t", repeats, width = longest_repeat); 67 | 68 | let group = task.group.as_deref().unwrap_or_default(); 69 | print!("{:width$}\t", group, width = longest_group); 70 | 71 | if show_descriptions { 72 | let description = task.description.clone(); 73 | print!("{} ", description.unwrap_or(String::from(""))); 74 | } 75 | 76 | if show_urls { 77 | let url = task.url.clone(); 78 | print!("{} ", url.unwrap_or(String::from(""))); 79 | } 80 | 81 | println!(); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/cli/complete.rs: -------------------------------------------------------------------------------- 1 | use super::{cli_utils, formats::Format}; 2 | use crate::app::App; 3 | use anyhow::Result; 4 | use clap::{Parser, ValueEnum}; 5 | 6 | #[derive(Parser)] 7 | pub struct Args { 8 | /// The ID of the task to modify 9 | #[arg(short, long)] 10 | id: usize, 11 | /// Whether the task should be marked as complete or incomplete 12 | #[arg(short, long)] 13 | complete: CompleteStatus, 14 | /// The format to print the updated task with 15 | #[arg(short, long)] 16 | format: Option, 17 | } 18 | 19 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] 20 | enum CompleteStatus { 21 | Complete, 22 | Incomplete, 23 | } 24 | 25 | pub fn run(mut app: App, args: Args) -> Result<()> { 26 | let Args { 27 | id, 28 | complete, 29 | format, 30 | } = args; 31 | let complete_bool = match complete { 32 | CompleteStatus::Complete => true, 33 | CompleteStatus::Incomplete => false, 34 | }; 35 | 36 | let task_id = app.set_complete(id, complete_bool); 37 | match task_id { 38 | Some(task_id) => { 39 | let task = app.get_task(task_id).unwrap(); 40 | cli_utils::print_task(task, format, &app.settings); 41 | } 42 | None => { 43 | println!("Task with id {} not found", id); 44 | } 45 | } 46 | 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /src/cli/config.rs: -------------------------------------------------------------------------------- 1 | use crate::{app::App, configuration::SettingsBuilder}; 2 | use anyhow::Result; 3 | use clap::{Parser, ValueEnum}; 4 | 5 | #[derive(Parser)] 6 | pub struct Args { 7 | /// Reset the configuration to default 8 | #[clap(long)] 9 | reset: bool, 10 | /// Show the configuration 11 | #[clap(long)] 12 | show: bool, 13 | /// Set the keybindings mode 14 | #[clap(long)] 15 | mode: Option, 16 | /// Set the icons 17 | #[clap(long)] 18 | icons: Option, 19 | } 20 | 21 | #[derive(Parser, Clone, Copy, ValueEnum)] 22 | enum Icons { 23 | /// Set the icons to special characters 24 | Special, 25 | /// Set the icons to char 26 | Chars, 27 | } 28 | 29 | #[derive(Parser, Clone, Copy, ValueEnum)] 30 | enum Mode { 31 | /// Set the mode to vi 32 | Vi, 33 | /// Set the mode to normal 34 | Normal, 35 | } 36 | 37 | pub fn run(mut app: App, args: Args) -> Result<()> { 38 | let Args { 39 | reset, 40 | show, 41 | mode, 42 | icons, 43 | } = args; 44 | 45 | if reset { 46 | let mut sb = SettingsBuilder::default(); 47 | sb.save_to_file()?; 48 | app.settings = sb.build(); 49 | } 50 | 51 | match mode { 52 | Some(Mode::Vi) => app.settings.set_vi_mode(), 53 | Some(Mode::Normal) => app.settings.set_normal_mode(), 54 | None => {} 55 | } 56 | 57 | match icons { 58 | Some(Icons::Special) => app.settings.set_special_icons(), 59 | Some(Icons::Chars) => app.settings.set_char_icons(), 60 | None => {} 61 | } 62 | 63 | if show { 64 | println!("{}", serde_json::to_string_pretty(&app.settings)?); 65 | } 66 | 67 | Ok(()) 68 | } 69 | -------------------------------------------------------------------------------- /src/cli/delete.rs: -------------------------------------------------------------------------------- 1 | use super::{cli_utils, formats::Format}; 2 | use crate::app::App; 3 | use anyhow::Result; 4 | use clap::Parser; 5 | 6 | #[derive(Parser)] 7 | pub struct Args { 8 | /// The ID of the task to delete 9 | #[arg(short, long)] 10 | id: usize, 11 | /// The format to print the deleted task with 12 | #[arg(short, long)] 13 | format: Option, 14 | } 15 | 16 | pub fn run(mut app: App, args: Args) -> Result<()> { 17 | let Args { id, format } = args; 18 | let task = app.get_task(id).cloned(); 19 | if task.is_none() { 20 | println!("Task with id {} not found", id); 21 | } 22 | 23 | let task_id = app.delete_task(id); 24 | 25 | match task_id { 26 | Some(_) => { 27 | cli_utils::print_task(&task.unwrap(), format, &app.settings); 28 | } 29 | None => println!("Task with id {} not found", id), 30 | } 31 | 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /src/cli/formats.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | 3 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] 4 | pub enum Format { 5 | Json, 6 | JsonPretty, 7 | PlainText, 8 | } 9 | 10 | -------------------------------------------------------------------------------- /src/cli/ls.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::app::{App, Id}; 4 | use crate::configuration::Settings; 5 | use crate::task::Task; 6 | use crate::utils; 7 | use anyhow::Result; 8 | use clap::{Parser, ValueEnum}; 9 | 10 | use super::cli_utils; 11 | use super::formats::Format; 12 | 13 | #[derive(Parser)] 14 | pub struct Args { 15 | /// The format to print the tasks with 16 | #[arg(long)] 17 | format: Option, 18 | /// Whether to show complete tasks 19 | #[arg(short, long)] 20 | show_complete: bool, 21 | /// Whether to show task descriptions 22 | #[arg(long)] 23 | show_descriptions: bool, 24 | /// Whether to show task urls 25 | #[arg(long)] 26 | show_urls: bool, 27 | /// Filter tasks by relative date 28 | #[arg(long)] 29 | date_filter: Option, 30 | /// Filter tasks by date 31 | #[arg(long)] 32 | date: Option, 33 | /// Filter by group 34 | #[arg(long)] 35 | group: Option, 36 | } 37 | 38 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] 39 | pub enum DateFilter { 40 | All, 41 | Today, 42 | Past, 43 | TodayAndPast, 44 | Next24, 45 | } 46 | 47 | pub fn filter_by_relative_date( 48 | tasks: HashMap, 49 | date_filter: Option, 50 | ) -> HashMap { 51 | let now = chrono::Local::now(); 52 | match date_filter { 53 | Some(DateFilter::Today) => tasks 54 | .into_iter() 55 | .filter(|(_, t)| { 56 | let today = now.date_naive(); 57 | t.date.date_naive() == today 58 | }) 59 | .collect(), 60 | Some(DateFilter::Past) => tasks.into_iter().filter(|(_, t)| t.date < now).collect(), 61 | Some(DateFilter::TodayAndPast) => tasks 62 | .into_iter() 63 | .filter(|(_, t)| { 64 | let today = now.date_naive(); 65 | t.date.date_naive() <= today 66 | }) 67 | .collect(), 68 | Some(DateFilter::Next24) => tasks 69 | .into_iter() 70 | .filter(|(_, t)| { 71 | let tomorrow = now + chrono::Duration::days(1); 72 | t.date >= now && t.date < tomorrow 73 | }) 74 | .collect(), 75 | _ => tasks, 76 | } 77 | } 78 | 79 | pub fn filter_by_exact_date( 80 | tasks: HashMap, 81 | date: Option, 82 | settings: &Settings, 83 | ) -> Result> { 84 | let tasks = match date { 85 | Some(date) => { 86 | let date = utils::parse_date(date.as_str(), settings)?; 87 | tasks 88 | .into_iter() 89 | .filter(|(_, t)| t.date == date) 90 | .collect() 91 | } 92 | None => tasks, 93 | }; 94 | Ok(tasks) 95 | } 96 | 97 | pub fn filter_by_group(tasks: HashMap, group: Option) -> HashMap { 98 | match group { 99 | Some(group) => { 100 | let group = group.to_lowercase(); 101 | tasks 102 | .into_iter() 103 | .filter(|(_, t)| { 104 | t.group 105 | .as_ref() 106 | .map(|g| g.to_lowercase() == group) 107 | .unwrap_or(false) 108 | }) 109 | .collect() 110 | } 111 | None => tasks, 112 | } 113 | } 114 | 115 | pub fn run(app: App, args: Args) -> Result<()> { 116 | let Args { 117 | format, 118 | show_complete, 119 | show_descriptions, 120 | show_urls, 121 | date_filter, 122 | date, 123 | group, 124 | } = args; 125 | 126 | let tasks: HashMap = if !show_complete { 127 | app.tasks.into_iter().filter(|(_, t)| !t.complete).collect() 128 | } else { 129 | app.tasks 130 | }; 131 | 132 | let tasks = filter_by_relative_date(tasks, date_filter); 133 | let tasks = filter_by_exact_date(tasks, date, &app.settings)?; 134 | let tasks = filter_by_group(tasks, group); 135 | 136 | let mut tasks_vec = tasks.values().collect::>(); 137 | 138 | tasks_vec.sort_by(|a, b| { 139 | a.date.cmp(&b.date).then_with(|| a.name.cmp(&b.name)) 140 | }); 141 | 142 | cli_utils::print_tasks( 143 | tasks_vec, 144 | format, 145 | show_descriptions, 146 | show_urls, 147 | &app.settings, 148 | ); 149 | 150 | Ok(()) 151 | } 152 | -------------------------------------------------------------------------------- /src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::Parser; 3 | use crate::app::App; 4 | 5 | mod ls; 6 | mod add; 7 | mod delete; 8 | mod complete; 9 | mod config; 10 | mod cli_utils; 11 | 12 | // Shared enums and structs 13 | mod formats; 14 | 15 | #[derive(Parser)] 16 | #[command(author, version, about, long_about = None)] 17 | struct Args { 18 | #[command(subcommand)] 19 | command: Command, 20 | } 21 | 22 | #[derive(Parser)] 23 | enum Command { 24 | /// Lists all the tasks 25 | Ls(ls::Args), 26 | /// Adds a task to your todos 27 | Add(add::Args), 28 | /// Deletes a task from your todos 29 | Delete(delete::Args), 30 | /// Marks a task as complete or incomplete 31 | Complete(complete::Args), 32 | /// Sets default configurations 33 | Config(config::Args) 34 | } 35 | 36 | pub fn start_cli(app: App) -> Result<()> { 37 | let args = Args::parse(); 38 | 39 | match args.command { 40 | Command::Ls(args) => ls::run(app, args), 41 | Command::Add(args) => add::run(app, args), 42 | Command::Delete(args) => delete::run(app, args), 43 | Command::Complete(args) => complete::run(app, args), 44 | Command::Config(args) => config::run(app, args) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/configuration.rs: -------------------------------------------------------------------------------- 1 | use crate::utils; 2 | use anyhow::{anyhow, Result}; 3 | use crossterm::event::KeyCode; 4 | use serde::{Deserialize, Serialize}; 5 | use std::fs; 6 | use std::fs::OpenOptions; 7 | use std::io::prelude::*; 8 | use std::path::{Path, PathBuf}; 9 | 10 | #[derive(Deserialize, Serialize, Debug, Clone)] 11 | pub struct Icons { 12 | pub complete: String, 13 | pub incomplete: String, 14 | pub repeats: String, 15 | } 16 | 17 | impl Icons { 18 | pub fn get_complete_icon(&self, complete: bool) -> String { 19 | let icon = if complete { 20 | self.complete.clone() 21 | } else { 22 | self.incomplete.clone() 23 | }; 24 | 25 | // Needs some padding 26 | format!(" {}", icon) 27 | } 28 | 29 | pub fn special() -> Self { 30 | Icons { 31 | complete: "󰄴".to_string(), 32 | incomplete: "󰝦".to_string(), 33 | repeats: "".to_string(), 34 | } 35 | } 36 | } 37 | 38 | impl Default for Icons { 39 | fn default() -> Self { 40 | Icons { 41 | complete: "[x]".to_string(), 42 | incomplete: "[ ]".to_string(), 43 | repeats: "[r]".to_string(), 44 | } 45 | } 46 | } 47 | 48 | pub fn serialize_color(color: &tui::style::Color, serializer: S) -> Result 49 | where 50 | S: serde::Serializer, 51 | { 52 | let color = Color::from_tui_color(*color); 53 | color.serialize(serializer) 54 | } 55 | 56 | pub fn deserialize_color<'de, D>(deserializer: D) -> Result 57 | where 58 | D: serde::Deserializer<'de>, 59 | { 60 | let color = Color::deserialize(deserializer)?; 61 | Ok(color.to_tui_color()) 62 | } 63 | 64 | #[derive(Deserialize, Serialize, Debug, Clone)] 65 | pub enum Color { 66 | Reset, 67 | Black, 68 | Red, 69 | Green, 70 | Yellow, 71 | Blue, 72 | Magenta, 73 | Cyan, 74 | Gray, 75 | DarkGray, 76 | LightRed, 77 | LightGreen, 78 | LightYellow, 79 | LightBlue, 80 | LightMagenta, 81 | LightCyan, 82 | White, 83 | Rgb(u8, u8, u8), 84 | Indexed(u8), 85 | } 86 | 87 | impl Color { 88 | pub fn to_tui_color(&self) -> tui::style::Color { 89 | match self { 90 | Color::Reset => tui::style::Color::Reset, 91 | Color::Black => tui::style::Color::Black, 92 | Color::Red => tui::style::Color::Red, 93 | Color::Green => tui::style::Color::Green, 94 | Color::Yellow => tui::style::Color::Yellow, 95 | Color::Blue => tui::style::Color::Blue, 96 | Color::Magenta => tui::style::Color::Magenta, 97 | Color::Cyan => tui::style::Color::Cyan, 98 | Color::Gray => tui::style::Color::Gray, 99 | Color::DarkGray => tui::style::Color::DarkGray, 100 | Color::LightRed => tui::style::Color::LightRed, 101 | Color::LightGreen => tui::style::Color::LightGreen, 102 | Color::LightYellow => tui::style::Color::LightYellow, 103 | Color::LightBlue => tui::style::Color::LightBlue, 104 | Color::LightMagenta => tui::style::Color::LightMagenta, 105 | Color::LightCyan => tui::style::Color::LightCyan, 106 | Color::White => tui::style::Color::White, 107 | Color::Rgb(r, g, b) => tui::style::Color::Rgb(*r, *g, *b), 108 | Color::Indexed(i) => tui::style::Color::Indexed(*i), 109 | } 110 | } 111 | 112 | pub fn from_tui_color(color: tui::style::Color) -> Self { 113 | match color { 114 | tui::style::Color::Reset => Color::Reset, 115 | tui::style::Color::Black => Color::Black, 116 | tui::style::Color::Red => Color::Red, 117 | tui::style::Color::Green => Color::Green, 118 | tui::style::Color::Yellow => Color::Yellow, 119 | tui::style::Color::Blue => Color::Blue, 120 | tui::style::Color::Magenta => Color::Magenta, 121 | tui::style::Color::Cyan => Color::Cyan, 122 | tui::style::Color::Gray => Color::Gray, 123 | tui::style::Color::DarkGray => Color::DarkGray, 124 | tui::style::Color::LightRed => Color::LightRed, 125 | tui::style::Color::LightGreen => Color::LightGreen, 126 | tui::style::Color::LightYellow => Color::LightYellow, 127 | tui::style::Color::LightBlue => Color::LightBlue, 128 | tui::style::Color::LightMagenta => Color::LightMagenta, 129 | tui::style::Color::LightCyan => Color::LightCyan, 130 | tui::style::Color::White => Color::White, 131 | tui::style::Color::Rgb(r, g, b) => Color::Rgb(r, g, b), 132 | tui::style::Color::Indexed(i) => Color::Indexed(i), 133 | } 134 | } 135 | } 136 | 137 | pub fn deserialize_key<'de, D>(deserializer: D) -> Result 138 | where 139 | D: serde::Deserializer<'de>, 140 | { 141 | let s = String::deserialize(deserializer)?; 142 | match s.to_lowercase().as_ref() { 143 | "esc" => Ok(KeyCode::Esc), 144 | "backspace" => Ok(KeyCode::Backspace), 145 | "left" => Ok(KeyCode::Left), 146 | "right" => Ok(KeyCode::Right), 147 | "up" => Ok(KeyCode::Up), 148 | "down" => Ok(KeyCode::Down), 149 | "home" => Ok(KeyCode::Home), 150 | "end" => Ok(KeyCode::End), 151 | "delete" => Ok(KeyCode::Delete), 152 | "insert" => Ok(KeyCode::Insert), 153 | "pageup" => Ok(KeyCode::PageUp), 154 | "pagedown" => Ok(KeyCode::PageDown), 155 | "f1" => Ok(KeyCode::F(1)), 156 | "f2" => Ok(KeyCode::F(2)), 157 | "f3" => Ok(KeyCode::F(3)), 158 | "f4" => Ok(KeyCode::F(4)), 159 | "f5" => Ok(KeyCode::F(5)), 160 | "f6" => Ok(KeyCode::F(6)), 161 | "f7" => Ok(KeyCode::F(7)), 162 | "f8" => Ok(KeyCode::F(8)), 163 | "f9" => Ok(KeyCode::F(9)), 164 | "f10" => Ok(KeyCode::F(10)), 165 | "f11" => Ok(KeyCode::F(11)), 166 | "f12" => Ok(KeyCode::F(12)), 167 | "space" => Ok(KeyCode::Char(' ')), 168 | "tab" => Ok(KeyCode::Tab), 169 | "backtab" => Ok(KeyCode::BackTab), 170 | "null" => Ok(KeyCode::Null), 171 | "capslock" => Ok(KeyCode::Null), 172 | "scrolllock" => Ok(KeyCode::ScrollLock), 173 | "numlock" => Ok(KeyCode::NumLock), 174 | "printscreen" => Ok(KeyCode::PrintScreen), 175 | "pause" => Ok(KeyCode::Pause), 176 | "menu" => Ok(KeyCode::Menu), 177 | "keypadbegin" => Ok(KeyCode::KeypadBegin), 178 | "enter" => Ok(KeyCode::Enter), 179 | c if c.len() == 1 => Ok(KeyCode::Char(c.chars().next().unwrap())), 180 | _ => Err(serde::de::Error::custom("Invalid key")), 181 | } 182 | } 183 | 184 | pub fn serialize_key(key: &KeyCode, serializer: S) -> Result 185 | where 186 | S: serde::Serializer, 187 | { 188 | let key = KeyBindings::key_to_str(key); 189 | key.serialize(serializer) 190 | } 191 | #[derive(Deserialize, Serialize, Debug, Clone)] 192 | pub struct KeyBindings { 193 | #[serde(deserialize_with = "deserialize_key", serialize_with = "serialize_key")] 194 | pub quit: KeyCode, 195 | #[serde(deserialize_with = "deserialize_key", serialize_with = "serialize_key")] 196 | pub down: KeyCode, 197 | #[serde(deserialize_with = "deserialize_key", serialize_with = "serialize_key")] 198 | pub up: KeyCode, 199 | #[serde(deserialize_with = "deserialize_key", serialize_with = "serialize_key")] 200 | pub complete_task: KeyCode, 201 | #[serde(deserialize_with = "deserialize_key", serialize_with = "serialize_key")] 202 | pub toggle_completed_tasks: KeyCode, 203 | #[serde(deserialize_with = "deserialize_key", serialize_with = "serialize_key")] 204 | pub delete_task: KeyCode, 205 | #[serde(deserialize_with = "deserialize_key", serialize_with = "serialize_key")] 206 | pub new_task: KeyCode, 207 | #[serde(deserialize_with = "deserialize_key", serialize_with = "serialize_key")] 208 | pub edit_task: KeyCode, 209 | #[serde(deserialize_with = "deserialize_key", serialize_with = "serialize_key")] 210 | pub save_changes: KeyCode, 211 | #[serde(deserialize_with = "deserialize_key", serialize_with = "serialize_key")] 212 | pub enter_insert_mode: KeyCode, 213 | #[serde(deserialize_with = "deserialize_key", serialize_with = "serialize_key")] 214 | pub enter_normal_mode: KeyCode, 215 | #[serde(deserialize_with = "deserialize_key", serialize_with = "serialize_key")] 216 | pub go_back: KeyCode, 217 | #[serde(deserialize_with = "deserialize_key", serialize_with = "serialize_key")] 218 | pub open_link: KeyCode, 219 | #[serde(deserialize_with = "deserialize_key", serialize_with = "serialize_key")] 220 | pub next_group: KeyCode, 221 | #[serde(deserialize_with = "deserialize_key", serialize_with = "serialize_key")] 222 | pub prev_group: KeyCode, 223 | } 224 | 225 | impl KeyBindings { 226 | pub fn key_to_str(key: &KeyCode) -> String { 227 | match key { 228 | KeyCode::Esc => "Esc".to_string(), 229 | KeyCode::Backspace => "Backspace".to_string(), 230 | KeyCode::Left => "Left".to_string(), 231 | KeyCode::Right => "Right".to_string(), 232 | KeyCode::Up => "Up".to_string(), 233 | KeyCode::Down => "Down".to_string(), 234 | KeyCode::Home => "Home".to_string(), 235 | KeyCode::End => "End".to_string(), 236 | KeyCode::Delete => "Delete".to_string(), 237 | KeyCode::Insert => "Insert".to_string(), 238 | KeyCode::PageUp => "PageUp".to_string(), 239 | KeyCode::PageDown => "PageDown".to_string(), 240 | KeyCode::F(1) => "F1".to_string(), 241 | KeyCode::F(2) => "F2".to_string(), 242 | KeyCode::F(3) => "F3".to_string(), 243 | KeyCode::F(4) => "F4".to_string(), 244 | KeyCode::F(5) => "F5".to_string(), 245 | KeyCode::F(6) => "F6".to_string(), 246 | KeyCode::F(7) => "F7".to_string(), 247 | KeyCode::F(8) => "F8".to_string(), 248 | KeyCode::F(9) => "F9".to_string(), 249 | KeyCode::F(10) => "F10".to_string(), 250 | KeyCode::F(11) => "F11".to_string(), 251 | KeyCode::F(12) => "F12".to_string(), 252 | KeyCode::Char(' ') => "Space".to_string(), 253 | KeyCode::Tab => "Tab".to_string(), 254 | KeyCode::Enter => "Enter".to_string(), 255 | KeyCode::Char(c) => c.to_string(), 256 | _ => "Unknown".to_string(), 257 | } 258 | } 259 | 260 | pub fn get_vi_default() -> Self { 261 | Self { 262 | quit: KeyCode::Char('q'), 263 | down: KeyCode::Char('j'), 264 | up: KeyCode::Char('k'), 265 | complete_task: KeyCode::Char(' '), 266 | toggle_completed_tasks: KeyCode::Char('c'), 267 | delete_task: KeyCode::Char('d'), 268 | new_task: KeyCode::Char('n'), 269 | edit_task: KeyCode::Char('e'), 270 | save_changes: KeyCode::Enter, 271 | enter_insert_mode: KeyCode::Char('i'), 272 | enter_normal_mode: KeyCode::Esc, 273 | go_back: KeyCode::Esc, 274 | open_link: KeyCode::Enter, 275 | next_group: KeyCode::Char('l'), 276 | prev_group: KeyCode::Char('h'), 277 | } 278 | } 279 | } 280 | 281 | impl Default for KeyBindings { 282 | fn default() -> Self { 283 | Self { 284 | quit: KeyCode::Char('q'), 285 | down: KeyCode::Down, 286 | up: KeyCode::Up, 287 | complete_task: KeyCode::Char(' '), 288 | toggle_completed_tasks: KeyCode::Char('h'), 289 | delete_task: KeyCode::Delete, 290 | new_task: KeyCode::Char('n'), 291 | edit_task: KeyCode::Char('e'), 292 | save_changes: KeyCode::Enter, 293 | enter_insert_mode: KeyCode::Char('i'), 294 | enter_normal_mode: KeyCode::Esc, 295 | go_back: KeyCode::Esc, 296 | open_link: KeyCode::Enter, 297 | next_group: KeyCode::Right, 298 | prev_group: KeyCode::Left, 299 | } 300 | } 301 | } 302 | 303 | #[derive(Deserialize, Serialize, Debug, Clone)] 304 | pub struct DateFormats { 305 | pub display_date_format: String, 306 | pub display_datetime_format: String, 307 | pub input_date_format: String, 308 | pub input_date_hint: String, 309 | pub input_datetime_format: String, 310 | pub input_datetime_hint: String, 311 | } 312 | 313 | impl DateFormats { 314 | fn new() -> Self { 315 | DateFormats { 316 | display_date_format: "%a %b %-d".to_string(), 317 | display_datetime_format: "%a %b %-d at %-H:%M".to_string(), 318 | input_datetime_format: "%d-%m-%Y %H:%M".to_string(), 319 | input_datetime_hint: "DD-MM-YYYY HH:MM".to_string(), 320 | input_date_format: "%d-%m-%Y".to_string(), 321 | input_date_hint: "DD-MM-YYYY".to_string(), 322 | } 323 | } 324 | } 325 | 326 | #[derive(Deserialize, Serialize, Debug, Clone)] 327 | pub struct Colors { 328 | #[serde( 329 | serialize_with = "serialize_color", 330 | deserialize_with = "deserialize_color" 331 | )] 332 | pub primary_color: tui::style::Color, 333 | #[serde( 334 | serialize_with = "serialize_color", 335 | deserialize_with = "deserialize_color" 336 | )] 337 | pub secondary_color: tui::style::Color, 338 | #[serde( 339 | serialize_with = "serialize_color", 340 | deserialize_with = "deserialize_color" 341 | )] 342 | pub accent_color: tui::style::Color, 343 | } 344 | 345 | impl Colors { 346 | fn default() -> Self { 347 | Colors { 348 | primary_color: tui::style::Color::LightGreen, 349 | secondary_color: tui::style::Color::LightYellow, 350 | accent_color: tui::style::Color::LightBlue, 351 | } 352 | } 353 | } 354 | 355 | #[derive(Deserialize, Serialize, Debug, Clone)] 356 | pub struct Settings { 357 | pub date_formats: DateFormats, 358 | pub show_complete: bool, 359 | pub current_group: Option, 360 | pub icons: Icons, 361 | pub colors: Colors, 362 | pub keybindings: KeyBindings, 363 | } 364 | 365 | impl Settings { 366 | pub fn set_show_complete(&mut self, show_complete: bool) { 367 | self.show_complete = show_complete; 368 | self.save_state(); 369 | } 370 | 371 | pub fn set_current_group(&mut self, group: Option) { 372 | self.current_group = group; 373 | self.save_state(); 374 | } 375 | 376 | pub fn set_vi_mode(&mut self) { 377 | self.keybindings = KeyBindings::get_vi_default(); 378 | self.save_state() 379 | } 380 | 381 | pub fn set_normal_mode(&mut self) { 382 | self.keybindings = KeyBindings::default(); 383 | self.save_state() 384 | } 385 | 386 | pub fn set_special_icons(&mut self) { 387 | self.icons = Icons::special(); 388 | self.save_state() 389 | } 390 | 391 | pub fn set_char_icons(&mut self) { 392 | self.icons = Icons::default(); 393 | self.save_state() 394 | } 395 | 396 | pub fn save_state(&self) { 397 | let settings_path = 398 | SettingsBuilder::get_settings_path().expect("Settings file should exist."); 399 | utils::save_settings(&settings_path, self); 400 | } 401 | } 402 | 403 | #[derive(Deserialize, Serialize, Debug)] 404 | pub struct SettingsBuilder { 405 | pub date_formats: DateFormats, 406 | pub show_complete: bool, 407 | pub current_group: Option, 408 | pub icons: Icons, 409 | pub colors: Colors, 410 | pub keybindings: KeyBindings, 411 | } 412 | 413 | impl SettingsBuilder { 414 | pub fn default_path() -> Result { 415 | match dirs::home_dir() { 416 | Some(path) => { 417 | let path = Path::new(&path); 418 | 419 | let config_dir = path.join(".config"); 420 | if !path.exists() { 421 | fs::create_dir_all(path)?; 422 | } 423 | 424 | let todo_dir = config_dir.join("todui"); 425 | if !todo_dir.exists() { 426 | fs::create_dir_all(&todo_dir)?; 427 | } 428 | Ok(todo_dir) 429 | } 430 | None => Err(anyhow!("Could not find home directory")), 431 | } 432 | } 433 | 434 | pub fn get_default_db_file() -> Result { 435 | let default_path = Self::default_path()?; 436 | let path = default_path.join("tasks.json"); 437 | 438 | if !path.exists() { 439 | let mut file = OpenOptions::new().write(true).create(true).open(&path)?; 440 | writeln!(file, "{{}}")?; 441 | } 442 | 443 | Ok(path) 444 | } 445 | 446 | pub fn get_settings_path() -> Result { 447 | let default_path = Self::default_path()?; 448 | let path = default_path.join("settings.json"); 449 | if !path.exists() { 450 | Self::default().save_to_file()?; 451 | } 452 | Ok(path) 453 | } 454 | 455 | pub fn save_to_file(&self) -> Result<()> { 456 | let default_path = Self::default_path()?; 457 | let path = default_path.join("settings.json"); 458 | let settings_json = serde_json::to_string_pretty(&self)?; 459 | fs::write(&path, settings_json)?; 460 | Ok(()) 461 | } 462 | 463 | pub fn build(&mut self) -> Settings { 464 | Settings { 465 | date_formats: self.date_formats.clone(), 466 | show_complete: self.show_complete, 467 | current_group: self.current_group.clone(), 468 | icons: self.icons.clone(), 469 | colors: self.colors.clone(), 470 | keybindings: self.keybindings.clone(), 471 | } 472 | } 473 | } 474 | 475 | impl Default for SettingsBuilder { 476 | fn default() -> Self { 477 | SettingsBuilder { 478 | show_complete: true, 479 | current_group: None, 480 | icons: Icons::default(), 481 | date_formats: DateFormats::new(), 482 | colors: Colors::default(), 483 | keybindings: KeyBindings::default(), 484 | } 485 | } 486 | } 487 | 488 | pub fn get_configuration() -> Settings { 489 | let settings_path = SettingsBuilder::get_settings_path().expect("Settings file should exist."); 490 | let file = OpenOptions::new() 491 | .read(true) 492 | .open(settings_path) 493 | .expect("Could not open settings file"); 494 | serde_json::from_reader(file).expect("Could not parse settings file") 495 | } 496 | 497 | pub fn get_db_file() -> PathBuf { 498 | SettingsBuilder::get_default_db_file().expect("Could not find default db file") 499 | } 500 | -------------------------------------------------------------------------------- /src/day_of_week.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use serde::{Deserialize, Serialize}; 3 | use std::{ 4 | fmt::{Display, Formatter}, 5 | str::FromStr, 6 | }; 7 | 8 | #[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] 9 | pub enum DayOfWeek { 10 | Monday, 11 | Tuesday, 12 | Wednesday, 13 | Thursday, 14 | Friday, 15 | Saturday, 16 | Sunday, 17 | } 18 | 19 | impl DayOfWeek { 20 | pub fn to_int(&self) -> u32 { 21 | match self { 22 | DayOfWeek::Monday => 1, 23 | DayOfWeek::Tuesday => 2, 24 | DayOfWeek::Wednesday => 3, 25 | DayOfWeek::Thursday => 4, 26 | DayOfWeek::Friday => 5, 27 | DayOfWeek::Saturday => 6, 28 | DayOfWeek::Sunday => 7, 29 | } 30 | } 31 | 32 | pub fn from_chrono(day: chrono::Weekday) -> Self { 33 | match day { 34 | chrono::Weekday::Mon => Self::Monday, 35 | chrono::Weekday::Tue => Self::Tuesday, 36 | chrono::Weekday::Wed => Self::Wednesday, 37 | chrono::Weekday::Thu => Self::Thursday, 38 | chrono::Weekday::Fri => Self::Friday, 39 | chrono::Weekday::Sat => Self::Saturday, 40 | chrono::Weekday::Sun => Self::Sunday, 41 | } 42 | } 43 | } 44 | 45 | impl FromStr for DayOfWeek { 46 | type Err = anyhow::Error; 47 | 48 | fn from_str(s: &str) -> Result { 49 | match s.to_lowercase().as_str() { 50 | "mon" => Ok(DayOfWeek::Monday), 51 | "tue" => Ok(DayOfWeek::Tuesday), 52 | "wed" => Ok(DayOfWeek::Wednesday), 53 | "thu" => Ok(DayOfWeek::Thursday), 54 | "fri" => Ok(DayOfWeek::Friday), 55 | "sat" => Ok(DayOfWeek::Saturday), 56 | "sun" => Ok(DayOfWeek::Sunday), 57 | _ => Err(anyhow::anyhow!("Invalid day of the week")), 58 | } 59 | } 60 | } 61 | 62 | impl Display for DayOfWeek { 63 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 64 | match self { 65 | DayOfWeek::Monday => write!(f, "Mon"), 66 | DayOfWeek::Tuesday => write!(f, "Tue"), 67 | DayOfWeek::Wednesday => write!(f, "Wed"), 68 | DayOfWeek::Thursday => write!(f, "Thu"), 69 | DayOfWeek::Friday => write!(f, "Fri"), 70 | DayOfWeek::Saturday => write!(f, "Sat"), 71 | DayOfWeek::Sunday => write!(f, "Sun"), 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod app; 2 | pub mod cli; 3 | pub mod ui; 4 | 5 | pub mod day_of_week; 6 | pub mod repeat; 7 | pub mod task; 8 | pub mod task_form; 9 | 10 | pub mod configuration; 11 | pub mod utils; 12 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use todui::configuration::get_configuration; 2 | use todui::{app::App, cli, ui}; 3 | 4 | fn main() { 5 | // Check args, if none, run ui, else run cli 6 | let settings = get_configuration(); 7 | let app = App::new(settings); 8 | 9 | let res = if std::env::args().len() > 1 { 10 | cli::start_cli(app) 11 | } else { 12 | ui::start_ui(app) 13 | }; 14 | 15 | if let Err(e) = res { 16 | eprintln!("{}", e); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/repeat.rs: -------------------------------------------------------------------------------- 1 | use crate::day_of_week::DayOfWeek; 2 | use anyhow::Result; 3 | use itertools::Itertools; 4 | use serde::{Deserialize, Serialize}; 5 | use std::{fmt::Display, str::FromStr}; 6 | 7 | #[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] 8 | pub enum Repeat { 9 | Never, 10 | Daily, 11 | Weekly, 12 | Monthly, 13 | Yearly, 14 | DaysOfWeek(Vec), 15 | } 16 | 17 | impl Repeat { 18 | pub fn parse_from_str(s: &str) -> Result { 19 | match s.to_lowercase().as_str() { 20 | "never" | "" => Ok(Repeat::Never), 21 | "daily" => Ok(Repeat::Daily), 22 | "weekly" => Ok(Repeat::Weekly), 23 | "monthly" => Ok(Repeat::Monthly), 24 | "yearly" => Ok(Repeat::Yearly), 25 | _ => { 26 | let days: Vec> = s 27 | .split(',') 28 | .map(|s| s.trim()) 29 | .map(DayOfWeek::from_str) 30 | .collect(); 31 | if days.iter().any(|d| d.is_err()) { 32 | return Err(anyhow::anyhow!("Invalid day of the week")); 33 | } 34 | 35 | let days: Vec = 36 | days.iter().map(|d| d.as_ref().unwrap()).cloned().collect(); 37 | 38 | Ok(Repeat::DaysOfWeek(days)) 39 | } 40 | } 41 | } 42 | } 43 | 44 | impl Display for Repeat { 45 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 46 | match self { 47 | Repeat::Never => write!(f, "Never"), 48 | Repeat::Daily => write!(f, "Daily"), 49 | Repeat::Weekly => write!(f, "Weekly"), 50 | Repeat::Monthly => write!(f, "Monthly"), 51 | Repeat::Yearly => write!(f, "Yearly"), 52 | Repeat::DaysOfWeek(days) => { 53 | let days = days.iter().map(|d| d.to_string()).join(","); 54 | write!(f, "{}", days) 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/task.rs: -------------------------------------------------------------------------------- 1 | use crate::{day_of_week::DayOfWeek, repeat::Repeat}; 2 | use anyhow::Result; 3 | use chrono::{DateTime, Datelike, Days, Duration, Local, Months, TimeZone}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | pub fn serialize_dt(date: &DateTime, serializer: S) -> Result 7 | where 8 | S: serde::Serializer, 9 | { 10 | let s = date.format("%+").to_string(); 11 | serializer.serialize_str(&s) 12 | } 13 | 14 | pub fn deserialize_dt<'de, D>(deserializer: D) -> Result, D::Error> 15 | where 16 | D: serde::Deserializer<'de>, 17 | { 18 | let s = String::deserialize(deserializer)?; 19 | let dt = Local.datetime_from_str(&s, "%+").unwrap(); 20 | Ok(dt) 21 | } 22 | 23 | #[derive(Serialize, Deserialize, Clone)] 24 | pub struct Task { 25 | pub id: Option, 26 | pub name: String, 27 | #[serde(serialize_with = "serialize_dt", deserialize_with = "deserialize_dt")] 28 | pub date: DateTime, 29 | pub repeats: Repeat, 30 | pub group: Option, 31 | pub description: Option, 32 | pub url: Option, 33 | pub complete: bool, 34 | } 35 | 36 | impl Task { 37 | pub fn set_id(&mut self, id: Option) { 38 | self.id = id; 39 | } 40 | 41 | pub fn set_name(&mut self, name: String) { 42 | self.name = name; 43 | } 44 | 45 | pub fn set_date(&mut self, date: DateTime) { 46 | self.date = date; 47 | } 48 | 49 | pub fn set_repeats(&mut self, repeats: Repeat) { 50 | self.repeats = repeats; 51 | } 52 | 53 | pub fn set_group(&mut self, group: String) { 54 | self.group = Some(group); 55 | } 56 | 57 | pub fn set_description(&mut self, description: String) { 58 | self.description = Some(description); 59 | } 60 | 61 | pub fn set_url(&mut self, url: String) { 62 | self.url = Some(url); 63 | } 64 | 65 | pub fn set_complete(&mut self) -> Option { 66 | self.complete = true; 67 | let date = match &self.repeats { 68 | Repeat::DaysOfWeek(days) => { 69 | let mut new_date = None; 70 | for i in 1..=7 { 71 | let day = self.date + Duration::days(i); 72 | let weekday = DayOfWeek::from_chrono(day.weekday()); 73 | if days.contains(&weekday) { 74 | new_date = Some(day); 75 | break; 76 | } 77 | } 78 | new_date 79 | } 80 | Repeat::Never => None, 81 | Repeat::Daily => Some(self.date + Days::new(1)), 82 | Repeat::Weekly => Some(self.date + Days::new(7)), 83 | Repeat::Monthly => Some(self.date + Months::new(1)), 84 | Repeat::Yearly => Some(self.date + Months::new(12)), 85 | }; 86 | 87 | if let Some(date) = date { 88 | let mut new_task = self.clone(); 89 | new_task.set_date(date); 90 | new_task.set_incomplete(); 91 | Some(new_task) 92 | } else { 93 | None 94 | } 95 | } 96 | 97 | pub fn set_incomplete(&mut self) -> Option { 98 | self.complete = false; 99 | None 100 | } 101 | 102 | pub fn toggle_complete(&mut self) -> Option { 103 | if self.complete { 104 | self.set_incomplete() 105 | } else { 106 | self.set_complete() 107 | } 108 | } 109 | } 110 | 111 | impl Default for Task { 112 | fn default() -> Self { 113 | Self { 114 | id: None, 115 | name: "".to_string(), 116 | date: Local::now(), 117 | repeats: Repeat::Never, 118 | group: None, 119 | description: None, 120 | url: None, 121 | complete: false, 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/task_form.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use anyhow::Result; 3 | 4 | use crate::configuration::Settings; 5 | use crate::repeat::Repeat; 6 | use crate::task::Task; 7 | use crate::utils; 8 | 9 | #[derive(Default)] 10 | pub struct TaskForm { 11 | pub id: Option, 12 | pub name: String, 13 | pub date: String, 14 | pub repeats: String, 15 | pub group: String, 16 | pub description: String, 17 | pub url: String, 18 | } 19 | 20 | impl TaskForm { 21 | pub fn from_task(task: &Task, settings: &Settings) -> Self { 22 | Self { 23 | id: task.id, 24 | name: task.name.to_string(), 25 | date: utils::date_to_input_str(&task.date, settings), 26 | repeats: task.repeats.to_string(), 27 | group: task.group.clone().unwrap_or_default(), 28 | description: task.description.clone().unwrap_or_default(), 29 | url: task.url.clone().unwrap_or_default(), 30 | } 31 | } 32 | 33 | pub fn submit(&mut self, settings: &Settings) -> Result { 34 | let mut task = Task::default(); 35 | 36 | let repeat = Repeat::parse_from_str(&self.repeats).context("Invalid repeat format")?; 37 | let date = utils::parse_date(&self.date, settings).unwrap_or(utils::get_today()); 38 | 39 | if self.name.is_empty() { 40 | return Err(anyhow::anyhow!("Task name cannot be empty")); 41 | } 42 | 43 | task.set_id(self.id); 44 | task.set_name(self.name.clone()); 45 | task.set_date(date); 46 | task.set_repeats(repeat); 47 | if !self.group.is_empty() { 48 | task.set_group(self.group.clone()); 49 | } 50 | if !self.description.is_empty() { 51 | task.set_description(self.description.clone()); 52 | } 53 | if !self.url.is_empty() { 54 | task.set_url(self.url.clone()); 55 | } 56 | 57 | Ok(task) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ui/all_tasks_page.rs: -------------------------------------------------------------------------------- 1 | use crate::app::App; 2 | use crate::repeat::Repeat; 3 | use crate::task::Task; 4 | use crate::ui::Page; 5 | use crate::utils; 6 | use anyhow::Result; 7 | use chrono::{DateTime, Local, TimeZone}; 8 | use itertools::Itertools; 9 | use std::cell::RefCell; 10 | use std::rc::Rc; 11 | use tui::layout::{Direction, Rect}; 12 | use tui::text::{Line, Span}; 13 | use tui::widgets::{Block, BorderType, Borders, Cell, Row, Table, Tabs}; 14 | use tui::{ 15 | layout::{Constraint, Layout}, 16 | style::{Color, Modifier, Style}, 17 | Frame, 18 | }; 19 | 20 | pub struct AllTasksPage { 21 | pub show_hidden: bool, 22 | pub current_id: Option, 23 | pub app: Rc>, 24 | 25 | current_group: Option, 26 | } 27 | 28 | impl AllTasksPage { 29 | pub fn new(app: Rc>) -> AllTasksPage { 30 | let show_hidden = app.borrow().settings.show_complete; 31 | let current_group = app.borrow().settings.current_group.clone(); 32 | 33 | let mut atp = AllTasksPage { 34 | show_hidden, 35 | current_id: None, 36 | current_group, 37 | app, 38 | }; 39 | 40 | let any_in_group = atp 41 | .visible_tasks() 42 | .iter() 43 | .any(|t| t.group == atp.current_group); 44 | if !any_in_group { 45 | atp.set_group(None); 46 | } 47 | 48 | atp 49 | } 50 | 51 | /// Returns the tasks that should be displayed on the page 52 | pub fn visible_tasks(&self) -> Vec { 53 | let app = self.app.borrow_mut(); 54 | let tasks: Vec<&Task> = app.tasks.values().collect(); 55 | 56 | let tasks: Vec<&Task> = if !self.show_hidden { 57 | tasks.into_iter().filter(|t| !t.complete).collect() 58 | } else { 59 | tasks 60 | }; 61 | 62 | // Filter out tasks not in the current group 63 | let tasks: Vec<&Task> = if let Some(group) = &self.current_group { 64 | tasks 65 | .into_iter() 66 | .filter(|t| t.group.is_some()) 67 | .filter(|t| t.group.as_ref().unwrap() == group) 68 | .collect() 69 | } else { 70 | tasks 71 | }; 72 | 73 | tasks 74 | .into_iter() 75 | .cloned() 76 | .sorted_by(|a, b| a.date.cmp(&b.date)) 77 | .collect() 78 | } 79 | 80 | pub fn ensure_group_exists(&mut self) { 81 | // Check that there are still visible tasks in group 82 | let any = self 83 | .visible_tasks() 84 | .iter() 85 | .any(|t| t.group == self.get_current_group()); 86 | if !any { 87 | self.set_group(None); 88 | } 89 | } 90 | 91 | pub fn ensure_task_exists(&mut self) { 92 | // Check that the current task still exists 93 | if let Some(id) = self.current_id { 94 | let any = self.visible_tasks().iter().any(|t| t.id.unwrap() == id); 95 | if !any { 96 | self.current_id = None; 97 | } 98 | } 99 | } 100 | 101 | /// Toggles the complete status of the currently selected task 102 | pub fn toggle_selected(&mut self) { 103 | if let Some(task_id) = self.current_id { 104 | self.app.borrow_mut().toggle_complete_task(task_id); 105 | 106 | if !self.show_hidden { 107 | self.move_closest(); 108 | } 109 | } 110 | self.ensure_group_exists(); 111 | } 112 | 113 | pub fn next(&mut self) { 114 | let tasks = self.visible_tasks(); 115 | match self.current_id { 116 | Some(id) => { 117 | let idx = tasks.iter().position(|t| t.id.unwrap() == id).unwrap(); 118 | if idx < tasks.len() - 1 { 119 | self.current_id = Some(tasks[idx + 1].id.unwrap()); 120 | } 121 | } 122 | None => { 123 | if !tasks.is_empty() { 124 | self.current_id = Some(tasks[0].id.unwrap()); 125 | } 126 | } 127 | } 128 | } 129 | 130 | pub fn prev(&mut self) { 131 | let tasks = self.visible_tasks(); 132 | match self.current_id { 133 | Some(id) => { 134 | let idx = tasks.iter().position(|t| t.id.unwrap() == id).unwrap(); 135 | if idx > 0 { 136 | self.current_id = Some(tasks[idx - 1].id.unwrap()); 137 | } 138 | } 139 | None => { 140 | if !tasks.is_empty() { 141 | self.current_id = Some(tasks[tasks.len() - 1].id.unwrap()); 142 | } 143 | } 144 | } 145 | } 146 | 147 | pub fn get_current_group(&self) -> Option { 148 | self.current_group.clone() 149 | } 150 | 151 | pub fn set_group(&mut self, group: Option) { 152 | self.current_group = group.clone(); 153 | if self.current_id.is_some() { 154 | let id = self.current_id.unwrap(); 155 | let visible_tasks = self.visible_tasks(); 156 | if !visible_tasks.iter().any(|t| t.id.unwrap() == id) { 157 | self.current_id = None; 158 | } 159 | } 160 | self.app.borrow_mut().settings.set_current_group(group); 161 | } 162 | 163 | pub fn next_group(&mut self) { 164 | let groups = self.get_groups(); 165 | self.current_id = None; 166 | match &self.current_group { 167 | Some(group) => { 168 | let idx = groups.iter().position(|g| g == group).unwrap(); 169 | if idx < groups.len() - 1 { 170 | self.current_group = Some(groups[idx + 1].clone()); 171 | } 172 | } 173 | None => { 174 | if groups.len() > 1 { 175 | self.current_group = Some(groups[1].clone()); 176 | } 177 | } 178 | } 179 | self.app 180 | .borrow_mut() 181 | .settings 182 | .set_current_group(self.current_group.clone()); 183 | } 184 | 185 | pub fn prev_group(&mut self) { 186 | let groups = self.get_groups(); 187 | self.current_id = None; 188 | match &self.current_group { 189 | Some(group) => { 190 | let idx = groups.iter().position(|g| g == group).unwrap(); 191 | if idx > 1 { 192 | self.current_group = Some(groups[idx - 1].clone()); 193 | } else { 194 | self.current_group = None; 195 | } 196 | } 197 | None => {} 198 | } 199 | self.app 200 | .borrow_mut() 201 | .settings 202 | .set_current_group(self.current_group.clone()); 203 | } 204 | 205 | pub fn groups(&self) -> Vec> { 206 | self.visible_tasks() 207 | .into_iter() 208 | .group_by(|t| t.date.date_naive()) 209 | .into_iter() 210 | .map(|(_, group)| { 211 | group 212 | .sorted_by(|a, b| a.date.cmp(&b.date)) 213 | .collect::>() 214 | }) 215 | .collect() 216 | } 217 | 218 | pub fn move_closest(&mut self) { 219 | let current_date: Option> = { 220 | match self.current_id { 221 | Some(id) => self.app.borrow().get_task(id).map(|t| t.date), 222 | None => None, 223 | } 224 | }; 225 | 226 | // Move to next task if any, else previous, else none 227 | let tasks = self.visible_tasks(); 228 | let current_date = current_date.unwrap_or_else(Local::now); 229 | let closest = tasks.iter().min_by_key(|t| { 230 | t.date 231 | .signed_duration_since(current_date) 232 | .num_seconds() 233 | .abs() 234 | }); 235 | match closest { 236 | Some(task) => self.current_id = Some(task.id.unwrap()), 237 | None => self.current_id = None, 238 | } 239 | } 240 | 241 | pub fn toggle_hidden(&mut self) { 242 | self.show_hidden = !self.show_hidden; 243 | self.app 244 | .borrow_mut() 245 | .settings 246 | .set_show_complete(self.show_hidden); 247 | self.ensure_group_exists(); 248 | if !self.show_hidden { 249 | self.move_closest(); 250 | } 251 | } 252 | 253 | pub fn get_groups(&self) -> Vec { 254 | let mut groups = vec!["All Tasks".to_string()]; 255 | let tasks: Vec = self.app.borrow().tasks.values().cloned().collect(); 256 | 257 | let tasks: Vec = if !self.show_hidden { 258 | tasks.into_iter().filter(|t| !t.complete).collect() 259 | } else { 260 | tasks 261 | }; 262 | let mut other_groups = tasks 263 | .iter() 264 | .filter_map(|t| t.group.clone()) 265 | .unique() 266 | .collect::>(); 267 | groups.append(&mut other_groups); 268 | groups 269 | } 270 | 271 | pub fn get_complete_icon(&self, complete: bool) -> String { 272 | self.app.borrow().settings.icons.get_complete_icon(complete) 273 | } 274 | 275 | pub fn get_repeats_icon(&self, repeats: &Repeat) -> String { 276 | match repeats { 277 | Repeat::Never => String::from(""), 278 | _ => self.app.borrow().settings.icons.repeats.clone(), 279 | } 280 | } 281 | 282 | pub fn date_to_str(&self, date: &DateTime) -> String { 283 | utils::date_to_display_str(date, &self.app.borrow().settings) 284 | } 285 | 286 | pub fn open_selected_link(&self) -> Result<()> { 287 | if let Some(task_id) = self.current_id { 288 | let url = self 289 | .app 290 | .borrow() 291 | .get_task(task_id) 292 | .unwrap() 293 | .url 294 | .clone() 295 | .unwrap_or_default(); 296 | 297 | if !url.is_empty() { 298 | open::that(url)?; 299 | } 300 | } 301 | Ok(()) 302 | } 303 | 304 | pub fn get_primary_color(&self) -> Color { 305 | self.app.borrow().settings.colors.primary_color 306 | } 307 | 308 | pub fn get_secondary_color(&self) -> Color { 309 | self.app.borrow().settings.colors.secondary_color 310 | } 311 | 312 | pub fn get_accent_color(&self) -> Color { 313 | self.app.borrow().settings.colors.accent_color 314 | } 315 | } 316 | 317 | impl Page for AllTasksPage { 318 | fn ui(&self, f: &mut Frame, area: Rect, focused: bool) { 319 | let chunks = Layout::default() 320 | .direction(Direction::Vertical) 321 | .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) 322 | .split(area); 323 | 324 | // Render tabs 325 | let groups = self.get_groups(); 326 | let titles: Vec<_> = groups 327 | .iter() 328 | .map(|t| Line::from(Span::styled(t, Style::default().fg(Color::White)))) 329 | .collect(); 330 | let current_group_idx = match &self.current_group { 331 | None => 0, 332 | Some(group) => groups.iter().position(|g| g == group).unwrap(), 333 | }; 334 | let tabs = Tabs::new(titles) 335 | .block(Block::default().borders(Borders::ALL).title("Groups")) 336 | .select(current_group_idx) 337 | // .style(Style::default().fg(self.get_primary_color())) 338 | .highlight_style( 339 | Style::default() 340 | .fg(self.get_secondary_color()) 341 | .add_modifier(Modifier::BOLD), 342 | ); 343 | f.render_widget(tabs, chunks[0]); 344 | 345 | // Build list 346 | let chunks = Layout::default() 347 | .direction(Direction::Horizontal) 348 | .constraints([Constraint::Percentage(100)].as_ref()) 349 | .split(chunks[1]); 350 | 351 | let mut rows = vec![]; 352 | for group in self.groups() { 353 | // Group title 354 | let group_date = &group[0].date.date_naive().and_hms_opt(23, 59, 59).unwrap(); 355 | let group_date = Local.from_local_datetime(group_date).unwrap(); 356 | let date_str = self.date_to_str(&group_date).to_uppercase(); 357 | let group_title = " ".to_string() + date_str.as_str(); 358 | let cell = Cell::from(Span::styled( 359 | group_title, 360 | Style::default() 361 | .add_modifier(Modifier::BOLD) 362 | .fg(self.get_accent_color()), 363 | )); 364 | rows.push(Row::new(vec![cell])); 365 | let pre_count = rows.len(); 366 | 367 | // All tasks in group 368 | for (idx, item) in group.iter().enumerate() { 369 | // Skip if hidden 370 | if !self.show_hidden && item.complete { 371 | continue; 372 | } 373 | 374 | // Create string 375 | let complete_icon = self.get_complete_icon(item.complete); 376 | let recurring_icon = self.get_repeats_icon(&item.repeats); 377 | let title = format!("{} {} {} ", complete_icon, item.name, recurring_icon); 378 | let title_style = match (item.complete, self.current_id) { 379 | (_, Some(task_id)) if task_id == item.id.unwrap() => Style::default() 380 | .fg(self.get_secondary_color()) 381 | .add_modifier(Modifier::BOLD), 382 | (true, _) => Style::default().fg(Color::DarkGray), 383 | _ => Style::default().fg(Color::White), 384 | }; 385 | let title_style = title_style.add_modifier(Modifier::BOLD); 386 | let title_cell = Line::from(Span::styled(title, title_style)); 387 | 388 | // Create row 389 | let cell = Cell::from(title_cell); 390 | let mut new_row = Row::new(vec![cell]); 391 | 392 | // Add bottom margin if last item in group 393 | if idx == group.len() - 1 { 394 | new_row = new_row.bottom_margin(1); 395 | } 396 | 397 | rows.push(new_row); 398 | } 399 | 400 | // If no tasks in group, pop the group title 401 | if rows.len() == pre_count { 402 | rows.pop(); 403 | } 404 | } 405 | let border_style = match focused { 406 | true => Style::default().fg(self.get_primary_color()), 407 | false => Style::default(), 408 | }; 409 | let border_type = match focused { 410 | true => BorderType::Thick, 411 | false => BorderType::Plain, 412 | }; 413 | let list = Table::new(rows, &[Constraint::Percentage(100)]).block( 414 | Block::default() 415 | .borders(Borders::ALL) 416 | .title("Todos") 417 | .border_style(border_style) 418 | .border_type(border_type), 419 | ); 420 | f.render_widget(list, chunks[0]); 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /src/ui/delete_task_page.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | app::{App, Id}, 3 | configuration::KeyBindings, 4 | key, 5 | }; 6 | use std::{cell::RefCell, rc::Rc}; 7 | use tui::{ 8 | layout::{Alignment, Constraint, Direction, Layout, Rect}, 9 | style::{Color, Style}, 10 | text::{Line, Span, Text}, 11 | widgets::{Block, BorderType, Borders, Paragraph, Wrap}, 12 | Frame, 13 | }; 14 | use unicode_width::UnicodeWidthStr; 15 | 16 | use super::{InputMode, Page}; 17 | 18 | pub struct DeleteTaskPage { 19 | pub task_form: String, 20 | pub input_mode: InputMode, 21 | pub task_id: Id, 22 | pub error: Option, 23 | pub app: Rc>, 24 | } 25 | 26 | impl DeleteTaskPage { 27 | pub fn new(app: Rc>, task_id: Id) -> Self { 28 | Self { 29 | task_form: String::default(), 30 | input_mode: InputMode::Normal, 31 | error: None, 32 | task_id, 33 | app, 34 | } 35 | } 36 | 37 | pub fn get_task_name(&self) -> String { 38 | self.app 39 | .borrow() 40 | .get_task(self.task_id) 41 | .unwrap() 42 | .name 43 | .clone() 44 | } 45 | 46 | pub fn remove_task(&self) { 47 | self.app.borrow_mut().delete_task(self.task_id); 48 | } 49 | 50 | pub fn add_char(&mut self, c: char) { 51 | self.task_form.push(c); 52 | } 53 | 54 | pub fn remove_char(&mut self) { 55 | self.task_form.pop(); 56 | } 57 | 58 | pub fn submit(&mut self) -> bool { 59 | if self.task_form == self.get_task_name() { 60 | self.remove_task(); 61 | true 62 | } else { 63 | self.error = Some(format!( 64 | "The name you entered is not the same as the task name: '{}'", 65 | self.get_task_name() 66 | )); 67 | false 68 | } 69 | } 70 | 71 | fn get_keybind_hint(&self) -> Line { 72 | let color = self.get_secondary_color(); 73 | let kb = &self.app.borrow().settings.keybindings; 74 | let i = key!(kb.enter_insert_mode, color); 75 | let q = key!(kb.quit, color); 76 | let enter = key!(kb.save_changes, color); 77 | let esc = key!(kb.enter_normal_mode, color); 78 | let b = key!(kb.go_back, color); 79 | 80 | Line::from(vec![ 81 | Span::raw("Press "), 82 | i, 83 | Span::raw(" to enter insert mode, "), 84 | q, 85 | Span::raw(" to quit, "), 86 | enter, 87 | Span::raw(" to save, "), 88 | esc, 89 | Span::raw(" to exit input mode, and "), 90 | b, 91 | Span::raw(" to go back to the main screen. (*) Fields are required."), 92 | ]) 93 | } 94 | 95 | pub fn get_primary_color(&self) -> Color { 96 | self.app.borrow().settings.colors.primary_color 97 | } 98 | 99 | pub fn get_secondary_color(&self) -> Color { 100 | self.app.borrow().settings.colors.secondary_color 101 | } 102 | } 103 | 104 | impl Page for DeleteTaskPage { 105 | fn ui(&self, f: &mut Frame, area: Rect, focused: bool) { 106 | let chunks = Layout::default() 107 | .direction(Direction::Vertical) 108 | .margin(2) 109 | .constraints( 110 | [ 111 | Constraint::Length(3), 112 | Constraint::Length(3), 113 | Constraint::Length(3), 114 | Constraint::Length(3), 115 | ] 116 | .as_ref(), 117 | ) 118 | .split(area); 119 | 120 | // Draw border around area 121 | let border_style = match focused { 122 | true => Style::default().fg(self.get_primary_color()), 123 | false => Style::default(), 124 | }; 125 | let border_type = match focused { 126 | true => BorderType::Thick, 127 | false => BorderType::Plain, 128 | }; 129 | let block = Block::default() 130 | .borders(Borders::ALL) 131 | .title("Task") 132 | .border_style(border_style) 133 | .border_type(border_type); 134 | f.render_widget(block, area); 135 | 136 | // Keybinds description paragraph 137 | let keybinds = Paragraph::new(self.get_keybind_hint()) 138 | .alignment(Alignment::Center) 139 | .wrap(Wrap { trim: true }); 140 | f.render_widget(keybinds, chunks[0]); 141 | 142 | // Description 143 | let description = Paragraph::new(Line::from(format!( 144 | "To delete this task, please write down the exact name: '{}'", 145 | self.get_task_name() 146 | ))) 147 | .alignment(Alignment::Center) 148 | .wrap(Wrap { trim: true }); 149 | f.render_widget(description, chunks[1]); 150 | 151 | // Name input 152 | let curr_text = Text::from(self.task_form.clone()); 153 | let style = match self.input_mode { 154 | InputMode::Normal => Style::default(), 155 | InputMode::Insert => Style::default().fg(self.get_primary_color()), 156 | }; 157 | let input = Paragraph::new(curr_text) 158 | .style(style) 159 | .block(Block::default().borders(Borders::ALL)); 160 | f.render_widget(input, chunks[2]); 161 | 162 | // Place cursor 163 | if focused { 164 | f.set_cursor_position(( 165 | chunks[2].x + self.task_form.width() as u16 + 1, 166 | chunks[2].y + 1, 167 | )); 168 | } 169 | 170 | // Error message 171 | if let Some(error) = &self.error { 172 | let error = Paragraph::new(Text::from(error.to_owned())) 173 | .style(Style::default().fg(Color::Red)) 174 | .block(Block::default().borders(Borders::ALL).title("Error")); 175 | f.render_widget(error, chunks[3]); 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::app::App; 2 | use anyhow::Result; 3 | use crossterm::{ 4 | event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, 5 | execute, 6 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 7 | }; 8 | use std::cell::RefCell; 9 | use std::io::stdout; 10 | use std::rc::Rc; 11 | use tui::{ 12 | backend::{Backend, CrosstermBackend}, 13 | layout::{Constraint, Direction, Layout, Rect}, 14 | Frame, Terminal, 15 | }; 16 | 17 | mod all_tasks_page; 18 | mod delete_task_page; 19 | mod task_page; 20 | 21 | use all_tasks_page::AllTasksPage; 22 | use delete_task_page::DeleteTaskPage; 23 | use task_page::TaskPage; 24 | 25 | #[macro_export] 26 | macro_rules! key { 27 | ($keybind:expr, $color:expr) => {{ 28 | let keybind = KeyBindings::key_to_str(&$keybind); 29 | let keybind = format!("'{}'", keybind); 30 | Span::styled(keybind, Style::default().fg($color)) 31 | }}; 32 | } 33 | 34 | pub fn start_ui(app: App) -> Result<()> { 35 | let mut stdout = stdout(); 36 | execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; 37 | enable_raw_mode()?; 38 | let backend = CrosstermBackend::new(stdout); 39 | let mut terminal = Terminal::new(backend)?; 40 | terminal.hide_cursor()?; 41 | 42 | run_app(&mut terminal, app)?; 43 | 44 | // restore terminal 45 | disable_raw_mode()?; 46 | execute!( 47 | terminal.backend_mut(), 48 | LeaveAlternateScreen, 49 | DisableMouseCapture 50 | )?; 51 | terminal.show_cursor()?; 52 | 53 | Ok(()) 54 | } 55 | 56 | #[derive(Eq, PartialEq)] 57 | pub enum UIPage { 58 | AllTasks, 59 | NewTask, 60 | EditTask, 61 | DeleteTask, 62 | } 63 | 64 | #[derive(Eq, PartialEq)] 65 | pub enum InputMode { 66 | Insert, 67 | Normal, 68 | } 69 | 70 | pub trait Page { 71 | fn ui(&self, f: &mut Frame, area: Rect, focused: bool); 72 | } 73 | 74 | fn run_app(terminal: &mut Terminal, app: App) -> Result<()> { 75 | let app = Rc::new(RefCell::new(app)); 76 | let mut all_tasks_page = AllTasksPage::new(Rc::clone(&app)); 77 | let mut task_page = TaskPage::new(Rc::clone(&app)); 78 | let mut current_page = UIPage::AllTasks; 79 | let mut delete_task_page = None; 80 | 81 | loop { 82 | terminal.draw(|f| { 83 | render_app( 84 | f, 85 | &mut all_tasks_page, 86 | &mut task_page, 87 | &mut delete_task_page, 88 | ¤t_page, 89 | ) 90 | })?; 91 | let keybindings = &app.borrow().settings.keybindings.clone(); 92 | 93 | if let Event::Key(key) = event::read()? { 94 | let code = key.code; 95 | match current_page { 96 | UIPage::AllTasks => match code { 97 | _ if code == keybindings.quit => break, 98 | _ if code == keybindings.down => { 99 | all_tasks_page.next(); 100 | if let Some(task_id) = all_tasks_page.current_id { 101 | task_page = TaskPage::new_from_task(Rc::clone(&app), task_id); 102 | } 103 | } 104 | _ if code == keybindings.up => { 105 | all_tasks_page.prev(); 106 | if let Some(task_id) = all_tasks_page.current_id { 107 | task_page = TaskPage::new_from_task(Rc::clone(&app), task_id); 108 | } 109 | } 110 | _ if code == keybindings.complete_task => { 111 | all_tasks_page.toggle_selected(); 112 | } 113 | _ if code == keybindings.toggle_completed_tasks => { 114 | all_tasks_page.toggle_hidden() 115 | } 116 | _ if code == keybindings.delete_task => { 117 | if let Some(task_id) = all_tasks_page.current_id { 118 | delete_task_page = Some(DeleteTaskPage::new(Rc::clone(&app), task_id)); 119 | current_page = UIPage::DeleteTask; 120 | } 121 | } 122 | _ if code == keybindings.open_link => all_tasks_page.open_selected_link()?, 123 | _ if code == keybindings.new_task => { 124 | current_page = UIPage::NewTask; 125 | task_page = TaskPage::new(Rc::clone(&app)); 126 | } 127 | _ if code == keybindings.edit_task => { 128 | if all_tasks_page.current_id.is_some() { 129 | current_page = UIPage::EditTask; 130 | } 131 | } 132 | _ if code == keybindings.next_group => { 133 | all_tasks_page.next_group(); 134 | } 135 | _ if code == keybindings.prev_group => { 136 | all_tasks_page.prev_group(); 137 | } 138 | _ => {} 139 | }, 140 | UIPage::DeleteTask => { 141 | let dtp = delete_task_page.as_mut().unwrap(); 142 | match dtp.input_mode { 143 | InputMode::Normal => match key.code { 144 | _ if code == keybindings.quit => break, 145 | _ if code == keybindings.enter_insert_mode => { 146 | dtp.input_mode = InputMode::Insert; 147 | } 148 | _ if code == keybindings.go_back => { 149 | current_page = UIPage::AllTasks; 150 | } 151 | _ if code == keybindings.save_changes => { 152 | if dtp.submit() { 153 | all_tasks_page.ensure_group_exists(); 154 | all_tasks_page.ensure_task_exists(); 155 | current_page = UIPage::AllTasks; 156 | delete_task_page = None; 157 | } 158 | } 159 | _ => {} 160 | }, 161 | InputMode::Insert => match key.code { 162 | _ if code == keybindings.enter_normal_mode => { 163 | dtp.input_mode = InputMode::Normal; 164 | } 165 | _ if code == keybindings.save_changes => { 166 | if dtp.submit() { 167 | all_tasks_page.ensure_group_exists(); 168 | all_tasks_page.ensure_task_exists(); 169 | current_page = UIPage::AllTasks; 170 | delete_task_page = None; 171 | } 172 | } 173 | KeyCode::Char(c) => dtp.add_char(c), 174 | KeyCode::Backspace => dtp.remove_char(), 175 | _ => {} 176 | }, 177 | } 178 | } 179 | UIPage::NewTask | UIPage::EditTask => match task_page.input_mode { 180 | InputMode::Normal => match key.code { 181 | _ if code == keybindings.down => task_page.next_field(), 182 | _ if code == keybindings.up => task_page.prev_field(), 183 | _ if code == keybindings.quit => break, 184 | _ if code == keybindings.enter_insert_mode => { 185 | task_page.input_mode = InputMode::Insert; 186 | } 187 | _ if code == keybindings.go_back => { 188 | current_page = UIPage::AllTasks; 189 | } 190 | _ if code == keybindings.save_changes => { 191 | if task_page.submit() { 192 | all_tasks_page.ensure_group_exists(); 193 | all_tasks_page.ensure_task_exists(); 194 | current_page = UIPage::AllTasks; 195 | } 196 | } 197 | _ => {} 198 | }, 199 | InputMode::Insert => match key.code { 200 | _ if code == keybindings.enter_normal_mode => { 201 | task_page.input_mode = InputMode::Normal; 202 | } 203 | _ if code == keybindings.save_changes => { 204 | if task_page.submit() { 205 | all_tasks_page.ensure_group_exists(); 206 | all_tasks_page.ensure_task_exists(); 207 | current_page = UIPage::AllTasks; 208 | } 209 | } 210 | KeyCode::Char(c) => task_page.add_char(c), 211 | KeyCode::Backspace => task_page.remove_char(), 212 | _ => {} 213 | }, 214 | }, 215 | } 216 | } 217 | } 218 | 219 | Ok(()) 220 | } 221 | 222 | fn render_app( 223 | f: &mut Frame, 224 | all_tasks_page: &mut AllTasksPage, 225 | task_page: &mut TaskPage, 226 | delete_task_page: &mut Option, 227 | current_page: &UIPage, 228 | ) { 229 | let constraints = match (current_page, all_tasks_page.current_id) { 230 | (UIPage::AllTasks | UIPage::EditTask, Some(_)) => { 231 | [Constraint::Percentage(50), Constraint::Percentage(50)].as_ref() 232 | } 233 | _ => [Constraint::Percentage(100)].as_ref(), 234 | }; 235 | let chunks = Layout::default() 236 | .direction(Direction::Horizontal) 237 | .constraints(constraints) 238 | .split(f.area()); 239 | 240 | match current_page { 241 | UIPage::NewTask => { 242 | task_page.ui(f, chunks[0], true); 243 | } 244 | UIPage::DeleteTask => { 245 | delete_task_page.as_mut().unwrap().ui(f, chunks[0], true); 246 | } 247 | _ => match all_tasks_page.current_id { 248 | Some(_) => { 249 | all_tasks_page.ui(f, chunks[0], current_page == &UIPage::AllTasks); 250 | task_page.ui(f, chunks[1], current_page == &UIPage::EditTask); 251 | } 252 | None => { 253 | all_tasks_page.ui(f, chunks[0], true); 254 | } 255 | }, 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/ui/task_page.rs: -------------------------------------------------------------------------------- 1 | use crate::{app::App, configuration::KeyBindings, key, task_form::TaskForm}; 2 | use std::{cell::RefCell, rc::Rc}; 3 | use tui::{ 4 | layout::{Alignment, Constraint, Direction, Layout, Rect}, 5 | style::{Color, Style}, 6 | text::{Line, Span, Text}, 7 | widgets::{Block, BorderType, Borders, Paragraph, Wrap}, 8 | Frame, 9 | }; 10 | use unicode_width::UnicodeWidthStr; 11 | 12 | use super::{InputMode, Page}; 13 | 14 | pub struct TaskPage { 15 | pub task_form: TaskForm, 16 | pub input_mode: InputMode, 17 | pub editing_task: Option, 18 | pub current_idx: usize, 19 | pub num_fields: usize, 20 | pub error: Option, 21 | pub app: Rc>, 22 | } 23 | 24 | impl TaskPage { 25 | pub fn new(app: Rc>) -> TaskPage { 26 | TaskPage { 27 | task_form: TaskForm::default(), 28 | input_mode: InputMode::Normal, 29 | current_idx: 0, 30 | error: None, 31 | num_fields: 6, 32 | editing_task: None, 33 | app, 34 | } 35 | } 36 | 37 | pub fn new_from_task(app: Rc>, task_id: usize) -> TaskPage { 38 | let task = app.borrow().get_task(task_id).unwrap().clone(); 39 | let task_form = TaskForm::from_task(&task, &app.borrow().settings); 40 | 41 | TaskPage { 42 | task_form, 43 | input_mode: InputMode::Normal, 44 | current_idx: 0, 45 | error: None, 46 | num_fields: 6, 47 | editing_task: Some(task_id), 48 | app, 49 | } 50 | } 51 | 52 | pub fn next_field(&mut self) { 53 | if self.current_idx < self.num_fields - 1 { 54 | self.current_idx += 1; 55 | } 56 | } 57 | 58 | pub fn prev_field(&mut self) { 59 | if self.current_idx > 0 { 60 | self.current_idx -= 1; 61 | } 62 | } 63 | 64 | pub fn add_char(&mut self, c: char) { 65 | match self.current_idx { 66 | 0 => self.task_form.name.push(c), 67 | 1 => self.task_form.date.push(c), 68 | 2 => self.task_form.repeats.push(c), 69 | 3 => self.task_form.group.push(c), 70 | 4 => self.task_form.description.push(c), 71 | 5 => self.task_form.url.push(c), 72 | _ => {} 73 | }; 74 | } 75 | 76 | pub fn remove_char(&mut self) { 77 | match self.current_idx { 78 | 0 => self.task_form.name.pop(), 79 | 1 => self.task_form.date.pop(), 80 | 2 => self.task_form.repeats.pop(), 81 | 3 => self.task_form.group.pop(), 82 | 4 => self.task_form.description.pop(), 83 | 5 => self.task_form.url.pop(), 84 | _ => None, 85 | }; 86 | } 87 | 88 | pub fn submit(&mut self) -> bool { 89 | let mut app = self.app.borrow_mut(); 90 | let settings = &app.settings; 91 | let form_result = self.task_form.submit(settings); 92 | match form_result { 93 | Ok(new_task) => { 94 | if let Some(task_id) = self.editing_task { 95 | app.delete_task(task_id); 96 | } 97 | app.add_task(new_task); 98 | true 99 | } 100 | Err(e) => { 101 | self.error = Some(e.to_string()); 102 | false 103 | } 104 | } 105 | } 106 | 107 | fn border_style(&self, idx: usize) -> Style { 108 | if self.current_idx == idx && self.input_mode == InputMode::Insert { 109 | Style::default().fg(self.get_primary_color()) 110 | } else { 111 | Style::default() 112 | } 113 | } 114 | 115 | fn get_date_hint(&self) -> String { 116 | let date_hint = self 117 | .app 118 | .borrow() 119 | .settings 120 | .date_formats 121 | .input_date_hint 122 | .clone(); 123 | let datetime_hint = self 124 | .app 125 | .borrow() 126 | .settings 127 | .date_formats 128 | .input_datetime_hint 129 | .clone(); 130 | format!("{} or {}", date_hint, datetime_hint) 131 | } 132 | 133 | fn get_keybind_hint(&self) -> Line { 134 | let color = self.get_secondary_color(); 135 | let kb = &self.app.borrow().settings.keybindings; 136 | let i = key!(kb.enter_insert_mode, color); 137 | let q = key!(kb.quit, color); 138 | let j = key!(kb.down, color); 139 | let k = key!(kb.up, color); 140 | let enter = key!(kb.save_changes, color); 141 | let esc = key!(kb.enter_normal_mode, color); 142 | let b = key!(kb.go_back, color); 143 | 144 | Line::from(vec![ 145 | Span::raw("Press "), 146 | i, 147 | Span::raw(" to enter insert mode, "), 148 | q, 149 | Span::raw(" to quit, "), 150 | k, 151 | Span::raw(" and "), 152 | j, 153 | Span::raw(" to move up and down, "), 154 | enter, 155 | Span::raw(" to save, "), 156 | esc, 157 | Span::raw(" to exit input mode, and "), 158 | b, 159 | Span::raw(" to go back to the main screen. (*) Fields are required."), 160 | ]) 161 | } 162 | 163 | pub fn get_primary_color(&self) -> Color { 164 | self.app.borrow().settings.colors.primary_color 165 | } 166 | 167 | pub fn get_secondary_color(&self) -> Color { 168 | self.app.borrow().settings.colors.secondary_color 169 | } 170 | } 171 | 172 | impl Page for TaskPage { 173 | fn ui(&self, f: &mut Frame, area: Rect, focused: bool) { 174 | let chunks = Layout::default() 175 | .direction(Direction::Vertical) 176 | .margin(2) 177 | .constraints( 178 | [ 179 | Constraint::Length(3), 180 | Constraint::Length(3), 181 | Constraint::Length(3), 182 | Constraint::Length(3), 183 | Constraint::Length(3), 184 | Constraint::Length(3), 185 | Constraint::Length(3), 186 | Constraint::Length(3), 187 | ] 188 | .as_ref(), 189 | ) 190 | .split(area); 191 | 192 | // Draw border around area 193 | let border_style = match focused { 194 | true => Style::default().fg(self.get_primary_color()), 195 | false => Style::default(), 196 | }; 197 | let border_type = match focused { 198 | true => BorderType::Thick, 199 | false => BorderType::Plain, 200 | }; 201 | let block = Block::default() 202 | .borders(Borders::ALL) 203 | .title("Task") 204 | .border_style(border_style) 205 | .border_type(border_type); 206 | f.render_widget(block, area); 207 | 208 | // Keybinds description paragraph 209 | let keybinds = Paragraph::new(self.get_keybind_hint()) 210 | .alignment(Alignment::Center) 211 | .wrap(Wrap { trim: true }); 212 | f.render_widget(keybinds, chunks[0]); 213 | 214 | // Name 215 | let curr_text = Text::from(self.task_form.name.clone()); 216 | let input = Paragraph::new(curr_text) 217 | .style(self.border_style(0)) 218 | .block(Block::default().borders(Borders::ALL).title("Name (*)")); 219 | f.render_widget(input, chunks[1]); 220 | 221 | // Date 222 | let curr_text = Text::from(self.task_form.date.clone()); 223 | let input = Paragraph::new(curr_text).style(self.border_style(1)).block( 224 | Block::default() 225 | .borders(Borders::ALL) 226 | .title(format!("Date ({})", self.get_date_hint())), 227 | ); 228 | f.render_widget(input, chunks[2]); 229 | 230 | // Repeats 231 | let curr_text = Text::from(self.task_form.repeats.to_string()); 232 | let input = 233 | Paragraph::new(curr_text) 234 | .style(self.border_style(2)) 235 | .block(Block::default().borders(Borders::ALL).title( 236 | "Repeats (Never | Daily | Weekly | Monthly | Yearly | Mon,Tue,Wed,Thu,Fri,Sat,Sun)", 237 | )); 238 | f.render_widget(input, chunks[3]); 239 | 240 | // Group 241 | let curr_text = Text::from(self.task_form.group.clone()); 242 | let input = Paragraph::new(curr_text) 243 | .style(self.border_style(3)) 244 | .block(Block::default().borders(Borders::ALL).title("Group")); 245 | f.render_widget(input, chunks[4]); 246 | 247 | // Description 248 | let curr_text = Text::from(self.task_form.description.clone()); 249 | let input = Paragraph::new(curr_text) 250 | .style(self.border_style(4)) 251 | .block(Block::default().borders(Borders::ALL).title("Description")); 252 | f.render_widget(input, chunks[5]); 253 | 254 | // Description 255 | let curr_text = Text::from(self.task_form.url.clone()); 256 | let input = Paragraph::new(curr_text) 257 | .style(self.border_style(5)) 258 | .block(Block::default().borders(Borders::ALL).title("URL")); 259 | f.render_widget(input, chunks[6]); 260 | 261 | // Place cursor 262 | if focused { 263 | match self.current_idx { 264 | 0 => f.set_cursor_position(( 265 | chunks[1].x + self.task_form.name.width() as u16 + 1, 266 | chunks[1].y + 1, 267 | )), 268 | 1 => f.set_cursor_position(( 269 | chunks[2].x + self.task_form.date.width() as u16 + 1, 270 | chunks[2].y + 1, 271 | )), 272 | 2 => f.set_cursor_position(( 273 | chunks[3].x + self.task_form.repeats.width() as u16 + 1, 274 | chunks[3].y + 1, 275 | )), 276 | 3 => f.set_cursor_position(( 277 | chunks[4].x + self.task_form.group.width() as u16 + 1, 278 | chunks[4].y + 1, 279 | )), 280 | 4 => f.set_cursor_position(( 281 | chunks[5].x + self.task_form.description.width() as u16 + 1, 282 | chunks[5].y + 1, 283 | )), 284 | 5 => f.set_cursor_position(( 285 | chunks[6].x + self.task_form.url.width() as u16 + 1, 286 | chunks[6].y + 1, 287 | )), 288 | _ => {} 289 | } 290 | } 291 | 292 | // Error message 293 | if let Some(error) = &self.error { 294 | let error = Paragraph::new(Text::from(error.to_owned())) 295 | .style(Style::default().fg(Color::Red)) 296 | .block(Block::default().borders(Borders::ALL).title("Error")); 297 | f.render_widget(error, chunks[7]); 298 | } 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use chrono::{DateTime, Local, NaiveDate, TimeZone, Timelike}; 3 | 4 | use crate::app::{App, Id}; 5 | use crate::configuration::Settings; 6 | use crate::task::Task; 7 | use std::collections::HashMap; 8 | use std::fs; 9 | use std::path::PathBuf; 10 | 11 | pub fn load_tasks(file: PathBuf) -> HashMap { 12 | let file = fs::read_to_string(file).expect("Unable to read file"); 13 | let tasks_map: HashMap = 14 | serde_json::from_str(&file).expect("Unable to parse database file"); 15 | tasks_map 16 | } 17 | 18 | pub fn save_tasks(file: PathBuf, app: &App) { 19 | let file = fs::File::create(file).expect("Unable to create file"); 20 | serde_json::to_writer(file, &app.tasks).expect("Unable to write file"); 21 | } 22 | 23 | pub fn save_settings(file: &PathBuf, settings: &Settings) { 24 | let file = fs::File::create(file).expect("Unable to create file"); 25 | serde_json::to_writer(file, &settings).expect("Unable to write file"); 26 | } 27 | 28 | pub fn date_has_time(date: &DateTime) -> bool { 29 | let time = date.time(); 30 | if time.hour() == 23 && time.minute() == 59 { 31 | return false; 32 | } 33 | true 34 | } 35 | 36 | pub fn date_to_display_str(dt: &DateTime, settings: &Settings) -> String { 37 | let format = if date_has_time(dt) { 38 | settings.date_formats.display_datetime_format.clone() 39 | } else { 40 | settings.date_formats.display_date_format.clone() 41 | }; 42 | dt.format(format.as_str()).to_string() 43 | } 44 | 45 | pub fn date_to_input_str(dt: &DateTime, settings: &Settings) -> String { 46 | let format = if date_has_time(dt) { 47 | settings.date_formats.input_datetime_format.clone() 48 | } else { 49 | settings.date_formats.input_date_format.clone() 50 | }; 51 | dt.format(format.as_str()).to_string() 52 | } 53 | 54 | pub fn get_today() -> DateTime { 55 | let today = Local::now().date_naive().and_hms_opt(23, 59, 59).unwrap(); 56 | Local.from_local_datetime(&today).unwrap() 57 | } 58 | 59 | pub fn parse_date(s: &str, settings: &Settings) -> Result> { 60 | let datetime_format = settings.date_formats.input_datetime_format.as_str(); 61 | let date_format = settings.date_formats.input_date_format.as_str(); 62 | 63 | let attempt_datetime = Local.datetime_from_str(s, datetime_format); 64 | let attempt_date = NaiveDate::parse_from_str(s, date_format); 65 | 66 | if let Ok(datetime) = attempt_datetime { 67 | Ok(datetime) 68 | } else if let Ok(date) = attempt_date { 69 | let datetime = date.and_hms_opt(23, 59, 59).unwrap(); 70 | Ok(Local.from_local_datetime(&datetime).unwrap()) 71 | } else { 72 | Err(anyhow!("Unable to parse date")) 73 | } 74 | } 75 | --------------------------------------------------------------------------------