diff --git a/.envrc b/.envrc index 469a352b..809f9ff0 100644 --- a/.envrc +++ b/.envrc @@ -3,3 +3,5 @@ export TASKDATA=`pwd`/tests/data/.task export TASKWARRIOR_TUI_CONFIG=`pwd`/tests/data/.config export TASKWARRIOR_TUI_DATA=`pwd`/tests/data/.data export TASKWARRIOR_TUI_LOG_LEVEL=debug +export RUST_LOG=debug +export RUST_BACKTRACE=full diff --git a/.rustfmt.toml b/.rustfmt.toml index db495491..ef5985c9 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,4 +1,4 @@ -max_width = 150 +max_width = 120 tab_spaces = 2 group_imports = "StdExternalCrate" imports_granularity = "Crate" diff --git a/Cargo.lock b/Cargo.lock index 8981dc33..b5a64cd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" dependencies = [ "memchr", ] @@ -90,16 +90,10 @@ dependencies = [ ] [[package]] -name = "anyhow" -version = "1.0.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" - -[[package]] -name = "arc-swap" -version = "1.6.0" +name = "atomic" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" [[package]] name = "autocfg" @@ -143,6 +137,9 @@ name = "bitflags" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +dependencies = [ + "serde", +] [[package]] name = "bumpalo" @@ -179,9 +176,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "95ed24df0632f708f5f6d8082675bef2596f7084dee3dd55f632290bf35bfe0f" dependencies = [ "android-tzdata", "iana-time-zone", @@ -189,30 +186,32 @@ dependencies = [ "num-traits", "time", "wasm-bindgen", - "winapi", + "windows-targets 0.48.5", ] [[package]] name = "clap" -version = "4.4.1" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c8d502cbaec4595d2e7d5f61e318f05417bd2b66fdc3809498f0d3fdf0bea27" +checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.4.1" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5891c7bc0edb3e1c2204fc5e94009affabeb1821c9e5fdc3959536c5c0bb984d" +checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", + "terminal_size", + "unicase", + "unicode-width", ] [[package]] @@ -226,9 +225,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.4.0" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9fd1a5729c4548118d7d70ff234a44868d00489a4b6597b0b020918a0e91a1a" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" dependencies = [ "heck", "proc-macro2", @@ -253,6 +252,34 @@ dependencies = [ "winapi", ] +[[package]] +name = "color-eyre" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", + "url", +] + +[[package]] +name = "color-spantrace" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba75b3d9449ecdccb27ecbc479fdc0b87fa2dd43d2f8298f9bf0e59aacc8dce" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + [[package]] name = "colorchoice" version = "1.0.0" @@ -289,6 +316,7 @@ dependencies = [ "libc", "mio", "parking_lot", + "serde", "signal-hook", "signal-hook-mio", "winapi", @@ -338,17 +366,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "derive_builder" version = "0.12.0" @@ -381,10 +398,19 @@ dependencies = [ ] [[package]] -name = "destructure_traitobject" -version = "0.2.0" +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "directories" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] [[package]] name = "dirs" @@ -425,6 +451,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.2" @@ -456,6 +488,16 @@ dependencies = [ "str-buf", ] +[[package]] +name = "eyre" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fd-lock" version = "3.0.13" @@ -463,16 +505,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" dependencies = [ "cfg-if", - "rustix", + "rustix 0.38.8", "windows-sys 0.48.0", ] +[[package]] +name = "figment" +version = "0.10.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4547e226f4c9ab860571e070a9034192b3175580ecea38da34fcdb53a018c9a5" +dependencies = [ + "atomic", + "pear", + "serde", + "toml", + "uncased", + "version_check", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futures" version = "0.3.28" @@ -581,9 +646,9 @@ checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" [[package]] name = "heck" @@ -607,10 +672,20 @@ dependencies = [ ] [[package]] -name = "humantime" -version = "2.1.0" +name = "human-panic" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "eb2df2fb4e13fa697d21d93061ebcbbd876f5ef643b48ff59cfab57a726ef140" +dependencies = [ + "anstream", + "anstyle", + "backtrace", + "os_info", + "serde", + "serde_derive", + "toml", + "uuid", +] [[package]] name = "iana-time-zone" @@ -641,13 +716,29 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexmap" -version = "1.9.3" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ - "autocfg", + "equivalent", "hashbrown", ] @@ -657,6 +748,23 @@ version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c785eefb63ebd0e33416dfcb8d6da0bf27ce752843a45632a67bf10d4d4b5c4" +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "itertools" version = "0.11.0" @@ -694,10 +802,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] -name = "linked-hash-map" -version = "0.5.6" +name = "linux-raw-sys" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" @@ -720,47 +828,21 @@ name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" -dependencies = [ - "serde", -] [[package]] -name = "log-mdc" +name = "matchers" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" - -[[package]] -name = "log4rs" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d36ca1786d9e79b8193a68d480a0907b612f109537115c6ff655a3a1967533fd" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "anyhow", - "arc-swap", - "chrono", - "derivative", - "fnv", - "humantime", - "libc", - "log", - "log-mdc", - "parking_lot", - "serde", - "serde-value", - "serde_json", - "serde_yaml", - "thiserror", - "thread-id", - "typemap-ors", - "winapi", + "regex-automata 0.1.10", ] [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "minimal-lexical" @@ -820,6 +902,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-traits" version = "0.2.16" @@ -861,14 +953,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] -name = "ordered-float" -version = "2.10.0" +name = "os_info" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87" +checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" dependencies = [ - "num-traits", + "log", + "serde", + "winapi", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "parking_lot" version = "0.12.1" @@ -904,6 +1010,35 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" +[[package]] +name = "pear" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a386cd715229d399604b50d1361683fe687066f42d56f54be995bc6868f71c" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi 1.0.0-rc.1", +] + +[[package]] +name = "pear_codegen" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f0f13dac8069c139e8300a6510e3f4143ecf5259c60b116a9b271b4ca0d54" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + [[package]] name = "pin-project-lite" version = "0.2.12" @@ -922,6 +1057,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi 0.5.1", +] + [[package]] name = "proc-macro2" version = "1.0.66" @@ -931,6 +1076,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", + "version_check", + "yansi 1.0.0-rc.1", +] + [[package]] name = "quote" version = "1.0.33" @@ -1028,27 +1186,42 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.3.8", + "regex-syntax 0.7.5", ] [[package]] name = "regex-automata" -version = "0.3.7" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.7.5", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.7.5" @@ -1061,6 +1234,20 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustix" +version = "0.37.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + [[package]] name = "rustix" version = "0.38.8" @@ -1070,7 +1257,7 @@ dependencies = [ "bitflags 2.4.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.5", "windows-sys 0.48.0", ] @@ -1136,16 +1323,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float", - "serde", -] - [[package]] name = "serde_derive" version = "1.0.188" @@ -1169,15 +1346,21 @@ dependencies = [ ] [[package]] -name = "serde_yaml" -version = "0.8.26" +name = "serde_spanned" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" dependencies = [ - "indexmap", - "ryu", "serde", - "yaml-rust", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", ] [[package]] @@ -1262,6 +1445,15 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" +[[package]] +name = "strip-ansi-escapes" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.10.0" @@ -1331,31 +1523,42 @@ dependencies = [ name = "taskwarrior-tui" version = "0.25.4" dependencies = [ - "anyhow", "better-panic", "cassowary", "chrono", "clap", "clap_complete", + "color-eyre", "crossterm", - "dirs", + "directories", + "figment", "futures", + "human-panic", "itertools", "lazy_static", + "libc", "log", - "log4rs", "path-clean", + "pretty_assertions", "rand", "ratatui", "regex", "rustyline", "serde", + "serde_derive", "serde_json", "shellexpand", "shlex", + "signal-hook", + "strip-ansi-escapes", "task-hookrs", "tokio", - "tokio-stream", + "tokio-util", + "toml", + "tracing", + "tracing-error", + "tracing-subscriber", + "tui-input", "unicode-segmentation", "unicode-truncate", "unicode-width", @@ -1363,6 +1566,16 @@ dependencies = [ "versions", ] +[[package]] +name = "terminal_size" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" +dependencies = [ + "rustix 0.37.23", + "windows-sys 0.48.0", +] + [[package]] name = "thiserror" version = "1.0.47" @@ -1384,14 +1597,13 @@ dependencies = [ ] [[package]] -name = "thread-id" -version = "4.2.0" +name = "thread_local" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79474f573561cdc4871a0de34a51c92f7f5a56039113fbb5b9c9f96bdb756669" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ - "libc", - "redox_syscall 0.2.16", - "winapi", + "cfg-if", + "once_cell", ] [[package]] @@ -1405,6 +1617,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.32.0" @@ -1436,31 +1663,173 @@ dependencies = [ ] [[package]] -name = "tokio-stream" -version = "0.1.14" +name = "tokio-util" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ + "bytes", "futures-core", + "futures-sink", "pin-project-lite", "tokio", ] [[package]] -name = "typemap-ors" -version = "1.0.0" +name = "toml" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tui-input" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e785f863a3af4c800a2a669d0b64c879b538738e352607e2624d03f868dc01" +dependencies = [ + "crossterm", + "unicode-width", +] + +[[package]] +name = "uncased" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicase" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68c24b707f02dd18f1e4ccceb9d49f2058c2fb86384ef9972592904d7a28867" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ - "unsafe-any-ors", + "version_check", ] +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + [[package]] name = "unicode-ident" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.10.1" @@ -1483,12 +1852,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] -name = "unsafe-any-ors" -version = "1.0.0" +name = "url" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a303d30665362d9680d7d91d78b23f5f899504d4f08b3c4cf08d055d87c0ad" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ - "destructure_traitobject", + "form_urlencoded", + "idna", + "percent-encoding", ] [[package]] @@ -1507,6 +1878,18 @@ dependencies = [ "serde", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "versions" version = "5.0.1" @@ -1517,6 +1900,26 @@ dependencies = [ "nom", ] +[[package]] +name = "vte" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +dependencies = [ + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -1747,10 +2150,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] -name = "yaml-rust" -version = "0.4.5" +name = "winnow" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" dependencies = [ - "linked-hash-map", + "memchr", ] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "yansi" +version = "1.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" diff --git a/Cargo.toml b/Cargo.toml index d4ef53bc..4012fd92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,38 +7,47 @@ repository = "https://github.com/kdheepak/taskwarrior-tui/" homepage = "https://kdheepak.com/taskwarrior-tui" readme = "README.md" authors = ["Dheepak Krishnamurthy "] -edition = "2018" +edition = "2021" keywords = ["taskwarrior", "tui"] categories = ["command-line-utilities"] [dependencies] -anyhow = "1.0.75" better-panic = "0.3.0" cassowary = "0.3.0" -chrono = "0.4.26" -clap = { version = "4.4.1", features = ["derive"] } -crossterm = { version = "0.27.0", features = [ - "event-stream", -] } -dirs = "5.0.1" +chrono = "0.4.28" +clap = { version = "4.4.2", features = ["std", "color", "help", "usage", "error-context", "suggestions", "derive", "cargo", "wrap_help", "unicode", "string", "unstable-styles"] } +color-eyre = { version = "0.6.2", features = ["issue-url"] } +crossterm = { version = "0.27.0", features = ["event-stream", "serde"] } +directories = "5.0.1" +figment = { version = "0.10.10", features = ["toml", "env"] } futures = "0.3.28" +human-panic = "1.2.0" itertools = "0.11.0" lazy_static = "1.4.0" +libc = "0.2.147" log = "0.4.20" -log4rs = "1.2.0" path-clean = "1.0.1" +pretty_assertions = "1.4.0" rand = "0.8.5" -regex = "1.9.4" +ratatui = "0.23.0" +regex = "1.9.5" rustyline = { version = "12.0.0", features = ["with-file-history", "derive"] } serde = { version = "1.0.188", features = ["derive"] } +serde_derive = "1.0.188" serde_json = "1.0.105" shellexpand = "3.1.0" shlex = "1.1.0" +signal-hook = "0.3.17" +strip-ansi-escapes = "0.2.0" task-hookrs = "0.9.0" tokio = { version = "1.32.0", features = ["full"] } -tokio-stream = "0.1.14" -ratatui = "0.23.0" +tokio-util = "0.7.8" +toml = "0.7.6" +tracing = "0.1.37" +tracing-error = "0.2.0" +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } +tui-input = "0.8.0" unicode-segmentation = "1.10.1" unicode-truncate = "0.2.0" unicode-width = "0.1.10" @@ -55,11 +64,10 @@ buildflags = ["--release"] taskwarrior-tui = { path = "/usr/bin/taskwarrior-tui" } [profile.release] -debug = 1 incremental = true -lto = "off" +lto = true [build-dependencies] -clap = { version = "4.4.1", features = ["derive"] } +clap = { version = "4.4.2", features = ["derive"] } clap_complete = "4.4.0" shlex = "1.1.0" diff --git a/build.rs b/build.rs index 1031588e..02f2e608 100644 --- a/build.rs +++ b/build.rs @@ -18,7 +18,64 @@ fn run_pandoc() -> Result { cmd.output() } +fn get_commit_hash() { + let git_output = std::process::Command::new("git") + .args(["rev-parse", "--git-dir"]) + .output() + .ok(); + let git_dir = git_output.as_ref().and_then(|output| { + std::str::from_utf8(&output.stdout) + .ok() + .and_then(|s| s.strip_suffix('\n').or_else(|| s.strip_suffix("\r\n"))) + }); + + // Tell cargo to rebuild if the head or any relevant refs change. + if let Some(git_dir) = git_dir { + let git_path = std::path::Path::new(git_dir); + let refs_path = git_path.join("refs"); + if git_path.join("HEAD").exists() { + println!("cargo:rerun-if-changed={}/HEAD", git_dir); + } + if git_path.join("packed-refs").exists() { + println!("cargo:rerun-if-changed={}/packed-refs", git_dir); + } + if refs_path.join("heads").exists() { + println!("cargo:rerun-if-changed={}/refs/heads", git_dir); + } + if refs_path.join("tags").exists() { + println!("cargo:rerun-if-changed={}/refs/tags", git_dir); + } + } + + let git_output = std::process::Command::new("git") + .args(["describe", "--always", "--tags", "--long", "--dirty"]) + .output() + .ok(); + let git_info = git_output + .as_ref() + .and_then(|output| std::str::from_utf8(&output.stdout).ok().map(str::trim)); + let cargo_pkg_version = env!("CARGO_PKG_VERSION"); + + // Default git_describe to cargo_pkg_version + let mut git_describe = String::from(cargo_pkg_version); + + if let Some(git_info) = git_info { + // If the `git_info` contains `CARGO_PKG_VERSION`, we simply use `git_info` as it is. + // Otherwise, prepend `CARGO_PKG_VERSION` to `git_info`. + if git_info.contains(cargo_pkg_version) { + // Remove the 'g' before the commit sha + let git_info = &git_info.replace('g', ""); + git_describe = git_info.to_string(); + } else { + git_describe = format!("v{}-{}", cargo_pkg_version, git_info); + } + } + + println!("cargo:rustc-env=TASKWARRIOR_TUI_GIT_INFO={}", git_describe); +} + fn main() { + get_commit_hash(); 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/"); diff --git a/completions/_taskwarrior-tui b/completions/_taskwarrior-tui index b8bd2fa5..d93eeed0 100644 --- a/completions/_taskwarrior-tui +++ b/completions/_taskwarrior-tui @@ -17,8 +17,8 @@ _taskwarrior-tui() { _arguments "${_arguments_options[@]}" \ '-d+[Sets the data folder for taskwarrior-tui]:FOLDER: ' \ '--data=[Sets the data folder for taskwarrior-tui]:FOLDER: ' \ -'-c+[Sets the config folder for taskwarrior-tui (currently not used)]:FOLDER: ' \ -'--config=[Sets the config folder for taskwarrior-tui (currently not used)]:FOLDER: ' \ +'-c+[Sets the config folder for taskwarrior-tui]:FOLDER: ' \ +'--config=[Sets the config folder for taskwarrior-tui]:FOLDER: ' \ '--taskdata=[Sets the .task folder using the TASKDATA environment variable for taskwarrior]:FOLDER: ' \ '--taskrc=[Sets the .taskrc file using the TASKRC environment variable for taskwarrior]:FILE: ' \ '-r+[Sets default report]:STRING: ' \ diff --git a/completions/_taskwarrior-tui.ps1 b/completions/_taskwarrior-tui.ps1 index c80f1937..f1305960 100644 --- a/completions/_taskwarrior-tui.ps1 +++ b/completions/_taskwarrior-tui.ps1 @@ -23,8 +23,8 @@ Register-ArgumentCompleter -Native -CommandName 'taskwarrior-tui' -ScriptBlock { 'taskwarrior-tui' { [CompletionResult]::new('-d', 'd', [CompletionResultType]::ParameterName, 'Sets the data folder for taskwarrior-tui') [CompletionResult]::new('--data', 'data', [CompletionResultType]::ParameterName, 'Sets the data folder for taskwarrior-tui') - [CompletionResult]::new('-c', 'c', [CompletionResultType]::ParameterName, 'Sets the config folder for taskwarrior-tui (currently not used)') - [CompletionResult]::new('--config', 'config', [CompletionResultType]::ParameterName, 'Sets the config folder for taskwarrior-tui (currently not used)') + [CompletionResult]::new('-c', 'c', [CompletionResultType]::ParameterName, 'Sets the config folder for taskwarrior-tui') + [CompletionResult]::new('--config', 'config', [CompletionResultType]::ParameterName, 'Sets the config folder for taskwarrior-tui') [CompletionResult]::new('--taskdata', 'taskdata', [CompletionResultType]::ParameterName, 'Sets the .task folder using the TASKDATA environment variable for taskwarrior') [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') diff --git a/completions/taskwarrior-tui.fish b/completions/taskwarrior-tui.fish index 7a1ba99e..89e1bdea 100644 --- a/completions/taskwarrior-tui.fish +++ b/completions/taskwarrior-tui.fish @@ -1,5 +1,5 @@ complete -c taskwarrior-tui -s d -l data -d 'Sets the data folder for taskwarrior-tui' -r -complete -c taskwarrior-tui -s c -l config -d 'Sets the config folder for taskwarrior-tui (currently not used)' -r +complete -c taskwarrior-tui -s c -l config -d 'Sets the config folder for taskwarrior-tui' -r 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 diff --git a/src/action.rs b/src/action.rs index 7d8ddae6..697ceae4 100644 --- a/src/action.rs +++ b/src/action.rs @@ -1,17 +1,39 @@ -#[derive(Clone, PartialEq, Eq, Debug, Copy)] +use serde_derive::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Action { - Report, + Tick, + Error(String), + Quit, + Refresh, + GotoBottom, + GotoTop, + GotoPageBottom, + GotoPageTop, + Down, + Up, + PageDown, + PageUp, + Delete, + Done, + ToggleStartStop, + ToggleMark, + ToggleMarkAll, + QuickTag, + Select, + SelectAll, + Undo, + Edit, + Shell, + Help, + ToggleZoom, + Context, + Next, + Previous, + Shortcut(usize), + Modify, + Log, + Annotate, Filter, Add, - Annotate, - Subprocess, - Log, - Modify, - HelpPopup, - ContextMenu, - Jump, - DeletePrompt, - UndoPrompt, - DonePrompt, - Error, } diff --git a/src/app.rs b/src/app.rs index ba4c9791..950fee42 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,15 +5,15 @@ use std::{ convert::TryInto, fs, io, io::{Read, Write}, - path::Path, + path::{Path, PathBuf}, sync::{mpsc, Arc, Mutex}, time::{Duration, Instant, SystemTime}, }; -use anyhow::{anyhow, Context as AnyhowContext, Result}; use chrono::{DateTime, Datelike, FixedOffset, Local, NaiveDate, NaiveDateTime, TimeZone, Timelike}; +use color_eyre::eyre::{anyhow, Context, Result}; use crossterm::{ - event::{DisableMouseCapture, EnableMouseCapture}, + event::{DisableMouseCapture, EnableMouseCapture, KeyCode, KeyEvent, KeyModifiers}, execute, style::style, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, @@ -32,8 +32,9 @@ use ratatui::{ Terminal, }; use regex::Regex; -use rustyline::{history::SearchDirection as HistoryDirection, line_buffer::LineBuffer, At, Editor, Word}; +use rustyline::{history::SearchDirection as HistoryDirection, At, Editor, Word}; use task_hookrs::{date::Date, import::import, project::Project, status::TaskStatus, task::Task}; +use tui_input::{backend::crossterm::EventHandler, Input}; use unicode_segmentation::{Graphemes, UnicodeSegmentation}; use unicode_width::UnicodeWidthStr; use uuid::Uuid; @@ -45,7 +46,6 @@ use crate::{ completion::{get_start_word_under_cursor, CompletionList}, config, config::Config, - event::{Event, KeyCode}, help::Help, history::HistoryContext, keyconfig::KeyConfig, @@ -57,7 +57,11 @@ use crate::{ scrollbar::Scrollbar, table::{Row, Table, TableMode, TableState}, task_report::TaskReportTable, - ui, utils, + trace_dbg, + traits::TaskwarriorTuiTask, + tui::{self, Event}, + ui, + utils::{self, get_data_dir}, }; const MAX_LINE: usize = 4096; @@ -131,7 +135,7 @@ fn get_formatted_datetime(date: &Date) -> String { ) } -fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { +fn centered_rect(r: Rect, percent_x: u16, percent_y: u16) -> Rect { let popup_layout = Layout::default() .direction(Direction::Vertical) .constraints( @@ -159,25 +163,38 @@ fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { #[derive(Debug, Clone, PartialEq, Eq)] pub enum Mode { - Tasks(Action), + TaskReport, + TaskFilter, + TaskAdd, + TaskAnnotate, + TaskSubprocess, + TaskLog, + TaskModify, + TaskHelpPopup, + TaskContextMenu, + TaskJump, + TaskDeletePrompt, + TaskUndoPrompt, + TaskDonePrompt, + TaskError, Projects, Calendar, } pub struct TaskwarriorTui { + pub tick_rate: u64, 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 command: Input, + pub filter: Input, + pub modify: Input, 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, @@ -204,13 +221,12 @@ pub struct TaskwarriorTui { 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 { + pub fn new(report: &str) -> Result { let output = std::process::Command::new("task") .arg("rc.color=off") .arg("rc._forcecolor=off") @@ -232,7 +248,7 @@ impl TaskwarriorTui { } let data = String::from_utf8_lossy(&output.stdout); - let c = Config::new(&data, report)?; + let c = Config::new()?; let kc = KeyConfig::new(&data)?; let output = std::process::Command::new("task") @@ -240,18 +256,15 @@ impl TaskwarriorTui { .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()).ok_or(anyhow!("Unable to get version string"))?; 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 data_dir = get_data_dir(); let mut app = Self { + tick_rate: c.uda_tick_rate, should_quit: false, dirty: true, task_table_state: TableState::default(), @@ -264,10 +277,10 @@ impl TaskwarriorTui { 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), + command: Input::default(), + filter: Input::default(), + modify: Input::default(), + mode: Mode::TaskReport, previous_mode: None, task_report_height: 0, task_details_scroll: 0, @@ -280,8 +293,8 @@ impl TaskwarriorTui { keyconfig: kc, terminal_width: w, terminal_height: h, - filter_history: HistoryContext::new("filter.history"), - command_history: HistoryContext::new("command.history"), + filter_history: HistoryContext::new("filter.history", data_dir.clone()), + command_history: HistoryContext::new("command.history", data_dir.clone()), history_status: None, completion_list: CompletionList::with_items(vec![]), show_completion_pane: false, @@ -290,21 +303,18 @@ impl TaskwarriorTui { 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, &mut app.changes); - } + app.filter = app.filter.with_value(app.config.filter.clone()); 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)?; app.filter_history.load()?; - app.filter_history.add(app.filter.as_str()); + app.filter_history.add(app.filter.value()); app.command_history.load()?; app.task_background(); @@ -313,88 +323,51 @@ impl TaskwarriorTui { "Found taskwarrior version {} but taskwarrior-tui works with taskwarrior>={}", app.task_version, *TASKWARRIOR_VERSION_SUPPORTED )); - app.mode = Mode::Tasks(Action::Error); + app.mode = Mode::TaskError; } 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 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 run(&mut self) -> Result<()> { + let mut tui = tui::Tui::new()?; + tui.tick_rate((self.tick_rate as usize, self.tick_rate as usize)); + tui.enter()?; - pub async fn next(&mut self) -> Option> { - self.event_loop.rx.recv().await - } + let mut events: Vec = Vec::new(); + // let mut ticker = 0; - pub async fn run(&mut self, terminal: &mut Terminal) -> Result<()> { loop { if self.requires_redraw { - terminal.resize(terminal.size()?)?; + let s = tui.size()?; + tui.resize(s)?; 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?; - } + if let Some(event) = tui.next().await { + let mut maybe_action = match event { + Event::Quit => Some(Action::Quit), + Event::Error => Some(Action::Error("Received event error".into())), + Event::Closed => Some(Action::Quit), Event::Tick => { - debug!("Tick event"); - self.update(false).await?; - } - Event::Closed => { - debug!("Event loop closed"); - } + events.clear(); + Some(Action::Tick) + } + Event::Key(key_event) => { + events.push(key_event); + self.handle_event(&events)? + } + Event::Mouse(_) => None, + Event::Resize(x, y) => None, + Event::Render => { + tui.draw(|f| self.draw(f))?; + None + } + Event::FocusGained => None, + Event::FocusLost => None, + Event::Paste(s) => None, + }; + while let Some(action) = maybe_action { + maybe_action = self.update(action)?; } } @@ -402,15 +375,27 @@ impl TaskwarriorTui { break; } } + tui.exit()?; Ok(()) } + pub fn update(&mut self, action: Action) -> Result> { + if let Action::Quit = action { + self.should_quit = true; + return Ok(None); + } + Ok(None) + } + pub fn reset_command(&mut self) { - self.command.update("", 0, &mut self.changes) + self.command.reset() } pub fn get_context(&mut self) -> Result<()> { - let output = std::process::Command::new("task").arg("_get").arg("rc.context").output()?; + 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(); @@ -449,9 +434,9 @@ impl TaskwarriorTui { 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), + _ => self.draw_task(f, main_layout), } } @@ -459,11 +444,11 @@ impl TaskwarriorTui { let titles: Vec<&str> = vec!["Tasks", "Projects", "Calendar"]; let tab_names: Vec<_> = titles.into_iter().map(Line::from).collect(); let selected_tab = match self.mode { - Mode::Tasks(_) => 0, Mode::Projects => 1, Mode::Calendar => 2, + _ => 0, }; - let navbar_block = Block::default().style(self.config.uda_style_navbar); + let navbar_block = Block::default().style(*self.config.uda_style_navbar); let context = Line::from(vec![ Span::from("["), Span::from(if self.current_context.is_empty() { @@ -488,10 +473,11 @@ impl TaskwarriorTui { } pub fn draw_debug(&mut self, f: &mut Frame) { - let area = centered_rect(50, 50, f.size()); + let area = centered_rect(f.size(), 50, 50); 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)); + let p = + Paragraph::new(Text::from(t)).block(Block::default().borders(Borders::ALL).border_type(BorderType::Rounded)); f.render_widget(p, area); } @@ -513,7 +499,7 @@ impl TaskwarriorTui { .get(&format!("color.project.{}", project[0])) .copied() .unwrap_or_default(); - style = style.patch(s); + style = style.patch(*s); } &_ => {} } @@ -523,7 +509,7 @@ impl TaskwarriorTui { pub fn draw_calendar(&mut self, f: &mut Frame, layout: Rect) { let mut c = Calendar::default() - .today_style(self.config.uda_style_calendar_today) + .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) @@ -532,7 +518,7 @@ impl TaskwarriorTui { f.render_widget(c, layout); } - pub fn draw_task(&mut self, f: &mut Frame, layout: Rect, action: Action) { + pub fn draw_task(&mut self, f: &mut Frame, layout: Rect) { let rects = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(0), Constraint::Length(2)].as_ref()) @@ -573,27 +559,35 @@ impl TaskwarriorTui { }; // render task mode - self.handle_task_mode_action(f, &rects, &task_ids, action); + self.draw_task_mode_action(f, &rects, &task_ids); } - fn handle_task_mode_action(&mut self, f: &mut Frame, rects: &[Rect], task_ids: &[String], action: Action) { - match action { - Action::Error => { + fn draw_task_mode_action(&mut self, f: &mut Frame, rects: &[Rect], task_ids: &[String]) { + match self.mode { + Mode::TaskError => { self.draw_command( f, rects[1], "Press any key to continue.", - (Span::styled("Error", Style::default().add_modifier(Modifier::BOLD)), None), + ( + 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()); + let rect = centered_rect(f.size(), 90, 60); f.render_widget(Clear, rect); let p = Paragraph::new(Text::from(text)) - .block(Block::default().borders(Borders::ALL).border_type(BorderType::Rounded).title(title)) + .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 @@ -602,42 +596,45 @@ impl TaskwarriorTui { .constraints([Constraint::Min(0)].as_ref()) .split(f.size()); } - Action::Report => { + Mode::TaskReport => { // reset error when entering Action::Report self.previous_mode = None; self.error = None; - let position = Self::get_position(&self.command); + let position = self.command.visual_cursor(); self.draw_command( f, rects[1], - self.filter.as_str(), + self.filter.value(), (Span::raw("Filter Tasks"), self.history_status.as_ref().map(Span::raw)), - Self::get_position(&self.filter), + self.filter.visual_cursor(), false, self.error.clone(), ); } - Action::Jump => { - let position = Self::get_position(&self.command); + Mode::TaskJump => { + let position = self.command.visual_cursor(); self.draw_command( f, rects[1], - self.command.as_str(), - (Span::styled("Jump to Task", Style::default().add_modifier(Modifier::BOLD)), None), + self.command.value(), + ( + 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); + Mode::TaskFilter => { + let position = self.filter.visual_cursor(); if self.show_completion_pane { self.draw_completion_pop_up(f, rects[1], position); } self.draw_command( f, rects[1], - self.filter.as_str(), + self.filter.value(), ( Span::styled("Filter Tasks", Style::default().add_modifier(Modifier::BOLD)), self @@ -650,18 +647,18 @@ impl TaskwarriorTui { 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); + Mode::TaskLog => { + if self.config.uda_auto_insert_double_quotes_on_log && self.command.value().is_empty() { + self.command = self.command.clone().with_value(r#""""#.to_string()); }; - let position = Self::get_position(&self.command); + let position = self.command.visual_cursor(); if self.show_completion_pane { self.draw_completion_pop_up(f, rects[1], position); } self.draw_command( f, rects[1], - self.command.as_str(), + self.command.value(), ( Span::styled("Log Task", Style::default().add_modifier(Modifier::BOLD)), self @@ -674,20 +671,23 @@ impl TaskwarriorTui { self.error.clone(), ); } - Action::Subprocess => { - let position = Self::get_position(&self.command); + Mode::TaskSubprocess => { + let position = self.command.visual_cursor(); self.draw_command( f, rects[1], - self.command.as_str(), - (Span::styled("Shell Command", Style::default().add_modifier(Modifier::BOLD)), None), + self.command.value(), + ( + 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); + Mode::TaskModify => { + let position = self.modify.visual_cursor(); if self.show_completion_pane { self.draw_completion_pop_up(f, rects[1], position); } @@ -699,7 +699,7 @@ impl TaskwarriorTui { self.draw_command( f, rects[1], - self.modify.as_str(), + self.modify.value(), ( Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), self @@ -712,11 +712,11 @@ impl TaskwarriorTui { 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); + Mode::TaskAnnotate => { + if self.config.uda_auto_insert_double_quotes_on_annotate && self.command.value().is_empty() { + self.command = self.command.clone().with_value(r#""""#.to_string()); }; - let position = Self::get_position(&self.command); + let position = self.command.visual_cursor(); if self.show_completion_pane { self.draw_completion_pop_up(f, rects[1], position); } @@ -728,7 +728,7 @@ impl TaskwarriorTui { self.draw_command( f, rects[1], - self.command.as_str(), + self.command.value(), ( Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), self @@ -741,18 +741,18 @@ impl TaskwarriorTui { 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); + Mode::TaskAdd => { + if self.config.uda_auto_insert_double_quotes_on_add && self.command.value().is_empty() { + self.command = self.command.clone().with_value(r#""""#.to_string()); }; - let position = Self::get_position(&self.command); + let position = self.command.visual_cursor(); if self.show_completion_pane { self.draw_completion_pop_up(f, rects[1], position); } self.draw_command( f, rects[1], - self.command.as_str(), + self.command.value(), ( Span::styled("Add Task", Style::default().add_modifier(Modifier::BOLD)), self @@ -765,31 +765,31 @@ impl TaskwarriorTui { self.error.clone(), ); } - Action::HelpPopup => { + Mode::TaskHelpPopup => { self.draw_command( f, rects[1], - self.filter.as_str(), + self.filter.value(), ("Filter Tasks".into(), None), - Self::get_position(&self.filter), + self.filter.visual_cursor(), false, self.error.clone(), ); self.draw_help_popup(f, 80, 90); } - Action::ContextMenu => { + Mode::TaskContextMenu => { self.draw_command( f, rects[1], - self.filter.as_str(), + self.filter.value(), ("Filter Tasks".into(), None), - Self::get_position(&self.filter), + self.filter.visual_cursor(), false, self.error.clone(), ); self.draw_context_menu(f, 80, 50); } - Action::DonePrompt => { + Mode::TaskDonePrompt => { let label = if task_ids.len() > 1 { format!("Done Tasks {}?", task_ids.join(",")) } else { @@ -813,7 +813,7 @@ impl TaskwarriorTui { self.error.clone(), ); } - Action::DeletePrompt => { + Mode::TaskDeletePrompt => { let label = if task_ids.len() > 1 { format!("Delete Tasks {}?", task_ids.join(",")) } else { @@ -837,7 +837,7 @@ impl TaskwarriorTui { self.error.clone(), ); } - Action::UndoPrompt => { + Mode::TaskUndoPrompt => { let label = "Run `task undo`?"; let k = match self.keyconfig.undo { KeyCode::Char(c) => c.to_string(), @@ -857,6 +857,7 @@ impl TaskwarriorTui { self.error.clone(), ); } + _ => {} } } @@ -877,19 +878,8 @@ impl TaskwarriorTui { } } - 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()); + let area = centered_rect(f.size(), percent_x, percent_y); f.render_widget(Clear, area); let chunks = Layout::default() @@ -919,9 +909,15 @@ impl TaskwarriorTui { .constraints([Constraint::Min(0)].as_ref()) .split(f.size()); - let area = centered_rect(percent_x, percent_y, f.size()); + let area = centered_rect(f.size(), percent_x, percent_y); - f.render_widget(Clear, area.inner(&Margin { vertical: 0, horizontal: 0 })); + f.render_widget( + Clear, + area.inner(&Margin { + vertical: 0, + horizontal: 0, + }), + ); let (contexts, headers) = self.get_all_contexts(); @@ -935,7 +931,7 @@ impl TaskwarriorTui { 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; + 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() { @@ -954,7 +950,10 @@ impl TaskwarriorTui { Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .title(Line::from(vec![Span::styled("Context", Style::default().add_modifier(Modifier::BOLD))])), + .title(Line::from(vec![Span::styled( + "Context", + Style::default().add_modifier(Modifier::BOLD), + )])), ) .header_style( self @@ -994,8 +993,8 @@ impl TaskwarriorTui { // 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) + .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(); @@ -1004,7 +1003,12 @@ impl TaskwarriorTui { 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), + 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 { @@ -1030,7 +1034,10 @@ impl TaskwarriorTui { ) { // 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); + 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) @@ -1038,7 +1045,7 @@ impl TaskwarriorTui { .split(rect); // render command title - let mut style = self.config.uda_style_command; + let mut style = self.config.uda_style_command.0; if error.is_some() { style = style.fg(Color::Red); }; @@ -1070,7 +1077,9 @@ impl TaskwarriorTui { 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), + (data.lines().count() as u16) + .saturating_sub(rect.height) + .saturating_add(2), self.task_details_scroll, ); let p = Paragraph::new(Text::from(&data[..])) @@ -1128,21 +1137,31 @@ impl TaskwarriorTui { 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); + let s = self + .config + .color + .get(&format!("color.uda.priority.{}", p)) + .copied() + .unwrap_or_default(); + style = style.patch(s.0); } } 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); + style = style.patch(s.0); } } } 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); + let s = self + .config + .color + .get(&format!("color.project.{}", p)) + .copied() + .unwrap_or_default(); + style = style.patch(s.0); } } else if task .tags() @@ -1151,7 +1170,7 @@ impl TaskwarriorTui { { 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 = style.patch(s.0); } } @@ -1187,7 +1206,10 @@ impl TaskwarriorTui { // 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(); + let index = widths + .iter() + .position(|i| i == widths.iter().max().unwrap_or(&0)) + .unwrap_or_default(); if widths[index] == 1 { break; } @@ -1228,7 +1250,7 @@ impl TaskwarriorTui { let style = self.style_for_task(&self.tasks[i]); if i == selected { pos = i; - highlight_style = style.patch(self.config.uda_style_report_selection); + highlight_style = style.patch(self.config.uda_style_report_selection.0); if self.config.uda_selection_bold { highlight_style = highlight_style.add_modifier(Modifier::BOLD); } @@ -1272,9 +1294,9 @@ impl TaskwarriorTui { 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_style = self.config.uda_style_report_scrollbar.0; widget.pos_symbol = self.config.uda_scrollbar_indicator.clone(); - widget.area_style = self.config.uda_style_report_scrollbar_area; + widget.area_style = self.config.uda_style_report_scrollbar_area.0; widget.area_symbol = self.config.uda_scrollbar_area.clone(); f.render_widget(widget, rect); } @@ -1298,39 +1320,39 @@ impl TaskwarriorTui { (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(); - - Ok(()) - } + // pub 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()?; + // } + // self.selection_fix(); + // + // Ok(()) + // } pub fn selection_fix(&mut self) { if let (Some(t), Some(id)) = (self.task_current(), self.current_selection_id) { @@ -1364,7 +1386,7 @@ impl TaskwarriorTui { } } - pub async fn update_task_details(&mut self) -> Result<()> { + pub fn update_task_details(&mut self) -> Result<()> { if self.tasks.is_empty() { return Ok(()); } @@ -1395,7 +1417,7 @@ impl TaskwarriorTui { l.dedup(); - let (tx, mut rx) = tokio::sync::mpsc::channel(100); + let (tx, rx) = std::sync::mpsc::channel(); let tasks = self.tasks.clone(); let defaultwidth = self.terminal_width.saturating_sub(2); for s in &l { @@ -1419,13 +1441,13 @@ impl TaskwarriorTui { .await; if let Ok(output) = output { let data = String::from_utf8_lossy(&output.stdout).to_string(); - _tx.send(Some((task_uuid, data))).await.unwrap(); + _tx.send(Some((task_uuid, data))).unwrap(); } }); } } drop(tx); - while let Some(Some((task_uuid, data))) = rx.recv().await { + while let Some((task_uuid, data)) = rx.recv()? { self.task_details.insert(task_uuid, data); } Ok(()) @@ -1596,7 +1618,7 @@ impl TaskwarriorTui { if self.tasks.is_empty() { return Ok(()); } - let i = self.command.as_str().parse::()?; + let i = self.command.value().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; @@ -1662,17 +1684,20 @@ impl TaskwarriorTui { 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)); + if self.mode == Mode::TaskError { + self.mode = self.previous_mode.clone().unwrap_or(Mode::TaskReport); self.previous_mode = None; } } else { self.error = Some(format!("Unable to parse output of `{:?}`:\n`{:?}`", task, data)); - self.mode = Mode::Tasks(Action::Error); + self.mode = Mode::TaskError; debug!("Unable to parse output: {:?}", data); } } else { - self.error = Some(format!("Cannot run `{:?}` - ({}) error:\n{}", &task, output.status, error)); + self.error = Some(format!( + "Cannot run `{:?}` - ({}) error:\n{}", + &task, output.status, error + )); } Ok(()) @@ -1689,7 +1714,9 @@ impl TaskwarriorTui { .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()) { + if let Some(args) = + shlex::split(format!(r#"rc.report.{}.filter='{}'"#, self.report, self.filter.value().trim()).trim()) + { for arg in args { task.arg(arg); } @@ -1721,17 +1748,20 @@ impl TaskwarriorTui { 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)); + if self.mode == Mode::TaskError { + self.mode = self.previous_mode.clone().unwrap_or(Mode::TaskReport); self.previous_mode = None; } } else { self.error = Some(format!("Unable to parse output of `{:?}`:\n`{:?}`", task, data)); - self.mode = Mode::Tasks(Action::Error); + self.mode = Mode::TaskError; debug!("Unable to parse output: {:?}", data); } } else { - self.error = Some(format!("Cannot run `{:?}` - ({}) error:\n{}", &task, output.status, error)); + self.error = Some(format!( + "Cannot run `{:?}` - ({}) error:\n{}", + &task, output.status, error + )); } Ok(()) @@ -1758,9 +1788,13 @@ impl TaskwarriorTui { } pub fn task_subprocess(&mut self) -> Result<(), String> { - let task_uuids = if self.tasks.is_empty() { vec![] } else { self.selected_task_uuids() }; + let task_uuids = if self.tasks.is_empty() { + vec![] + } else { + self.selected_task_uuids() + }; - let shell = self.command.as_str(); + let shell = self.command.value(); let r = match shlex::split(shell) { Some(cmd) => { @@ -1781,7 +1815,14 @@ impl TaskwarriorTui { 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)) + Err(format!( + r#"Shell command `{}` ran successfully but printed the following output: + + {} + + Suppress output of shell commands to prevent the error prompt from showing up."#, + shell, output + )) } else { Ok(()) } @@ -1807,7 +1848,7 @@ impl TaskwarriorTui { command.arg("log"); - let shell = self.command.as_str(); + let shell = self.command.value(); match shlex::split(shell) { Some(cmd) => { @@ -1817,10 +1858,16 @@ impl TaskwarriorTui { let output = command.output(); match output { Ok(_) => Ok(()), - Err(_) => Err(format!("Cannot run `task log {}`. Check documentation for more information", shell)), + Err(_) => Err(format!( + "Cannot run `task log {}`. Check documentation for more information", + shell + )), } } - None => Err(format!("Unable to run `{:?}`: shlex::split(`{}`) failed.", command, shell)), + None => Err(format!( + "Unable to run `{:?}`: shlex::split(`{}`) failed.", + command, shell + )), } } @@ -1852,22 +1899,30 @@ impl TaskwarriorTui { }); } - pub async fn task_shortcut(&mut self, s: usize) -> Result<(), String> { - self.pause_tui().await.unwrap(); + pub fn task_shortcut(&mut self, s: usize) -> Result<(), String> { + // self.pause_tui().await.unwrap(); - let task_uuids = if self.tasks.is_empty() { vec![] } else { self.selected_task_uuids() }; + let task_uuids = if self.tasks.is_empty() { + vec![] + } else { + self.selected_task_uuids() + }; let shell = &self.config.uda_shortcuts[s]; if shell.is_empty() { - self.resume_tui().await.unwrap(); + // self.resume_tui().await.unwrap(); return Err("Trying to run empty shortcut.".to_string()); } let shell = format!( "{} {}", shell, - task_uuids.iter().map(ToString::to_string).collect::>().join(" ") + task_uuids + .iter() + .map(ToString::to_string) + .collect::>() + .join(" ") ); let shell = shellexpand::tilde(&shell).into_owned(); @@ -1897,10 +1952,16 @@ impl TaskwarriorTui { Err(s) => Err(format!("`{}` failed to wait with output: {}", shell, s)), } } - Err(err) => Err(format!("`{}` failed: Unable to spawn shortcut number {} - Error: {}", shell, s, err)), + Err(err) => Err(format!( + "`{}` failed: Unable to spawn shortcut number {} - Error: {}", + shell, s, err + )), } } - None => Err(format!("Unable to run shortcut number {}: shlex::split(`{}`) failed.", s, shell)), + None => Err(format!( + "Unable to run shortcut number {}: shlex::split(`{}`) failed.", + s, shell + )), }; if task_uuids.len() == 1 { @@ -1909,7 +1970,7 @@ impl TaskwarriorTui { } } - self.resume_tui().await.unwrap(); + // self.resume_tui().await.unwrap(); r } @@ -1931,7 +1992,7 @@ impl TaskwarriorTui { } command.arg("modify"); - let shell = self.modify.as_str(); + let shell = self.modify.value(); let r = match shlex::split(shell) { Some(cmd) => { @@ -1982,7 +2043,7 @@ impl TaskwarriorTui { } command.arg("annotate"); - let shell = self.command.as_str(); + let shell = self.command.value(); let r = match shlex::split(shell) { Some(cmd) => { @@ -2000,7 +2061,11 @@ impl TaskwarriorTui { } Err(_) => Err(format!( "Cannot run `task {} annotate {}`. Check documentation for more information", - task_uuids.iter().map(ToString::to_string).collect::>().join(" "), + task_uuids + .iter() + .map(ToString::to_string) + .collect::>() + .join(" "), shell )), } @@ -2020,7 +2085,7 @@ impl TaskwarriorTui { let mut command = std::process::Command::new("task"); command.arg("add"); - let shell = self.command.as_str(); + let shell = self.command.value(); match shlex::split(shell) { Some(cmd) => { @@ -2046,12 +2111,17 @@ impl TaskwarriorTui { Err(e) => Err(format!("Cannot run `{:?}`: {}", command, e)), } } - None => Err(format!("Unable to run `{:?}`: shlex::split(`{}`) failed.", command, shell)), + None => Err(format!( + "Unable to run `{:?}`: shlex::split(`{}`) failed.", + command, shell + )), } } pub fn task_virtual_tags(task_uuid: Uuid) -> Result { - let output = std::process::Command::new("task").arg(format!("{}", task_uuid)).output(); + let output = std::process::Command::new("task") + .arg(format!("{}", task_uuid)) + .output(); match output { Ok(output) => { @@ -2070,7 +2140,10 @@ impl TaskwarriorTui { task_uuid )) } - Err(_) => Err(format!("Cannot run `task {}`. Check documentation for more information", task_uuid)), + Err(_) => Err(format!( + "Cannot run `task {}`. Check documentation for more information", + task_uuid + )), } } @@ -2083,13 +2156,19 @@ impl TaskwarriorTui { for task_uuid in &task_uuids { let mut command = "start"; - for tag in TaskwarriorTui::task_virtual_tags(*task_uuid).unwrap_or_default().split(' ') { + 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(); + 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)); } @@ -2130,7 +2209,10 @@ impl TaskwarriorTui { .output(); if output.is_err() { - return Err(format!("Error running `task modify {}` for task `{}`.", tag_to_set, task_uuid,)); + return Err(format!( + "Error running `task modify {}` for task `{}`.", + tag_to_set, task_uuid, + )); } } } @@ -2166,7 +2248,11 @@ impl TaskwarriorTui { 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(" ") + task_uuids + .iter() + .map(ToString::to_string) + .collect::>() + .join(" ") )), }; self.current_selection_uuid = None; @@ -2194,7 +2280,11 @@ impl TaskwarriorTui { 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(" ") + task_uuids + .iter() + .map(ToString::to_string) + .collect::>() + .join(" ") )), }; self.current_selection_uuid = None; @@ -2203,12 +2293,17 @@ impl TaskwarriorTui { } pub fn task_undo(&mut self) -> Result<(), String> { - let output = std::process::Command::new("task").arg("rc.confirmation=off").arg("undo").output(); + 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(); + 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); @@ -2220,18 +2315,21 @@ impl TaskwarriorTui { } } - pub async fn task_edit(&mut self) -> Result<(), String> { + pub fn task_edit(&mut self) -> Result<(), String> { if self.tasks.is_empty() { return Ok(()); } - self.pause_tui().await.unwrap(); + // 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 = std::process::Command::new("task") + .arg(format!("{}", task_uuid)) + .arg("edit") + .spawn(); let r = match r { Ok(child) => { @@ -2260,7 +2358,7 @@ impl TaskwarriorTui { self.current_selection_uuid = Some(task_uuid); - self.resume_tui().await.unwrap(); + // self.resume_tui().await.unwrap(); r } @@ -2280,7 +2378,7 @@ impl TaskwarriorTui { 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()); + tasks[l_i].add_tag("UNBLOCKED".to_string()); for dep in deps { for r_i in 0..tasks.len() { if tasks[r_i].uuid() == &dep { @@ -2291,9 +2389,9 @@ impl TaskwarriorTui { && 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()); + tasks[l_i].remove_tag("UNBLOCKED"); + tasks[l_i].add_tag("BLOCKED".to_string()); + tasks[r_i].add_tag("BLOCKING".to_string()); } break; } @@ -2305,45 +2403,45 @@ impl TaskwarriorTui { // 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::Waiting => task.add_tag("WAITING".to_string()), + TaskStatus::Completed => task.add_tag("COMPLETED".to_string()), + TaskStatus::Pending => task.add_tag("PENDING".to_string()), + TaskStatus::Deleted => task.add_tag("DELETED".to_string()), TaskStatus::Recurring => (), } if task.start().is_some() { - add_tag(task, "ACTIVE".to_string()); + task.add_tag("ACTIVE".to_string()); } if task.scheduled().is_some() { - add_tag(task, "SCHEDULED".to_string()); + task.add_tag("SCHEDULED".to_string()); } if task.parent().is_some() { - add_tag(task, "INSTANCE".to_string()); + task.add_tag("INSTANCE".to_string()); } if task.until().is_some() { - add_tag(task, "UNTIL".to_string()); + task.add_tag("UNTIL".to_string()); } if task.annotations().is_some() { - add_tag(task, "ANNOTATED".to_string()); + task.add_tag("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()); + task.add_tag("TAGGED".to_string()); } if !task.uda().is_empty() { - add_tag(task, "UDA".to_string()); + task.add_tag("UDA".to_string()); } if task.mask().is_some() { - add_tag(task, "TEMPLATE".to_string()); + task.add_tag("TEMPLATE".to_string()); } if task.project().is_some() { - add_tag(task, "PROJECT".to_string()); + task.add_tag("PROJECT".to_string()); } if task.priority().is_some() { - add_tag(task, "PRIORITY".to_string()); + task.add_tag("PRIORITY".to_string()); } if task.recur().is_some() { - add_tag(task, "RECURRING".to_string()); + task.add_tag("RECURRING".to_string()); let r = task.recur().unwrap(); } if let Some(d) = task.due() { @@ -2355,24 +2453,24 @@ impl TaskwarriorTui { 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()); + task.add_tag("MONTH".to_string()); } if (reference - chrono::Duration::nanoseconds(1)).month() % 4 == now.month() % 4 { - add_tag(task, "QUARTER".to_string()); + task.add_tag("QUARTER".to_string()); } if reference.year() == now.year() { - add_tag(task, "YEAR".to_string()); + task.add_tag("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()); + task.add_tag("DUE".to_string()); + task.add_tag("TODAY".to_string()); + task.add_tag("DUETODAY".to_string()); } DateState::AfterToday => { - add_tag(task, "DUE".to_string()); + task.add_tag("DUE".to_string()); if reference.date_naive() == (now + chrono::Duration::days(1)).date_naive() { - add_tag(task, "TOMORROW".to_string()); + task.add_tag("TOMORROW".to_string()); } } _ => (), @@ -2386,7 +2484,7 @@ impl TaskwarriorTui { let now = Local::now().naive_utc(); let d = NaiveDateTime::new(d.date(), d.time()); if d < now { - add_tag(task, "OVERDUE".to_string()); + task.add_tag("OVERDUE".to_string()); } } } @@ -2429,1184 +2527,198 @@ impl TaskwarriorTui { es } - pub async fn handle_input(&mut self, input: KeyCode) -> Result<()> { + pub fn handle_event(&mut self, input: &Vec) -> 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?; + ProjectsState::handle_input(self, *input.first().unwrap())?; + // self.update(false)?; } 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); - self.mode = Mode::Tasks(Action::Error); - } - } - if self.calendar_year > 0 { - self.calendar_year -= 10; - } + // if input == self.keyconfig.quit { + // 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); + // self.mode = Mode::Tasks(Action::Error); + // } + // } + // if self.calendar_year > 0 { + // self.calendar_year -= 10; + // } + // } + // } + } + _ => { + return self.handle_input_by_task_mode(input); + } + } + self.update_task_table_state(); + Ok(None) + } + + fn handle_input_by_task_mode(&mut self, input: &Vec) -> Result> { + match self.mode { + Mode::TaskReport => { + if let Some(keymap) = self.config.keymap.get("task-report") { + log::info!("Received input: {:?}", &input); + log::info!("Action {:?}", keymap.get(input)); + if let Some(action) = keymap.get(input) { + log::info!("Got action: {:?}", &action); + return Ok(Some(action.clone())); } } } + _ => {} } - self.update_task_table_state(); - Ok(()) + Ok(None) } - 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); - self.mode = Mode::Tasks(Action::Error); - } - } - } - } 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); - self.mode = Mode::Tasks(Action::Error); - } - } - } - } else if input == self.keyconfig.start_stop { - match self.task_start_stop() { - Ok(_) => self.update(true).await?, - Err(e) => { - self.error = Some(e); - self.mode = Mode::Tasks(Action::Error); - } - } - } else if input == self.keyconfig.quick_tag { - match self.task_quick_tag() { - Ok(_) => self.update(true).await?, - Err(e) => { - self.error = Some(e); - self.mode = Mode::Tasks(Action::Error); - } - } - } else if input == self.keyconfig.edit { - match self.task_edit().await { - Ok(_) => self.update(true).await?, - Err(e) => { - self.error = Some(e); - self.mode = Mode::Tasks(Action::Error); - } - } - } else if input == self.keyconfig.undo { - if self.config.uda_task_report_prompt_on_undo { - self.mode = Mode::Tasks(Action::UndoPrompt); - if self.task_current().is_none() { - self.mode = Mode::Tasks(Action::Report); - } - } else { - match self.task_undo() { - Ok(_) => self.update(true).await?, - Err(e) => { - self.error = Some(e); - self.mode = Mode::Tasks(Action::Error); - } - } - } - } 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(), &mut self.changes); - } - None => self.modify.update("", 0, &mut self.changes), - }, - TableMode::MultipleSelection => self.modify.update("", 0, &mut self.changes), - } - } 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!( - "{} / {}", + 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::TaskModify | Mode::TaskFilter | Mode::TaskAnnotate | Mode::TaskAdd | Mode::TaskLog = self.mode { + for s in [ + "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)); + } + } + + if let Mode::TaskModify | Mode::TaskFilter | Mode::TaskAnnotate | Mode::TaskAdd | Mode::TaskLog = self.mode { + for s in [ + ".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::TaskModify | Mode::TaskFilter | Mode::TaskAnnotate | Mode::TaskAdd | Mode::TaskLog = 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 - .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); - self.mode = Mode::Tasks(Action::Error); - } - } - } 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); - self.mode = Mode::Tasks(Action::Error); - } - } - } 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); - self.mode = Mode::Tasks(Action::Error); - } - } - } 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); - self.mode = Mode::Tasks(Action::Error); - } - } - } 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); - self.mode = Mode::Tasks(Action::Error); - } - } - } 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); - self.mode = Mode::Tasks(Action::Error); - } - } - } 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); - self.mode = Mode::Tasks(Action::Error); - } - } - } 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); - self.mode = Mode::Tasks(Action::Error); - } - } - } 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); - self.mode = Mode::Tasks(Action::Error); - } - } - } 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; + .completion_list + .insert(("tag".to_string(), format!("tag:{}", &tag))); } - } 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()); - } - } - } - } - } 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()); - self.mode = Mode::Tasks(Action::Error); - } - } + } + 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))); } } } - 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); - } - } - 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); - } - } - 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?; - } - Err(e) => { - self.error = Some(e); - self.mode = Mode::Tasks(Action::Error); - } - } - } - } - 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, &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.mode = Mode::Tasks(Action::Error); - } - } - } - } - 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?; - } - Err(e) => { - self.error = Some(e); - self.mode = Mode::Tasks(Action::Error); - } - } - } - } - 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?; - } - Err(e) => { - self.error = Some(e); - self.mode = Mode::Tasks(Action::Error); - } - } - } - } - 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?; - } - Err(e) => { - self.reset_command(); - self.error = Some(e.to_string()); - self.mode = Mode::Tasks(Action::Error); - } - } - } - } - 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); - self.mode = Mode::Tasks(Action::Error); - } - } - } - } - 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::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?; - } - } - 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?; - } - } - 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; - } - } - 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; - } - } - 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, &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); - self.mode = Mode::Tasks(Action::Error); - } - } - } - } 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); - self.mode = Mode::Tasks(Action::Error); - } - } - } - } 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::UndoPrompt => { - if input == self.keyconfig.undo || 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_undo() { - Ok(_) => { - self.mode = Mode::Tasks(Action::Report); - self.update(true).await?; - } - Err(e) => { - self.error = Some(e); - self.mode = Mode::Tasks(Action::Error); - } - } - } - } 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(()) - } - - 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 [ - "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)); - } - } - - if let Mode::Tasks(Action::Modify | Action::Filter | Action::Annotate | Action::Add | Action::Log) = self.mode { - for s in [ - ".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))); - } - } - } - } - 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(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))); + 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))); + 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))); + 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))); + self + .completion_list + .insert(("end".to_string(), get_formatted_datetime(date))); } } } - if self.mode == Mode::Tasks(Action::Filter) { + if self.mode == Mode::TaskFilter { 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())); @@ -3616,19 +2728,19 @@ impl TaskwarriorTui { 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(); + Mode::TaskAdd | Mode::TaskAnnotate | Mode::TaskLog => { + let i = get_start_word_under_cursor(self.command.value(), self.command.cursor()); + let input = self.command.value()[i..self.command.cursor()].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(); + Mode::TaskModify => { + let i = get_start_word_under_cursor(self.modify.value(), self.modify.cursor()); + let input = self.modify.value()[i..self.modify.cursor()].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(); + Mode::TaskFilter => { + let i = get_start_word_under_cursor(self.filter.value(), self.filter.cursor()); + let input = self.filter.value()[i..self.filter.cursor()].to_string(); self.completion_list.input(input, "".to_string()); } _ => {} @@ -3636,1145 +2748,5 @@ impl TaskwarriorTui { } } -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])), - } -} - -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); - } - } -} - #[cfg(test)] -mod tests { - use std::{ffi::OsStr, fmt::Write, fs::File, io, path::Path}; - - use ratatui::{backend::TestBackend, buffer::Buffer}; - - use super::*; - - /// 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)); - } - - 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(); - } - 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.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(); - 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 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)); - } - - 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; - } - - app.task_quick_tag().unwrap(); - app.update(true).await.unwrap(); - - 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.task_quick_tag().unwrap(); - app.update(true).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)); - } - } - - 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_eq!(style, Style::default().fg(Color::Indexed(2))); - - let task = app.task_by_id(11).unwrap(); - let style = app.style_for_task(&task); - } - - async fn test_task_context() { - let mut app = TaskwarriorTui::new("next", false).await.unwrap(); - - assert!(app.update(true).await.is_ok()); - - app.context_select().unwrap(); - - assert_eq!(app.tasks.len(), 26); - 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)); - - assert!(app.update(true).await.is_ok()); - - assert_eq!(app.tasks.len(), 1); - assert_eq!(app.current_context_filter, "+finance -private"); - - 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(), 26); - assert_eq!(app.current_context_filter, ""); - } - - async fn test_task_tomorrow() { - 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 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(); - - 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 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_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 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 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(), - ); - - 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 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 mut app = TaskwarriorTui::new("next", false).await.unwrap(); - - app.task_report_next(); - app.context_next(); - - let total_tasks: u64 = 0; - - 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()); - - 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)); - } - - let mut app = TaskwarriorTui::new("next", false).await.unwrap(); - - let total_tasks: u64 = 26; - - 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()); - - 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), - } - - 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)); - } - } - - 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; - - 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 1 for testing draw' priority:U"; - - 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 0..=49 { - expected.get_mut(i, 2).set_style(Style::default().add_modifier(Modifier::UNDERLINED)); - } - - for i in 3..=22 { - expected.get_mut(i, 4).set_style(Style::default().bg(Color::Reset)); - } - - for i in 25..=44 { - expected.get_mut(i, 4).set_style(Style::default().bg(Color::Reset)); - } - - for i in 3..=22 { - expected - .get_mut(i, 5) - .set_style(Style::default().bg(Color::Reset).add_modifier(Modifier::UNDERLINED)); - } - - for i in 25..=44 { - expected - .get_mut(i, 5) - .set_style(Style::default().bg(Color::Reset).add_modifier(Modifier::UNDERLINED)); - } - - 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)); - } - - for i in 1..=10 { - // Task - expected.get_mut(i, 1).set_style(Style::default().add_modifier(Modifier::UNDERLINED)); - } - - for i in 12..=71 { - // Task - expected.get_mut(i, 1).set_style(Style::default().add_modifier(Modifier::UNDERLINED)); - } - - for i in 73..=78 { - // Task - expected.get_mut(i, 1).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)); - } - - 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(); - - 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())); - } -} +mod tests {} diff --git a/src/calendar.rs b/src/calendar.rs index 04d50566..bd065104 100644 --- a/src/calendar.rs +++ b/src/calendar.rs @@ -7,7 +7,9 @@ const COL_WIDTH: usize = 21; use std::cmp::min; -use chrono::{format::Fixed, DateTime, Datelike, Duration, FixedOffset, Local, Month, NaiveDate, NaiveDateTime, TimeZone}; +use chrono::{ + format::Fixed, DateTime, Datelike, Duration, FixedOffset, Local, Month, NaiveDate, NaiveDateTime, TimeZone, +}; use ratatui::{ buffer::Buffer, layout::Rect, @@ -228,13 +230,20 @@ impl<'a> Widget for Calendar<'a> { .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()))) + ( + 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 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); diff --git a/src/cli.rs b/src/cli.rs index 3482cebe..dbf0a92c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -21,7 +21,7 @@ pub fn generate_cli_app() -> clap::Command { .short('c') .long("config") .value_name("FOLDER") - .help("Sets the config folder for taskwarrior-tui (currently not used)") + .help("Sets the config folder for taskwarrior-tui") .action(clap::ArgAction::Set), ) .arg( diff --git a/src/completion.rs b/src/completion.rs index 858c3d84..3e413b8d 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -100,7 +100,11 @@ impl CompletionList { state: ListState::default(), current: String::new(), pos: 0, - helper: TaskwarriorTuiCompletionHelper { candidates, context, input }, + helper: TaskwarriorTuiCompletionHelper { + candidates, + context, + input, + }, } } diff --git a/src/config.rs b/src/config.rs index fa7afa1c..59478116 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,52 +1,274 @@ -use std::{collections::HashMap, error::Error, str}; +use std::{collections::HashMap, error::Error, path::PathBuf, str}; -use anyhow::{Context, Result}; +use color_eyre::eyre::{eyre, Context, Result}; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use figment::{ + providers::{Env, Format, Serialized, Toml}, + Figment, +}; use ratatui::{ style::{Color, Modifier, Style}, - symbols::{bar::FULL, line::DOUBLE_VERTICAL}, + symbols::line::DOUBLE_VERTICAL, +}; +use serde::{ + de::{self, Deserialize, Deserializer, MapAccess, Visitor}, + ser::{self, Serialize, Serializer}, }; +use serde_derive::{Deserialize, Serialize}; + +use crate::{action::Action, keyevent::parse_key_sequence, keymap::KeyMap, utils::get_config_dir}; + +#[derive(Default, Clone, Debug, Copy)] +pub struct SerdeStyle(pub Style); -trait TaskWarriorBool { - fn get_bool(&self) -> Option; +impl std::ops::Deref for SerdeStyle { + type Target = Style; + + fn deref(&self) -> &Self::Target { + &self.0 + } } +impl std::ops::DerefMut for SerdeStyle { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl<'de> Deserialize<'de> for SerdeStyle { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct StyleVisitor; + + impl<'de> Visitor<'de> for StyleVisitor { + type Value = SerdeStyle; -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 expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string representation of tui::style::Style") + } + + fn visit_str(self, v: &str) -> Result { + Ok(SerdeStyle(get_tcolor(v))) + } } + + deserializer.deserialize_str(StyleVisitor) + } +} + +pub fn get_tcolor(line: &str) -> Style { + let (foreground, background) = line.split_at(line.to_lowercase().find("on ").unwrap_or(line.len())); + let (mut foreground, mut background) = (String::from(foreground), String::from(background)); + background = background.replace("on ", ""); + let mut modifiers = Modifier::empty(); + if foreground.contains("bright") { + foreground = foreground.replace("bright ", ""); + background = background.replace("bright ", ""); + background.insert_str(0, "bright "); + } + foreground = foreground.replace("grey", "gray"); + background = background.replace("grey", "gray"); + if foreground.contains("underline") { + modifiers |= Modifier::UNDERLINED; + } + let foreground = foreground.replace("underline ", ""); + // TODO: use bold, bright boolean flags + if foreground.contains("bold") { + modifiers |= Modifier::BOLD; + } + // let foreground = foreground.replace("bold ", ""); + if foreground.contains("inverse") { + modifiers |= Modifier::REVERSED; + } + let foreground = foreground.replace("inverse ", ""); + let mut style = Style::default(); + if let Some(fg) = get_color_foreground(foreground.as_str()) { + style = style.fg(fg); + } + if let Some(bg) = get_color_background(background.as_str()) { + style = style.bg(bg); + } + style = style.add_modifier(modifiers); + style +} + +fn get_color_foreground(s: &str) -> Option { + let s = s.trim_start(); + let s = s.trim_end(); + if s.contains("color") { + let c = s.trim_start_matches("color").parse::().unwrap_or_default(); + Some(Color::Indexed(c)) + } else if s.contains("gray") { + let c = 232 + s.trim_start_matches("gray").parse::().unwrap_or_default(); + Some(Color::Indexed(c)) + } else if s.contains("rgb") { + let red = (s.as_bytes()[3] as char).to_digit(10).unwrap_or_default() as u8; + let green = (s.as_bytes()[4] as char).to_digit(10).unwrap_or_default() as u8; + let blue = (s.as_bytes()[5] as char).to_digit(10).unwrap_or_default() as u8; + let c = 16 + red * 36 + green * 6 + blue; + Some(Color::Indexed(c)) + } else if s == "bold black" { + Some(Color::Indexed(8)) + } else if s == "bold red" { + Some(Color::Indexed(9)) + } else if s == "bold green" { + Some(Color::Indexed(10)) + } else if s == "bold yellow" { + Some(Color::Indexed(11)) + } else if s == "bold blue" { + Some(Color::Indexed(12)) + } else if s == "bold magenta" { + Some(Color::Indexed(13)) + } else if s == "bold cyan" { + Some(Color::Indexed(14)) + } else if s == "bold white" { + Some(Color::Indexed(15)) + } else if s == "black" { + Some(Color::Indexed(0)) + } else if s == "red" { + Some(Color::Indexed(1)) + } else if s == "green" { + Some(Color::Indexed(2)) + } else if s == "yellow" { + Some(Color::Indexed(3)) + } else if s == "blue" { + Some(Color::Indexed(4)) + } else if s == "magenta" { + Some(Color::Indexed(5)) + } else if s == "cyan" { + Some(Color::Indexed(6)) + } else if s == "white" { + Some(Color::Indexed(7)) + } 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_color_background(s: &str) -> Option { + let s = s.trim_start(); + let s = s.trim_end(); + if s.contains("bright color") { + let s = s.trim_start_matches("bright "); + let c = s.trim_start_matches("color").parse::().unwrap_or_default(); + Some(Color::Indexed(c.wrapping_shl(8))) + } else if s.contains("color") { + let c = s.trim_start_matches("color").parse::().unwrap_or_default(); + Some(Color::Indexed(c)) + } else if s.contains("gray") { + let s = s.trim_start_matches("bright "); + let c = 232 + s.trim_start_matches("gray").parse::().unwrap_or_default(); + Some(Color::Indexed(c.wrapping_shl(8))) + } else if s.contains("rgb") { + let s = s.trim_start_matches("bright "); + let red = (s.as_bytes()[3] as char).to_digit(10).unwrap_or_default() as u8; + let green = (s.as_bytes()[4] as char).to_digit(10).unwrap_or_default() as u8; + let blue = (s.as_bytes()[5] as char).to_digit(10).unwrap_or_default() as u8; + let c = 16 + red * 36 + green * 6 + blue; + Some(Color::Indexed(c.wrapping_shl(8))) + } else if s == "bright black" { + Some(Color::Indexed(8)) + } else if s == "bright red" { + Some(Color::Indexed(9)) + } else if s == "bright green" { + Some(Color::Indexed(10)) + } else if s == "bright yellow" { + Some(Color::Indexed(11)) + } else if s == "bright blue" { + Some(Color::Indexed(12)) + } else if s == "bright magenta" { + Some(Color::Indexed(13)) + } else if s == "bright cyan" { + Some(Color::Indexed(14)) + } else if s == "bright white" { + Some(Color::Indexed(15)) + } else if s == "black" { + Some(Color::Indexed(0)) + } else if s == "red" { + Some(Color::Indexed(1)) + } else if s == "green" { + Some(Color::Indexed(2)) + } else if s == "yellow" { + Some(Color::Indexed(3)) + } else if s == "blue" { + Some(Color::Indexed(4)) + } else if s == "magenta" { + Some(Color::Indexed(5)) + } else if s == "cyan" { + Some(Color::Indexed(6)) + } else if s == "white" { + Some(Color::Indexed(7)) + } else { + None + } +} + +impl Serialize for SerdeStyle { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Getting the foreground color string + let fg_str = color_to_string(self.0.fg.unwrap_or_default()); + + // Getting the background color string + let mut bg_str = color_to_string(self.0.bg.unwrap_or_default()); + + // If the background is not default, prepend with "on " + if bg_str != "" { + bg_str.insert_str(0, "on "); } + + // Building the modifier string + let mut mod_str = String::new(); + let mod_val = self.0.add_modifier; + if mod_val.contains(Modifier::BOLD) { + mod_str.push_str("bold "); + } + if mod_val.contains(Modifier::UNDERLINED) { + mod_str.push_str("underline "); + } + if mod_val.contains(Modifier::REVERSED) { + mod_str.push_str("inverse "); + } + + // Constructing the final style string + let style_str = format!("{}{} {}", mod_str, fg_str, bg_str).trim().to_string(); + + serializer.serialize_str(&style_str) } } -#[derive(Debug)] -pub struct Uda { - label: String, - kind: String, - values: Option>, - default: Option, - urgency: Option, +fn color_to_string(color: Color) -> String { + match color { + Color::Black => "black".to_string(), + Color::Red => "red".to_string(), + Color::Green => "green".to_string(), + Color::Reset => "reset".to_string(), + Color::Yellow => "yellow".to_string(), + Color::Blue => "blue".to_string(), + Color::Magenta => "magenta".to_string(), + Color::Cyan => "cyan".to_string(), + Color::Gray => "gray".to_string(), + Color::DarkGray => "darkgray".to_string(), + Color::LightRed => "lightred".to_string(), + Color::LightGreen => "lightgreen".to_string(), + Color::LightYellow => "lightyellow".to_string(), + Color::LightBlue => "lightblue".to_string(), + Color::LightMagenta => "lightmagenta".to_string(), + Color::LightCyan => "lightcyan".to_string(), + Color::White => "white".to_string(), + Color::Rgb(r, g, b) => format!("#{}{}{}", r, g, b), + Color::Indexed(u) => format!("#{}", u), + } } -#[derive(Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Config { + pub tick_rate: usize, + pub keymap: HashMap, pub enabled: bool, - pub color: HashMap, + pub color: HashMap, pub filter: String, pub data_location: String, pub obfuscate: bool, @@ -71,22 +293,22 @@ pub struct Config { 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_style_report_scrollbar: SerdeStyle, + pub uda_style_report_scrollbar_area: SerdeStyle, 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_style_context_active: SerdeStyle, + pub uda_style_report_selection: SerdeStyle, + pub uda_style_calendar_title: SerdeStyle, + pub uda_style_calendar_today: SerdeStyle, + pub uda_style_navbar: SerdeStyle, + pub uda_style_command: SerdeStyle, + pub uda_style_report_completion_pane: SerdeStyle, + pub uda_style_report_completion_pane_highlight: SerdeStyle, pub uda_shortcuts: Vec, pub uda_change_focus_rotate: bool, pub uda_background_process: String, @@ -97,84 +319,112 @@ pub struct Config { 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(); +impl Default for Config { + fn default() -> Self { + let tick_rate = 250; + + let mut task_report_keymap: KeyMap = Default::default(); + task_report_keymap.insert(parse_key_sequence("q").unwrap(), Action::Quit); + task_report_keymap.insert(parse_key_sequence("r").unwrap(), Action::Refresh); + task_report_keymap.insert(parse_key_sequence("G").unwrap(), Action::GotoBottom); + task_report_keymap.insert(parse_key_sequence("").unwrap(), Action::GotoTop); + task_report_keymap.insert(parse_key_sequence("").unwrap(), Action::GotoPageBottom); + task_report_keymap.insert(parse_key_sequence("").unwrap(), Action::GotoPageTop); + task_report_keymap.insert(parse_key_sequence("").unwrap(), Action::GotoBottom); + task_report_keymap.insert(parse_key_sequence("j").unwrap(), Action::Down); + task_report_keymap.insert(parse_key_sequence("k").unwrap(), Action::Up); + task_report_keymap.insert(parse_key_sequence("J").unwrap(), Action::PageDown); + task_report_keymap.insert(parse_key_sequence("K").unwrap(), Action::PageUp); + task_report_keymap.insert(parse_key_sequence("").unwrap(), Action::Delete); + task_report_keymap.insert(parse_key_sequence("").unwrap(), Action::Done); + task_report_keymap.insert(parse_key_sequence("s").unwrap(), Action::ToggleStartStop); + task_report_keymap.insert(parse_key_sequence("t").unwrap(), Action::QuickTag); + task_report_keymap.insert(parse_key_sequence("v").unwrap(), Action::Select); + task_report_keymap.insert(parse_key_sequence("V").unwrap(), Action::SelectAll); + task_report_keymap.insert(parse_key_sequence("u").unwrap(), Action::Undo); + task_report_keymap.insert(parse_key_sequence("e").unwrap(), Action::Edit); + task_report_keymap.insert(parse_key_sequence("m").unwrap(), Action::Modify); + task_report_keymap.insert(parse_key_sequence("!").unwrap(), Action::Shell); + task_report_keymap.insert(parse_key_sequence("l").unwrap(), Action::Log); + task_report_keymap.insert(parse_key_sequence("a").unwrap(), Action::Add); + task_report_keymap.insert(parse_key_sequence("A").unwrap(), Action::Annotate); + task_report_keymap.insert(parse_key_sequence("?").unwrap(), Action::Help); + task_report_keymap.insert(parse_key_sequence("/").unwrap(), Action::Filter); + task_report_keymap.insert(parse_key_sequence("z").unwrap(), Action::ToggleZoom); + task_report_keymap.insert(parse_key_sequence("c").unwrap(), Action::Context); + task_report_keymap.insert(parse_key_sequence("]").unwrap(), Action::Next); + task_report_keymap.insert(parse_key_sequence("[").unwrap(), Action::Previous); + task_report_keymap.insert(parse_key_sequence("1").unwrap(), Action::Shortcut(1)); + task_report_keymap.insert(parse_key_sequence("2").unwrap(), Action::Shortcut(2)); + task_report_keymap.insert(parse_key_sequence("3").unwrap(), Action::Shortcut(3)); + task_report_keymap.insert(parse_key_sequence("4").unwrap(), Action::Shortcut(4)); + task_report_keymap.insert(parse_key_sequence("5").unwrap(), Action::Shortcut(5)); + task_report_keymap.insert(parse_key_sequence("6").unwrap(), Action::Shortcut(6)); + task_report_keymap.insert(parse_key_sequence("7").unwrap(), Action::Shortcut(7)); + task_report_keymap.insert(parse_key_sequence("8").unwrap(), Action::Shortcut(8)); + task_report_keymap.insert(parse_key_sequence("9").unwrap(), Action::Shortcut(9)); + + let mut keymap: HashMap = Default::default(); + keymap.insert("task-report".into(), task_report_keymap); 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_undo = Self::get_uda_task_report_prompt_on_undo(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 { + let color = Default::default(); + let filter = Default::default(); + let data_location = Default::default(); + let obfuscate = false; + let print_empty_columns = false; + let due = 7; // due 7 days + let weekstart = true; // starts on monday + let rule_precedence_color = Default::default(); + let uda_priority_values = Default::default(); + let uda_tick_rate = 250; + let uda_auto_insert_double_quotes_on_add = true; + let uda_auto_insert_double_quotes_on_annotate = true; + let uda_auto_insert_double_quotes_on_log = true; + let uda_prefill_task_metadata = Default::default(); + let uda_reset_filter_on_esc = true; + let uda_task_detail_prefetch = 10; + let uda_task_report_use_all_tasks_for_completion = Default::default(); + let uda_task_report_show_info = true; + let uda_task_report_looping = true; + let uda_task_report_jump_to_task_on_add = true; + let uda_selection_indicator = "\u{2022} ".to_string(); + let uda_mark_indicator = "\u{2714} ".to_string(); + let uda_unmark_indicator = " ".to_string(); + let uda_scrollbar_indicator = "█".to_string(); + let uda_scrollbar_area = "║".to_string(); + let uda_style_report_scrollbar = Default::default(); + let uda_style_report_scrollbar_area = Default::default(); + let uda_selection_bold = true; + let uda_selection_italic = Default::default(); + let uda_selection_dim = Default::default(); + let uda_selection_blink = Default::default(); + let uda_selection_reverse = Default::default(); + let uda_calendar_months_per_row = 4; + let uda_style_context_active = Default::default(); + let uda_style_report_selection = Default::default(); + let uda_style_calendar_title = Default::default(); + let uda_style_calendar_today = Default::default(); + let uda_style_navbar = Default::default(); + let uda_style_command = Default::default(); + let uda_style_report_completion_pane = Default::default(); + let uda_style_report_completion_pane_highlight = Default::default(); + let uda_shortcuts = Default::default(); + let uda_change_focus_rotate = Default::default(); + let uda_background_process = Default::default(); + let uda_background_process_period = Default::default(); + let uda_quick_tag_name = Default::default(); + let uda_task_report_prompt_on_undo = Default::default(); + let uda_task_report_prompt_on_delete = Default::default(); + let uda_task_report_prompt_on_done = Default::default(); + let uda_task_report_date_time_vague_more_precise = Default::default(); + let uda_context_menu_select_on_move = Default::default(); + + Self { + tick_rate, + keymap, enabled, color, filter, @@ -186,7 +436,6 @@ impl Config { 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, @@ -202,23 +451,24 @@ impl Config { uda_unmark_indicator, uda_scrollbar_indicator, uda_scrollbar_area, + uda_style_report_scrollbar, + uda_style_report_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_report_selection, 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_change_focus_rotate, uda_background_process, uda_background_process_period, uda_quick_tag_name, @@ -227,498 +477,24 @@ impl Config { 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