diff --git a/Cargo.lock b/Cargo.lock index d67ef9b..872879a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -17,18 +26,69 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -36,10 +96,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] -name = "cassowary" -version = "0.3.0" +name = "bytemuck" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" [[package]] name = "castaway" @@ -66,6 +126,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.42" @@ -81,9 +147,9 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" dependencies = [ "castaway", "cfg-if", @@ -108,13 +174,22 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crossterm" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags", + "bitflags 2.10.0", "crossterm_winapi", "mio", "parking_lot", @@ -130,7 +205,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags", + "bitflags 2.10.0", "crossterm_winapi", "derive_more", "document-features", @@ -151,6 +226,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + [[package]] name = "darling" version = "0.20.11" @@ -172,7 +267,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.111", ] [[package]] @@ -183,7 +278,22 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.111", +] + +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", ] [[package]] @@ -205,7 +315,17 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 2.0.111", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", ] [[package]] @@ -260,6 +380,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "euclid" +version = "0.22.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" +dependencies = [ + "num-traits", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -272,12 +401,45 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "find-msvc-tools" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "fnv" version = "1.0.7" @@ -286,15 +448,19 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.5" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] -name = "foldhash" -version = "0.2.0" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] [[package]] name = "getrandom" @@ -319,24 +485,15 @@ dependencies = [ "wasip2", ] -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.1.5", -] - [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "foldhash 0.2.0", + "allocator-api2", + "equivalent", + "foldhash", ] [[package]] @@ -345,7 +502,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" dependencies = [ - "hashbrown 0.16.1", + "hashbrown", ] [[package]] @@ -354,6 +511,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -403,14 +566,14 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] name = "itertools" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] @@ -431,16 +594,40 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kasuari" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fe90c1150662e858c7d5f945089b7517b0a80d8bf7ba4b1b5ffc984e7230a5b" +dependencies = [ + "hashbrown", + "portable-atomic", + "thiserror 2.0.17", +] + +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "lazytasks" version = "0.1.0" dependencies = [ "chrono", - "crossterm 0.29.0", + "crossterm 0.28.1", "dirs", "ratatui", "rusqlite", "tui-input", + "tui-tree-widget", "uuid", ] @@ -456,7 +643,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" dependencies = [ - "bitflags", + "bitflags 2.10.0", "libc", ] @@ -471,6 +658,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "line-clipping" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -506,13 +702,50 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" -version = "0.12.5" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "mac_address" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" dependencies = [ - "hashbrown 0.15.5", + "nix", + "winapi", ] +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mio" version = "1.1.1" @@ -525,6 +758,46 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -534,6 +807,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -546,6 +828,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -570,10 +861,99 @@ dependencies = [ ] [[package]] -name = "paste" -version = "1.0.15" +name = "pest" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "pest_meta" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] [[package]] name = "pkg-config" @@ -581,6 +961,18 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.103" @@ -605,25 +997,104 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "ratatui" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" dependencies = [ - "bitflags", - "cassowary", + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termwiz", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" +dependencies = [ + "bitflags 2.10.0", "compact_str", - "crossterm 0.28.1", + "hashbrown", "indoc", - "instability", "itertools", + "kasuari", "lru", - "paste", "strum", + "thiserror 2.0.17", "unicode-segmentation", "unicode-truncate", - "unicode-width 0.2.0", + "unicode-width", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" +dependencies = [ + "cfg-if", + "crossterm 0.29.0", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" +dependencies = [ + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" +dependencies = [ + "bitflags 2.10.0", + "hashbrown", + "indoc", + "instability", + "itertools", + "line-clipping", + "ratatui-core", + "strum", + "time", + "unicode-segmentation", + "unicode-width", ] [[package]] @@ -632,7 +1103,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.10.0", ] [[package]] @@ -643,16 +1114,45 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror", + "thiserror 2.0.17", ] +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + [[package]] name = "rusqlite" version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3" dependencies = [ - "bitflags", + "bitflags 2.10.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -676,7 +1176,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -689,7 +1189,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -720,6 +1220,47 @@ version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -756,6 +1297,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "smallvec" version = "1.15.1" @@ -769,9 +1316,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05e98301bf8b0540c7de45ecd760539b9c62f5772aed172f08efba597c11cd5d" dependencies = [ "cc", - "hashbrown 0.16.1", + "hashbrown", "js-sys", - "thiserror", + "thiserror 2.0.17", "wasm-bindgen", ] @@ -789,24 +1336,34 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.3" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.26.4" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", - "syn", + "syn 2.0.111", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] @@ -820,13 +1377,96 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom", + "phf", + "phf_codegen", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.10.0", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix", + "num-derive", + "num-traits", + "ordered-float", + "pest", + "pest_derive", + "phf", + "sha2", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] @@ -837,19 +1477,63 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", +] + +[[package]] +name = "time" +version = "0.3.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", ] +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + [[package]] name = "tui-input" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911e93158bf80bbc94bad533b2b16e3d711e1132d69a6a6980c3920a63422c19" +checksum = "79c1ee964298f136020f5f69e0e601f4d3a1f610a7baf1af9fcb96152e8a2c45" dependencies = [ "ratatui", - "unicode-width 0.2.0", + "unicode-width", ] +[[package]] +name = "tui-tree-widget" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deca119555009eee2e0cfb9c020f39f632444dc4579918d5fc009d51d75dff92" +dependencies = [ + "ratatui-core", + "ratatui-widgets", + "unicode-width", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.22" @@ -864,26 +1548,26 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-truncate" -version = "1.1.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" dependencies = [ "itertools", "unicode-segmentation", - "unicode-width 0.1.14", + "unicode-width", ] [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] -name = "unicode-width" -version = "0.2.0" +name = "utf8parse" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" @@ -891,6 +1575,7 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ + "atomic", "getrandom 0.3.4", "js-sys", "wasm-bindgen", @@ -902,6 +1587,21 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -949,7 +1649,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.111", "wasm-bindgen-shared", ] @@ -962,6 +1662,78 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1005,7 +1777,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -1016,7 +1788,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a1a01ba..5245b64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,9 +5,10 @@ edition = "2024" [dependencies] chrono = "0.4.42" -crossterm = "0.29.0" +crossterm = "0.28.1" dirs = "6.0.0" -ratatui = "0.29.0" -tui-input = "0.14.0" +ratatui = "0.30.0" +tui-input = "0.15.0" uuid = { version = "1.19.0", features = ["v4"] } rusqlite = { version = "0.38.0", features = ["bundled"] } +tui-tree-widget = "0.24.0" diff --git a/src/actions/archive_workspace.rs b/src/actions/archive_workspace.rs new file mode 100644 index 0000000..3555b59 --- /dev/null +++ b/src/actions/archive_workspace.rs @@ -0,0 +1,47 @@ +use chrono::Utc; +use uuid::Uuid; + +use crate::{ + app::App, + db::repositories::{WorkspaceRepository, TaskRepository}, +}; + +pub fn archive_workspace(app: &mut App, option_idx: Option, workspace_id: Uuid) { + if option_idx != Some(0) { + return; + } + + let workspace = match app.workspaces.iter_mut().find(|s| s.id == workspace_id) { + Some(s) => s, + None => return, + }; + + workspace.archived = !workspace.archived; + workspace.archived_at = if workspace.archived { + Some(Utc::now()) + } else { + None + }; + workspace.updated_at = Some(Utc::now()); + + if let Err(e) = WorkspaceRepository::update(&app.db.connection, workspace) { + app.error = Some(e.to_string()); + return; + } + + let is_archived = workspace.archived; + + for task in app.tasks.iter_mut().filter(|t| t.workspace_id == Some(workspace_id)) { + task.archived = is_archived; + task.archived_at = if is_archived { Some(Utc::now()) } else { None }; + task.updated_at = Some(Utc::now()); + + if let Err(e) = TaskRepository::update(&app.db.connection, task) { + app.error = Some(e.to_string()); + return; + } + } + + app.selected_tasks.clear(); + app.state.workspaces_tree_state.select_first(); +} diff --git a/src/actions/create_task.rs b/src/actions/create_task.rs index 616d521..80dedfa 100644 --- a/src/actions/create_task.rs +++ b/src/actions/create_task.rs @@ -1,7 +1,9 @@ +use uuid::Uuid; + use crate::{app::App, db::repositories::TaskRepository, models}; -pub fn create_task(app: &mut App, title: String) { - let new_task = models::Task::new(title); +pub fn create_task(app: &mut App, title: String, workspace_id: Option) { + let new_task = models::Task::new(title, workspace_id); if let Err(e) = TaskRepository::create(&app.db.connection, &new_task) { app.error = Some(e.to_string()); diff --git a/src/actions/create_workspace.rs b/src/actions/create_workspace.rs new file mode 100644 index 0000000..b47948b --- /dev/null +++ b/src/actions/create_workspace.rs @@ -0,0 +1,18 @@ +use crate::{app::App, db::repositories::WorkspaceRepository, models}; + +pub fn create_workspace(app: &mut App, title: String) { + let new_workspace = models::Workspace::new(title); + let workspace_id = new_workspace.id.to_string(); + + if let Err(e) = WorkspaceRepository::create(&app.db.connection, &new_workspace) { + app.error = Some(e.to_string()); + return; + }; + + let is_first_workspace = app.workspaces.is_empty(); + app.workspaces.push(new_workspace); + + if is_first_workspace { + app.state.workspaces_tree_state.select(vec![workspace_id]); + } +} diff --git a/src/actions/delete_workspace.rs b/src/actions/delete_workspace.rs new file mode 100644 index 0000000..423c076 --- /dev/null +++ b/src/actions/delete_workspace.rs @@ -0,0 +1,19 @@ +use uuid::Uuid; + +use crate::{app::App, db::repositories::WorkspaceRepository}; + +pub fn delete_workspace(app: &mut App, option_idx: Option, workspace_id: Uuid) { + if option_idx != Some(0) { + return; + } + + if let Err(e) = WorkspaceRepository::delete(&app.db.connection, &workspace_id) { + app.error = Some(e.to_string()); + return; + } + + app.tasks.retain(|t| t.workspace_id != Some(workspace_id)); + app.workspaces.retain(|s| s.id != workspace_id); + app.selected_tasks.clear(); + app.state.workspaces_tree_state.select_first(); +} diff --git a/src/actions/edit_task.rs b/src/actions/edit_task.rs index d7c9955..498a57c 100644 --- a/src/actions/edit_task.rs +++ b/src/actions/edit_task.rs @@ -1,35 +1,41 @@ use chrono::Utc; use ratatui::DefaultTerminal; +use uuid::Uuid; use crate::{app::App, db::repositories::TaskRepository, editor, state::PanelState}; pub fn edit_task(app: &mut App, terminal: &mut DefaultTerminal) { - if app.state.active_panel == PanelState::ActiveTasks { - let task_id = app - .state - .get_selected_panel_state() - .and_then(|s| s.selected()) - .and_then(|idx| app.get_current_tasks().get(idx).map(|t| t.id)); - - if let Some(task_id) = task_id - && let Some(task_ref) = app.tasks.iter().find(|t| t.id == task_id) - { - let update = editor::open_in_editor(task_ref, terminal); - - // Only apply changes if title is not empty - if !update.title.is_empty() - && let Some(task) = app.tasks.iter_mut().find(|t| t.id == task_id) - { - task.title = update.title; - task.description = Some(update.description); - task.updated_at = Some(Utc::now()); - - if let Err(e) = TaskRepository::update(&app.db.connection, task) { - app.error = Some(e.to_string()); - - return; - }; - } + if app.state.active_panel != PanelState::ActiveTasks { + return; + } + + let selected = app.state.workspaces_tree_state.selected(); + if selected.is_empty() { + return; + } + + let selected_id = selected.last().unwrap(); + let task_id = match Uuid::parse_str(selected_id) { + Ok(uuid) => uuid, + Err(_) => return, + }; + + let task_ref = match app.tasks.iter().find(|t| t.id == task_id) { + Some(t) => t, + None => return, + }; + + let update = editor::open_in_editor(task_ref, terminal); + + if !update.title.is_empty() { + if let Some(task) = app.tasks.iter_mut().find(|t| t.id == task_id) { + task.title = update.title; + task.description = Some(update.description); + task.updated_at = Some(Utc::now()); + + if let Err(e) = TaskRepository::update(&app.db.connection, task) { + app.error = Some(e.to_string()); + }; } } } diff --git a/src/actions/mod.rs b/src/actions/mod.rs index 71f3868..2faf081 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -1,14 +1,22 @@ +pub mod archive_workspace; pub mod clean_err_msg; pub mod close_modal; pub mod create_task; +pub mod create_workspace; pub mod delete_task; +pub mod delete_workspace; pub mod edit_priority; pub mod edit_task; pub mod edit_title; +pub mod move_task; pub mod open_archive_modal; -pub mod open_create_modal; +pub mod open_archive_workspace_modal; +pub mod open_create_task_modal; +pub mod open_create_workspace_modal; pub mod open_delete_modal; +pub mod open_delete_workspace_modal; pub mod open_edit_title_modal; +pub mod open_move_task_modal; pub mod open_priority_modal; pub mod quit; pub mod select_next_task; @@ -18,17 +26,25 @@ pub mod toggle_archive_task; pub mod toggle_task_completion; pub mod toggle_task_selection; +pub use archive_workspace::archive_workspace; pub use clean_err_msg::clean_err_msg; pub use close_modal::close_modal; pub use create_task::create_task; +pub use create_workspace::create_workspace; pub use delete_task::delete_task; +pub use delete_workspace::delete_workspace; pub use edit_priority::edit_priority; pub use edit_task::edit_task; pub use edit_title::edit_title; +pub use move_task::move_task; pub use open_archive_modal::open_archive_modal; -pub use open_create_modal::open_create_modal; +pub use open_archive_workspace_modal::open_archive_workspace_modal; +pub use open_create_task_modal::open_create_task_modal; +pub use open_create_workspace_modal::open_create_workspace_modal; pub use open_delete_modal::open_delete_modal; +pub use open_delete_workspace_modal::open_delete_workspace_modal; pub use open_edit_title_modal::open_edit_title_modal; +pub use open_move_task_modal::open_move_task_modal; pub use open_priority_modal::open_priority_modal; pub use quit::quit; pub use select_next_task::select_next_task; diff --git a/src/actions/move_task.rs b/src/actions/move_task.rs new file mode 100644 index 0000000..46f1fa7 --- /dev/null +++ b/src/actions/move_task.rs @@ -0,0 +1,29 @@ +use chrono::Utc; +use uuid::Uuid; + +use crate::{app::App, db::repositories::TaskRepository, models::Workspace}; + +pub fn move_task(app: &mut App, option_idx: Option, task_id: Uuid, workspaces: &[Workspace]) { + let idx = match option_idx { + Some(i) => i, + None => return, + }; + + let new_workspace_id = if idx >= workspaces.len() { + None + } else { + workspaces.get(idx).map(|w| w.id) + }; + + let task = match app.tasks.iter_mut().find(|t| t.id == task_id) { + Some(t) => t, + None => return, + }; + + task.workspace_id = new_workspace_id; + task.updated_at = Some(Utc::now()); + + if let Err(e) = TaskRepository::update(&app.db.connection, task) { + app.error = Some(e.to_string()); + } +} diff --git a/src/actions/open_archive_modal.rs b/src/actions/open_archive_modal.rs index 464b944..4b23406 100644 --- a/src/actions/open_archive_modal.rs +++ b/src/actions/open_archive_modal.rs @@ -1,15 +1,34 @@ +use uuid::Uuid; + use crate::{app::App, state::PanelState}; pub fn open_archive_modal(app: &mut App) { let is_archived = app.state.active_panel == PanelState::ArchivedTasks; - if app.selected_tasks.is_empty() { - if let Some(task_index) = app.state.get_selected_panel_state().and_then(|s| s.selected()) { - let task_id = app.get_current_tasks()[task_index].id; - app.state.open_archived_task(vec![task_id], is_archived) - } - } else { + if !app.selected_tasks.is_empty() { app.state .open_archived_task(app.selected_tasks.clone(), is_archived); + return; + } + + if app.state.active_panel == PanelState::ActiveTasks { + let selected = app.state.workspaces_tree_state.selected(); + if selected.is_empty() { + return; + } + + let selected_id = selected.last().unwrap(); + if let Ok(uuid) = Uuid::parse_str(selected_id) { + if app.tasks.iter().any(|t| t.id == uuid) { + app.state.open_archived_task(vec![uuid], is_archived); + } + } + } else if let Some(task_index) = app.state.get_selected_panel_state().and_then(|s| s.selected()) + { + let tasks = app.get_current_tasks(); + if task_index < tasks.len() { + app.state + .open_archived_task(vec![tasks[task_index].id], is_archived); + } } } diff --git a/src/actions/open_archive_workspace_modal.rs b/src/actions/open_archive_workspace_modal.rs new file mode 100644 index 0000000..1869c7a --- /dev/null +++ b/src/actions/open_archive_workspace_modal.rs @@ -0,0 +1,22 @@ +use uuid::Uuid; + +use crate::{app::App, state::PanelState}; + +pub fn open_archive_workspace_modal(app: &mut App) { + if app.state.active_panel != PanelState::ActiveTasks { + return; + } + + let selected = app.state.workspaces_tree_state.selected(); + if selected.len() != 1 { + return; + } + + let selected_id = &selected[0]; + + if let Ok(uuid) = Uuid::parse_str(selected_id) { + if let Some(workspace) = app.workspaces.iter().find(|s| s.id == uuid) { + app.state.open_archive_workspace(uuid, workspace.archived); + } + } +} diff --git a/src/actions/open_create_task_modal.rs b/src/actions/open_create_task_modal.rs new file mode 100644 index 0000000..404c52a --- /dev/null +++ b/src/actions/open_create_task_modal.rs @@ -0,0 +1,21 @@ +use uuid::Uuid; + +use crate::{app::App, state::PanelState}; + +pub fn open_create_task_modal(app: &mut App) { + if app.state.active_panel != PanelState::ActiveTasks { + return; + } + + let selected = app.state.workspaces_tree_state.selected(); + + let workspace_id = if selected.is_empty() { + None + } else { + Uuid::parse_str(&selected[0]).ok().filter(|uuid| { + app.workspaces.iter().any(|w| w.id == *uuid) + }) + }; + + app.state.open_create_task(workspace_id) +} diff --git a/src/actions/open_create_modal.rs b/src/actions/open_create_workspace_modal.rs similarity index 53% rename from src/actions/open_create_modal.rs rename to src/actions/open_create_workspace_modal.rs index 26d4210..6531ac3 100644 --- a/src/actions/open_create_modal.rs +++ b/src/actions/open_create_workspace_modal.rs @@ -1,7 +1,7 @@ use crate::{app::App, state::PanelState}; -pub fn open_create_modal(app: &mut App) { +pub fn open_create_workspace_modal(app: &mut App) { if app.state.active_panel == PanelState::ActiveTasks { - app.state.open_create_task() + app.state.open_create_workspace() } } diff --git a/src/actions/open_delete_modal.rs b/src/actions/open_delete_modal.rs index d38da86..19af2ab 100644 --- a/src/actions/open_delete_modal.rs +++ b/src/actions/open_delete_modal.rs @@ -1,12 +1,30 @@ -use crate::app::App; +use uuid::Uuid; + +use crate::{app::App, state::PanelState}; pub fn open_delete_modal(app: &mut App) { - if app.selected_tasks.is_empty() { - if let Some(task_index) = app.state.get_selected_panel_state().and_then(|s| s.selected()) { - let task_id = app.get_current_tasks()[task_index].id; - app.state.open_delete_task(vec![task_id]); - } - } else { + if !app.selected_tasks.is_empty() { app.state.open_delete_task(app.selected_tasks.clone()); + return; + } + + if app.state.active_panel == PanelState::ActiveTasks { + let selected = app.state.workspaces_tree_state.selected(); + if selected.is_empty() { + return; + } + + let selected_id = selected.last().unwrap(); + if let Ok(uuid) = Uuid::parse_str(selected_id) { + if app.tasks.iter().any(|t| t.id == uuid) { + app.state.open_delete_task(vec![uuid]); + } + } + } else if let Some(task_index) = app.state.get_selected_panel_state().and_then(|s| s.selected()) + { + let tasks = app.get_current_tasks(); + if task_index < tasks.len() { + app.state.open_delete_task(vec![tasks[task_index].id]); + } } } diff --git a/src/actions/open_delete_workspace_modal.rs b/src/actions/open_delete_workspace_modal.rs new file mode 100644 index 0000000..afb0cd6 --- /dev/null +++ b/src/actions/open_delete_workspace_modal.rs @@ -0,0 +1,22 @@ +use uuid::Uuid; + +use crate::{app::App, state::PanelState}; + +pub fn open_delete_workspace_modal(app: &mut App) { + if app.state.active_panel != PanelState::ActiveTasks { + return; + } + + let selected = app.state.workspaces_tree_state.selected(); + if selected.len() != 1 { + return; + } + + let selected_id = &selected[0]; + + if let Ok(uuid) = Uuid::parse_str(selected_id) { + if app.workspaces.iter().any(|s| s.id == uuid) { + app.state.open_delete_workspace(uuid); + } + } +} diff --git a/src/actions/open_edit_title_modal.rs b/src/actions/open_edit_title_modal.rs index da29aed..4535f37 100644 --- a/src/actions/open_edit_title_modal.rs +++ b/src/actions/open_edit_title_modal.rs @@ -1,10 +1,19 @@ +use uuid::Uuid; + use crate::{app::App, state::PanelState}; pub fn open_edit_title_modal(app: &mut App) { - if let Some(task_index) = app.state.get_selected_panel_state().and_then(|s| s.selected()) - && app.state.active_panel == PanelState::ActiveTasks - { - let task = &app.get_current_tasks()[task_index]; - app.state.open_edit_task(task.id, task.title.clone()); + if app.state.active_panel == PanelState::ActiveTasks { + let selected = app.state.workspaces_tree_state.selected(); + if selected.is_empty() { + return; + } + + let selected_id = selected.last().unwrap(); + if let Ok(uuid) = Uuid::parse_str(selected_id) { + if let Some(task) = app.tasks.iter().find(|t| t.id == uuid) { + app.state.open_edit_task(task.id, task.title.clone()); + } + } } } diff --git a/src/actions/open_move_task_modal.rs b/src/actions/open_move_task_modal.rs new file mode 100644 index 0000000..a040071 --- /dev/null +++ b/src/actions/open_move_task_modal.rs @@ -0,0 +1,22 @@ +use uuid::Uuid; + +use crate::{app::App, state::PanelState}; + +pub fn open_move_task_modal(app: &mut App) { + if app.state.active_panel != PanelState::ActiveTasks { + return; + } + + let selected = app.state.workspaces_tree_state.selected(); + if selected.is_empty() { + return; + } + + let selected_id = selected.last().unwrap(); + + if let Ok(uuid) = Uuid::parse_str(selected_id) { + if app.tasks.iter().any(|t| t.id == uuid && !t.archived) { + app.state.open_move_task(uuid); + } + } +} diff --git a/src/actions/open_priority_modal.rs b/src/actions/open_priority_modal.rs index 606498b..4be374b 100644 --- a/src/actions/open_priority_modal.rs +++ b/src/actions/open_priority_modal.rs @@ -1,20 +1,26 @@ +use uuid::Uuid; + use crate::{app::App, state::PanelState}; pub fn open_priority_modal(app: &mut App) { - let is_active_tasks = app.state.active_panel == PanelState::ActiveTasks; + if app.state.active_panel != PanelState::ActiveTasks { + return; + } + + if !app.selected_tasks.is_empty() { + app.state.open_priority_task(app.selected_tasks.clone()); + return; + } + + let selected = app.state.workspaces_tree_state.selected(); + if selected.is_empty() { + return; + } - if is_active_tasks { - if app.selected_tasks.is_empty() { - if let Some(task_index) = app - .state - .get_selected_panel_state() - .and_then(|s| s.selected()) - { - let task_id = app.get_current_tasks()[task_index].id; - app.state.open_priority_task(vec![task_id]); - } - } else { - app.state.open_priority_task(app.selected_tasks.clone()); + let selected_id = selected.last().unwrap(); + if let Ok(uuid) = Uuid::parse_str(selected_id) { + if app.tasks.iter().any(|t| t.id == uuid) { + app.state.open_priority_task(vec![uuid]); } } } diff --git a/src/actions/toggle_task_completion.rs b/src/actions/toggle_task_completion.rs index 3d74ac2..0544396 100644 --- a/src/actions/toggle_task_completion.rs +++ b/src/actions/toggle_task_completion.rs @@ -1,16 +1,12 @@ use chrono::Utc; +use uuid::Uuid; -use crate::{app::App, db::repositories::TaskRepository}; +use crate::{app::App, db::repositories::TaskRepository, state::PanelState}; pub fn toggle_task_completion(app: &mut App) { - if app.selected_tasks.is_empty() { - if let Some(task_index) = app - .state - .get_selected_panel_state() - .and_then(|s| s.selected()) - { - let task = app.get_current_tasks()[task_index].clone(); - if let Some(task) = app.tasks.iter_mut().find(|t| t.id == task.id) { + if !app.selected_tasks.is_empty() { + for task in app.tasks.iter_mut() { + if app.selected_tasks.contains(&task.id) { task.completed = !task.completed; task.updated_at = Some(Utc::now()); @@ -20,18 +16,26 @@ pub fn toggle_task_completion(app: &mut App) { }; } } - } else { - for task in app.tasks.iter_mut() { - if app.selected_tasks.contains(&task.id) { + app.selected_tasks.clear(); + return; + } + + if app.state.active_panel == PanelState::ActiveTasks { + let selected = app.state.workspaces_tree_state.selected(); + if selected.is_empty() { + return; + } + + let selected_id = selected.last().unwrap(); + if let Ok(uuid) = Uuid::parse_str(selected_id) { + if let Some(task) = app.tasks.iter_mut().find(|t| t.id == uuid) { task.completed = !task.completed; task.updated_at = Some(Utc::now()); if let Err(e) = TaskRepository::update(&app.db.connection, task) { app.error = Some(e.to_string()); - return; }; } } - app.selected_tasks.clear(); } } diff --git a/src/actions/toggle_task_selection.rs b/src/actions/toggle_task_selection.rs index 2810ffd..4d05895 100644 --- a/src/actions/toggle_task_selection.rs +++ b/src/actions/toggle_task_selection.rs @@ -1,16 +1,50 @@ -use crate::app::App; +use uuid::Uuid; + +use crate::{app::App, state::PanelState}; pub fn toggle_task_selection(app: &mut App) { - if let Some(task_index) = app - .state - .get_selected_panel_state() - .and_then(|s| s.selected()) - { - let task_id = app.get_current_tasks()[task_index].id; - if app.selected_tasks.contains(&task_id) { - app.selected_tasks.retain(|id| *id != task_id); - } else { - app.selected_tasks.push(task_id); + if app.state.active_panel != PanelState::ActiveTasks { + return; + } + + let selected = app.state.workspaces_tree_state.selected(); + if selected.is_empty() { + return; + } + + let selected_id = selected.last().unwrap(); + if let Ok(uuid) = Uuid::parse_str(selected_id) { + if app.tasks.iter().any(|t| t.id == uuid) { + if app.selected_tasks.contains(&uuid) { + app.selected_tasks.retain(|id| *id != uuid); + } else { + app.selected_tasks.push(uuid); + } + return; + } + + if app.workspaces.iter().any(|w| w.id == uuid) { + let workspace_task_ids: Vec = app + .tasks + .iter() + .filter(|t| t.workspace_id == Some(uuid) && !t.archived) + .map(|t| t.id) + .collect(); + + let all_selected = workspace_task_ids + .iter() + .all(|id| app.selected_tasks.contains(id)); + + if all_selected { + app.selected_tasks + .retain(|id| !workspace_task_ids.contains(id)); + } else { + for task_id in workspace_task_ids { + if !app.selected_tasks.contains(&task_id) { + app.selected_tasks.push(task_id); + } + } + } } } } diff --git a/src/app.rs b/src/app.rs index 1c4d82b..19a906b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,7 +8,10 @@ use uuid::Uuid; use crate::{ components, - db::{Db, repositories::TaskRepository}, + db::{ + Db, + repositories::{WorkspaceRepository, TaskRepository}, + }, keybindings::handle_key_event, models::Task, state, @@ -17,6 +20,7 @@ use crate::{models, state::ModalState}; pub struct App { pub exit: bool, + pub workspaces: Vec, pub tasks: Vec, pub selected_tasks: Vec, pub state: state::AppState, @@ -29,18 +33,24 @@ impl App { let state = state::AppState::new(); let db = Db::new(); - let (tasks, error) = match TaskRepository::get_all(&db.connection) { + let (tasks, _) = match TaskRepository::get_all(&db.connection) { Ok(tasks) => (tasks, None), Err(err) => (vec![], Some(err.to_string())), }; + let (workspaces, workspace_err) = match WorkspaceRepository::get_all(&db.connection) { + Ok(workspaces) => (workspaces, None), + Err(err) => (vec![], Some(err.to_string())), + }; + App { exit: false, selected_tasks: Vec::new(), state, db, tasks, - error, + workspaces, + error: workspace_err, } } @@ -64,8 +74,8 @@ impl App { components::bottom_bar::render(frame, layout[1], self); match &mut self.state.active_modal { - Some(ModalState::CreateTask { input }) => { - components::modals::create_task::render(frame, input); + Some(ModalState::CreateTask { input, workspace_id: _ }) => { + components::modals::create_task::render(frame, input, "Create task"); } Some(ModalState::EditTask { task_id: _, input }) => { components::modals::edit_task::render(frame, input); @@ -89,6 +99,29 @@ impl App { }) => { components::modals::priority_task::render(frame, selected_option); } + Some(ModalState::CreateWorkspace { input }) => { + components::modals::create_task::render(frame, input, "Create workspace"); + } + Some(ModalState::DeleteWorkspace { + workspace_id: _, + selected_option, + }) => { + components::modals::delete_workspace::render(frame, selected_option); + } + Some(ModalState::ArchiveWorkspace { + workspace_id: _, + selected_option, + is_archived, + }) => { + components::modals::archive_workspace::render(frame, selected_option, *is_archived); + } + Some(ModalState::MoveTask { + task_id: _, + selected_option, + }) => { + let workspaces: Vec<_> = self.workspaces.iter().filter(|s| !s.archived).cloned().collect(); + components::modals::move_task::render(frame, selected_option, &workspaces); + } None => {} } } diff --git a/src/components/bottom_bar/key_hints.rs b/src/components/bottom_bar/key_hints.rs index 5caaa0c..791918b 100644 --- a/src/components/bottom_bar/key_hints.rs +++ b/src/components/bottom_bar/key_hints.rs @@ -10,7 +10,7 @@ use crate::{app::App, state::PanelState}; pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { let help_text = match app.state.active_panel { PanelState::ActiveTasks => { - "Create: c | Edit (Title): e | Edit: E | Delete: d | Complete: y | Archive: a | Select: | Keybindings: ?" + "Create task: c | Create workspace: w | Edit title: e | Edit: E | Delete: d | Complete: y | Archive: a | Select: | Keybindings: ?" } PanelState::ArchivedTasks => { "Unarchive: a | Complete: y | Delete: d | Select: | Keybindings: ?" diff --git a/src/components/modals/archive_workspace.rs b/src/components/modals/archive_workspace.rs new file mode 100644 index 0000000..fc9ec76 --- /dev/null +++ b/src/components/modals/archive_workspace.rs @@ -0,0 +1,23 @@ +use ratatui::{ + Frame, + style::{Color, Modifier, Style}, + widgets::{List, ListItem, ListState}, +}; + +use crate::components::shared; + +pub fn render(frame: &mut Frame, selected_option: &mut ListState, is_archived: bool) { + let action_name = if is_archived { "Unarchive" } else { "Archive" }; + let area = shared::modal::Modal::new(format!("{} workspace", action_name)) + .height(4) + .render(frame); + let list_items: Vec = vec![ListItem::from(action_name), ListItem::from("Cancel")]; + let options = List::new(list_items).highlight_style( + Style::default() + .bg(Color::Blue) + .fg(Color::White) + .add_modifier(Modifier::BOLD), + ); + + frame.render_stateful_widget(options, area, selected_option); +} diff --git a/src/components/modals/create_task.rs b/src/components/modals/create_task.rs index dfacf5b..aef912c 100644 --- a/src/components/modals/create_task.rs +++ b/src/components/modals/create_task.rs @@ -3,8 +3,8 @@ use tui_input::Input; use crate::components::shared; -pub fn render(frame: &mut Frame, input_state: &Input) { - let area = shared::modal::Modal::new("Create task") +pub fn render(frame: &mut Frame, input_state: &Input, title: &str) { + let area = shared::modal::Modal::new(title) .height(3) .render(frame); let width = area.width.saturating_sub(2) as usize; diff --git a/src/components/modals/delete_workspace.rs b/src/components/modals/delete_workspace.rs new file mode 100644 index 0000000..48b2344 --- /dev/null +++ b/src/components/modals/delete_workspace.rs @@ -0,0 +1,22 @@ +use ratatui::{ + Frame, + style::{Color, Modifier, Style}, + widgets::{List, ListItem, ListState}, +}; + +use crate::components::shared; + +pub fn render(frame: &mut Frame, selected_option: &mut ListState) { + let area = shared::modal::Modal::new("Delete workspace") + .height(4) + .render(frame); + let list_items: Vec = vec![ListItem::from("Delete"), ListItem::from("Cancel")]; + let delete_options = List::new(list_items).highlight_style( + Style::default() + .bg(Color::Blue) + .fg(Color::White) + .add_modifier(Modifier::BOLD), + ); + + frame.render_stateful_widget(delete_options, area, selected_option); +} diff --git a/src/components/modals/mod.rs b/src/components/modals/mod.rs index 5c6ebdd..048a563 100644 --- a/src/components/modals/mod.rs +++ b/src/components/modals/mod.rs @@ -1,5 +1,8 @@ pub mod archive_task; +pub mod archive_workspace; pub mod create_task; pub mod delete_task; +pub mod delete_workspace; pub mod edit_task; +pub mod move_task; pub mod priority_task; diff --git a/src/components/modals/move_task.rs b/src/components/modals/move_task.rs new file mode 100644 index 0000000..d082c90 --- /dev/null +++ b/src/components/modals/move_task.rs @@ -0,0 +1,29 @@ +use ratatui::{ + Frame, + style::{Color, Modifier, Style}, + widgets::{List, ListItem, ListState}, +}; + +use crate::{components::shared, models::Workspace}; + +pub fn render(frame: &mut Frame, selected_option: &mut ListState, workspaces: &[Workspace]) { + let height = (workspaces.len() + 3).max(3).min(10) as u16; + let area = shared::modal::Modal::new("Move to workspace") + .height(height) + .render(frame); + + let mut list_items: Vec = workspaces + .iter() + .map(|w| ListItem::from(w.title.clone())) + .collect(); + list_items.push(ListItem::from("No workspace")); + + let options = List::new(list_items).highlight_style( + Style::default() + .bg(Color::Blue) + .fg(Color::White) + .add_modifier(Modifier::BOLD), + ); + + frame.render_stateful_widget(options, area, selected_option); +} diff --git a/src/components/workspace/context_view/mod.rs b/src/components/workspace/context_view/mod.rs index ee84edc..62caae8 100644 --- a/src/components/workspace/context_view/mod.rs +++ b/src/components/workspace/context_view/mod.rs @@ -1,17 +1,36 @@ pub mod about; pub mod task_view; +pub mod workspace_view; use ratatui::{Frame, layout::Rect}; +use uuid::Uuid; use crate::{app::App, components::workspace::context_view, state::PanelState}; pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { match app.state.active_panel { PanelState::ActiveTasks => { - let title = String::from(" Task details "); - let tasks = app.active_tasks(); - let current_list = &app.state.active_tasks_state; - context_view::task_view::render(frame, area, title, current_list, tasks); + let selected = app.state.workspaces_tree_state.selected(); + + if selected.is_empty() { + context_view::task_view::render_empty(frame, area); + return; + } + + let selected_id = selected.last().unwrap(); + if let Ok(uuid) = Uuid::parse_str(selected_id) { + if let Some(workspace) = app.workspaces.iter().find(|w| w.id == uuid) { + context_view::workspace_view::render(frame, area, workspace, &app.tasks); + return; + } + + if let Some(task) = app.tasks.iter().find(|t| t.id == uuid) { + context_view::task_view::render_task(frame, area, task); + return; + } + } + + context_view::task_view::render_empty(frame, area); } PanelState::ArchivedTasks => { let title = String::from(" Task details "); diff --git a/src/components/workspace/context_view/task_view.rs b/src/components/workspace/context_view/task_view.rs index 7e10684..6b30ea5 100644 --- a/src/components/workspace/context_view/task_view.rs +++ b/src/components/workspace/context_view/task_view.rs @@ -20,68 +20,7 @@ pub fn render( let text = if let Some(selected_idx) = current_list.selected() { if let Some(task) = tasks.get(selected_idx) { - let updated_at = task - .updated_at - .map(|d| d.with_timezone(&Local).format("%d/%m/%Y %H:%M").to_string()) - .unwrap_or_else(|| "-".to_string()); - let archived_at = task - .archived_at - .map(|d| d.with_timezone(&Local).format("%d/%m/%Y %H:%M").to_string()) - .unwrap_or_else(|| "-".to_string()); - - let mut lines = vec![ - Line::from(Span::styled("---", dim_style)), - Line::from(Span::styled( - format!("ID : {}", task.id), - dim_style, - )), - Line::from(Span::styled( - format!( - "Created_at : {}", - task.created_at - .with_timezone(&Local) - .format("%d/%m/%Y %H:%M") - ), - dim_style, - )), - Line::from(Span::styled( - format!("Updated_at : {}", updated_at), - dim_style, - )), - Line::from(Span::styled( - format!("Archived_at : {}", archived_at), - dim_style, - )), - Line::from(Span::styled( - format!("Completed : {}", task.completed), - dim_style, - )), - Line::from(Span::styled( - format!( - "Priority : {}", - match &task.priority { - Some(p) => Priority::label(p), - None => "x", - } - ), - dim_style, - )), - Line::from(Span::styled("---", dim_style)), - Line::from(""), - Line::from(Span::styled( - format!("# {}", task.title), - Style::default().add_modifier(Modifier::BOLD), - )), - Line::from(""), - ]; - - if let Some(description) = &task.description { - for desc_line in description.lines() { - lines.push(Line::from(format!(" {}", desc_line))); - } - } - - lines + build_task_lines(task) } else { vec![Line::from("Task not found")] } @@ -99,3 +38,87 @@ pub fn render( .wrap(Wrap { trim: true }); frame.render_widget(main_view, area); } + +pub fn render_task(frame: &mut Frame, area: Rect, task: &Task) { + let text = build_task_lines(task); + + let main_view = Paragraph::new(text) + .block( + Block::new() + .title(" Task details ") + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ) + .wrap(Wrap { trim: true }); + frame.render_widget(main_view, area); +} + +pub fn render_empty(frame: &mut Frame, area: Rect) { + let dim_style = Style::default().fg(Color::DarkGray); + let text = vec![Line::from(Span::styled("No task selected", dim_style))]; + + let main_view = Paragraph::new(text) + .block( + Block::new() + .title(" Task details ") + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ) + .wrap(Wrap { trim: true }); + frame.render_widget(main_view, area); +} + +fn build_task_lines(task: &Task) -> Vec> { + let dim_style = Style::default().fg(Color::DarkGray); + + let updated_at = task + .updated_at + .map(|d| d.with_timezone(&Local).format("%d/%m/%Y %H:%M").to_string()) + .unwrap_or_else(|| "-".to_string()); + let archived_at = task + .archived_at + .map(|d| d.with_timezone(&Local).format("%d/%m/%Y %H:%M").to_string()) + .unwrap_or_else(|| "-".to_string()); + + let mut lines = vec![ + Line::from(Span::styled("---", dim_style)), + Line::from(Span::styled(format!("ID : {}", task.id), dim_style)), + Line::from(Span::styled( + format!( + "Created_at : {}", + task.created_at + .with_timezone(&Local) + .format("%d/%m/%Y %H:%M") + ), + dim_style, + )), + Line::from(Span::styled(format!("Updated_at : {}", updated_at), dim_style)), + Line::from(Span::styled(format!("Archived_at : {}", archived_at), dim_style)), + Line::from(Span::styled(format!("Completed : {}", task.completed), dim_style)), + Line::from(Span::styled( + format!( + "Priority : {}", + match &task.priority { + Some(p) => Priority::label(p), + None => "x", + } + ), + dim_style, + )), + Line::from(Span::styled("---", dim_style)), + Line::from(""), + Line::from(Span::styled( + format!("# {}", task.title), + Style::default().add_modifier(Modifier::BOLD), + )), + Line::from(""), + ]; + + if let Some(description) = &task.description { + for desc_line in description.lines() { + lines.push(Line::from(format!(" {}", desc_line))); + } + } + + lines +} diff --git a/src/components/workspace/context_view/workspace_view.rs b/src/components/workspace/context_view/workspace_view.rs new file mode 100644 index 0000000..09e5cb5 --- /dev/null +++ b/src/components/workspace/context_view/workspace_view.rs @@ -0,0 +1,76 @@ +use chrono::Local; +use ratatui::{ + Frame, + layout::{Alignment, Constraint, Rect}, + style::{Color, Modifier, Style}, + text::Line, + widgets::{Block, BorderType, Borders, Cell, Row, Table}, +}; + +use crate::models::{Task, Workspace}; + +pub fn render(frame: &mut Frame, area: Rect, workspace: &Workspace, tasks: &[Task]) { + let mut workspace_tasks: Vec = tasks + .iter() + .filter(|t| t.workspace_id == Some(workspace.id) && !t.archived) + .cloned() + .collect(); + Task::sort_by_priority(&mut workspace_tasks); + + let header = Row::new(vec![ + Cell::from(Line::from("Priority").alignment(Alignment::Center)), + Cell::from("Title"), + Cell::from(Line::from("Created").alignment(Alignment::Center)), + Cell::from(Line::from("Completed").alignment(Alignment::Center)), + ]) + .style(Style::default().add_modifier(Modifier::BOLD)) + .height(1); + + let rows: Vec = workspace_tasks + .iter() + .map(|task| { + let priority_cell = match &task.priority { + Some(p) => Cell::from(Line::from(p.label()).alignment(Alignment::Center)) + .style(Style::default().fg(p.color())), + None => Cell::from(Line::from("-").alignment(Alignment::Center)), + }; + + let created_at = task + .created_at + .with_timezone(&Local) + .format("%d/%m/%Y") + .to_string(); + + let completed_cell = if task.completed { + Cell::from(Line::from("✓").alignment(Alignment::Center)) + .style(Style::default().fg(Color::Green)) + } else { + Cell::from(Line::from("✗").alignment(Alignment::Center)) + .style(Style::default().fg(Color::DarkGray)) + }; + + Row::new(vec![ + priority_cell, + Cell::from(task.title.clone()), + Cell::from(Line::from(created_at).alignment(Alignment::Center)), + completed_cell, + ]) + }) + .collect(); + + let table = Table::new(rows, [ + Constraint::Length(8), + Constraint::Fill(1), + Constraint::Length(12), + Constraint::Length(10), + ]) + .header(header) + .block( + Block::new() + .title(format!(" {} ({} tasks) ", workspace.title, workspace_tasks.len())) + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ); + + frame.render_widget(table, area); +} diff --git a/src/components/workspace/sidebar/active_workspaces.rs b/src/components/workspace/sidebar/active_workspaces.rs new file mode 100644 index 0000000..987b34b --- /dev/null +++ b/src/components/workspace/sidebar/active_workspaces.rs @@ -0,0 +1,132 @@ +use ratatui::{ + Frame, + layout::Rect, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Block, BorderType, Borders}, +}; +use tui_tree_widget::{Tree, TreeItem}; + +use crate::{app::App, components::shared, models::Task, state::PanelState}; + +pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { + let is_active = + app.state.active_modal.is_none() && app.state.active_panel == PanelState::ActiveTasks; + let mut items: Vec> = Vec::new(); + + let border_color = if is_active { + Color::Green + } else { + Color::White + }; + + for workspace in app.workspaces.iter().filter(|w| !w.archived) { + let mut tasks_in_workspace: Vec = app + .tasks + .iter() + .filter(|t| t.workspace_id == Some(workspace.id) && !t.archived) + .cloned() + .collect(); + Task::sort_by_priority(&mut tasks_in_workspace); + + let workspace_tasks: Vec> = tasks_in_workspace + .iter() + .map(|task| { + let is_selected = app.selected_tasks.contains(&task.id); + let mut title_style = if is_selected { + Style::default().fg(Color::LightGreen) + } else { + Style::default() + }; + + if task.completed { + title_style = title_style.add_modifier(Modifier::CROSSED_OUT); + } + + let line = match &task.priority { + Some(p) => Line::from(vec![ + Span::styled(format!("{} ", p.label()), Style::default().fg(p.color())), + Span::styled(task.title.clone(), title_style), + ]), + None => Line::from(Span::styled(task.title.clone(), title_style)), + }; + TreeItem::new_leaf(task.id.to_string(), line) + }) + .collect(); + + let workspace_task_ids: Vec<_> = tasks_in_workspace.iter().map(|t| t.id).collect(); + let all_selected = + !workspace_task_ids.is_empty() && workspace_task_ids.iter().all(|id| app.selected_tasks.contains(id)); + + let workspace_title = if all_selected { + Line::from(Span::styled( + format!("{} ({})", workspace.title.clone(), workspace_tasks.len()), + Style::default().fg(Color::LightGreen), + )) + } else { + Line::from(format!("{} ({})", workspace.title.clone(), workspace_tasks.len())) + }; + + let workspace_item = TreeItem::new(workspace.id.to_string(), workspace_title, workspace_tasks).unwrap(); + + items.push(workspace_item); + } + + let mut orphan_tasks: Vec = app + .tasks + .iter() + .filter(|t| t.workspace_id.is_none() && !t.archived) + .cloned() + .collect(); + Task::sort_by_priority(&mut orphan_tasks); + + for task in &orphan_tasks { + let is_selected = app.selected_tasks.contains(&task.id); + let mut title_style = if is_selected { + Style::default().fg(Color::LightGreen) + } else { + Style::default() + }; + + if task.completed { + title_style = title_style.add_modifier(Modifier::CROSSED_OUT); + } + + let line = match &task.priority { + Some(p) => Line::from(vec![ + Span::styled(format!("{} ", p.label()), Style::default().fg(p.color())), + Span::styled(task.title.clone(), title_style), + ]), + None => Line::from(Span::styled(task.title.clone(), title_style)), + }; + items.push(TreeItem::new_leaf(task.id.to_string(), line)); + } + + let tree = Tree::new(&items) + .expect("identifiers are unique") + .block( + Block::new() + .title(" Workspaces ") + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(border_color), + ) + .highlight_style(if is_active { + Style::default() + .bg(Color::Blue) + .add_modifier(Modifier::BOLD) + } else { + Style::default() + }) + .node_closed_symbol("▶ ") + .node_open_symbol("▼ ") + .node_no_children_symbol(" "); + + frame.render_stateful_widget(tree, area, &mut app.state.workspaces_tree_state); + shared::scrollbar::render( + frame, + area, + app.workspaces.len(), + app.state.active_tasks_state.offset(), + ); +} diff --git a/src/components/workspace/sidebar/mod.rs b/src/components/workspace/sidebar/mod.rs index 2665ac1..cc37d04 100644 --- a/src/components/workspace/sidebar/mod.rs +++ b/src/components/workspace/sidebar/mod.rs @@ -1,4 +1,5 @@ pub mod about; +pub mod active_workspaces; pub mod active_tasks; pub mod archived_tasks; @@ -32,6 +33,7 @@ pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { Task::sort_by_archived_date(&mut archived_tasks); sidebar::about::render(frame, sidebar[0], app); - sidebar::active_tasks::render(frame, sidebar[1], app, active_tasks); + // sidebar::active_tasks::render(frame, sidebar[1], app, active_tasks); + sidebar::active_workspaces::render(frame, sidebar[1], app); sidebar::archived_tasks::render(frame, sidebar[2], app, archived_tasks); } diff --git a/src/db/connection.rs b/src/db/connection.rs index b0fb5a0..ed60491 100644 --- a/src/db/connection.rs +++ b/src/db/connection.rs @@ -16,7 +16,7 @@ impl Db { fs::create_dir(&data_dir).expect("Couldn't create your data directory") } - let db_path = data_dir.join("tasks.db"); + let db_path = data_dir.join("lazytasks.db"); let connection = Connection::open(&db_path).expect("Couldn't open database"); @@ -28,8 +28,12 @@ impl Db { } fn init_schema(&self) -> Result<(), rusqlite::Error> { - let schema = include_str!("schema/tasks.sql"); + let workspaces_schema = include_str!("schema/workspaces.sql"); + self.connection.execute_batch(workspaces_schema)?; - self.connection.execute_batch(schema) + let tasks_schema = include_str!("schema/tasks.sql"); + self.connection.execute_batch(tasks_schema)?; + + Ok(()) } } diff --git a/src/db/repositories/mod.rs b/src/db/repositories/mod.rs index 9c16939..a2c8534 100644 --- a/src/db/repositories/mod.rs +++ b/src/db/repositories/mod.rs @@ -1,3 +1,5 @@ pub mod task; +pub mod workspace; pub use task::TaskRepository; +pub use workspace::WorkspaceRepository; diff --git a/src/db/repositories/task.rs b/src/db/repositories/task.rs index 98d2ff8..35e00ea 100644 --- a/src/db/repositories/task.rs +++ b/src/db/repositories/task.rs @@ -10,12 +10,18 @@ pub struct TaskRepository; impl TaskRepository { pub fn create(connection: &Connection, task: &Task) -> Result<(), rusqlite::Error> { + let workspace_id = match &task.workspace_id { + Some(v) => Some(Uuid::to_string(v)), + None => None, + }; + connection.execute( - "INSERT INTO tasks (id, title, created_at) VALUES (?1, ?2, ?3) ", + "INSERT INTO tasks (id, title, created_at, workspace_id) VALUES (?1, ?2, ?3, ?4) ", ( task.id.to_string(), &task.title, task.created_at.to_rfc3339(), + workspace_id, ), )?; @@ -74,6 +80,7 @@ impl TaskRepository { fn parse_row(row: &Row) -> Result> { let id: String = row.get("id")?; + let workspace_id: Option = row.get("workspace_id")?; let priority: Option = row.get("priority")?; let created_at: String = row.get("created_at")?; let updated_at: Option = row.get("updated_at")?; @@ -93,6 +100,7 @@ impl TaskRepository { archived_at: archived_at .map(|s| DateTime::parse_from_rfc3339(&s).map(|d| d.with_timezone(&Utc))) .transpose()?, + workspace_id: workspace_id.map(|s| Uuid::parse_str(&s)).transpose()?, }) } } diff --git a/src/db/repositories/workspace.rs b/src/db/repositories/workspace.rs new file mode 100644 index 0000000..499aa71 --- /dev/null +++ b/src/db/repositories/workspace.rs @@ -0,0 +1,93 @@ +use std::error::Error; + +use chrono::{DateTime, Utc}; +use rusqlite::{Connection, Row}; +use uuid::Uuid; + +use crate::models::Workspace; + +pub struct WorkspaceRepository; + +impl WorkspaceRepository { + pub fn create(connection: &Connection, workspace: &Workspace) -> Result<(), rusqlite::Error> { + connection.execute( + "INSERT INTO workspaces (id, title, created_at) VALUES (?1, ?2, ?3)", + ( + workspace.id.to_string(), + &workspace.title, + workspace.created_at.to_rfc3339(), + ), + )?; + + Ok(()) + } + + pub fn get_all(connection: &Connection) -> Result, Box> { + let mut stmt = connection.prepare("SELECT * from workspaces")?; + let mut rows = stmt.query([])?; + + let mut workspaces: Vec = Vec::new(); + while let Some(row) = rows.next()? { + workspaces.push(Self::parse_row(row)?); + } + + Ok(workspaces) + } + + fn parse_row(row: &Row) -> Result> { + let id: String = row.get("id")?; + let created_at: String = row.get("created_at")?; + let updated_at: Option = row.get("updated_at")?; + let archived_at: Option = row.get("archived_at")?; + + Ok(Workspace { + id: Uuid::parse_str(&id)?, + title: row.get("title")?, + archived: row.get::<_, i32>("archived")? != 0, + created_at: DateTime::parse_from_rfc3339(&created_at)?.with_timezone(&Utc), + updated_at: updated_at + .map(|s| DateTime::parse_from_rfc3339(&s).map(|d| d.with_timezone(&Utc))) + .transpose()?, + archived_at: archived_at + .map(|s| DateTime::parse_from_rfc3339(&s).map(|d| d.with_timezone(&Utc))) + .transpose()?, + }) + } + + pub fn update(connection: &Connection, workspace: &Workspace) -> Result<(), rusqlite::Error> { + connection.execute( + "UPDATE workspaces SET + title = ?2, + archived = ?3, + updated_at = ?4, + archived_at = ?5 + WHERE id = ?1", + ( + workspace.id.to_string(), + &workspace.title, + workspace.archived as u32, + workspace.updated_at.map(|d| d.to_rfc3339()), + workspace.archived_at.map(|d| d.to_rfc3339()), + ), + )?; + + Ok(()) + } + + pub fn delete(connection: &Connection, workspace_id: &Uuid) -> Result<(), rusqlite::Error> { + let tx = connection.unchecked_transaction()?; + + tx.execute( + "DELETE FROM tasks WHERE workspace_id = ?1", + [workspace_id.to_string()], + )?; + tx.execute( + "DELETE FROM workspaces WHERE id = ?1", + [workspace_id.to_string()], + )?; + + tx.commit()?; + + Ok(()) + } +} diff --git a/src/db/schema/tasks.sql b/src/db/schema/tasks.sql index 2eceafe..f61d1d1 100644 --- a/src/db/schema/tasks.sql +++ b/src/db/schema/tasks.sql @@ -7,6 +7,8 @@ CREATE TABLE IF NOT EXISTS tasks ( priority TEXT, created_at TEXT NOT NULL, updated_at TEXT, - archived_at TEXT -); + archived_at TEXT, + workspace_id TEXT, + FOREIGN KEY(workspace_id) REFERENCES workspaces(id) +); diff --git a/src/db/schema/workspaces.sql b/src/db/schema/workspaces.sql new file mode 100644 index 0000000..6a4ea94 --- /dev/null +++ b/src/db/schema/workspaces.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS workspaces ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + archived INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL, + updated_at TEXT, + archived_at TEXT +); diff --git a/src/keybindings.rs b/src/keybindings.rs index 62cb7e0..3b5aeff 100644 --- a/src/keybindings.rs +++ b/src/keybindings.rs @@ -2,20 +2,23 @@ use ratatui::crossterm::{self, event::Event}; use ratatui::DefaultTerminal; use tui_input::backend::crossterm::EventHandler; +use tui_tree_widget::TreeState; use crate::actions; +use crate::state::PanelState; use crate::{app::App, state::ModalState}; pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerminal) { if let crossterm::event::Event::Key(key) = event { match &mut app.state.active_modal { - Some(ModalState::CreateTask { input }) => match key.code { + Some(ModalState::CreateTask { input, workspace_id }) => match key.code { crossterm::event::KeyCode::Esc => actions::close_modal(app), crossterm::event::KeyCode::Enter => { let title = input.value().trim().to_owned(); + let workspace_id = workspace_id.clone(); if !title.is_empty() { - actions::create_task(app, title); + actions::create_task(app, title, workspace_id); } actions::close_modal(app); } @@ -97,20 +100,137 @@ pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerm } _ => {} }, + Some(ModalState::CreateWorkspace { input }) => match key.code { + crossterm::event::KeyCode::Esc => actions::close_modal(app), + crossterm::event::KeyCode::Enter => { + let title = input.value().trim().to_owned(); + + if !title.is_empty() { + actions::create_workspace(app, title); + } + actions::close_modal(app); + } + _ => { + input.handle_event(event); + } + }, + Some(ModalState::DeleteWorkspace { + workspace_id, + selected_option, + }) => match key.code { + crossterm::event::KeyCode::Esc => actions::close_modal(app), + crossterm::event::KeyCode::Enter => { + let option_idx = selected_option.selected(); + let workspace_id = *workspace_id; + + actions::delete_workspace(app, option_idx, workspace_id); + actions::close_modal(app); + } + crossterm::event::KeyCode::Char('j') => { + selected_option.select_next(); + } + crossterm::event::KeyCode::Char('k') => { + selected_option.select_previous(); + } + _ => {} + }, + Some(ModalState::ArchiveWorkspace { + workspace_id, + selected_option, + is_archived: _, + }) => match key.code { + crossterm::event::KeyCode::Esc => actions::close_modal(app), + crossterm::event::KeyCode::Enter => { + let option_idx = selected_option.selected(); + let workspace_id = *workspace_id; + + actions::archive_workspace(app, option_idx, workspace_id); + actions::close_modal(app); + } + crossterm::event::KeyCode::Char('j') => { + selected_option.select_next(); + } + crossterm::event::KeyCode::Char('k') => { + selected_option.select_previous(); + } + _ => {} + }, + Some(ModalState::MoveTask { + task_id, + selected_option, + }) => match key.code { + crossterm::event::KeyCode::Esc => actions::close_modal(app), + crossterm::event::KeyCode::Enter => { + let option_idx = selected_option.selected(); + let task_id = *task_id; + let workspaces: Vec<_> = app.workspaces.iter().filter(|s| !s.archived).cloned().collect(); + + actions::move_task(app, option_idx, task_id, &workspaces); + actions::close_modal(app); + } + crossterm::event::KeyCode::Char('j') => { + selected_option.select_next(); + } + crossterm::event::KeyCode::Char('k') => { + selected_option.select_previous(); + } + _ => {} + }, None => match key.code { - crossterm::event::KeyCode::Char('a') => actions::open_archive_modal(app), - crossterm::event::KeyCode::Char('c') => actions::open_create_modal(app), + crossterm::event::KeyCode::Char('a') => { + if app.state.active_panel == PanelState::ActiveTasks { + actions::open_archive_workspace_modal(app); + } + if app.state.active_modal.is_none() { + actions::open_archive_modal(app); + } + } + crossterm::event::KeyCode::Char('c') => actions::open_create_task_modal(app), + crossterm::event::KeyCode::Char('C') => app.state.open_create_task(None), + crossterm::event::KeyCode::Char('w') => actions::open_create_workspace_modal(app), crossterm::event::KeyCode::Char('e') => actions::open_edit_title_modal(app), crossterm::event::KeyCode::Char('p') => actions::open_priority_modal(app), crossterm::event::KeyCode::Char('E') => actions::edit_task(app, terminal), crossterm::event::KeyCode::Char('y') => actions::toggle_task_completion(app), crossterm::event::KeyCode::Char('q') => actions::quit(app), - crossterm::event::KeyCode::Char('d') => actions::open_delete_modal(app), - crossterm::event::KeyCode::Char('j') => actions::select_next_task(app), - crossterm::event::KeyCode::Char('k') => actions::select_previous_task(app), + crossterm::event::KeyCode::Char('m') => actions::open_move_task_modal(app), + crossterm::event::KeyCode::Char('d') => { + if app.state.active_panel == PanelState::ActiveTasks { + actions::open_delete_workspace_modal(app); + } + if app.state.active_modal.is_none() { + actions::open_delete_modal(app); + } + } + crossterm::event::KeyCode::Char('j') => { + match app.state.active_panel { + PanelState::ActiveTasks => { + TreeState::key_down(&mut app.state.workspaces_tree_state); + } + _ => actions::select_next_task(app), + }; + } + crossterm::event::KeyCode::Char('k') => { + match app.state.active_panel { + PanelState::ActiveTasks => { + TreeState::key_up(&mut app.state.workspaces_tree_state); + } + _ => actions::select_previous_task(app), + }; + } crossterm::event::KeyCode::Char(' ') => actions::toggle_task_selection(app), crossterm::event::KeyCode::Tab => actions::switch_panel(app), - crossterm::event::KeyCode::Enter => actions::clean_err_msg(app), + crossterm::event::KeyCode::Enter => match app.state.active_panel { + PanelState::ActiveTasks => { + if app.error.is_some() { + actions::clean_err_msg(app); + } else { + let selected = app.state.workspaces_tree_state.selected().to_vec(); + TreeState::toggle(&mut app.state.workspaces_tree_state, selected); + } + } + _ => actions::clean_err_msg(app), + }, _ => {} }, } diff --git a/src/models/mod.rs b/src/models/mod.rs index 9eb572f..5343479 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,5 +1,7 @@ pub mod priority; pub mod task; +pub mod workspace; pub use priority::Priority; pub use task::Task; +pub use workspace::Workspace; diff --git a/src/models/task.rs b/src/models/task.rs index 6072da8..0cdf25c 100644 --- a/src/models/task.rs +++ b/src/models/task.rs @@ -14,10 +14,11 @@ pub struct Task { pub created_at: DateTime, pub updated_at: Option>, pub archived_at: Option>, + pub workspace_id: Option, } impl Task { - pub fn new(title: impl Into) -> Self { + pub fn new(title: impl Into, workspace_id: Option) -> Self { Task { id: Uuid::new_v4(), title: title.into(), @@ -28,6 +29,7 @@ impl Task { created_at: Utc::now(), updated_at: None, archived_at: None, + workspace_id, } } diff --git a/src/models/workspace.rs b/src/models/workspace.rs new file mode 100644 index 0000000..d3064d5 --- /dev/null +++ b/src/models/workspace.rs @@ -0,0 +1,25 @@ +use chrono::{DateTime, Utc}; +use uuid::Uuid; + +#[derive(Clone)] +pub struct Workspace { + pub id: Uuid, + pub title: String, + pub archived: bool, + pub created_at: DateTime, + pub updated_at: Option>, + pub archived_at: Option>, +} + +impl Workspace { + pub fn new(title: impl Into) -> Self { + Workspace { + id: Uuid::new_v4(), + title: title.into(), + archived: false, + created_at: Utc::now(), + updated_at: None, + archived_at: None, + } + } +} diff --git a/src/state.rs b/src/state.rs index 2396ee7..c3bbdbd 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,5 +1,6 @@ use ratatui::widgets::ListState; use tui_input::Input; +use tui_tree_widget::TreeState; use uuid::Uuid; /// The application global state @@ -13,6 +14,9 @@ pub struct AppState { /// State of the focus pane pub active_panel: PanelState, + /// State of the workspaces tree (identifier = UUID as String) + pub workspaces_tree_state: TreeState, + /// State of the current active modal (CreateTask, EditTask, ArchivedTask, DeleteTask) pub active_modal: Option, } @@ -27,6 +31,7 @@ pub enum PanelState { pub enum ModalState { CreateTask { input: Input, + workspace_id: Option, }, EditTask { task_id: Uuid, @@ -45,6 +50,22 @@ pub enum ModalState { task_ids: Vec, selected_option: ListState, }, + CreateWorkspace { + input: Input, + }, + DeleteWorkspace { + workspace_id: Uuid, + selected_option: ListState, + }, + ArchiveWorkspace { + workspace_id: Uuid, + selected_option: ListState, + is_archived: bool, + }, + MoveTask { + task_id: Uuid, + selected_option: ListState, + }, } impl AppState { @@ -55,11 +76,15 @@ impl AppState { let mut archived_tasks_state = ListState::default(); archived_tasks_state.select(Some(0)); + let mut workspaces_tree_state = TreeState::default(); + workspaces_tree_state.select_first(); + AppState { active_tasks_state, archived_tasks_state, active_panel: PanelState::ActiveTasks, active_modal: None, + workspaces_tree_state, } } @@ -97,9 +122,10 @@ impl AppState { } } - pub fn open_create_task(&mut self) { + pub fn open_create_task(&mut self, workspace_id: Option) { self.active_modal = Some(ModalState::CreateTask { input: Input::default(), + workspace_id, }) } @@ -146,7 +172,41 @@ impl AppState { } } + pub fn open_create_workspace(&mut self) { + self.active_modal = Some(ModalState::CreateWorkspace { + input: Input::default(), + }) + } + pub fn close_modal(&mut self) { self.active_modal = None } + + pub fn open_delete_workspace(&mut self, workspace_id: Uuid) { + let mut option_list_state = ListState::default(); + option_list_state.select(Some(0)); + self.active_modal = Some(ModalState::DeleteWorkspace { + workspace_id, + selected_option: option_list_state, + }) + } + + pub fn open_archive_workspace(&mut self, workspace_id: Uuid, is_archived: bool) { + let mut option_list_state = ListState::default(); + option_list_state.select(Some(0)); + self.active_modal = Some(ModalState::ArchiveWorkspace { + workspace_id, + selected_option: option_list_state, + is_archived, + }) + } + + pub fn open_move_task(&mut self, task_id: Uuid) { + let mut option_list_state = ListState::default(); + option_list_state.select(Some(0)); + self.active_modal = Some(ModalState::MoveTask { + task_id, + selected_option: option_list_state, + }) + } }