diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 6ad79c0c..4836e3a8 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -110,6 +110,19 @@ jobs: toolchain: stable override: true + # - name: Build + # uses: actions-rs/cargo@v1 + # with: + # command: build + # args: --release + + # - name: Publish + # uses: actions-rs/cargo@v1 + # with: + # command: publish + # env: + # CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + homebrew: name: Bump Homebrew formula runs-on: macos-latest diff --git a/.rustfmt.toml b/.rustfmt.toml index 75306517..d870fddb 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1 +1,2 @@ -max_width = 120 +max_width = 150 +tab_spaces = 2 diff --git a/Cargo.lock b/Cargo.lock index aa5d7cbf..0a3ea060 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.17.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" dependencies = [ "gimli", ] @@ -19,45 +19,83 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "0.7.18" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" dependencies = [ "memchr", ] [[package]] name = "android_system_properties" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] -name = "anyhow" -version = "1.0.62" +name = "anstream" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1485d4d2cc45e7b201ee3767015c96faa5904387c9d87c6efdd0fb511f12d305" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] [[package]] -name = "arc-swap" -version = "1.5.1" +name = "anstyle" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "983cd8b9d4b02a6dc6ffa557262eb5858a27a0038ffffe21a0f133eaa819a164" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" [[package]] -name = "atty" -version = "0.2.14" +name = "anstyle-parse" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" dependencies = [ - "hermit-abi", - "libc", - "winapi", + "utf8parse", ] +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + [[package]] name = "autocfg" version = "1.1.0" @@ -66,9 +104,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.66" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" dependencies = [ "addr2line", "cc", @@ -97,15 +135,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bumpalo" -version = "3.11.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "bytes" -version = "1.2.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cassowary" @@ -115,9 +153,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.73" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" [[package]] name = "cfg-if" @@ -127,9 +165,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.22" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" dependencies = [ "iana-time-zone", "js-sys", @@ -142,87 +180,95 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.17" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29e724a68d9319343bb3328c9cc2dfde263f4b3142ee1059a9980580171c954b" +checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc" dependencies = [ - "atty", - "bitflags", + "clap_builder", "clap_derive", - "clap_lex", - "indexmap", "once_cell", - "strsim 0.10.0", - "termcolor", - "textwrap", +] + +[[package]] +name = "clap_builder" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990" +dependencies = [ + "anstream", + "anstyle", + "bitflags", + "clap_lex", + "strsim", ] [[package]] name = "clap_complete" -version = "3.2.4" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4179da71abd56c26b54dd0c248cc081c1f43b0a1a7e8448e28e57a29baa993d" +checksum = "a04ddfaacc3bc9e6ea67d024575fafc2a813027cf374b8f24f7bc233c6b6be12" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "3.2.17" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13547f7012c01ab4a0e8f8967730ada8f9fdf419e8b6c792788f39cf4e46eefa" +checksum = "191d9573962933b4027f932c600cd252ce27a8ad5979418fe78e43c07996f27b" dependencies = [ "heck", - "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 2.0.18", ] [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[package]] name = "clipboard-win" -version = "4.4.2" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4ab1b92798304eedc095b53942963240037c0516452cb11aeba709d420b2219" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" dependencies = [ "error-code", "str-buf", "winapi", ] +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "console" -version = "0.15.1" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" dependencies = [ "encode_unicode", + "lazy_static", "libc", - "once_cell", - "terminal_size", - "winapi", + "windows-sys 0.45.0", ] [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "crossterm" -version = "0.25.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" dependencies = [ "bitflags", "crossterm_winapi", @@ -246,9 +292,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.10.2" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" dependencies = [ "darling_core", "darling_macro", @@ -256,27 +302,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.10.2" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.9.3", - "syn", + "strsim", + "syn 1.0.109", ] [[package]] name = "darling_macro" -version = "0.10.2" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ "darling_core", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -287,39 +333,51 @@ checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "derive_builder" -version = "0.9.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" dependencies = [ - "darling", - "derive_builder_core", - "proc-macro2", - "quote", - "syn", + "derive_builder_macro", ] [[package]] name = "derive_builder_core" -version = "0.9.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] +[[package]] +name = "derive_builder_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + +[[package]] +name = "destructure_traitobject" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" + [[package]] name = "dirs" -version = "4.0.0" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ "dirs-sys", ] @@ -336,13 +394,14 @@ dependencies = [ [[package]] name = "dirs-sys" -version = "0.3.7" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", + "option-ext", "redox_users", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -358,9 +417,9 @@ dependencies = [ [[package]] name = "either" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" [[package]] name = "encode_unicode" @@ -376,13 +435,13 @@ checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" [[package]] name = "errno" -version = "0.2.8" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -405,37 +464,15 @@ dependencies = [ "str-buf", ] -[[package]] -name = "failure" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" -dependencies = [ - "backtrace", - "failure_derive", -] - -[[package]] -name = "failure_derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "fd-lock" -version = "3.0.6" +version = "3.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11dcc7e4d79a8c89b9ab4c6f5c30b1fc4a83c420792da3542fd31179ed5f517" +checksum = "39ae6b3d9530211fb3b12a95374b8b0823be812f53d09e18c5675c0146b09642" dependencies = [ "cfg-if", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -446,9 +483,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "futures" -version = "0.3.23" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab30e97ab6aacfe635fad58f22c2bb06c8b685f7421eb1e064a729e2a5f481fa" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", @@ -461,9 +498,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.23" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bfc52cbddcfd745bf1740338492bb0bd83d76c67b445f91c5fb29fae29ecaa1" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", "futures-sink", @@ -471,15 +508,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.23" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2acedae88d38235936c3922476b10fced7b2b68136f5e3c03c2d5be348a1115" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" -version = "0.3.23" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d11aa21b5b587a64682c0094c2bdd4df0076c5324961a40cc3abd7f37930528" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", @@ -488,38 +525,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.23" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93a66fc6d035a26a3ae255a6d2bca35eda63ae4c5512bef54449113f7a1228e5" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-macro" -version = "0.3.23" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0db9cce532b0eae2ccf2766ab246f114b56b9cf6d445e00c2549fbc100ca045d" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.18", ] [[package]] name = "futures-sink" -version = "0.3.23" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca0bae1fe9752cf7fd9b0064c674ae63f97b37bc714d745cbde0afb7ec4e6765" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" -version = "0.3.23" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "842fc63b931f4056a24d59de13fb1272134ce261816e063e634ad0c15cdc5306" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.23" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0828a5471e340229c11c77ca80017937ce3c58cb788a17e5f1c2d5c485a9577" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-channel", "futures-core", @@ -535,9 +572,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if", "libc", @@ -546,9 +583,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.26.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" [[package]] name = "hashbrown" @@ -558,19 +595,25 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "heck" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "humantime" version = "2.1.0" @@ -579,15 +622,25 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "iana-time-zone" -version = "0.1.46" +version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501" +checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" dependencies = [ "android_system_properties", "core-foundation-sys", + "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "winapi", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", ] [[package]] @@ -598,9 +651,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indexmap" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown", @@ -608,30 +661,47 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "0.7.3" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "is-terminal" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ea37f355c05dde75b84bba2d767906ad522e97cd9e2eef2be7a4ab7fb442c06" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.48.0", +] [[package]] name = "itertools" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.3" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "js-sys" -version = "0.3.59" +version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" dependencies = [ "wasm-bindgen", ] @@ -644,9 +714,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.132" +version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "linked-hash-map" @@ -656,15 +726,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.0.46" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "lock_api" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ "autocfg", "scopeguard", @@ -688,9 +758,9 @@ checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" [[package]] name = "log4rs" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "893eaf59f4bef8e2e94302adf56385db445a0306b9823582b0b8d5a06d8822f3" +checksum = "d36ca1786d9e79b8193a68d480a0907b612f109537115c6ff655a3a1967533fd" dependencies = [ "anyhow", "arc-swap", @@ -708,7 +778,7 @@ dependencies = [ "serde_yaml", "thiserror", "thread-id", - "typemap", + "typemap-ors", "winapi", ] @@ -726,23 +796,23 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.5.3" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] @@ -756,20 +826,21 @@ dependencies = [ [[package]] name = "nix" -version = "0.24.2" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ "bitflags", "cfg-if", "libc", + "static_assertions", ] [[package]] name = "nom" -version = "7.1.1" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", @@ -796,28 +867,34 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi", + "hermit-abi 0.2.6", "libc", ] [[package]] name = "object" -version = "0.29.0" +version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.13.1" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "option-ext" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ordered-float" @@ -828,12 +905,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "os_str_bytes" -version = "6.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" - [[package]] name = "parking_lot" version = "0.12.1" @@ -846,22 +917,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.3" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] name = "path-clean" -version = "0.1.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecba01bf2678719532c5e3059e0b5f0811273d94b397088b82e3bd0a78c78fdd" +checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" [[package]] name = "pin-project-lite" @@ -877,48 +948,24 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "ppv-lite86" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" - -[[package]] -name = "proc-macro-error" -version = "1.0.4" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.21" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] @@ -956,13 +1003,26 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] +[[package]] +name = "ratatui" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcc0d032bccba900ee32151ec0265667535c230169f5a011154cdcd984e16829" +dependencies = [ + "bitflags", + "cassowary", + "crossterm", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -985,9 +1045,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.6.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" dependencies = [ "aho-corasick", "memchr", @@ -996,35 +1056,35 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "rustc-demangle" -version = "0.1.21" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.35.9" +version = "0.37.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c825b8aa8010eb9ee99b75f05e10180b9278d161583034d7574c9d617aeada" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "rustyline" -version = "10.0.0" +version = "11.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1cd5ae51d3f7bf65d7969d579d502168ef578f289452bd8ccc91de28fda20e" +checksum = "5dfc8644681285d1fb67a467fb3021bfea306b99b4146b166a1fe3ada965eece" dependencies = [ "bitflags", "cfg-if", @@ -1036,6 +1096,7 @@ dependencies = [ "memchr", "nix", "radix_trie", + "rustyline-derive", "scopeguard", "unicode-segmentation", "unicode-width", @@ -1043,11 +1104,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "rustyline-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8218eaf5d960e3c478a1b0f129fa888dd3d8d22eb3de097e9af14c1ab4438024" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "ryu" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "scopeguard" @@ -1057,9 +1129,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.144" +version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" dependencies = [ "serde_derive", ] @@ -1076,20 +1148,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.144" +version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.18", ] [[package]] name = "serde_json" -version = "1.0.85" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ "itoa", "ryu", @@ -1110,9 +1182,9 @@ dependencies = [ [[package]] name = "shellexpand" -version = "2.1.2" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" +checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" dependencies = [ "dirs", ] @@ -1125,9 +1197,9 @@ checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" [[package]] name = "signal-hook" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" dependencies = [ "libc", "signal-hook-registry", @@ -1146,49 +1218,49 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] [[package]] name = "slab" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" dependencies = [ "autocfg", ] [[package]] name = "smallvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "socket2" -version = "0.4.4" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi", ] [[package]] -name = "str-buf" -version = "1.0.6" +name = "static_assertions" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] -name = "strsim" -version = "0.9.3" +name = "str-buf" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" [[package]] name = "strsim" @@ -1198,9 +1270,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.99" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -1208,29 +1280,27 @@ dependencies = [ ] [[package]] -name = "synstructure" -version = "0.12.6" +name = "syn" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" dependencies = [ "proc-macro2", "quote", - "syn", - "unicode-xid", + "unicode-ident", ] [[package]] name = "task-hookrs" -version = "0.7.0" -source = "git+https://github.com/kdheepak/task-hookrs#6f04ee63c0d58bb0fe9bd6563457df52b5b5f84d" +version = "0.8.0" +source = "git+https://github.com/kdheepak/task-hookrs#9fe7bd42f96a14571009b5de3ef395cd26988bbe" dependencies = [ "chrono", "derive_builder", - "failure", "log", "serde", - "serde_derive", "serde_json", + "thiserror", "uuid", ] @@ -1253,6 +1323,7 @@ dependencies = [ "log4rs", "path-clean", "rand", + "ratatui", "regex", "rustyline", "serde", @@ -1262,7 +1333,6 @@ dependencies = [ "task-hookrs", "tokio", "tokio-stream", - "tui", "unicode-segmentation", "unicode-truncate", "unicode-width", @@ -1270,56 +1340,31 @@ dependencies = [ "versions", ] -[[package]] -name = "termcolor" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "terminal_size" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "textwrap" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" - [[package]] name = "thiserror" -version = "1.0.32" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.32" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.18", ] [[package]] name = "thread-id" -version = "4.0.0" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fdfe0627923f7411a43ec9ec9c39c3a9b4151be313e0922042581fb6c9b717f" +checksum = "3ee93aa2b8331c0fec9091548843f2c90019571814057da3b783f9de09349d73" dependencies = [ "libc", "redox_syscall", @@ -1328,9 +1373,9 @@ dependencies = [ [[package]] name = "time" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" dependencies = [ "libc", "wasi 0.10.0+wasi-snapshot-preview1", @@ -1339,41 +1384,39 @@ dependencies = [ [[package]] name = "tokio" -version = "1.20.1" +version = "1.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" +checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" dependencies = [ "autocfg", "bytes", "libc", - "memchr", "mio", "num_cpus", - "once_cell", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "winapi", + "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "1.8.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.18", ] [[package]] name = "tokio-stream" -version = "0.1.9" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite", @@ -1381,44 +1424,25 @@ dependencies = [ ] [[package]] -name = "traitobject" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" - -[[package]] -name = "tui" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" -dependencies = [ - "bitflags", - "cassowary", - "crossterm", - "unicode-segmentation", - "unicode-width", -] - -[[package]] -name = "typemap" -version = "0.3.3" +name = "typemap-ors" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653be63c80a3296da5551e1bfd2cca35227e13cdd08c6668903ae2f4f77aa1f6" +checksum = "a68c24b707f02dd18f1e4ccceb9d49f2058c2fb86384ef9972592904d7a28867" dependencies = [ - "unsafe-any", + "unsafe-any-ors", ] [[package]] name = "unicode-ident" -version = "1.0.3" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-segmentation" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-truncate" @@ -1431,47 +1455,35 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] -name = "unicode-xid" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" - -[[package]] -name = "unsafe-any" -version = "0.4.2" +name = "unsafe-any-ors" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30360d7979f5e9c6e6cea48af192ea8fab4afb3cf72597154b8f08935bc9c7f" +checksum = "e0a303d30665362d9680d7d91d78b23f5f899504d4f08b3c4cf08d055d87c0ad" dependencies = [ - "traitobject", + "destructure_traitobject", ] [[package]] name = "utf8parse" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "0.8.2" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" dependencies = [ "getrandom", "serde", ] -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - [[package]] name = "versions" version = "4.1.0" @@ -1496,9 +1508,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.82" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1506,24 +1518,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.82" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.18", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.82" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1531,22 +1543,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.82" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.18", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.82" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" +checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" [[package]] name = "winapi" @@ -1565,62 +1577,151 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] -name = "winapi-util" -version = "0.1.5" +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "winapi", + "windows-targets 0.48.0", ] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows-sys" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] [[package]] name = "windows-sys" -version = "0.36.1" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + [[package]] name = "windows_aarch64_msvc" -version = "0.36.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" -version = "0.36.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" -version = "0.36.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" -version = "0.36.1" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.36.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "yaml-rust" diff --git a/Cargo.toml b/Cargo.toml index b47b5309..c2015d06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,37 +17,37 @@ default = ["crossterm-backend"] crossterm-backend = ["tui/crossterm", "crossterm"] [dependencies] -anyhow = "1.0.56" +anyhow = "1.0.71" better-panic = "0.3.0" cassowary = "0.3.0" -chrono = "0.4.19" -clap = { version = "3.1.6", features = ["derive"] } -crossterm = { version = "0.25.0", optional = true, default-features = false, features = [ - "event-stream" +chrono = "0.4.24" +clap = { version = "4.3.0", features = ["derive"] } +crossterm = { version = "0.26.1", optional = true, default-features = false, features = [ + "event-stream", ] } -dirs = "4.0.0" -futures = "0.3.21" -itertools = "0.10.3" +dirs = "5.0.1" +futures = "0.3.28" +itertools = "0.10.5" lazy_static = "1.4.0" -log = "0.4.14" -log4rs = "1.0.0" -path-clean = "0.1.0" +log = "0.4.17" +log4rs = "1.2.0" +path-clean = "1.0.1" rand = "0.8.5" -regex = "1.5.5" -rustyline = "10.0.0" -serde = { version = "1.0.136", features = ["derive"] } -serde_json = "1.0.79" -shellexpand = "2.1.0" +regex = "1.8.3" +rustyline = { version = "11.0.0", features = ["with-file-history", "derive"] } +serde = { version = "1.0.163", features = ["derive"] } +serde_json = "1.0.96" +shellexpand = "3.1.0" shlex = "1.1.0" task-hookrs = { git = "https://github.com/kdheepak/task-hookrs" } -tokio = { version = "1.17.0", features = ["full"] } -tokio-stream = "0.1.3" -tui = { version = "0.19.0", optional = true, default-features = false } -unicode-segmentation = "1.9.0" +tokio = { version = "1.28.1", features = ["full"] } +tokio-stream = "0.1.14" +tui = { package = "ratatui", version = "0.20.1" } +unicode-segmentation = "1.10.1" unicode-truncate = "0.2.0" -unicode-width = "0.1.9" -uuid = { version = "0.8.2", features = ["serde", "v4"] } -versions = "4.0.0" +unicode-width = "0.1.10" +uuid = { version = "1.3.3", features = ["serde", "v4"] } +versions = "4.1.0" [package.metadata.rpm] package = "taskwarrior-tui" @@ -64,6 +64,6 @@ incremental = true lto = "off" [build-dependencies] -clap = { version = "3.1.6", features = ["derive"] } -clap_complete = "3.1.1" +clap = { version = "4.3.0", features = ["derive"] } +clap_complete = "4.3.0" shlex = "1.1.0" diff --git a/README.md b/README.md index a3358922..1f89a325 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,6 @@ A Terminal User Interface (TUI) for [Taskwarrior](https://taskwarrior.org/) that ![](https://user-images.githubusercontent.com/1813121/159858280-3ca31e9a-fc38-4547-a92d-36a7758cf5dc.gif) - ### Showcase
@@ -125,7 +124,11 @@ uda.taskwarrior-tui.report.next.filter=(status:pending or status:waiting) ### References / Resources -If you like `taskwarrior-tui`, please consider donating to [me](https://github.com/sponsors/kdheepak), [`@GothenburgBitFactory`](https://github.com/sponsors/GothenburgBitFactory) or a charity of your choice. +If you like `taskwarrior-tui`, please consider donating to + +- [`kdheepak`](https://github.com/sponsors/kdheepak) +- [`@GothenburgBitFactory`](https://github.com/sponsors/GothenburgBitFactory) +- and/or a charity of your choice.
Additional resources diff --git a/build.rs b/build.rs index a666b3cd..1031588e 100644 --- a/build.rs +++ b/build.rs @@ -2,32 +2,32 @@ use std::process::{Command, Output}; use clap_complete::{ - generate_to, - shells::{Bash, Fish, PowerShell, Zsh}, + generate_to, + shells::{Bash, Fish, PowerShell, Zsh}, }; include!("src/cli.rs"); fn run_pandoc() -> Result { - let mut cmd = Command::new("pandoc"); - if let Some(args) = shlex::split("--standalone --to=man docs/taskwarrior-tui.1.md -o docs/taskwarrior-tui.1") { - for arg in args { - cmd.arg(arg); - } + let mut cmd = Command::new("pandoc"); + if let Some(args) = shlex::split("--standalone --to=man docs/taskwarrior-tui.1.md -o docs/taskwarrior-tui.1") { + for arg in args { + cmd.arg(arg); } - cmd.output() + } + cmd.output() } fn main() { - let mut app = generate_cli_app(); - let name = app.get_name().to_string(); - let outdir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("completions/"); - dbg!(&outdir); - generate_to(Bash, &mut app, &name, &outdir).unwrap(); - generate_to(Zsh, &mut app, &name, &outdir).unwrap(); - generate_to(Fish, &mut app, &name, &outdir).unwrap(); - generate_to(PowerShell, &mut app, &name, &outdir).unwrap(); - if run_pandoc().is_err() { - dbg!("Unable to run pandoc to generate man page documentation"); - } + let mut app = generate_cli_app(); + let name = app.get_name().to_string(); + let outdir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("completions/"); + dbg!(&outdir); + generate_to(Bash, &mut app, &name, &outdir).unwrap(); + generate_to(Zsh, &mut app, &name, &outdir).unwrap(); + generate_to(Fish, &mut app, &name, &outdir).unwrap(); + generate_to(PowerShell, &mut app, &name, &outdir).unwrap(); + if run_pandoc().is_err() { + dbg!("Unable to run pandoc to generate man page documentation"); + } } diff --git a/completions/_taskwarrior-tui b/completions/_taskwarrior-tui index 1cbdf079..b8bd2fa5 100644 --- a/completions/_taskwarrior-tui +++ b/completions/_taskwarrior-tui @@ -23,10 +23,10 @@ _taskwarrior-tui() { '--taskrc=[Sets the .taskrc file using the TASKRC environment variable for taskwarrior]:FILE: ' \ '-r+[Sets default report]:STRING: ' \ '--report=[Sets default report]:STRING: ' \ -'-h[Print help information]' \ -'--help[Print help information]' \ -'-V[Print version information]' \ -'--version[Print version information]' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ && ret=0 } @@ -36,4 +36,8 @@ _taskwarrior-tui_commands() { _describe -t commands 'taskwarrior-tui commands' commands "$@" } -_taskwarrior-tui "$@" +if [ "$funcstack[1]" = "_taskwarrior-tui" ]; then + _taskwarrior-tui "$@" +else + compdef _taskwarrior-tui taskwarrior-tui +fi diff --git a/completions/_taskwarrior-tui.ps1 b/completions/_taskwarrior-tui.ps1 index 56436f5f..97a5e14e 100644 --- a/completions/_taskwarrior-tui.ps1 +++ b/completions/_taskwarrior-tui.ps1 @@ -29,10 +29,10 @@ Register-ArgumentCompleter -Native -CommandName 'taskwarrior-tui' -ScriptBlock { [CompletionResult]::new('--taskrc', 'taskrc', [CompletionResultType]::ParameterName, 'Sets the .taskrc file using the TASKRC environment variable for taskwarrior') [CompletionResult]::new('-r', 'r', [CompletionResultType]::ParameterName, 'Sets default report') [CompletionResult]::new('--report', 'report', [CompletionResultType]::ParameterName, 'Sets default report') - [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information') - [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information') - [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information') - [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information') + [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version') break } }) diff --git a/completions/taskwarrior-tui.bash b/completions/taskwarrior-tui.bash index 06d7ce2b..361c9ca7 100644 --- a/completions/taskwarrior-tui.bash +++ b/completions/taskwarrior-tui.bash @@ -1,5 +1,5 @@ _taskwarrior-tui() { - local i cur prev opts cmds + local i cur prev opts cmd COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" @@ -8,8 +8,8 @@ _taskwarrior-tui() { for i in ${COMP_WORDS[@]} do - case "${i}" in - "$1") + case "${cmd},${i}" in + ",$1") cmd="taskwarrior__tui" ;; *) @@ -19,7 +19,7 @@ _taskwarrior-tui() { case "${cmd}" in taskwarrior__tui) - opts="-h -V -d -c -r --help --version --data --config --taskdata --taskrc --report" + opts="-d -c -r -h -V --data --config --taskdata --taskrc --report --help --version" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/completions/taskwarrior-tui.fish b/completions/taskwarrior-tui.fish index 3348ecd1..7a1ba99e 100644 --- a/completions/taskwarrior-tui.fish +++ b/completions/taskwarrior-tui.fish @@ -3,5 +3,5 @@ complete -c taskwarrior-tui -s c -l config -d 'Sets the config folder for taskwa complete -c taskwarrior-tui -l taskdata -d 'Sets the .task folder using the TASKDATA environment variable for taskwarrior' -r complete -c taskwarrior-tui -l taskrc -d 'Sets the .taskrc file using the TASKRC environment variable for taskwarrior' -r complete -c taskwarrior-tui -s r -l report -d 'Sets default report' -r -complete -c taskwarrior-tui -s h -l help -d 'Print help information' -complete -c taskwarrior-tui -s V -l version -d 'Print version information' +complete -c taskwarrior-tui -s h -l help -d 'Print help' +complete -c taskwarrior-tui -s V -l version -d 'Print version' diff --git a/justfile b/justfile new file mode 100644 index 00000000..f6e26bf3 --- /dev/null +++ b/justfile @@ -0,0 +1,2 @@ +clean: + rm -rf tests/data/.task tests/data/.config diff --git a/src/action.rs b/src/action.rs index 3ffb0a49..01272508 100644 --- a/src/action.rs +++ b/src/action.rs @@ -1,16 +1,16 @@ #[derive(Clone, PartialEq, Eq, Debug, Copy)] pub enum Action { - Report, - Filter, - Add, - Annotate, - Subprocess, - Log, - Modify, - HelpPopup, - ContextMenu, - Jump, - DeletePrompt, - DonePrompt, - Error, + Report, + Filter, + Add, + Annotate, + Subprocess, + Log, + Modify, + HelpPopup, + ContextMenu, + Jump, + DeletePrompt, + DonePrompt, + Error, } diff --git a/src/app.rs b/src/app.rs index 8d53da7f..727bb794 100644 --- a/src/app.rs +++ b/src/app.rs @@ -9,6 +9,7 @@ use crate::scrollbar::Scrollbar; use crate::table::{Row, Table, TableMode, TableState}; use crate::task_report::TaskReportTable; use crate::ui; +use crate::utils; use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; @@ -32,7 +33,7 @@ use unicode_segmentation::Graphemes; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; -use chrono::{Datelike, FixedOffset, Local, NaiveDate, NaiveDateTime, TimeZone, Timelike}; +use chrono::{DateTime, Datelike, FixedOffset, Local, NaiveDate, NaiveDateTime, TimeZone, Timelike}; use anyhow::Context as AnyhowContext; use anyhow::{anyhow, Result}; @@ -43,12 +44,12 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; use tui::{ - backend::Backend, - layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}, - style::{Color, Modifier, Style}, - terminal::Frame, - text::{Span, Spans, Text}, - widgets::{Block, BorderType, Borders, Clear, Gauge, LineGauge, List, ListItem, Paragraph, Wrap}, + backend::Backend, + layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}, + style::{Color, Modifier, Style}, + terminal::Frame, + text::{Span, Spans, Text}, + widgets::{Block, BorderType, Borders, Clear, Gauge, LineGauge, List, ListItem, Paragraph, Wrap}, }; use rustyline::history::SearchDirection as HistoryDirection; @@ -74,9 +75,9 @@ use crate::pane::Pane; use crossterm::style::style; use crossterm::{ - event::{DisableMouseCapture, EnableMouseCapture}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + event::{DisableMouseCapture, EnableMouseCapture}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use futures::SinkExt; @@ -92,4801 +93,4641 @@ use tui::widgets::Tabs; const MAX_LINE: usize = 4096; lazy_static! { - static ref START_TIME: Instant = Instant::now(); - static ref TASKWARRIOR_VERSION_SUPPORTED: Versioning = Versioning::new("2.6.0").unwrap(); + static ref START_TIME: Instant = Instant::now(); + static ref TASKWARRIOR_VERSION_SUPPORTED: Versioning = Versioning::new("2.6.0").unwrap(); } #[derive(Debug)] pub enum DateState { - BeforeToday, - EarlierToday, - LaterToday, - AfterToday, - NotDue, + BeforeToday, + EarlierToday, + LaterToday, + AfterToday, + NotDue, } pub fn get_date_state(reference: &Date, due: usize) -> DateState { - let now = Local::now(); - let reference = TimeZone::from_utc_datetime(now.offset(), reference); - let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc()); - - if reference.date() < now.date() { - return DateState::BeforeToday; - } + let now = Local::now(); + let reference = TimeZone::from_utc_datetime(now.offset(), reference); + let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc()); - if reference.date() == now.date() { - return if reference.time() < now.time() { - DateState::EarlierToday - } else { - DateState::LaterToday - }; - } + if reference.date_naive() < now.date_naive() { + return DateState::BeforeToday; + } - if reference <= now + chrono::Duration::days(7) { - DateState::AfterToday + if reference.date_naive() == now.date_naive() { + return if reference.time() < now.time() { + DateState::EarlierToday } else { - DateState::NotDue - } + DateState::LaterToday + }; + } + + if reference <= now + chrono::Duration::days(7) { + DateState::AfterToday + } else { + DateState::NotDue + } } fn get_offset_hour_minute() -> (&'static str, i32, i32) { - let off = Local::now().offset().local_minus_utc(); - let sym = if off >= 0 { "+" } else { "-" }; - let off = off.abs(); - let h = if off > 60 * 60 { off / 60 / 60 } else { 0 }; - let m = if (off - ((off / 60 / 60) * 60 * 60)) > 60 { - (off - ((off / 60 / 60) * 60 * 60)) / 60 - } else { - 0 - }; - (sym, h, m) + let off = Local::now().offset().local_minus_utc(); + let sym = if off >= 0 { "+" } else { "-" }; + let off = off.abs(); + let h = if off > 60 * 60 { off / 60 / 60 } else { 0 }; + let m = if (off - ((off / 60 / 60) * 60 * 60)) > 60 { + (off - ((off / 60 / 60) * 60 * 60)) / 60 + } else { + 0 + }; + (sym, h, m) } fn get_formatted_datetime(date: &Date) -> String { - let now = Local::now(); - let date = TimeZone::from_utc_datetime(now.offset(), date); - let (sym, h, m) = get_offset_hour_minute(); - format!( - "'{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{:02}:{:02}'", - date.year(), - date.month(), - date.day(), - date.hour(), - date.minute(), - date.second(), - sym, - h, - m, - ) + let now = Local::now(); + let date = TimeZone::from_utc_datetime(now.offset(), date); + let (sym, h, m) = get_offset_hour_minute(); + format!( + "'{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{:02}:{:02}'", + date.year(), + date.month(), + date.day(), + date.hour(), + date.minute(), + date.second(), + sym, + h, + m, + ) } fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { - let popup_layout = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Percentage((100 - percent_y) / 2), - Constraint::Percentage(percent_y), - Constraint::Percentage((100 - percent_y) / 2), - ] - .as_ref(), - ) - .split(r); - - Layout::default() - .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Percentage((100 - percent_x) / 2), - Constraint::Percentage(percent_x), - Constraint::Percentage((100 - percent_x) / 2), - ] - .as_ref(), - ) - .split(popup_layout[1])[1] + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Percentage((100 - percent_y) / 2), + Constraint::Percentage(percent_y), + Constraint::Percentage((100 - percent_y) / 2), + ] + .as_ref(), + ) + .split(r); + + Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ] + .as_ref(), + ) + .split(popup_layout[1])[1] } #[derive(Debug, Clone, PartialEq, Eq)] pub enum Mode { - Tasks(Action), - Projects, - Calendar, + Tasks(Action), + Projects, + Calendar, } pub struct TaskwarriorTui { - pub should_quit: bool, - pub dirty: bool, - pub task_table_state: TableState, - pub current_context_filter: String, - pub current_context: String, - pub command: LineBuffer, - pub filter: LineBuffer, - pub modify: LineBuffer, - pub tasks: Vec, - pub all_tasks: Vec, - pub task_details: HashMap, - pub marked: HashSet, - // stores index of current task that is highlighted - pub current_selection: usize, - pub current_selection_uuid: Option, - pub current_selection_id: Option, - pub task_report_table: TaskReportTable, - pub calendar_year: i32, - pub mode: Mode, - pub previous_mode: Option, - pub config: Config, - pub task_report_show_info: bool, - pub task_report_height: u16, - pub task_details_scroll: u16, - pub help_popup: Help, - pub last_export: Option, - pub keyconfig: KeyConfig, - pub terminal_width: u16, - pub terminal_height: u16, - pub filter_history: HistoryContext, - pub command_history: HistoryContext, - pub history_status: Option, - pub completion_list: CompletionList, - pub show_completion_pane: bool, - pub report: String, - pub projects: ProjectsState, - pub contexts: ContextsState, - pub task_version: Versioning, - pub error: Option, - pub event_loop: crate::event::EventLoop, - pub requires_redraw: bool, + pub should_quit: bool, + pub dirty: bool, + pub task_table_state: TableState, + pub current_context_filter: String, + pub current_context: String, + pub command: LineBuffer, + pub filter: LineBuffer, + pub modify: LineBuffer, + pub tasks: Vec, + pub all_tasks: Vec, + pub task_details: HashMap, + pub marked: HashSet, + // stores index of current task that is highlighted + pub current_selection: usize, + pub current_selection_uuid: Option, + pub current_selection_id: Option, + pub task_report_table: TaskReportTable, + pub calendar_year: i32, + pub mode: Mode, + pub previous_mode: Option, + pub config: Config, + pub task_report_show_info: bool, + pub task_report_height: u16, + pub task_details_scroll: u16, + pub help_popup: Help, + pub last_export: Option, + pub keyconfig: KeyConfig, + pub terminal_width: u16, + pub terminal_height: u16, + pub filter_history: HistoryContext, + pub command_history: HistoryContext, + pub history_status: Option, + pub completion_list: CompletionList, + pub show_completion_pane: bool, + pub report: String, + pub projects: ProjectsState, + pub contexts: ContextsState, + pub task_version: Versioning, + pub error: Option, + pub event_loop: crate::event::EventLoop, + pub requires_redraw: bool, + pub changes: utils::Changeset, } impl TaskwarriorTui { - pub async fn new(report: &str, init_event_loop: bool) -> Result { - let output = std::process::Command::new("task") - .arg("rc.color=off") - .arg("rc._forcecolor=off") - .arg("rc.defaultwidth=0") - .arg("show") - .output() - .context("Unable to run `task show`.")?; - - if !output.status.success() { - let output = std::process::Command::new("task") - .arg("diagnostics") - .output() - .context("Unable to run `task diagnostics`.")?; - return Err(anyhow!( - "Unable to run `task show`.\n{}\n{}\nPlease check your configuration or open a issue on github.", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )); - } + pub async fn new(report: &str, init_event_loop: bool) -> Result { + let output = std::process::Command::new("task") + .arg("rc.color=off") + .arg("rc._forcecolor=off") + .arg("rc.defaultwidth=0") + .arg("show") + .output() + .context("Unable to run `task show`.")?; + + if !output.status.success() { + let output = std::process::Command::new("task") + .arg("diagnostics") + .output() + .context("Unable to run `task diagnostics`.")?; + return Err(anyhow!( + "Unable to run `task show`.\n{}\n{}\nPlease check your configuration or open a issue on github.", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )); + } - let data = String::from_utf8_lossy(&output.stdout); - let c = Config::new(&data, report)?; - let kc = KeyConfig::new(&data)?; + let data = String::from_utf8_lossy(&output.stdout); + let c = Config::new(&data, report)?; + let kc = KeyConfig::new(&data)?; - let output = std::process::Command::new("task") - .arg("--version") - .output() - .context("Unable to run `task --version`")?; + let output = std::process::Command::new("task") + .arg("--version") + .output() + .context("Unable to run `task --version`")?; - let task_version = - Versioning::new(String::from_utf8_lossy(&output.stdout).trim()).context("Unable to get version string")?; + let task_version = Versioning::new(String::from_utf8_lossy(&output.stdout).trim()).context("Unable to get version string")?; - let (w, h) = crossterm::terminal::size().unwrap_or((50, 15)); + let (w, h) = crossterm::terminal::size().unwrap_or((50, 15)); - let tick_rate = if c.uda_tick_rate > 0 { - Some(std::time::Duration::from_millis(c.uda_tick_rate)) - } else { - None - }; - let event_loop = crate::event::EventLoop::new(tick_rate, init_event_loop); - - let mut app = Self { - should_quit: false, - dirty: true, - task_table_state: TableState::default(), - tasks: vec![], - all_tasks: vec![], - task_details: HashMap::new(), - marked: HashSet::new(), - current_selection: 0, - current_selection_uuid: None, - current_selection_id: None, - current_context_filter: "".to_string(), - current_context: "".to_string(), - command: LineBuffer::with_capacity(MAX_LINE), - filter: LineBuffer::with_capacity(MAX_LINE), - modify: LineBuffer::with_capacity(MAX_LINE), - mode: Mode::Tasks(Action::Report), - previous_mode: None, - task_report_height: 0, - task_details_scroll: 0, - task_report_show_info: c.uda_task_report_show_info, - config: c, - task_report_table: TaskReportTable::new(&data, report)?, - calendar_year: Local::today().year(), - help_popup: Help::new(), - last_export: None, - keyconfig: kc, - terminal_width: w, - terminal_height: h, - filter_history: HistoryContext::new("filter.history"), - command_history: HistoryContext::new("command.history"), - history_status: None, - completion_list: CompletionList::with_items(vec![]), - show_completion_pane: false, - report: report.to_string(), - projects: ProjectsState::new(), - contexts: ContextsState::new(), - task_version, - error: None, - event_loop, - requires_redraw: false, - }; + let tick_rate = if c.uda_tick_rate > 0 { + Some(std::time::Duration::from_millis(c.uda_tick_rate)) + } else { + None + }; + let event_loop = crate::event::EventLoop::new(tick_rate, init_event_loop); + + let mut app = Self { + should_quit: false, + dirty: true, + task_table_state: TableState::default(), + tasks: vec![], + all_tasks: vec![], + task_details: HashMap::new(), + marked: HashSet::new(), + current_selection: 0, + current_selection_uuid: None, + current_selection_id: None, + current_context_filter: "".to_string(), + current_context: "".to_string(), + command: LineBuffer::with_capacity(MAX_LINE), + filter: LineBuffer::with_capacity(MAX_LINE), + modify: LineBuffer::with_capacity(MAX_LINE), + mode: Mode::Tasks(Action::Report), + previous_mode: None, + task_report_height: 0, + task_details_scroll: 0, + task_report_show_info: c.uda_task_report_show_info, + config: c, + task_report_table: TaskReportTable::new(&data, report)?, + calendar_year: Local::now().year(), + help_popup: Help::new(), + last_export: None, + keyconfig: kc, + terminal_width: w, + terminal_height: h, + filter_history: HistoryContext::new("filter.history"), + command_history: HistoryContext::new("command.history"), + history_status: None, + completion_list: CompletionList::with_items(vec![]), + show_completion_pane: false, + report: report.to_string(), + projects: ProjectsState::new(), + contexts: ContextsState::new(), + task_version, + error: None, + event_loop, + requires_redraw: false, + changes: utils::Changeset::default(), + }; - for c in app.config.filter.chars() { - app.filter.insert(c, 1); - } + for c in app.config.filter.chars() { + app.filter.insert(c, 1, &mut app.changes); + } - app.task_report_table.date_time_vague_precise = app.config.uda_task_report_date_time_vague_more_precise; + app.task_report_table.date_time_vague_precise = app.config.uda_task_report_date_time_vague_more_precise; - app.update(true).await?; + app.update(true).await?; - app.filter_history.load()?; - app.filter_history.add(app.filter.as_str()); - app.command_history.load()?; - app.task_background(); + app.filter_history.load()?; + app.filter_history.add(app.filter.as_str()); + app.command_history.load()?; + app.task_background(); - if app.task_version < *TASKWARRIOR_VERSION_SUPPORTED { - app.error = Some(format!( - "Found taskwarrior version {} but taskwarrior-tui works with taskwarrior>={}", - app.task_version, *TASKWARRIOR_VERSION_SUPPORTED - )); - app.mode = Mode::Tasks(Action::Error); - } + if app.task_version < *TASKWARRIOR_VERSION_SUPPORTED { + app.error = Some(format!( + "Found taskwarrior version {} but taskwarrior-tui works with taskwarrior>={}", + app.task_version, *TASKWARRIOR_VERSION_SUPPORTED + )); + app.mode = Mode::Tasks(Action::Error); + } - Ok(app) + Ok(app) + } + + pub fn start_tui(&mut self) -> Result>> { + enable_raw_mode()?; + let mut stdout = std::io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + terminal.hide_cursor()?; + Ok(terminal) + } + + pub async fn resume_tui(&mut self) -> Result<()> { + self.resume_event_loop().await?; + let backend = CrosstermBackend::new(io::stdout()); + let mut terminal = Terminal::new(backend)?; + execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?; + enable_raw_mode()?; + self.requires_redraw = true; + terminal.hide_cursor()?; + Ok(()) + } + + pub async fn abort_event_loop(&mut self) -> Result<()> { + self.event_loop.abort.send(())?; + while let Some(event) = self.next().await { + if let Event::Closed = event { + break; + } } + Ok(()) + } - pub fn start_tui(&mut self) -> Result>> { - enable_raw_mode()?; - let mut stdout = std::io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - terminal.hide_cursor()?; - Ok(terminal) + pub async fn resume_event_loop(&mut self) -> Result<()> { + let tick_rate = if self.config.uda_tick_rate > 0 { + Some(std::time::Duration::from_millis(self.config.uda_tick_rate)) + } else { + None + }; + self.event_loop = crate::event::EventLoop::new(tick_rate, true); + Ok(()) + } + + pub async fn pause_tui(&mut self) -> Result<()> { + self.abort_event_loop().await?; + let backend = CrosstermBackend::new(io::stdout()); + let mut terminal = Terminal::new(backend)?; + disable_raw_mode()?; + execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)?; + terminal.show_cursor()?; + Ok(()) + } + + pub async fn next(&mut self) -> Option> { + self.event_loop.rx.recv().await + } + + pub async fn run(&mut self, terminal: &mut Terminal) -> Result<()> { + loop { + if self.requires_redraw { + terminal.resize(terminal.size()?)?; + self.requires_redraw = false; + } + terminal.draw(|f| self.draw(f))?; + // Handle input + if let Some(event) = self.next().await { + match event { + Event::Input(input) => { + debug!("Received input = {:?}", input); + self.handle_input(input).await?; + } + Event::Tick => { + debug!("Tick event"); + self.update(false).await?; + } + Event::Closed => { + debug!("Event loop closed"); + } + } + } + + if self.should_quit { + break; + } + } + Ok(()) + } + + pub fn reset_command(&mut self) { + self.command.update("", 0, &mut self.changes) + } + + pub fn get_context(&mut self) -> Result<()> { + let output = std::process::Command::new("task").arg("_get").arg("rc.context").output()?; + self.current_context = String::from_utf8_lossy(&output.stdout).to_string(); + self.current_context = self.current_context.strip_suffix('\n').unwrap_or("").to_string(); + + // support new format for context + let output = std::process::Command::new("task") + .arg("_get") + .arg(format!("rc.context.{}.read", self.current_context)) + .output()?; + self.current_context_filter = String::from_utf8_lossy(&output.stdout).to_string(); + self.current_context_filter = self.current_context_filter.strip_suffix('\n').unwrap_or("").to_string(); + + // If new format is not used, check if old format is used + if self.current_context_filter.is_empty() { + let output = std::process::Command::new("task") + .arg("_get") + .arg(format!("rc.context.{}", self.current_context)) + .output()?; + self.current_context_filter = String::from_utf8_lossy(&output.stdout).to_string(); + self.current_context_filter = self.current_context_filter.strip_suffix('\n').unwrap_or("").to_string(); } + Ok(()) + } + + pub fn draw(&mut self, f: &mut Frame) { + let rect = f.size(); + self.terminal_width = rect.width; + self.terminal_height = rect.height; + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Min(0)]) + .split(f.size()); + + let tab_layout = chunks[0]; + let main_layout = chunks[1]; + + self.draw_tabs(f, tab_layout); + match self.mode { + Mode::Tasks(action) => self.draw_task(f, main_layout, action), + Mode::Calendar => self.draw_calendar(f, main_layout), + Mode::Projects => self.draw_projects(f, main_layout), + } + } + + fn draw_tabs(&self, f: &mut Frame, layout: Rect) { + let titles: Vec<&str> = vec!["Tasks", "Projects", "Calendar"]; + let tab_names: Vec<_> = titles.into_iter().map(Spans::from).collect(); + let selected_tab = match self.mode { + Mode::Tasks(_) => 0, + Mode::Projects => 1, + Mode::Calendar => 2, + }; + let navbar_block = Block::default().style(self.config.uda_style_navbar); + let context = Spans::from(vec![ + Span::from("["), + Span::from(if self.current_context.is_empty() { + "none" + } else { + &self.current_context + }), + Span::from("]"), + ]); + let tabs = Tabs::new(tab_names) + .block(navbar_block.clone()) + .select(selected_tab) + .divider(" ") + .highlight_style(Style::default().add_modifier(Modifier::BOLD)); + let rects = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Min(0), Constraint::Length(context.width() as u16)]) + .split(layout); + + f.render_widget(tabs, rects[0]); + f.render_widget(Paragraph::new(Text::from(context)).block(navbar_block), rects[1]); + } + + pub fn draw_debug(&mut self, f: &mut Frame) { + let area = centered_rect(50, 50, f.size()); + f.render_widget(Clear, area); + let t = format!("{}", self.current_selection); + let p = Paragraph::new(Text::from(t)).block(Block::default().borders(Borders::ALL).border_type(BorderType::Rounded)); + f.render_widget(p, area); + } + + pub fn draw_projects(&mut self, f: &mut Frame, rect: Rect) { + let data = self.projects.data.clone(); + let p = Paragraph::new(Text::from(&data[..])); + f.render_widget(p, rect); + } + + fn style_for_project(&self, project: &[String]) -> Style { + let virtual_tag_names_in_precedence = &self.config.rule_precedence_color; + let mut style = Style::default(); + for tag_name in virtual_tag_names_in_precedence.iter().rev() { + match tag_name.as_str() { + "project." => { + let s = self + .config + .color + .get(&format!("color.project.{}", project[0])) + .copied() + .unwrap_or_default(); + style = style.patch(s); + } + &_ => {} + } + } + style + } + + pub fn draw_calendar(&mut self, f: &mut Frame, layout: Rect) { + let mut c = Calendar::default() + .today_style(self.config.uda_style_calendar_today) + .year(self.calendar_year) + .date_style(self.get_dates_with_styles()) + .months_per_row(self.config.uda_calendar_months_per_row) + .start_on_monday(self.config.weekstart); + c.title_background_color = self.config.uda_style_calendar_title.bg.unwrap_or(Color::Reset); + f.render_widget(c, layout); + } + + pub fn draw_task(&mut self, f: &mut Frame, layout: Rect, action: Action) { + let rects = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(0), Constraint::Length(2)].as_ref()) + .split(layout); + + // render task report and task details if required + if self.task_report_show_info { + let split_task_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(rects[0]); - pub async fn resume_tui(&mut self) -> Result<()> { - self.resume_event_loop().await?; - let backend = CrosstermBackend::new(io::stdout()); - let mut terminal = Terminal::new(backend)?; - execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?; - enable_raw_mode()?; - self.requires_redraw = true; - terminal.hide_cursor()?; - Ok(()) + self.task_report_height = split_task_layout[0].height; + self.draw_task_report(f, split_task_layout[0]); + self.draw_task_details(f, split_task_layout[1]); + } else { + self.task_report_height = rects[0].height; + self.draw_task_report(f, rects[0]); } - pub async fn abort_event_loop(&mut self) -> Result<()> { - self.event_loop.abort.send(())?; - while let Some(event) = self.next().await { - if let Event::Closed = event { - break; + // calculate selected tasks + let selected = self.current_selection; + let task_ids = if self.tasks.is_empty() { + vec!["0".to_string()] + } else { + match self.task_table_state.mode() { + TableMode::SingleSelection => vec![self.tasks[selected].id().unwrap_or_default().to_string()], + TableMode::MultipleSelection => { + let mut tids = vec![]; + for uuid in &self.marked { + if let Some(t) = self.task_by_uuid(*uuid) { + tids.push(t.id().unwrap_or_default().to_string()); } + } + tids } - Ok(()) - } + } + }; - pub async fn resume_event_loop(&mut self) -> Result<()> { - let tick_rate = if self.config.uda_tick_rate > 0 { - Some(std::time::Duration::from_millis(self.config.uda_tick_rate)) + // render task mode + self.handle_task_mode_action(f, &rects, &task_ids, action); + } + + fn handle_task_mode_action(&mut self, f: &mut Frame, rects: &[Rect], task_ids: &[String], action: Action) { + match action { + Action::Error => { + self.draw_command( + f, + rects[1], + "Press any key to continue.", + (Span::styled("Error", Style::default().add_modifier(Modifier::BOLD)), None), + 0, + false, + self.error.clone(), + ); + let text = self.error.clone().unwrap_or_else(|| "Unknown error.".to_string()); + let title = vec![Span::styled("Error", Style::default().add_modifier(Modifier::BOLD))]; + let rect = centered_rect(90, 60, f.size()); + f.render_widget(Clear, rect); + let p = Paragraph::new(Text::from(text)) + .block(Block::default().borders(Borders::ALL).border_type(BorderType::Rounded).title(title)) + .wrap(Wrap { trim: true }); + f.render_widget(p, rect); + // draw error pop up + let rects = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(0)].as_ref()) + .split(f.size()); + } + Action::Report => { + // reset error when entering Action::Report + self.previous_mode = None; + self.error = None; + let position = Self::get_position(&self.command); + self.draw_command( + f, + rects[1], + self.filter.as_str(), + (Span::raw("Filter Tasks"), self.history_status.as_ref().map(Span::raw)), + Self::get_position(&self.filter), + false, + self.error.clone(), + ); + } + Action::Jump => { + let position = Self::get_position(&self.command); + self.draw_command( + f, + rects[1], + self.command.as_str(), + (Span::styled("Jump to Task", Style::default().add_modifier(Modifier::BOLD)), None), + position, + true, + self.error.clone(), + ); + } + Action::Filter => { + let position = Self::get_position(&self.filter); + if self.show_completion_pane { + self.draw_completion_pop_up(f, rects[1], position); + } + self.draw_command( + f, + rects[1], + self.filter.as_str(), + ( + Span::styled("Filter Tasks", Style::default().add_modifier(Modifier::BOLD)), + self + .history_status + .as_ref() + .map(|s| Span::styled(s, Style::default().add_modifier(Modifier::BOLD))), + ), + position, + true, + self.error.clone(), + ); + } + Action::Log => { + if self.config.uda_auto_insert_double_quotes_on_log && self.command.is_empty() { + self.command.update(r#""""#, 1, &mut self.changes); + }; + let position = Self::get_position(&self.command); + if self.show_completion_pane { + self.draw_completion_pop_up(f, rects[1], position); + } + self.draw_command( + f, + rects[1], + self.command.as_str(), + ( + Span::styled("Log Task", Style::default().add_modifier(Modifier::BOLD)), + self + .history_status + .as_ref() + .map(|s| Span::styled(s, Style::default().add_modifier(Modifier::BOLD))), + ), + position, + true, + self.error.clone(), + ); + } + Action::Subprocess => { + let position = Self::get_position(&self.command); + self.draw_command( + f, + rects[1], + self.command.as_str(), + (Span::styled("Shell Command", Style::default().add_modifier(Modifier::BOLD)), None), + position, + true, + self.error.clone(), + ); + } + Action::Modify => { + let position = Self::get_position(&self.modify); + if self.show_completion_pane { + self.draw_completion_pop_up(f, rects[1], position); + } + let label = if task_ids.len() > 1 { + format!("Modify Tasks {}", task_ids.join(",")) } else { - None + format!("Modify Task {}", task_ids.join(",")) }; - self.event_loop = crate::event::EventLoop::new(tick_rate, true); - Ok(()) + self.draw_command( + f, + rects[1], + self.modify.as_str(), + ( + Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), + self + .history_status + .as_ref() + .map(|s| Span::styled(s, Style::default().add_modifier(Modifier::BOLD))), + ), + position, + true, + self.error.clone(), + ); + } + Action::Annotate => { + if self.config.uda_auto_insert_double_quotes_on_annotate && self.command.is_empty() { + self.command.update(r#""""#, 1, &mut self.changes); + }; + let position = Self::get_position(&self.command); + if self.show_completion_pane { + self.draw_completion_pop_up(f, rects[1], position); + } + let label = if task_ids.len() > 1 { + format!("Annotate Tasks {}", task_ids.join(",")) + } else { + format!("Annotate Task {}", task_ids.join(",")) + }; + self.draw_command( + f, + rects[1], + self.command.as_str(), + ( + Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), + self + .history_status + .as_ref() + .map(|s| Span::styled(s, Style::default().add_modifier(Modifier::BOLD))), + ), + position, + true, + self.error.clone(), + ); + } + Action::Add => { + if self.config.uda_auto_insert_double_quotes_on_add && self.command.is_empty() { + self.command.update(r#""""#, 1, &mut self.changes); + }; + let position = Self::get_position(&self.command); + if self.show_completion_pane { + self.draw_completion_pop_up(f, rects[1], position); + } + self.draw_command( + f, + rects[1], + self.command.as_str(), + ( + Span::styled("Add Task", Style::default().add_modifier(Modifier::BOLD)), + self + .history_status + .as_ref() + .map(|s| Span::styled(s, Style::default().add_modifier(Modifier::BOLD))), + ), + position, + true, + self.error.clone(), + ); + } + Action::HelpPopup => { + self.draw_command( + f, + rects[1], + self.filter.as_str(), + ("Filter Tasks".into(), None), + Self::get_position(&self.filter), + false, + self.error.clone(), + ); + self.draw_help_popup(f, 80, 90); + } + Action::ContextMenu => { + self.draw_command( + f, + rects[1], + self.filter.as_str(), + ("Filter Tasks".into(), None), + Self::get_position(&self.filter), + false, + self.error.clone(), + ); + self.draw_context_menu(f, 80, 50); + } + Action::DonePrompt => { + let label = if task_ids.len() > 1 { + format!("Done Tasks {}?", task_ids.join(",")) + } else { + format!("Done Task {}?", task_ids.join(",")) + }; + let x = match self.keyconfig.done { + KeyCode::Char(c) => c.to_string(), + _ => "Enter".to_string(), + }; + let q = match self.keyconfig.quit { + KeyCode::Char(c) => c.to_string(), + _ => "Esc".to_string(), + }; + self.draw_command( + f, + rects[1], + &format!("Press <{}> to confirm or <{}> to abort.", x, q), + (Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), None), + 0, + false, + self.error.clone(), + ); + } + Action::DeletePrompt => { + let label = if task_ids.len() > 1 { + format!("Delete Tasks {}?", task_ids.join(",")) + } else { + format!("Delete Task {}?", task_ids.join(",")) + }; + let x = match self.keyconfig.delete { + KeyCode::Char(c) => c.to_string(), + _ => "Enter".to_string(), + }; + let q = match self.keyconfig.quit { + KeyCode::Char(c) => c.to_string(), + _ => "Esc".to_string(), + }; + self.draw_command( + f, + rects[1], + &format!("Press <{}> to confirm or <{}> to abort.", x, q), + (Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), None), + 0, + false, + self.error.clone(), + ); + } } - - pub async fn pause_tui(&mut self) -> Result<()> { - self.abort_event_loop().await?; - let backend = CrosstermBackend::new(io::stdout()); - let mut terminal = Terminal::new(backend)?; - disable_raw_mode()?; - execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)?; - terminal.show_cursor()?; - Ok(()) + } + + pub fn get_dates_with_styles(&self) -> Vec<(chrono::NaiveDate, Style)> { + if !self.tasks.is_empty() { + let tasks = &self.tasks; + tasks + .iter() + .filter_map(|t| t.due().map(|d| (d.clone(), self.style_for_task(t)))) + .map(|(d, t)| { + let now = Local::now(); + let reference = TimeZone::from_utc_datetime(now.offset(), &d); + (reference.date_naive(), t) + }) + .collect() + } else { + vec![] + } + } + + pub fn get_position(lb: &LineBuffer) -> usize { + let mut position = 0; + for (i, (j, g)) in lb.as_str().grapheme_indices(true).enumerate() { + if j == lb.pos() { + break; + } + position += g.width(); + } + position + } + + fn draw_help_popup(&mut self, f: &mut Frame, percent_x: u16, percent_y: u16) { + let area = centered_rect(percent_x, percent_y, f.size()); + f.render_widget(Clear, area); + + let chunks = Layout::default() + .constraints([Constraint::Max(area.height - 1), Constraint::Max(1)].as_ref()) + .margin(0) + .split(area); + + self.help_popup.scroll = std::cmp::min( + self.help_popup.scroll, + (self.help_popup.text_height as u16).saturating_sub(chunks[0].height - 3), + ); + + let ratio = ((self.help_popup.scroll + chunks[0].height) as f64 / self.help_popup.text_height as f64).min(1.0); + + let gauge = LineGauge::default() + .block(Block::default()) + .gauge_style(Style::default().fg(Color::Gray)) + .ratio(ratio); + + f.render_widget(gauge, chunks[1]); + f.render_widget(&self.help_popup, chunks[0]); + } + + fn draw_context_menu(&mut self, f: &mut Frame, percent_x: u16, percent_y: u16) { + let rects = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(0)].as_ref()) + .split(f.size()); + + let area = centered_rect(percent_x, percent_y, f.size()); + + f.render_widget(Clear, area.inner(&Margin { vertical: 0, horizontal: 0 })); + + let (contexts, headers) = self.get_all_contexts(); + + let maximum_column_width = area.width; + let widths = self.calculate_widths(&contexts, &headers, maximum_column_width); + + let selected = self.contexts.table_state.current_selection().unwrap_or_default(); + let header = headers.iter(); + let mut rows = vec![]; + let mut highlight_style = Style::default(); + for (i, context) in contexts.iter().enumerate() { + let mut style = Style::default(); + if &self.contexts.rows[i].active == "yes" { + style = self.config.uda_style_context_active; + } + rows.push(Row::StyledData(context.iter(), style)); + if i == self.contexts.table_state.current_selection().unwrap_or_default() { + highlight_style = style; + } } - pub async fn next(&mut self) -> Option> { - self.event_loop.rx.recv().await + let constraints: Vec = widths + .iter() + .map(|i| Constraint::Length((*i).try_into().unwrap_or(maximum_column_width))) + .collect(); + + let highlight_style = highlight_style.add_modifier(Modifier::BOLD); + let t = Table::new(header, rows.into_iter()) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .title(Spans::from(vec![Span::styled("Context", Style::default().add_modifier(Modifier::BOLD))])), + ) + .header_style( + self + .config + .color + .get("color.label") + .copied() + .unwrap_or_default() + .add_modifier(Modifier::UNDERLINED), + ) + .highlight_style(highlight_style) + .highlight_symbol(&self.config.uda_selection_indicator) + .widths(&constraints); + + f.render_stateful_widget(t, area, &mut self.contexts.table_state); + } + + fn draw_completion_pop_up(&mut self, f: &mut Frame, rect: Rect, cursor_position: usize) { + if self.completion_list.candidates().is_empty() { + self.show_completion_pane = false; + return; + } + // Iterate through all elements in the `items` app and append some debug text to it. + let items: Vec = self + .completion_list + .candidates() + .iter() + .map(|p| { + let lines = vec![Spans::from(vec![ + Span::styled(p.3.clone(), Style::default().add_modifier(Modifier::BOLD)), + Span::from(p.4.clone()), + ])]; + ListItem::new(lines) + }) + .collect(); + + // Create a List from all list items and highlight the currently selected one + let items = List::new(items) + .block(Block::default().borders(Borders::NONE).title("")) + .style(self.config.uda_style_report_completion_pane) + .highlight_style(self.config.uda_style_report_completion_pane_highlight) + .highlight_symbol(&self.config.uda_selection_indicator); + + let area = f.size(); + + let mut rect = rect; + rect.height = std::cmp::min(area.height / 2, self.completion_list.len() as u16 + 2); + rect.width = std::cmp::min( + area.width / 2, + self.completion_list.max_width().unwrap_or(40).try_into().unwrap_or(area.width / 2), + ); + rect.y = rect.y.saturating_sub(rect.height); + if cursor_position as u16 + rect.width >= area.width { + rect.x = area.width - rect.width; + } else { + rect.x = cursor_position as u16; } - pub async fn run(&mut self, terminal: &mut Terminal) -> Result<()> { - loop { - if self.requires_redraw { - terminal.resize(terminal.size()?)?; - self.requires_redraw = false; - } - terminal.draw(|f| self.draw(f))?; - // Handle input - if let Some(event) = self.next().await { - match event { - Event::Input(input) => { - debug!("Received input = {:?}", input); - self.handle_input(input).await?; - } - Event::Tick => { - debug!("Tick event"); - self.update(false).await?; - } - Event::Closed => { - debug!("Event loop closed"); - } - } - } + // We can now render the item list + f.render_widget(Clear, rect); + f.render_stateful_widget(items, rect, &mut self.completion_list.state); + } + + fn draw_command( + &self, + f: &mut Frame, + rect: Rect, + text: &str, + title: (Span, Option), + position: usize, + cursor: bool, + error: Option, + ) { + // f.render_widget(Clear, rect); + if cursor { + f.set_cursor(std::cmp::min(rect.x + position as u16, rect.x + rect.width.saturating_sub(2)), rect.y + 1); + } + let rects = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Min(0)].as_ref()) + .split(rect); + + // render command title + let mut style = self.config.uda_style_command; + if error.is_some() { + style = style.fg(Color::Red); + }; + let title_spans = if let Some(subtitle) = title.1 { + Spans::from(vec![title.0, Span::from(" ["), subtitle, Span::from("]")]) + } else { + Spans::from(vec![title.0]) + }; + let title = Paragraph::new(Text::from(title_spans)).style(style); + f.render_widget(title, rects[0]); + + // render command + let p = Paragraph::new(Text::from(text)).scroll((0, ((position + 2) as u16).saturating_sub(rects[1].width))); + f.render_widget(p, rects[1]); + } + + fn draw_task_details(&mut self, f: &mut Frame, rect: Rect) { + if self.tasks.is_empty() { + let p = Paragraph::new(Text::from("Task not found")).block(Block::default().borders(Borders::TOP)); + f.render_widget(p, rect); + return; + } + let selected = self.current_selection; + let task_id = self.tasks[selected].id().unwrap_or_default(); + let task_uuid = *self.tasks[selected].uuid(); - if self.should_quit { - break; - } - } - Ok(()) + let data = match self.task_details.get(&task_uuid) { + Some(s) => s.clone(), + None => "Loading task details ...".to_string(), + }; + self.task_details_scroll = std::cmp::min( + (data.lines().count() as u16).saturating_sub(rect.height).saturating_add(2), + self.task_details_scroll, + ); + let p = Paragraph::new(Text::from(&data[..])) + .block(Block::default().borders(Borders::TOP)) + .scroll((self.task_details_scroll, 0)); + f.render_widget(p, rect); + } + + fn task_details_scroll_up(&mut self) { + self.task_details_scroll = self.task_details_scroll.saturating_sub(1); + } + + fn task_details_scroll_down(&mut self) { + self.task_details_scroll = self.task_details_scroll.saturating_add(1); + } + + fn task_by_index(&self, i: usize) -> Option { + let tasks = &self.tasks; + if i >= tasks.len() { + None + } else { + Some(tasks[i].clone()) } + } + + fn task_by_uuid(&self, uuid: Uuid) -> Option { + let tasks = &self.tasks; + let m = tasks.iter().find(|t| *t.uuid() == uuid); + m.cloned() + } + + fn task_by_id(&self, id: u64) -> Option { + let tasks = &self.tasks; + let m = tasks.iter().find(|t| t.id() == Some(id)); + m.cloned() + } + + fn task_index_by_id(&self, id: u64) -> Option { + let tasks = &self.tasks; + let m = tasks.iter().position(|t| t.id() == Some(id)); + m + } + + fn task_index_by_uuid(&self, uuid: Uuid) -> Option { + let tasks = &self.tasks; + let m = tasks.iter().position(|t| *t.uuid() == uuid); + m + } + + fn style_for_task(&self, task: &Task) -> Style { + let virtual_tag_names_in_precedence = &self.config.rule_precedence_color; + + let mut style = Style::default(); + + for tag_name in virtual_tag_names_in_precedence.iter().rev() { + if tag_name == "uda." || tag_name == "priority" { + if let Some(p) = task.priority() { + let s = self.config.color.get(&format!("color.uda.priority.{}", p)).copied().unwrap_or_default(); + style = style.patch(s); + } + } else if tag_name == "tag." { + if let Some(tags) = task.tags() { + for t in tags { + let color_tag_name = format!("color.tag.{}", t); + let s = self.config.color.get(&color_tag_name).copied().unwrap_or_default(); + style = style.patch(s); + } + } + } else if tag_name == "project." { + if let Some(p) = task.project() { + let s = self.config.color.get(&format!("color.project.{}", p)).copied().unwrap_or_default(); + style = style.patch(s); + } + } else if task + .tags() + .unwrap_or(&vec![]) + .contains(&tag_name.to_string().replace('.', "").to_uppercase()) + { + let color_tag_name = format!("color.{}", tag_name); + let s = self.config.color.get(&color_tag_name).copied().unwrap_or_default(); + style = style.patch(s); + } + } + + style + } - pub fn reset_command(&mut self) { - self.command.update("", 0) + pub fn calculate_widths(&self, tasks: &[Vec], headers: &[String], maximum_column_width: u16) -> Vec { + // naive implementation of calculate widths + let mut widths = headers.iter().map(String::len).collect::>(); + + for row in tasks.iter() { + for (i, cell) in row.iter().enumerate() { + widths[i] = std::cmp::max(cell.len(), widths[i]); + } } - pub fn get_context(&mut self) -> Result<()> { - let output = std::process::Command::new("task") - .arg("_get") - .arg("rc.context") - .output()?; - self.current_context = String::from_utf8_lossy(&output.stdout).to_string(); - self.current_context = self.current_context.strip_suffix('\n').unwrap_or("").to_string(); + for (i, header) in headers.iter().enumerate() { + if header == "Description" || header == "Definition" { + // always give description or definition the most room to breath + widths[i] = maximum_column_width as usize; + break; + } + } + for (i, header) in headers.iter().enumerate() { + if i == 0 { + // always give ID a couple of extra for indicator + widths[i] += self.config.uda_selection_indicator.as_str().width(); + // if let TableMode::MultipleSelection = self.task_table_state.mode() { + // widths[i] += 2 + // }; + } + } - // support new format for context - let output = std::process::Command::new("task") - .arg("_get") - .arg(format!("rc.context.{}.read", self.current_context)) - .output()?; - self.current_context_filter = String::from_utf8_lossy(&output.stdout).to_string(); - self.current_context_filter = self.current_context_filter.strip_suffix('\n').unwrap_or("").to_string(); - - // If new format is not used, check if old format is used - if self.current_context_filter.is_empty() { - let output = std::process::Command::new("task") - .arg("_get") - .arg(format!("rc.context.{}", self.current_context)) - .output()?; - self.current_context_filter = String::from_utf8_lossy(&output.stdout).to_string(); - self.current_context_filter = self.current_context_filter.strip_suffix('\n').unwrap_or("").to_string(); - } - Ok(()) + // now start trimming + while (widths.iter().sum::() as u16) >= maximum_column_width - (headers.len()) as u16 { + let index = widths.iter().position(|i| i == widths.iter().max().unwrap_or(&0)).unwrap_or_default(); + if widths[index] == 1 { + break; + } + widths[index] -= 1; } - pub fn draw(&mut self, f: &mut Frame) { - let rect = f.size(); - self.terminal_width = rect.width; - self.terminal_height = rect.height; + widths + } - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(1), Constraint::Min(0)]) - .split(f.size()); + fn draw_task_report(&mut self, f: &mut Frame, rect: Rect) { + let (tasks, headers) = self.get_task_report(); - let tab_layout = chunks[0]; - let main_layout = chunks[1]; + if tasks.is_empty() { + if !self.current_context.is_empty() { + let context_style = Style::default(); + context_style.add_modifier(Modifier::ITALIC); + } - self.draw_tabs(f, tab_layout); - match self.mode { - Mode::Tasks(action) => self.draw_task(f, main_layout, action), - Mode::Calendar => self.draw_calendar(f, main_layout), - Mode::Projects => self.draw_projects(f, main_layout), - } + f.render_widget(Block::default(), rect); + return; } - fn draw_tabs(&self, f: &mut Frame, layout: Rect) { - let titles: Vec<&str> = vec!["Tasks", "Projects", "Calendar"]; - let tab_names: Vec<_> = titles.into_iter().map(Spans::from).collect(); - let selected_tab = match self.mode { - Mode::Tasks(_) => 0, - Mode::Projects => 1, - Mode::Calendar => 2, - }; - let navbar_block = Block::default().style(self.config.uda_style_navbar); - let context = Spans::from(vec![ - Span::from("["), - Span::from(if self.current_context.is_empty() { - "none" - } else { - &self.current_context - }), - Span::from("]"), - ]); - let tabs = Tabs::new(tab_names) - .block(navbar_block.clone()) - .select(selected_tab) - .divider(" ") - .highlight_style(Style::default().add_modifier(Modifier::BOLD)); - let rects = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Min(0), Constraint::Length(context.width() as u16)]) - .split(layout); + let maximum_column_width = rect.width; + let widths = self.calculate_widths(&tasks, &headers, maximum_column_width); - f.render_widget(tabs, rects[0]); - f.render_widget(Paragraph::new(Text::from(context)).block(navbar_block), rects[1]); + for (i, header) in headers.iter().enumerate() { + if header == "Description" || header == "Definition" { + self.task_report_table.description_width = widths[i] - 1; + break; + } } - - pub fn draw_debug(&mut self, f: &mut Frame) { - let area = centered_rect(50, 50, f.size()); - f.render_widget(Clear, area); - let t = format!("{}", self.current_selection); - let p = Paragraph::new(Text::from(t)) - .block(Block::default().borders(Borders::ALL).border_type(BorderType::Rounded)); - f.render_widget(p, area); + let selected = self.current_selection; + let header = headers.iter(); + let mut rows = vec![]; + let mut highlight_style = Style::default(); + let mut pos = 0; + for (i, task) in tasks.iter().enumerate() { + let style = self.style_for_task(&self.tasks[i]); + if i == selected { + pos = i; + highlight_style = style.patch(self.config.uda_style_report_selection); + if self.config.uda_selection_bold { + highlight_style = highlight_style.add_modifier(Modifier::BOLD); + } + if self.config.uda_selection_italic { + highlight_style = highlight_style.add_modifier(Modifier::ITALIC); + } + if self.config.uda_selection_dim { + highlight_style = highlight_style.add_modifier(Modifier::DIM); + } + if self.config.uda_selection_blink { + highlight_style = highlight_style.add_modifier(Modifier::SLOW_BLINK); + } + if self.config.uda_selection_reverse { + highlight_style = highlight_style.add_modifier(Modifier::REVERSED); + } + } + rows.push(Row::StyledData(task.iter(), style)); } - pub fn draw_projects(&mut self, f: &mut Frame, rect: Rect) { - let data = self.projects.data.clone(); - let p = Paragraph::new(Text::from(&data[..])); - f.render_widget(p, rect); + let constraints: Vec = widths + .iter() + .map(|i| Constraint::Length((*i).try_into().unwrap_or(maximum_column_width))) + .collect(); + + let t = Table::new(header, rows.into_iter()) + .header_style( + self + .config + .color + .get("color.label") + .copied() + .unwrap_or_default() + .add_modifier(Modifier::UNDERLINED), + ) + .highlight_style(highlight_style) + .highlight_symbol(&self.config.uda_selection_indicator) + .mark_symbol(&self.config.uda_mark_indicator) + .unmark_symbol(&self.config.uda_unmark_indicator) + .widths(&constraints); + + f.render_stateful_widget(t, rect, &mut self.task_table_state); + if tasks.iter().len() as u16 > rect.height.saturating_sub(4) { + let mut widget = Scrollbar::new(pos, tasks.iter().len()); + widget.pos_style = self.config.uda_style_report_scrollbar; + widget.pos_symbol = self.config.uda_scrollbar_indicator.clone(); + widget.area_style = self.config.uda_style_report_scrollbar_area; + widget.area_symbol = self.config.uda_scrollbar_area.clone(); + f.render_widget(widget, rect); } - - fn style_for_project(&self, project: &[String]) -> Style { - let virtual_tag_names_in_precedence = &self.config.rule_precedence_color; - let mut style = Style::default(); - for tag_name in virtual_tag_names_in_precedence.iter().rev() { - match tag_name.as_str() { - "project." => { - let s = self - .config - .color - .get(&format!("color.project.{}", project[0])) - .copied() - .unwrap_or_default(); - style = style.patch(s); - } - &_ => {} - } - } - style + } + + fn get_all_contexts(&self) -> (Vec>, Vec) { + let contexts = self + .contexts + .rows + .iter() + .filter(|c| &c.type_ == "read") + .map(|c| vec![c.name.clone(), c.definition.clone(), c.active.clone()]) + .collect(); + let headers = vec!["Name".to_string(), "Definition".to_string(), "Active".to_string()]; + (contexts, headers) + } + + fn get_task_report(&mut self) -> (Vec>, Vec) { + self.task_report_table.generate_table(&self.tasks); + let (tasks, headers) = self.task_report_table.simplify_table(); + (tasks, headers) + } + + pub async fn update(&mut self, force: bool) -> Result<()> { + trace!("self.update({:?});", force); + if force || self.dirty || self.tasks_changed_since(self.last_export).unwrap_or(true) { + self.get_context()?; + let task_uuids = self.selected_task_uuids(); + if self.current_selection_uuid.is_none() && self.current_selection_id.is_none() && task_uuids.len() == 1 { + if let Some(uuid) = task_uuids.get(0) { + self.current_selection_uuid = Some(*uuid); + } + } + + self.last_export = Some(std::time::SystemTime::now()); + self.task_report_table.export_headers(None, &self.report)?; + self.export_tasks()?; + if self.config.uda_task_report_use_all_tasks_for_completion { + self.export_all_tasks()?; + } + self.contexts.update_data()?; + self.projects.update_data()?; + self.update_tags(); + self.task_details.clear(); + self.dirty = false; + self.save_history()?; + } + self.cursor_fix(); + self.update_task_table_state(); + if self.task_report_show_info { + self.update_task_details().await?; } + self.selection_fix(); - pub fn draw_calendar(&mut self, f: &mut Frame, layout: Rect) { - let mut c = Calendar::default() - .today_style(self.config.uda_style_calendar_today) - .year(self.calendar_year) - .date_style(self.get_dates_with_styles()) - .months_per_row(self.config.uda_calendar_months_per_row) - .start_on_monday(self.config.weekstart); - c.title_background_color = self.config.uda_style_calendar_title.bg.unwrap_or(Color::Reset); - f.render_widget(c, layout); + Ok(()) + } + + pub fn selection_fix(&mut self) { + if let (Some(t), Some(id)) = (self.task_current(), self.current_selection_id) { + if t.id() != Some(id) { + if let Some(i) = self.task_index_by_id(id) { + self.current_selection = i; + self.current_selection_id = None; + } + } } - pub fn draw_task(&mut self, f: &mut Frame, layout: Rect, action: Action) { - let rects = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(0), Constraint::Length(2)].as_ref()) - .split(layout); - - // render task report and task details if required - if self.task_report_show_info { - let split_task_layout = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(rects[0]); - - self.task_report_height = split_task_layout[0].height; - self.draw_task_report(f, split_task_layout[0]); - self.draw_task_details(f, split_task_layout[1]); - } else { - self.task_report_height = rects[0].height; - self.draw_task_report(f, rects[0]); + if let (Some(t), Some(uuid)) = (self.task_current(), self.current_selection_uuid) { + if t.uuid() != &uuid { + if let Some(i) = self.task_index_by_uuid(uuid) { + self.current_selection = i; + self.current_selection_uuid = None; } + } + } + } - // calculate selected tasks - let selected = self.current_selection; - let task_ids = if self.tasks.is_empty() { - vec!["0".to_string()] - } else { - match self.task_table_state.mode() { - TableMode::SingleSelection => vec![self.tasks[selected].id().unwrap_or_default().to_string()], - TableMode::MultipleSelection => { - let mut tids = vec![]; - for uuid in &self.marked { - if let Some(t) = self.task_by_uuid(*uuid) { - tids.push(t.id().unwrap_or_default().to_string()); - } - } - tids - } - } - }; + pub fn save_history(&mut self) -> Result<()> { + self.filter_history.write()?; + self.command_history.write()?; + Ok(()) + } - // render task mode - self.handle_task_mode_action(f, &rects, &task_ids, action); - } - - fn handle_task_mode_action( - &mut self, - f: &mut Frame, - rects: &[Rect], - task_ids: &[String], - action: Action, - ) { - match action { - Action::Error => { - self.draw_command( - f, - rects[1], - "Press any key to continue.", - ( - Span::styled("Error", Style::default().add_modifier(Modifier::BOLD)), - None, - ), - 0, - false, - self.error.clone(), - ); - let text = self.error.clone().unwrap_or_else(|| "Unknown error.".to_string()); - let title = vec![Span::styled("Error", Style::default().add_modifier(Modifier::BOLD))]; - let rect = centered_rect(90, 60, f.size()); - f.render_widget(Clear, rect); - let p = Paragraph::new(Text::from(text)) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .title(title), - ) - .wrap(Wrap { trim: true }); - f.render_widget(p, rect); - // draw error pop up - let rects = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(0)].as_ref()) - .split(f.size()); - } - Action::Report => { - // reset error when entering Action::Report - self.previous_mode = None; - self.error = None; - let position = Self::get_position(&self.command); - self.draw_command( - f, - rects[1], - self.filter.as_str(), - (Span::raw("Filter Tasks"), self.history_status.as_ref().map(Span::raw)), - Self::get_position(&self.filter), - false, - self.error.clone(), - ); - } - Action::Jump => { - let position = Self::get_position(&self.command); - self.draw_command( - f, - rects[1], - self.command.as_str(), - ( - Span::styled("Jump to Task", Style::default().add_modifier(Modifier::BOLD)), - None, - ), - position, - true, - self.error.clone(), - ); - } - Action::Filter => { - let position = Self::get_position(&self.filter); - if self.show_completion_pane { - self.draw_completion_pop_up(f, rects[1], position); - } - self.draw_command( - f, - rects[1], - self.filter.as_str(), - ( - Span::styled("Filter Tasks", Style::default().add_modifier(Modifier::BOLD)), - self.history_status - .as_ref() - .map(|s| Span::styled(s, Style::default().add_modifier(Modifier::BOLD))), - ), - position, - true, - self.error.clone(), - ); - } - Action::Log => { - if self.config.uda_auto_insert_double_quotes_on_log && self.command.is_empty() { - self.command.update(r#""""#, 1); - }; - let position = Self::get_position(&self.command); - if self.show_completion_pane { - self.draw_completion_pop_up(f, rects[1], position); - } - self.draw_command( - f, - rects[1], - self.command.as_str(), - ( - Span::styled("Log Task", Style::default().add_modifier(Modifier::BOLD)), - self.history_status - .as_ref() - .map(|s| Span::styled(s, Style::default().add_modifier(Modifier::BOLD))), - ), - position, - true, - self.error.clone(), - ); - } - Action::Subprocess => { - let position = Self::get_position(&self.command); - self.draw_command( - f, - rects[1], - self.command.as_str(), - ( - Span::styled("Shell Command", Style::default().add_modifier(Modifier::BOLD)), - None, - ), - position, - true, - self.error.clone(), - ); - } - Action::Modify => { - let position = Self::get_position(&self.modify); - if self.show_completion_pane { - self.draw_completion_pop_up(f, rects[1], position); - } - let label = if task_ids.len() > 1 { - format!("Modify Tasks {}", task_ids.join(",")) - } else { - format!("Modify Task {}", task_ids.join(",")) - }; - self.draw_command( - f, - rects[1], - self.modify.as_str(), - ( - Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), - self.history_status - .as_ref() - .map(|s| Span::styled(s, Style::default().add_modifier(Modifier::BOLD))), - ), - position, - true, - self.error.clone(), - ); - } - Action::Annotate => { - if self.config.uda_auto_insert_double_quotes_on_annotate && self.command.is_empty() { - self.command.update(r#""""#, 1); - }; - let position = Self::get_position(&self.command); - if self.show_completion_pane { - self.draw_completion_pop_up(f, rects[1], position); - } - let label = if task_ids.len() > 1 { - format!("Annotate Tasks {}", task_ids.join(",")) - } else { - format!("Annotate Task {}", task_ids.join(",")) - }; - self.draw_command( - f, - rects[1], - self.command.as_str(), - ( - Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), - self.history_status - .as_ref() - .map(|s| Span::styled(s, Style::default().add_modifier(Modifier::BOLD))), - ), - position, - true, - self.error.clone(), - ); - } - Action::Add => { - if self.config.uda_auto_insert_double_quotes_on_add && self.command.is_empty() { - self.command.update(r#""""#, 1); - }; - let position = Self::get_position(&self.command); - if self.show_completion_pane { - self.draw_completion_pop_up(f, rects[1], position); - } - self.draw_command( - f, - rects[1], - self.command.as_str(), - ( - Span::styled("Add Task", Style::default().add_modifier(Modifier::BOLD)), - self.history_status - .as_ref() - .map(|s| Span::styled(s, Style::default().add_modifier(Modifier::BOLD))), - ), - position, - true, - self.error.clone(), - ); - } - Action::HelpPopup => { - self.draw_command( - f, - rects[1], - self.filter.as_str(), - ("Filter Tasks".into(), None), - Self::get_position(&self.filter), - false, - self.error.clone(), - ); - self.draw_help_popup(f, 80, 90); - } - Action::ContextMenu => { - self.draw_command( - f, - rects[1], - self.filter.as_str(), - ("Filter Tasks".into(), None), - Self::get_position(&self.filter), - false, - self.error.clone(), - ); - self.draw_context_menu(f, 80, 50); - } - Action::DonePrompt => { - let label = if task_ids.len() > 1 { - format!("Done Tasks {}?", task_ids.join(",")) - } else { - format!("Done Task {}?", task_ids.join(",")) - }; - let x = match self.keyconfig.done { - KeyCode::Char(c) => c.to_string(), - _ => "Enter".to_string(), - }; - let q = match self.keyconfig.quit { - KeyCode::Char(c) => c.to_string(), - _ => "Esc".to_string(), - }; - self.draw_command( - f, - rects[1], - &format!("Press <{}> to confirm or <{}> to abort.", x, q), - (Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), None), - 0, - false, - self.error.clone(), - ); - } - Action::DeletePrompt => { - let label = if task_ids.len() > 1 { - format!("Delete Tasks {}?", task_ids.join(",")) - } else { - format!("Delete Task {}?", task_ids.join(",")) - }; - let x = match self.keyconfig.delete { - KeyCode::Char(c) => c.to_string(), - _ => "Enter".to_string(), - }; - let q = match self.keyconfig.quit { - KeyCode::Char(c) => c.to_string(), - _ => "Esc".to_string(), - }; - self.draw_command( - f, - rects[1], - &format!("Press <{}> to confirm or <{}> to abort.", x, q), - (Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), None), - 0, - false, - self.error.clone(), - ); - } - } + pub fn cursor_fix(&mut self) { + while !self.tasks.is_empty() && self.current_selection >= self.tasks.len() { + self.task_report_previous(); } + } - pub fn get_dates_with_styles(&self) -> Vec<(chrono::Date, Style)> { - if !self.tasks.is_empty() { - let tasks = &self.tasks; - tasks - .iter() - .filter_map(|t| t.due().map(|d| (d.clone(), self.style_for_task(t)))) - .map(|(d, t)| { - let now = Local::now(); - let reference = TimeZone::from_utc_datetime(now.offset(), &d); - (reference.date(), t) - }) - .collect() - } else { - vec![] - } + pub async fn update_task_details(&mut self) -> Result<()> { + if self.tasks.is_empty() { + return Ok(()); } - pub fn get_position(lb: &LineBuffer) -> usize { - let mut position = 0; - for (i, (j, g)) in lb.as_str().grapheme_indices(true).enumerate() { - if j == lb.pos() { - break; - } - position += g.width(); - } - position + // remove task_details of tasks not in task report + let mut to_delete = vec![]; + for k in self.task_details.keys() { + if !self.tasks.iter().map(Task::uuid).any(|x| x == k) { + to_delete.push(*k); + } + } + for k in to_delete { + self.task_details.remove(&k); } - fn draw_help_popup(&mut self, f: &mut Frame, percent_x: u16, percent_y: u16) { - let area = centered_rect(percent_x, percent_y, f.size()); - f.render_widget(Clear, area); + let selected = self.current_selection; + if selected >= self.tasks.len() { + return Ok(()); + } + let current_task_uuid = *self.tasks[selected].uuid(); - let chunks = Layout::default() - .constraints([Constraint::Max(area.height - 1), Constraint::Max(1)].as_ref()) - .margin(0) - .split(area); + let mut l = vec![selected]; - self.help_popup.scroll = std::cmp::min( - self.help_popup.scroll, - (self.help_popup.text_height as u16).saturating_sub(chunks[0].height - 3), - ); + for s in 1..=self.config.uda_task_detail_prefetch { + l.insert(0, std::cmp::min(selected.saturating_sub(s), self.tasks.len() - 1)); + l.push(std::cmp::min(selected + s, self.tasks.len() - 1)); + } - let ratio = ((self.help_popup.scroll + chunks[0].height) as f64 / self.help_popup.text_height as f64).min(1.0); + l.dedup(); + + let (tx, mut rx) = tokio::sync::mpsc::channel(100); + let tasks = self.tasks.clone(); + let defaultwidth = self.terminal_width.saturating_sub(2); + for s in &l { + if tasks.is_empty() { + return Ok(()); + } + if s >= &tasks.len() { + break; + } + let task_uuid = *tasks[*s].uuid(); + if !self.task_details.contains_key(&task_uuid) || task_uuid == current_task_uuid { + debug!("Running task details for {}", task_uuid); + let _tx = tx.clone(); + tokio::spawn(async move { + let output = tokio::process::Command::new("task") + .arg("rc.color=off") + .arg("rc._forcecolor=off") + .arg(format!("rc.defaultwidth={}", defaultwidth)) + .arg(format!("{}", task_uuid)) + .output() + .await; + if let Ok(output) = output { + let data = String::from_utf8_lossy(&output.stdout).to_string(); + _tx.send(Some((task_uuid, data))).await.unwrap(); + } + }); + } + } + drop(tx); + while let Some(Some((task_uuid, data))) = rx.recv().await { + self.task_details.insert(task_uuid, data); + } + Ok(()) + } - let gauge = LineGauge::default() - .block(Block::default()) - .gauge_style(Style::default().fg(Color::Gray)) - .ratio(ratio); + pub fn update_task_table_state(&mut self) { + trace!("self.update_task_table_state()"); + self.task_table_state.select(Some(self.current_selection)); - f.render_widget(gauge, chunks[1]); - f.render_widget(&self.help_popup, chunks[0]); + for uuid in self.marked.clone() { + if self.task_by_uuid(uuid).is_none() { + self.marked.remove(&uuid); + } } - fn draw_context_menu(&mut self, f: &mut Frame, percent_x: u16, percent_y: u16) { - let rects = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(0)].as_ref()) - .split(f.size()); - - let area = centered_rect(percent_x, percent_y, f.size()); - - f.render_widget( - Clear, - area.inner(&Margin { - vertical: 0, - horizontal: 0, - }), - ); + if self.marked.is_empty() { + self.task_table_state.single_selection(); + } - let (contexts, headers) = self.get_all_contexts(); + self.task_table_state.clear(); - let maximum_column_width = area.width; - let widths = self.calculate_widths(&contexts, &headers, maximum_column_width); + for uuid in &self.marked { + self.task_table_state.mark(self.task_index_by_uuid(*uuid)); + } + } - let selected = self.contexts.table_state.current_selection().unwrap_or_default(); - let header = headers.iter(); - let mut rows = vec![]; - let mut highlight_style = Style::default(); - for (i, context) in contexts.iter().enumerate() { - let mut style = Style::default(); - if &self.contexts.rows[i].active == "yes" { - style = self.config.uda_style_context_active; - } - rows.push(Row::StyledData(context.iter(), style)); - if i == self.contexts.table_state.current_selection().unwrap_or_default() { - highlight_style = style; - } + pub fn context_next(&mut self) { + let i = match self.contexts.table_state.current_selection() { + Some(i) => { + if i >= self.contexts.len() - 1 { + 0 + } else { + i + 1 } - - let constraints: Vec = widths - .iter() - .map(|i| Constraint::Length((*i).try_into().unwrap_or(maximum_column_width as u16))) - .collect(); - - let highlight_style = highlight_style.add_modifier(Modifier::BOLD); - let t = Table::new(header, rows.into_iter()) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .title(Spans::from(vec![Span::styled( - "Context", - Style::default().add_modifier(Modifier::BOLD), - )])), - ) - .header_style( - self.config - .color - .get("color.label") - .copied() - .unwrap_or_default() - .add_modifier(Modifier::UNDERLINED), - ) - .highlight_style(highlight_style) - .highlight_symbol(&self.config.uda_selection_indicator) - .widths(&constraints); - - f.render_stateful_widget(t, area, &mut self.contexts.table_state); - } - - fn draw_completion_pop_up(&mut self, f: &mut Frame, rect: Rect, cursor_position: usize) { - if self.completion_list.candidates().is_empty() { - self.show_completion_pane = false; - return; + } + None => 0, + }; + self.contexts.table_state.select(Some(i)); + } + + pub fn context_previous(&mut self) { + let i = match self.contexts.table_state.current_selection() { + Some(i) => { + if i == 0 { + self.contexts.len() - 1 + } else { + i - 1 } - // Iterate through all elements in the `items` app and append some debug text to it. - let items: Vec = self - .completion_list - .candidates() - .iter() - .map(|p| { - let lines = vec![Spans::from(vec![ - Span::styled(p.3.clone(), Style::default().add_modifier(Modifier::BOLD)), - Span::from(p.4.clone()), - ])]; - ListItem::new(lines) - }) - .collect(); - - // Create a List from all list items and highlight the currently selected one - let items = List::new(items) - .block(Block::default().borders(Borders::NONE).title("")) - .style(self.config.uda_style_report_completion_pane) - .highlight_style(self.config.uda_style_report_completion_pane_highlight) - .highlight_symbol(&self.config.uda_selection_indicator); - - let area = f.size(); - - let mut rect = rect; - rect.height = std::cmp::min(area.height / 2, self.completion_list.len() as u16 + 2); - rect.width = std::cmp::min( - area.width / 2, - self.completion_list - .max_width() - .unwrap_or(40) - .try_into() - .unwrap_or(area.width / 2), - ); - rect.y = rect.y.saturating_sub(rect.height); - if cursor_position as u16 + rect.width >= area.width { - rect.x = area.width - rect.width; + } + None => 0, + }; + self.contexts.table_state.select(Some(i)); + } + + pub fn context_select(&mut self) -> Result<()> { + let i = self.contexts.table_state.current_selection().unwrap_or_default(); + let mut command = std::process::Command::new("task"); + command.arg("context").arg(&self.contexts.rows[i].name); + command.output()?; + Ok(()) + } + + pub fn task_report_top(&mut self) { + if self.tasks.is_empty() { + return; + } + self.current_selection = 0; + self.current_selection_id = None; + self.current_selection_uuid = None; + } + + pub fn task_report_bottom(&mut self) { + if self.tasks.is_empty() { + return; + } + self.current_selection = self.tasks.len() - 1; + self.current_selection_id = None; + self.current_selection_uuid = None; + } + + pub fn task_report_next(&mut self) { + if self.tasks.is_empty() { + return; + } + let i = { + if self.current_selection >= self.tasks.len() - 1 { + if self.config.uda_task_report_looping { + 0 } else { - rect.x = cursor_position as u16; + self.current_selection } - - // We can now render the item list - f.render_widget(Clear, rect); - f.render_stateful_widget(items, rect, &mut self.completion_list.state); - } - - fn draw_command( - &self, - f: &mut Frame, - rect: Rect, - text: &str, - title: (Span, Option), - position: usize, - cursor: bool, - error: Option, - ) { - // f.render_widget(Clear, rect); - if cursor { - f.set_cursor( - std::cmp::min(rect.x + position as u16, rect.x + rect.width.saturating_sub(2)), - rect.y + 1, - ); + } else { + self.current_selection + 1 + } + }; + self.current_selection = i; + self.current_selection_id = None; + self.current_selection_uuid = None; + } + + pub fn task_report_previous(&mut self) { + if self.tasks.is_empty() { + return; + } + let i = { + if self.current_selection == 0 { + if self.config.uda_task_report_looping { + self.tasks.len() - 1 + } else { + 0 } - let rects = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(1), Constraint::Min(0)].as_ref()) - .split(rect); - - // render command title - let mut style = self.config.uda_style_command; - if error.is_some() { - style = style.fg(Color::Red); - }; - let title_spans = if let Some(subtitle) = title.1 { - Spans::from(vec![title.0, Span::from(" ["), subtitle, Span::from("]")]) + } else { + self.current_selection - 1 + } + }; + self.current_selection = i; + self.current_selection_id = None; + self.current_selection_uuid = None; + } + + pub fn task_report_next_page(&mut self) { + if self.tasks.is_empty() { + return; + } + let i = { + if self.current_selection == self.tasks.len() - 1 { + if self.config.uda_task_report_looping { + 0 } else { - Spans::from(vec![title.0]) - }; - let title = Paragraph::new(Text::from(title_spans)).style(style); - f.render_widget(title, rects[0]); - - // render command - let p = Paragraph::new(Text::from(text)).scroll((0, ((position + 2) as u16).saturating_sub(rects[1].width))); - f.render_widget(p, rects[1]); + self.tasks.len() - 1 + } + } else { + std::cmp::min( + self + .current_selection + .checked_add(self.task_report_height as usize) + .unwrap_or(self.tasks.len() - 1), + self.tasks.len() - 1, + ) + } + }; + self.current_selection = i; + self.current_selection_id = None; + self.current_selection_uuid = None; + } + + pub fn task_report_previous_page(&mut self) { + if self.tasks.is_empty() { + return; } - - fn draw_task_details(&mut self, f: &mut Frame, rect: Rect) { - if self.tasks.is_empty() { - let p = Paragraph::new(Text::from("Task not found")).block(Block::default().borders(Borders::TOP)); - f.render_widget(p, rect); - return; + let i = { + if self.current_selection == 0 { + if self.config.uda_task_report_looping { + self.tasks.len() - 1 + } else { + 0 } - let selected = self.current_selection; - let task_id = self.tasks[selected].id().unwrap_or_default(); - let task_uuid = *self.tasks[selected].uuid(); - - let data = match self.task_details.get(&task_uuid) { - Some(s) => s.clone(), - None => "Loading task details ...".to_string(), - }; - self.task_details_scroll = std::cmp::min( - (data.lines().count() as u16) - .saturating_sub(rect.height) - .saturating_add(2), - self.task_details_scroll, - ); - let p = Paragraph::new(Text::from(&data[..])) - .block(Block::default().borders(Borders::TOP)) - .scroll((self.task_details_scroll, 0)); - f.render_widget(p, rect); + } else { + self.current_selection.saturating_sub(self.task_report_height as usize) + } + }; + self.current_selection = i; + self.current_selection_id = None; + self.current_selection_uuid = None; + } + + pub fn task_report_jump(&mut self) -> Result<()> { + if self.tasks.is_empty() { + return Ok(()); } - - fn task_details_scroll_up(&mut self) { - self.task_details_scroll = self.task_details_scroll.saturating_sub(1); + let i = self.command.as_str().parse::()?; + if let Some(task) = self.task_by_id(i as u64) { + let j = self.task_index_by_uuid(*task.uuid()).unwrap_or_default(); + self.current_selection = j; + self.current_selection_id = None; + self.current_selection_uuid = None; + Ok(()) + } else { + Err(anyhow!("Cannot locate task id {} in report", i)) } - - fn task_details_scroll_down(&mut self) { - self.task_details_scroll = self.task_details_scroll.saturating_add(1); + } + + fn get_task_files_max_mtime(&self) -> Result { + let data_dir = shellexpand::tilde(&self.config.data_location).into_owned(); + ["backlog.data", "completed.data", "pending.data"] + .iter() + .map(|n| fs::metadata(Path::new(&data_dir).join(n)).map(|m| m.modified())) + .filter_map(Result::ok) + .filter_map(Result::ok) + .max() + .ok_or_else(|| anyhow!("Unable to get task files max time")) + } + + pub fn tasks_changed_since(&mut self, prev: Option) -> Result { + if let Some(prev) = prev { + let mtime = self.get_task_files_max_mtime()?; + if mtime > prev { + Ok(true) + } else { + // Unfortunately, we can not use std::time::Instant which is guaranteed to be monotonic, + // because we need to compare it to a file mtime as SystemTime, so as a safety for unexpected + // time shifts, cap maximum wait to 1 min + let now = SystemTime::now(); + let max_delta = Duration::from_secs(60); + Ok(now.duration_since(prev)? > max_delta) + } + } else { + Ok(true) } - - fn task_by_index(&self, i: usize) -> Option { - let tasks = &self.tasks; - if i >= tasks.len() { - None - } else { - Some(tasks[i].clone()) - } + } + + pub fn export_all_tasks(&mut self) -> Result<()> { + let mut task = std::process::Command::new("task"); + + task + .arg("rc.json.array=on") + .arg("rc.confirmation=off") + .arg("rc.json.depends.array=on") + .arg("rc.color=off") + .arg("rc._forcecolor=off"); + // .arg("rc.verbose:override=false"); + + task.arg("export"); + + task.arg("all"); + + info!("Running `{:?}`", task); + let output = task.output()?; + let data = String::from_utf8_lossy(&output.stdout); + let error = String::from_utf8_lossy(&output.stderr); + + if output.status.success() { + if let Ok(imported) = import(data.as_bytes()) { + self.all_tasks = imported; + info!("Imported {} tasks", self.tasks.len()); + self.error = None; + if self.mode == Mode::Tasks(Action::Error) { + self.mode = self.previous_mode.clone().unwrap_or(Mode::Tasks(Action::Report)); + self.previous_mode = None; + } + } else { + self.error = Some(format!("Unable to parse output of `{:?}`:\n`{:?}`", task, data)); + self.mode = Mode::Tasks(Action::Error); + debug!("Unable to parse output: {:?}", data); + } + } else { + self.error = Some(format!("Cannot run `{:?}` - ({}) error:\n{}", &task, output.status, error)); } - fn task_by_uuid(&self, uuid: Uuid) -> Option { - let tasks = &self.tasks; - let m = tasks.iter().find(|t| *t.uuid() == uuid); - m.cloned() + Ok(()) + } + + pub fn export_tasks(&mut self) -> Result<()> { + let mut task = std::process::Command::new("task"); + + task + .arg("rc.json.array=on") + .arg("rc.confirmation=off") + .arg("rc.json.depends.array=on") + .arg("rc.color=off") + .arg("rc._forcecolor=off"); + // .arg("rc.verbose:override=false"); + + if let Some(args) = shlex::split(format!(r#"rc.report.{}.filter='{}'"#, self.report, self.filter.trim()).trim()) { + for arg in args { + task.arg(arg); + } } - fn task_by_id(&self, id: u64) -> Option { - let tasks = &self.tasks; - let m = tasks.iter().find(|t| t.id() == Some(id)); - m.cloned() + if !self.current_context_filter.trim().is_empty() && self.task_version >= *TASKWARRIOR_VERSION_SUPPORTED { + if let Some(args) = shlex::split(&self.current_context_filter) { + for arg in args { + task.arg(arg); + } + } + } else if !self.current_context_filter.trim().is_empty() { + task.arg(format!("'\\({}\\)'", self.current_context_filter)); } - fn task_index_by_id(&self, id: u64) -> Option { - let tasks = &self.tasks; - let m = tasks.iter().position(|t| t.id() == Some(id)); - m + task.arg("export"); + + if self.task_version >= *TASKWARRIOR_VERSION_SUPPORTED { + task.arg(&self.report); } - fn task_index_by_uuid(&self, uuid: Uuid) -> Option { - let tasks = &self.tasks; - let m = tasks.iter().position(|t| *t.uuid() == uuid); - m + info!("Running `{:?}`", task); + let output = task.output()?; + let data = String::from_utf8_lossy(&output.stdout); + let error = String::from_utf8_lossy(&output.stderr); + + if output.status.success() { + if let Ok(imported) = import(data.as_bytes()) { + self.tasks = imported; + info!("Imported {} tasks", self.tasks.len()); + self.error = None; + if self.mode == Mode::Tasks(Action::Error) { + self.mode = self.previous_mode.clone().unwrap_or(Mode::Tasks(Action::Report)); + self.previous_mode = None; + } + } else { + self.error = Some(format!("Unable to parse output of `{:?}`:\n`{:?}`", task, data)); + self.mode = Mode::Tasks(Action::Error); + debug!("Unable to parse output: {:?}", data); + } + } else { + self.error = Some(format!("Cannot run `{:?}` - ({}) error:\n{}", &task, output.status, error)); } - fn style_for_task(&self, task: &Task) -> Style { - let virtual_tag_names_in_precedence = &self.config.rule_precedence_color; + Ok(()) + } - let mut style = Style::default(); + pub fn selected_task_uuids(&self) -> Vec { + let selected = match self.task_table_state.mode() { + TableMode::SingleSelection => vec![self.current_selection], + TableMode::MultipleSelection => self.task_table_state.marked().copied().collect::>(), + }; - for tag_name in virtual_tag_names_in_precedence.iter().rev() { - if tag_name == "uda." || tag_name == "priority" { - if let Some(p) = task.priority() { - let s = self - .config - .color - .get(&format!("color.uda.priority.{}", p)) - .copied() - .unwrap_or_default(); - style = style.patch(s); - } - } else if tag_name == "tag." { - if let Some(tags) = task.tags() { - for t in tags { - let color_tag_name = format!("color.tag.{}", t); - let s = self.config.color.get(&color_tag_name).copied().unwrap_or_default(); - style = style.patch(s); - } - } - } else if tag_name == "project." { - if let Some(p) = task.project() { - let s = self - .config - .color - .get(&format!("color.project.{}", p)) - .copied() - .unwrap_or_default(); - style = style.patch(s); - } - } else if task - .tags() - .unwrap_or(&vec![]) - .contains(&tag_name.to_string().replace('.', "").to_uppercase()) - { - let color_tag_name = format!("color.{}", tag_name); - let s = self.config.color.get(&color_tag_name).copied().unwrap_or_default(); - style = style.patch(s); - } - } + let mut task_uuids = vec![]; - style + for s in selected { + if self.tasks.is_empty() { + break; + } + let task_id = self.tasks[s].id().unwrap_or_default(); + let task_uuid = *self.tasks[s].uuid(); + task_uuids.push(task_uuid); } - pub fn calculate_widths(&self, tasks: &[Vec], headers: &[String], maximum_column_width: u16) -> Vec { - // naive implementation of calculate widths - let mut widths = headers.iter().map(String::len).collect::>(); + task_uuids + } - for row in tasks.iter() { - for (i, cell) in row.iter().enumerate() { - widths[i] = std::cmp::max(cell.len(), widths[i]); - } - } + pub fn task_subprocess(&mut self) -> Result<(), String> { + let task_uuids = if self.tasks.is_empty() { vec![] } else { self.selected_task_uuids() }; - for (i, header) in headers.iter().enumerate() { - if header == "Description" || header == "Definition" { - // always give description or definition the most room to breath - widths[i] = maximum_column_width as usize; - break; - } - } - for (i, header) in headers.iter().enumerate() { + let shell = self.command.as_str(); + + let r = match shlex::split(shell) { + Some(cmd) => { + if cmd.is_empty() { + Err(format!("Shell command empty: {}", shell)) + } else { + // first argument must be a binary + let mut command = std::process::Command::new(&cmd[0]); + // remaining arguments are args + for (i, s) in cmd.iter().enumerate() { if i == 0 { - // always give ID a couple of extra for indicator - widths[i] += self.config.uda_selection_indicator.as_str().width(); - // if let TableMode::MultipleSelection = self.task_table_state.mode() { - // widths[i] += 2 - // }; + continue; } - } - - // now start trimming - while (widths.iter().sum::() as u16) >= maximum_column_width - (headers.len()) as u16 { - let index = widths - .iter() - .position(|i| i == widths.iter().max().unwrap_or(&0)) - .unwrap_or_default(); - if widths[index] == 1 { - break; + command.arg(s); + } + let output = command.output(); + match output { + Ok(o) => { + let output = String::from_utf8_lossy(&o.stdout); + if !output.is_empty() { + Err(format!("Shell command `{}` ran successfully but printed the following output:\n\n{}\n\nSuppress output of shell commands to prevent the error prompt from showing up.", shell, output)) + } else { + Ok(()) + } } - widths[index] -= 1; + Err(_) => Err(format!("Shell command `{}` exited with non-zero output", shell)), + } } + } + None => Err(format!("Cannot run subprocess. Unable to shlex split `{}`", shell)), + }; - widths + if task_uuids.len() == 1 { + if let Some(uuid) = task_uuids.get(0) { + self.current_selection_uuid = Some(*uuid); + } } - fn draw_task_report(&mut self, f: &mut Frame, rect: Rect) { - let (tasks, headers) = self.get_task_report(); + r + } - if tasks.is_empty() { - if !self.current_context.is_empty() { - let context_style = Style::default(); - context_style.add_modifier(Modifier::ITALIC); - } + pub fn task_log(&mut self) -> Result<(), String> { + let mut command = std::process::Command::new("task"); - f.render_widget(Block::default(), rect); - return; - } + command.arg("log"); - let maximum_column_width = rect.width; - let widths = self.calculate_widths(&tasks, &headers, maximum_column_width); + let shell = self.command.as_str(); - for (i, header) in headers.iter().enumerate() { - if header == "Description" || header == "Definition" { - self.task_report_table.description_width = widths[i] - 1; - break; - } - } - let selected = self.current_selection; - let header = headers.iter(); - let mut rows = vec![]; - let mut highlight_style = Style::default(); - let mut pos = 0; - for (i, task) in tasks.iter().enumerate() { - let style = self.style_for_task(&self.tasks[i]); - if i == selected { - pos = i; - highlight_style = style.patch(self.config.uda_style_report_selection); - if self.config.uda_selection_bold { - highlight_style = highlight_style.add_modifier(Modifier::BOLD); - } - if self.config.uda_selection_italic { - highlight_style = highlight_style.add_modifier(Modifier::ITALIC); - } - if self.config.uda_selection_dim { - highlight_style = highlight_style.add_modifier(Modifier::DIM); - } - if self.config.uda_selection_blink { - highlight_style = highlight_style.add_modifier(Modifier::SLOW_BLINK); - } - if self.config.uda_selection_reverse { - highlight_style = highlight_style.add_modifier(Modifier::REVERSED); - } - } - rows.push(Row::StyledData(task.iter(), style)); + match shlex::split(shell) { + Some(cmd) => { + for s in cmd { + command.arg(&s); } - - let constraints: Vec = widths - .iter() - .map(|i| Constraint::Length((*i).try_into().unwrap_or(maximum_column_width as u16))) - .collect(); - - let t = Table::new(header, rows.into_iter()) - .header_style( - self.config - .color - .get("color.label") - .copied() - .unwrap_or_default() - .add_modifier(Modifier::UNDERLINED), - ) - .highlight_style(highlight_style) - .highlight_symbol(&self.config.uda_selection_indicator) - .mark_symbol(&self.config.uda_mark_indicator) - .unmark_symbol(&self.config.uda_unmark_indicator) - .widths(&constraints); - - f.render_stateful_widget(t, rect, &mut self.task_table_state); - if tasks.iter().len() as u16 > rect.height.saturating_sub(4) { - let mut widget = Scrollbar::new(pos, tasks.iter().len()); - widget.pos_style = self.config.uda_style_report_scrollbar; - widget.pos_symbol = self.config.uda_scrollbar_indicator.clone(); - widget.area_style = self.config.uda_style_report_scrollbar_area; - widget.area_symbol = self.config.uda_scrollbar_area.clone(); - f.render_widget(widget, rect); + let output = command.output(); + match output { + Ok(_) => Ok(()), + Err(_) => Err(format!("Cannot run `task log {}`. Check documentation for more information", shell)), } + } + None => Err(format!("Unable to run `{:?}`: shlex::split(`{}`) failed.", command, shell)), } + } - fn get_all_contexts(&self) -> (Vec>, Vec) { - let contexts = self - .contexts - .rows - .iter() - .filter(|c| &c.type_ == "read") - .map(|c| vec![c.name.clone(), c.definition.clone(), c.active.clone()]) - .collect(); - let headers = vec!["Name".to_string(), "Definition".to_string(), "Active".to_string()]; - (contexts, headers) - } - - fn get_task_report(&mut self) -> (Vec>, Vec) { - self.task_report_table.generate_table(&self.tasks); - let (tasks, headers) = self.task_report_table.simplify_table(); - (tasks, headers) - } - - pub async fn update(&mut self, force: bool) -> Result<()> { - trace!("self.update({:?});", force); - if force || self.dirty || self.tasks_changed_since(self.last_export).unwrap_or(true) { - self.get_context()?; - let task_uuids = self.selected_task_uuids(); - if self.current_selection_uuid.is_none() && self.current_selection_id.is_none() && task_uuids.len() == 1 { - if let Some(uuid) = task_uuids.get(0) { - self.current_selection_uuid = Some(*uuid); - } + pub fn task_background(&mut self) { + let shell = self.config.uda_background_process.clone(); + if shell.is_empty() { + return; + } + let shell = shellexpand::tilde(&shell).into_owned(); + let period = self.config.uda_background_process_period; + std::thread::spawn(move || loop { + std::thread::sleep(Duration::from_secs(period as u64)); + match shlex::split(&shell) { + Some(cmd) => { + let mut command = std::process::Command::new(&cmd[0]); + for s in cmd.iter().skip(1) { + command.arg(s); + } + if let Ok(output) = command.output() { + if !output.status.success() { + break; } - - self.last_export = Some(std::time::SystemTime::now()); - self.task_report_table.export_headers(None, &self.report)?; - self.export_tasks()?; - if self.config.uda_task_report_use_all_tasks_for_completion { - self.export_all_tasks()?; - } - self.contexts.update_data()?; - self.projects.update_data()?; - self.update_tags(); - self.task_details.clear(); - self.dirty = false; - self.save_history()?; - } - self.cursor_fix(); - self.update_task_table_state(); - if self.task_report_show_info { - self.update_task_details().await?; - } - self.selection_fix(); - - Ok(()) - } - - pub fn selection_fix(&mut self) { - if let (Some(t), Some(id)) = (self.task_current(), self.current_selection_id) { - if t.id() != Some(id) { - if let Some(i) = self.task_index_by_id(id) { - self.current_selection = i; - self.current_selection_id = None; - } - } - } - - if let (Some(t), Some(uuid)) = (self.task_current(), self.current_selection_uuid) { - if t.uuid() != &uuid { - if let Some(i) = self.task_index_by_uuid(uuid) { - self.current_selection = i; - self.current_selection_uuid = None; - } - } - } - } - - pub fn save_history(&mut self) -> Result<()> { - self.filter_history.write()?; - self.command_history.write()?; - Ok(()) - } - - pub fn cursor_fix(&mut self) { - while !self.tasks.is_empty() && self.current_selection >= self.tasks.len() { - self.task_report_previous(); - } - } - - pub async fn update_task_details(&mut self) -> Result<()> { - if self.tasks.is_empty() { - return Ok(()); - } - - // remove task_details of tasks not in task report - let mut to_delete = vec![]; - for k in self.task_details.keys() { - if !self.tasks.iter().map(Task::uuid).any(|x| x == k) { - to_delete.push(*k); - } - } - for k in to_delete { - self.task_details.remove(&k); - } - - let selected = self.current_selection; - if selected >= self.tasks.len() { - return Ok(()); + } else { + break; + } } - let current_task_uuid = *self.tasks[selected].uuid(); + None => break, + }; + }); + } - let mut l = vec![selected]; + pub async fn task_shortcut(&mut self, s: usize) -> Result<(), String> { + self.pause_tui().await.unwrap(); - for s in 1..=self.config.uda_task_detail_prefetch { - l.insert(0, std::cmp::min(selected.saturating_sub(s), self.tasks.len() - 1)); - l.push(std::cmp::min(selected + s, self.tasks.len() - 1)); - } + let task_uuids = if self.tasks.is_empty() { vec![] } else { self.selected_task_uuids() }; - l.dedup(); + let shell = &self.config.uda_shortcuts[s]; - let (tx, mut rx) = tokio::sync::mpsc::channel(100); - let tasks = self.tasks.clone(); - let defaultwidth = self.terminal_width.saturating_sub(2); - for s in &l { - if tasks.is_empty() { - return Ok(()); - } - if s >= &tasks.len() { - break; - } - let task_uuid = *tasks[*s].uuid(); - if !self.task_details.contains_key(&task_uuid) || task_uuid == current_task_uuid { - debug!("Running task details for {}", task_uuid); - let _tx = tx.clone(); - tokio::spawn(async move { - let output = tokio::process::Command::new("task") - .arg("rc.color=off") - .arg("rc._forcecolor=off") - .arg(format!("rc.defaultwidth={}", defaultwidth)) - .arg(format!("{}", task_uuid)) - .output() - .await; - if let Ok(output) = output { - let data = String::from_utf8_lossy(&output.stdout).to_string(); - _tx.send(Some((task_uuid, data))).await.unwrap(); - } - }); - } - } - drop(tx); - while let Some(Some((task_uuid, data))) = rx.recv().await { - self.task_details.insert(task_uuid, data); - } - Ok(()) + if shell.is_empty() { + return Err("Trying to run empty shortcut.".to_string()); } - - pub fn update_task_table_state(&mut self) { - trace!("self.update_task_table_state()"); - self.task_table_state.select(Some(self.current_selection)); - - for uuid in self.marked.clone() { - if self.task_by_uuid(uuid).is_none() { - self.marked.remove(&uuid); + let shell = format!( + "{} {}", + shell, + task_uuids.iter().map(ToString::to_string).collect::>().join(" ") + ); + + let shell = shellexpand::tilde(&shell).into_owned(); + let r = match shlex::split(&shell) { + Some(cmd) => { + let mut command = std::process::Command::new(&cmd[0]); + for s in cmd.iter().skip(1) { + command.arg(s); + } + if let Ok(child) = command.spawn() { + let output = child.wait_with_output(); + match output { + Ok(o) => { + if o.status.success() { + Ok(()) + } else { + Err(format!( + "Unable to run shortcut {}. Status Code: {} - stdout: {} stderr: {}", + s, + o.status.code().unwrap_or_default(), + String::from_utf8_lossy(&o.stdout), + String::from_utf8_lossy(&o.stderr), + )) + } } + Err(s) => Err(format!("`{}` failed: {}", shell, s)), + } + } else { + Err(format!("`{}` failed: {}", shell, s)) } + } + None => Err(format!("Unable to run shortcut `{}`: shlex::split(`{}`) failed.", s, shell)), + }; - if self.marked.is_empty() { - self.task_table_state.single_selection(); - } - - self.task_table_state.clear(); - - for uuid in &self.marked { - self.task_table_state.mark(self.task_index_by_uuid(*uuid)); - } + if task_uuids.len() == 1 { + if let Some(uuid) = task_uuids.get(0) { + self.current_selection_uuid = Some(*uuid); + } } - pub fn context_next(&mut self) { - let i = match self.contexts.table_state.current_selection() { - Some(i) => { - if i >= self.contexts.len() - 1 { - 0 - } else { - i + 1 - } - } - None => 0, - }; - self.contexts.table_state.select(Some(i)); - } + self.resume_tui().await.unwrap(); + r + } - pub fn context_previous(&mut self) { - let i = match self.contexts.table_state.current_selection() { - Some(i) => { - if i == 0 { - self.contexts.len() - 1 - } else { - i - 1 - } - } - None => 0, - }; - self.contexts.table_state.select(Some(i)); + pub fn task_modify(&mut self) -> Result<(), String> { + if self.tasks.is_empty() { + return Ok(()); } - pub fn context_select(&mut self) -> Result<()> { - let i = self.contexts.table_state.current_selection().unwrap_or_default(); - let mut command = std::process::Command::new("task"); - command.arg("context").arg(&self.contexts.rows[i].name); - command.output()?; - Ok(()) - } - - pub fn task_report_top(&mut self) { - if self.tasks.is_empty() { - return; - } - self.current_selection = 0; - self.current_selection_id = None; - self.current_selection_uuid = None; - } + let task_uuids = self.selected_task_uuids(); - pub fn task_report_bottom(&mut self) { - if self.tasks.is_empty() { - return; - } - self.current_selection = self.tasks.len() - 1; - self.current_selection_id = None; - self.current_selection_uuid = None; + let mut command = std::process::Command::new("task"); + command.arg("rc.bulk=0"); + command.arg("rc.confirmation=off"); + command.arg("rc.dependency.confirmation=off"); + command.arg("rc.recurrence.confirmation=off"); + for task_uuid in &task_uuids { + command.arg(task_uuid.to_string()); } + command.arg("modify"); - pub fn task_report_next(&mut self) { - if self.tasks.is_empty() { - return; - } - let i = { - if self.current_selection >= self.tasks.len() - 1 { - if self.config.uda_task_report_looping { - 0 - } else { - self.current_selection - } - } else { - self.current_selection + 1 - } - }; - self.current_selection = i; - self.current_selection_id = None; - self.current_selection_uuid = None; - } + let shell = self.modify.as_str(); - pub fn task_report_previous(&mut self) { - if self.tasks.is_empty() { - return; + let r = match shlex::split(shell) { + Some(cmd) => { + for s in cmd { + command.arg(&s); } - let i = { - if self.current_selection == 0 { - if self.config.uda_task_report_looping { - self.tasks.len() - 1 - } else { - 0 - } + let output = command.output(); + match output { + Ok(o) => { + if o.status.success() { + Ok(()) } else { - self.current_selection - 1 + Err(format!("Modify failed. {}", String::from_utf8_lossy(&o.stdout))) } - }; - self.current_selection = i; - self.current_selection_id = None; - self.current_selection_uuid = None; - } + } + Err(_) => Err(format!( + "Cannot run `task {:?} modify {}`. Check documentation for more information", + task_uuids, shell, + )), + } + } + None => Err(format!("Cannot shlex split `{}`", shell)), + }; - pub fn task_report_next_page(&mut self) { - if self.tasks.is_empty() { - return; - } - let i = { - if self.current_selection == self.tasks.len() - 1 { - if self.config.uda_task_report_looping { - 0 - } else { - self.tasks.len() - 1 - } - } else { - std::cmp::min( - self.current_selection - .checked_add(self.task_report_height as usize) - .unwrap_or(self.tasks.len() - 1), - self.tasks.len() - 1, - ) - } - }; - self.current_selection = i; - self.current_selection_id = None; - self.current_selection_uuid = None; + if task_uuids.len() == 1 { + if let Some(uuid) = task_uuids.get(0) { + self.current_selection_uuid = Some(*uuid); + } } - pub fn task_report_previous_page(&mut self) { - if self.tasks.is_empty() { - return; - } - let i = { - if self.current_selection == 0 { - if self.config.uda_task_report_looping { - self.tasks.len() - 1 - } else { - 0 - } - } else { - self.current_selection.saturating_sub(self.task_report_height as usize) - } - }; - self.current_selection = i; - self.current_selection_id = None; - self.current_selection_uuid = None; - } + r + } - pub fn task_report_jump(&mut self) -> Result<()> { - if self.tasks.is_empty() { - return Ok(()); - } - let i = self.command.as_str().parse::()?; - if let Some(task) = self.task_by_id(i as u64) { - let j = self.task_index_by_uuid(*task.uuid()).unwrap_or_default(); - self.current_selection = j; - self.current_selection_id = None; - self.current_selection_uuid = None; - Ok(()) - } else { - Err(anyhow!("Cannot locate task id {} in report", i)) - } + pub fn task_annotate(&mut self) -> Result<(), String> { + if self.tasks.is_empty() { + return Ok(()); } - fn get_task_files_max_mtime(&self) -> Result { - let data_dir = shellexpand::tilde(&self.config.data_location).into_owned(); - ["backlog.data", "completed.data", "pending.data"] - .iter() - .map(|n| fs::metadata(Path::new(&data_dir).join(n)).map(|m| m.modified())) - .filter_map(Result::ok) - .filter_map(Result::ok) - .max() - .ok_or_else(|| anyhow!("Unable to get task files max time")) - } + let task_uuids = self.selected_task_uuids(); - pub fn tasks_changed_since(&mut self, prev: Option) -> Result { - if let Some(prev) = prev { - let mtime = self.get_task_files_max_mtime()?; - if mtime > prev { - Ok(true) - } else { - // Unfortunately, we can not use std::time::Instant which is guaranteed to be monotonic, - // because we need to compare it to a file mtime as SystemTime, so as a safety for unexpected - // time shifts, cap maximum wait to 1 min - let now = SystemTime::now(); - let max_delta = Duration::from_secs(60); - Ok(now.duration_since(prev)? > max_delta) - } - } else { - Ok(true) - } + let mut command = std::process::Command::new("task"); + command.arg("rc.bulk=0"); + command.arg("rc.confirmation=off"); + command.arg("rc.dependency.confirmation=off"); + command.arg("rc.recurrence.confirmation=off"); + for task_uuid in &task_uuids { + command.arg(task_uuid.to_string()); } + command.arg("annotate"); - pub fn export_all_tasks(&mut self) -> Result<()> { - let mut task = std::process::Command::new("task"); - - task.arg("rc.json.array=on") - .arg("rc.confirmation=off") - .arg("rc.json.depends.array=on") - .arg("rc.color=off") - .arg("rc._forcecolor=off"); - // .arg("rc.verbose:override=false"); - - task.arg("export"); + let shell = self.command.as_str(); - task.arg("all"); - - info!("Running `{:?}`", task); - let output = task.output()?; - let data = String::from_utf8_lossy(&output.stdout); - let error = String::from_utf8_lossy(&output.stderr); - - if output.status.success() { - if let Ok(imported) = import(data.as_bytes()) { - self.all_tasks = imported; - info!("Imported {} tasks", self.tasks.len()); - self.error = None; - if self.mode == Mode::Tasks(Action::Error) { - self.mode = self.previous_mode.clone().unwrap_or(Mode::Tasks(Action::Report)); - self.previous_mode = None; - } + let r = match shlex::split(shell) { + Some(cmd) => { + for s in cmd { + command.arg(&s); + } + let output = command.output(); + match output { + Ok(o) => { + if o.status.success() { + Ok(()) } else { - self.error = Some(format!("Unable to parse output of `{:?}`:\n`{:?}`", task, data)); - self.mode = Mode::Tasks(Action::Error); - debug!("Unable to parse output: {:?}", data); + Err(format!("Annotate failed. {}", String::from_utf8_lossy(&o.stdout))) } - } else { - self.error = Some(format!( - "Cannot run `{:?}` - ({}) error:\n{}", - &task, output.status, error - )); - } + } + Err(_) => Err(format!( + "Cannot run `task {} annotate {}`. Check documentation for more information", + task_uuids.iter().map(ToString::to_string).collect::>().join(" "), + shell + )), + } + } + None => Err(format!("Cannot shlex split `{}`", shell)), + }; - Ok(()) + if task_uuids.len() == 1 { + if let Some(uuid) = task_uuids.get(0) { + self.current_selection_uuid = Some(*uuid); + } } + r + } - pub fn export_tasks(&mut self) -> Result<()> { - let mut task = std::process::Command::new("task"); - - task.arg("rc.json.array=on") - .arg("rc.confirmation=off") - .arg("rc.json.depends.array=on") - .arg("rc.color=off") - .arg("rc._forcecolor=off"); - // .arg("rc.verbose:override=false"); + pub fn task_add(&mut self) -> Result<(), String> { + let mut command = std::process::Command::new("task"); + command.arg("add"); - if let Some(args) = shlex::split(format!(r#"rc.report.{}.filter='{}'"#, self.report, self.filter.trim()).trim()) - { - for arg in args { - task.arg(arg); - } - } + let shell = self.command.as_str(); - if !self.current_context_filter.trim().is_empty() && self.task_version >= *TASKWARRIOR_VERSION_SUPPORTED { - if let Some(args) = shlex::split(&self.current_context_filter) { - for arg in args { - task.arg(arg); - } - } - } else if !self.current_context_filter.trim().is_empty() { - task.arg(format!("'\\({}\\)'", self.current_context_filter)); - } - - task.arg("export"); - - if self.task_version >= *TASKWARRIOR_VERSION_SUPPORTED { - task.arg(&self.report); + match shlex::split(shell) { + Some(cmd) => { + for s in cmd { + command.arg(&s); } - - info!("Running `{:?}`", task); - let output = task.output()?; - let data = String::from_utf8_lossy(&output.stdout); - let error = String::from_utf8_lossy(&output.stderr); - - if output.status.success() { - if let Ok(imported) = import(data.as_bytes()) { - self.tasks = imported; - info!("Imported {} tasks", self.tasks.len()); - self.error = None; - if self.mode == Mode::Tasks(Action::Error) { - self.mode = self.previous_mode.clone().unwrap_or(Mode::Tasks(Action::Report)); - self.previous_mode = None; + let output = command.output(); + match output { + Ok(output) => { + if output.status.code() == Some(0) { + let data = String::from_utf8_lossy(&output.stdout); + let re = Regex::new(r"^Created task (?P\d+).\n$").unwrap(); + if self.config.uda_task_report_jump_to_task_on_add { + if let Some(caps) = re.captures(&data) { + self.current_selection_id = Some(caps["task_id"].parse::().unwrap_or_default()); } + } + Ok(()) } else { - self.error = Some(format!("Unable to parse output of `{:?}`:\n`{:?}`", task, data)); - self.mode = Mode::Tasks(Action::Error); - debug!("Unable to parse output: {:?}", data); + Err(format!("Error: {}", String::from_utf8_lossy(&output.stderr))) } - } else { - self.error = Some(format!( - "Cannot run `{:?}` - ({}) error:\n{}", - &task, output.status, error - )); + } + Err(e) => Err(format!("Cannot run `{:?}`: {}", command, e)), } - - Ok(()) + } + None => Err(format!("Unable to run `{:?}`: shlex::split(`{}`) failed.", command, shell)), } + } - pub fn selected_task_uuids(&self) -> Vec { - let selected = match self.task_table_state.mode() { - TableMode::SingleSelection => vec![self.current_selection], - TableMode::MultipleSelection => self.task_table_state.marked().copied().collect::>(), - }; + pub fn task_virtual_tags(task_uuid: Uuid) -> Result { + let output = std::process::Command::new("task").arg(format!("{}", task_uuid)).output(); - let mut task_uuids = vec![]; - - for s in selected { - if self.tasks.is_empty() { - break; + match output { + Ok(output) => { + let data = String::from_utf8_lossy(&output.stdout); + for line in data.split('\n') { + for prefix in &["Virtual tags", "Virtual"] { + if line.starts_with(prefix) { + let line = line.to_string(); + let line = line.replace(prefix, ""); + return Ok(line); } - let task_id = self.tasks[s].id().unwrap_or_default(); - let task_uuid = *self.tasks[s].uuid(); - task_uuids.push(task_uuid); - } - - task_uuids + } + } + Err(format!( + "Cannot find any tags for `task {}`. Check documentation for more information", + task_uuid + )) + } + Err(_) => Err(format!("Cannot run `task {}`. Check documentation for more information", task_uuid)), } + } - pub fn task_subprocess(&mut self) -> Result<(), String> { - let task_uuids = if self.tasks.is_empty() { - vec![] - } else { - self.selected_task_uuids() - }; - - let shell = self.command.as_str(); - - let r = match shlex::split(shell) { - Some(cmd) => { - if cmd.is_empty() { - Err(format!("Shell command empty: {}", shell)) - } else { - // first argument must be a binary - let mut command = std::process::Command::new(&cmd[0]); - // remaining arguments are args - for (i, s) in cmd.iter().enumerate() { - if i == 0 { - continue; - } - command.arg(s); - } - let output = command.output(); - match output { - Ok(o) => { - let output = String::from_utf8_lossy(&o.stdout); - if !output.is_empty() { - Err(format!("Shell command `{}` ran successfully but printed the following output:\n\n{}\n\nSuppress output of shell commands to prevent the error prompt from showing up.", shell, output)) - } else { - Ok(()) - } - } - Err(_) => Err(format!("Shell command `{}` exited with non-zero output", shell)), - } - } - } - None => Err(format!("Cannot run subprocess. Unable to shlex split `{}`", shell)), - }; - - if task_uuids.len() == 1 { - if let Some(uuid) = task_uuids.get(0) { - self.current_selection_uuid = Some(*uuid); - } - } - - r + pub fn task_start_stop(&mut self) -> Result<(), String> { + if self.tasks.is_empty() { + return Ok(()); } - pub fn task_log(&mut self) -> Result<(), String> { - let mut command = std::process::Command::new("task"); - - command.arg("log"); + let task_uuids = self.selected_task_uuids(); - let shell = self.command.as_str(); - - match shlex::split(shell) { - Some(cmd) => { - for s in cmd { - command.arg(&s); - } - let output = command.output(); - match output { - Ok(_) => Ok(()), - Err(_) => Err(format!( - "Cannot run `task log {}`. Check documentation for more information", - shell - )), - } - } - None => Err(format!( - "Unable to run `{:?}`: shlex::split(`{}`) failed.", - command, shell - )), + for task_uuid in &task_uuids { + let mut command = "start"; + for tag in TaskwarriorTui::task_virtual_tags(*task_uuid).unwrap_or_default().split(' ') { + if tag == "ACTIVE" { + command = "stop"; } + } + + let output = std::process::Command::new("task").arg(task_uuid.to_string()).arg(command).output(); + if output.is_err() { + return Err(format!("Error running `task {}` for task `{}`.", command, task_uuid)); + } } - pub fn task_background(&mut self) { - let shell = self.config.uda_background_process.clone(); - if shell.is_empty() { - return; - } - let shell = shellexpand::tilde(&shell).into_owned(); - let period = self.config.uda_background_process_period; - std::thread::spawn(move || loop { - std::thread::sleep(Duration::from_secs(period as u64)); - match shlex::split(&shell) { - Some(cmd) => { - let mut command = std::process::Command::new(&cmd[0]); - for s in cmd.iter().skip(1) { - command.arg(s); - } - if let Ok(output) = command.output() { - if !output.status.success() { - break; - } - } else { - break; - } - } - None => break, - }; - }); + if task_uuids.len() == 1 { + if let Some(uuid) = task_uuids.get(0) { + self.current_selection_uuid = Some(*uuid); + } } - pub async fn task_shortcut(&mut self, s: usize) -> Result<(), String> { - self.pause_tui().await.unwrap(); + Ok(()) + } - let task_uuids = if self.tasks.is_empty() { - vec![] - } else { - self.selected_task_uuids() - }; + pub fn task_quick_tag(&mut self) -> Result<(), String> { + let tag_name = &self.config.uda_quick_tag_name; + let ptag_name = format!("+{}", tag_name); + let ntag_name = format!("-{}", tag_name); + if self.tasks.is_empty() { + return Ok(()); + } - let shell = &self.config.uda_shortcuts[s]; + let task_uuids = self.selected_task_uuids(); - if shell.is_empty() { - return Err("Trying to run empty shortcut.".to_string()); + for task_uuid in &task_uuids { + if let Some(task) = self.task_by_uuid(*task_uuid) { + let mut tag_to_set = &ptag_name; + for tag in task.tags().unwrap() { + if tag == tag_name { + tag_to_set = &ntag_name; + } } - let shell = format!( - "{} {}", - shell, - task_uuids - .iter() - .map(ToString::to_string) - .collect::>() - .join(" ") - ); - let shell = shellexpand::tilde(&shell).into_owned(); - let r = match shlex::split(&shell) { - Some(cmd) => { - let mut command = std::process::Command::new(&cmd[0]); - for s in cmd.iter().skip(1) { - command.arg(s); - } - if let Ok(child) = command.spawn() { - let output = child.wait_with_output(); - match output { - Ok(o) => { - if o.status.success() { - Ok(()) - } else { - Err(format!( - "Unable to run shortcut {}. Status Code: {} - stdout: {} stderr: {}", - s, - o.status.code().unwrap_or_default(), - String::from_utf8_lossy(&o.stdout), - String::from_utf8_lossy(&o.stderr), - )) - } - } - Err(s) => Err(format!("`{}` failed: {}", shell, s)), - } - } else { - Err(format!("`{}` failed: {}", shell, s)) - } - } - None => Err(format!( - "Unable to run shortcut `{}`: shlex::split(`{}`) failed.", - s, shell - )), - }; + let output = std::process::Command::new("task") + .arg(task_uuid.to_string()) + .arg("modify") + .arg(tag_to_set) + .output(); - if task_uuids.len() == 1 { - if let Some(uuid) = task_uuids.get(0) { - self.current_selection_uuid = Some(*uuid); - } + if output.is_err() { + return Err(format!("Error running `task modify {}` for task `{}`.", tag_to_set, task_uuid,)); } + } + } - self.resume_tui().await.unwrap(); - r + if task_uuids.len() == 1 { + if let Some(uuid) = task_uuids.get(0) { + self.current_selection_uuid = Some(*uuid); + } } - pub fn task_modify(&mut self) -> Result<(), String> { - if self.tasks.is_empty() { - return Ok(()); - } + Ok(()) + } - let task_uuids = self.selected_task_uuids(); + pub fn task_delete(&mut self) -> Result<(), String> { + if self.tasks.is_empty() { + return Ok(()); + } - let mut command = std::process::Command::new("task"); - command.arg("rc.bulk=0"); - command.arg("rc.confirmation=off"); - command.arg("rc.dependency.confirmation=off"); - command.arg("rc.recurrence.confirmation=off"); - for task_uuid in &task_uuids { - command.arg(task_uuid.to_string()); - } - command.arg("modify"); + let task_uuids = self.selected_task_uuids(); - let shell = self.modify.as_str(); + let mut cmd = std::process::Command::new("task"); + cmd + .arg("rc.bulk=0") + .arg("rc.confirmation=off") + .arg("rc.dependency.confirmation=off") + .arg("rc.recurrence.confirmation=off"); + for task_uuid in &task_uuids { + cmd.arg(task_uuid.to_string()); + } + cmd.arg("delete"); + let output = cmd.output(); + let r = match output { + Ok(_) => Ok(()), + Err(_) => Err(format!( + "Cannot run `task delete` for tasks `{}`. Check documentation for more information", + task_uuids.iter().map(ToString::to_string).collect::>().join(" ") + )), + }; + self.current_selection_uuid = None; + self.current_selection_id = None; + r + } + + pub fn task_done(&mut self) -> Result<(), String> { + if self.tasks.is_empty() { + return Ok(()); + } + let task_uuids = self.selected_task_uuids(); + let mut cmd = std::process::Command::new("task"); + cmd + .arg("rc.bulk=0") + .arg("rc.confirmation=off") + .arg("rc.dependency.confirmation=off") + .arg("rc.recurrence.confirmation=off"); + for task_uuid in &task_uuids { + cmd.arg(task_uuid.to_string()); + } + cmd.arg("done"); + let output = cmd.output(); + let r = match output { + Ok(_) => Ok(()), + Err(_) => Err(format!( + "Cannot run `task done` for task `{}`. Check documentation for more information", + task_uuids.iter().map(ToString::to_string).collect::>().join(" ") + )), + }; + self.current_selection_uuid = None; + self.current_selection_id = None; + r + } - let r = match shlex::split(shell) { - Some(cmd) => { - for s in cmd { - command.arg(&s); - } - let output = command.output(); - match output { - Ok(o) => { - if o.status.success() { - Ok(()) - } else { - Err(format!("Modify failed. {}", String::from_utf8_lossy(&o.stdout))) - } - } - Err(_) => Err(format!( - "Cannot run `task {:?} modify {}`. Check documentation for more information", - task_uuids, shell, - )), - } - } - None => Err(format!("Cannot shlex split `{}`", shell)), - }; + pub fn task_undo(&mut self) -> Result<(), String> { + let output = std::process::Command::new("task").arg("rc.confirmation=off").arg("undo").output(); - if task_uuids.len() == 1 { - if let Some(uuid) = task_uuids.get(0) { - self.current_selection_uuid = Some(*uuid); - } + match output { + Ok(output) => { + let data = String::from_utf8_lossy(&output.stdout); + let re = Regex::new(r"(?P[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})").unwrap(); + if let Some(caps) = re.captures(&data) { + if let Ok(uuid) = Uuid::parse_str(&caps["task_uuid"]) { + self.current_selection_uuid = Some(uuid); + } } - - r + Ok(()) + } + Err(_) => Err("Cannot run `task undo`. Check documentation for more information".to_string()), } + } - pub fn task_annotate(&mut self) -> Result<(), String> { - if self.tasks.is_empty() { - return Ok(()); - } + pub async fn task_edit(&mut self) -> Result<(), String> { + if self.tasks.is_empty() { + return Ok(()); + } - let task_uuids = self.selected_task_uuids(); + self.pause_tui().await.unwrap(); - let mut command = std::process::Command::new("task"); - command.arg("rc.bulk=0"); - command.arg("rc.confirmation=off"); - command.arg("rc.dependency.confirmation=off"); - command.arg("rc.recurrence.confirmation=off"); - for task_uuid in &task_uuids { - command.arg(task_uuid.to_string()); - } - command.arg("annotate"); + let selected = self.current_selection; + let task_id = self.tasks[selected].id().unwrap_or_default(); + let task_uuid = *self.tasks[selected].uuid(); - let shell = self.command.as_str(); + let r = std::process::Command::new("task").arg(format!("{}", task_uuid)).arg("edit").spawn(); - let r = match shlex::split(shell) { - Some(cmd) => { - for s in cmd { - command.arg(&s); - } - let output = command.output(); - match output { - Ok(o) => { - if o.status.success() { - Ok(()) - } else { - Err(format!("Annotate failed. {}", String::from_utf8_lossy(&o.stdout))) - } - } - Err(_) => Err(format!( - "Cannot run `task {} annotate {}`. Check documentation for more information", - task_uuids - .iter() - .map(ToString::to_string) - .collect::>() - .join(" "), - shell - )), - } + let r = match r { + Ok(child) => { + let output = child.wait_with_output(); + match output { + Ok(output) => { + if output.status.success() { + Ok(()) + } else { + Err(format!( + "`task edit` for task `{}` failed. {}{}", + task_uuid, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + )) } - None => Err(format!("Cannot shlex split `{}`", shell)), - }; + } + Err(err) => Err(format!("Cannot run `task edit` for task `{}`. {}", task_uuid, err)), + } + } + _ => Err(format!( + "Cannot start `task edit` for task `{}`. Check documentation for more information", + task_uuid + )), + }; - if task_uuids.len() == 1 { - if let Some(uuid) = task_uuids.get(0) { - self.current_selection_uuid = Some(*uuid); - } - } - r - } + self.current_selection_uuid = Some(task_uuid); - pub fn task_add(&mut self) -> Result<(), String> { - let mut command = std::process::Command::new("task"); - command.arg("add"); + self.resume_tui().await.unwrap(); - let shell = self.command.as_str(); + r + } - match shlex::split(shell) { - Some(cmd) => { - for s in cmd { - command.arg(&s); - } - let output = command.output(); - match output { - Ok(output) => { - if output.status.code() == Some(0) { - let data = String::from_utf8_lossy(&output.stdout); - let re = Regex::new(r"^Created task (?P\d+).\n$").unwrap(); - if self.config.uda_task_report_jump_to_task_on_add { - if let Some(caps) = re.captures(&data) { - self.current_selection_id = - Some(caps["task_id"].parse::().unwrap_or_default()); - } - } - Ok(()) - } else { - Err(format!("Error: {}", String::from_utf8_lossy(&output.stderr))) - } - } - Err(e) => Err(format!("Cannot run `{:?}`: {}", command, e)), - } + pub fn task_current(&self) -> Option { + if self.tasks.is_empty() { + return None; + } + let selected = self.current_selection; + Some(self.tasks[selected].clone()) + } + + pub fn update_tags(&mut self) { + let tasks = &mut self.tasks; + + // dependency scan + for l_i in 0..tasks.len() { + let default_deps = vec![]; + let deps = tasks[l_i].depends().unwrap_or(&default_deps).clone(); + add_tag(&mut tasks[l_i], "UNBLOCKED".to_string()); + for dep in deps { + for r_i in 0..tasks.len() { + if tasks[r_i].uuid() == &dep { + let l_status = tasks[l_i].status(); + let r_status = tasks[r_i].status(); + if l_status != &TaskStatus::Completed + && l_status != &TaskStatus::Deleted + && r_status != &TaskStatus::Completed + && r_status != &TaskStatus::Deleted + { + remove_tag(&mut tasks[l_i], "UNBLOCKED"); + add_tag(&mut tasks[l_i], "BLOCKED".to_string()); + add_tag(&mut tasks[r_i], "BLOCKING".to_string()); } - None => Err(format!( - "Unable to run `{:?}`: shlex::split(`{}`) failed.", - command, shell - )), + break; + } } + } } - pub fn task_virtual_tags(task_uuid: Uuid) -> Result { - let output = std::process::Command::new("task") - .arg(format!("{}", task_uuid)) - .output(); - - match output { - Ok(output) => { - let data = String::from_utf8_lossy(&output.stdout); - for line in data.split('\n') { - for prefix in &["Virtual tags", "Virtual"] { - if line.starts_with(prefix) { - let line = line.to_string(); - let line = line.replace(prefix, ""); - return Ok(line); - } - } - } - Err(format!( - "Cannot find any tags for `task {}`. Check documentation for more information", - task_uuid - )) + // other virtual tags + // TODO: support all virtual tags that taskwarrior supports + for task in tasks.iter_mut() { + match task.status() { + TaskStatus::Waiting => add_tag(task, "WAITING".to_string()), + TaskStatus::Completed => add_tag(task, "COMPLETED".to_string()), + TaskStatus::Pending => add_tag(task, "PENDING".to_string()), + TaskStatus::Deleted => add_tag(task, "DELETED".to_string()), + TaskStatus::Recurring => (), + } + if task.start().is_some() { + add_tag(task, "ACTIVE".to_string()); + } + if task.scheduled().is_some() { + add_tag(task, "SCHEDULED".to_string()); + } + if task.parent().is_some() { + add_tag(task, "INSTANCE".to_string()); + } + if task.until().is_some() { + add_tag(task, "UNTIL".to_string()); + } + if task.annotations().is_some() { + add_tag(task, "ANNOTATED".to_string()); + } + let virtual_tags = self.task_report_table.virtual_tags.clone(); + if task.tags().is_some() && task.tags().unwrap().iter().any(|s| !virtual_tags.contains(s)) { + add_tag(task, "TAGGED".to_string()); + } + if !task.uda().is_empty() { + add_tag(task, "UDA".to_string()); + } + if task.mask().is_some() { + add_tag(task, "TEMPLATE".to_string()); + } + if task.project().is_some() { + add_tag(task, "PROJECT".to_string()); + } + if task.priority().is_some() { + add_tag(task, "PRIORITY".to_string()); + } + if task.recur().is_some() { + add_tag(task, "RECURRING".to_string()); + let r = task.recur().unwrap(); + } + if let Some(d) = task.due() { + let status = task.status(); + // due today + if status != &TaskStatus::Completed && status != &TaskStatus::Deleted { + let now = Local::now(); + let reference = TimeZone::from_utc_datetime(now.offset(), d); + let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc()); + let d = d.clone(); + if (reference - chrono::Duration::nanoseconds(1)).month() == now.month() { + add_tag(task, "MONTH".to_string()); + } + if (reference - chrono::Duration::nanoseconds(1)).month() % 4 == now.month() % 4 { + add_tag(task, "QUARTER".to_string()); + } + if reference.year() == now.year() { + add_tag(task, "YEAR".to_string()); + } + match get_date_state(&d, self.config.due) { + DateState::EarlierToday | DateState::LaterToday => { + add_tag(task, "DUE".to_string()); + add_tag(task, "TODAY".to_string()); + add_tag(task, "DUETODAY".to_string()); } - Err(_) => Err(format!( - "Cannot run `task {}`. Check documentation for more information", - task_uuid - )), - } + DateState::AfterToday => { + add_tag(task, "DUE".to_string()); + if reference.date_naive() == (now + chrono::Duration::days(1)).date_naive() { + add_tag(task, "TOMORROW".to_string()); + } + } + _ => (), + } + } + } + if let Some(d) = task.due() { + let status = task.status(); + // overdue + if status != &TaskStatus::Completed && status != &TaskStatus::Deleted && status != &TaskStatus::Recurring { + let now = Local::now().naive_utc(); + let d = NaiveDateTime::new(d.date(), d.time()); + if d < now { + add_tag(task, "OVERDUE".to_string()); + } + } + } } + } - pub fn task_start_stop(&mut self) -> Result<(), String> { - if self.tasks.is_empty() { - return Ok(()); - } + pub fn toggle_mark(&mut self) { + if !self.tasks.is_empty() { + let selected = self.current_selection; + let task_id = self.tasks[selected].id().unwrap_or_default(); + let task_uuid = *self.tasks[selected].uuid(); - let task_uuids = self.selected_task_uuids(); + if !self.marked.insert(task_uuid) { + self.marked.remove(&task_uuid); + } + } + } - for task_uuid in &task_uuids { - let mut command = "start"; - for tag in TaskwarriorTui::task_virtual_tags(*task_uuid) - .unwrap_or_default() - .split(' ') - { - if tag == "ACTIVE" { - command = "stop"; - } + pub fn toggle_mark_all(&mut self) { + for task in &self.tasks { + if !self.marked.insert(*task.uuid()) { + self.marked.remove(task.uuid()); + } + } + } + + pub fn escape(s: &str) -> String { + let mut es = String::with_capacity(s.len() + 2); + es.push('"'); + for ch in s.chars() { + match ch { + '"' => { + es.push('\\'); + es.push(ch); + } + _ => es.push(ch), + } + } + es.push('"'); + es + } + + pub async fn handle_input(&mut self, input: KeyCode) -> Result<()> { + match self.mode { + Mode::Tasks(_) => { + self.handle_input_by_task_mode(input).await?; + } + Mode::Projects => { + ProjectsState::handle_input(self, input)?; + self.update(false).await?; + } + Mode::Calendar => { + if input == self.keyconfig.quit || input == KeyCode::Ctrl('c') { + self.should_quit = true; + } else if input == self.keyconfig.next_tab { + if self.config.uda_change_focus_rotate { + self.mode = Mode::Tasks(Action::Report); + } + } else if input == self.keyconfig.previous_tab { + self.mode = Mode::Projects; + } else if input == KeyCode::Up || input == self.keyconfig.up { + if self.calendar_year > 0 { + self.calendar_year -= 1; + } + } else if input == KeyCode::Down || input == self.keyconfig.down { + self.calendar_year += 1; + } else if input == KeyCode::PageUp || input == self.keyconfig.page_up { + self.task_report_previous_page(); + } else if input == KeyCode::PageDown || input == self.keyconfig.page_down { + self.calendar_year += 10; + } else if input == KeyCode::Ctrl('e') { + self.task_details_scroll_down(); + } else if input == KeyCode::Ctrl('y') { + self.task_details_scroll_up(); + } else if input == self.keyconfig.done { + if self.config.uda_task_report_prompt_on_done { + self.mode = Mode::Tasks(Action::DonePrompt); + if self.task_current().is_none() { + self.mode = Mode::Tasks(Action::Report); } - - let output = std::process::Command::new("task") - .arg(task_uuid.to_string()) - .arg(command) - .output(); - if output.is_err() { - return Err(format!("Error running `task {}` for task `{}`.", command, task_uuid)); + } else { + match self.task_done() { + Ok(_) => self.update(true).await?, + Err(e) => { + self.error = Some(e); + } } - } - - if task_uuids.len() == 1 { - if let Some(uuid) = task_uuids.get(0) { - self.current_selection_uuid = Some(*uuid); + if self.calendar_year > 0 { + self.calendar_year -= 10; } + } } - - Ok(()) + } } - - pub fn task_quick_tag(&mut self) -> Result<(), String> { - let tag_name = &self.config.uda_quick_tag_name; - let ptag_name = format!("+{}", tag_name); - let ntag_name = format!("-{}", tag_name); - if self.tasks.is_empty() { - return Ok(()); - } - - let task_uuids = self.selected_task_uuids(); - - for task_uuid in &task_uuids { - if let Some(task) = self.task_by_uuid(*task_uuid) { - let mut tag_to_set = &ptag_name; - for tag in task.tags().unwrap() { - if tag == tag_name { - tag_to_set = &ntag_name; - } + self.update_task_table_state(); + Ok(()) + } + + async fn handle_input_by_task_mode(&mut self, input: KeyCode) -> Result<()> { + if let Mode::Tasks(task_mode) = &self.mode { + match task_mode { + Action::Report => { + if input == KeyCode::Esc { + self.marked.clear(); + } else if input == self.keyconfig.quit || input == KeyCode::Ctrl('c') { + self.should_quit = true; + } else if input == self.keyconfig.select { + self.task_table_state.multiple_selection(); + self.toggle_mark(); + } else if input == self.keyconfig.select_all { + self.task_table_state.multiple_selection(); + self.toggle_mark_all(); + } else if input == self.keyconfig.refresh { + self.update(true).await?; + } else if input == self.keyconfig.go_to_bottom || input == KeyCode::End { + self.task_report_bottom(); + } else if input == self.keyconfig.go_to_top || input == KeyCode::Home { + self.task_report_top(); + } else if input == KeyCode::Down || input == self.keyconfig.down { + self.task_report_next(); + } else if input == KeyCode::Up || input == self.keyconfig.up { + self.task_report_previous(); + } else if input == KeyCode::PageDown || input == self.keyconfig.page_down { + self.task_report_next_page(); + } else if input == KeyCode::PageUp || input == self.keyconfig.page_up { + self.task_report_previous_page(); + } else if input == KeyCode::Ctrl('e') { + self.task_details_scroll_down(); + } else if input == KeyCode::Ctrl('y') { + self.task_details_scroll_up(); + } else if input == self.keyconfig.done { + if self.config.uda_task_report_prompt_on_done { + self.mode = Mode::Tasks(Action::DonePrompt); + if self.task_current().is_none() { + self.mode = Mode::Tasks(Action::Report); + } + } else { + match self.task_done() { + Ok(_) => self.update(true).await?, + Err(e) => { + self.error = Some(e); } - - let output = std::process::Command::new("task") - .arg(task_uuid.to_string()) - .arg("modify") - .arg(tag_to_set) - .output(); - - if output.is_err() { - return Err(format!( - "Error running `task modify {}` for task `{}`.", - tag_to_set, task_uuid, - )); + } + } + } else if input == self.keyconfig.delete { + if self.config.uda_task_report_prompt_on_delete { + self.mode = Mode::Tasks(Action::DeletePrompt); + if self.task_current().is_none() { + self.mode = Mode::Tasks(Action::Report); + } + } else { + match self.task_delete() { + Ok(_) => self.update(true).await?, + Err(e) => { + self.error = Some(e); } + } } - } - - if task_uuids.len() == 1 { - if let Some(uuid) = task_uuids.get(0) { - self.current_selection_uuid = Some(*uuid); + } else if input == self.keyconfig.start_stop { + match self.task_start_stop() { + Ok(_) => self.update(true).await?, + Err(e) => { + self.error = Some(e); + } } - } - - Ok(()) - } - - pub fn task_delete(&mut self) -> Result<(), String> { - if self.tasks.is_empty() { - return Ok(()); - } - - let task_uuids = self.selected_task_uuids(); - - let mut cmd = std::process::Command::new("task"); - cmd.arg("rc.bulk=0") - .arg("rc.confirmation=off") - .arg("rc.dependency.confirmation=off") - .arg("rc.recurrence.confirmation=off"); - for task_uuid in &task_uuids { - cmd.arg(task_uuid.to_string()); - } - cmd.arg("delete"); - let output = cmd.output(); - let r = match output { - Ok(_) => Ok(()), - Err(_) => Err(format!( - "Cannot run `task delete` for tasks `{}`. Check documentation for more information", - task_uuids - .iter() - .map(ToString::to_string) - .collect::>() - .join(" ") - )), - }; - self.current_selection_uuid = None; - self.current_selection_id = None; - r - } - - pub fn task_done(&mut self) -> Result<(), String> { - if self.tasks.is_empty() { - return Ok(()); - } - let task_uuids = self.selected_task_uuids(); - let mut cmd = std::process::Command::new("task"); - cmd.arg("rc.bulk=0") - .arg("rc.confirmation=off") - .arg("rc.dependency.confirmation=off") - .arg("rc.recurrence.confirmation=off"); - for task_uuid in &task_uuids { - cmd.arg(task_uuid.to_string()); - } - cmd.arg("done"); - let output = cmd.output(); - let r = match output { - Ok(_) => Ok(()), - Err(_) => Err(format!( - "Cannot run `task done` for task `{}`. Check documentation for more information", - task_uuids - .iter() - .map(ToString::to_string) - .collect::>() - .join(" ") - )), - }; - self.current_selection_uuid = None; - self.current_selection_id = None; - r - } - - pub fn task_undo(&mut self) -> Result<(), String> { - let output = std::process::Command::new("task") - .arg("rc.confirmation=off") - .arg("undo") - .output(); - - match output { - Ok(output) => { - let data = String::from_utf8_lossy(&output.stdout); - let re = Regex::new( - r"(?P[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})", - ) - .unwrap(); - if let Some(caps) = re.captures(&data) { - if let Ok(uuid) = Uuid::parse_str(&caps["task_uuid"]) { - self.current_selection_uuid = Some(uuid); - } - } - Ok(()) + } else if input == self.keyconfig.quick_tag { + match self.task_quick_tag() { + Ok(_) => self.update(true).await?, + Err(e) => { + self.error = Some(e); + } } - Err(_) => Err("Cannot run `task undo`. Check documentation for more information".to_string()), - } - } - - pub async fn task_edit(&mut self) -> Result<(), String> { - if self.tasks.is_empty() { - return Ok(()); - } - - self.pause_tui().await.unwrap(); - - let selected = self.current_selection; - let task_id = self.tasks[selected].id().unwrap_or_default(); - let task_uuid = *self.tasks[selected].uuid(); - - let r = std::process::Command::new("task") - .arg(format!("{}", task_uuid)) - .arg("edit") - .spawn(); - - let r = match r { - Ok(child) => { - let output = child.wait_with_output(); - match output { - Ok(output) => { - if output.status.success() { - Ok(()) - } else { - Err(format!( - "`task edit` for task `{}` failed. {}{}", - task_uuid, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - )) - } - } - Err(err) => Err(format!("Cannot run `task edit` for task `{}`. {}", task_uuid, err)), - } + } else if input == self.keyconfig.edit { + match self.task_edit().await { + Ok(_) => self.update(true).await?, + Err(e) => { + self.error = Some(e); + } } - _ => Err(format!( - "Cannot start `task edit` for task `{}`. Check documentation for more information", - task_uuid - )), - }; - - self.current_selection_uuid = Some(task_uuid); - - self.resume_tui().await.unwrap(); - - r - } - - pub fn task_current(&self) -> Option { - if self.tasks.is_empty() { - return None; - } - let selected = self.current_selection; - Some(self.tasks[selected].clone()) - } - - pub fn update_tags(&mut self) { - let tasks = &mut self.tasks; - - // dependency scan - for l_i in 0..tasks.len() { - let default_deps = vec![]; - let deps = tasks[l_i].depends().unwrap_or(&default_deps).clone(); - add_tag(&mut tasks[l_i], "UNBLOCKED".to_string()); - for dep in deps { - for r_i in 0..tasks.len() { - if tasks[r_i].uuid() == &dep { - let l_status = tasks[l_i].status(); - let r_status = tasks[r_i].status(); - if l_status != &TaskStatus::Completed - && l_status != &TaskStatus::Deleted - && r_status != &TaskStatus::Completed - && r_status != &TaskStatus::Deleted - { - remove_tag(&mut tasks[l_i], "UNBLOCKED"); - add_tag(&mut tasks[l_i], "BLOCKED".to_string()); - add_tag(&mut tasks[r_i], "BLOCKING".to_string()); + } else if input == self.keyconfig.undo { + match self.task_undo() { + Ok(_) => self.update(true).await?, + Err(e) => { + self.error = Some(e); + } + } + } else if input == self.keyconfig.modify { + self.mode = Mode::Tasks(Action::Modify); + self.command_history.reset(); + self.history_status = Some(format!( + "{} / {}", + self + .command_history + .history_index() + .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) + .saturating_add(1), + self.command_history.history_len() + )); + self.update_completion_list(); + match self.task_table_state.mode() { + TableMode::SingleSelection => match self.task_current() { + Some(t) => { + let mut s = format!("{} ", Self::escape(t.description())); + if self.config.uda_prefill_task_metadata { + if t.tags().is_some() { + let virtual_tags = self.task_report_table.virtual_tags.clone(); + for tag in t.tags().unwrap() { + if !virtual_tags.contains(tag) { + s = format!("{}+{} ", s, tag); } - break; + } + } + if t.project().is_some() { + s = format!("{}project:{} ", s, t.project().unwrap()); } + if t.priority().is_some() { + s = format!("{}priority:{} ", s, t.priority().unwrap()); + } + if t.due().is_some() { + let date = t.due().unwrap(); + s = format!("{}due:{} ", s, get_formatted_datetime(date)); + } + } + self.modify.update(&s, s.as_str().len(), &mut self.changes); } + None => self.modify.update("", 0, &mut self.changes), + }, + TableMode::MultipleSelection => self.modify.update("", 0, &mut self.changes), } - } - - // other virtual tags - // TODO: support all virtual tags that taskwarrior supports - for task in tasks.iter_mut() { - match task.status() { - TaskStatus::Waiting => add_tag(task, "WAITING".to_string()), - TaskStatus::Completed => add_tag(task, "COMPLETED".to_string()), - TaskStatus::Pending => add_tag(task, "PENDING".to_string()), - TaskStatus::Deleted => add_tag(task, "DELETED".to_string()), - TaskStatus::Recurring => (), - } - if task.start().is_some() { - add_tag(task, "ACTIVE".to_string()); + } else if input == self.keyconfig.shell { + self.mode = Mode::Tasks(Action::Subprocess); + } else if input == self.keyconfig.log { + self.mode = Mode::Tasks(Action::Log); + self.command_history.reset(); + self.history_status = Some(format!( + "{} / {}", + self + .command_history + .history_index() + .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) + .saturating_add(1), + self.command_history.history_len() + )); + self.update_completion_list(); + } else if input == self.keyconfig.add { + self.mode = Mode::Tasks(Action::Add); + self.command_history.reset(); + self.history_status = Some(format!( + "{} / {}", + self + .command_history + .history_index() + .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) + .saturating_add(1), + self.command_history.history_len() + )); + self.update_completion_list(); + } else if input == self.keyconfig.annotate { + self.mode = Mode::Tasks(Action::Annotate); + self.command_history.reset(); + self.history_status = Some(format!( + "{} / {}", + self + .command_history + .history_index() + .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) + .saturating_add(1), + self.command_history.history_len() + )); + self.update_completion_list(); + } else if input == self.keyconfig.help { + self.mode = Mode::Tasks(Action::HelpPopup); + } else if input == self.keyconfig.filter { + self.mode = Mode::Tasks(Action::Filter); + self.filter_history.reset(); + self.history_status = Some(format!( + "{} / {}", + self + .filter_history + .history_index() + .unwrap_or_else(|| self.filter_history.history_len().saturating_sub(1)) + .saturating_add(1), + self.filter_history.history_len() + )); + self.update_completion_list(); + } else if input == KeyCode::Char(':') { + self.mode = Mode::Tasks(Action::Jump); + } else if input == self.keyconfig.shortcut1 { + match self.task_shortcut(1).await { + Ok(_) => self.update(true).await?, + Err(e) => { + self.update(true).await?; + self.error = Some(e); + } } - if task.scheduled().is_some() { - add_tag(task, "SCHEDULED".to_string()); + } else if input == self.keyconfig.shortcut2 { + match self.task_shortcut(2).await { + Ok(_) => self.update(true).await?, + Err(e) => { + self.update(true).await?; + self.error = Some(e); + } } - if task.parent().is_some() { - add_tag(task, "INSTANCE".to_string()); + } else if input == self.keyconfig.shortcut3 { + match self.task_shortcut(3).await { + Ok(_) => self.update(true).await?, + Err(e) => { + self.update(true).await?; + self.error = Some(e); + } } - if task.until().is_some() { - add_tag(task, "UNTIL".to_string()); + } else if input == self.keyconfig.shortcut4 { + match self.task_shortcut(4).await { + Ok(_) => self.update(true).await?, + Err(e) => { + self.update(true).await?; + self.error = Some(e); + } } - if task.annotations().is_some() { - add_tag(task, "ANNOTATED".to_string()); + } else if input == self.keyconfig.shortcut5 { + match self.task_shortcut(5).await { + Ok(_) => self.update(true).await?, + Err(e) => { + self.update(true).await?; + self.error = Some(e); + } } - let virtual_tags = self.task_report_table.virtual_tags.clone(); - if task.tags().is_some() && task.tags().unwrap().iter().any(|s| !virtual_tags.contains(s)) { - add_tag(task, "TAGGED".to_string()); + } else if input == self.keyconfig.shortcut6 { + match self.task_shortcut(6).await { + Ok(_) => self.update(true).await?, + Err(e) => { + self.update(true).await?; + self.error = Some(e); + } } - if !task.uda().is_empty() { - add_tag(task, "UDA".to_string()); + } else if input == self.keyconfig.shortcut7 { + match self.task_shortcut(7).await { + Ok(_) => self.update(true).await?, + Err(e) => { + self.update(true).await?; + self.error = Some(e); + } } - if task.mask().is_some() { - add_tag(task, "TEMPLATE".to_string()); + } else if input == self.keyconfig.shortcut8 { + match self.task_shortcut(8).await { + Ok(_) => self.update(true).await?, + Err(e) => { + self.update(true).await?; + self.error = Some(e); + } } - if task.project().is_some() { - add_tag(task, "PROJECT".to_string()); + } else if input == self.keyconfig.shortcut9 { + match self.task_shortcut(9).await { + Ok(_) => self.update(true).await?, + Err(e) => { + self.update(true).await?; + self.error = Some(e); + } } - if task.priority().is_some() { - add_tag(task, "PRIORITY".to_string()); + } else if input == self.keyconfig.zoom { + self.task_report_show_info = !self.task_report_show_info; + } else if input == self.keyconfig.context_menu { + self.mode = Mode::Tasks(Action::ContextMenu); + } else if input == self.keyconfig.previous_tab { + if self.config.uda_change_focus_rotate { + self.mode = Mode::Calendar; } - if task.recur().is_some() { - add_tag(task, "RECURRING".to_string()); - let r = task.recur().unwrap(); + } else if input == self.keyconfig.next_tab { + self.mode = Mode::Projects; + } + } + Action::ContextMenu => { + if input == self.keyconfig.quit || input == KeyCode::Esc { + self.mode = Mode::Tasks(Action::Report); + } else if input == KeyCode::Down || input == self.keyconfig.down { + self.context_next(); + if self.config.uda_context_menu_select_on_move { + if self.error.is_some() { + self.previous_mode = Some(self.mode.clone()); + self.mode = Mode::Tasks(Action::Error); + } else { + match self.context_select() { + Ok(_) => self.update(true).await?, + Err(e) => { + self.error = Some(e.to_string()); + } + } + } } - if let Some(d) = task.due() { - let status = task.status(); - // due today - if status != &TaskStatus::Completed && status != &TaskStatus::Deleted { - let now = Local::now(); - let reference = TimeZone::from_utc_datetime(now.offset(), d); - let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc()); - let d = d.clone(); - if (reference - chrono::Duration::nanoseconds(1)).month() == now.month() { - add_tag(task, "MONTH".to_string()); - } - if (reference - chrono::Duration::nanoseconds(1)).month() % 4 == now.month() % 4 { - add_tag(task, "QUARTER".to_string()); - } - if reference.year() == now.year() { - add_tag(task, "YEAR".to_string()); - } - match get_date_state(&d, self.config.due) { - DateState::EarlierToday | DateState::LaterToday => { - add_tag(task, "DUE".to_string()); - add_tag(task, "TODAY".to_string()); - add_tag(task, "DUETODAY".to_string()); - } - DateState::AfterToday => { - add_tag(task, "DUE".to_string()); - if reference.date() == (now + chrono::Duration::days(1)).date() { - add_tag(task, "TOMORROW".to_string()); - } - } - _ => (), - } + } else if input == KeyCode::Up || input == self.keyconfig.up { + self.context_previous(); + if self.config.uda_context_menu_select_on_move { + if self.error.is_some() { + self.previous_mode = Some(self.mode.clone()); + self.mode = Mode::Tasks(Action::Error); + } else { + match self.context_select() { + Ok(_) => self.update(true).await?, + Err(e) => { + self.error = Some(e.to_string()); + } } + } } - if let Some(d) = task.due() { - let status = task.status(); - // overdue - if status != &TaskStatus::Completed - && status != &TaskStatus::Deleted - && status != &TaskStatus::Recurring - { - let now = Local::now().naive_utc(); - let d = NaiveDateTime::new(d.date(), d.time()); - if d < now { - add_tag(task, "OVERDUE".to_string()); - } + } else if input == KeyCode::Char('\n') { + if self.error.is_some() { + self.previous_mode = Some(self.mode.clone()); + self.mode = Mode::Tasks(Action::Error); + } else if self.config.uda_context_menu_select_on_move { + self.mode = Mode::Tasks(Action::Report); + } else { + match self.context_select() { + Ok(_) => self.update(true).await?, + Err(e) => { + self.error = Some(e.to_string()); } + } } - } - } - - pub fn toggle_mark(&mut self) { - if !self.tasks.is_empty() { - let selected = self.current_selection; - let task_id = self.tasks[selected].id().unwrap_or_default(); - let task_uuid = *self.tasks[selected].uuid(); - - if !self.marked.insert(task_uuid) { - self.marked.remove(&task_uuid); + } + } + Action::HelpPopup => { + if input == self.keyconfig.quit || input == KeyCode::Esc { + self.mode = Mode::Tasks(Action::Report); + } else if input == self.keyconfig.down { + self.help_popup.scroll = self.help_popup.scroll.checked_add(1).unwrap_or(0); + let th = (self.help_popup.text_height as u16).saturating_sub(1); + if self.help_popup.scroll > th { + self.help_popup.scroll = th; } - } - } - - pub fn toggle_mark_all(&mut self) { - for task in &self.tasks { - if !self.marked.insert(*task.uuid()) { - self.marked.remove(task.uuid()); + } else if input == self.keyconfig.up { + self.help_popup.scroll = self.help_popup.scroll.saturating_sub(1); + } + } + Action::Modify => match input { + KeyCode::Esc => { + if self.show_completion_pane { + self.show_completion_pane = false; + self.completion_list.unselect(); + } else { + self.modify.update("", 0, &mut self.changes); + self.mode = Mode::Tasks(Action::Report); } - } - } - - pub fn escape(s: &str) -> String { - let mut es = String::with_capacity(s.len() + 2); - es.push('"'); - for ch in s.chars() { - match ch { - '"' => { - es.push('\\'); - es.push(ch); + } + KeyCode::Char('\n') => { + if self.show_completion_pane { + self.show_completion_pane = false; + if let Some((i, (r, m, o, _, _))) = self.completion_list.selected() { + let (before, after) = self.modify.as_str().split_at(self.modify.pos()); + let fs = format!("{}{}{}", before.trim_end_matches(&o), r, after); + self.modify.update(&fs, self.modify.pos() + r.len() - o.len(), &mut self.changes); + } + self.completion_list.unselect(); + } else if self.error.is_some() { + self.previous_mode = Some(self.mode.clone()); + self.mode = Mode::Tasks(Action::Error); + } else { + match self.task_modify() { + Ok(_) => { + self.mode = Mode::Tasks(Action::Report); + self.command_history.add(self.modify.as_str()); + self.modify.update("", 0, &mut self.changes); + self.update(true).await?; } - _ => es.push(ch), + Err(e) => { + self.error = Some(e); + } + } } - } - es.push('"'); - es - } - - pub async fn handle_input(&mut self, input: KeyCode) -> Result<()> { - match self.mode { - Mode::Tasks(_) => { - self.handle_input_by_task_mode(input).await?; + } + KeyCode::Tab | KeyCode::Ctrl('n') => { + if !self.completion_list.is_empty() { + self.update_input_for_completion(); + if !self.show_completion_pane { + self.show_completion_pane = true; + } + self.completion_list.next(); } - Mode::Projects => { - ProjectsState::handle_input(self, input)?; - self.update(false).await?; + } + KeyCode::BackTab | KeyCode::Ctrl('p') => { + if self.show_completion_pane && !self.completion_list.is_empty() { + self.completion_list.previous(); } - Mode::Calendar => { - if input == self.keyconfig.quit || input == KeyCode::Ctrl('c') { - self.should_quit = true; - } else if input == self.keyconfig.next_tab { - if self.config.uda_change_focus_rotate { - self.mode = Mode::Tasks(Action::Report); - } - } else if input == self.keyconfig.previous_tab { - self.mode = Mode::Projects; - } else if input == KeyCode::Up || input == self.keyconfig.up { - if self.calendar_year > 0 { - self.calendar_year -= 1; - } - } else if input == KeyCode::Down || input == self.keyconfig.down { - self.calendar_year += 1; - } else if input == KeyCode::PageUp || input == self.keyconfig.page_up { - self.task_report_previous_page(); - } else if input == KeyCode::PageDown || input == self.keyconfig.page_down { - self.calendar_year += 10; - } else if input == KeyCode::Ctrl('e') { - self.task_details_scroll_down(); - } else if input == KeyCode::Ctrl('y') { - self.task_details_scroll_up(); - } else if input == self.keyconfig.done { - if self.config.uda_task_report_prompt_on_done { - self.mode = Mode::Tasks(Action::DonePrompt); - if self.task_current().is_none() { - self.mode = Mode::Tasks(Action::Report); - } - } else { - match self.task_done() { - Ok(_) => self.update(true).await?, - Err(e) => { - self.error = Some(e); - } - } - if self.calendar_year > 0 { - self.calendar_year -= 10; - } - } + } + + KeyCode::Up => { + if self.show_completion_pane && !self.completion_list.is_empty() { + self.completion_list.previous(); + } else if let Some(s) = self + .command_history + .history_search(&self.modify.as_str()[..self.modify.pos()], HistoryDirection::Reverse) + { + let p = self.modify.pos(); + self.modify.update("", 0, &mut self.changes); + self.modify.update(&s, std::cmp::min(s.len(), p), &mut self.changes); + self.history_status = Some(format!( + "{} / {}", + self + .command_history + .history_index() + .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) + .saturating_add(1), + self.command_history.history_len() + )); + } + } + KeyCode::Down => { + if self.show_completion_pane && !self.completion_list.is_empty() { + self.completion_list.next(); + } else if let Some(s) = self + .command_history + .history_search(&self.modify.as_str()[..self.modify.pos()], HistoryDirection::Forward) + { + let p = self.modify.pos(); + self.modify.update("", 0, &mut self.changes); + self.modify.update(&s, std::cmp::min(s.len(), p), &mut self.changes); + self.history_status = Some(format!( + "{} / {}", + self + .command_history + .history_index() + .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) + .saturating_add(1), + self.command_history.history_len() + )); + } + } + _ => { + self.command_history.reset(); + handle_movement(&mut self.modify, input, &mut self.changes); + self.update_input_for_completion(); + } + }, + Action::Subprocess => match input { + KeyCode::Char('\n') => { + if self.error.is_some() { + self.previous_mode = Some(self.mode.clone()); + self.mode = Mode::Tasks(Action::Error); + } else { + match self.task_subprocess() { + Ok(_) => { + self.mode = Mode::Tasks(Action::Report); + self.reset_command(); + self.update(true).await?; } + Err(e) => { + self.error = Some(e); + } + } } - } - self.update_task_table_state(); - Ok(()) - } - - async fn handle_input_by_task_mode(&mut self, input: KeyCode) -> Result<()> { - if let Mode::Tasks(task_mode) = &self.mode { - match task_mode { - Action::Report => { - if input == KeyCode::Esc { - self.marked.clear(); - } else if input == self.keyconfig.quit || input == KeyCode::Ctrl('c') { - self.should_quit = true; - } else if input == self.keyconfig.select { - self.task_table_state.multiple_selection(); - self.toggle_mark(); - } else if input == self.keyconfig.select_all { - self.task_table_state.multiple_selection(); - self.toggle_mark_all(); - } else if input == self.keyconfig.refresh { - self.update(true).await?; - } else if input == self.keyconfig.go_to_bottom || input == KeyCode::End { - self.task_report_bottom(); - } else if input == self.keyconfig.go_to_top || input == KeyCode::Home { - self.task_report_top(); - } else if input == KeyCode::Down || input == self.keyconfig.down { - self.task_report_next(); - } else if input == KeyCode::Up || input == self.keyconfig.up { - self.task_report_previous(); - } else if input == KeyCode::PageDown || input == self.keyconfig.page_down { - self.task_report_next_page(); - } else if input == KeyCode::PageUp || input == self.keyconfig.page_up { - self.task_report_previous_page(); - } else if input == KeyCode::Ctrl('e') { - self.task_details_scroll_down(); - } else if input == KeyCode::Ctrl('y') { - self.task_details_scroll_up(); - } else if input == self.keyconfig.done { - if self.config.uda_task_report_prompt_on_done { - self.mode = Mode::Tasks(Action::DonePrompt); - if self.task_current().is_none() { - self.mode = Mode::Tasks(Action::Report); - } - } else { - match self.task_done() { - Ok(_) => self.update(true).await?, - Err(e) => { - self.error = Some(e); - } - } - } - } else if input == self.keyconfig.delete { - if self.config.uda_task_report_prompt_on_delete { - self.mode = Mode::Tasks(Action::DeletePrompt); - if self.task_current().is_none() { - self.mode = Mode::Tasks(Action::Report); - } - } else { - match self.task_delete() { - Ok(_) => self.update(true).await?, - Err(e) => { - self.error = Some(e); - } - } - } - } else if input == self.keyconfig.start_stop { - match self.task_start_stop() { - Ok(_) => self.update(true).await?, - Err(e) => { - self.error = Some(e); - } - } - } else if input == self.keyconfig.quick_tag { - match self.task_quick_tag() { - Ok(_) => self.update(true).await?, - Err(e) => { - self.error = Some(e); - } - } - } else if input == self.keyconfig.edit { - match self.task_edit().await { - Ok(_) => self.update(true).await?, - Err(e) => { - self.error = Some(e); - } - } - } else if input == self.keyconfig.undo { - match self.task_undo() { - Ok(_) => self.update(true).await?, - Err(e) => { - self.error = Some(e); - } - } - } else if input == self.keyconfig.modify { - self.mode = Mode::Tasks(Action::Modify); - self.command_history.reset(); - self.history_status = Some(format!( - "{} / {}", - self.command_history - .history_index() - .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) - .saturating_add(1), - self.command_history.history_len() - )); - self.update_completion_list(); - match self.task_table_state.mode() { - TableMode::SingleSelection => match self.task_current() { - Some(t) => { - let mut s = format!("{} ", Self::escape(t.description())); - if self.config.uda_prefill_task_metadata { - if t.tags().is_some() { - let virtual_tags = self.task_report_table.virtual_tags.clone(); - for tag in t.tags().unwrap() { - if !virtual_tags.contains(tag) { - s = format!("{}+{} ", s, tag); - } - } - } - if t.project().is_some() { - s = format!("{}project:{} ", s, t.project().unwrap()); - } - if t.priority().is_some() { - s = format!("{}priority:{} ", s, t.priority().unwrap()); - } - if t.due().is_some() { - let date = t.due().unwrap(); - s = format!("{}due:{} ", s, get_formatted_datetime(date)); - } - } - self.modify.update(&s, s.as_str().len()); - } - None => self.modify.update("", 0), - }, - TableMode::MultipleSelection => self.modify.update("", 0), - } - } else if input == self.keyconfig.shell { - self.mode = Mode::Tasks(Action::Subprocess); - } else if input == self.keyconfig.log { - self.mode = Mode::Tasks(Action::Log); - self.command_history.reset(); - self.history_status = Some(format!( - "{} / {}", - self.command_history - .history_index() - .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) - .saturating_add(1), - self.command_history.history_len() - )); - self.update_completion_list(); - } else if input == self.keyconfig.add { - self.mode = Mode::Tasks(Action::Add); - self.command_history.reset(); - self.history_status = Some(format!( - "{} / {}", - self.command_history - .history_index() - .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) - .saturating_add(1), - self.command_history.history_len() - )); - self.update_completion_list(); - } else if input == self.keyconfig.annotate { - self.mode = Mode::Tasks(Action::Annotate); - self.command_history.reset(); - self.history_status = Some(format!( - "{} / {}", - self.command_history - .history_index() - .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) - .saturating_add(1), - self.command_history.history_len() - )); - self.update_completion_list(); - } else if input == self.keyconfig.help { - self.mode = Mode::Tasks(Action::HelpPopup); - } else if input == self.keyconfig.filter { - self.mode = Mode::Tasks(Action::Filter); - self.filter_history.reset(); - self.history_status = Some(format!( - "{} / {}", - self.filter_history - .history_index() - .unwrap_or_else(|| self.filter_history.history_len().saturating_sub(1)) - .saturating_add(1), - self.filter_history.history_len() - )); - self.update_completion_list(); - } else if input == KeyCode::Char(':') { - self.mode = Mode::Tasks(Action::Jump); - } else if input == self.keyconfig.shortcut1 { - match self.task_shortcut(1).await { - Ok(_) => self.update(true).await?, - Err(e) => { - self.update(true).await?; - self.error = Some(e); - } - } - } else if input == self.keyconfig.shortcut2 { - match self.task_shortcut(2).await { - Ok(_) => self.update(true).await?, - Err(e) => { - self.update(true).await?; - self.error = Some(e); - } - } - } else if input == self.keyconfig.shortcut3 { - match self.task_shortcut(3).await { - Ok(_) => self.update(true).await?, - Err(e) => { - self.update(true).await?; - self.error = Some(e); - } - } - } else if input == self.keyconfig.shortcut4 { - match self.task_shortcut(4).await { - Ok(_) => self.update(true).await?, - Err(e) => { - self.update(true).await?; - self.error = Some(e); - } - } - } else if input == self.keyconfig.shortcut5 { - match self.task_shortcut(5).await { - Ok(_) => self.update(true).await?, - Err(e) => { - self.update(true).await?; - self.error = Some(e); - } - } - } else if input == self.keyconfig.shortcut6 { - match self.task_shortcut(6).await { - Ok(_) => self.update(true).await?, - Err(e) => { - self.update(true).await?; - self.error = Some(e); - } - } - } else if input == self.keyconfig.shortcut7 { - match self.task_shortcut(7).await { - Ok(_) => self.update(true).await?, - Err(e) => { - self.update(true).await?; - self.error = Some(e); - } - } - } else if input == self.keyconfig.shortcut8 { - match self.task_shortcut(8).await { - Ok(_) => self.update(true).await?, - Err(e) => { - self.update(true).await?; - self.error = Some(e); - } - } - } else if input == self.keyconfig.shortcut9 { - match self.task_shortcut(9).await { - Ok(_) => self.update(true).await?, - Err(e) => { - self.update(true).await?; - self.error = Some(e); - } - } - } else if input == self.keyconfig.zoom { - self.task_report_show_info = !self.task_report_show_info; - } else if input == self.keyconfig.context_menu { - self.mode = Mode::Tasks(Action::ContextMenu); - } else if input == self.keyconfig.previous_tab { - if self.config.uda_change_focus_rotate { - self.mode = Mode::Calendar; - } - } else if input == self.keyconfig.next_tab { - self.mode = Mode::Projects; - } + } + KeyCode::Esc => { + self.reset_command(); + self.mode = Mode::Tasks(Action::Report); + } + _ => handle_movement(&mut self.command, input, &mut self.changes), + }, + Action::Log => match input { + KeyCode::Esc => { + if self.show_completion_pane { + self.show_completion_pane = false; + self.completion_list.unselect(); + } else { + self.reset_command(); + self.history_status = None; + self.mode = Mode::Tasks(Action::Report); + } + } + KeyCode::Char('\n') => { + if self.show_completion_pane { + self.show_completion_pane = false; + if let Some((i, (r, m, o, _, _))) = self.completion_list.selected() { + let (before, after) = self.command.as_str().split_at(self.command.pos()); + let fs = format!("{}{}{}", before.trim_end_matches(&o), r, after); + self.command.update(&fs, self.command.pos() + r.len() - o.len(), &mut self.changes); + } + self.completion_list.unselect(); + } else if self.error.is_some() { + self.previous_mode = Some(self.mode.clone()); + self.mode = Mode::Tasks(Action::Error); + } else { + match self.task_log() { + Ok(_) => { + self.mode = Mode::Tasks(Action::Report); + self.command_history.add(self.command.as_str()); + self.reset_command(); + self.history_status = None; + self.update(true).await?; } - Action::ContextMenu => { - if input == self.keyconfig.quit || input == KeyCode::Esc { - self.mode = Mode::Tasks(Action::Report); - } else if input == KeyCode::Down || input == self.keyconfig.down { - self.context_next(); - if self.config.uda_context_menu_select_on_move { - if self.error.is_some() { - self.previous_mode = Some(self.mode.clone()); - self.mode = Mode::Tasks(Action::Error); - } else { - match self.context_select() { - Ok(_) => self.update(true).await?, - Err(e) => { - self.error = Some(e.to_string()); - } - } - } - } - } else if input == KeyCode::Up || input == self.keyconfig.up { - self.context_previous(); - if self.config.uda_context_menu_select_on_move { - if self.error.is_some() { - self.previous_mode = Some(self.mode.clone()); - self.mode = Mode::Tasks(Action::Error); - } else { - match self.context_select() { - Ok(_) => self.update(true).await?, - Err(e) => { - self.error = Some(e.to_string()); - } - } - } - } - } else if input == KeyCode::Char('\n') { - if self.error.is_some() { - self.previous_mode = Some(self.mode.clone()); - self.mode = Mode::Tasks(Action::Error); - } else if self.config.uda_context_menu_select_on_move { - self.mode = Mode::Tasks(Action::Report); - } else { - match self.context_select() { - Ok(_) => self.update(true).await?, - Err(e) => { - self.error = Some(e.to_string()); - } - } - } - } + Err(e) => { + self.error = Some(e); } - Action::HelpPopup => { - if input == self.keyconfig.quit || input == KeyCode::Esc { - self.mode = Mode::Tasks(Action::Report); - } else if input == self.keyconfig.down { - self.help_popup.scroll = self.help_popup.scroll.checked_add(1).unwrap_or(0); - let th = (self.help_popup.text_height as u16).saturating_sub(1); - if self.help_popup.scroll > th { - self.help_popup.scroll = th; - } - } else if input == self.keyconfig.up { - self.help_popup.scroll = self.help_popup.scroll.saturating_sub(1); - } + } + } + } + KeyCode::Tab | KeyCode::Ctrl('n') => { + if !self.completion_list.is_empty() { + self.update_input_for_completion(); + if !self.show_completion_pane { + self.show_completion_pane = true; + } + self.completion_list.next(); + } + } + KeyCode::BackTab | KeyCode::Ctrl('p') => { + if self.show_completion_pane && !self.completion_list.is_empty() { + self.completion_list.previous(); + } + } + + KeyCode::Up => { + if self.show_completion_pane && !self.completion_list.is_empty() { + self.completion_list.previous(); + } else if let Some(s) = self + .command_history + .history_search(&self.command.as_str()[..self.command.pos()], HistoryDirection::Reverse) + { + let p = self.command.pos(); + self.command.update("", 0, &mut self.changes); + self.command.update(&s, std::cmp::min(s.len(), p), &mut self.changes); + self.history_status = Some(format!( + "{} / {}", + self + .command_history + .history_index() + .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) + .saturating_add(1), + self.command_history.history_len() + )); + } + } + KeyCode::Down => { + if self.show_completion_pane && !self.completion_list.is_empty() { + self.completion_list.next(); + } else if let Some(s) = self + .command_history + .history_search(&self.command.as_str()[..self.command.pos()], HistoryDirection::Forward) + { + let p = self.command.pos(); + self.command.update("", 0, &mut self.changes); + self.command.update(&s, std::cmp::min(s.len(), p), &mut self.changes); + self.history_status = Some(format!( + "{} / {}", + self + .command_history + .history_index() + .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) + .saturating_add(1), + self.command_history.history_len() + )); + } + } + _ => { + self.command_history.reset(); + handle_movement(&mut self.command, input, &mut self.changes); + self.update_input_for_completion(); + } + }, + Action::Annotate => match input { + KeyCode::Esc => { + if self.show_completion_pane { + self.show_completion_pane = false; + self.completion_list.unselect(); + } else { + self.reset_command(); + self.mode = Mode::Tasks(Action::Report); + self.history_status = None; + } + } + KeyCode::Char('\n') => { + if self.show_completion_pane { + self.show_completion_pane = false; + if let Some((i, (r, m, o, _, _))) = self.completion_list.selected() { + let (before, after) = self.command.as_str().split_at(self.command.pos()); + let fs = format!("{}{}{}", before.trim_end_matches(&o), r, after); + self.command.update(&fs, self.command.pos() + r.len() - o.len(), &mut self.changes); + } + self.completion_list.unselect(); + } else if self.error.is_some() { + self.previous_mode = Some(self.mode.clone()); + self.mode = Mode::Tasks(Action::Error); + } else { + match self.task_annotate() { + Ok(_) => { + self.mode = Mode::Tasks(Action::Report); + self.command_history.add(self.command.as_str()); + self.reset_command(); + self.history_status = None; + self.update(true).await?; } - Action::Modify => match input { - KeyCode::Esc => { - if self.show_completion_pane { - self.show_completion_pane = false; - self.completion_list.unselect(); - } else { - self.modify.update("", 0); - self.mode = Mode::Tasks(Action::Report); - } - } - KeyCode::Char('\n') => { - if self.show_completion_pane { - self.show_completion_pane = false; - if let Some((i, (r, m, o, _, _))) = self.completion_list.selected() { - let (before, after) = self.modify.as_str().split_at(self.modify.pos()); - let fs = format!("{}{}{}", before.trim_end_matches(&o), r, after); - self.modify.update(&fs, self.modify.pos() + r.len() - o.len()); - } - self.completion_list.unselect(); - } else if self.error.is_some() { - self.previous_mode = Some(self.mode.clone()); - self.mode = Mode::Tasks(Action::Error); - } else { - match self.task_modify() { - Ok(_) => { - self.mode = Mode::Tasks(Action::Report); - self.command_history.add(self.modify.as_str()); - self.modify.update("", 0); - self.update(true).await?; - } - Err(e) => { - self.error = Some(e); - } - } - } - } - KeyCode::Tab | KeyCode::Ctrl('n') => { - if !self.completion_list.is_empty() { - self.update_input_for_completion(); - if !self.show_completion_pane { - self.show_completion_pane = true; - } - self.completion_list.next(); - } - } - KeyCode::BackTab | KeyCode::Ctrl('p') => { - if self.show_completion_pane && !self.completion_list.is_empty() { - self.completion_list.previous(); - } - } - - KeyCode::Up => { - if self.show_completion_pane && !self.completion_list.is_empty() { - self.completion_list.previous(); - } else if let Some(s) = self - .command_history - .history_search(&self.modify.as_str()[..self.modify.pos()], HistoryDirection::Reverse) - { - let p = self.modify.pos(); - self.modify.update("", 0); - self.modify.update(&s, std::cmp::min(s.len(), p)); - self.history_status = Some(format!( - "{} / {}", - self.command_history - .history_index() - .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) - .saturating_add(1), - self.command_history.history_len() - )); - } - } - KeyCode::Down => { - if self.show_completion_pane && !self.completion_list.is_empty() { - self.completion_list.next(); - } else if let Some(s) = self - .command_history - .history_search(&self.modify.as_str()[..self.modify.pos()], HistoryDirection::Forward) - { - let p = self.modify.pos(); - self.modify.update("", 0); - self.modify.update(&s, std::cmp::min(s.len(), p)); - self.history_status = Some(format!( - "{} / {}", - self.command_history - .history_index() - .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) - .saturating_add(1), - self.command_history.history_len() - )); - } - } - _ => { - self.command_history.reset(); - handle_movement(&mut self.modify, input); - self.update_input_for_completion(); - } - }, - Action::Subprocess => match input { - KeyCode::Char('\n') => { - if self.error.is_some() { - self.previous_mode = Some(self.mode.clone()); - self.mode = Mode::Tasks(Action::Error); - } else { - match self.task_subprocess() { - Ok(_) => { - self.mode = Mode::Tasks(Action::Report); - self.reset_command(); - self.update(true).await?; - } - Err(e) => { - self.error = Some(e); - } - } - } - } - KeyCode::Esc => { - self.reset_command(); - self.mode = Mode::Tasks(Action::Report); - } - _ => handle_movement(&mut self.command, input), - }, - Action::Log => match input { - KeyCode::Esc => { - if self.show_completion_pane { - self.show_completion_pane = false; - self.completion_list.unselect(); - } else { - self.reset_command(); - self.history_status = None; - self.mode = Mode::Tasks(Action::Report); - } - } - KeyCode::Char('\n') => { - if self.show_completion_pane { - self.show_completion_pane = false; - if let Some((i, (r, m, o, _, _))) = self.completion_list.selected() { - let (before, after) = self.command.as_str().split_at(self.command.pos()); - let fs = format!("{}{}{}", before.trim_end_matches(&o), r, after); - self.command.update(&fs, self.command.pos() + r.len() - o.len()); - } - self.completion_list.unselect(); - } else if self.error.is_some() { - self.previous_mode = Some(self.mode.clone()); - self.mode = Mode::Tasks(Action::Error); - } else { - match self.task_log() { - Ok(_) => { - self.mode = Mode::Tasks(Action::Report); - self.command_history.add(self.command.as_str()); - self.reset_command(); - self.history_status = None; - self.update(true).await?; - } - Err(e) => { - self.error = Some(e); - } - } - } - } - KeyCode::Tab | KeyCode::Ctrl('n') => { - if !self.completion_list.is_empty() { - self.update_input_for_completion(); - if !self.show_completion_pane { - self.show_completion_pane = true; - } - self.completion_list.next(); - } - } - KeyCode::BackTab | KeyCode::Ctrl('p') => { - if self.show_completion_pane && !self.completion_list.is_empty() { - self.completion_list.previous(); - } - } - - KeyCode::Up => { - if self.show_completion_pane && !self.completion_list.is_empty() { - self.completion_list.previous(); - } else if let Some(s) = self - .command_history - .history_search(&self.command.as_str()[..self.command.pos()], HistoryDirection::Reverse) - { - let p = self.command.pos(); - self.command.update("", 0); - self.command.update(&s, std::cmp::min(s.len(), p)); - self.history_status = Some(format!( - "{} / {}", - self.command_history - .history_index() - .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) - .saturating_add(1), - self.command_history.history_len() - )); - } - } - KeyCode::Down => { - if self.show_completion_pane && !self.completion_list.is_empty() { - self.completion_list.next(); - } else if let Some(s) = self - .command_history - .history_search(&self.command.as_str()[..self.command.pos()], HistoryDirection::Forward) - { - let p = self.command.pos(); - self.command.update("", 0); - self.command.update(&s, std::cmp::min(s.len(), p)); - self.history_status = Some(format!( - "{} / {}", - self.command_history - .history_index() - .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) - .saturating_add(1), - self.command_history.history_len() - )); - } - } - _ => { - self.command_history.reset(); - handle_movement(&mut self.command, input); - self.update_input_for_completion(); - } - }, - Action::Annotate => match input { - KeyCode::Esc => { - if self.show_completion_pane { - self.show_completion_pane = false; - self.completion_list.unselect(); - } else { - self.reset_command(); - self.mode = Mode::Tasks(Action::Report); - self.history_status = None; - } - } - KeyCode::Char('\n') => { - if self.show_completion_pane { - self.show_completion_pane = false; - if let Some((i, (r, m, o, _, _))) = self.completion_list.selected() { - let (before, after) = self.command.as_str().split_at(self.command.pos()); - let fs = format!("{}{}{}", before.trim_end_matches(&o), r, after); - self.command.update(&fs, self.command.pos() + r.len() - o.len()); - } - self.completion_list.unselect(); - } else if self.error.is_some() { - self.previous_mode = Some(self.mode.clone()); - self.mode = Mode::Tasks(Action::Error); - } else { - match self.task_annotate() { - Ok(_) => { - self.mode = Mode::Tasks(Action::Report); - self.command_history.add(self.command.as_str()); - self.reset_command(); - self.history_status = None; - self.update(true).await?; - } - Err(e) => { - self.error = Some(e); - } - } - } - } - KeyCode::Tab | KeyCode::Ctrl('n') => { - if !self.completion_list.is_empty() { - self.update_input_for_completion(); - if !self.show_completion_pane { - self.show_completion_pane = true; - } - self.completion_list.next(); - } - } - KeyCode::BackTab | KeyCode::Ctrl('p') => { - if self.show_completion_pane && !self.completion_list.is_empty() { - self.completion_list.previous(); - } - } - KeyCode::Up => { - if self.show_completion_pane && !self.completion_list.is_empty() { - self.completion_list.previous(); - } else if let Some(s) = self - .command_history - .history_search(&self.command.as_str()[..self.command.pos()], HistoryDirection::Reverse) - { - let p = self.command.pos(); - self.command.update("", 0); - self.command.update(&s, std::cmp::min(s.len(), p)); - self.history_status = Some(format!( - "{} / {}", - self.command_history - .history_index() - .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) - .saturating_add(1), - self.command_history.history_len() - )); - } - } - KeyCode::Down => { - if self.show_completion_pane && !self.completion_list.is_empty() { - self.completion_list.next(); - } else if let Some(s) = self - .command_history - .history_search(&self.command.as_str()[..self.command.pos()], HistoryDirection::Forward) - { - let p = self.command.pos(); - self.command.update("", 0); - self.command.update(&s, std::cmp::min(s.len(), p)); - self.history_status = Some(format!( - "{} / {}", - self.command_history - .history_index() - .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) - .saturating_add(1), - self.command_history.history_len() - )); - } - } - - _ => { - self.command_history.reset(); - handle_movement(&mut self.command, input); - self.update_input_for_completion(); - } - }, - Action::Jump => match input { - KeyCode::Char('\n') => { - if self.error.is_some() { - self.previous_mode = Some(self.mode.clone()); - self.mode = Mode::Tasks(Action::Error); - } else { - match self.task_report_jump() { - Ok(_) => { - self.mode = Mode::Tasks(Action::Report); - self.reset_command(); - self.update(true).await?; - } - Err(e) => { - self.reset_command(); - self.error = Some(e.to_string()); - } - } - } - } - KeyCode::Esc => { - self.reset_command(); - self.mode = Mode::Tasks(Action::Report); - } - _ => handle_movement(&mut self.command, input), - }, - Action::Add => match input { - KeyCode::Esc => { - if self.show_completion_pane { - self.show_completion_pane = false; - self.completion_list.unselect(); - } else { - self.reset_command(); - self.history_status = None; - self.mode = Mode::Tasks(Action::Report); - } - } - KeyCode::Char('\n') => { - if self.show_completion_pane { - self.show_completion_pane = false; - if let Some((i, (r, m, o, _, _))) = self.completion_list.selected() { - let (before, after) = self.command.as_str().split_at(self.command.pos()); - let fs = format!("{}{}{}", before.trim_end_matches(&o), r, after); - self.command.update(&fs, self.command.pos() + r.len() - o.len()); - } - self.completion_list.unselect(); - } else if self.error.is_some() { - self.previous_mode = Some(self.mode.clone()); - self.mode = Mode::Tasks(Action::Error); - } else { - match self.task_add() { - Ok(_) => { - self.mode = Mode::Tasks(Action::Report); - self.command_history.add(self.command.as_str()); - self.reset_command(); - self.history_status = None; - self.update(true).await?; - } - Err(e) => { - self.error = Some(e); - } - } - } - } - KeyCode::Tab | KeyCode::Ctrl('n') => { - if !self.completion_list.is_empty() { - self.update_input_for_completion(); - if !self.show_completion_pane { - self.show_completion_pane = true; - } - self.completion_list.next(); - } - } - KeyCode::BackTab | KeyCode::Ctrl('p') => { - if self.show_completion_pane && !self.completion_list.is_empty() { - self.completion_list.previous(); - } - } - KeyCode::Up => { - if self.show_completion_pane && !self.completion_list.is_empty() { - self.completion_list.previous(); - } else if let Some(s) = self - .command_history - .history_search(&self.command.as_str()[..self.command.pos()], HistoryDirection::Reverse) - { - let p = self.command.pos(); - self.command.update("", 0); - self.command.update(&s, std::cmp::min(s.len(), p)); - self.history_status = Some(format!( - "{} / {}", - self.command_history - .history_index() - .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) - .saturating_add(1), - self.command_history.history_len() - )); - } - } - - KeyCode::Down => { - if self.show_completion_pane && !self.completion_list.is_empty() { - self.completion_list.next(); - } else if let Some(s) = self - .command_history - .history_search(&self.command.as_str()[..self.command.pos()], HistoryDirection::Forward) - { - let p = self.command.pos(); - self.command.update("", 0); - self.command.update(&s, std::cmp::min(s.len(), p)); - self.history_status = Some(format!( - "{} / {}", - self.command_history - .history_index() - .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) - .saturating_add(1), - self.command_history.history_len() - )); - } - } - _ => { - self.command_history.reset(); - handle_movement(&mut self.command, input); - self.update_input_for_completion(); - } - }, - Action::Filter => match input { - KeyCode::Esc => { - if self.show_completion_pane { - self.show_completion_pane = false; - self.completion_list.unselect(); - } else { - self.mode = Mode::Tasks(Action::Report); - self.filter_history.add(self.filter.as_str()); - if self.config.uda_reset_filter_on_esc { - self.filter.update("", 0); - for c in self.config.filter.chars() { - self.filter.insert(c, 1); - } - self.update_input_for_completion(); - self.dirty = true; - } - self.history_status = None; - self.update(true).await?; - } - } - KeyCode::Char('\n') => { - if self.show_completion_pane { - self.show_completion_pane = false; - if let Some((i, (r, m, o, _, _))) = self.completion_list.selected() { - let (before, after) = self.filter.as_str().split_at(self.filter.pos()); - let fs = format!("{}{}{}", before.trim_end_matches(&o), r, after); - self.filter.update(&fs, self.filter.pos() + r.len() - o.len()); - } - self.completion_list.unselect(); - self.dirty = true; - } else if self.error.is_some() { - self.previous_mode = Some(self.mode.clone()); - self.mode = Mode::Tasks(Action::Error); - } else { - self.mode = Mode::Tasks(Action::Report); - self.filter_history.add(self.filter.as_str()); - self.history_status = None; - self.update(true).await?; - } - } - KeyCode::Up => { - if self.show_completion_pane && !self.completion_list.is_empty() { - self.completion_list.previous(); - } else if let Some(s) = self - .filter_history - .history_search(&self.filter.as_str()[..self.filter.pos()], HistoryDirection::Reverse) - { - let p = self.filter.pos(); - self.filter.update("", 0); - self.filter.update(&s, std::cmp::min(p, s.len())); - self.history_status = Some(format!( - "{} / {}", - self.filter_history - .history_index() - .unwrap_or_else(|| self.filter_history.history_len().saturating_sub(1)) - .saturating_add(1), - self.filter_history.history_len() - )); - self.dirty = true; - } - } - KeyCode::Down => { - if self.show_completion_pane && !self.completion_list.is_empty() { - self.completion_list.next(); - } else if let Some(s) = self - .filter_history - .history_search(&self.filter.as_str()[..self.filter.pos()], HistoryDirection::Forward) - { - let p = self.filter.pos(); - self.filter.update("", 0); - self.filter.update(&s, std::cmp::min(p, s.len())); - self.history_status = Some(format!( - "{} / {}", - self.filter_history - .history_index() - .unwrap_or_else(|| self.filter_history.history_len().saturating_sub(1)) - .saturating_add(1), - self.filter_history.history_len() - )); - self.dirty = true; - } - } - KeyCode::Tab | KeyCode::Ctrl('n') => { - if !self.completion_list.is_empty() { - self.update_input_for_completion(); - if !self.show_completion_pane { - self.show_completion_pane = true; - } - self.completion_list.next(); - } - } - KeyCode::BackTab | KeyCode::Ctrl('p') => { - if self.show_completion_pane && !self.completion_list.is_empty() { - self.completion_list.previous(); - } - } - KeyCode::Ctrl('r') => { - self.filter.update("", 0); - for c in self.config.filter.chars() { - self.filter.insert(c, 1); - } - self.history_status = None; - self.update_input_for_completion(); - self.dirty = true; - } - _ => { - handle_movement(&mut self.filter, input); - self.update_input_for_completion(); - self.dirty = true; - } - }, - Action::DonePrompt => { - if input == self.keyconfig.done || input == KeyCode::Char('\n') { - if self.error.is_some() { - self.previous_mode = Some(self.mode.clone()); - self.mode = Mode::Tasks(Action::Error); - } else { - match self.task_done() { - Ok(_) => { - self.mode = Mode::Tasks(Action::Report); - self.update(true).await?; - } - Err(e) => { - self.error = Some(e); - } - } - } - } else if input == self.keyconfig.quit || input == KeyCode::Esc { - self.mode = Mode::Tasks(Action::Report); - } else { - handle_movement(&mut self.command, input); - } + Err(e) => { + self.error = Some(e); } - Action::DeletePrompt => { - if input == self.keyconfig.delete || input == KeyCode::Char('\n') { - if self.error.is_some() { - self.previous_mode = Some(self.mode.clone()); - self.mode = Mode::Tasks(Action::Error); - } else { - match self.task_delete() { - Ok(_) => { - self.mode = Mode::Tasks(Action::Report); - self.update(true).await?; - } - Err(e) => { - self.error = Some(e); - } - } - } - } else if input == self.keyconfig.quit || input == KeyCode::Esc { - self.mode = Mode::Tasks(Action::Report); - } else { - handle_movement(&mut self.command, input); - } + } + } + } + KeyCode::Tab | KeyCode::Ctrl('n') => { + if !self.completion_list.is_empty() { + self.update_input_for_completion(); + if !self.show_completion_pane { + self.show_completion_pane = true; + } + self.completion_list.next(); + } + } + KeyCode::BackTab | KeyCode::Ctrl('p') => { + if self.show_completion_pane && !self.completion_list.is_empty() { + self.completion_list.previous(); + } + } + KeyCode::Up => { + if self.show_completion_pane && !self.completion_list.is_empty() { + self.completion_list.previous(); + } else if let Some(s) = self + .command_history + .history_search(&self.command.as_str()[..self.command.pos()], HistoryDirection::Reverse) + { + let p = self.command.pos(); + self.command.update("", 0, &mut self.changes); + self.command.update(&s, std::cmp::min(s.len(), p), &mut self.changes); + self.history_status = Some(format!( + "{} / {}", + self + .command_history + .history_index() + .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) + .saturating_add(1), + self.command_history.history_len() + )); + } + } + KeyCode::Down => { + if self.show_completion_pane && !self.completion_list.is_empty() { + self.completion_list.next(); + } else if let Some(s) = self + .command_history + .history_search(&self.command.as_str()[..self.command.pos()], HistoryDirection::Forward) + { + let p = self.command.pos(); + self.command.update("", 0, &mut self.changes); + self.command.update(&s, std::cmp::min(s.len(), p), &mut self.changes); + self.history_status = Some(format!( + "{} / {}", + self + .command_history + .history_index() + .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) + .saturating_add(1), + self.command_history.history_len() + )); + } + } + + _ => { + self.command_history.reset(); + handle_movement(&mut self.command, input, &mut self.changes); + self.update_input_for_completion(); + } + }, + Action::Jump => match input { + KeyCode::Char('\n') => { + if self.error.is_some() { + self.previous_mode = Some(self.mode.clone()); + self.mode = Mode::Tasks(Action::Error); + } else { + match self.task_report_jump() { + Ok(_) => { + self.mode = Mode::Tasks(Action::Report); + self.reset_command(); + self.update(true).await?; } - Action::Error => { - // since filter live updates, don't reset error status - // for other actions, resetting error to None is required otherwise user cannot - // ever successfully execute mode. - if self.previous_mode != Some(Mode::Tasks(Action::Filter)) { - self.error = None; - } - self.mode = self.previous_mode.clone().unwrap_or(Mode::Tasks(Action::Report)); - self.previous_mode = None; + Err(e) => { + self.reset_command(); + self.error = Some(e.to_string()); } + } + } + } + KeyCode::Esc => { + self.reset_command(); + self.mode = Mode::Tasks(Action::Report); + } + _ => handle_movement(&mut self.command, input, &mut self.changes), + }, + Action::Add => match input { + KeyCode::Esc => { + if self.show_completion_pane { + self.show_completion_pane = false; + self.completion_list.unselect(); + } else { + self.reset_command(); + self.history_status = None; + self.mode = Mode::Tasks(Action::Report); + } + } + KeyCode::Char('\n') => { + if self.show_completion_pane { + self.show_completion_pane = false; + if let Some((i, (r, m, o, _, _))) = self.completion_list.selected() { + let (before, after) = self.command.as_str().split_at(self.command.pos()); + let fs = format!("{}{}{}", before.trim_end_matches(&o), r, after); + self.command.update(&fs, self.command.pos() + r.len() - o.len(), &mut self.changes); + } + self.completion_list.unselect(); + } else if self.error.is_some() { + self.previous_mode = Some(self.mode.clone()); + self.mode = Mode::Tasks(Action::Error); + } else { + match self.task_add() { + Ok(_) => { + self.mode = Mode::Tasks(Action::Report); + self.command_history.add(self.command.as_str()); + self.reset_command(); + self.history_status = None; + self.update(true).await?; + } + Err(e) => { + self.error = Some(e); + } + } + } + } + KeyCode::Tab | KeyCode::Ctrl('n') => { + if !self.completion_list.is_empty() { + self.update_input_for_completion(); + if !self.show_completion_pane { + self.show_completion_pane = true; + } + self.completion_list.next(); } - } - self.update_task_table_state(); - Ok(()) - } - - pub fn update_completion_list(&mut self) { - self.completion_list.clear(); - - let tasks = if self.config.uda_task_report_use_all_tasks_for_completion { - &self.all_tasks - } else { - &self.tasks - }; - - if let Mode::Tasks(Action::Modify | Action::Filter | Action::Annotate | Action::Add | Action::Log) = self.mode { - for s in vec![ - "project:".to_string(), - "priority:".to_string(), - "due:".to_string(), - "scheduled:".to_string(), - "wait:".to_string(), - "depends:".to_string(), - ] { - self.completion_list.insert(("attribute".to_string(), s)); + } + KeyCode::BackTab | KeyCode::Ctrl('p') => { + if self.show_completion_pane && !self.completion_list.is_empty() { + self.completion_list.previous(); } - } - - if let Mode::Tasks(Action::Modify | Action::Filter | Action::Annotate | Action::Add | Action::Log) = self.mode { - for s in vec![ - ".before:", - ".under:", - ".below:", - ".after:", - ".over:", - ".above:", - ".by:", - ".none:", - ".any:", - ".is:", - ".equals:", - ".isnt:", - ".not:", - ".has:", - ".contains:", - ".hasnt:", - ".startswith:", - ".left:", - ".endswith:", - ".right:", - ".word:", - ".noword:", - ] { - self.completion_list.insert(("modifier".to_string(), s.to_string())); + } + KeyCode::Up => { + if self.show_completion_pane && !self.completion_list.is_empty() { + self.completion_list.previous(); + } else if let Some(s) = self + .command_history + .history_search(&self.command.as_str()[..self.command.pos()], HistoryDirection::Reverse) + { + let p = self.command.pos(); + self.command.update("", 0, &mut self.changes); + self.command.update(&s, std::cmp::min(s.len(), p), &mut self.changes); + self.history_status = Some(format!( + "{} / {}", + self + .command_history + .history_index() + .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) + .saturating_add(1), + self.command_history.history_len() + )); } - } - - if let Mode::Tasks(Action::Modify | Action::Filter | Action::Annotate | Action::Add | Action::Log) = self.mode { - for priority in &self.config.uda_priority_values { - let p = priority.to_string(); - self.completion_list.insert(("priority".to_string(), p)); + } + + KeyCode::Down => { + if self.show_completion_pane && !self.completion_list.is_empty() { + self.completion_list.next(); + } else if let Some(s) = self + .command_history + .history_search(&self.command.as_str()[..self.command.pos()], HistoryDirection::Forward) + { + let p = self.command.pos(); + self.command.update("", 0, &mut self.changes); + self.command.update(&s, std::cmp::min(s.len(), p), &mut self.changes); + self.history_status = Some(format!( + "{} / {}", + self + .command_history + .history_index() + .unwrap_or_else(|| self.command_history.history_len().saturating_sub(1)) + .saturating_add(1), + self.command_history.history_len() + )); } - let virtual_tags = self.task_report_table.virtual_tags.clone(); - for task in tasks { - if let Some(tags) = task.tags() { - for tag in tags { - if !virtual_tags.contains(tag) { - self.completion_list - .insert(("tag".to_string(), format!("tag:{}", &tag))); - } - } + } + _ => { + self.command_history.reset(); + handle_movement(&mut self.command, input, &mut self.changes); + self.update_input_for_completion(); + } + }, + Action::Filter => match input { + KeyCode::Esc => { + if self.show_completion_pane { + self.show_completion_pane = false; + self.completion_list.unselect(); + } else { + self.mode = Mode::Tasks(Action::Report); + self.filter_history.add(self.filter.as_str()); + if self.config.uda_reset_filter_on_esc { + self.filter.update("", 0, &mut self.changes); + for c in self.config.filter.chars() { + self.filter.insert(c, 1, &mut self.changes); } + self.update_input_for_completion(); + self.dirty = true; + } + self.history_status = None; + self.update(true).await?; } - for task in tasks { - if let Some(tags) = task.tags() { - for tag in tags { - if !virtual_tags.contains(tag) { - self.completion_list.insert(("+".to_string(), format!("+{}", &tag))); - } - } - } + } + KeyCode::Char('\n') => { + if self.show_completion_pane { + self.show_completion_pane = false; + if let Some((i, (r, m, o, _, _))) = self.completion_list.selected() { + let (before, after) = self.filter.as_str().split_at(self.filter.pos()); + let fs = format!("{}{}{}", before.trim_end_matches(&o), r, after); + self.filter.update(&fs, self.filter.pos() + r.len() - o.len(), &mut self.changes); + } + self.completion_list.unselect(); + self.dirty = true; + } else if self.error.is_some() { + self.previous_mode = Some(self.mode.clone()); + self.mode = Mode::Tasks(Action::Error); + } else { + self.mode = Mode::Tasks(Action::Report); + self.filter_history.add(self.filter.as_str()); + self.history_status = None; + self.update(true).await?; } - for task in tasks { - if let Some(project) = task.project() { - let p = if project.contains(' ') { - format!(r#""{}""#, &project) - } else { - project.to_string() - }; - self.completion_list.insert(("project".to_string(), p)); - } + } + KeyCode::Up => { + if self.show_completion_pane && !self.completion_list.is_empty() { + self.completion_list.previous(); + } else if let Some(s) = self + .filter_history + .history_search(&self.filter.as_str()[..self.filter.pos()], HistoryDirection::Reverse) + { + let p = self.filter.pos(); + self.filter.update("", 0, &mut self.changes); + self.filter.update(&s, std::cmp::min(p, s.len()), &mut self.changes); + self.history_status = Some(format!( + "{} / {}", + self + .filter_history + .history_index() + .unwrap_or_else(|| self.filter_history.history_len().saturating_sub(1)) + .saturating_add(1), + self.filter_history.history_len() + )); + self.dirty = true; } - for task in tasks { - if let Some(date) = task.due() { - self.completion_list - .insert(("due".to_string(), get_formatted_datetime(date))); - } + } + KeyCode::Down => { + if self.show_completion_pane && !self.completion_list.is_empty() { + self.completion_list.next(); + } else if let Some(s) = self + .filter_history + .history_search(&self.filter.as_str()[..self.filter.pos()], HistoryDirection::Forward) + { + let p = self.filter.pos(); + self.filter.update("", 0, &mut self.changes); + self.filter.update(&s, std::cmp::min(p, s.len()), &mut self.changes); + self.history_status = Some(format!( + "{} / {}", + self + .filter_history + .history_index() + .unwrap_or_else(|| self.filter_history.history_len().saturating_sub(1)) + .saturating_add(1), + self.filter_history.history_len() + )); + self.dirty = true; } - for task in tasks { - if let Some(date) = task.wait() { - self.completion_list - .insert(("wait".to_string(), get_formatted_datetime(date))); - } + } + KeyCode::Tab | KeyCode::Ctrl('n') => { + if !self.completion_list.is_empty() { + self.update_input_for_completion(); + if !self.show_completion_pane { + self.show_completion_pane = true; + } + self.completion_list.next(); } - for task in tasks { - if let Some(date) = task.scheduled() { - self.completion_list - .insert(("scheduled".to_string(), get_formatted_datetime(date))); + } + KeyCode::BackTab | KeyCode::Ctrl('p') => { + if self.show_completion_pane && !self.completion_list.is_empty() { + self.completion_list.previous(); + } + } + KeyCode::Ctrl('r') => { + self.filter.update("", 0, &mut self.changes); + for c in self.config.filter.chars() { + self.filter.insert(c, 1, &mut self.changes); + } + self.history_status = None; + self.update_input_for_completion(); + self.dirty = true; + } + _ => { + handle_movement(&mut self.filter, input, &mut self.changes); + self.update_input_for_completion(); + self.dirty = true; + } + }, + Action::DonePrompt => { + if input == self.keyconfig.done || input == KeyCode::Char('\n') { + if self.error.is_some() { + self.previous_mode = Some(self.mode.clone()); + self.mode = Mode::Tasks(Action::Error); + } else { + match self.task_done() { + Ok(_) => { + self.mode = Mode::Tasks(Action::Report); + self.update(true).await?; + } + Err(e) => { + self.error = Some(e); } + } } - for task in tasks { - if let Some(date) = task.end() { - self.completion_list - .insert(("end".to_string(), get_formatted_datetime(date))); + } else if input == self.keyconfig.quit || input == KeyCode::Esc { + self.mode = Mode::Tasks(Action::Report); + } else { + handle_movement(&mut self.command, input, &mut self.changes); + } + } + Action::DeletePrompt => { + if input == self.keyconfig.delete || input == KeyCode::Char('\n') { + if self.error.is_some() { + self.previous_mode = Some(self.mode.clone()); + self.mode = Mode::Tasks(Action::Error); + } else { + match self.task_delete() { + Ok(_) => { + self.mode = Mode::Tasks(Action::Report); + self.update(true).await?; + } + Err(e) => { + self.error = Some(e); } + } } - } + } else if input == self.keyconfig.quit || input == KeyCode::Esc { + self.mode = Mode::Tasks(Action::Report); + } else { + handle_movement(&mut self.command, input, &mut self.changes); + } + } + Action::Error => { + // since filter live updates, don't reset error status + // for other actions, resetting error to None is required otherwise user cannot + // ever successfully execute mode. + if self.previous_mode != Some(Mode::Tasks(Action::Filter)) { + self.error = None; + } + self.mode = self.previous_mode.clone().unwrap_or(Mode::Tasks(Action::Report)); + self.previous_mode = None; + } + } + } + self.update_task_table_state(); + Ok(()) + } - if self.mode == Mode::Tasks(Action::Filter) { - self.completion_list.insert(("status".to_string(), "pending".into())); - self.completion_list.insert(("status".to_string(), "completed".into())); - self.completion_list.insert(("status".to_string(), "deleted".into())); - self.completion_list.insert(("status".to_string(), "recurring".into())); - } + pub fn update_completion_list(&mut self) { + self.completion_list.clear(); + + let tasks = if self.config.uda_task_report_use_all_tasks_for_completion { + &self.all_tasks + } else { + &self.tasks + }; + + if let Mode::Tasks(Action::Modify | Action::Filter | Action::Annotate | Action::Add | Action::Log) = self.mode { + for s in vec![ + "project:".to_string(), + "priority:".to_string(), + "due:".to_string(), + "scheduled:".to_string(), + "wait:".to_string(), + "depends:".to_string(), + ] { + self.completion_list.insert(("attribute".to_string(), s)); + } } - pub fn update_input_for_completion(&mut self) { - match self.mode { - Mode::Tasks(Action::Add | Action::Annotate | Action::Log) => { - let i = get_start_word_under_cursor(self.command.as_str(), self.command.pos()); - let input = self.command.as_str()[i..self.command.pos()].to_string(); - self.completion_list.input(input, "".to_string()); - } - Mode::Tasks(Action::Modify) => { - let i = get_start_word_under_cursor(self.modify.as_str(), self.modify.pos()); - let input = self.modify.as_str()[i..self.modify.pos()].to_string(); - self.completion_list.input(input, "".to_string()); + if let Mode::Tasks(Action::Modify | Action::Filter | Action::Annotate | Action::Add | Action::Log) = self.mode { + for s in vec![ + ".before:", + ".under:", + ".below:", + ".after:", + ".over:", + ".above:", + ".by:", + ".none:", + ".any:", + ".is:", + ".equals:", + ".isnt:", + ".not:", + ".has:", + ".contains:", + ".hasnt:", + ".startswith:", + ".left:", + ".endswith:", + ".right:", + ".word:", + ".noword:", + ] { + self.completion_list.insert(("modifier".to_string(), s.to_string())); + } + } + + if let Mode::Tasks(Action::Modify | Action::Filter | Action::Annotate | Action::Add | Action::Log) = self.mode { + for priority in &self.config.uda_priority_values { + let p = priority.to_string(); + self.completion_list.insert(("priority".to_string(), p)); + } + let virtual_tags = self.task_report_table.virtual_tags.clone(); + for task in tasks { + if let Some(tags) = task.tags() { + for tag in tags { + if !virtual_tags.contains(tag) { + self.completion_list.insert(("tag".to_string(), format!("tag:{}", &tag))); } - Mode::Tasks(Action::Filter) => { - let i = get_start_word_under_cursor(self.filter.as_str(), self.filter.pos()); - let input = self.filter.as_str()[i..self.filter.pos()].to_string(); - self.completion_list.input(input, "".to_string()); + } + } + } + for task in tasks { + if let Some(tags) = task.tags() { + for tag in tags { + if !virtual_tags.contains(tag) { + self.completion_list.insert(("+".to_string(), format!("+{}", &tag))); } - _ => {} - } + } + } + } + for task in tasks { + if let Some(project) = task.project() { + let p = if project.contains(' ') { + format!(r#""{}""#, &project) + } else { + project.to_string() + }; + self.completion_list.insert(("project".to_string(), p)); + } + } + for task in tasks { + if let Some(date) = task.due() { + self.completion_list.insert(("due".to_string(), get_formatted_datetime(date))); + } + } + for task in tasks { + if let Some(date) = task.wait() { + self.completion_list.insert(("wait".to_string(), get_formatted_datetime(date))); + } + } + for task in tasks { + if let Some(date) = task.scheduled() { + self.completion_list.insert(("scheduled".to_string(), get_formatted_datetime(date))); + } + } + for task in tasks { + if let Some(date) = task.end() { + self.completion_list.insert(("end".to_string(), get_formatted_datetime(date))); + } + } + } + + if self.mode == Mode::Tasks(Action::Filter) { + self.completion_list.insert(("status".to_string(), "pending".into())); + self.completion_list.insert(("status".to_string(), "completed".into())); + self.completion_list.insert(("status".to_string(), "deleted".into())); + self.completion_list.insert(("status".to_string(), "recurring".into())); + } + } + + pub fn update_input_for_completion(&mut self) { + match self.mode { + Mode::Tasks(Action::Add | Action::Annotate | Action::Log) => { + let i = get_start_word_under_cursor(self.command.as_str(), self.command.pos()); + let input = self.command.as_str()[i..self.command.pos()].to_string(); + self.completion_list.input(input, "".to_string()); + } + Mode::Tasks(Action::Modify) => { + let i = get_start_word_under_cursor(self.modify.as_str(), self.modify.pos()); + let input = self.modify.as_str()[i..self.modify.pos()].to_string(); + self.completion_list.input(input, "".to_string()); + } + Mode::Tasks(Action::Filter) => { + let i = get_start_word_under_cursor(self.filter.as_str(), self.filter.pos()); + let input = self.filter.as_str()[i..self.filter.pos()].to_string(); + self.completion_list.input(input, "".to_string()); + } + _ => {} } + } } -pub fn handle_movement(linebuffer: &mut LineBuffer, input: KeyCode) { - match input { - KeyCode::Ctrl('f') | KeyCode::Right => { - linebuffer.move_forward(1); - } - KeyCode::Ctrl('b') | KeyCode::Left => { - linebuffer.move_backward(1); - } - KeyCode::Ctrl('h') | KeyCode::Backspace => { - linebuffer.backspace(1); - } - KeyCode::Ctrl('d') | KeyCode::Delete => { - linebuffer.delete(1); - } - KeyCode::Ctrl('a') | KeyCode::Home => { - linebuffer.move_home(); - } - KeyCode::Ctrl('e') | KeyCode::End => { - linebuffer.move_end(); - } - KeyCode::Ctrl('k') => { - linebuffer.kill_line(); - } - KeyCode::Ctrl('u') => { - linebuffer.discard_line(); - } - KeyCode::Ctrl('w') | KeyCode::AltBackspace | KeyCode::CtrlBackspace => { - linebuffer.delete_prev_word(Word::Emacs, 1); - } - KeyCode::Alt('d') | KeyCode::AltDelete | KeyCode::CtrlDelete => { - linebuffer.delete_word(At::AfterEnd, Word::Emacs, 1); - } - KeyCode::Alt('f') => { - linebuffer.move_to_next_word(At::AfterEnd, Word::Emacs, 1); - } - KeyCode::Alt('b') => { - linebuffer.move_to_prev_word(Word::Emacs, 1); - } - KeyCode::Alt('t') => { - linebuffer.transpose_words(1); - } - KeyCode::Char(c) => { - linebuffer.insert(c, 1); - } - _ => {} +pub fn handle_movement(linebuffer: &mut LineBuffer, input: KeyCode, changes: &mut utils::Changeset) { + match input { + KeyCode::Ctrl('f') | KeyCode::Right => { + linebuffer.move_forward(1); + } + KeyCode::Ctrl('b') | KeyCode::Left => { + linebuffer.move_backward(1); + } + KeyCode::Ctrl('h') | KeyCode::Backspace => { + linebuffer.backspace(1, changes); } + KeyCode::Ctrl('d') | KeyCode::Delete => { + linebuffer.delete(1, changes); + } + KeyCode::Ctrl('a') | KeyCode::Home => { + linebuffer.move_home(); + } + KeyCode::Ctrl('e') | KeyCode::End => { + linebuffer.move_end(); + } + KeyCode::Ctrl('k') => { + linebuffer.kill_line(changes); + } + KeyCode::Ctrl('u') => { + linebuffer.discard_line(changes); + } + KeyCode::Ctrl('w') | KeyCode::AltBackspace | KeyCode::CtrlBackspace => { + linebuffer.delete_prev_word(Word::Emacs, 1, changes); + } + KeyCode::Alt('d') | KeyCode::AltDelete | KeyCode::CtrlDelete => { + linebuffer.delete_word(At::AfterEnd, Word::Emacs, 1, changes); + } + KeyCode::Alt('f') => { + linebuffer.move_to_next_word(At::AfterEnd, Word::Emacs, 1); + } + KeyCode::Alt('b') => { + linebuffer.move_to_prev_word(Word::Emacs, 1); + } + KeyCode::Alt('t') => { + linebuffer.transpose_words(1, changes); + } + KeyCode::Char(c) => { + linebuffer.insert(c, 1, changes); + } + _ => {} + } } pub fn add_tag(task: &mut Task, tag: String) { - match task.tags_mut() { - Some(t) => t.push(tag), - None => task.set_tags(Some(vec![tag])), - } + match task.tags_mut() { + Some(t) => t.push(tag), + None => task.set_tags(Some(vec![tag])), + } } pub fn remove_tag(task: &mut Task, tag: &str) { - if let Some(t) = task.tags_mut() { - if let Some(index) = t.iter().position(|x| *x == tag) { - t.remove(index); - } + if let Some(t) = task.tags_mut() { + if let Some(index) = t.iter().position(|x| *x == tag) { + t.remove(index); } + } } #[cfg(test)] mod tests { - use super::*; - use std::ffi::OsStr; - use std::fs::File; - use std::path::Path; - use std::{fmt::Write, io}; - use tui::backend::TestBackend; - use tui::buffer::Buffer; - - /// Returns a string representation of the given buffer for debugging purpose. - fn buffer_view(buffer: &Buffer) -> String { - let mut view = String::with_capacity(buffer.content.len() + buffer.area.height as usize * 3); - for cells in buffer.content.chunks(buffer.area.width as usize) { - let mut overwritten = vec![]; - let mut skip: usize = 0; - view.push('"'); - for (x, c) in cells.iter().enumerate() { - if skip == 0 { - view.push_str(&c.symbol); - } else { - overwritten.push((x, &c.symbol)) - } - skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1); - } - view.push('"'); - if !overwritten.is_empty() { - write!(&mut view, " Hidden by multi-width symbols: {:?}", overwritten).unwrap(); - } - view.push('\n'); - } - view - } - - #[test] - fn test_centered_rect() { - assert_eq!( - centered_rect(50, 50, Rect::new(0, 0, 100, 100)), - Rect::new(25, 25, 50, 50) - ); + use super::*; + use std::ffi::OsStr; + use std::fs::File; + use std::path::Path; + use std::{fmt::Write, io}; + use tui::backend::TestBackend; + use tui::buffer::Buffer; + + /// Returns a string representation of the given buffer for debugging purpose. + fn buffer_view(buffer: &Buffer) -> String { + let mut view = String::with_capacity(buffer.content.len() + buffer.area.height as usize * 3); + for cells in buffer.content.chunks(buffer.area.width as usize) { + let mut overwritten = vec![]; + let mut skip: usize = 0; + view.push('"'); + for (x, c) in cells.iter().enumerate() { + if skip == 0 { + view.push_str(&c.symbol); + } else { + overwritten.push((x, &c.symbol)) + } + skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1); + } + view.push('"'); + if !overwritten.is_empty() { + write!(&mut view, " Hidden by multi-width symbols: {:?}", overwritten).unwrap(); + } + view.push('\n'); } - - fn setup() { - use std::process::Stdio; - let mut f = File::open(Path::new(env!("TASKDATA")).parent().unwrap().join("export.json")).unwrap(); - let mut s = String::new(); - f.read_to_string(&mut s).unwrap(); - let tasks = task_hookrs::import::import(s.as_bytes()).unwrap(); - // tasks.iter_mut().find(| t | t.id().unwrap() == 1).unwrap().priority_mut().replace(&mut "H".to_string()); - // tasks.iter_mut().find(| t | t.id().unwrap() == 2).unwrap().priority_mut().replace(&mut "H".to_string()); - // tasks.iter_mut().find(| t | t.id().unwrap() == 4).unwrap().tags_mut().replace(&mut vec!["test".to_string(), "another tag".to_string()]); - assert!(task_hookrs::tw::save(&tasks).is_ok()); + view + } + + #[test] + fn test_centered_rect() { + assert_eq!(centered_rect(50, 50, Rect::new(0, 0, 100, 100)), Rect::new(25, 25, 50, 50)); + } + + fn setup() { + use std::process::Stdio; + let mut f = File::open(Path::new(env!("TASKDATA")).parent().unwrap().join("export.json")).unwrap(); + let mut s = String::new(); + f.read_to_string(&mut s).unwrap(); + let tasks = task_hookrs::import::import(s.as_bytes()).unwrap(); + // tasks.iter_mut().find(| t | t.id().unwrap() == 1).unwrap().priority_mut().replace(&mut "H".to_string()); + // tasks.iter_mut().find(| t | t.id().unwrap() == 2).unwrap().priority_mut().replace(&mut "H".to_string()); + // tasks.iter_mut().find(| t | t.id().unwrap() == 4).unwrap().tags_mut().replace(&mut vec!["test".to_string(), "another tag".to_string()]); + assert!(task_hookrs::tw::save(&tasks).is_ok()); + } + + fn teardown() { + let cd = Path::new(env!("TASKDATA")); + std::fs::remove_dir_all(cd).unwrap(); + } + + async fn test_taskwarrior_tui_history() { + let mut app = TaskwarriorTui::new("next", false).await.unwrap(); + // setup(); + app.mode = Mode::Tasks(Action::Add); + app.update_completion_list(); + let input = "Wash car"; + for c in input.chars() { + app.handle_input(KeyCode::Char(c)).await.unwrap(); } - - fn teardown() { - let cd = Path::new(env!("TASKDATA")); - std::fs::remove_dir_all(cd).unwrap(); + app.handle_input(KeyCode::Right).await.unwrap(); + let input = " +test"; + for c in input.chars() { + app.handle_input(KeyCode::Char(c)).await.unwrap(); } + app.handle_input(KeyCode::Char('\n')).await.unwrap(); - async fn test_taskwarrior_tui_history() { - let mut app = TaskwarriorTui::new("next", false).await.unwrap(); - // setup(); - app.mode = Mode::Tasks(Action::Add); - app.update_completion_list(); - let input = "Wash car"; - for c in input.chars() { - app.handle_input(KeyCode::Char(c)).await.unwrap(); - } - app.handle_input(KeyCode::Right).await.unwrap(); - let input = " +test"; - for c in input.chars() { - app.handle_input(KeyCode::Char(c)).await.unwrap(); - } - app.handle_input(KeyCode::Char('\n')).await.unwrap(); - - app.mode = Mode::Tasks(Action::Add); - - app.update_completion_list(); - - let backend = TestBackend::new(50, 15); - let mut terminal = Terminal::new(backend).unwrap(); - terminal - .draw(|f| { - app.draw(f); - app.draw(f); - }) - .unwrap(); - - let input = "Buy groceries"; - for c in input.chars() { - app.handle_input(KeyCode::Char(c)).await.unwrap(); - } - app.handle_input(KeyCode::Right).await.unwrap(); - let input = " +test"; - for c in input.chars() { - app.handle_input(KeyCode::Char(c)).await.unwrap(); - } - app.update(true).await.unwrap(); - app.handle_input(KeyCode::Down).await.unwrap(); - - assert_eq!("\"Buy groceries\" +test", app.command.as_str()); + app.mode = Mode::Tasks(Action::Add); - app.handle_input(KeyCode::Char('\n')).await.unwrap(); + app.update_completion_list(); - app.mode = Mode::Tasks(Action::Add); - app.update_completion_list(); + let backend = TestBackend::new(50, 15); + let mut terminal = Terminal::new(backend).unwrap(); + terminal + .draw(|f| { + app.draw(f); + app.draw(f); + }) + .unwrap(); - let backend = TestBackend::new(50, 15); - let mut terminal = Terminal::new(backend).unwrap(); - terminal - .draw(|f| { - app.draw(f); - app.draw(f); - }) - .unwrap(); - - let input = "Buy groceries"; - for c in input.chars() { - app.handle_input(KeyCode::Char(c)).await.unwrap(); - } - app.handle_input(KeyCode::Right).await.unwrap(); - app.handle_input(KeyCode::Backspace).await.unwrap(); - app.update(true).await.unwrap(); - app.handle_input(KeyCode::Down).await.unwrap(); - - assert_eq!("\"Buy groceries", app.command.as_str()); - - app.update(true).await.unwrap(); - - app.handle_input(KeyCode::Up).await.unwrap(); - - assert_eq!("\"Buy groceries\" +test", app.command.as_str()); - // teardown(); + let input = "Buy groceries"; + for c in input.chars() { + app.handle_input(KeyCode::Char(c)).await.unwrap(); } - - #[test] - fn test_taskwarrior_tui() { - let r = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap() - .block_on(async { _test_taskwarrior_tui().await }); + app.handle_input(KeyCode::Right).await.unwrap(); + let input = " +test"; + for c in input.chars() { + app.handle_input(KeyCode::Char(c)).await.unwrap(); } + app.update(true).await.unwrap(); + app.handle_input(KeyCode::Down).await.unwrap(); - async fn _test_taskwarrior_tui() { - let app = TaskwarriorTui::new("next", false).await.unwrap(); - - assert!(app.task_by_index(0).is_none(), "Expected task data to be empty but found {} tasks. Delete contents of {:?} and {:?} and run the tests again.", app.tasks.len(), Path::new(env!("TASKDATA")), Path::new(env!("TASKDATA")).parent().unwrap().join(".config")); + assert_eq!("\"Buy groceries\" +test", app.command.as_str()); - let app = TaskwarriorTui::new("next", false).await.unwrap(); - assert!(app - .task_by_uuid(Uuid::parse_str("3f43831b-88dc-45e2-bf0d-4aea6db634cc").unwrap()) - .is_none()); + app.handle_input(KeyCode::Char('\n')).await.unwrap(); - test_draw_empty_task_report().await; + app.mode = Mode::Tasks(Action::Add); + app.update_completion_list(); - test_draw_calendar().await; - test_draw_help_popup().await; + let backend = TestBackend::new(50, 15); + let mut terminal = Terminal::new(backend).unwrap(); + terminal + .draw(|f| { + app.draw(f); + app.draw(f); + }) + .unwrap(); - setup(); + let input = "Buy groceries"; + for c in input.chars() { + app.handle_input(KeyCode::Char(c)).await.unwrap(); + } + app.handle_input(KeyCode::Right).await.unwrap(); + app.handle_input(KeyCode::Backspace).await.unwrap(); + app.update(true).await.unwrap(); + app.handle_input(KeyCode::Down).await.unwrap(); + + assert_eq!("\"Buy groceries", app.command.as_str()); + + app.update(true).await.unwrap(); + + app.handle_input(KeyCode::Up).await.unwrap(); + + assert_eq!("\"Buy groceries\" +test", app.command.as_str()); + // teardown(); + } + + #[test] + fn test_taskwarrior_tui() { + let r = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { _test_taskwarrior_tui().await }); + } + + async fn _test_taskwarrior_tui() { + let app = TaskwarriorTui::new("next", false).await.unwrap(); + + assert!( + app.task_by_index(0).is_none(), + "Expected task data to be empty but found {} tasks. Delete contents of {:?} and {:?} and run the tests again.", + app.tasks.len(), + Path::new(env!("TASKDATA")), + Path::new(env!("TASKDATA")).parent().unwrap().join(".config") + ); + + let app = TaskwarriorTui::new("next", false).await.unwrap(); + assert!(app + .task_by_uuid(Uuid::parse_str("3f43831b-88dc-45e2-bf0d-4aea6db634cc").unwrap()) + .is_none()); + + test_draw_empty_task_report().await; + + test_draw_calendar().await; + test_draw_help_popup().await; + + setup(); + + let app = TaskwarriorTui::new("next", false).await.unwrap(); + assert!(app.task_by_index(0).is_some()); + + let app = TaskwarriorTui::new("next", false).await.unwrap(); + assert!(app + .task_by_uuid(Uuid::parse_str("3f43831b-88dc-45e2-bf0d-4aea6db634cc").unwrap()) + .is_some()); + + test_draw_task_report_with_extended_modify_command().await; + // test_draw_task_report(); + test_task_tags().await; + test_task_style().await; + test_task_context().await; + test_task_tomorrow().await; + test_task_earlier_today().await; + test_task_later_today().await; + test_taskwarrior_tui_history().await; + + teardown(); + } + + async fn test_task_tags() { + // testing tags + let app = TaskwarriorTui::new("next", false).await.unwrap(); + let task = app.task_by_id(1).unwrap(); + + let tags = vec!["PENDING".to_string(), "PRIORITY".to_string()]; + + for tag in tags { + assert!(task.tags().unwrap().contains(&tag)); + } - let app = TaskwarriorTui::new("next", false).await.unwrap(); - assert!(app.task_by_index(0).is_some()); + let mut app = TaskwarriorTui::new("next", false).await.unwrap(); + let task = app.task_by_id(11).unwrap(); + let tags = vec!["finance", "UNBLOCKED", "PENDING", "TAGGED", "UDA"] + .iter() + .map(ToString::to_string) + .collect::>(); + for tag in tags { + assert!(task.tags().unwrap().contains(&tag)); + } - let app = TaskwarriorTui::new("next", false).await.unwrap(); - assert!(app - .task_by_uuid(Uuid::parse_str("3f43831b-88dc-45e2-bf0d-4aea6db634cc").unwrap()) - .is_some()); + if let Some(task) = app.task_by_id(11) { + let i = app.task_index_by_uuid(*task.uuid()).unwrap_or_default(); + app.current_selection = i; + app.current_selection_id = None; + app.current_selection_uuid = None; + } - test_draw_task_report_with_extended_modify_command().await; - // test_draw_task_report(); - test_task_tags().await; - test_task_style().await; - test_task_context().await; - test_task_tomorrow().await; - test_task_earlier_today().await; - test_task_later_today().await; - test_taskwarrior_tui_history().await; + app.task_quick_tag().unwrap(); + app.update(true).await.unwrap(); - teardown(); + let task = app.task_by_id(11).unwrap(); + let tags = vec!["next", "finance", "UNBLOCKED", "PENDING", "TAGGED", "UDA"] + .iter() + .map(ToString::to_string) + .collect::>(); + for tag in tags { + assert!(task.tags().unwrap().contains(&tag)); } - async fn test_task_tags() { - // testing tags - let app = TaskwarriorTui::new("next", false).await.unwrap(); - let task = app.task_by_id(1).unwrap(); + app.task_quick_tag().unwrap(); + app.update(true).await.unwrap(); - let tags = vec!["PENDING".to_string(), "PRIORITY".to_string()]; + let task = app.task_by_id(11).unwrap(); + let tags = vec!["finance", "UNBLOCKED", "PENDING", "TAGGED", "UDA"] + .iter() + .map(ToString::to_string) + .collect::>(); + for tag in tags { + assert!(task.tags().unwrap().contains(&tag)); + } + } + + async fn test_task_style() { + let app = TaskwarriorTui::new("next", false).await.unwrap(); + let task = app.task_by_id(1).unwrap(); + for r in vec![ + "active", + "blocked", + "blocking", + "completed", + "deleted", + "due", + "due.today", + "keyword.", + "overdue", + "project.", + "recurring", + "scheduled", + "tag.", + "tagged", + "uda.", + ] { + assert!(app.config.rule_precedence_color.contains(&r.to_string())); + } + let style = app.style_for_task(&task); - for tag in tags { - assert!(task.tags().unwrap().contains(&tag)); - } + assert_eq!(style, Style::default().fg(Color::Indexed(2))); - let mut app = TaskwarriorTui::new("next", false).await.unwrap(); - let task = app.task_by_id(11).unwrap(); - let tags = vec!["finance", "UNBLOCKED", "PENDING", "TAGGED", "UDA"] - .iter() - .map(ToString::to_string) - .collect::>(); - for tag in tags { - assert!(task.tags().unwrap().contains(&tag)); - } + let task = app.task_by_id(11).unwrap(); + let style = app.style_for_task(&task); + } - if let Some(task) = app.task_by_id(11) { - let i = app.task_index_by_uuid(*task.uuid()).unwrap_or_default(); - app.current_selection = i; - app.current_selection_id = None; - app.current_selection_uuid = None; - } + async fn test_task_context() { + let mut app = TaskwarriorTui::new("next", false).await.unwrap(); - app.task_quick_tag().unwrap(); - app.update(true).await.unwrap(); + assert!(app.update(true).await.is_ok()); - let task = app.task_by_id(11).unwrap(); - let tags = vec!["next", "finance", "UNBLOCKED", "PENDING", "TAGGED", "UDA"] - .iter() - .map(ToString::to_string) - .collect::>(); - for tag in tags { - assert!(task.tags().unwrap().contains(&tag)); - } + app.context_select().unwrap(); - app.task_quick_tag().unwrap(); - app.update(true).await.unwrap(); + assert_eq!(app.tasks.len(), 26); + assert_eq!(app.current_context_filter, ""); - let task = app.task_by_id(11).unwrap(); - let tags = vec!["finance", "UNBLOCKED", "PENDING", "TAGGED", "UDA"] - .iter() - .map(ToString::to_string) - .collect::>(); - for tag in tags { - assert!(task.tags().unwrap().contains(&tag)); - } - } + assert_eq!(app.contexts.table_state.current_selection(), Some(0)); + app.context_next(); + app.context_next(); + app.context_select().unwrap(); + assert_eq!(app.contexts.table_state.current_selection(), Some(2)); - async fn test_task_style() { - let app = TaskwarriorTui::new("next", false).await.unwrap(); - let task = app.task_by_id(1).unwrap(); - for r in vec![ - "active", - "blocked", - "blocking", - "completed", - "deleted", - "due", - "due.today", - "keyword.", - "overdue", - "project.", - "recurring", - "scheduled", - "tag.", - "tagged", - "uda.", - ] { - assert!(app.config.rule_precedence_color.contains(&r.to_string())); - } - let style = app.style_for_task(&task); + assert!(app.update(true).await.is_ok()); - assert_eq!(style, Style::default().fg(Color::Indexed(2))); + assert_eq!(app.tasks.len(), 1); + assert_eq!(app.current_context_filter, "+finance -private"); - let task = app.task_by_id(11).unwrap(); - let style = app.style_for_task(&task); - } + assert_eq!(app.contexts.table_state.current_selection(), Some(2)); + app.context_previous(); + app.context_previous(); + app.context_select().unwrap(); + assert_eq!(app.contexts.table_state.current_selection(), Some(0)); - async fn test_task_context() { - let mut app = TaskwarriorTui::new("next", false).await.unwrap(); + assert!(app.update(true).await.is_ok()); - assert!(app.update(true).await.is_ok()); + assert_eq!(app.tasks.len(), 26); + assert_eq!(app.current_context_filter, ""); + } - app.context_select().unwrap(); + async fn test_task_tomorrow() { + let total_tasks: u64 = 26; - assert_eq!(app.tasks.len(), 26); - assert_eq!(app.current_context_filter, ""); + let mut app = TaskwarriorTui::new("next", false).await.unwrap(); + assert!(app.update(true).await.is_ok()); + assert_eq!(app.tasks.len(), total_tasks as usize); + assert_eq!(app.current_context_filter, ""); - assert_eq!(app.contexts.table_state.current_selection(), Some(0)); - app.context_next(); - app.context_next(); - app.context_select().unwrap(); - assert_eq!(app.contexts.table_state.current_selection(), Some(2)); + let now = Local::now(); + let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc()); - assert!(app.update(true).await.is_ok()); + let mut command = std::process::Command::new("task"); + command.arg("add"); + let tomorrow = now + chrono::Duration::days(1); + let message = format!( + "'new task for testing tomorrow' due:{:04}-{:02}-{:02}", + tomorrow.year(), + tomorrow.month(), + tomorrow.day(), + ); + + let shell = message.as_str().replace("'", "\\'"); + let cmd = shlex::split(&shell).unwrap(); + for s in cmd { + command.arg(&s); + } + let output = command.output().unwrap(); + let s = String::from_utf8_lossy(&output.stdout); + let re = Regex::new(r"^Created task (?P\d+).\n$").unwrap(); + let caps = re.captures(&s).unwrap(); - assert_eq!(app.tasks.len(), 1); - assert_eq!(app.current_context_filter, "+finance -private"); + let task_id = caps["task_id"].parse::().unwrap(); + assert_eq!(task_id, total_tasks + 1); - assert_eq!(app.contexts.table_state.current_selection(), Some(2)); - app.context_previous(); - app.context_previous(); - app.context_select().unwrap(); - assert_eq!(app.contexts.table_state.current_selection(), Some(0)); + assert!(app.update(true).await.is_ok()); + assert_eq!(app.tasks.len(), (total_tasks + 1) as usize); + assert_eq!(app.current_context_filter, ""); - assert!(app.update(true).await.is_ok()); + let task = app.task_by_id(task_id).unwrap(); - assert_eq!(app.tasks.len(), 26); - assert_eq!(app.current_context_filter, ""); + for s in &["DUE", "MONTH", "PENDING", "QUARTER", "TOMORROW", "UDA", "UNBLOCKED", "YEAR"] { + if !(task.tags().unwrap().contains(&s.to_string())) { + println!("Expected {} to be in tags", s); + } } - async fn test_task_tomorrow() { - let total_tasks: u64 = 26; + let output = std::process::Command::new("task") + .arg("rc.confirmation=off") + .arg("undo") + .output() + .unwrap(); - let mut app = TaskwarriorTui::new("next", false).await.unwrap(); - assert!(app.update(true).await.is_ok()); - assert_eq!(app.tasks.len(), total_tasks as usize); - assert_eq!(app.current_context_filter, ""); + let mut app = TaskwarriorTui::new("next", false).await.unwrap(); + assert!(app.update(true).await.is_ok()); + assert_eq!(app.tasks.len(), total_tasks as usize); + assert_eq!(app.current_context_filter, ""); + } - let now = Local::now(); - let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc()); + async fn test_task_earlier_today() { + let total_tasks: u64 = 26; - let mut command = std::process::Command::new("task"); - command.arg("add"); - let tomorrow = now + chrono::Duration::days(1); - let message = format!( - "'new task for testing tomorrow' due:{:04}-{:02}-{:02}", - tomorrow.year(), - tomorrow.month(), - tomorrow.day(), - ); + let mut app = TaskwarriorTui::new("next", false).await.unwrap(); + assert!(app.update(true).await.is_ok()); + assert_eq!(app.tasks.len(), total_tasks as usize); + assert_eq!(app.current_context_filter, ""); - let shell = message.as_str().replace("'", "\\'"); - let cmd = shlex::split(&shell).unwrap(); - for s in cmd { - command.arg(&s); - } - let output = command.output().unwrap(); - let s = String::from_utf8_lossy(&output.stdout); - let re = Regex::new(r"^Created task (?P\d+).\n$").unwrap(); - let caps = re.captures(&s).unwrap(); - - let task_id = caps["task_id"].parse::().unwrap(); - assert_eq!(task_id, total_tasks + 1); - - assert!(app.update(true).await.is_ok()); - assert_eq!(app.tasks.len(), (total_tasks + 1) as usize); - assert_eq!(app.current_context_filter, ""); - - let task = app.task_by_id(task_id).unwrap(); - - for s in &[ - "DUE", - "MONTH", - "PENDING", - "QUARTER", - "TOMORROW", - "UDA", - "UNBLOCKED", - "YEAR", - ] { - if !(task.tags().unwrap().contains(&s.to_string())) { - println!("Expected {} to be in tags", s); - } - } + let now = Local::now(); + let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc()); - let output = std::process::Command::new("task") - .arg("rc.confirmation=off") - .arg("undo") - .output() - .unwrap(); + let mut command = std::process::Command::new("task"); + command.arg("add"); + let message = "'new task for testing earlier today' due:now"; - let mut app = TaskwarriorTui::new("next", false).await.unwrap(); - assert!(app.update(true).await.is_ok()); - assert_eq!(app.tasks.len(), total_tasks as usize); - assert_eq!(app.current_context_filter, ""); + let shell = message.replace("'", "\\'"); + let cmd = shlex::split(&shell).unwrap(); + for s in cmd { + command.arg(&s); + } + let output = command.output().unwrap(); + let s = String::from_utf8_lossy(&output.stdout); + let re = Regex::new(r"^Created task (?P\d+).\n$").unwrap(); + let caps = re.captures(&s).unwrap(); + let task_id = caps["task_id"].parse::().unwrap(); + assert_eq!(task_id, total_tasks + 1); + + assert!(app.update(true).await.is_ok()); + assert_eq!(app.tasks.len(), (total_tasks + 1) as usize); + assert_eq!(app.current_context_filter, ""); + + let task = app.task_by_id(task_id).unwrap(); + for s in &[ + "DUE", + "DUETODAY", + "MONTH", + "OVERDUE", + "PENDING", + "QUARTER", + "TODAY", + "UDA", + "UNBLOCKED", + "YEAR", + ] { + assert!(task.tags().unwrap().contains(&s.to_string())); } - async fn test_task_earlier_today() { - let total_tasks: u64 = 26; - - let mut app = TaskwarriorTui::new("next", false).await.unwrap(); - assert!(app.update(true).await.is_ok()); - assert_eq!(app.tasks.len(), total_tasks as usize); - assert_eq!(app.current_context_filter, ""); - - let now = Local::now(); - let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc()); - - let mut command = std::process::Command::new("task"); - command.arg("add"); - let message = "'new task for testing earlier today' due:now"; + let output = std::process::Command::new("task") + .arg("rc.confirmation=off") + .arg("undo") + .output() + .unwrap(); - let shell = message.replace("'", "\\'"); - let cmd = shlex::split(&shell).unwrap(); - for s in cmd { - command.arg(&s); - } - let output = command.output().unwrap(); - let s = String::from_utf8_lossy(&output.stdout); - let re = Regex::new(r"^Created task (?P\d+).\n$").unwrap(); - let caps = re.captures(&s).unwrap(); - let task_id = caps["task_id"].parse::().unwrap(); - assert_eq!(task_id, total_tasks + 1); - - assert!(app.update(true).await.is_ok()); - assert_eq!(app.tasks.len(), (total_tasks + 1) as usize); - assert_eq!(app.current_context_filter, ""); - - let task = app.task_by_id(task_id).unwrap(); - for s in &[ - "DUE", - "DUETODAY", - "MONTH", - "OVERDUE", - "PENDING", - "QUARTER", - "TODAY", - "UDA", - "UNBLOCKED", - "YEAR", - ] { - assert!(task.tags().unwrap().contains(&s.to_string())); - } + let mut app = TaskwarriorTui::new("next", false).await.unwrap(); + assert!(app.update(true).await.is_ok()); + assert_eq!(app.tasks.len(), total_tasks as usize); + assert_eq!(app.current_context_filter, ""); + } - let output = std::process::Command::new("task") - .arg("rc.confirmation=off") - .arg("undo") - .output() - .unwrap(); - - let mut app = TaskwarriorTui::new("next", false).await.unwrap(); - assert!(app.update(true).await.is_ok()); - assert_eq!(app.tasks.len(), total_tasks as usize); - assert_eq!(app.current_context_filter, ""); - } - - async fn test_task_later_today() { - let total_tasks: u64 = 26; - - let mut app = TaskwarriorTui::new("next", false).await.unwrap(); - assert!(app.update(true).await.is_ok()); - assert_eq!(app.tasks.len(), total_tasks as usize); - assert_eq!(app.current_context_filter, ""); - - let now = Local::now(); - let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc()); - - let mut command = std::process::Command::new("task"); - command.arg("add"); - let message = format!( - "'new task for testing later today' due:'{:04}-{:02}-{:02}T{:02}:{:02}:{:02}'", - now.year(), - now.month(), - now.day(), - now.hour(), - now.minute() + 1, - now.second(), - ); + async fn test_task_later_today() { + let total_tasks: u64 = 26; - let shell = message.as_str().replace("'", "\\'"); - let cmd = shlex::split(&shell).unwrap(); - for s in cmd { - command.arg(&s); - } - let output = command.output().unwrap(); - let s = String::from_utf8_lossy(&output.stdout); - let re = Regex::new(r"^Created task (?P\d+).\n$").unwrap(); - let caps = re.captures(&s).unwrap(); - let task_id = caps["task_id"].parse::().unwrap(); - assert_eq!(task_id, total_tasks + 1); - - assert!(app.update(true).await.is_ok()); - assert_eq!(app.tasks.len(), (total_tasks + 1) as usize); - assert_eq!(app.current_context_filter, ""); - - let task = app.task_by_id(task_id).unwrap(); - for s in &[ - "DUE", - "DUETODAY", - "MONTH", - "PENDING", - "QUARTER", - "TODAY", - "UDA", - "UNBLOCKED", - "YEAR", - ] { - assert!(task.tags().unwrap().contains(&s.to_string())); - } + let mut app = TaskwarriorTui::new("next", false).await.unwrap(); + assert!(app.update(true).await.is_ok()); + assert_eq!(app.tasks.len(), total_tasks as usize); + assert_eq!(app.current_context_filter, ""); - let output = std::process::Command::new("task") - .arg("rc.confirmation=off") - .arg("undo") - .output() - .unwrap(); - - let mut app = TaskwarriorTui::new("next", false).await.unwrap(); - assert!(app.update(true).await.is_ok()); - assert_eq!(app.tasks.len(), total_tasks as usize); - assert_eq!(app.current_context_filter, ""); - } - - async fn test_draw_empty_task_report() { - let mut expected = Buffer::with_lines(vec![ - " Tasks Projects Calendar [none]", - " ", - " ", - " ", - " ", - " ", - " ", - "──────────────────────────────────────────────────", - "Task not found ", - " ", - " ", - " ", - " ", - "Filter Tasks ", - "(status:pending or status:waiting) ", - ]); - - for i in 0..=49 { - // First line - expected - .get_mut(i, 0) - .set_style(Style::default().add_modifier(Modifier::REVERSED)); - } - for i in 1..=5 { - // Tasks - expected.get_mut(i, 0).set_style( - Style::default() - .add_modifier(Modifier::BOLD) - .add_modifier(Modifier::REVERSED), - ); - } - for i in 0..=49 { - // Command line - expected - .get_mut(i, 13) - .set_style(Style::default().add_modifier(Modifier::REVERSED)); - } + let now = Local::now(); + let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc()); - let mut app = TaskwarriorTui::new("next", false).await.unwrap(); + let mut command = std::process::Command::new("task"); + command.arg("add"); + let message = format!( + "'new task for testing later today' due:'{:04}-{:02}-{:02}T{:02}:{:02}:{:02}'", + now.year(), + now.month(), + now.day(), + now.hour(), + now.minute() + 1, + now.second(), + ); + + let shell = message.as_str().replace("'", "\\'"); + let cmd = shlex::split(&shell).unwrap(); + for s in cmd { + command.arg(&s); + } + let output = command.output().unwrap(); + let s = String::from_utf8_lossy(&output.stdout); + let re = Regex::new(r"^Created task (?P\d+).\n$").unwrap(); + let caps = re.captures(&s).unwrap(); + let task_id = caps["task_id"].parse::().unwrap(); + assert_eq!(task_id, total_tasks + 1); + + assert!(app.update(true).await.is_ok()); + assert_eq!(app.tasks.len(), (total_tasks + 1) as usize); + assert_eq!(app.current_context_filter, ""); + + let task = app.task_by_id(task_id).unwrap(); + for s in &["DUE", "DUETODAY", "MONTH", "PENDING", "QUARTER", "TODAY", "UDA", "UNBLOCKED", "YEAR"] { + assert!(task.tags().unwrap().contains(&s.to_string())); + } - app.task_report_next(); - app.context_next(); + let output = std::process::Command::new("task") + .arg("rc.confirmation=off") + .arg("undo") + .output() + .unwrap(); + + let mut app = TaskwarriorTui::new("next", false).await.unwrap(); + assert!(app.update(true).await.is_ok()); + assert_eq!(app.tasks.len(), total_tasks as usize); + assert_eq!(app.current_context_filter, ""); + } + + async fn test_draw_empty_task_report() { + let mut expected = Buffer::with_lines(vec![ + " Tasks Projects Calendar [none]", + " ", + " ", + " ", + " ", + " ", + " ", + "──────────────────────────────────────────────────", + "Task not found ", + " ", + " ", + " ", + " ", + "Filter Tasks ", + "(status:pending or status:waiting) ", + ]); + + for i in 0..=49 { + // First line + expected.get_mut(i, 0).set_style(Style::default().add_modifier(Modifier::REVERSED)); + } + for i in 1..=5 { + // Tasks + expected + .get_mut(i, 0) + .set_style(Style::default().add_modifier(Modifier::BOLD).add_modifier(Modifier::REVERSED)); + } + for i in 0..=49 { + // Command line + expected.get_mut(i, 13).set_style(Style::default().add_modifier(Modifier::REVERSED)); + } - let total_tasks: u64 = 0; + let mut app = TaskwarriorTui::new("next", false).await.unwrap(); - assert!(app.update(true).await.is_ok()); - assert_eq!(app.tasks.len(), total_tasks as usize); - assert_eq!(app.current_context_filter, ""); + app.task_report_next(); + app.context_next(); - let now = Local::now(); - let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc()); + let total_tasks: u64 = 0; - app.update(true).await.unwrap(); + assert!(app.update(true).await.is_ok()); + assert_eq!(app.tasks.len(), total_tasks as usize); + assert_eq!(app.current_context_filter, ""); - let backend = TestBackend::new(50, 15); - let mut terminal = Terminal::new(backend).unwrap(); - terminal - .draw(|f| { - app.draw(f); - }) - .unwrap(); + let now = Local::now(); + let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc()); - assert_eq!(terminal.backend().size().unwrap(), expected.area); - terminal.backend().assert_buffer(&expected); + app.update(true).await.unwrap(); + + let backend = TestBackend::new(50, 15); + let mut terminal = Terminal::new(backend).unwrap(); + terminal + .draw(|f| { + app.draw(f); + }) + .unwrap(); + + assert_eq!(terminal.backend().size().unwrap(), expected.area); + terminal.backend().assert_buffer(&expected); + } + + async fn test_draw_task_report_with_extended_modify_command() { + let mut expected1 = Buffer::with_lines(vec![ + "Modify Task 10 ", + " based on your .taskrc ", + " ", + ]); + + let mut expected2 = Buffer::with_lines(vec![ + "Modify Task 10 ", + "Support color for tasks b", + " ", + ]); + + for i in 0..=13 { + // Task + expected1.get_mut(i, 0).set_style(Style::default().add_modifier(Modifier::BOLD)); + expected2.get_mut(i, 0).set_style(Style::default().add_modifier(Modifier::BOLD)); + } + for i in 0..=24 { + // Command line + expected1.get_mut(i, 0).set_style(Style::default().add_modifier(Modifier::REVERSED)); + expected2.get_mut(i, 0).set_style(Style::default().add_modifier(Modifier::REVERSED)); } - async fn test_draw_task_report_with_extended_modify_command() { - let mut expected1 = Buffer::with_lines(vec![ - "Modify Task 10 ", - " based on your .taskrc ", - " ", - ]); + let mut app = TaskwarriorTui::new("next", false).await.unwrap(); - let mut expected2 = Buffer::with_lines(vec![ - "Modify Task 10 ", - "Support color for tasks b", - " ", - ]); + let total_tasks: u64 = 26; - for i in 0..=13 { - // Task - expected1 - .get_mut(i, 0) - .set_style(Style::default().add_modifier(Modifier::BOLD)); - expected2 - .get_mut(i, 0) - .set_style(Style::default().add_modifier(Modifier::BOLD)); - } - for i in 0..=24 { - // Command line - expected1 - .get_mut(i, 0) - .set_style(Style::default().add_modifier(Modifier::REVERSED)); - expected2 - .get_mut(i, 0) - .set_style(Style::default().add_modifier(Modifier::REVERSED)); - } + assert!(app.update(true).await.is_ok()); + assert_eq!(app.tasks.len(), total_tasks as usize); + assert_eq!(app.current_context_filter, ""); - let mut app = TaskwarriorTui::new("next", false).await.unwrap(); + let now = Local::now(); + let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc()); - let total_tasks: u64 = 26; + app.mode = Mode::Tasks(Action::Modify); + match app.task_table_state.mode() { + TableMode::SingleSelection => match app.task_current() { + Some(t) => { + let s = format!("{} ", t.description()); + app.modify.update(&s, s.as_str().len(), &mut app.changes) + } + None => app.modify.update("", 0, &mut app.changes), + }, + TableMode::MultipleSelection => app.modify.update("", 0, &mut app.changes), + } - assert!(app.update(true).await.is_ok()); - assert_eq!(app.tasks.len(), total_tasks as usize); - assert_eq!(app.current_context_filter, ""); + app.update(true).await.unwrap(); - let now = Local::now(); - let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc()); + let backend = TestBackend::new(25, 3); + let mut terminal = Terminal::new(backend).unwrap(); + terminal + .draw(|f| { + let rects = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(0), Constraint::Length(3)].as_ref()) + .split(f.size()); - app.mode = Mode::Tasks(Action::Modify); - match app.task_table_state.mode() { - TableMode::SingleSelection => match app.task_current() { - Some(t) => { - let s = format!("{} ", t.description()); - app.modify.update(&s, s.as_str().len()) + let position = TaskwarriorTui::get_position(&app.modify); + f.set_cursor( + std::cmp::min(rects[1].x + position as u16, rects[1].x + rects[1].width.saturating_sub(2)), + rects[1].y + 1, + ); + f.render_widget(Clear, rects[1]); + let selected = app.current_selection; + let task_ids = if app.tasks.is_empty() { + vec!["0".to_string()] + } else { + match app.task_table_state.mode() { + TableMode::SingleSelection => { + vec![app.tasks[selected].id().unwrap_or_default().to_string()] + } + TableMode::MultipleSelection => { + let mut tids = vec![]; + for uuid in app.marked.iter() { + if let Some(t) = app.task_by_uuid(*uuid) { + tids.push(t.id().unwrap_or_default().to_string()); } - None => app.modify.update("", 0), - }, - TableMode::MultipleSelection => app.modify.update("", 0), - } - - app.update(true).await.unwrap(); - - let backend = TestBackend::new(25, 3); - let mut terminal = Terminal::new(backend).unwrap(); - terminal - .draw(|f| { - let rects = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(0), Constraint::Length(3)].as_ref()) - .split(f.size()); - - let position = TaskwarriorTui::get_position(&app.modify); - f.set_cursor( - std::cmp::min( - rects[1].x + position as u16, - rects[1].x + rects[1].width.saturating_sub(2), - ), - rects[1].y + 1, - ); - f.render_widget(Clear, rects[1]); - let selected = app.current_selection; - let task_ids = if app.tasks.is_empty() { - vec!["0".to_string()] - } else { - match app.task_table_state.mode() { - TableMode::SingleSelection => { - vec![app.tasks[selected].id().unwrap_or_default().to_string()] - } - TableMode::MultipleSelection => { - let mut tids = vec![]; - for uuid in app.marked.iter() { - if let Some(t) = app.task_by_uuid(*uuid) { - tids.push(t.id().unwrap_or_default().to_string()); - } - } - tids - } - } - }; - let label = if task_ids.len() > 1 { - format!("Modify Tasks {}", task_ids.join(",")) - } else { - format!("Modify Task {}", task_ids.join(",")) - }; - app.draw_command( - f, - rects[1], - app.modify.as_str(), - (Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), None), - position, - true, - app.error.clone(), - ); - }) - .unwrap(); - - assert_eq!(terminal.backend().size().unwrap(), expected1.area); - terminal.backend().assert_buffer(&expected1); - - app.modify.move_home(); - - terminal - .draw(|f| { - let rects = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(0), Constraint::Length(3)].as_ref()) - .split(f.size()); - - let position = TaskwarriorTui::get_position(&app.modify); - f.set_cursor( - std::cmp::min( - rects[1].x + position as u16, - rects[1].x + rects[1].width.saturating_sub(2), - ), - rects[1].y + 1, - ); - f.render_widget(Clear, rects[1]); - let selected = app.current_selection; - let task_ids = if app.tasks.is_empty() { - vec!["0".to_string()] - } else { - match app.task_table_state.mode() { - TableMode::SingleSelection => { - vec![app.tasks[selected].id().unwrap_or_default().to_string()] - } - TableMode::MultipleSelection => { - let mut tids = vec![]; - for uuid in app.marked.iter() { - if let Some(t) = app.task_by_uuid(*uuid) { - tids.push(t.id().unwrap_or_default().to_string()); - } - } - tids - } - } - }; - let label = if task_ids.len() > 1 { - format!("Modify Tasks {}", task_ids.join(",")) - } else { - format!("Modify Task {}", task_ids.join(",")) - }; - app.draw_command( - f, - rects[1], - app.modify.as_str(), - (Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), None), - position, - true, - app.error.clone(), - ); - }) - .unwrap(); - - assert_eq!(terminal.backend().size().unwrap(), expected2.area); - terminal.backend().assert_buffer(&expected2); - } - - async fn test_draw_task_report() { - let mut expected = Buffer::with_lines(vec![ - "╭Task|Calendar───────────────────────────────────╮", - "│ ID Age Deps P Projec Tag Due Descrip Urg │", - "│ │", - "│• 27 0s U new ta… 15.00│", - "│ 28 0s U none new ta… 15.00│", - "╰────────────────────────────────────────────────╯", - "╭Task 27─────────────────────────────────────────╮", - "│ │", - "│Name Value │", - "│------------- ----------------------------------│", - "│ID 27 │", - "╰────────────────────────────────────────────────╯", - "╭Filter Tasks────────────────────────────────────╮", - "│(status:pending or status:waiting) │", - "╰────────────────────────────────────────────────╯", - ]); - - for i in 1..=4 { - // Task - expected - .get_mut(i, 0) - .set_style(Style::default().add_modifier(Modifier::BOLD)); - } - for i in 6..=13 { - // Calendar - expected - .get_mut(i, 0) - .set_style(Style::default().add_modifier(Modifier::DIM)); - } - - for r in &[ - 1..=4, // ID - 6..=8, // Age - 10..=13, // Deps - 15..=15, // P - 17..=22, // Projec - 24..=30, // Tag - 32..=34, // Due - 36..=42, // Descr - 44..=48, // Urg - ] { - for i in r.clone() { - expected - .get_mut(i, 1) - .set_style(Style::default().add_modifier(Modifier::UNDERLINED)); + } + tids } - } - - for i in 1..expected.area().width - 1 { - expected.get_mut(i, 3).set_style( - Style::default() - .fg(Color::Indexed(1)) - .bg(Color::Reset) - .add_modifier(Modifier::BOLD), - ); - } - - for i in 1..expected.area().width - 1 { - expected - .get_mut(i, 4) - .set_style(Style::default().fg(Color::Indexed(1)).bg(Color::Indexed(4))); - } - - let mut app = TaskwarriorTui::new("next", false).await.unwrap(); - - app.task_report_next(); - app.context_next(); - - let total_tasks: u64 = 26; + } + }; + let label = if task_ids.len() > 1 { + format!("Modify Tasks {}", task_ids.join(",")) + } else { + format!("Modify Task {}", task_ids.join(",")) + }; + app.draw_command( + f, + rects[1], + app.modify.as_str(), + (Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), None), + position, + true, + app.error.clone(), + ); + }) + .unwrap(); - assert!(app.update(true).await.is_ok()); - assert_eq!(app.tasks.len(), total_tasks as usize); - assert_eq!(app.current_context_filter, ""); + assert_eq!(terminal.backend().size().unwrap(), expected1.area); + terminal.backend().assert_buffer(&expected1); - let now = Local::now(); - let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc()); + app.modify.move_home(); - let mut command = std::process::Command::new("task"); - command.arg("add"); - let message = "'new task 1 for testing draw' priority:U"; + terminal + .draw(|f| { + let rects = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(0), Constraint::Length(3)].as_ref()) + .split(f.size()); - let shell = message.replace("'", "\\'"); - let cmd = shlex::split(&shell).unwrap(); - for s in cmd { - command.arg(&s); - } - let output = command.output().unwrap(); - let s = String::from_utf8_lossy(&output.stdout); - let re = Regex::new(r"^Created task (?P\d+).\n$").unwrap(); - let caps = re.captures(&s).unwrap(); - let task_id = caps["task_id"].parse::().unwrap(); - assert_eq!(task_id, total_tasks + 1); - - let mut command = std::process::Command::new("task"); - command.arg("add"); - let message = "'new task 2 for testing draw' priority:U +none"; - - let shell = message.replace("'", "\\'"); - let cmd = shlex::split(&shell).unwrap(); - for s in cmd { - command.arg(&s); - } - let output = command.output().unwrap(); - let s = String::from_utf8_lossy(&output.stdout); - let re = Regex::new(r"^Created task (?P\d+).\n$").unwrap(); - let caps = re.captures(&s).unwrap(); - let task_id = caps["task_id"].parse::().unwrap(); - assert_eq!(task_id, total_tasks + 2); - - app.task_report_next(); - app.task_report_previous(); - app.task_report_next_page(); - app.task_report_previous_page(); - app.task_report_bottom(); - app.task_report_top(); - app.update(true).await.unwrap(); - - let backend = TestBackend::new(50, 15); - let mut terminal = Terminal::new(backend).unwrap(); - app.task_report_show_info = !app.task_report_show_info; - terminal - .draw(|f| { - app.draw(f); - app.draw(f); - }) - .unwrap(); - app.task_report_show_info = !app.task_report_show_info; - terminal - .draw(|f| { - app.draw(f); - app.draw(f); - }) - .unwrap(); + let position = TaskwarriorTui::get_position(&app.modify); + f.set_cursor( + std::cmp::min(rects[1].x + position as u16, rects[1].x + rects[1].width.saturating_sub(2)), + rects[1].y + 1, + ); + f.render_widget(Clear, rects[1]); + let selected = app.current_selection; + let task_ids = if app.tasks.is_empty() { + vec!["0".to_string()] + } else { + match app.task_table_state.mode() { + TableMode::SingleSelection => { + vec![app.tasks[selected].id().unwrap_or_default().to_string()] + } + TableMode::MultipleSelection => { + let mut tids = vec![]; + for uuid in app.marked.iter() { + if let Some(t) = app.task_by_uuid(*uuid) { + tids.push(t.id().unwrap_or_default().to_string()); + } + } + tids + } + } + }; + let label = if task_ids.len() > 1 { + format!("Modify Tasks {}", task_ids.join(",")) + } else { + format!("Modify Task {}", task_ids.join(",")) + }; + app.draw_command( + f, + rects[1], + app.modify.as_str(), + (Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), None), + position, + true, + app.error.clone(), + ); + }) + .unwrap(); + + assert_eq!(terminal.backend().size().unwrap(), expected2.area); + terminal.backend().assert_buffer(&expected2); + } + + async fn test_draw_task_report() { + let mut expected = Buffer::with_lines(vec![ + "╭Task|Calendar───────────────────────────────────╮", + "│ ID Age Deps P Projec Tag Due Descrip Urg │", + "│ │", + "│• 27 0s U new ta… 15.00│", + "│ 28 0s U none new ta… 15.00│", + "╰────────────────────────────────────────────────╯", + "╭Task 27─────────────────────────────────────────╮", + "│ │", + "│Name Value │", + "│------------- ----------------------------------│", + "│ID 27 │", + "╰────────────────────────────────────────────────╯", + "╭Filter Tasks────────────────────────────────────╮", + "│(status:pending or status:waiting) │", + "╰────────────────────────────────────────────────╯", + ]); + + for i in 1..=4 { + // Task + expected.get_mut(i, 0).set_style(Style::default().add_modifier(Modifier::BOLD)); + } + for i in 6..=13 { + // Calendar + expected.get_mut(i, 0).set_style(Style::default().add_modifier(Modifier::DIM)); + } - let output = std::process::Command::new("task") - .arg("rc.confirmation=off") - .arg("undo") - .output() - .unwrap(); - let output = std::process::Command::new("task") - .arg("rc.confirmation=off") - .arg("undo") - .output() - .unwrap(); - - assert_eq!(terminal.backend().size().unwrap(), expected.area); - terminal.backend().assert_buffer(&expected); - } - - async fn test_draw_calendar() { - let mut expected = Buffer::with_lines(vec![ - " Tasks Projects Calendar [none]", - " ", - " 2020 ", - " ", - " January February ", - " Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa ", - " 1 2 3 4 1 ", - " 5 6 7 8 9 10 11 2 3 4 5 6 7 8 ", - " 12 13 14 15 16 17 18 9 10 11 12 13 14 15 ", - " 19 20 21 22 23 24 25 16 17 18 19 20 21 22 ", - " 26 27 28 29 30 31 23 24 25 26 27 28 29 ", - " ", - " ", - " ", - " ", - ]); - - for i in 0..=49 { - // First line - expected - .get_mut(i, 0) - .set_style(Style::default().add_modifier(Modifier::REVERSED)); - } - for i in 20..=27 { - // Calendar - expected.get_mut(i, 0).set_style( - Style::default() - .add_modifier(Modifier::BOLD) - .add_modifier(Modifier::REVERSED), - ); - } + for r in &[ + 1..=4, // ID + 6..=8, // Age + 10..=13, // Deps + 15..=15, // P + 17..=22, // Projec + 24..=30, // Tag + 32..=34, // Due + 36..=42, // Descr + 44..=48, // Urg + ] { + for i in r.clone() { + expected.get_mut(i, 1).set_style(Style::default().add_modifier(Modifier::UNDERLINED)); + } + } - for i in 0..=49 { - expected - .get_mut(i, 2) - .set_style(Style::default().add_modifier(Modifier::UNDERLINED)); - } + for i in 1..expected.area().width - 1 { + expected + .get_mut(i, 3) + .set_style(Style::default().fg(Color::Indexed(1)).bg(Color::Reset).add_modifier(Modifier::BOLD)); + } - for i in 3..=22 { - expected.get_mut(i, 4).set_style(Style::default().bg(Color::Reset)); - } + for i in 1..expected.area().width - 1 { + expected + .get_mut(i, 4) + .set_style(Style::default().fg(Color::Indexed(1)).bg(Color::Indexed(4))); + } - for i in 25..=44 { - expected.get_mut(i, 4).set_style(Style::default().bg(Color::Reset)); - } + let mut app = TaskwarriorTui::new("next", false).await.unwrap(); - for i in 3..=22 { - expected - .get_mut(i, 5) - .set_style(Style::default().bg(Color::Reset).add_modifier(Modifier::UNDERLINED)); - } + app.task_report_next(); + app.context_next(); - for i in 25..=44 { - expected - .get_mut(i, 5) - .set_style(Style::default().bg(Color::Reset).add_modifier(Modifier::UNDERLINED)); - } + let total_tasks: u64 = 26; - let mut app = TaskwarriorTui::new("next", false).await.unwrap(); - - app.task_report_next(); - app.context_next(); - app.update(true).await.unwrap(); - - app.calendar_year = 2020; - app.mode = Mode::Calendar; - - let backend = TestBackend::new(50, 15); - let mut terminal = Terminal::new(backend).unwrap(); - terminal - .draw(|f| { - app.draw(f); - app.draw(f); - }) - .unwrap(); - - assert_eq!(terminal.backend().size().unwrap(), expected.area); - terminal.backend().assert_buffer(&expected); - } - - async fn test_draw_help_popup() { - let mut expected = Buffer::with_lines(vec![ - "╭Help──────────────────────────────────╮", - "│# Default Keybindings │", - "│ │", - "│Keybindings: │", - "│ │", - "│ Esc: │", - "│ │", - "│ ]: Next view │", - "│ │", - "│ [: Previous view │", - "│ │", - "╰──────────────────────────────────────╯", - ]); - - for i in 1..=4 { - // Calendar - expected - .get_mut(i, 0) - .set_style(Style::default().add_modifier(Modifier::BOLD)); - } + assert!(app.update(true).await.is_ok()); + assert_eq!(app.tasks.len(), total_tasks as usize); + assert_eq!(app.current_context_filter, ""); - let mut app = TaskwarriorTui::new("next", false).await.unwrap(); - - app.mode = Mode::Tasks(Action::HelpPopup); - app.task_report_next(); - app.context_next(); - app.update(true).await.unwrap(); - - let backend = TestBackend::new(40, 12); - let mut terminal = Terminal::new(backend).unwrap(); - terminal - .draw(|f| { - app.draw_help_popup(f, 100, 100); - }) - .unwrap(); - - assert_eq!(terminal.backend().size().unwrap(), expected.area); - terminal.backend().assert_buffer(&expected); - } - - // #[test] - async fn test_draw_context_menu() { - let mut expected = Buffer::with_lines(vec![ - "╭Context───────────────────────────────────────────────────────────────────────╮", - "│Name Description Active│", - "│ │", - "│• none yes │", - "│ finance +finance -private no │", - "│ personal +personal -private no │", - "│ work -personal -private no │", - "│ │", - "│ │", - "╰──────────────────────────────────────────────────────────────────────────────╯", - ]); - - for i in 1..=7 { - // Task - expected - .get_mut(i, 0) - .set_style(Style::default().add_modifier(Modifier::BOLD)); - } + let now = Local::now(); + let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc()); - for i in 1..=10 { - // Task - expected - .get_mut(i, 1) - .set_style(Style::default().add_modifier(Modifier::UNDERLINED)); - } + let mut command = std::process::Command::new("task"); + command.arg("add"); + let message = "'new task 1 for testing draw' priority:U"; - for i in 12..=71 { - // Task - expected - .get_mut(i, 1) - .set_style(Style::default().add_modifier(Modifier::UNDERLINED)); - } + let shell = message.replace("'", "\\'"); + let cmd = shlex::split(&shell).unwrap(); + for s in cmd { + command.arg(&s); + } + let output = command.output().unwrap(); + let s = String::from_utf8_lossy(&output.stdout); + let re = Regex::new(r"^Created task (?P\d+).\n$").unwrap(); + let caps = re.captures(&s).unwrap(); + let task_id = caps["task_id"].parse::().unwrap(); + assert_eq!(task_id, total_tasks + 1); + + let mut command = std::process::Command::new("task"); + command.arg("add"); + let message = "'new task 2 for testing draw' priority:U +none"; + + let shell = message.replace("'", "\\'"); + let cmd = shlex::split(&shell).unwrap(); + for s in cmd { + command.arg(&s); + } + let output = command.output().unwrap(); + let s = String::from_utf8_lossy(&output.stdout); + let re = Regex::new(r"^Created task (?P\d+).\n$").unwrap(); + let caps = re.captures(&s).unwrap(); + let task_id = caps["task_id"].parse::().unwrap(); + assert_eq!(task_id, total_tasks + 2); + + app.task_report_next(); + app.task_report_previous(); + app.task_report_next_page(); + app.task_report_previous_page(); + app.task_report_bottom(); + app.task_report_top(); + app.update(true).await.unwrap(); + + let backend = TestBackend::new(50, 15); + let mut terminal = Terminal::new(backend).unwrap(); + app.task_report_show_info = !app.task_report_show_info; + terminal + .draw(|f| { + app.draw(f); + app.draw(f); + }) + .unwrap(); + app.task_report_show_info = !app.task_report_show_info; + terminal + .draw(|f| { + app.draw(f); + app.draw(f); + }) + .unwrap(); + + let output = std::process::Command::new("task") + .arg("rc.confirmation=off") + .arg("undo") + .output() + .unwrap(); + let output = std::process::Command::new("task") + .arg("rc.confirmation=off") + .arg("undo") + .output() + .unwrap(); + + assert_eq!(terminal.backend().size().unwrap(), expected.area); + terminal.backend().assert_buffer(&expected); + } + + async fn test_draw_calendar() { + let mut expected = Buffer::with_lines(vec![ + " Tasks Projects Calendar [none]", + " ", + " 2020 ", + " ", + " January February ", + " Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa ", + " 1 2 3 4 1 ", + " 5 6 7 8 9 10 11 2 3 4 5 6 7 8 ", + " 12 13 14 15 16 17 18 9 10 11 12 13 14 15 ", + " 19 20 21 22 23 24 25 16 17 18 19 20 21 22 ", + " 26 27 28 29 30 31 23 24 25 26 27 28 29 ", + " ", + " ", + " ", + " ", + ]); + + for i in 0..=49 { + // First line + expected.get_mut(i, 0).set_style(Style::default().add_modifier(Modifier::REVERSED)); + } + for i in 20..=27 { + // Calendar + expected + .get_mut(i, 0) + .set_style(Style::default().add_modifier(Modifier::BOLD).add_modifier(Modifier::REVERSED)); + } - for i in 73..=78 { - // Task - expected - .get_mut(i, 1) - .set_style(Style::default().add_modifier(Modifier::UNDERLINED)); - } + for i in 0..=49 { + expected.get_mut(i, 2).set_style(Style::default().add_modifier(Modifier::UNDERLINED)); + } - for i in 1..=78 { - // Task - expected - .get_mut(i, 3) - .set_style(Style::default().add_modifier(Modifier::BOLD)); - } + for i in 3..=22 { + expected.get_mut(i, 4).set_style(Style::default().bg(Color::Reset)); + } - let mut app = TaskwarriorTui::new("next", false).await.unwrap(); + for i in 25..=44 { + expected.get_mut(i, 4).set_style(Style::default().bg(Color::Reset)); + } - app.mode = Mode::Tasks(Action::ContextMenu); - app.task_report_next(); - app.update(true).await.unwrap(); + for i in 3..=22 { + expected + .get_mut(i, 5) + .set_style(Style::default().bg(Color::Reset).add_modifier(Modifier::UNDERLINED)); + } - let backend = TestBackend::new(80, 10); - let mut terminal = Terminal::new(backend).unwrap(); - terminal - .draw(|f| { - app.draw_context_menu(f, 100, 100); - app.draw_context_menu(f, 100, 100); - }) - .unwrap(); + for i in 25..=44 { + expected + .get_mut(i, 5) + .set_style(Style::default().bg(Color::Reset).add_modifier(Modifier::UNDERLINED)); + } - assert_eq!(terminal.backend().size().unwrap(), expected.area); - terminal.backend().assert_buffer(&expected); + let mut app = TaskwarriorTui::new("next", false).await.unwrap(); + + app.task_report_next(); + app.context_next(); + app.update(true).await.unwrap(); + + app.calendar_year = 2020; + app.mode = Mode::Calendar; + + app.update(true).await.unwrap(); + + let backend = TestBackend::new(50, 15); + let mut terminal = Terminal::new(backend).unwrap(); + terminal + .draw(|f| { + app.draw(f); + app.draw(f); + }) + .unwrap(); + + assert_eq!(terminal.backend().size().unwrap(), expected.area); + terminal.backend().assert_buffer(&expected); + } + + async fn test_draw_help_popup() { + let mut expected = Buffer::with_lines(vec![ + "╭Help──────────────────────────────────╮", + "│# Default Keybindings │", + "│ │", + "│Keybindings: │", + "│ │", + "│ Esc: │", + "│ │", + "│ ]: Next view │", + "│ │", + "│ [: Previous view │", + "╰──────────────────────────────────────╯", + "9% ─────────────────────────────────────", + ]); + + for i in 1..=4 { + // Calendar + expected.get_mut(i, 0).set_style(Style::default().add_modifier(Modifier::BOLD)); + } + expected.get_mut(3, 11).set_style(Style::default().fg(Color::Gray)); + expected.get_mut(4, 11).set_style(Style::default().fg(Color::Gray)); + expected.get_mut(5, 11).set_style(Style::default().fg(Color::Gray)); + + let mut app = TaskwarriorTui::new("next", false).await.unwrap(); + + app.mode = Mode::Tasks(Action::HelpPopup); + app.task_report_next(); + app.context_next(); + app.update(true).await.unwrap(); + + let backend = TestBackend::new(40, 12); + let mut terminal = Terminal::new(backend).unwrap(); + terminal + .draw(|f| { + app.draw_help_popup(f, 100, 100); + }) + .unwrap(); + + assert_eq!(terminal.backend().size().unwrap(), expected.area); + terminal.backend().assert_buffer(&expected); + } + + // #[test] + async fn test_draw_context_menu() { + let mut expected = Buffer::with_lines(vec![ + "╭Context───────────────────────────────────────────────────────────────────────╮", + "│Name Description Active│", + "│ │", + "│• none yes │", + "│ finance +finance -private no │", + "│ personal +personal -private no │", + "│ work -personal -private no │", + "│ │", + "│ │", + "╰──────────────────────────────────────────────────────────────────────────────╯", + ]); + + for i in 1..=7 { + // Task + expected.get_mut(i, 0).set_style(Style::default().add_modifier(Modifier::BOLD)); } - // #[test] - async fn test_graphemes() { - dbg!("写作业".graphemes(true).count()); - dbg!(UnicodeWidthStr::width("写作业")); - dbg!(UnicodeWidthStr::width("abc")); + for i in 1..=10 { + // Task + expected.get_mut(i, 1).set_style(Style::default().add_modifier(Modifier::UNDERLINED)); + } - let mut app = TaskwarriorTui::new("next", false).await.unwrap(); + for i in 12..=71 { + // Task + expected.get_mut(i, 1).set_style(Style::default().add_modifier(Modifier::UNDERLINED)); + } - if let Some(task) = app.task_by_id(27) { - let i = app.task_index_by_uuid(*task.uuid()).unwrap_or_default(); - app.current_selection = i; - app.current_selection_id = None; - app.current_selection_uuid = None; - } - app.update(true).await.unwrap(); - app.mode = Mode::Tasks(Action::Modify); - match app.task_current() { - Some(t) => { - let s = format!("{} ", t.description()); - app.modify.update(&s, s.as_str().len()) - } - None => app.modify.update("", 0), - } - app.update(true).await.unwrap(); + for i in 73..=78 { + // Task + expected.get_mut(i, 1).set_style(Style::default().add_modifier(Modifier::UNDERLINED)); + } - dbg!(app.modify.as_str()); - dbg!(app.modify.as_str().len()); - dbg!(app.modify.graphemes(true).count()); - dbg!(app.modify.pos()); - let position = TaskwarriorTui::get_position(&app.modify); - dbg!(position); - } - - // #[test] - async fn test_taskwarrior_tui_completion() { - let mut app = TaskwarriorTui::new("next", false).await.unwrap(); - app.handle_input(KeyCode::Char('z')).await.unwrap(); - app.mode = Mode::Tasks(Action::Add); - app.update_completion_list(); - let input = "Wash car"; - for c in input.chars() { - app.handle_input(KeyCode::Char(c)).await.unwrap(); - } - app.handle_input(KeyCode::Ctrl('e')).await.unwrap(); + for i in 1..=78 { + // Task + expected.get_mut(i, 3).set_style(Style::default().add_modifier(Modifier::BOLD)); + } - let input = " project:CO"; - for c in input.chars() { - app.handle_input(KeyCode::Char(c)).await.unwrap(); - } + let mut app = TaskwarriorTui::new("next", false).await.unwrap(); + + app.mode = Mode::Tasks(Action::ContextMenu); + app.task_report_next(); + app.update(true).await.unwrap(); + + let backend = TestBackend::new(80, 10); + let mut terminal = Terminal::new(backend).unwrap(); + terminal + .draw(|f| { + app.draw_context_menu(f, 100, 100); + app.draw_context_menu(f, 100, 100); + }) + .unwrap(); + + assert_eq!(terminal.backend().size().unwrap(), expected.area); + terminal.backend().assert_buffer(&expected); + } + + // #[test] + async fn test_graphemes() { + dbg!("写作业".graphemes(true).count()); + dbg!(UnicodeWidthStr::width("写作业")); + dbg!(UnicodeWidthStr::width("abc")); + + let mut app = TaskwarriorTui::new("next", false).await.unwrap(); + + if let Some(task) = app.task_by_id(27) { + let i = app.task_index_by_uuid(*task.uuid()).unwrap_or_default(); + app.current_selection = i; + app.current_selection_id = None; + app.current_selection_uuid = None; + } + app.update(true).await.unwrap(); + app.mode = Mode::Tasks(Action::Modify); + match app.task_current() { + Some(t) => { + let s = format!("{} ", t.description()); + app.modify.update(&s, s.as_str().len(), &mut app.changes) + } + None => app.modify.update("", 0, &mut app.changes), + } + app.update(true).await.unwrap(); + + dbg!(app.modify.as_str()); + dbg!(app.modify.as_str().len()); + dbg!(app.modify.graphemes(true).count()); + dbg!(app.modify.pos()); + let position = TaskwarriorTui::get_position(&app.modify); + dbg!(position); + } + + // #[test] + async fn test_taskwarrior_tui_completion() { + let mut app = TaskwarriorTui::new("next", false).await.unwrap(); + app.handle_input(KeyCode::Char('z')).await.unwrap(); + app.mode = Mode::Tasks(Action::Add); + app.update_completion_list(); + let input = "Wash car"; + for c in input.chars() { + app.handle_input(KeyCode::Char(c)).await.unwrap(); + } + app.handle_input(KeyCode::Ctrl('e')).await.unwrap(); - app.mode = Mode::Tasks(Action::Add); - app.update_completion_list(); - app.handle_input(KeyCode::Tab).await.unwrap(); - app.handle_input(KeyCode::Char('\n')).await.unwrap(); - let backend = TestBackend::new(80, 50); - let mut terminal = Terminal::new(backend).unwrap(); - terminal - .draw(|f| { - app.draw(f); - app.draw(f); - }) - .unwrap(); - println!("{}", buffer_view(terminal.backend().buffer())); + let input = " project:CO"; + for c in input.chars() { + app.handle_input(KeyCode::Char(c)).await.unwrap(); } + + app.mode = Mode::Tasks(Action::Add); + app.update_completion_list(); + app.handle_input(KeyCode::Tab).await.unwrap(); + app.handle_input(KeyCode::Char('\n')).await.unwrap(); + let backend = TestBackend::new(80, 50); + let mut terminal = Terminal::new(backend).unwrap(); + terminal + .draw(|f| { + app.draw(f); + app.draw(f); + }) + .unwrap(); + println!("{}", buffer_view(terminal.backend().buffer())); + } } diff --git a/src/calendar.rs b/src/calendar.rs index f881ff3a..a7058573 100644 --- a/src/calendar.rs +++ b/src/calendar.rs @@ -5,275 +5,265 @@ use std::fmt; const COL_WIDTH: usize = 21; -use chrono::{format::Fixed, Date, Datelike, Duration, FixedOffset, Local, Month, NaiveDate, NaiveDateTime, TimeZone}; +use chrono::{format::Fixed, DateTime, Datelike, Duration, FixedOffset, Local, Month, NaiveDate, NaiveDateTime, TimeZone}; use tui::{ - buffer::Buffer, - layout::Rect, - style::{Color, Modifier, Style}, - symbols, - widgets::{Block, Widget}, + buffer::Buffer, + layout::Rect, + style::{Color, Modifier, Style}, + symbols, + widgets::{Block, Widget}, }; use std::cmp::min; #[derive(Debug, Clone)] pub struct Calendar<'a> { - pub block: Option>, - pub year: i32, - pub month: u32, - pub style: Style, - pub months_per_row: usize, - pub date_style: Vec<(Date, Style)>, - pub today_style: Style, - pub start_on_monday: bool, - pub title_background_color: Color, + pub block: Option>, + pub year: i32, + pub month: u32, + pub style: Style, + pub months_per_row: usize, + pub date_style: Vec<(NaiveDate, Style)>, + pub today_style: Style, + pub start_on_monday: bool, + pub title_background_color: Color, } impl<'a> Default for Calendar<'a> { - fn default() -> Calendar<'a> { - let year = Local::today().year(); - let month = Local::today().month(); - Calendar { - block: None, - style: Style::default(), - months_per_row: 0, - year, - month, - date_style: vec![], - today_style: Style::default(), - start_on_monday: false, - title_background_color: Color::Reset, - } + fn default() -> Calendar<'a> { + let year = Local::now().year(); + let month = Local::now().month(); + Calendar { + block: None, + style: Style::default(), + months_per_row: 0, + year, + month, + date_style: vec![], + today_style: Style::default(), + start_on_monday: false, + title_background_color: Color::Reset, } + } } impl<'a> Calendar<'a> { - pub fn block(mut self, block: Block<'a>) -> Self { - self.block = Some(block); - self - } + pub fn block(mut self, block: Block<'a>) -> Self { + self.block = Some(block); + self + } - pub fn style(mut self, style: Style) -> Self { - self.style = style; - self - } + pub fn style(mut self, style: Style) -> Self { + self.style = style; + self + } - pub fn year(mut self, year: i32) -> Self { - self.year = year; - if self.year < 0 { - self.year = 0; - } - self + pub fn year(mut self, year: i32) -> Self { + self.year = year; + if self.year < 0 { + self.year = 0; } + self + } - pub fn month(mut self, month: u32) -> Self { - self.month = month; - self - } + pub fn month(mut self, month: u32) -> Self { + self.month = month; + self + } - pub fn date_style(mut self, date_style: Vec<(Date, Style)>) -> Self { - self.date_style = date_style; - self - } + pub fn date_style(mut self, date_style: Vec<(NaiveDate, Style)>) -> Self { + self.date_style = date_style; + self + } - pub fn today_style(mut self, today_style: Style) -> Self { - self.today_style = today_style; - self - } + pub fn today_style(mut self, today_style: Style) -> Self { + self.today_style = today_style; + self + } - pub fn months_per_row(mut self, months_per_row: usize) -> Self { - self.months_per_row = months_per_row; - self - } + pub fn months_per_row(mut self, months_per_row: usize) -> Self { + self.months_per_row = months_per_row; + self + } - pub fn start_on_monday(mut self, start_on_monday: bool) -> Self { - self.start_on_monday = start_on_monday; - self - } + pub fn start_on_monday(mut self, start_on_monday: bool) -> Self { + self.start_on_monday = start_on_monday; + self + } } impl<'a> Widget for Calendar<'a> { - fn render(mut self, area: Rect, buf: &mut Buffer) { - let month_names = Self::generate_month_names(); - buf.set_style(area, self.style); + fn render(mut self, area: Rect, buf: &mut Buffer) { + let month_names = Self::generate_month_names(); + buf.set_style(area, self.style); - let area = match self.block.take() { - Some(b) => { - let inner_area = b.inner(area); - b.render(area, buf); - inner_area - } - None => area, - }; + let area = match self.block.take() { + Some(b) => { + let inner_area = b.inner(area); + b.render(area, buf); + inner_area + } + None => area, + }; - if area.height < 7 { - return; - } + if area.height < 7 { + return; + } - let style = self.style; - let today = Local::today(); + let style = self.style; + let today = Local::now(); - let year = self.year; - let month = self.month; + let year = self.year; + let month = self.month; - let months: Vec<_> = (0..12).collect(); + let months: Vec<_> = (0..12).collect(); - let mut days: Vec<(Date, Date)> = months - .iter() - .map(|i| { - let first = Date::from_utc(NaiveDate::from_ymd(year, i + 1, 1), *Local::now().offset()); - let num_days = if self.start_on_monday { - first.weekday().num_days_from_monday() - } else { - first.weekday().num_days_from_sunday() - }; - (first, first - Duration::days(i64::from(num_days))) - }) - .collect(); + let mut days: Vec<(NaiveDate, NaiveDate)> = months + .iter() + .map(|i| { + let first = NaiveDate::from_ymd_opt(year, i + 1, 1).unwrap(); + let num_days = if self.start_on_monday { + first.weekday().num_days_from_monday() + } else { + first.weekday().num_days_from_sunday() + }; + (first, first - Duration::days(i64::from(num_days))) + }) + .collect(); - let mut start_m = 0_usize; - if self.months_per_row > area.width as usize / 8 / 3 || self.months_per_row == 0 { - self.months_per_row = area.width as usize / 8 / 3; - } - let mut y = area.y; - y += 1; + let mut start_m = 0_usize; + if self.months_per_row > area.width as usize / 8 / 3 || self.months_per_row == 0 { + self.months_per_row = area.width as usize / 8 / 3; + } + let mut y = area.y; + y += 1; - let x = area.x; - let s = format!("{year:^width$}", year = year, width = area.width as usize); + let x = area.x; + let s = format!("{year:^width$}", year = year, width = area.width as usize); - let mut new_year = 0; - let style = Style::default().add_modifier(Modifier::UNDERLINED); - if self.year + new_year as i32 == today.year() { - buf.set_string(x, y, &s, self.today_style.add_modifier(Modifier::UNDERLINED)); + let mut new_year = 0; + let style = Style::default().add_modifier(Modifier::UNDERLINED); + if self.year + new_year as i32 == today.year() { + buf.set_string(x, y, &s, self.today_style.add_modifier(Modifier::UNDERLINED)); + } else { + buf.set_string(x, y, &s, style); + } + + let start_x = (area.width - 3 * 7 * self.months_per_row as u16 - self.months_per_row as u16) / 2; + y += 2; + loop { + let endm = std::cmp::min(start_m + self.months_per_row, 12); + let mut x = area.x + start_x; + for (c, d) in days.iter_mut().enumerate().take(endm).skip(start_m) { + if c > start_m { + x += 1; + } + let m = d.0.month() as usize; + let s = format!("{:^20}", month_names[m - 1]); + let style = Style::default().bg(self.title_background_color); + if m == today.month() as usize && self.year + new_year as i32 == today.year() { + buf.set_string(x, y, &s, self.today_style); } else { - buf.set_string(x, y, &s, style); + buf.set_string(x, y, &s, style); } - - let start_x = (area.width - 3 * 7 * self.months_per_row as u16 - self.months_per_row as u16) / 2; - y += 2; - loop { - let endm = std::cmp::min(start_m + self.months_per_row, 12); - let mut x = area.x + start_x; - for (c, d) in days.iter_mut().enumerate().take(endm).skip(start_m) { - if c > start_m { - x += 1; - } - let m = d.0.month() as usize; - let s = format!("{:^20}", month_names[m - 1]); - let style = Style::default().bg(self.title_background_color); - if m == today.month() as usize && self.year + new_year as i32 == today.year() { - buf.set_string(x, y, &s, self.today_style); - } else { - buf.set_string(x, y, &s, style); - } - x += s.len() as u16 + 1; - } - y += 1; - let mut x = area.x + start_x; - for d in days.iter_mut().take(endm).skip(start_m) { - let m = d.0.month() as usize; - let style = Style::default().bg(self.title_background_color); - let days_string = if self.start_on_monday { - "Mo Tu We Th Fr Sa Su" - } else { - "Su Mo Tu We Th Fr Sa" - }; - buf.set_string(x as u16, y, days_string, style.add_modifier(Modifier::UNDERLINED)); - x += 21 + 1; + x += s.len() as u16 + 1; + } + y += 1; + let mut x = area.x + start_x; + for d in days.iter_mut().take(endm).skip(start_m) { + let m = d.0.month() as usize; + let style = Style::default().bg(self.title_background_color); + let days_string = if self.start_on_monday { + "Mo Tu We Th Fr Sa Su" + } else { + "Su Mo Tu We Th Fr Sa" + }; + buf.set_string(x, y, days_string, style.add_modifier(Modifier::UNDERLINED)); + x += 21 + 1; + } + y += 1; + loop { + let mut moredays = false; + let mut x = area.x + start_x; + for c in start_m..endm { + if c > start_m { + x += 1; + } + let d = &mut days[c + new_year * 12]; + for _ in 0..7 { + let s = if d.0.month() == d.1.month() { + format!("{:>2}", d.1.day()) + } else { + " ".to_string() + }; + let mut style = Style::default(); + let index = self.date_style.iter().position(|(date, style)| d.1 == *date); + if let Some(i) = index { + style = self.date_style[i].1; } - y += 1; - loop { - let mut moredays = false; - let mut x = area.x + start_x; - for c in start_m..endm { - if c > start_m { - x += 1; - } - let d = &mut days[c + new_year * 12]; - for _ in 0..7 { - let s = if d.0.month() == d.1.month() { - format!("{:>2}", d.1.day()) - } else { - " ".to_string() - }; - let mut style = Style::default(); - let index = self.date_style.iter().position(|(date, style)| d.1 == *date); - if let Some(i) = index { - style = self.date_style[i].1; - } - if d.1 == Local::today() { - buf.set_string(x, y, s, self.today_style); - } else { - buf.set_string(x, y, s, style); - } - x += 3; - d.1 = Date::from_utc(d.1.naive_local() + Duration::days(1), *Local::now().offset()); - } - moredays |= d.0.month() == d.1.month() || d.1 < d.0; - } - y += 1; - if !moredays { - break; - } + if d.1 == Local::now().date_naive() { + buf.set_string(x, y, s, self.today_style); + } else { + buf.set_string(x, y, s, style); } - start_m += self.months_per_row; - y += 2; - if y + 8 > area.height { - break; - } else if start_m >= 12 { - start_m = 0; - new_year += 1; - days.append( - &mut months - .iter() - .map(|i| { - let first = Date::from_utc( - NaiveDate::from_ymd(self.year + new_year as i32, i + 1, 1), - *Local::now().offset(), - ); - ( - first, - first - Duration::days(i64::from(first.weekday().num_days_from_sunday())), - ) - }) - .collect::>(), - ); + x += 3; + d.1 += Duration::days(1); + } + moredays |= d.0.month() == d.1.month() || d.1 < d.0; + } + y += 1; + if !moredays { + break; + } + } + start_m += self.months_per_row; + y += 2; + if y + 8 > area.height { + break; + } else if start_m >= 12 { + start_m = 0; + new_year += 1; + days.append( + &mut months + .iter() + .map(|i| { + let first = NaiveDate::from_ymd_opt(self.year + new_year as i32, i + 1, 1).unwrap(); + (first, first - Duration::days(i64::from(first.weekday().num_days_from_sunday()))) + }) + .collect::>(), + ); - let x = area.x; - let s = format!( - "{year:^width$}", - year = self.year as usize + new_year, - width = area.width as usize - ); - let mut style = Style::default().add_modifier(Modifier::UNDERLINED); - if self.year + new_year as i32 == today.year() { - style = style.add_modifier(Modifier::BOLD); - } - buf.set_string(x, y, &s, style); - y += 1; - } - y += 1; + let x = area.x; + let s = format!("{year:^width$}", year = self.year as usize + new_year, width = area.width as usize); + let mut style = Style::default().add_modifier(Modifier::UNDERLINED); + if self.year + new_year as i32 == today.year() { + style = style.add_modifier(Modifier::BOLD); } + buf.set_string(x, y, &s, style); + y += 1; + } + y += 1; } + } } impl<'a> Calendar<'a> { - fn generate_month_names() -> [&'a str; 12] { - let month_names = [ - Month::January.name(), - Month::February.name(), - Month::March.name(), - Month::April.name(), - Month::May.name(), - Month::June.name(), - Month::July.name(), - Month::August.name(), - Month::September.name(), - Month::October.name(), - Month::November.name(), - Month::December.name(), - ]; - month_names - } + fn generate_month_names() -> [&'a str; 12] { + let month_names = [ + Month::January.name(), + Month::February.name(), + Month::March.name(), + Month::April.name(), + Month::May.name(), + Month::June.name(), + Month::July.name(), + Month::August.name(), + Month::September.name(), + Month::October.name(), + Month::November.name(), + Month::December.name(), + ]; + month_names + } } diff --git a/src/cli.rs b/src/cli.rs index 4d6d1234..3482cebe 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,50 +3,50 @@ use clap::Arg; const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); const APP_NAME: &str = env!("CARGO_PKG_NAME"); -pub fn generate_cli_app() -> clap::Command<'static> { - let mut app = clap::Command::new(APP_NAME) - .version(APP_VERSION) - .author("Dheepak Krishnamurthy <@kdheepak>") - .about("A taskwarrior terminal user interface") - .arg( - Arg::new("data") - .short('d') - .long("data") - .value_name("FOLDER") - .help("Sets the data folder for taskwarrior-tui") - .takes_value(true), - ) - .arg( - Arg::new("config") - .short('c') - .long("config") - .value_name("FOLDER") - .help("Sets the config folder for taskwarrior-tui (currently not used)") - .takes_value(true), - ) - .arg( - Arg::new("taskdata") - .long("taskdata") - .value_name("FOLDER") - .help("Sets the .task folder using the TASKDATA environment variable for taskwarrior") - .takes_value(true), - ) - .arg( - Arg::new("taskrc") - .long("taskrc") - .value_name("FILE") - .help("Sets the .taskrc file using the TASKRC environment variable for taskwarrior") - .takes_value(true), - ) - .arg( - Arg::new("report") - .short('r') - .long("report") - .value_name("STRING") - .help("Sets default report") - .takes_value(true), - ); +pub fn generate_cli_app() -> clap::Command { + let mut app = clap::Command::new(APP_NAME) + .version(APP_VERSION) + .author("Dheepak Krishnamurthy <@kdheepak>") + .about("A taskwarrior terminal user interface") + .arg( + Arg::new("data") + .short('d') + .long("data") + .value_name("FOLDER") + .help("Sets the data folder for taskwarrior-tui") + .action(clap::ArgAction::Set), + ) + .arg( + Arg::new("config") + .short('c') + .long("config") + .value_name("FOLDER") + .help("Sets the config folder for taskwarrior-tui (currently not used)") + .action(clap::ArgAction::Set), + ) + .arg( + Arg::new("taskdata") + .long("taskdata") + .value_name("FOLDER") + .help("Sets the .task folder using the TASKDATA environment variable for taskwarrior") + .action(clap::ArgAction::Set), + ) + .arg( + Arg::new("taskrc") + .long("taskrc") + .value_name("FILE") + .help("Sets the .taskrc file using the TASKRC environment variable for taskwarrior") + .action(clap::ArgAction::Set), + ) + .arg( + Arg::new("report") + .short('r') + .long("report") + .value_name("STRING") + .help("Sets default report") + .action(clap::ArgAction::Set), + ); - app.set_bin_name(APP_NAME); - app + app.set_bin_name(APP_NAME); + app } diff --git a/src/completion.rs b/src/completion.rs index e760a305..54a759ad 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -1,207 +1,201 @@ use log::{debug, error, info, log_enabled, trace, warn, Level, LevelFilter}; use std::{error::Error, io}; use tui::{ - layout::{Constraint, Corner, Direction, Layout}, - style::{Color, Modifier, Style}, - text::{Span, Spans}, - widgets::{Block, Borders, List, ListItem, ListState}, - Terminal, + layout::{Constraint, Corner, Direction, Layout}, + style::{Color, Modifier, Style}, + text::{Span, Spans}, + widgets::{Block, Borders, List, ListItem, ListState}, + Terminal, }; -use rustyline::error::ReadlineError; use rustyline::highlight::{Highlighter, MatchingBracketHighlighter}; use rustyline::hint::Hinter; use rustyline::line_buffer::LineBuffer; use rustyline::Context; +use rustyline::{error::ReadlineError, history::FileHistory}; use unicode_segmentation::Graphemes; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; pub fn get_start_word_under_cursor(line: &str, cursor_pos: usize) -> usize { - let mut chars = line[..cursor_pos].chars(); - let mut res = cursor_pos; - while let Some(c) = chars.next_back() { - if c == ' ' || c == '(' || c == ')' { - break; - } - res -= c.len_utf8(); - } - // if iter == None, res == 0. - res + let mut chars = line[..cursor_pos].chars(); + let mut res = cursor_pos; + while let Some(c) = chars.next_back() { + if c == ' ' || c == '(' || c == ')' { + break; + } + res -= c.len_utf8(); + } + // if iter == None, res == 0. + res } pub struct TaskwarriorTuiCompletionHelper { - pub candidates: Vec<(String, String)>, - pub context: String, - pub input: String, + pub candidates: Vec<(String, String)>, + pub context: String, + pub input: String, } type Completion = (String, String, String, String, String); impl TaskwarriorTuiCompletionHelper { - fn complete(&self, word: &str, pos: usize, _ctx: &Context) -> rustyline::Result<(usize, Vec)> { - let candidates: Vec = self - .candidates - .iter() - .filter_map(|(context, candidate)| { - if context == &self.context - && (candidate.starts_with(&word[..pos]) - || candidate.to_lowercase().starts_with(&word[..pos].to_lowercase())) - && (!self.input.contains(candidate) - || !self.input.to_lowercase().contains(&candidate.to_lowercase())) - { - Some(( - candidate.clone(), // display - candidate.to_string(), // replacement - word[..pos].to_string(), // original - candidate[..pos].to_string(), - candidate[pos..].to_string(), - )) - } else { - None - } - }) - .collect(); - Ok((pos, candidates)) - } + fn complete(&self, word: &str, pos: usize, _ctx: &Context) -> rustyline::Result<(usize, Vec)> { + let candidates: Vec = self + .candidates + .iter() + .filter_map(|(context, candidate)| { + if context == &self.context + && (candidate.starts_with(&word[..pos]) || candidate.to_lowercase().starts_with(&word[..pos].to_lowercase())) + && (!self.input.contains(candidate) || !self.input.to_lowercase().contains(&candidate.to_lowercase())) + { + Some(( + candidate.clone(), // display + candidate.to_string(), // replacement + word[..pos].to_string(), // original + candidate[..pos].to_string(), + candidate[pos..].to_string(), + )) + } else { + None + } + }) + .collect(); + Ok((pos, candidates)) + } } pub struct CompletionList { - pub state: ListState, - pub current: String, - pub pos: usize, - pub helper: TaskwarriorTuiCompletionHelper, + pub state: ListState, + pub current: String, + pub pos: usize, + pub helper: TaskwarriorTuiCompletionHelper, } impl CompletionList { - pub fn new() -> CompletionList { - CompletionList { - state: ListState::default(), - current: String::new(), - pos: 0, - helper: TaskwarriorTuiCompletionHelper { - candidates: vec![], - context: String::new(), - input: String::new(), - }, - } - } - - pub fn with_items(items: Vec<(String, String)>) -> CompletionList { - let mut candidates = vec![]; - for i in items { - if !candidates.contains(&i) { - candidates.push(i); - } - } - let context = String::new(); - let input = String::new(); - CompletionList { - state: ListState::default(), - current: String::new(), - pos: 0, - helper: TaskwarriorTuiCompletionHelper { - candidates, - context, - input, - }, - } - } - - pub fn insert(&mut self, item: (String, String)) { - if !self.helper.candidates.contains(&item) { - self.helper.candidates.push(item); - } - } - - pub fn next(&mut self) { - let i = match self.state.selected() { - Some(i) => { - if i >= self.candidates().len() - 1 { - 0 - } else { - i + 1 - } - } - None => 0, - }; - self.state.select(Some(i)); - } - - pub fn previous(&mut self) { - let i = match self.state.selected() { - Some(i) => { - if i == 0 { - self.candidates().len() - 1 - } else { - i - 1 - } - } - None => 0, - }; - self.state.select(Some(i)); - } - - pub fn unselect(&mut self) { - self.state.select(None); - } - - pub fn clear(&mut self) { - self.helper.candidates.clear(); - self.state.select(None); - } - - pub fn len(&self) -> usize { - self.candidates().len() - } - - pub fn max_width(&self) -> Option { - self.candidates().iter().map(|p| p.1.width() + 4).max() - } - - pub fn get(&self, i: usize) -> Option { - let candidates = self.candidates(); - if i < candidates.len() { - Some(candidates[i].clone()) + pub fn new() -> CompletionList { + CompletionList { + state: ListState::default(), + current: String::new(), + pos: 0, + helper: TaskwarriorTuiCompletionHelper { + candidates: vec![], + context: String::new(), + input: String::new(), + }, + } + } + + pub fn with_items(items: Vec<(String, String)>) -> CompletionList { + let mut candidates = vec![]; + for i in items { + if !candidates.contains(&i) { + candidates.push(i); + } + } + let context = String::new(); + let input = String::new(); + CompletionList { + state: ListState::default(), + current: String::new(), + pos: 0, + helper: TaskwarriorTuiCompletionHelper { candidates, context, input }, + } + } + + pub fn insert(&mut self, item: (String, String)) { + if !self.helper.candidates.contains(&item) { + self.helper.candidates.push(item); + } + } + + pub fn next(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i >= self.candidates().len() - 1 { + 0 } else { - None + i + 1 } - } - - pub fn selected(&self) -> Option<(usize, Completion)> { - self.state.selected().and_then(|i| self.get(i)).map(|s| (self.pos, s)) - } - - pub fn is_empty(&self) -> bool { - self.candidates().is_empty() - } - - pub fn candidates(&self) -> Vec { - let hist = rustyline::history::History::new(); - let ctx = rustyline::Context::new(&hist); - let (pos, candidates) = self.helper.complete(&self.current, self.pos, &ctx).unwrap(); - candidates - } - - pub fn input(&mut self, current: String, i: String) { - self.helper.input = i; - if current.contains('.') && current.contains(':') { - self.current = current.split_once(':').unwrap().1.to_string(); - self.helper.context = current.split_once('.').unwrap().0.to_string(); - } else if current.contains('.') { - self.current = format!(".{}", current.split_once('.').unwrap().1); - self.helper.context = "modifier".to_string(); - } else if current.contains(':') { - self.current = current.split_once(':').unwrap().1.to_string(); - self.helper.context = current.split_once(':').unwrap().0.to_string(); - } else if current.contains('+') { - self.current = format!("+{}", current.split_once('+').unwrap().1); - self.helper.context = "+".to_string(); + } + None => 0, + }; + self.state.select(Some(i)); + } + + pub fn previous(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i == 0 { + self.candidates().len() - 1 } else { - self.current = current; - self.helper.context = "attribute".to_string(); + i - 1 } - self.pos = self.current.len(); - } + } + None => 0, + }; + self.state.select(Some(i)); + } + + pub fn unselect(&mut self) { + self.state.select(None); + } + + pub fn clear(&mut self) { + self.helper.candidates.clear(); + self.state.select(None); + } + + pub fn len(&self) -> usize { + self.candidates().len() + } + + pub fn max_width(&self) -> Option { + self.candidates().iter().map(|p| p.1.width() + 4).max() + } + + pub fn get(&self, i: usize) -> Option { + let candidates = self.candidates(); + if i < candidates.len() { + Some(candidates[i].clone()) + } else { + None + } + } + + pub fn selected(&self) -> Option<(usize, Completion)> { + self.state.selected().and_then(|i| self.get(i)).map(|s| (self.pos, s)) + } + + pub fn is_empty(&self) -> bool { + self.candidates().is_empty() + } + + pub fn candidates(&self) -> Vec { + let hist = FileHistory::new(); + let ctx = rustyline::Context::new(&hist); + let (pos, candidates) = self.helper.complete(&self.current, self.pos, &ctx).unwrap(); + candidates + } + + pub fn input(&mut self, current: String, i: String) { + self.helper.input = i; + if current.contains('.') && current.contains(':') { + self.current = current.split_once(':').unwrap().1.to_string(); + self.helper.context = current.split_once('.').unwrap().0.to_string(); + } else if current.contains('.') { + self.current = format!(".{}", current.split_once('.').unwrap().1); + self.helper.context = "modifier".to_string(); + } else if current.contains(':') { + self.current = current.split_once(':').unwrap().1.to_string(); + self.helper.context = current.split_once(':').unwrap().0.to_string(); + } else if current.contains('+') { + self.current = format!("+{}", current.split_once('+').unwrap().1); + self.helper.context = "+".to_string(); + } else { + self.current = current; + self.helper.context = "attribute".to_string(); + } + self.pos = self.current.len(); + } } diff --git a/src/config.rs b/src/config.rs index e1797c01..e4152b8f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,893 +3,873 @@ use std::collections::HashMap; use std::error::Error; use std::str; use tui::{ - style::{Color, Modifier, Style}, - symbols::bar::FULL, - symbols::line::DOUBLE_VERTICAL, + style::{Color, Modifier, Style}, + symbols::bar::FULL, + symbols::line::DOUBLE_VERTICAL, }; trait TaskWarriorBool { - fn get_bool(&self) -> Option; + fn get_bool(&self) -> Option; } impl TaskWarriorBool for String { - fn get_bool(&self) -> Option { - if self == "true" || self == "1" || self == "y" || self == "yes" || self == "on" { - Some(true) - } else if self == "false" || self == "0" || self == "n" || self == "no" || self == "off" { - Some(false) - } else { - None - } - } + fn get_bool(&self) -> Option { + if self == "true" || self == "1" || self == "y" || self == "yes" || self == "on" { + Some(true) + } else if self == "false" || self == "0" || self == "n" || self == "no" || self == "off" { + Some(false) + } else { + None + } + } } impl TaskWarriorBool for str { - fn get_bool(&self) -> Option { - if self == "true" || self == "1" || self == "y" || self == "yes" || self == "on" { - Some(true) - } else if self == "false" || self == "0" || self == "n" || self == "no" || self == "off" { - Some(false) - } else { - None - } - } + fn get_bool(&self) -> Option { + if self == "true" || self == "1" || self == "y" || self == "yes" || self == "on" { + Some(true) + } else if self == "false" || self == "0" || self == "n" || self == "no" || self == "off" { + Some(false) + } else { + None + } + } } #[derive(Debug)] pub struct Uda { - label: String, - kind: String, - values: Option>, - default: Option, - urgency: Option, + label: String, + kind: String, + values: Option>, + default: Option, + urgency: Option, } #[derive(Debug)] pub struct Config { - pub enabled: bool, - pub color: HashMap, - pub filter: String, - pub data_location: String, - pub obfuscate: bool, - pub print_empty_columns: bool, - pub due: usize, - pub weekstart: bool, - pub rule_precedence_color: Vec, - pub uda_priority_values: Vec, - pub uda_tick_rate: u64, - pub uda_auto_insert_double_quotes_on_add: bool, - pub uda_auto_insert_double_quotes_on_annotate: bool, - pub uda_auto_insert_double_quotes_on_log: bool, - pub uda_prefill_task_metadata: bool, - pub uda_reset_filter_on_esc: bool, - pub uda_task_detail_prefetch: usize, - pub uda_task_report_use_all_tasks_for_completion: bool, - pub uda_task_report_show_info: bool, - pub uda_task_report_looping: bool, - pub uda_task_report_jump_to_task_on_add: bool, - pub uda_selection_indicator: String, - pub uda_mark_indicator: String, - pub uda_unmark_indicator: String, - pub uda_scrollbar_indicator: String, - pub uda_scrollbar_area: String, - pub uda_style_report_scrollbar: Style, - pub uda_style_report_scrollbar_area: Style, - pub uda_selection_bold: bool, - pub uda_selection_italic: bool, - pub uda_selection_dim: bool, - pub uda_selection_blink: bool, - pub uda_selection_reverse: bool, - pub uda_calendar_months_per_row: usize, - pub uda_style_context_active: Style, - pub uda_style_report_selection: Style, - pub uda_style_calendar_title: Style, - pub uda_style_calendar_today: Style, - pub uda_style_navbar: Style, - pub uda_style_command: Style, - pub uda_style_report_completion_pane: Style, - pub uda_style_report_completion_pane_highlight: Style, - pub uda_shortcuts: Vec, - pub uda_change_focus_rotate: bool, - pub uda_background_process: String, - pub uda_background_process_period: usize, - pub uda_quick_tag_name: String, - pub uda_task_report_prompt_on_delete: bool, - pub uda_task_report_prompt_on_done: bool, - pub uda_task_report_date_time_vague_more_precise: bool, - pub uda_context_menu_select_on_move: bool, - pub uda: Vec, + pub enabled: bool, + pub color: HashMap, + pub filter: String, + pub data_location: String, + pub obfuscate: bool, + pub print_empty_columns: bool, + pub due: usize, + pub weekstart: bool, + pub rule_precedence_color: Vec, + pub uda_priority_values: Vec, + pub uda_tick_rate: u64, + pub uda_auto_insert_double_quotes_on_add: bool, + pub uda_auto_insert_double_quotes_on_annotate: bool, + pub uda_auto_insert_double_quotes_on_log: bool, + pub uda_prefill_task_metadata: bool, + pub uda_reset_filter_on_esc: bool, + pub uda_task_detail_prefetch: usize, + pub uda_task_report_use_all_tasks_for_completion: bool, + pub uda_task_report_show_info: bool, + pub uda_task_report_looping: bool, + pub uda_task_report_jump_to_task_on_add: bool, + pub uda_selection_indicator: String, + pub uda_mark_indicator: String, + pub uda_unmark_indicator: String, + pub uda_scrollbar_indicator: String, + pub uda_scrollbar_area: String, + pub uda_style_report_scrollbar: Style, + pub uda_style_report_scrollbar_area: Style, + pub uda_selection_bold: bool, + pub uda_selection_italic: bool, + pub uda_selection_dim: bool, + pub uda_selection_blink: bool, + pub uda_selection_reverse: bool, + pub uda_calendar_months_per_row: usize, + pub uda_style_context_active: Style, + pub uda_style_report_selection: Style, + pub uda_style_calendar_title: Style, + pub uda_style_calendar_today: Style, + pub uda_style_navbar: Style, + pub uda_style_command: Style, + pub uda_style_report_completion_pane: Style, + pub uda_style_report_completion_pane_highlight: Style, + pub uda_shortcuts: Vec, + pub uda_change_focus_rotate: bool, + pub uda_background_process: String, + pub uda_background_process_period: usize, + pub uda_quick_tag_name: String, + pub uda_task_report_prompt_on_delete: bool, + pub uda_task_report_prompt_on_done: bool, + pub uda_task_report_date_time_vague_more_precise: bool, + pub uda_context_menu_select_on_move: bool, + pub uda: Vec, } impl Config { - pub fn new(data: &str, report: &str) -> Result { - let bool_collection = Self::get_bool_collection(); - - let enabled = true; - let obfuscate = bool_collection.get("obfuscate").copied().unwrap_or(false); - let print_empty_columns = bool_collection.get("print_empty_columns").copied().unwrap_or(false); - - let color = Self::get_color_collection(data); - let filter = Self::get_filter(data, report)?; - let filter = if filter.trim_start().trim_end().is_empty() { - filter - } else { - format!("{} ", filter) + pub fn new(data: &str, report: &str) -> Result { + let bool_collection = Self::get_bool_collection(); + + let enabled = true; + let obfuscate = bool_collection.get("obfuscate").copied().unwrap_or(false); + let print_empty_columns = bool_collection.get("print_empty_columns").copied().unwrap_or(false); + + let color = Self::get_color_collection(data); + let filter = Self::get_filter(data, report)?; + let filter = if filter.trim_start().trim_end().is_empty() { + filter + } else { + format!("{} ", filter) + }; + let data_location = Self::get_data_location(data); + let due = Self::get_due(data); + let weekstart = Self::get_weekstart(data); + let rule_precedence_color = Self::get_rule_precedence_color(data); + let uda_priority_values = Self::get_uda_priority_values(data); + let uda_tick_rate = Self::get_uda_tick_rate(data); + let uda_change_focus_rotate = Self::get_uda_change_focus_rotate(data); + let uda_auto_insert_double_quotes_on_add = Self::get_uda_auto_insert_double_quotes_on_add(data); + let uda_auto_insert_double_quotes_on_annotate = Self::get_uda_auto_insert_double_quotes_on_annotate(data); + let uda_auto_insert_double_quotes_on_log = Self::get_uda_auto_insert_double_quotes_on_log(data); + let uda_prefill_task_metadata = Self::get_uda_prefill_task_metadata(data); + let uda_reset_filter_on_esc = Self::get_uda_reset_filter_on_esc(data); + let uda_task_detail_prefetch = Self::get_uda_task_detail_prefetch(data); + let uda_task_report_use_all_tasks_for_completion = Self::get_uda_task_report_use_all_tasks_for_completion(data); + let uda_task_report_show_info = Self::get_uda_task_report_show_info(data); + let uda_task_report_looping = Self::get_uda_task_report_looping(data); + let uda_task_report_jump_to_task_on_add = Self::get_uda_task_report_jump_to_task_on_add(data); + let uda_selection_indicator = Self::get_uda_selection_indicator(data); + let uda_mark_indicator = Self::get_uda_mark_indicator(data); + let uda_unmark_indicator = Self::get_uda_unmark_indicator(data); + let uda_scrollbar_indicator = Self::get_uda_scrollbar_indicator(data); + let uda_scrollbar_area = Self::get_uda_scrollbar_area(data); + let uda_selection_bold = Self::get_uda_selection_bold(data); + let uda_selection_italic = Self::get_uda_selection_italic(data); + let uda_selection_dim = Self::get_uda_selection_dim(data); + let uda_selection_blink = Self::get_uda_selection_blink(data); + let uda_selection_reverse = Self::get_uda_selection_reverse(data); + let uda_calendar_months_per_row = Self::get_uda_months_per_row(data); + let uda_style_report_selection = Self::get_uda_style("report.selection", data); + let uda_style_report_scrollbar = Self::get_uda_style("report.scrollbar", data); + let uda_style_report_scrollbar_area = Self::get_uda_style("report.scrollbar.area", data); + let uda_style_calendar_title = Self::get_uda_style("calendar.title", data); + let uda_style_calendar_today = Self::get_uda_style("calendar.today", data); + let uda_style_navbar = Self::get_uda_style("navbar", data); + let uda_style_command = Self::get_uda_style("command", data); + let uda_style_context_active = Self::get_uda_style("context.active", data); + let uda_style_report_completion_pane = Self::get_uda_style("report.completion-pane", data); + let uda_style_report_completion_pane_highlight = Self::get_uda_style("report.completion-pane-highlight", data); + let uda_shortcuts = Self::get_uda_shortcuts(data); + let uda_background_process = Self::get_uda_background_process(data); + let uda_background_process_period = Self::get_uda_background_process_period(data); + let uda_style_report_selection = uda_style_report_selection.unwrap_or_default(); + let uda_style_report_scrollbar = uda_style_report_scrollbar.unwrap_or_else(|| Style::default().fg(Color::Black)); + let uda_style_report_scrollbar_area = uda_style_report_scrollbar_area.unwrap_or_default(); + let uda_style_calendar_title = uda_style_calendar_title.unwrap_or_default(); + let uda_style_calendar_today = uda_style_calendar_today.unwrap_or_else(|| Style::default().add_modifier(Modifier::BOLD)); + let uda_style_navbar = uda_style_navbar.unwrap_or_else(|| Style::default().add_modifier(Modifier::REVERSED)); + let uda_style_command = uda_style_command.unwrap_or_else(|| Style::default().add_modifier(Modifier::REVERSED)); + let uda_style_context_active = uda_style_context_active.unwrap_or_default(); + let uda_style_report_completion_pane = + uda_style_report_completion_pane.unwrap_or_else(|| Style::default().fg(Color::Black).bg(Color::Rgb(223, 223, 223))); + let uda_style_report_completion_pane_highlight = uda_style_report_completion_pane_highlight.unwrap_or(uda_style_report_completion_pane); + let uda_quick_tag_name = Self::get_uda_quick_tag_name(data); + let uda_task_report_prompt_on_delete = Self::get_uda_task_report_prompt_on_delete(data); + let uda_task_report_prompt_on_done = Self::get_uda_task_report_prompt_on_done(data); + let uda_context_menu_select_on_move = Self::get_uda_context_menu_select_on_move(data); + let uda_task_report_date_time_vague_more_precise = Self::get_uda_task_report_date_time_vague_more_precise(data); + + Ok(Self { + enabled, + color, + filter, + data_location, + obfuscate, + print_empty_columns, + due, + weekstart, + rule_precedence_color, + uda_priority_values, + uda_tick_rate, + uda_change_focus_rotate, + uda_auto_insert_double_quotes_on_add, + uda_auto_insert_double_quotes_on_annotate, + uda_auto_insert_double_quotes_on_log, + uda_prefill_task_metadata, + uda_reset_filter_on_esc, + uda_task_detail_prefetch, + uda_task_report_use_all_tasks_for_completion, + uda_task_report_show_info, + uda_task_report_looping, + uda_task_report_jump_to_task_on_add, + uda_selection_indicator, + uda_mark_indicator, + uda_unmark_indicator, + uda_scrollbar_indicator, + uda_scrollbar_area, + uda_selection_bold, + uda_selection_italic, + uda_selection_dim, + uda_selection_blink, + uda_selection_reverse, + uda_calendar_months_per_row, + uda_style_report_selection, + uda_style_context_active, + uda_style_calendar_title, + uda_style_calendar_today, + uda_style_navbar, + uda_style_command, + uda_style_report_completion_pane, + uda_style_report_completion_pane_highlight, + uda_style_report_scrollbar, + uda_style_report_scrollbar_area, + uda_shortcuts, + uda_background_process, + uda_background_process_period, + uda_quick_tag_name, + uda_task_report_prompt_on_delete, + uda_task_report_prompt_on_done, + uda_task_report_date_time_vague_more_precise, + uda_context_menu_select_on_move, + uda: vec![], + }) + } + + fn get_bool_collection() -> HashMap { + HashMap::new() + } + + fn get_uda_background_process(data: &str) -> String { + Self::get_config("uda.taskwarrior-tui.background_process", data).unwrap_or_default() + } + + fn get_uda_background_process_period(data: &str) -> usize { + Self::get_config("uda.taskwarrior-tui.background_process_period", data) + .unwrap_or_default() + .parse::() + .unwrap_or(60) + } + + fn get_uda_shortcuts(data: &str) -> Vec { + let mut v = vec![]; + for s in 0..=9 { + let c = format!("uda.taskwarrior-tui.shortcuts.{}", s); + let s = Self::get_config(&c, data).unwrap_or_default(); + v.push(s); + } + v + } + + fn get_uda_style(config: &str, data: &str) -> Option