max / goingson
4 files changed,
+804 insertions,
-12 deletions
| @@ -61,6 +61,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 61 | 61 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" | |
| 62 | 62 | ||
| 63 | 63 | [[package]] | |
| 64 | + | name = "alloy_tui" | |
| 65 | + | version = "0.0.0" | |
| 66 | + | dependencies = [ | |
| 67 | + | "makeover", | |
| 68 | + | "ratatui", | |
| 69 | + | ] | |
| 70 | + | ||
| 71 | + | [[package]] | |
| 64 | 72 | name = "ammonia" | |
| 65 | 73 | version = "4.1.3" | |
| 66 | 74 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -88,6 +96,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 88 | 96 | checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" | |
| 89 | 97 | ||
| 90 | 98 | [[package]] | |
| 99 | + | name = "approx" | |
| 100 | + | version = "0.5.1" | |
| 101 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 102 | + | checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" | |
| 103 | + | dependencies = [ | |
| 104 | + | "num-traits", | |
| 105 | + | ] | |
| 106 | + | ||
| 107 | + | [[package]] | |
| 91 | 108 | name = "arbitrary" | |
| 92 | 109 | version = "1.4.2" | |
| 93 | 110 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -318,6 +335,15 @@ dependencies = [ | |||
| 318 | 335 | ] | |
| 319 | 336 | ||
| 320 | 337 | [[package]] | |
| 338 | + | name = "atomic" | |
| 339 | + | version = "0.6.1" | |
| 340 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 341 | + | checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" | |
| 342 | + | dependencies = [ | |
| 343 | + | "bytemuck", | |
| 344 | + | ] | |
| 345 | + | ||
| 346 | + | [[package]] | |
| 321 | 347 | name = "atomic-waker" | |
| 322 | 348 | version = "1.1.2" | |
| 323 | 349 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -401,15 +427,30 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" | |||
| 401 | 427 | ||
| 402 | 428 | [[package]] | |
| 403 | 429 | name = "bit-set" | |
| 430 | + | version = "0.5.3" | |
| 431 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 432 | + | checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" | |
| 433 | + | dependencies = [ | |
| 434 | + | "bit-vec 0.6.3", | |
| 435 | + | ] | |
| 436 | + | ||
| 437 | + | [[package]] | |
| 438 | + | name = "bit-set" | |
| 404 | 439 | version = "0.8.0" | |
| 405 | 440 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 406 | 441 | checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" | |
| 407 | 442 | dependencies = [ | |
| 408 | - | "bit-vec", | |
| 443 | + | "bit-vec 0.8.0", | |
| 409 | 444 | ] | |
| 410 | 445 | ||
| 411 | 446 | [[package]] | |
| 412 | 447 | name = "bit-vec" | |
| 448 | + | version = "0.6.3" | |
| 449 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 450 | + | checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" | |
| 451 | + | ||
| 452 | + | [[package]] | |
| 453 | + | name = "bit-vec" | |
| 413 | 454 | version = "0.8.0" | |
| 414 | 455 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 415 | 456 | checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" | |
| @@ -422,9 +463,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" | |||
| 422 | 463 | ||
| 423 | 464 | [[package]] | |
| 424 | 465 | name = "bitflags" | |
| 425 | - | version = "2.11.0" | |
| 466 | + | version = "2.13.1" | |
| 426 | 467 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 427 | - | checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" | |
| 468 | + | checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" | |
| 428 | 469 | dependencies = [ | |
| 429 | 470 | "serde_core", | |
| 430 | 471 | ] | |
| @@ -497,6 +538,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 497 | 538 | checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" | |
| 498 | 539 | ||
| 499 | 540 | [[package]] | |
| 541 | + | name = "by_address" | |
| 542 | + | version = "1.2.1" | |
| 543 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 544 | + | checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" | |
| 545 | + | ||
| 546 | + | [[package]] | |
| 500 | 547 | name = "bytemuck" | |
| 501 | 548 | version = "1.25.0" | |
| 502 | 549 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -529,7 +576,7 @@ version = "0.18.5" | |||
| 529 | 576 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 530 | 577 | checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" | |
| 531 | 578 | dependencies = [ | |
| 532 | - | "bitflags 2.11.0", | |
| 579 | + | "bitflags 2.13.1", | |
| 533 | 580 | "cairo-sys-rs", | |
| 534 | 581 | "glib", | |
| 535 | 582 | "libc", | |
| @@ -591,6 +638,15 @@ dependencies = [ | |||
| 591 | 638 | ] | |
| 592 | 639 | ||
| 593 | 640 | [[package]] | |
| 641 | + | name = "castaway" | |
| 642 | + | version = "0.2.4" | |
| 643 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 644 | + | checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" | |
| 645 | + | dependencies = [ | |
| 646 | + | "rustversion", | |
| 647 | + | ] | |
| 648 | + | ||
| 649 | + | [[package]] | |
| 594 | 650 | name = "cc" | |
| 595 | 651 | version = "1.2.58" | |
| 596 | 652 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -634,6 +690,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 634 | 690 | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" | |
| 635 | 691 | ||
| 636 | 692 | [[package]] | |
| 693 | + | name = "cfg_aliases" | |
| 694 | + | version = "0.2.2" | |
| 695 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 696 | + | checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" | |
| 697 | + | ||
| 698 | + | [[package]] | |
| 637 | 699 | name = "chacha20" | |
| 638 | 700 | version = "0.9.1" | |
| 639 | 701 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -713,6 +775,20 @@ dependencies = [ | |||
| 713 | 775 | ] | |
| 714 | 776 | ||
| 715 | 777 | [[package]] | |
| 778 | + | name = "compact_str" | |
| 779 | + | version = "0.9.1" | |
| 780 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 781 | + | checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" | |
| 782 | + | dependencies = [ | |
| 783 | + | "castaway", | |
| 784 | + | "cfg-if", | |
| 785 | + | "itoa", | |
| 786 | + | "rustversion", | |
| 787 | + | "ryu", | |
| 788 | + | "static_assertions", | |
| 789 | + | ] | |
| 790 | + | ||
| 791 | + | [[package]] | |
| 716 | 792 | name = "compression-codecs" | |
| 717 | 793 | version = "0.4.37" | |
| 718 | 794 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -750,6 +826,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 750 | 826 | checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" | |
| 751 | 827 | ||
| 752 | 828 | [[package]] | |
| 829 | + | name = "convert_case" | |
| 830 | + | version = "0.10.0" | |
| 831 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 832 | + | checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" | |
| 833 | + | dependencies = [ | |
| 834 | + | "unicode-segmentation", | |
| 835 | + | ] | |
| 836 | + | ||
| 837 | + | [[package]] | |
| 753 | 838 | name = "cookie" | |
| 754 | 839 | version = "0.18.1" | |
| 755 | 840 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -791,7 +876,7 @@ version = "0.25.0" | |||
| 791 | 876 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 792 | 877 | checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" | |
| 793 | 878 | dependencies = [ | |
| 794 | - | "bitflags 2.11.0", | |
| 879 | + | "bitflags 2.13.1", | |
| 795 | 880 | "core-foundation 0.10.1", | |
| 796 | 881 | "core-graphics-types", | |
| 797 | 882 | "foreign-types 0.5.0", | |
| @@ -804,7 +889,7 @@ version = "0.2.0" | |||
| 804 | 889 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 805 | 890 | checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" | |
| 806 | 891 | dependencies = [ | |
| 807 | - | "bitflags 2.11.0", | |
| 892 | + | "bitflags 2.13.1", | |
| 808 | 893 | "core-foundation 0.10.1", | |
| 809 | 894 | "libc", | |
| 810 | 895 | ] | |
| @@ -843,6 +928,12 @@ dependencies = [ | |||
| 843 | 928 | ] | |
| 844 | 929 | ||
| 845 | 930 | [[package]] | |
| 931 | + | name = "critical-section" | |
| 932 | + | version = "1.2.0" | |
| 933 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 934 | + | checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" | |
| 935 | + | ||
| 936 | + | [[package]] | |
| 846 | 937 | name = "crossbeam-channel" | |
| 847 | 938 | version = "0.5.15" | |
| 848 | 939 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -867,6 +958,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 867 | 958 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" | |
| 868 | 959 | ||
| 869 | 960 | [[package]] | |
| 961 | + | name = "crossterm" | |
| 962 | + | version = "0.29.0" | |
| 963 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 964 | + | checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" | |
| 965 | + | dependencies = [ | |
| 966 | + | "bitflags 2.13.1", | |
| 967 | + | "crossterm_winapi", | |
| 968 | + | "derive_more 2.1.1", | |
| 969 | + | "document-features", | |
| 970 | + | "mio 1.2.0", | |
| 971 | + | "parking_lot", | |
| 972 | + | "rustix", | |
| 973 | + | "signal-hook", | |
| 974 | + | "signal-hook-mio", | |
| 975 | + | "winapi", | |
| 976 | + | ] | |
| 977 | + | ||
| 978 | + | [[package]] | |
| 979 | + | name = "crossterm_winapi" | |
| 980 | + | version = "0.9.1" | |
| 981 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 982 | + | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" | |
| 983 | + | dependencies = [ | |
| 984 | + | "winapi", | |
| 985 | + | ] | |
| 986 | + | ||
| 987 | + | [[package]] | |
| 870 | 988 | name = "crypto-common" | |
| 871 | 989 | version = "0.1.7" | |
| 872 | 990 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -878,6 +996,16 @@ dependencies = [ | |||
| 878 | 996 | ] | |
| 879 | 997 | ||
| 880 | 998 | [[package]] | |
| 999 | + | name = "csscolorparser" | |
| 1000 | + | version = "0.6.2" | |
| 1001 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1002 | + | checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" | |
| 1003 | + | dependencies = [ | |
| 1004 | + | "lab", | |
| 1005 | + | "phf 0.11.3", | |
| 1006 | + | ] | |
| 1007 | + | ||
| 1008 | + | [[package]] | |
| 881 | 1009 | name = "cssparser" | |
| 882 | 1010 | version = "0.29.6" | |
| 883 | 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -1000,6 +1128,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 1000 | 1128 | checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" | |
| 1001 | 1129 | ||
| 1002 | 1130 | [[package]] | |
| 1131 | + | name = "deltae" | |
| 1132 | + | version = "0.3.2" | |
| 1133 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1134 | + | checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" | |
| 1135 | + | ||
| 1136 | + | [[package]] | |
| 1003 | 1137 | name = "der" | |
| 1004 | 1138 | version = "0.7.10" | |
| 1005 | 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -1037,7 +1171,7 @@ version = "0.99.20" | |||
| 1037 | 1171 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1038 | 1172 | checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" | |
| 1039 | 1173 | dependencies = [ | |
| 1040 | - | "convert_case", | |
| 1174 | + | "convert_case 0.4.0", | |
| 1041 | 1175 | "proc-macro2", | |
| 1042 | 1176 | "quote", | |
| 1043 | 1177 | "rustc_version", | |
| @@ -1059,6 +1193,7 @@ version = "2.1.1" | |||
| 1059 | 1193 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1060 | 1194 | checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" | |
| 1061 | 1195 | dependencies = [ | |
| 1196 | + | "convert_case 0.10.0", | |
| 1062 | 1197 | "proc-macro2", | |
| 1063 | 1198 | "quote", | |
| 1064 | 1199 | "rustc_version", | |
| @@ -1104,7 +1239,7 @@ version = "0.3.1" | |||
| 1104 | 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1105 | 1240 | checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" | |
| 1106 | 1241 | dependencies = [ | |
| 1107 | - | "bitflags 2.11.0", | |
| 1242 | + | "bitflags 2.13.1", | |
| 1108 | 1243 | "block2", | |
| 1109 | 1244 | "libc", | |
| 1110 | 1245 | "objc2", | |
| @@ -1154,12 +1289,21 @@ dependencies = [ | |||
| 1154 | 1289 | ] | |
| 1155 | 1290 | ||
| 1156 | 1291 | [[package]] | |
| 1292 | + | name = "document-features" | |
| 1293 | + | version = "0.2.12" | |
| 1294 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1295 | + | checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" | |
| 1296 | + | dependencies = [ | |
| 1297 | + | "litrs", | |
| 1298 | + | ] | |
| 1299 | + | ||
| 1300 | + | [[package]] | |
| 1157 | 1301 | name = "dom_query" | |
| 1158 | 1302 | version = "0.27.0" | |
| 1159 | 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1160 | 1304 | checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" | |
| 1161 | 1305 | dependencies = [ | |
| 1162 | - | "bit-set", | |
| 1306 | + | "bit-set 0.8.0", | |
| 1163 | 1307 | "cssparser 0.36.0", | |
| 1164 | 1308 | "foldhash 0.2.0", | |
| 1165 | 1309 | "html5ever 0.38.0", | |
| @@ -1336,6 +1480,15 @@ dependencies = [ | |||
| 1336 | 1480 | ] | |
| 1337 | 1481 | ||
| 1338 | 1482 | [[package]] | |
| 1483 | + | name = "euclid" | |
| 1484 | + | version = "0.22.14" | |
| 1485 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1486 | + | checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" | |
| 1487 | + | dependencies = [ | |
| 1488 | + | "num-traits", | |
| 1489 | + | ] | |
| 1490 | + | ||
| 1491 | + | [[package]] | |
| 1339 | 1492 | name = "event-listener" | |
| 1340 | 1493 | version = "2.5.3" | |
| 1341 | 1494 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -1375,6 +1528,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 1375 | 1528 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" | |
| 1376 | 1529 | ||
| 1377 | 1530 | [[package]] | |
| 1531 | + | name = "fancy-regex" | |
| 1532 | + | version = "0.11.0" | |
| 1533 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1534 | + | checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" | |
| 1535 | + | dependencies = [ | |
| 1536 | + | "bit-set 0.5.3", | |
| 1537 | + | "regex", | |
| 1538 | + | ] | |
| 1539 | + | ||
| 1540 | + | [[package]] | |
| 1541 | + | name = "fast-srgb8" | |
| 1542 | + | version = "1.0.0" | |
| 1543 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1544 | + | checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" | |
| 1545 | + | ||
| 1546 | + | [[package]] | |
| 1378 | 1547 | name = "fastrand" | |
| 1379 | 1548 | version = "2.3.0" | |
| 1380 | 1549 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -1400,6 +1569,17 @@ dependencies = [ | |||
| 1400 | 1569 | ] | |
| 1401 | 1570 | ||
| 1402 | 1571 | [[package]] | |
| 1572 | + | name = "filedescriptor" | |
| 1573 | + | version = "0.8.3" | |
| 1574 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1575 | + | checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" | |
| 1576 | + | dependencies = [ | |
| 1577 | + | "libc", | |
| 1578 | + | "thiserror 1.0.69", | |
| 1579 | + | "winapi", | |
| 1580 | + | ] | |
| 1581 | + | ||
| 1582 | + | [[package]] | |
| 1403 | 1583 | name = "filetime" | |
| 1404 | 1584 | version = "0.2.27" | |
| 1405 | 1585 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -1417,6 +1597,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 1417 | 1597 | checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" | |
| 1418 | 1598 | ||
| 1419 | 1599 | [[package]] | |
| 1600 | + | name = "finl_unicode" | |
| 1601 | + | version = "1.4.0" | |
| 1602 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1603 | + | checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" | |
| 1604 | + | ||
| 1605 | + | [[package]] | |
| 1606 | + | name = "fixedbitset" | |
| 1607 | + | version = "0.4.2" | |
| 1608 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1609 | + | checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" | |
| 1610 | + | ||
| 1611 | + | [[package]] | |
| 1420 | 1612 | name = "flate2" | |
| 1421 | 1613 | version = "1.1.9" | |
| 1422 | 1614 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -1849,7 +2041,7 @@ version = "0.18.5" | |||
| 1849 | 2041 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1850 | 2042 | checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" | |
| 1851 | 2043 | dependencies = [ | |
| 1852 | - | "bitflags 2.11.0", | |
| 2044 | + | "bitflags 2.13.1", | |
| 1853 | 2045 | "futures-channel", | |
| 1854 | 2046 | "futures-core", | |
| 1855 | 2047 | "futures-executor", | |
| @@ -1936,8 +2128,8 @@ dependencies = [ | |||
| 1936 | 2128 | "serde_json", | |
| 1937 | 2129 | "sha2", | |
| 1938 | 2130 | "sqlx", | |
| 1939 | - | "strum", | |
| 1940 | - | "strum_macros", | |
| 2131 | + | "strum 0.26.3", | |
| 2132 | + | "strum_macros 0.26.4", | |
| 1941 | 2133 | "tagtree", | |
| 1942 | 2134 | "thiserror 2.0.18", | |
| 1943 | 2135 | "uuid", | |
| @@ -2014,6 +2206,22 @@ dependencies = [ | |||
| 2014 | 2206 | ] | |
| 2015 | 2207 | ||
| 2016 | 2208 | [[package]] | |
| 2209 | + | name = "got" | |
| 2210 | + | version = "0.5.0" | |
| 2211 | + | dependencies = [ | |
| 2212 | + | "alloy_tui", | |
| 2213 | + | "anyhow", | |
| 2214 | + | "chrono", | |
| 2215 | + | "goingson-core", | |
| 2216 | + | "goingson-db-sqlite", | |
| 2217 | + | "makeover", | |
| 2218 | + | "ratatui", | |
| 2219 | + | "sqlx", | |
| 2220 | + | "tokio", | |
| 2221 | + | "uuid", | |
| 2222 | + | ] | |
| 2223 | + | ||
| 2224 | + | [[package]] | |
| 2017 | 2225 | name = "gtk" | |
| 2018 | 2226 | version = "0.18.2" | |
| 2019 | 2227 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -2115,6 +2323,22 @@ name = "hashbrown" | |||
| 2115 | 2323 | version = "0.16.1" | |
| 2116 | 2324 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2117 | 2325 | checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" | |
| 2326 | + | dependencies = [ | |
| 2327 | + | "allocator-api2", | |
| 2328 | + | "equivalent", | |
| 2329 | + | "foldhash 0.2.0", | |
| 2330 | + | ] | |
| 2331 | + | ||
| 2332 | + | [[package]] | |
| 2333 | + | name = "hashbrown" | |
| 2334 | + | version = "0.17.1" | |
| 2335 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2336 | + | checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" | |
| 2337 | + | dependencies = [ | |
| 2338 | + | "allocator-api2", | |
| 2339 | + | "equivalent", | |
| 2340 | + | "foldhash 0.2.0", | |
| 2341 | + | ] | |
| 2118 | 2342 | ||
| 2119 | 2343 | [[package]] | |
| 2120 | 2344 | name = "hashlink" | |
| @@ -2535,6 +2759,25 @@ dependencies = [ | |||
| 2535 | 2759 | ] | |
| 2536 | 2760 | ||
| 2537 | 2761 | [[package]] | |
| 2762 | + | name = "include_dir" | |
| 2763 | + | version = "0.7.4" | |
| 2764 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2765 | + | checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" | |
| 2766 | + | dependencies = [ | |
| 2767 | + | "include_dir_macros", | |
| 2768 | + | ] | |
| 2769 | + | ||
| 2770 | + | [[package]] | |
| 2771 | + | name = "include_dir_macros" | |
| 2772 | + | version = "0.7.4" | |
| 2773 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2774 | + | checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" | |
| 2775 | + | dependencies = [ | |
| 2776 | + | "proc-macro2", | |
| 2777 | + | "quote", | |
| 2778 | + | ] | |
| 2779 | + | ||
| 2780 | + | [[package]] | |
| 2538 | 2781 | name = "indexmap" | |
| 2539 | 2782 | version = "1.9.3" | |
| 2540 | 2783 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -2558,6 +2801,15 @@ dependencies = [ | |||
| 2558 | 2801 | ] | |
| 2559 | 2802 | ||
| 2560 | 2803 | [[package]] | |
| 2804 | + | name = "indoc" | |
| 2805 | + | version = "2.0.7" | |
| 2806 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2807 | + | checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" | |
| 2808 | + | dependencies = [ | |
| 2809 | + | "rustversion", | |
| 2810 | + | ] | |
| 2811 | + | ||
| 2812 | + | [[package]] | |
| 2561 | 2813 | name = "infer" | |
| 2562 | 2814 | version = "0.19.0" | |
| 2563 | 2815 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -2596,6 +2848,19 @@ dependencies = [ | |||
| 2596 | 2848 | ] | |
| 2597 | 2849 | ||
| 2598 | 2850 | [[package]] | |
| 2851 | + | name = "instability" | |
| 2852 | + | version = "0.3.12" | |
| 2853 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2854 | + | checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" | |
| 2855 | + | dependencies = [ | |
| 2856 | + | "darling", | |
| 2857 | + | "indoc", | |
| 2858 | + | "proc-macro2", | |
| 2859 | + | "quote", | |
| 2860 | + | "syn 2.0.117", | |
| 2861 | + | ] | |
| 2862 | + | ||
| 2863 | + | [[package]] | |
| 2599 | 2864 | name = "ipnet" | |
| 2600 | 2865 | version = "2.12.0" | |
| 2601 | 2866 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -2640,6 +2905,15 @@ dependencies = [ | |||
| 2640 | 2905 | ] | |
| 2641 | 2906 | ||
| 2642 | 2907 | [[package]] | |
| 2908 | + | name = "itertools" | |
| 2909 | + | version = "0.14.0" |
Lines truncated
| @@ -3,6 +3,7 @@ members = [ | |||
| 3 | 3 | "crates/core", | |
| 4 | 4 | "crates/db-sqlite", | |
| 5 | 5 | "crates/go-mcp", | |
| 6 | + | "crates/got", | |
| 6 | 7 | "src-tauri", | |
| 7 | 8 | ] | |
| 8 | 9 | default-members = ["src-tauri"] |
| @@ -0,0 +1,24 @@ | |||
| 1 | + | [package] | |
| 2 | + | name = "got" | |
| 3 | + | version.workspace = true | |
| 4 | + | edition.workspace = true | |
| 5 | + | ||
| 6 | + | [[bin]] | |
| 7 | + | name = "got" | |
| 8 | + | path = "src/main.rs" | |
| 9 | + | ||
| 10 | + | [dependencies] | |
| 11 | + | goingson-core = { workspace = true, features = ["sqlx-sqlite"] } | |
| 12 | + | goingson-db-sqlite = { workspace = true } | |
| 13 | + | ||
| 14 | + | # Alloy's themed-ratatui layer. Path dep while alloy_tui lives next door under | |
| 15 | + | # ~/Code; becomes a git dep on the sr.ht alloy repo once alloy_tui is published. | |
| 16 | + | alloy_tui = { path = "../../../../alloy/crates/alloy_tui" } | |
| 17 | + | ||
| 18 | + | tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } | |
| 19 | + | sqlx = { workspace = true, features = ["sqlite"] } | |
| 20 | + | uuid = { workspace = true } | |
| 21 | + | chrono = { workspace = true } | |
| 22 | + | ratatui = "0.30" | |
| 23 | + | anyhow = "1" | |
| 24 | + | makeover = "0.9.0" |
| @@ -0,0 +1,1060 @@ | |||
| 1 | + | //! got — a read-first terminal cockpit over GoingsOn's local `goingson.db`. | |
| 2 | + | //! | |
| 3 | + | //! First consumer of `alloy_tui` (Alloy's themed-ratatui layer). Opens the same | |
| 4 | + | //! SQLite file the desktop app uses as a peer reader, exactly like `go-mcp` | |
| 5 | + | //! (see `crates/go-mcp`): links the domain crates directly, resolves the same | |
| 6 | + | //! default DB path, and reads through the normal repository layer. | |
| 7 | + | //! | |
| 8 | + | //! The shell mirrors the desktop app's two-level pill-nav: three top-level | |
| 9 | + | //! groups (Work / Time / Messages) each holding a set of panes. It is a slimmed | |
| 10 | + | //! down GoingsOn: Tasks and Projects (the pathways `go-mcp` also covers) plus | |
| 11 | + | //! Events, Timer, and Contacts. Email and the review views are deliberately | |
| 12 | + | //! absent — the desktop and web clients are the better surface for those. | |
| 13 | + | //! `Tab`/`Shift+Tab` cycle groups, number keys and the arrows pick a pane | |
| 14 | + | //! within the active group, `j/k` scroll, `r` refreshes. | |
| 15 | + | //! | |
| 16 | + | //! Usage: `got [--db <path>]` | |
| 17 | + | ||
| 18 | + | use std::collections::HashMap; | |
| 19 | + | use std::io::{self, Stdout}; | |
| 20 | + | use std::path::PathBuf; | |
| 21 | + | ||
| 22 | + | use anyhow::{Context, Result}; | |
| 23 | + | use chrono::{Datelike, Duration, Local, NaiveDate, TimeZone, Utc}; | |
| 24 | + | use goingson_core::{ | |
| 25 | + | Contact, ContactRepository, Event, EventRepository, Priority, Project, ProjectRepository, | |
| 26 | + | ProjectStatus, ProjectType, Task, TaskCrud, TaskStatus, TaskTimeTracking, TimeSession, | |
| 27 | + | TimeSummaryProject, TimeTrackingSummary, UserId, roll_up_time_summary, | |
| 28 | + | }; | |
| 29 | + | use goingson_db_sqlite::{ | |
| 30 | + | SqliteContactRepository, SqliteEventRepository, SqliteProjectRepository, SqliteTaskRepository, | |
| 31 | + | init_pool, | |
| 32 | + | }; | |
| 33 | + | use alloy_tui::{AlloyStatusBar, Theme, hint}; | |
| 34 | + | use ratatui::Terminal; | |
| 35 | + | use ratatui::crossterm::event::{self, Event as CtEvent, KeyCode, KeyEventKind}; | |
| 36 | + | use ratatui::crossterm::{execute, terminal}; | |
| 37 | + | use ratatui::layout::{Alignment, Constraint, Direction, Layout}; | |
| 38 | + | use ratatui::prelude::CrosstermBackend; | |
| 39 | + | use ratatui::style::{Modifier, Style}; | |
| 40 | + | use ratatui::text::{Line, Span}; | |
| 41 | + | use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph}; | |
| 42 | + | use sqlx::SqlitePool; | |
| 43 | + | use uuid::Uuid; | |
| 44 | + | ||
| 45 | + | /// Fixed single-user desktop id. MUST match `DESKTOP_USER_ID` in `go-mcp` | |
| 46 | + | /// (`crates/go-mcp/src/context.rs`) and the Tauri app (`src-tauri/src/state.rs`); | |
| 47 | + | /// tasks are keyed on it, so a mismatch would read an empty set. | |
| 48 | + | const DESKTOP_USER_ID: UserId = UserId::from_uuid(Uuid::from_u128(1)); | |
| 49 | + | ||
| 50 | + | /// The theme `got` renders in. | |
| 51 | + | /// | |
| 52 | + | /// `alloy_tui` ships no built-in palette on purpose: a missing or malformed | |
| 53 | + | /// theme is an error the user sees, not something papered over by rendering in | |
| 54 | + | /// colors that exist nowhere in the theme files. So load one. A theme dropped | |
| 55 | + | /// into `~/.config/goingson/themes/` wins; otherwise use the set `makeover` | |
| 56 | + | /// bundles, which includes the app's own `goingson` skin. | |
| 57 | + | fn load_theme() -> Result<Theme> { | |
| 58 | + | let mut dirs = Vec::new(); | |
| 59 | + | if let Some(home) = std::env::var_os("HOME") { | |
| 60 | + | let custom = PathBuf::from(home) | |
| 61 | + | .join(".config") | |
| 62 | + | .join("goingson") | |
| 63 | + | .join("themes"); | |
| 64 | + | if custom.is_dir() { | |
| 65 | + | dirs.push((custom, true)); | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | dirs.push(( | |
| 69 | + | makeover::bundled_themes_dir().context("makeover ships no themes directory")?, | |
| 70 | + | false, | |
| 71 | + | )); | |
| 72 | + | ||
| 73 | + | let id = std::env::var("GOT_THEME").unwrap_or_else(|_| DEFAULT_THEME.to_string()); | |
| 74 | + | let colors = makeover::load_theme(&dirs, &id) | |
| 75 | + | .map_err(anyhow::Error::msg) | |
| 76 | + | .with_context(|| format!("loading theme `{id}`"))?; | |
| 77 | + | Theme::from_theme(&colors) | |
| 78 | + | .map_err(|e| anyhow::anyhow!("{e:?}")) | |
| 79 | + | .with_context(|| format!("theme `{id}` is incomplete")) | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | /// The app's own skin, shared with the desktop build. | |
| 83 | + | const DEFAULT_THEME: &str = "goingson"; | |
| 84 | + | ||
| 85 | + | fn main() -> Result<()> { | |
| 86 | + | let db_arg = parse_db_arg()?; | |
| 87 | + | ||
| 88 | + | // We own the runtime rather than using `#[tokio::main]` so the synchronous | |
| 89 | + | // event loop can `block_on` a reload whenever the user asks for one. | |
| 90 | + | let rt = tokio::runtime::Runtime::new()?; | |
| 91 | + | ||
| 92 | + | let db_path = db_arg | |
| 93 | + | .or_else(default_db_path) | |
| 94 | + | .context("could not resolve a default goingson.db path; pass --db <path>")?; | |
| 95 | + | if !db_path.exists() { | |
| 96 | + | anyhow::bail!( | |
| 97 | + | "database not found at {}. Run GoingsOn once to create it, or pass --db.", | |
| 98 | + | db_path.display() | |
| 99 | + | ); | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | let pool = rt.block_on(init_pool(Some(&db_path.to_string_lossy())))?; | |
| 103 | + | ||
| 104 | + | let mut app = App::new(db_path, load_theme()?); | |
| 105 | + | app.reload(&rt, &pool)?; | |
| 106 | + | ||
| 107 | + | let mut terminal = setup_terminal()?; | |
| 108 | + | let result = run(&mut terminal, &mut app, &rt, &pool); | |
| 109 | + | restore_terminal(&mut terminal)?; | |
| 110 | + | result | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | /// The top-level pill-nav groups, mirroring the desktop app. Email lives under | |
| 114 | + | /// the app's "Messages" group but is intentionally not surfaced here. | |
| 115 | + | #[derive(Clone, Copy, PartialEq)] | |
| 116 | + | enum Group { | |
| 117 | + | Work, | |
| 118 | + | Time, | |
| 119 | + | Messages, | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | impl Group { | |
| 123 | + | const ALL: [Group; 3] = [Group::Work, Group::Time, Group::Messages]; | |
| 124 | + | ||
| 125 | + | fn label(self) -> &'static str { | |
| 126 | + | match self { | |
| 127 | + | Group::Work => "Work", | |
| 128 | + | Group::Time => "Time", | |
| 129 | + | Group::Messages => "Messages", | |
| 130 | + | } | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | /// The panes shown within this group, in tab order. | |
| 134 | + | fn panes(self) -> &'static [Pane] { | |
| 135 | + | match self { | |
| 136 | + | Group::Work => &[Pane::Tasks, Pane::Projects], | |
| 137 | + | Group::Time => &[Pane::Events, Pane::Timer], | |
| 138 | + | Group::Messages => &[Pane::Contacts], | |
| 139 | + | } | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | fn index(self) -> usize { | |
| 143 | + | Self::ALL.iter().position(|&g| g == self).unwrap_or(0) | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | fn step(self, delta: isize) -> Group { | |
| 147 | + | let n = Self::ALL.len() as isize; | |
| 148 | + | Self::ALL[(self.index() as isize + delta).rem_euclid(n) as usize] | |
| 149 | + | } | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | /// A single pane within a group, each backed by a live read of the local DB. | |
| 153 | + | #[derive(Clone, Copy, PartialEq)] | |
| 154 | + | enum Pane { | |
| 155 | + | Tasks, | |
| 156 | + | Projects, | |
| 157 | + | Events, | |
| 158 | + | Timer, | |
| 159 | + | Contacts, | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | impl Pane { | |
| 163 | + | fn label(self) -> &'static str { | |
| 164 | + | match self { | |
| 165 | + | Pane::Tasks => "Tasks", | |
| 166 | + | Pane::Projects => "Projects", | |
| 167 | + | Pane::Events => "Events", | |
| 168 | + | Pane::Timer => "Timer", | |
| 169 | + | Pane::Contacts => "Contacts", | |
| 170 | + | } | |
| 171 | + | } | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | /// A flattened board row: either a project header or one of its open tasks. | |
| 175 | + | enum Row { | |
| 176 | + | Header { name: String, open: usize }, | |
| 177 | + | Task(Box<Task>), | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | /// One line on the Projects pane: a project plus its open-task count. | |
| 181 | + | struct ProjectRow { | |
| 182 | + | name: String, | |
| 183 | + | status: ProjectStatus, | |
| 184 | + | project_type: ProjectType, | |
| 185 | + | open: usize, | |
| 186 | + | } | |
| 187 | + | ||
| 188 | + | /// How far ahead the Events pane looks. | |
| 189 | + | const EVENT_HORIZON_DAYS: i64 = 30; | |
| 190 | + | ||
| 191 | + | /// A flattened Events-pane row: a day header or one event under it. | |
| 192 | + | enum EventRow { | |
| 193 | + | DayHeader { label: String, count: usize }, | |
| 194 | + | Event(Box<Event>), | |
| 195 | + | } | |
| 196 | + | ||
| 197 | + | /// How many recent sessions the Timer pane lists. | |
| 198 | + | const RECENT_SESSION_LIMIT: usize = 10; | |
| 199 | + | ||
| 200 | + | /// The Timer pane's view: the running timer (if any), this week's tracked | |
| 201 | + | /// breakdown (matching the desktop Day-view panel), and recent sessions. | |
| 202 | + | #[derive(Default)] | |
| 203 | + | struct TimerView { | |
| 204 | + | active: Option<ActiveTimer>, | |
| 205 | + | today_minutes: i32, | |
| 206 | + | week: Vec<TimeSummaryProject>, | |
| 207 | + | recent: Vec<RecentSession>, | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | /// The currently running timer, resolved to its task and elapsed minutes. | |
| 211 | + | struct ActiveTimer { | |
| 212 | + | task: String, | |
| 213 | + | elapsed_min: i32, | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | /// One finished session in the Timer pane's recent list. | |
| 217 | + | struct RecentSession { | |
| 218 | + | day: String, | |
| 219 | + | task: String, | |
| 220 | + | project: Option<String>, | |
| 221 | + | minutes: i32, | |
| 222 | + | } | |
| 223 | + | ||
| 224 | + | struct App { | |
| 225 | + | db_path: PathBuf, | |
| 226 | + | group: Group, | |
| 227 | + | /// Index into `group.panes()` of the active pane. | |
| 228 | + | pane_idx: usize, | |
| 229 | + | rows: Vec<Row>, | |
| 230 | + | state: ListState, | |
| 231 | + | projects: Vec<ProjectRow>, | |
| 232 | + | proj_state: ListState, | |
| 233 | + | events: Vec<EventRow>, | |
| 234 | + | event_state: ListState, | |
| 235 | + | timer: TimerView, | |
| 236 | + | contacts: Vec<Contact>, | |
| 237 | + | contact_state: ListState, | |
| 238 | + | status: String, | |
| 239 | + | theme: Theme, | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | impl App { | |
| 243 | + | fn new(db_path: PathBuf, theme: Theme) -> Self { | |
| 244 | + | Self { | |
| 245 | + | db_path, | |
| 246 | + | theme, | |
| 247 | + | group: Group::Work, | |
| 248 | + | pane_idx: 0, | |
| 249 | + | rows: Vec::new(), | |
| 250 | + | state: ListState::default(), | |
| 251 | + | projects: Vec::new(), | |
| 252 | + | proj_state: ListState::default(), | |
| 253 | + | events: Vec::new(), | |
| 254 | + | event_state: ListState::default(), | |
| 255 | + | timer: TimerView::default(), | |
| 256 | + | contacts: Vec::new(), | |
| 257 | + | contact_state: ListState::default(), | |
| 258 | + | status: String::new(), | |
| 259 | + | } | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | fn current_pane(&self) -> Pane { | |
| 263 | + | self.group.panes()[self.pane_idx] | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | /// Switch groups, landing on the new group's first pane. | |
| 267 | + | fn cycle_group(&mut self, delta: isize) { | |
| 268 | + | self.group = self.group.step(delta); | |
| 269 | + | self.pane_idx = 0; | |
| 270 | + | } | |
| 271 | + | ||
| 272 | + | /// Move to another pane within the active group, wrapping. | |
| 273 | + | fn move_pane(&mut self, delta: isize) { | |
| 274 | + | let n = self.group.panes().len() as isize; | |
| 275 | + | self.pane_idx = (self.pane_idx as isize + delta).rem_euclid(n) as usize; | |
| 276 | + | } | |
| 277 | + | ||
| 278 | + | /// Jump straight to the pane at `idx` (0-based) if the group has one there. | |
| 279 | + | fn select_pane(&mut self, idx: usize) { | |
| 280 | + | if idx < self.group.panes().len() { | |
| 281 | + | self.pane_idx = idx; | |
| 282 | + | } | |
| 283 | + | } | |
| 284 | + | ||
| 285 | + | /// Fetch projects + open tasks and rebuild every pane's view. | |
| 286 | + | fn reload(&mut self, rt: &tokio::runtime::Runtime, pool: &SqlitePool) -> Result<()> { | |
| 287 | + | let Loaded { | |
| 288 | + | projects, | |
| 289 | + | tasks, | |
| 290 | + | events, | |
| 291 | + | active_timer, | |
| 292 | + | time_summary, | |
| 293 | + | sessions, | |
| 294 | + | contacts, | |
| 295 | + | } = rt.block_on(load(pool))?; | |
| 296 | + | self.projects = build_projects(&projects, &tasks); | |
| 297 | + | self.events = build_events(events); | |
| 298 | + | self.timer = build_timer(active_timer, time_summary, sessions, &tasks); | |
| 299 | + | self.contacts = build_contacts(contacts); | |
| 300 | + | self.rows = build_board(projects, tasks); | |
| 301 | + | self.status = format!( | |
| 302 | + | "{} open task(s) from {}", | |
| 303 | + | self.rows.iter().filter(|r| matches!(r, Row::Task(_))).count(), | |
| 304 | + | self.db_path.display() | |
| 305 | + | ); | |
| 306 | + | clamp_selection(&mut self.state, self.rows.len()); | |
| 307 | + | clamp_selection(&mut self.proj_state, self.projects.len()); | |
| 308 | + | clamp_selection(&mut self.event_state, self.events.len()); | |
| 309 | + | clamp_selection(&mut self.contact_state, self.contacts.len()); | |
| 310 | + | Ok(()) | |
| 311 | + | } | |
| 312 | + | ||
| 313 | + | /// Scroll the active pane's list, wrapping. No-op on panes without a list. | |
| 314 | + | fn move_selection(&mut self, delta: isize) { | |
| 315 | + | let (state, len) = match self.current_pane() { | |
| 316 | + | Pane::Tasks => (&mut self.state, self.rows.len()), | |
| 317 | + | Pane::Projects => (&mut self.proj_state, self.projects.len()), | |
| 318 | + | Pane::Events => (&mut self.event_state, self.events.len()), | |
| 319 | + | Pane::Contacts => (&mut self.contact_state, self.contacts.len()), | |
| 320 | + | _ => return, | |
| 321 | + | }; | |
| 322 | + | if len == 0 { | |
| 323 | + | return; | |
| 324 | + | } | |
| 325 | + | let cur = state.selected().unwrap_or(0) as isize; | |
| 326 | + | let next = (cur + delta).rem_euclid(len as isize); | |
| 327 | + | state.select(Some(next as usize)); | |
| 328 | + | } | |
| 329 | + | } | |
| 330 | + | ||
| 331 | + | /// Keep a list's selection in range after its contents change. | |
| 332 | + | fn clamp_selection(state: &mut ListState, len: usize) { | |
| 333 | + | if len == 0 { | |
| 334 | + | state.select(None); | |
| 335 | + | } else { | |
| 336 | + | state.select(Some(state.selected().unwrap_or(0).min(len - 1))); | |
| 337 | + | } | |
| 338 | + | } | |
| 339 | + | ||
| 340 | + | /// Everything the board reads in one refresh. | |
| 341 | + | struct Loaded { | |
| 342 | + | projects: Vec<Project>, | |
| 343 | + | tasks: Vec<Task>, | |
| 344 | + | events: Vec<Event>, | |
| 345 | + | active_timer: Option<(TimeSession, String)>, | |
| 346 | + | time_summary: Vec<TimeTrackingSummary>, | |
| 347 | + | sessions: Vec<TimeSession>, | |
| 348 | + | contacts: Vec<Contact>, | |
| 349 | + | } | |
| 350 | + | ||
| 351 | + | /// Read every pane's data for the desktop user in a single async pass. | |
| 352 | + | async fn load(pool: &SqlitePool) -> Result<Loaded> { | |
| 353 | + | let projects = SqliteProjectRepository::new(pool.clone()) | |
| 354 | + | .list_all(DESKTOP_USER_ID) | |
| 355 | + | .await?; | |
| 356 | + | let tasks = SqliteTaskRepository::new(pool.clone()) | |
| 357 | + | .list_all(DESKTOP_USER_ID) | |
| 358 | + | .await?; | |
| 359 | + | let events = SqliteEventRepository::new(pool.clone()) | |
| 360 | + | .get_upcoming(DESKTOP_USER_ID, EVENT_HORIZON_DAYS) | |
| 361 | + | .await?; | |
| 362 | + | ||
| 363 | + | // Time tracking. The week window matches the desktop Day-view panel: Monday | |
| 364 | + | // 00:00 local through the next Monday, so our totals agree with the app's. | |
| 365 | + | let task_repo = SqliteTaskRepository::new(pool.clone()); | |
| 366 | + | let active_timer = task_repo.get_active_timer(DESKTOP_USER_ID).await?; | |
| 367 | + | let now = Local::now(); | |
| 368 | + | let week_start = now.date_naive() | |
| 369 | + | - Duration::days(i64::from(now.weekday().num_days_from_monday())); | |
| 370 | + | let (start, end) = ( | |
| 371 | + | local_midnight_utc(week_start), | |
| 372 | + | local_midnight_utc(week_start + Duration::days(7)), | |
| 373 | + | ); | |
| 374 | + | let time_summary = task_repo | |
| 375 | + | .get_time_summary(DESKTOP_USER_ID, start, end) | |
| 376 | + | .await?; | |
| 377 | + | let sessions = task_repo.list_all_time_sessions(DESKTOP_USER_ID).await?; | |
| 378 | + | ||
| 379 | + | let contacts = SqliteContactRepository::new(pool.clone()) | |
| 380 | + | .list_all(DESKTOP_USER_ID) | |
| 381 | + | .await?; | |
| 382 | + | ||
| 383 | + | Ok(Loaded { | |
| 384 | + | projects, | |
| 385 | + | tasks, | |
| 386 | + | events, | |
| 387 | + | active_timer, | |
| 388 | + | time_summary, | |
| 389 | + | sessions, | |
| 390 | + | contacts, | |
| 391 | + | }) | |
| 392 | + | } | |
| 393 | + | ||
| 394 | + | /// Local midnight on `date`, expressed in UTC. Mirrors the desktop app's helper | |
| 395 | + | /// so the Timer pane buckets time exactly as the Day view does. | |
| 396 | + | fn local_midnight_utc(date: NaiveDate) -> chrono::DateTime<Utc> { | |
| 397 | + | let naive = date.and_hms_opt(0, 0, 0).expect("midnight is always valid"); | |
| 398 | + | Local | |
| 399 | + | .from_local_datetime(&naive) | |
| 400 | + | .earliest() | |
| 401 | + | .map(|dt| dt.with_timezone(&Utc)) | |
| 402 | + | .unwrap_or_else(|| Utc.from_utc_datetime(&naive)) | |
| 403 | + | } | |
| 404 | + | ||
| 405 | + | fn is_open(t: &Task) -> bool { | |
| 406 | + | matches!(t.status, TaskStatus::Pending | TaskStatus::Started) | |
| 407 | + | } | |
| 408 | + | ||
| 409 | + | /// Group open tasks under their project, projects with work first, and flatten. | |
| 410 | + | fn build_board(projects: Vec<Project>, tasks: Vec<Task>) -> Vec<Row> { | |
| 411 | + | let mut open: Vec<Task> = tasks.into_iter().filter(is_open).collect(); | |
| 412 | + | open.sort_by(|a, b| b.urgency.partial_cmp(&a.urgency).unwrap_or(std::cmp::Ordering::Equal)); | |
| 413 | + | ||
| 414 | + | // Preserve one bucket per project name plus a trailing "(no project)". | |
| 415 | + | let mut order: Vec<String> = projects.into_iter().map(|p| p.name).collect(); | |
| 416 | + | order.sort(); | |
| 417 | + | const NO_PROJECT: &str = "(no project)"; | |
| 418 | + | order.push(NO_PROJECT.to_string()); | |
| 419 | + | ||
| 420 | + | let mut rows = Vec::new(); | |
| 421 | + | for name in order { | |
| 422 | + | let bucket: Vec<Task> = open | |
| 423 | + | .iter() | |
| 424 | + | .filter(|t| { | |
| 425 | + | let pn = t.project_name.as_deref().unwrap_or(NO_PROJECT); | |
| 426 | + | pn == name | |
| 427 | + | }) | |
| 428 | + | .cloned() | |
| 429 | + | .collect(); | |
| 430 | + | if bucket.is_empty() { | |
| 431 | + | continue; | |
| 432 | + | } | |
| 433 | + | rows.push(Row::Header { name, open: bucket.len() }); | |
| 434 | + | rows.extend(bucket.into_iter().map(|t| Row::Task(Box::new(t)))); | |
| 435 | + | } | |
| 436 | + | rows | |
| 437 | + | } | |
| 438 | + | ||
| 439 | + | /// Build the Projects pane: every project with its open-task count, sorted so | |
| 440 | + | /// active work floats to the top and ties break alphabetically. | |
| 441 | + | fn build_projects(projects: &[Project], tasks: &[Task]) -> Vec<ProjectRow> { | |
| 442 | + | let mut open_counts: HashMap<&str, usize> = HashMap::new(); | |
| 443 | + | for t in tasks.iter().filter(|t| is_open(t)) { | |
| 444 | + | if let Some(name) = t.project_name.as_deref() { | |
| 445 | + | *open_counts.entry(name).or_default() += 1; | |
| 446 | + | } | |
| 447 | + | } | |
| 448 | + | ||
| 449 | + | let mut rows: Vec<ProjectRow> = projects | |
| 450 | + | .iter() | |
| 451 | + | .map(|p| ProjectRow { | |
| 452 | + | name: p.name.clone(), | |
| 453 | + | status: p.status.clone(), | |
| 454 | + | project_type: p.project_type.clone(), | |
| 455 | + | open: open_counts.get(p.name.as_str()).copied().unwrap_or(0), | |
| 456 | + | }) | |
| 457 | + | .collect(); | |
| 458 | + | rows.sort_by(|a, b| { | |
| 459 | + | status_rank(&a.status) | |
| 460 | + | .cmp(&status_rank(&b.status)) | |
| 461 | + | .then_with(|| a.name.cmp(&b.name)) | |
| 462 | + | }); | |
| 463 | + | rows | |
| 464 | + | } | |
| 465 | + | ||
| 466 | + | /// Sort key for project status: live work first, dormant last. | |
| 467 | + | fn status_rank(status: &ProjectStatus) -> u8 { | |
| 468 | + | match status { | |
| 469 | + | ProjectStatus::Active => 0, | |
| 470 | + | ProjectStatus::OnHold => 1, | |
| 471 | + | ProjectStatus::Completed => 2, | |
| 472 | + | ProjectStatus::Archived => 3, | |
| 473 | + | } | |
| 474 | + | } | |
| 475 | + | ||
| 476 | + | /// Group upcoming events (already sorted ascending) by local calendar day, | |
| 477 | + | /// emitting a day header before each day's events. | |
| 478 | + | fn build_events(events: Vec<Event>) -> Vec<EventRow> { | |
| 479 | + | let today = Local::now().date_naive(); | |
| 480 | + | ||
| 481 | + | let mut groups: Vec<(NaiveDate, Vec<Event>)> = Vec::new(); | |
| 482 | + | for ev in events { | |
| 483 | + | let day = ev.start_time.with_timezone(&Local).date_naive(); | |
| 484 | + | match groups.last_mut() { | |
| 485 | + | Some((d, bucket)) if *d == day => bucket.push(ev), | |
| 486 | + | _ => groups.push((day, vec![ev])), | |
| 487 | + | } | |
| 488 | + | } | |
| 489 | + | ||
| 490 | + | let mut rows = Vec::new(); | |
| 491 | + | for (day, bucket) in groups { | |
| 492 | + | rows.push(EventRow::DayHeader { | |
| 493 | + | label: day_label(day, today), | |
| 494 | + | count: bucket.len(), | |
| 495 | + | }); | |
| 496 | + | rows.extend(bucket.into_iter().map(|e| EventRow::Event(Box::new(e)))); | |
| 497 | + | } | |
| 498 | + | rows | |
| 499 | + | } | |
| 500 | + |
Lines truncated