diff --git a/Cargo.lock b/Cargo.lock index 4372f83..7183a4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,152 +38,6 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dabe5a181f83789739c194cbe5a897dde195078fac08568d09221fd6137a7ba8" -[[package]] -name = "async-attributes" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "async-channel" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" -dependencies = [ - "concurrent-queue", - "event-listener", - "futures-core", -] - -[[package]] -name = "async-executor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "once_cell", - "slab", -] - -[[package]] -name = "async-global-executor" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9586ec52317f36de58453159d48351bc244bc24ced3effc1fce22f3d48664af6" -dependencies = [ - "async-channel", - "async-executor", - "async-io", - "async-mutex", - "blocking", - "futures-lite", - "num_cpus", - "once_cell", -] - -[[package]] -name = "async-io" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b" -dependencies = [ - "concurrent-queue", - "futures-lite", - "libc", - "log", - "once_cell", - "parking", - "polling", - "slab", - "socket2", - "waker-fn", - "winapi", -] - -[[package]] -name = "async-lock" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6a8ea61bf9947a1007c5cada31e647dbc77b103c679858150003ba697ea798b" -dependencies = [ - "event-listener", -] - -[[package]] -name = "async-mutex" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" -dependencies = [ - "event-listener", -] - -[[package]] -name = "async-process" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83137067e3a2a6a06d67168e49e68a0957d215410473a740cea95a2425c0b7c6" -dependencies = [ - "async-io", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "libc", - "once_cell", - "signal-hook", - "winapi", -] - -[[package]] -name = "async-std" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8056f1455169ab86dd47b47391e4ab0cbd25410a70e9fe675544f49bafaf952" -dependencies = [ - "async-attributes", - "async-channel", - "async-global-executor", - "async-io", - "async-lock", - "async-process", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "num_cpus", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] - -[[package]] -name = "async-task" -version = "4.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91831deabf0d6d7ec49552e489aed63b7456a7a3c46cff62adad428110b0af0" - -[[package]] -name = "atomic-waker" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" - [[package]] name = "atty" version = "0.2.14" @@ -233,30 +87,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] -name = "blocking" +name = "bytes" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046e47d4b2d391b1f6f8b407b1deb8dee56c1852ccd868becf2710f601b5f427" -dependencies = [ - "async-channel", - "async-task", - "atomic-waker", - "fastrand", - "futures-lite", - "once_cell", -] - -[[package]] -name = "bumpalo" -version = "3.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" - -[[package]] -name = "cache-padded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cassowary" @@ -339,15 +173,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "concurrent-queue" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" -dependencies = [ - "cache-padded", -] - [[package]] name = "console" version = "0.15.0" @@ -361,16 +186,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "crossbeam-utils" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" -dependencies = [ - "cfg-if", - "lazy_static", -] - [[package]] name = "crossterm" version = "0.22.1" @@ -380,7 +195,7 @@ dependencies = [ "bitflags", "crossterm_winapi", "libc", - "mio", + "mio 0.7.14", "parking_lot 0.11.2", "signal-hook", "signal-hook-mio", @@ -389,15 +204,15 @@ dependencies = [ [[package]] name = "crossterm" -version = "0.23.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fd7173631a4e9e2ca8b32ae2fad58aab9843ea5aaf56642661937d87e28a3e" +checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17" dependencies = [ "bitflags", "crossterm_winapi", "futures-core", "libc", - "mio", + "mio 0.8.2", "parking_lot 0.12.0", "signal-hook", "signal-hook-mio", @@ -413,16 +228,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "ctor" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa" -dependencies = [ - "quote", - "syn", -] - [[package]] name = "darling" version = "0.10.2" @@ -563,12 +368,6 @@ dependencies = [ "str-buf", ] -[[package]] -name = "event-listener" -version = "2.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" - [[package]] name = "failure" version = "0.1.8" @@ -591,15 +390,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "fastrand" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "779d043b6a0b90cc4c0ed7ee380a6504394cee7efd7db050e3774eee387324b2" -dependencies = [ - "instant", -] - [[package]] name = "fd-lock" version = "3.0.2" @@ -665,21 +455,6 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" -[[package]] -name = "futures-lite" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - [[package]] name = "futures-macro" version = "0.3.21" @@ -703,12 +478,6 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" -[[package]] -name = "futures-timer" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" - [[package]] name = "futures-util" version = "0.3.21" @@ -735,7 +504,7 @@ checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.10.0+wasi-snapshot-preview1", ] [[package]] @@ -744,19 +513,6 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" -[[package]] -name = "gloo-timers" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f16c88aa13d2656ef20d1c042086b8767bbe2bdb62526894275a1b062161b2e" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "hashbrown" version = "0.11.2" @@ -824,24 +580,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" -[[package]] -name = "js-sys" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "kv-log-macro" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -dependencies = [ - "log", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -850,9 +588,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.112" +version = "0.2.124" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" +checksum = "21a41fed9d98f27ab1c6d161da622a4fa35e8a54a8adc24bbf3ddd0ef70b0e50" [[package]] name = "linked-hash-map" @@ -877,7 +615,6 @@ checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ "cfg-if", "serde", - "value-bag", ] [[package]] @@ -957,6 +694,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "mio" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "wasi 0.11.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "miow" version = "0.3.7" @@ -1070,12 +821,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "parking" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" - [[package]] name = "parking_lot" version = "0.11.2" @@ -1142,19 +887,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "polling" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" -dependencies = [ - "cfg-if", - "libc", - "log", - "wepoll-ffi", - "winapi", -] - [[package]] name = "ppv-lite86" version = "0.2.16" @@ -1407,12 +1139,13 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" dependencies = [ "libc", - "mio", + "mio 0.7.14", + "mio 0.8.2", "signal-hook", ] @@ -1439,9 +1172,9 @@ checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" [[package]] name = "socket2" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" dependencies = [ "libc", "winapi", @@ -1508,16 +1241,14 @@ name = "taskwarrior-tui" version = "0.22.0" dependencies = [ "anyhow", - "async-std", "better-panic", "cassowary", "chrono", "clap", "clap_complete", - "crossterm 0.23.1", + "crossterm 0.23.2", "dirs", "futures", - "futures-timer", "itertools", "lazy_static", "log", @@ -1531,6 +1262,8 @@ dependencies = [ "shellexpand", "shlex", "task-hookrs", + "tokio", + "tokio-stream", "tui", "unicode-segmentation", "unicode-truncate", @@ -1602,10 +1335,52 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", - "wasi", + "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] +[[package]] +name = "tokio" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +dependencies = [ + "bytes", + "libc", + "memchr", + "mio 0.8.2", + "num_cpus", + "once_cell", + "parking_lot 0.12.0", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "traitobject" version = "0.1.0" @@ -1686,16 +1461,6 @@ dependencies = [ "serde", ] -[[package]] -name = "value-bag" -version = "1.0.0-alpha.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79923f7731dc61ebfba3633098bf3ac533bbd35ccd8c57e7088d9a5eebe0263f" -dependencies = [ - "ctor", - "version_check", -] - [[package]] name = "version_check" version = "0.9.4" @@ -1712,12 +1477,6 @@ dependencies = [ "nom", ] -[[package]] -name = "waker-fn" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" - [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -1725,89 +1484,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] -name = "wasm-bindgen" -version = "0.2.78" +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.78" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" -dependencies = [ - "bumpalo", - "lazy_static", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.78" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.78" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.78" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" - -[[package]] -name = "web-sys" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wepoll-ffi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" -dependencies = [ - "cc", -] +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "winapi" diff --git a/Cargo.toml b/Cargo.toml index 1c071ef..9618fd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,15 +19,13 @@ crossterm-backend = ["tui/crossterm", "crossterm"] [dependencies] anyhow = "1.0.56" -async-std = { version = "1.10.0", features = ["attributes", "unstable"] } better-panic = "0.3.0" cassowary = "0.3.0" chrono = "0.4.19" clap = { version = "3.1.6", features = ["derive"] } -crossterm = { version = "0.23.1", optional = true, default-features = false, features = ["event-stream"] } +crossterm = { version = "0.23.2", optional = true, default-features = false, features = ["event-stream"] } dirs = "4.0.0" futures = "0.3.21" -futures-timer = "3.0.2" itertools = "0.10.3" lazy_static = "1.4.0" log = "0.4.14" @@ -41,6 +39,8 @@ serde_json = "1.0.79" shellexpand = "2.1.0" shlex = "1.1.0" task-hookrs = { git = "https://github.com/kdheepak/task-hookrs" } +tokio = { version = "1.17.0", features = ["full"] } +tokio-stream = "0.1.3" tui = { version = "0.17.0", optional = true, default-features = false } unicode-segmentation = "1.9.0" unicode-truncate = "0.2.0" diff --git a/src/app.rs b/src/app.rs index fecc839..04bcc7d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,13 +2,13 @@ use crate::calendar::Calendar; use crate::completion::{get_start_word_under_cursor, CompletionList}; use crate::config; use crate::config::Config; -use crate::event::Key; -use crate::event::{Event, Events}; +use crate::event::Event; use crate::help::Help; use crate::keyconfig::KeyConfig; use crate::scrollbar::Scrollbar; use crate::table::{Row, Table, TableMode, TableState}; use crate::task_report::TaskReportTable; +use crate::ui; use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; @@ -68,10 +68,18 @@ use regex::Regex; use lazy_static::lazy_static; use crate::action::Action; +use crate::event::KeyCode; use crate::pane::context::{ContextDetails, ContextsState}; use crate::pane::project::ProjectsState; use crate::pane::Pane; + use crossterm::style::style; +use crossterm::{ + event::{DisableMouseCapture, EnableMouseCapture}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; + use futures::SinkExt; use std::borrow::Borrow; use std::time::Instant; @@ -226,10 +234,12 @@ pub struct TaskwarriorTui { pub contexts: ContextsState, pub task_version: Versioning, pub error: Option, + pub event_loop: crate::event::EventLoop, + pub requires_redraw: bool, } impl TaskwarriorTui { - pub fn new(report: &str) -> Result { + pub async fn new(report: &str) -> Result { let output = std::process::Command::new("task") .arg("rc.color=off") .arg("rc._forcecolor=off") @@ -266,6 +276,13 @@ impl TaskwarriorTui { let (w, h) = crossterm::terminal::size()?; + 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); + let mut app = Self { should_quit: false, dirty: true, @@ -305,6 +322,8 @@ impl TaskwarriorTui { contexts: ContextsState::new(), task_version, error: None, + event_loop, + requires_redraw: false, }; for c in app.config.filter.chars() { @@ -313,7 +332,7 @@ impl TaskwarriorTui { app.task_report_table.date_time_vague_precise = app.config.uda_task_report_date_time_vague_more_precise; - app.update(true)?; + app.update(true).await?; app.filter_history.load()?; app.filter_history.add(app.filter.as_str()); @@ -331,6 +350,84 @@ impl TaskwarriorTui { Ok(app) } + pub async 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(())?; + self.event_loop.handle.abort(); + 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); + Ok(()) + } + + pub async fn pause_tui(&mut self) -> Result<()> { + self.abort_event_loop().await?; + let backend = CrosstermBackend::new(io::stdout()); + let mut terminal = Terminal::new(backend)?; + disable_raw_mode()?; + execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)?; + terminal.show_cursor()?; + Ok(()) + } + + pub async fn next(&mut self) -> Option> { + self.event_loop.rx.recv().await + } + + pub async fn run(&mut self, terminal: &mut Terminal) -> Result<()> { + loop { + if self.requires_redraw { + terminal.resize(terminal.size()?)?; + self.requires_redraw = false; + } + terminal.draw(|f| self.draw(f))?; + // Handle input + if let Some(event) = self.next().await { + match event { + Event::Input(input) => { + self.handle_input(input).await?; + } + Event::Tick => { + trace!("Tick event"); + self.update(false).await?; + } + } + } + + if self.should_quit { + break; + } + } + Ok(()) + } + pub fn reset_command(&mut self) { self.command.update("", 0) } @@ -360,14 +457,6 @@ impl TaskwarriorTui { Ok(()) } - pub fn render(&mut self, terminal: &mut Terminal) -> Result<()> - where - B: Backend, - { - terminal.draw(|f| self.draw(f))?; - Ok(()) - } - pub fn draw(&mut self, f: &mut Frame) { let rect = f.size(); self.terminal_width = rect.width; @@ -397,7 +486,7 @@ impl TaskwarriorTui { Mode::Projects => 1, Mode::Calendar => 2, }; - let tabs_block = Block::default().style(Style::default().add_modifier(Modifier::REVERSED)); + let navbar_block = Block::default().style(self.config.uda_style_navbar); let context = Spans::from(vec![ Span::from("["), Span::from(if self.current_context.is_empty() { @@ -408,7 +497,7 @@ impl TaskwarriorTui { Span::from("]"), ]); let tabs = Tabs::new(tab_names) - .block(tabs_block.clone()) + .block(navbar_block.clone()) .select(selected_tab) .divider(" ") .highlight_style(Style::default().add_modifier(Modifier::BOLD)); @@ -418,7 +507,7 @@ impl TaskwarriorTui { .split(layout); f.render_widget(tabs, rects[0]); - f.render_widget(Paragraph::new(Text::from(context)).block(tabs_block), rects[1]); + f.render_widget(Paragraph::new(Text::from(context)).block(navbar_block), rects[1]); } pub fn draw_debug(&mut self, f: &mut Frame) { @@ -519,7 +608,7 @@ impl TaskwarriorTui { ) { match action { Action::Error => { - Self::draw_command( + self.draw_command( f, rects[1], "Press any key to continue.", @@ -555,7 +644,7 @@ impl TaskwarriorTui { self.previous_mode = None; self.error = None; let position = Self::get_position(&self.command); - Self::draw_command( + self.draw_command( f, rects[1], self.filter.as_str(), @@ -567,7 +656,7 @@ impl TaskwarriorTui { } Action::Jump => { let position = Self::get_position(&self.command); - Self::draw_command( + self.draw_command( f, rects[1], self.command.as_str(), @@ -585,7 +674,7 @@ impl TaskwarriorTui { if self.show_completion_pane { self.draw_completion_pop_up(f, rects[1], position); } - Self::draw_command( + self.draw_command( f, rects[1], self.filter.as_str(), @@ -608,7 +697,7 @@ impl TaskwarriorTui { if self.show_completion_pane { self.draw_completion_pop_up(f, rects[1], position); } - Self::draw_command( + self.draw_command( f, rects[1], self.command.as_str(), @@ -625,7 +714,7 @@ impl TaskwarriorTui { } Action::Subprocess => { let position = Self::get_position(&self.command); - Self::draw_command( + self.draw_command( f, rects[1], self.command.as_str(), @@ -648,7 +737,7 @@ impl TaskwarriorTui { } else { format!("Modify Task {}", task_ids.join(",")) }; - Self::draw_command( + self.draw_command( f, rects[1], self.modify.as_str(), @@ -676,7 +765,7 @@ impl TaskwarriorTui { } else { format!("Annotate Task {}", task_ids.join(",")) }; - Self::draw_command( + self.draw_command( f, rects[1], self.command.as_str(), @@ -699,7 +788,7 @@ impl TaskwarriorTui { if self.show_completion_pane { self.draw_completion_pop_up(f, rects[1], position); } - Self::draw_command( + self.draw_command( f, rects[1], self.command.as_str(), @@ -715,7 +804,7 @@ impl TaskwarriorTui { ); } Action::HelpPopup => { - Self::draw_command( + self.draw_command( f, rects[1], self.filter.as_str(), @@ -727,7 +816,7 @@ impl TaskwarriorTui { self.draw_help_popup(f, 80, 90); } Action::ContextMenu => { - Self::draw_command( + self.draw_command( f, rects[1], self.filter.as_str(), @@ -745,14 +834,14 @@ impl TaskwarriorTui { format!("Done Task {}?", task_ids.join(",")) }; let x = match self.keyconfig.done { - Key::Char(c) => c.to_string(), + KeyCode::Char(c) => c.to_string(), _ => "Enter".to_string(), }; let q = match self.keyconfig.quit { - Key::Char(c) => c.to_string(), + KeyCode::Char(c) => c.to_string(), _ => "Esc".to_string(), }; - Self::draw_command( + self.draw_command( f, rects[1], &format!("Press <{}> to confirm or <{}> to abort.", x, q), @@ -769,14 +858,14 @@ impl TaskwarriorTui { format!("Delete Task {}?", task_ids.join(",")) }; let x = match self.keyconfig.delete { - Key::Char(c) => c.to_string(), + KeyCode::Char(c) => c.to_string(), _ => "Enter".to_string(), }; let q = match self.keyconfig.quit { - Key::Char(c) => c.to_string(), + KeyCode::Char(c) => c.to_string(), _ => "Esc".to_string(), }; - Self::draw_command( + self.draw_command( f, rects[1], &format!("Press <{}> to confirm or <{}> to abort.", x, q), @@ -945,6 +1034,7 @@ impl TaskwarriorTui { } fn draw_command( + &self, f: &mut Frame, rect: Rect, text: &str, @@ -966,14 +1056,16 @@ impl TaskwarriorTui { .split(rect); // render command title - let fg_color = if error.is_some() { Color::Red } else { Color::Reset }; + let mut style = self.config.uda_style_command; + if error.is_some() { + style = style.fg(Color::Red); + }; let title_spans = if let Some(subtitle) = title.1 { Spans::from(vec![title.0, Span::from(" ["), subtitle, Span::from("]")]) } else { Spans::from(vec![title.0]) }; - let title = Paragraph::new(Text::from(title_spans)) - .style(Style::default().fg(fg_color).add_modifier(Modifier::REVERSED)); + let title = Paragraph::new(Text::from(title_spans)).style(style); f.render_widget(title, rects[0]); // render command @@ -1238,7 +1330,7 @@ impl TaskwarriorTui { (tasks, headers) } - pub fn update(&mut self, force: bool) -> Result<()> { + 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()?; @@ -1265,7 +1357,7 @@ impl TaskwarriorTui { self.cursor_fix(); self.update_task_table_state(); if self.task_report_show_info { - self.update_task_details()?; + self.update_task_details().await?; } self.selection_fix(); @@ -1304,7 +1396,7 @@ impl TaskwarriorTui { } } - pub fn update_task_details(&mut self) -> Result<()> { + pub async fn update_task_details(&mut self) -> Result<()> { if self.tasks.is_empty() { return Ok(()); } @@ -1335,40 +1427,38 @@ impl TaskwarriorTui { l.dedup(); - let mut outputs = vec![]; - for s in &l { - if self.tasks.is_empty() { - return Ok(()); - } - if s >= &self.tasks.len() { - break; - } - let task_uuid = *self.tasks[*s].uuid(); - if !self.task_details.contains_key(&task_uuid) || task_uuid == current_task_uuid { - let (tx, rx) = mpsc::channel(); - let defaultwidth = self.terminal_width.saturating_sub(2); - std::thread::spawn(move || { - let output = Command::new("task") + let (tx, mut rx) = tokio::sync::mpsc::channel(100); + let tasks = self.tasks.clone(); + let mut task_details = self.task_details.clone(); + let defaultwidth = self.terminal_width.saturating_sub(2); + tokio::spawn(async move { + for s in &l { + if tasks.is_empty() { + return Ok(()); + } + if s >= &tasks.len() { + break; + } + let task_uuid = *tasks[*s].uuid(); + if !task_details.contains_key(&task_uuid) || task_uuid == current_task_uuid { + debug!("Running task details for {}", task_uuid); + let output = tokio::process::Command::new("task") .arg("rc.color=off") .arg("rc._forcecolor=off") .arg(format!("rc.defaultwidth={}", defaultwidth)) .arg(format!("{}", task_uuid)) - .output(); + .output() + .await; if let Ok(output) = output { let data = String::from_utf8_lossy(&output.stdout).to_string(); - tx.send(Some((task_uuid, data))).unwrap(); - } else { - tx.send(None).unwrap(); + task_details.insert(task_uuid, data); } - }); - outputs.push(rx); - } - } - - while !outputs.is_empty() { - if let Ok(Some((task_uuid, data))) = outputs.pop().unwrap().recv() { - self.task_details.insert(task_uuid, data); + } } + tx.send(task_details).await + }); + while let Some(td) = rx.recv().await { + self.task_details = td; } Ok(()) } @@ -1808,7 +1898,9 @@ impl TaskwarriorTui { }); } - pub fn task_shortcut(&mut self, s: usize) -> Result<(), String> { + pub async fn task_shortcut(&mut self, s: usize) -> Result<(), String> { + self.pause_tui().await.unwrap(); + let task_uuids = if self.tasks.is_empty() { vec![] } else { @@ -1820,7 +1912,6 @@ impl TaskwarriorTui { if shell.is_empty() { return Err("Trying to run empty shortcut.".to_string()); } - let shell = format!( "{} {}", shell, @@ -1830,6 +1921,7 @@ impl TaskwarriorTui { .collect::>() .join(" ") ); + let shell = shellexpand::tilde(&shell).into_owned(); let r = match shlex::split(&shell) { Some(cmd) => { @@ -1871,6 +1963,7 @@ impl TaskwarriorTui { } } + self.resume_tui().await.unwrap(); r } @@ -2206,13 +2299,17 @@ impl TaskwarriorTui { } } - pub fn task_edit(&mut self) -> Result<(), String> { + pub async fn task_edit(&mut self) -> Result<(), String> { if self.tasks.is_empty() { return Ok(()); } + + self.pause_tui().await.unwrap(); + let selected = self.current_selection; let task_id = self.tasks[selected].id().unwrap_or_default(); let task_uuid = *self.tasks[selected].uuid(); + let r = Command::new("task").arg(format!("{}", task_uuid)).arg("edit").spawn(); let r = match r { @@ -2242,6 +2339,8 @@ impl TaskwarriorTui { self.current_selection_uuid = Some(task_uuid); + self.resume_tui().await.unwrap(); + r } @@ -2412,32 +2511,33 @@ impl TaskwarriorTui { es } - pub fn handle_input(&mut self, input: Key) -> Result<()> { + pub async fn handle_input(&mut self, input: KeyCode) -> Result<()> { match self.mode { Mode::Tasks(_) => { - self.handle_input_by_task_mode(input)?; + self.handle_input_by_task_mode(input).await?; } Mode::Projects => { ProjectsState::handle_input(self, input)?; + self.update(false).await?; } Mode::Calendar => { - if input == self.keyconfig.quit || input == Key::Ctrl('c') { + if input == self.keyconfig.quit || input == KeyCode::Ctrl('c') { self.should_quit = true; } else if input == self.keyconfig.previous_tab { self.mode = Mode::Projects; - } else if input == Key::Up || input == self.keyconfig.up { + } else if input == KeyCode::Up || input == self.keyconfig.up { if self.calendar_year > 0 { self.calendar_year -= 1; } - } else if input == Key::Down || input == self.keyconfig.down { + } else if input == KeyCode::Down || input == self.keyconfig.down { self.calendar_year += 1; - } else if input == Key::PageUp || input == self.keyconfig.page_up { + } else if input == KeyCode::PageUp || input == self.keyconfig.page_up { self.task_report_previous_page(); - } else if input == Key::PageDown || input == self.keyconfig.page_down { + } else if input == KeyCode::PageDown || input == self.keyconfig.page_down { self.calendar_year += 10; - } else if input == Key::Ctrl('e') { + } else if input == KeyCode::Ctrl('e') { self.task_details_scroll_down(); - } else if input == Key::Ctrl('y') { + } 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 { @@ -2447,7 +2547,7 @@ impl TaskwarriorTui { } } else { match self.task_done() { - Ok(_) => self.update(true)?, + Ok(_) => self.update(true).await?, Err(e) => { self.error = Some(e); } @@ -2463,13 +2563,13 @@ impl TaskwarriorTui { Ok(()) } - fn handle_input_by_task_mode(&mut self, input: Key) -> Result<()> { + 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 == Key::Esc { + if input == KeyCode::Esc { self.marked.clear(); - } else if input == self.keyconfig.quit || input == Key::Ctrl('c') { + } 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(); @@ -2478,22 +2578,22 @@ impl TaskwarriorTui { self.task_table_state.multiple_selection(); self.toggle_mark_all(); } else if input == self.keyconfig.refresh { - self.update(true)?; - } else if input == self.keyconfig.go_to_bottom || input == Key::End { + 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 == Key::Home { + } else if input == self.keyconfig.go_to_top || input == KeyCode::Home { self.task_report_top(); - } else if input == Key::Down || input == self.keyconfig.down { + } else if input == KeyCode::Down || input == self.keyconfig.down { self.task_report_next(); - } else if input == Key::Up || input == self.keyconfig.up { + } else if input == KeyCode::Up || input == self.keyconfig.up { self.task_report_previous(); - } else if input == Key::PageDown || input == self.keyconfig.page_down { + } else if input == KeyCode::PageDown || input == self.keyconfig.page_down { self.task_report_next_page(); - } else if input == Key::PageUp || input == self.keyconfig.page_up { + } else if input == KeyCode::PageUp || input == self.keyconfig.page_up { self.task_report_previous_page(); - } else if input == Key::Ctrl('e') { + } else if input == KeyCode::Ctrl('e') { self.task_details_scroll_down(); - } else if input == Key::Ctrl('y') { + } 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 { @@ -2503,7 +2603,7 @@ impl TaskwarriorTui { } } else { match self.task_done() { - Ok(_) => self.update(true)?, + Ok(_) => self.update(true).await?, Err(e) => { self.error = Some(e); } @@ -2517,7 +2617,7 @@ impl TaskwarriorTui { } } else { match self.task_delete() { - Ok(_) => self.update(true)?, + Ok(_) => self.update(true).await?, Err(e) => { self.error = Some(e); } @@ -2525,28 +2625,28 @@ impl TaskwarriorTui { } } else if input == self.keyconfig.start_stop { match self.task_start_stop() { - Ok(_) => self.update(true)?, + Ok(_) => self.update(true).await?, Err(e) => { self.error = Some(e); } } } else if input == self.keyconfig.quick_tag { match self.task_quick_tag() { - Ok(_) => self.update(true)?, + Ok(_) => self.update(true).await?, Err(e) => { self.error = Some(e); } } } else if input == self.keyconfig.edit { - match self.task_edit() { - Ok(_) => self.update(true)?, + match self.task_edit().await { + Ok(_) => self.update(true).await?, Err(e) => { self.error = Some(e); } } } else if input == self.keyconfig.undo { match self.task_undo() { - Ok(_) => self.update(true)?, + Ok(_) => self.update(true).await?, Err(e) => { self.error = Some(e); } @@ -2645,77 +2745,77 @@ impl TaskwarriorTui { self.filter_history.history_len() )); self.update_completion_list(); - } else if input == Key::Char(':') { + } else if input == KeyCode::Char(':') { self.mode = Mode::Tasks(Action::Jump); } else if input == self.keyconfig.shortcut1 { - match self.task_shortcut(1) { - Ok(_) => self.update(true)?, + match self.task_shortcut(1).await { + Ok(_) => self.update(true).await?, Err(e) => { - self.update(true)?; + self.update(true).await?; self.error = Some(e); } } } else if input == self.keyconfig.shortcut2 { - match self.task_shortcut(2) { - Ok(_) => self.update(true)?, + match self.task_shortcut(2).await { + Ok(_) => self.update(true).await?, Err(e) => { - self.update(true)?; + self.update(true).await?; self.error = Some(e); } } } else if input == self.keyconfig.shortcut3 { - match self.task_shortcut(3) { - Ok(_) => self.update(true)?, + match self.task_shortcut(3).await { + Ok(_) => self.update(true).await?, Err(e) => { - self.update(true)?; + self.update(true).await?; self.error = Some(e); } } } else if input == self.keyconfig.shortcut4 { - match self.task_shortcut(4) { - Ok(_) => self.update(true)?, + match self.task_shortcut(4).await { + Ok(_) => self.update(true).await?, Err(e) => { - self.update(true)?; + self.update(true).await?; self.error = Some(e); } } } else if input == self.keyconfig.shortcut5 { - match self.task_shortcut(5) { - Ok(_) => self.update(true)?, + match self.task_shortcut(5).await { + Ok(_) => self.update(true).await?, Err(e) => { - self.update(true)?; + self.update(true).await?; self.error = Some(e); } } } else if input == self.keyconfig.shortcut6 { - match self.task_shortcut(6) { - Ok(_) => self.update(true)?, + match self.task_shortcut(6).await { + Ok(_) => self.update(true).await?, Err(e) => { - self.update(true)?; + self.update(true).await?; self.error = Some(e); } } } else if input == self.keyconfig.shortcut7 { - match self.task_shortcut(7) { - Ok(_) => self.update(true)?, + match self.task_shortcut(7).await { + Ok(_) => self.update(true).await?, Err(e) => { - self.update(true)?; + self.update(true).await?; self.error = Some(e); } } } else if input == self.keyconfig.shortcut8 { - match self.task_shortcut(8) { - Ok(_) => self.update(true)?, + match self.task_shortcut(8).await { + Ok(_) => self.update(true).await?, Err(e) => { - self.update(true)?; + self.update(true).await?; self.error = Some(e); } } } else if input == self.keyconfig.shortcut9 { - match self.task_shortcut(9) { - Ok(_) => self.update(true)?, + match self.task_shortcut(9).await { + Ok(_) => self.update(true).await?, Err(e) => { - self.update(true)?; + self.update(true).await?; self.error = Some(e); } } @@ -2728,9 +2828,9 @@ impl TaskwarriorTui { } } Action::ContextMenu => { - if input == self.keyconfig.quit || input == Key::Esc { + if input == self.keyconfig.quit || input == KeyCode::Esc { self.mode = Mode::Tasks(Action::Report); - } else if input == Key::Down || input == self.keyconfig.down { + } 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() { @@ -2738,14 +2838,14 @@ impl TaskwarriorTui { self.mode = Mode::Tasks(Action::Error); } else { match self.context_select() { - Ok(_) => self.update(true)?, + Ok(_) => self.update(true).await?, Err(e) => { self.error = Some(e.to_string()); } } } } - } else if input == Key::Up || input == self.keyconfig.up { + } 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() { @@ -2753,14 +2853,14 @@ impl TaskwarriorTui { self.mode = Mode::Tasks(Action::Error); } else { match self.context_select() { - Ok(_) => self.update(true)?, + Ok(_) => self.update(true).await?, Err(e) => { self.error = Some(e.to_string()); } } } } - } else if input == Key::Char('\n') { + } else if input == KeyCode::Char('\n') { if self.error.is_some() { self.previous_mode = Some(self.mode.clone()); self.mode = Mode::Tasks(Action::Error); @@ -2768,7 +2868,7 @@ impl TaskwarriorTui { self.mode = Mode::Tasks(Action::Report); } else { match self.context_select() { - Ok(_) => self.update(true)?, + Ok(_) => self.update(true).await?, Err(e) => { self.error = Some(e.to_string()); } @@ -2777,7 +2877,7 @@ impl TaskwarriorTui { } } Action::HelpPopup => { - if input == self.keyconfig.quit || input == Key::Esc { + 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); @@ -2790,7 +2890,7 @@ impl TaskwarriorTui { } } Action::Modify => match input { - Key::Esc => { + KeyCode::Esc => { if self.show_completion_pane { self.show_completion_pane = false; self.completion_list.unselect(); @@ -2799,7 +2899,7 @@ impl TaskwarriorTui { self.mode = Mode::Tasks(Action::Report); } } - Key::Char('\n') => { + KeyCode::Char('\n') => { if self.show_completion_pane { self.show_completion_pane = false; if let Some((i, (r, m, o, _, _))) = self.completion_list.selected() { @@ -2817,7 +2917,7 @@ impl TaskwarriorTui { self.mode = Mode::Tasks(Action::Report); self.command_history.add(self.modify.as_str()); self.modify.update("", 0); - self.update(true)?; + self.update(true).await?; } Err(e) => { self.error = Some(e); @@ -2825,7 +2925,7 @@ impl TaskwarriorTui { } } } - Key::Tab | Key::Ctrl('n') => { + KeyCode::Tab | KeyCode::Ctrl('n') => { if !self.completion_list.is_empty() { self.update_input_for_completion(); if !self.show_completion_pane { @@ -2834,13 +2934,13 @@ impl TaskwarriorTui { self.completion_list.next(); } } - Key::BackTab | Key::Ctrl('p') => { + KeyCode::BackTab | KeyCode::Ctrl('p') => { if self.show_completion_pane && !self.completion_list.is_empty() { self.completion_list.previous(); } } - Key::Up => { + KeyCode::Up => { if self.show_completion_pane && !self.completion_list.is_empty() { self.completion_list.previous(); } else if let Some(s) = self @@ -2860,7 +2960,7 @@ impl TaskwarriorTui { )); } } - Key::Down => { + KeyCode::Down => { if self.show_completion_pane && !self.completion_list.is_empty() { self.completion_list.next(); } else if let Some(s) = self @@ -2887,7 +2987,7 @@ impl TaskwarriorTui { } }, Action::Subprocess => match input { - Key::Char('\n') => { + KeyCode::Char('\n') => { if self.error.is_some() { self.previous_mode = Some(self.mode.clone()); self.mode = Mode::Tasks(Action::Error); @@ -2896,7 +2996,7 @@ impl TaskwarriorTui { Ok(_) => { self.mode = Mode::Tasks(Action::Report); self.reset_command(); - self.update(true)?; + self.update(true).await?; } Err(e) => { self.error = Some(e); @@ -2904,14 +3004,14 @@ impl TaskwarriorTui { } } } - Key::Esc => { + KeyCode::Esc => { self.reset_command(); self.mode = Mode::Tasks(Action::Report); } _ => handle_movement(&mut self.command, input), }, Action::Log => match input { - Key::Esc => { + KeyCode::Esc => { if self.show_completion_pane { self.show_completion_pane = false; self.completion_list.unselect(); @@ -2921,7 +3021,7 @@ impl TaskwarriorTui { self.mode = Mode::Tasks(Action::Report); } } - Key::Char('\n') => { + KeyCode::Char('\n') => { if self.show_completion_pane { self.show_completion_pane = false; if let Some((i, (r, m, o, _, _))) = self.completion_list.selected() { @@ -2940,7 +3040,7 @@ impl TaskwarriorTui { self.command_history.add(self.command.as_str()); self.reset_command(); self.history_status = None; - self.update(true)?; + self.update(true).await?; } Err(e) => { self.error = Some(e); @@ -2948,7 +3048,7 @@ impl TaskwarriorTui { } } } - Key::Tab | Key::Ctrl('n') => { + KeyCode::Tab | KeyCode::Ctrl('n') => { if !self.completion_list.is_empty() { self.update_input_for_completion(); if !self.show_completion_pane { @@ -2957,13 +3057,13 @@ impl TaskwarriorTui { self.completion_list.next(); } } - Key::BackTab | Key::Ctrl('p') => { + KeyCode::BackTab | KeyCode::Ctrl('p') => { if self.show_completion_pane && !self.completion_list.is_empty() { self.completion_list.previous(); } } - Key::Up => { + KeyCode::Up => { if self.show_completion_pane && !self.completion_list.is_empty() { self.completion_list.previous(); } else if let Some(s) = self @@ -2983,7 +3083,7 @@ impl TaskwarriorTui { )); } } - Key::Down => { + KeyCode::Down => { if self.show_completion_pane && !self.completion_list.is_empty() { self.completion_list.next(); } else if let Some(s) = self @@ -3010,7 +3110,7 @@ impl TaskwarriorTui { } }, Action::Annotate => match input { - Key::Esc => { + KeyCode::Esc => { if self.show_completion_pane { self.show_completion_pane = false; self.completion_list.unselect(); @@ -3020,7 +3120,7 @@ impl TaskwarriorTui { self.history_status = None; } } - Key::Char('\n') => { + KeyCode::Char('\n') => { if self.show_completion_pane { self.show_completion_pane = false; if let Some((i, (r, m, o, _, _))) = self.completion_list.selected() { @@ -3039,7 +3139,7 @@ impl TaskwarriorTui { self.command_history.add(self.command.as_str()); self.reset_command(); self.history_status = None; - self.update(true)?; + self.update(true).await?; } Err(e) => { self.error = Some(e); @@ -3047,7 +3147,7 @@ impl TaskwarriorTui { } } } - Key::Tab | Key::Ctrl('n') => { + KeyCode::Tab | KeyCode::Ctrl('n') => { if !self.completion_list.is_empty() { self.update_input_for_completion(); if !self.show_completion_pane { @@ -3056,12 +3156,12 @@ impl TaskwarriorTui { self.completion_list.next(); } } - Key::BackTab | Key::Ctrl('p') => { + KeyCode::BackTab | KeyCode::Ctrl('p') => { if self.show_completion_pane && !self.completion_list.is_empty() { self.completion_list.previous(); } } - Key::Up => { + KeyCode::Up => { if self.show_completion_pane && !self.completion_list.is_empty() { self.completion_list.previous(); } else if let Some(s) = self @@ -3081,7 +3181,7 @@ impl TaskwarriorTui { )); } } - Key::Down => { + KeyCode::Down => { if self.show_completion_pane && !self.completion_list.is_empty() { self.completion_list.next(); } else if let Some(s) = self @@ -3109,7 +3209,7 @@ impl TaskwarriorTui { } }, Action::Jump => match input { - Key::Char('\n') => { + KeyCode::Char('\n') => { if self.error.is_some() { self.previous_mode = Some(self.mode.clone()); self.mode = Mode::Tasks(Action::Error); @@ -3118,7 +3218,7 @@ impl TaskwarriorTui { Ok(_) => { self.mode = Mode::Tasks(Action::Report); self.reset_command(); - self.update(true)?; + self.update(true).await?; } Err(e) => { self.reset_command(); @@ -3127,14 +3227,14 @@ impl TaskwarriorTui { } } } - Key::Esc => { + KeyCode::Esc => { self.reset_command(); self.mode = Mode::Tasks(Action::Report); } _ => handle_movement(&mut self.command, input), }, Action::Add => match input { - Key::Esc => { + KeyCode::Esc => { if self.show_completion_pane { self.show_completion_pane = false; self.completion_list.unselect(); @@ -3144,7 +3244,7 @@ impl TaskwarriorTui { self.mode = Mode::Tasks(Action::Report); } } - Key::Char('\n') => { + KeyCode::Char('\n') => { if self.show_completion_pane { self.show_completion_pane = false; if let Some((i, (r, m, o, _, _))) = self.completion_list.selected() { @@ -3163,7 +3263,7 @@ impl TaskwarriorTui { self.command_history.add(self.command.as_str()); self.reset_command(); self.history_status = None; - self.update(true)?; + self.update(true).await?; } Err(e) => { self.error = Some(e); @@ -3171,7 +3271,7 @@ impl TaskwarriorTui { } } } - Key::Tab | Key::Ctrl('n') => { + KeyCode::Tab | KeyCode::Ctrl('n') => { if !self.completion_list.is_empty() { self.update_input_for_completion(); if !self.show_completion_pane { @@ -3180,12 +3280,12 @@ impl TaskwarriorTui { self.completion_list.next(); } } - Key::BackTab | Key::Ctrl('p') => { + KeyCode::BackTab | KeyCode::Ctrl('p') => { if self.show_completion_pane && !self.completion_list.is_empty() { self.completion_list.previous(); } } - Key::Up => { + KeyCode::Up => { if self.show_completion_pane && !self.completion_list.is_empty() { self.completion_list.previous(); } else if let Some(s) = self @@ -3206,7 +3306,7 @@ impl TaskwarriorTui { } } - Key::Down => { + KeyCode::Down => { if self.show_completion_pane && !self.completion_list.is_empty() { self.completion_list.next(); } else if let Some(s) = self @@ -3233,7 +3333,7 @@ impl TaskwarriorTui { } }, Action::Filter => match input { - Key::Esc => { + KeyCode::Esc => { if self.show_completion_pane { self.show_completion_pane = false; self.completion_list.unselect(); @@ -3249,10 +3349,10 @@ impl TaskwarriorTui { self.dirty = true; } self.history_status = None; - self.update(true)?; + self.update(true).await?; } } - Key::Char('\n') => { + KeyCode::Char('\n') => { if self.show_completion_pane { self.show_completion_pane = false; if let Some((i, (r, m, o, _, _))) = self.completion_list.selected() { @@ -3269,10 +3369,10 @@ impl TaskwarriorTui { self.mode = Mode::Tasks(Action::Report); self.filter_history.add(self.filter.as_str()); self.history_status = None; - self.update(true)?; + self.update(true).await?; } } - Key::Up => { + KeyCode::Up => { if self.show_completion_pane && !self.completion_list.is_empty() { self.completion_list.previous(); } else if let Some(s) = self @@ -3293,7 +3393,7 @@ impl TaskwarriorTui { self.dirty = true; } } - Key::Down => { + KeyCode::Down => { if self.show_completion_pane && !self.completion_list.is_empty() { self.completion_list.next(); } else if let Some(s) = self @@ -3314,7 +3414,7 @@ impl TaskwarriorTui { self.dirty = true; } } - Key::Tab | Key::Ctrl('n') => { + KeyCode::Tab | KeyCode::Ctrl('n') => { if !self.completion_list.is_empty() { self.update_input_for_completion(); if !self.show_completion_pane { @@ -3323,12 +3423,12 @@ impl TaskwarriorTui { self.completion_list.next(); } } - Key::BackTab | Key::Ctrl('p') => { + KeyCode::BackTab | KeyCode::Ctrl('p') => { if self.show_completion_pane && !self.completion_list.is_empty() { self.completion_list.previous(); } } - Key::Ctrl('r') => { + KeyCode::Ctrl('r') => { self.filter.update("", 0); for c in self.config.filter.chars() { self.filter.insert(c, 1); @@ -3344,7 +3444,7 @@ impl TaskwarriorTui { } }, Action::DonePrompt => { - if input == self.keyconfig.done || input == Key::Char('\n') { + 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); @@ -3352,21 +3452,21 @@ impl TaskwarriorTui { match self.task_done() { Ok(_) => { self.mode = Mode::Tasks(Action::Report); - self.update(true)?; + self.update(true).await?; } Err(e) => { self.error = Some(e); } } } - } else if input == self.keyconfig.quit || input == Key::Esc { + } else if input == self.keyconfig.quit || input == KeyCode::Esc { self.mode = Mode::Tasks(Action::Report); } else { handle_movement(&mut self.command, input); } } Action::DeletePrompt => { - if input == self.keyconfig.delete || input == Key::Char('\n') { + 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); @@ -3374,14 +3474,14 @@ impl TaskwarriorTui { match self.task_delete() { Ok(_) => { self.mode = Mode::Tasks(Action::Report); - self.update(true)?; + self.update(true).await?; } Err(e) => { self.error = Some(e); } } } - } else if input == self.keyconfig.quit || input == Key::Esc { + } else if input == self.keyconfig.quit || input == KeyCode::Esc { self.mode = Mode::Tasks(Action::Report); } else { handle_movement(&mut self.command, input); @@ -3545,48 +3645,48 @@ impl TaskwarriorTui { } } -pub fn handle_movement(linebuffer: &mut LineBuffer, input: Key) { +pub fn handle_movement(linebuffer: &mut LineBuffer, input: KeyCode) { match input { - Key::Ctrl('f') | Key::Right => { + KeyCode::Ctrl('f') | KeyCode::Right => { linebuffer.move_forward(1); } - Key::Ctrl('b') | Key::Left => { + KeyCode::Ctrl('b') | KeyCode::Left => { linebuffer.move_backward(1); } - Key::Ctrl('h') | Key::Backspace => { + KeyCode::Ctrl('h') | KeyCode::Backspace => { linebuffer.backspace(1); } - Key::Ctrl('d') | Key::Delete => { + KeyCode::Ctrl('d') | KeyCode::Delete => { linebuffer.delete(1); } - Key::Ctrl('a') | Key::Home => { + KeyCode::Ctrl('a') | KeyCode::Home => { linebuffer.move_home(); } - Key::Ctrl('e') | Key::End => { + KeyCode::Ctrl('e') | KeyCode::End => { linebuffer.move_end(); } - Key::Ctrl('k') => { + KeyCode::Ctrl('k') => { linebuffer.kill_line(); } - Key::Ctrl('u') => { + KeyCode::Ctrl('u') => { linebuffer.discard_line(); } - Key::Ctrl('w') | Key::AltBackspace | Key::CtrlBackspace => { + KeyCode::Ctrl('w') | KeyCode::AltBackspace | KeyCode::CtrlBackspace => { linebuffer.delete_prev_word(Word::Emacs, 1); } - Key::Alt('d') | Key::AltDelete | Key::CtrlDelete => { + KeyCode::Alt('d') | KeyCode::AltDelete | KeyCode::CtrlDelete => { linebuffer.delete_word(At::AfterEnd, Word::Emacs, 1); } - Key::Alt('f') => { + KeyCode::Alt('f') => { linebuffer.move_to_next_word(At::AfterEnd, Word::Emacs, 1); } - Key::Alt('b') => { + KeyCode::Alt('b') => { linebuffer.move_to_prev_word(Word::Emacs, 1); } - Key::Alt('t') => { + KeyCode::Alt('t') => { linebuffer.transpose_words(1); } - Key::Char(c) => { + KeyCode::Char(c) => { linebuffer.insert(c, 1); } _ => {} @@ -3667,21 +3767,21 @@ mod tests { std::fs::remove_dir_all(cd).unwrap(); } - fn test_taskwarrior_tui_history() { - let mut app = TaskwarriorTui::new("next").unwrap(); + async fn test_taskwarrior_tui_history() { + let mut app = TaskwarriorTui::new("next").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(Key::Char(c)).unwrap(); + app.handle_input(KeyCode::Char(c)).await.unwrap(); } - app.handle_input(Key::Right).unwrap(); + app.handle_input(KeyCode::Right).await.unwrap(); let input = " +test"; for c in input.chars() { - app.handle_input(Key::Char(c)).unwrap(); + app.handle_input(KeyCode::Char(c)).await.unwrap(); } - app.handle_input(Key::Char('\n')).unwrap(); + app.handle_input(KeyCode::Char('\n')).await.unwrap(); app.mode = Mode::Tasks(Action::Add); @@ -3698,19 +3798,19 @@ mod tests { let input = "Buy groceries"; for c in input.chars() { - app.handle_input(Key::Char(c)).unwrap(); + app.handle_input(KeyCode::Char(c)).await.unwrap(); } - app.handle_input(Key::Right).unwrap(); + app.handle_input(KeyCode::Right).await.unwrap(); let input = " +test"; for c in input.chars() { - app.handle_input(Key::Char(c)).unwrap(); + app.handle_input(KeyCode::Char(c)).await.unwrap(); } - app.update(true).unwrap(); - app.handle_input(Key::Down).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(Key::Char('\n')).unwrap(); + app.handle_input(KeyCode::Char('\n')).await.unwrap(); app.mode = Mode::Tasks(Action::Add); app.update_completion_list(); @@ -3726,26 +3826,26 @@ mod tests { let input = "Buy groceries"; for c in input.chars() { - app.handle_input(Key::Char(c)).unwrap(); + app.handle_input(KeyCode::Char(c)).await.unwrap(); } - app.handle_input(Key::Right).unwrap(); - app.handle_input(Key::Backspace).unwrap(); - app.update(true).unwrap(); - app.handle_input(Key::Down).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).unwrap(); + app.update(true).await.unwrap(); - app.handle_input(Key::Up).unwrap(); + app.handle_input(KeyCode::Up).await.unwrap(); assert_eq!("\"Buy groceries\" +test", app.command.as_str()); // teardown(); } - #[test] - fn test_taskwarrior_tui() { - let app = TaskwarriorTui::new("next"); + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] + async fn test_taskwarrior_tui() { + let app = TaskwarriorTui::new("next").await; if app.is_err() { return; } @@ -3753,42 +3853,42 @@ mod tests { 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").unwrap(); + let app = TaskwarriorTui::new("next").await.unwrap(); assert!(app .task_by_uuid(Uuid::parse_str("3f43831b-88dc-45e2-bf0d-4aea6db634cc").unwrap()) .is_none()); - test_draw_empty_task_report(); + test_draw_empty_task_report().await; - test_draw_calendar(); - test_draw_help_popup(); + test_draw_calendar().await; + test_draw_help_popup().await; setup(); - let app = TaskwarriorTui::new("next").unwrap(); + let app = TaskwarriorTui::new("next").await.unwrap(); assert!(app.task_by_index(0).is_some()); - let app = TaskwarriorTui::new("next").unwrap(); + let app = TaskwarriorTui::new("next").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(); + test_draw_task_report_with_extended_modify_command().await; // test_draw_task_report(); - test_task_tags(); - test_task_style(); - test_task_context(); - test_task_tomorrow(); - test_task_earlier_today(); - test_task_later_today(); - test_taskwarrior_tui_history(); + 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(); } - fn test_task_tags() { + async fn test_task_tags() { // testing tags - let app = TaskwarriorTui::new("next").unwrap(); + let app = TaskwarriorTui::new("next").await.unwrap(); let task = app.task_by_id(1).unwrap(); let tags = vec!["PENDING".to_string(), "PRIORITY".to_string()]; @@ -3797,7 +3897,7 @@ mod tests { assert!(task.tags().unwrap().contains(&tag)); } - let mut app = TaskwarriorTui::new("next").unwrap(); + let mut app = TaskwarriorTui::new("next").await.unwrap(); let task = app.task_by_id(11).unwrap(); let tags = vec!["finance", "UNBLOCKED", "PENDING", "TAGGED", "UDA"] .iter() @@ -3815,7 +3915,7 @@ mod tests { } app.task_quick_tag().unwrap(); - app.update(true).unwrap(); + app.update(true).await.unwrap(); let task = app.task_by_id(11).unwrap(); let tags = vec!["next", "finance", "UNBLOCKED", "PENDING", "TAGGED", "UDA"] @@ -3827,7 +3927,7 @@ mod tests { } app.task_quick_tag().unwrap(); - app.update(true).unwrap(); + app.update(true).await.unwrap(); let task = app.task_by_id(11).unwrap(); let tags = vec!["finance", "UNBLOCKED", "PENDING", "TAGGED", "UDA"] @@ -3839,8 +3939,8 @@ mod tests { } } - fn test_task_style() { - let app = TaskwarriorTui::new("next").unwrap(); + async fn test_task_style() { + let app = TaskwarriorTui::new("next").await.unwrap(); let task = app.task_by_id(1).unwrap(); for r in vec![ "active", @@ -3869,10 +3969,10 @@ mod tests { let style = app.style_for_task(&task); } - fn test_task_context() { - let mut app = TaskwarriorTui::new("next").unwrap(); + async fn test_task_context() { + let mut app = TaskwarriorTui::new("next").await.unwrap(); - assert!(app.update(true).is_ok()); + assert!(app.update(true).await.is_ok()); app.context_select().unwrap(); @@ -3885,7 +3985,7 @@ mod tests { app.context_select().unwrap(); assert_eq!(app.contexts.table_state.current_selection(), Some(2)); - assert!(app.update(true).is_ok()); + assert!(app.update(true).await.is_ok()); assert_eq!(app.tasks.len(), 1); assert_eq!(app.current_context_filter, "+finance -private"); @@ -3896,17 +3996,17 @@ mod tests { app.context_select().unwrap(); assert_eq!(app.contexts.table_state.current_selection(), Some(0)); - assert!(app.update(true).is_ok()); + assert!(app.update(true).await.is_ok()); assert_eq!(app.tasks.len(), 26); assert_eq!(app.current_context_filter, ""); } - fn test_task_tomorrow() { + async fn test_task_tomorrow() { let total_tasks: u64 = 26; - let mut app = TaskwarriorTui::new("next").unwrap(); - assert!(app.update(true).is_ok()); + let mut app = TaskwarriorTui::new("next").await.unwrap(); + assert!(app.update(true).await.is_ok()); assert_eq!(app.tasks.len(), total_tasks as usize); assert_eq!(app.current_context_filter, ""); @@ -3936,7 +4036,7 @@ mod tests { let task_id = caps["task_id"].parse::().unwrap(); assert_eq!(task_id, total_tasks + 1); - assert!(app.update(true).is_ok()); + assert!(app.update(true).await.is_ok()); assert_eq!(app.tasks.len(), (total_tasks + 1) as usize); assert_eq!(app.current_context_filter, ""); @@ -3963,17 +4063,17 @@ mod tests { .output() .unwrap(); - let mut app = TaskwarriorTui::new("next").unwrap(); - assert!(app.update(true).is_ok()); + let mut app = TaskwarriorTui::new("next").await.unwrap(); + assert!(app.update(true).await.is_ok()); assert_eq!(app.tasks.len(), total_tasks as usize); assert_eq!(app.current_context_filter, ""); } - fn test_task_earlier_today() { + async fn test_task_earlier_today() { let total_tasks: u64 = 26; - let mut app = TaskwarriorTui::new("next").unwrap(); - assert!(app.update(true).is_ok()); + let mut app = TaskwarriorTui::new("next").await.unwrap(); + assert!(app.update(true).await.is_ok()); assert_eq!(app.tasks.len(), total_tasks as usize); assert_eq!(app.current_context_filter, ""); @@ -3996,7 +4096,7 @@ mod tests { let task_id = caps["task_id"].parse::().unwrap(); assert_eq!(task_id, total_tasks + 1); - assert!(app.update(true).is_ok()); + assert!(app.update(true).await.is_ok()); assert_eq!(app.tasks.len(), (total_tasks + 1) as usize); assert_eq!(app.current_context_filter, ""); @@ -4022,17 +4122,17 @@ mod tests { .output() .unwrap(); - let mut app = TaskwarriorTui::new("next").unwrap(); - assert!(app.update(true).is_ok()); + let mut app = TaskwarriorTui::new("next").await.unwrap(); + assert!(app.update(true).await.is_ok()); assert_eq!(app.tasks.len(), total_tasks as usize); assert_eq!(app.current_context_filter, ""); } - fn test_task_later_today() { + async fn test_task_later_today() { let total_tasks: u64 = 26; - let mut app = TaskwarriorTui::new("next").unwrap(); - assert!(app.update(true).is_ok()); + let mut app = TaskwarriorTui::new("next").await.unwrap(); + assert!(app.update(true).await.is_ok()); assert_eq!(app.tasks.len(), total_tasks as usize); assert_eq!(app.current_context_filter, ""); @@ -4063,7 +4163,7 @@ mod tests { let task_id = caps["task_id"].parse::().unwrap(); assert_eq!(task_id, total_tasks + 1); - assert!(app.update(true).is_ok()); + assert!(app.update(true).await.is_ok()); assert_eq!(app.tasks.len(), (total_tasks + 1) as usize); assert_eq!(app.current_context_filter, ""); @@ -4088,42 +4188,13 @@ mod tests { .output() .unwrap(); - let mut app = TaskwarriorTui::new("next").unwrap(); - assert!(app.update(true).is_ok()); + let mut app = TaskwarriorTui::new("next").await.unwrap(); + assert!(app.update(true).await.is_ok()); assert_eq!(app.tasks.len(), total_tasks as usize); assert_eq!(app.current_context_filter, ""); } - fn test_draw_empty_task_report() { - let test_case = |expected: &Buffer| { - let mut app = TaskwarriorTui::new("next").unwrap(); - - app.task_report_next(); - app.context_next(); - - let total_tasks: u64 = 0; - - assert!(app.update(true).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).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_empty_task_report() { let mut expected = Buffer::with_lines(vec![ " Tasks Projects Calendar [none]", " ", @@ -4162,151 +4233,36 @@ mod tests { .get_mut(i, 13) .set_style(Style::default().add_modifier(Modifier::REVERSED)); } - test_case(&expected); + + let mut app = TaskwarriorTui::new("next").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); } - fn test_draw_task_report_with_extended_modify_command() { - let test_case = |expected1: &Buffer, expected2: &Buffer| { - let mut app = TaskwarriorTui::new("next").unwrap(); - - let total_tasks: u64 = 26; - - assert!(app.update(true).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()) - } - None => app.modify.update("", 0), - }, - TableMode::MultipleSelection => app.modify.update("", 0), - } - - app.update(true).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(",")) - }; - TaskwarriorTui::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(",")) - }; - TaskwarriorTui::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_with_extended_modify_command() { let mut expected1 = Buffer::with_lines(vec![ "Modify Task 10 ", " based on your .taskrc ", @@ -4338,97 +4294,147 @@ mod tests { .set_style(Style::default().add_modifier(Modifier::REVERSED)); } - test_case(&expected1, &expected2); + let mut app = TaskwarriorTui::new("next").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()) + } + None => app.modify.update("", 0), + }, + TableMode::MultipleSelection => app.modify.update("", 0), + } + + app.update(true).await.unwrap(); + + let backend = TestBackend::new(25, 3); + let mut terminal = Terminal::new(backend).unwrap(); + terminal + .draw(|f| { + let rects = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(0), Constraint::Length(3)].as_ref()) + .split(f.size()); + + let position = TaskwarriorTui::get_position(&app.modify); + f.set_cursor( + std::cmp::min( + rects[1].x + position as u16, + rects[1].x + rects[1].width.saturating_sub(2), + ), + rects[1].y + 1, + ); + f.render_widget(Clear, rects[1]); + let selected = app.current_selection; + let task_ids = if app.tasks.is_empty() { + vec!["0".to_string()] + } else { + match app.task_table_state.mode() { + TableMode::SingleSelection => { + vec![app.tasks[selected].id().unwrap_or_default().to_string()] + } + TableMode::MultipleSelection => { + let mut tids = vec![]; + for uuid in app.marked.iter() { + if let Some(t) = app.task_by_uuid(*uuid) { + tids.push(t.id().unwrap_or_default().to_string()); + } + } + tids + } + } + }; + let label = if task_ids.len() > 1 { + format!("Modify Tasks {}", task_ids.join(",")) + } else { + format!("Modify Task {}", task_ids.join(",")) + }; + app.draw_command( + f, + rects[1], + app.modify.as_str(), + (Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), None), + position, + true, + app.error.clone(), + ); + }) + .unwrap(); + + assert_eq!(terminal.backend().size().unwrap(), expected1.area); + terminal.backend().assert_buffer(&expected1); + + app.modify.move_home(); + + terminal + .draw(|f| { + let rects = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(0), Constraint::Length(3)].as_ref()) + .split(f.size()); + + let position = TaskwarriorTui::get_position(&app.modify); + f.set_cursor( + std::cmp::min( + rects[1].x + position as u16, + rects[1].x + rects[1].width.saturating_sub(2), + ), + rects[1].y + 1, + ); + f.render_widget(Clear, rects[1]); + let selected = app.current_selection; + let task_ids = if app.tasks.is_empty() { + vec!["0".to_string()] + } else { + match app.task_table_state.mode() { + TableMode::SingleSelection => { + vec![app.tasks[selected].id().unwrap_or_default().to_string()] + } + TableMode::MultipleSelection => { + let mut tids = vec![]; + for uuid in app.marked.iter() { + if let Some(t) = app.task_by_uuid(*uuid) { + tids.push(t.id().unwrap_or_default().to_string()); + } + } + tids + } + } + }; + let label = if task_ids.len() > 1 { + format!("Modify Tasks {}", task_ids.join(",")) + } else { + format!("Modify Task {}", task_ids.join(",")) + }; + app.draw_command( + f, + rects[1], + app.modify.as_str(), + (Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), None), + position, + true, + app.error.clone(), + ); + }) + .unwrap(); + + assert_eq!(terminal.backend().size().unwrap(), expected2.area); + terminal.backend().assert_buffer(&expected2); } - fn test_draw_task_report() { - let test_case = |expected: &Buffer| { - let mut app = TaskwarriorTui::new("next").unwrap(); - - app.task_report_next(); - app.context_next(); - - let total_tasks: u64 = 26; - - assert!(app.update(true).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 = 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 = 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).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 = Command::new("task") - .arg("rc.confirmation=off") - .arg("undo") - .output() - .unwrap(); - let output = 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_task_report() { let mut expected = Buffer::with_lines(vec![ "╭Task|Calendar───────────────────────────────────╮", "│ ID Age Deps P Projec Tag Due Descrip Urg │", @@ -4493,33 +4499,93 @@ mod tests { .set_style(Style::default().fg(Color::Indexed(1)).bg(Color::Indexed(4))); } - test_case(&expected); + let mut app = TaskwarriorTui::new("next").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 = 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 = 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 = Command::new("task") + .arg("rc.confirmation=off") + .arg("undo") + .output() + .unwrap(); + let output = Command::new("task") + .arg("rc.confirmation=off") + .arg("undo") + .output() + .unwrap(); + + assert_eq!(terminal.backend().size().unwrap(), expected.area); + terminal.backend().assert_buffer(&expected); } - fn test_draw_calendar() { - let test_case = |expected: &Buffer| { - let mut app = TaskwarriorTui::new("next").unwrap(); - - app.task_report_next(); - app.context_next(); - app.update(true).unwrap(); - - app.calendar_year = 2020; - app.mode = Mode::Calendar; - - let backend = TestBackend::new(50, 15); - let mut terminal = Terminal::new(backend).unwrap(); - terminal - .draw(|f| { - app.draw(f); - app.draw(f); - }) - .unwrap(); - - assert_eq!(terminal.backend().size().unwrap(), expected.area); - terminal.backend().assert_buffer(expected); - }; - + async fn test_draw_calendar() { let mut expected = Buffer::with_lines(vec![ " Tasks Projects Calendar [none]", " ", @@ -4579,30 +4645,29 @@ mod tests { .set_style(Style::default().bg(Color::Reset).add_modifier(Modifier::UNDERLINED)); } - test_case(&expected); + let mut app = TaskwarriorTui::new("next").await.unwrap(); + + app.task_report_next(); + app.context_next(); + app.update(true).await.unwrap(); + + app.calendar_year = 2020; + app.mode = Mode::Calendar; + + let backend = TestBackend::new(50, 15); + let mut terminal = Terminal::new(backend).unwrap(); + terminal + .draw(|f| { + app.draw(f); + app.draw(f); + }) + .unwrap(); + + assert_eq!(terminal.backend().size().unwrap(), expected.area); + terminal.backend().assert_buffer(&expected); } - fn test_draw_help_popup() { - let test_case = |expected: &Buffer| { - let mut app = TaskwarriorTui::new("next").unwrap(); - - app.mode = Mode::Tasks(Action::HelpPopup); - app.task_report_next(); - app.context_next(); - app.update(true).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); - }; - + async fn test_draw_help_popup() { let mut expected = Buffer::with_lines(vec![ "╭Help──────────────────────────────────╮", "│# Default Keybindings │", @@ -4625,31 +4690,27 @@ mod tests { .set_style(Style::default().add_modifier(Modifier::BOLD)); } - test_case(&expected); + let mut app = TaskwarriorTui::new("next").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] - fn test_draw_context_menu() { - let test_case = |expected: &Buffer| { - let mut app = TaskwarriorTui::new("next").unwrap(); - - app.mode = Mode::Tasks(Action::ContextMenu); - app.task_report_next(); - app.update(true).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); - }; - + async fn test_draw_context_menu() { let mut expected = Buffer::with_lines(vec![ "╭Context───────────────────────────────────────────────────────────────────────╮", "│Name Description Active│", @@ -4698,16 +4759,32 @@ mod tests { .set_style(Style::default().add_modifier(Modifier::BOLD)); } - test_case(&expected); + let mut app = TaskwarriorTui::new("next").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] - fn test_graphemes() { + async fn test_graphemes() { dbg!("写作业".graphemes(true).count()); dbg!(UnicodeWidthStr::width("写作业")); dbg!(UnicodeWidthStr::width("abc")); - let mut app = TaskwarriorTui::new("next").unwrap(); + let mut app = TaskwarriorTui::new("next").await.unwrap(); if let Some(task) = app.task_by_id(27) { let i = app.task_index_by_uuid(*task.uuid()).unwrap_or_default(); @@ -4715,7 +4792,7 @@ mod tests { app.current_selection_id = None; app.current_selection_uuid = None; } - app.update(true).unwrap(); + app.update(true).await.unwrap(); app.mode = Mode::Tasks(Action::Modify); match app.task_current() { Some(t) => { @@ -4724,7 +4801,7 @@ mod tests { } None => app.modify.update("", 0), } - app.update(true).unwrap(); + app.update(true).await.unwrap(); dbg!(app.modify.as_str()); dbg!(app.modify.as_str().len()); @@ -4734,43 +4811,27 @@ mod tests { dbg!(position); } - #[test] - fn test_app() { - let app = TaskwarriorTui::new("next"); - if app.is_err() { - return; - } - let mut app = app.unwrap(); - - let backend = TestBackend::new(80, 20); - let mut terminal = Terminal::new(backend).unwrap(); - - app.render(&mut terminal).unwrap(); - dbg!(app.get_dates_with_styles()); - println!("{}", buffer_view(terminal.backend().buffer())); - } - // #[test] - fn test_taskwarrior_tui_completion() { - let mut app = TaskwarriorTui::new("next").unwrap(); - app.handle_input(Key::Char('z')).unwrap(); + async fn test_taskwarrior_tui_completion() { + let mut app = TaskwarriorTui::new("next").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(Key::Char(c)).unwrap(); + app.handle_input(KeyCode::Char(c)).await.unwrap(); } - app.handle_input(Key::Ctrl('e')).unwrap(); + app.handle_input(KeyCode::Ctrl('e')).await.unwrap(); let input = " project:CO"; for c in input.chars() { - app.handle_input(Key::Char(c)).unwrap(); + app.handle_input(KeyCode::Char(c)).await.unwrap(); } app.mode = Mode::Tasks(Action::Add); app.update_completion_list(); - app.handle_input(Key::Tab).unwrap(); - app.handle_input(Key::Char('\n')).unwrap(); + 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 diff --git a/src/config.rs b/src/config.rs index a589bde..2c9afbb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -38,7 +38,7 @@ impl TaskWarriorBool for str { } #[derive(Debug)] -pub struct UDA { +pub struct Uda { label: String, kind: String, values: Option>, @@ -85,6 +85,8 @@ pub struct Config { pub uda_style_report_selection: Style, pub uda_style_calendar_title: Style, pub uda_style_calendar_today: Style, + pub uda_style_navbar: Style, + pub uda_style_command: Style, pub uda_style_report_completion_pane: Style, pub uda_style_report_completion_pane_highlight: Style, pub uda_shortcuts: Vec, @@ -95,7 +97,7 @@ 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, + pub uda: Vec, } impl Config { @@ -144,6 +146,8 @@ impl Config { 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); @@ -157,6 +161,8 @@ impl Config { 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))); @@ -204,6 +210,8 @@ impl Config { uda_style_context_active, uda_style_calendar_title, uda_style_calendar_today, + uda_style_navbar, + uda_style_command, uda_style_report_completion_pane, uda_style_report_completion_pane_highlight, uda_style_report_scrollbar, diff --git a/src/event.rs b/src/event.rs index 2d36b5f..d64acce 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,24 +1,24 @@ -use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, EventStream}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, -}; -use tui::{backend::CrosstermBackend, Terminal}; - -use async_std::channel::unbounded; -use async_std::sync::Arc; -use async_std::task; -use futures::prelude::*; -use futures::{future::FutureExt, select, StreamExt}; -use futures_timer::Delay; -use std::io::{self, Write}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::{Duration, Instant}; - +use crossterm::event::KeyEvent; +use futures::StreamExt; use serde::{Deserialize, Serialize}; +use tokio::{sync::mpsc, sync::oneshot, task::JoinHandle}; + +use crossterm::event::{ + KeyCode::{ + BackTab, Backspace, Char, Delete, Down, End, Enter, Esc, Home, Insert, Left, Null, PageDown, PageUp, Right, + Tab, Up, F, + }, + KeyModifiers, +}; + +#[derive(Debug, Clone, Copy)] +pub enum Event { + Input(I), + Tick, +} #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd, Eq)] -pub enum Key { +pub enum KeyCode { CtrlBackspace, CtrlDelete, AltBackspace, @@ -44,108 +44,87 @@ pub enum Key { Tab, } -#[derive(Debug, Clone, Copy)] -pub struct EventConfig { - pub tick_rate: Duration, +pub struct EventLoop { + pub rx: mpsc::UnboundedReceiver>, + pub tx: mpsc::UnboundedSender>, + pub abort: mpsc::UnboundedSender<()>, + pub handle: JoinHandle<()>, + pub tick_rate: std::time::Duration, } -#[derive(Debug, Clone, Copy)] -pub enum Event { - Input(I), - Tick, -} +impl EventLoop { + pub fn new(tick_rate: Option) -> Self { + let (tx, rx) = mpsc::unbounded_channel(); + let _tx = tx.clone(); + let mut reader = crossterm::event::EventStream::new(); + let should_tick = tick_rate.is_some(); + let tick_rate = tick_rate.unwrap_or(std::time::Duration::from_millis(250)); -pub struct Events { - pub rx: async_std::channel::Receiver>, -} - -impl Events { - pub fn with_config(config: EventConfig) -> Events { - use crossterm::event::{ - KeyCode::{ - BackTab, Backspace, Char, Delete, Down, End, Enter, Esc, Home, Insert, Left, Null, PageDown, PageUp, - Right, Tab, Up, F, - }, - KeyModifiers, - }; - let tick_rate = config.tick_rate; - let (tx, rx) = unbounded::>(); - task::spawn_local(async move { - let mut reader = EventStream::new(); + let (abort, mut recv) = mpsc::unbounded_channel(); + let handle = tokio::spawn(async move { loop { - let mut delay = Delay::new(tick_rate).fuse(); - let mut event = reader.next().fuse(); + let delay = tokio::time::sleep(tick_rate); + let event = reader.next(); - select! { - _ = delay => { - tx.send(Event::Tick).await.ok(); + tokio::select! { + _ = recv.recv() => break, + _ = delay, if should_tick => { + _tx.send(Event::Tick).unwrap(); }, + _ = _tx.closed() => break, maybe_event = event => { - if let Some(Ok(event::Event::Key(key))) = maybe_event { + if let Some(Ok(crossterm::event::Event::Key(key))) = maybe_event { let key = match key.code { Backspace => { match key.modifiers { - KeyModifiers::CONTROL => Key::CtrlBackspace, - KeyModifiers::ALT => Key::AltBackspace, - _ => Key::Backspace, + KeyModifiers::CONTROL => KeyCode::CtrlBackspace, + KeyModifiers::ALT => KeyCode::AltBackspace, + _ => KeyCode::Backspace, } }, Delete => { match key.modifiers { - KeyModifiers::CONTROL => Key::CtrlDelete, - KeyModifiers::ALT => Key::AltDelete, - _ => Key::Delete, + KeyModifiers::CONTROL => KeyCode::CtrlDelete, + KeyModifiers::ALT => KeyCode::AltDelete, + _ => KeyCode::Delete, } }, - Enter => Key::Char('\n'), - Left => Key::Left, - Right => Key::Right, - Up => Key::Up, - Down => Key::Down, - Home => Key::Home, - End => Key::End, - PageUp => Key::PageUp, - PageDown => Key::PageDown, - Tab => Key::Tab, - BackTab => Key::BackTab, - Insert => Key::Insert, - F(k) => Key::F(k), - Null => Key::Null, - Esc => Key::Esc, + Enter => KeyCode::Char('\n'), + Left => KeyCode::Left, + Right => KeyCode::Right, + Up => KeyCode::Up, + Down => KeyCode::Down, + Home => KeyCode::Home, + End => KeyCode::End, + PageUp => KeyCode::PageUp, + PageDown => KeyCode::PageDown, + Tab => KeyCode::Tab, + BackTab => KeyCode::BackTab, + Insert => KeyCode::Insert, + F(k) => KeyCode::F(k), + Null => KeyCode::Null, + Esc => KeyCode::Esc, Char(c) => match key.modifiers { - KeyModifiers::NONE | KeyModifiers::SHIFT => Key::Char(c), - KeyModifiers::CONTROL => Key::Ctrl(c), - KeyModifiers::ALT => Key::Alt(c), - _ => Key::Null, + KeyModifiers::NONE | KeyModifiers::SHIFT => KeyCode::Char(c), + KeyModifiers::CONTROL => KeyCode::Ctrl(c), + KeyModifiers::ALT => KeyCode::Alt(c), + _ => KeyCode::Null, }, }; - tx.send(Event::Input(key)).await.unwrap(); - task::sleep(Duration::from_millis(1)).await; - task::yield_now().await; - }; + _tx.send(Event::Input(key)).unwrap(); + } } } } }); - Events { rx } - } - /// Attempts to read an event. - /// This function will block the current thread. - pub async fn next(&self) -> Result, async_std::channel::RecvError> { - self.rx.recv().await - } - - pub fn leave_tui_mode(terminal: &mut Terminal>) { - disable_raw_mode().unwrap(); - execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture).unwrap(); - terminal.show_cursor().unwrap(); - } - - pub fn enter_tui_mode(terminal: &mut Terminal>) { - execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture).unwrap(); - enable_raw_mode().unwrap(); - terminal.resize(terminal.size().unwrap()).unwrap(); + Self { + tx, + rx, + handle, + tick_rate, + abort, + } } } diff --git a/src/keyconfig.rs b/src/keyconfig.rs index 8965b84..76a41c6 100644 --- a/src/keyconfig.rs +++ b/src/keyconfig.rs @@ -1,5 +1,5 @@ #![allow(clippy::eval_order_dependence)] -use crate::event::Key; +use crate::event::KeyCode; use anyhow::{anyhow, Result}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -8,85 +8,85 @@ use std::hash::Hash; #[derive(Serialize, Deserialize, Debug)] pub struct KeyConfig { - pub quit: Key, - pub refresh: Key, - pub go_to_bottom: Key, - pub go_to_top: Key, - pub down: Key, - pub up: Key, - pub page_down: Key, - pub page_up: Key, - pub delete: Key, - pub done: Key, - pub start_stop: Key, - pub quick_tag: Key, - pub select: Key, - pub select_all: Key, - pub undo: Key, - pub edit: Key, - pub modify: Key, - pub shell: Key, - pub log: Key, - pub add: Key, - pub annotate: Key, - pub help: Key, - pub filter: Key, - pub zoom: Key, - pub context_menu: Key, - pub next_tab: Key, - pub previous_tab: Key, - pub shortcut0: Key, - pub shortcut1: Key, - pub shortcut2: Key, - pub shortcut3: Key, - pub shortcut4: Key, - pub shortcut5: Key, - pub shortcut6: Key, - pub shortcut7: Key, - pub shortcut8: Key, - pub shortcut9: Key, + pub quit: KeyCode, + pub refresh: KeyCode, + pub go_to_bottom: KeyCode, + pub go_to_top: KeyCode, + pub down: KeyCode, + pub up: KeyCode, + pub page_down: KeyCode, + pub page_up: KeyCode, + pub delete: KeyCode, + pub done: KeyCode, + pub start_stop: KeyCode, + pub quick_tag: KeyCode, + pub select: KeyCode, + pub select_all: KeyCode, + pub undo: KeyCode, + pub edit: KeyCode, + pub modify: KeyCode, + pub shell: KeyCode, + pub log: KeyCode, + pub add: KeyCode, + pub annotate: KeyCode, + pub help: KeyCode, + pub filter: KeyCode, + pub zoom: KeyCode, + pub context_menu: KeyCode, + pub next_tab: KeyCode, + pub previous_tab: KeyCode, + pub shortcut0: KeyCode, + pub shortcut1: KeyCode, + pub shortcut2: KeyCode, + pub shortcut3: KeyCode, + pub shortcut4: KeyCode, + pub shortcut5: KeyCode, + pub shortcut6: KeyCode, + pub shortcut7: KeyCode, + pub shortcut8: KeyCode, + pub shortcut9: KeyCode, } impl Default for KeyConfig { fn default() -> Self { Self { - quit: Key::Char('q'), - refresh: Key::Char('r'), - go_to_bottom: Key::Char('G'), - go_to_top: Key::Char('g'), - down: Key::Char('j'), - up: Key::Char('k'), - page_down: Key::Char('J'), - page_up: Key::Char('K'), - delete: Key::Char('x'), - done: Key::Char('d'), - start_stop: Key::Char('s'), - quick_tag: Key::Char('t'), - select: Key::Char('v'), - select_all: Key::Char('V'), - undo: Key::Char('u'), - edit: Key::Char('e'), - modify: Key::Char('m'), - shell: Key::Char('!'), - log: Key::Char('l'), - add: Key::Char('a'), - annotate: Key::Char('A'), - help: Key::Char('?'), - filter: Key::Char('/'), - zoom: Key::Char('z'), - context_menu: Key::Char('c'), - next_tab: Key::Char(']'), - previous_tab: Key::Char('['), - shortcut0: Key::Char('0'), - shortcut1: Key::Char('1'), - shortcut2: Key::Char('2'), - shortcut3: Key::Char('3'), - shortcut4: Key::Char('4'), - shortcut5: Key::Char('5'), - shortcut6: Key::Char('6'), - shortcut7: Key::Char('7'), - shortcut8: Key::Char('8'), - shortcut9: Key::Char('9'), + quit: KeyCode::Char('q'), + refresh: KeyCode::Char('r'), + go_to_bottom: KeyCode::Char('G'), + go_to_top: KeyCode::Char('g'), + down: KeyCode::Char('j'), + up: KeyCode::Char('k'), + page_down: KeyCode::Char('J'), + page_up: KeyCode::Char('K'), + delete: KeyCode::Char('x'), + done: KeyCode::Char('d'), + start_stop: KeyCode::Char('s'), + quick_tag: KeyCode::Char('t'), + select: KeyCode::Char('v'), + select_all: KeyCode::Char('V'), + undo: KeyCode::Char('u'), + edit: KeyCode::Char('e'), + modify: KeyCode::Char('m'), + shell: KeyCode::Char('!'), + log: KeyCode::Char('l'), + add: KeyCode::Char('a'), + annotate: KeyCode::Char('A'), + help: KeyCode::Char('?'), + filter: KeyCode::Char('/'), + zoom: KeyCode::Char('z'), + context_menu: KeyCode::Char('c'), + next_tab: KeyCode::Char(']'), + previous_tab: KeyCode::Char('['), + shortcut0: KeyCode::Char('0'), + shortcut1: KeyCode::Char('1'), + shortcut2: KeyCode::Char('2'), + shortcut3: KeyCode::Char('3'), + shortcut4: KeyCode::Char('4'), + shortcut5: KeyCode::Char('5'), + shortcut6: KeyCode::Char('6'), + shortcut7: KeyCode::Char('7'), + shortcut8: KeyCode::Char('8'), + shortcut9: KeyCode::Char('9'), } } } @@ -195,12 +195,12 @@ impl KeyConfig { } } - fn get_config(config: &str, data: &str) -> Option { + fn get_config(config: &str, data: &str) -> Option { for line in data.split('\n') { if line.starts_with(config) { let line = line.trim_start_matches(config).trim_start().trim_end().to_string(); if line.len() == 1 { - return Some(Key::Char(line.chars().next().unwrap())); + return Some(KeyCode::Char(line.chars().next().unwrap())); } } else if line.starts_with(&config.replace('-', "_")) { let line = line @@ -209,7 +209,7 @@ impl KeyConfig { .trim_end() .to_string(); if line.len() == 1 { - return Some(Key::Char(line.chars().next().unwrap())); + return Some(KeyCode::Char(line.chars().next().unwrap())); } } } diff --git a/src/main.rs b/src/main.rs index 14168a1..8741726 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ #![allow(dead_code)] #![allow(unused_imports)] #![allow(unused_variables)] +#![allow(clippy::too_many_arguments)] mod action; mod app; @@ -16,6 +17,7 @@ mod pane; mod scrollbar; mod table; mod task_report; +mod ui; use log::{debug, error, info, log_enabled, trace, warn, Level, LevelFilter}; use log4rs::append::file::FileAppender; @@ -29,9 +31,6 @@ use std::path::{Path, PathBuf}; use std::time::Duration; use anyhow::Result; -use async_std::prelude::*; -use async_std::sync::{Arc, Mutex}; -use async_std::task; use crossterm::{ cursor, event::{DisableMouseCapture, EnableMouseCapture, EventStream}, @@ -46,20 +45,11 @@ use path_clean::PathClean; use app::{Mode, TaskwarriorTui}; use crate::action::Action; -use crate::event::{Event, EventConfig, Events, Key}; +use crate::event::Event; use crate::keyconfig::KeyConfig; const LOG_PATTERN: &str = "{d(%Y-%m-%d %H:%M:%S)} | {l} | {f}:{L} | {m}{n}"; -pub fn setup_terminal() -> Terminal> { - enable_raw_mode().expect("Running not in terminal"); - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen).unwrap(); - execute!(stdout, Clear(ClearType::All)).unwrap(); - let backend = CrosstermBackend::new(stdout); - Terminal::new(backend).unwrap() -} - pub fn destruct_terminal() { disable_raw_mode().unwrap(); execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture).unwrap(); @@ -116,7 +106,24 @@ pub fn absolute_path(path: impl AsRef) -> io::Result { Ok(absolute_path) } -fn main() { +async fn tui_main(report: &str) -> Result<()> { + panic::set_hook(Box::new(|panic_info| { + destruct_terminal(); + better_panic::Settings::auto().create_panic_handler()(panic_info); + })); + + let mut app = app::TaskwarriorTui::new(report).await?; + + let mut terminal = app.start_tui().await?; + + let r = app.run(&mut terminal).await; + + app.pause_tui().await?; + + r +} + +fn main() -> Result<()> { better_panic::install(); let matches = cli::generate_cli_app().get_matches(); @@ -180,91 +187,14 @@ fn main() { debug!("getting matches from clap..."); debug!("report = {:?}", &report); debug!("config = {:?}", &config); - let r = task::block_on(tui_main(report)); + + let r = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build()? + .block_on(async { tui_main(report).await }); if let Err(err) = r { eprintln!("\x1b[0;31m[taskwarrior-tui error]\x1b[0m: {}\n\nIf you need additional help, please report as a github issue on https://github.com/kdheepak/taskwarrior-tui", err); std::process::exit(1); } -} - -async fn tui_main(report: &str) -> Result<()> { - panic::set_hook(Box::new(|panic_info| { - destruct_terminal(); - better_panic::Settings::auto().create_panic_handler()(panic_info); - })); - - let maybeapp = TaskwarriorTui::new(report); - if maybeapp.is_err() { - destruct_terminal(); - return Err(maybeapp.err().unwrap()); - } - - let mut app = maybeapp.unwrap(); - let mut terminal = setup_terminal(); - - app.render(&mut terminal).unwrap(); - - // Setup event handlers - let events = Events::with_config(EventConfig { - tick_rate: Duration::from_millis(app.config.uda_tick_rate), - }); - - loop { - app.render(&mut terminal).unwrap(); - // Handle input - match events.next().await? { - Event::Input(input) => { - debug!("Received input = {:?}", input); - if (input == app.keyconfig.edit - || input == app.keyconfig.shortcut1 - || input == app.keyconfig.shortcut2 - || input == app.keyconfig.shortcut3 - || input == app.keyconfig.shortcut4 - || input == app.keyconfig.shortcut5 - || input == app.keyconfig.shortcut6 - || input == app.keyconfig.shortcut7 - || input == app.keyconfig.shortcut8 - || input == app.keyconfig.shortcut9) - && app.mode == Mode::Tasks(Action::Report) - { - Events::leave_tui_mode(&mut terminal); - } - - let r = app.handle_input(input); - - if (input == app.keyconfig.edit - || input == app.keyconfig.shortcut1 - || input == app.keyconfig.shortcut2 - || input == app.keyconfig.shortcut3 - || input == app.keyconfig.shortcut4 - || input == app.keyconfig.shortcut5 - || input == app.keyconfig.shortcut6 - || input == app.keyconfig.shortcut7 - || input == app.keyconfig.shortcut8 - || input == app.keyconfig.shortcut9) - && app.mode == Mode::Tasks(Action::Report) - { - Events::enter_tui_mode(&mut terminal); - } - if r.is_err() { - destruct_terminal(); - return r; - } - } - Event::Tick => { - trace!("Tick event"); - let r = app.update(false); - if r.is_err() { - destruct_terminal(); - return r; - } - } - } - - if app.should_quit { - destruct_terminal(); - break; - } - } Ok(()) } diff --git a/src/pane/context.rs b/src/pane/context.rs index a985544..b13dcfe 100644 --- a/src/pane/context.rs +++ b/src/pane/context.rs @@ -20,7 +20,7 @@ use tui::{ use crate::action::Action; use crate::app::{Mode, TaskwarriorTui}; -use crate::event::Key; +use crate::event::KeyCode; use crate::pane::Pane; use crate::table::TableState; use itertools::Itertools; diff --git a/src/pane/mod.rs b/src/pane/mod.rs index 0153e93..64c01c3 100644 --- a/src/pane/mod.rs +++ b/src/pane/mod.rs @@ -2,14 +2,14 @@ use anyhow::Result; use crate::action::Action; use crate::app::{Mode, TaskwarriorTui}; -use crate::event::Key; +use crate::event::KeyCode; use std::ops::Index; pub mod context; pub mod project; pub trait Pane { - fn handle_input(app: &mut TaskwarriorTui, input: Key) -> Result<()>; + fn handle_input(app: &mut TaskwarriorTui, input: KeyCode) -> Result<()>; fn change_focus_to_left_pane(app: &mut TaskwarriorTui) { match app.mode { Mode::Tasks(_) => {} diff --git a/src/pane/project.rs b/src/pane/project.rs index 1813b2f..2b000df 100644 --- a/src/pane/project.rs +++ b/src/pane/project.rs @@ -20,7 +20,7 @@ use tui::{ use crate::action::Action; use crate::app::{Mode, TaskwarriorTui}; -use crate::event::Key; +use crate::event::KeyCode; use crate::pane::Pane; use crate::table::TableState; use itertools::Itertools; @@ -148,16 +148,16 @@ impl ProjectsState { } impl Pane for ProjectsState { - fn handle_input(app: &mut TaskwarriorTui, input: Key) -> Result<()> { - if input == app.keyconfig.quit || input == Key::Ctrl('c') { + fn handle_input(app: &mut TaskwarriorTui, input: KeyCode) -> Result<()> { + if input == app.keyconfig.quit || input == KeyCode::Ctrl('c') { app.should_quit = true; } else if input == app.keyconfig.next_tab { Self::change_focus_to_right_pane(app); } else if input == app.keyconfig.previous_tab { Self::change_focus_to_left_pane(app); - } else if input == Key::Down || input == app.keyconfig.down { + } else if input == KeyCode::Down || input == app.keyconfig.down { self::focus_on_next_project(app); - } else if input == Key::Up || input == app.keyconfig.up { + } else if input == KeyCode::Up || input == app.keyconfig.up { self::focus_on_previous_project(app); } else if input == app.keyconfig.select { self::update_task_filter_by_selection(app)?; @@ -192,25 +192,5 @@ fn update_task_filter_by_selection(app: &mut TaskwarriorTui) -> Result<()> { let mut filter = current_filter.replace(&last_project_pattern, ""); filter = format!("{}{}", filter, new_project_pattern); app.filter.update(filter.as_str(), filter.len()); - app.update(true)?; Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_project_summary() { - let app = TaskwarriorTui::new("next"); - if app.is_err() { - return; - } - let mut app = app.unwrap(); - - app.update(true).unwrap(); - - dbg!(&app.projects.rows); - dbg!(&app.projects.list); - } -} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..d5194b6 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,37 @@ +use tui::{ + backend::Backend, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + symbols, + text::{Span, Spans}, + widgets::{Block, BorderType, Borders, Cell, LineGauge, Paragraph, Row, Table}, + Frame, +}; + +use crate::app::TaskwarriorTui; + +pub fn draw(rect: &mut Frame, app: &TaskwarriorTui) +where + B: Backend, +{ + let size = rect.size(); + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(3), Constraint::Min(10), Constraint::Length(3)].as_ref()) + .split(size); + + let title = draw_title(); + rect.render_widget(title, chunks[0]); +} + +fn draw_title<'a>() -> Paragraph<'a> { + Paragraph::new("Taskwarrior TUI") + .style(Style::default().fg(Color::LightCyan)) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .style(Style::default().fg(Color::White)) + .border_type(BorderType::Plain), + ) +}