diff --git a/.config/config.json5 b/.config/config.json5 new file mode 100644 index 0000000..34d7614 --- /dev/null +++ b/.config/config.json5 @@ -0,0 +1,11 @@ +{ + "keybindings": { + // KeyBindings for TaskReport + "TaskReport": { + "": "Quit", // Quit the application + "": "Quit", // Another way to quit + "": "Quit", // Yet another way to quit + "": "Suspend" // Suspend the application + }, + } +} diff --git a/.rustfmt.toml b/.rustfmt.toml index ef5985c..1cf4c15 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,4 +1,16 @@ max_width = 120 -tab_spaces = 2 -group_imports = "StdExternalCrate" +use_small_heuristics = "Max" +empty_item_single_line = false +force_multiline_blocks = true +format_code_in_doc_comments = true +match_block_trailing_comma = true imports_granularity = "Crate" +normalize_comments = true +normalize_doc_attributes = true +overflow_delimited_expr = true +reorder_impl_items = true +reorder_imports = true +group_imports = "StdExternalCrate" +tab_spaces = 2 +use_field_init_shorthand = true +use_try_shorthand = true diff --git a/Cargo.lock b/Cargo.lock index b5a64cd..2a9a0b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "1.0.5" @@ -90,10 +101,15 @@ dependencies = [ ] [[package]] -name = "atomic" -version = "0.5.3" +name = "async-trait" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] [[package]] name = "autocfg" @@ -116,6 +132,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "better-panic" version = "0.3.0" @@ -141,6 +163,15 @@ dependencies = [ "serde", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.13.0" @@ -184,16 +215,16 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", - "time", + "time 0.1.45", "wasm-bindgen", "windows-targets 0.48.5", ] [[package]] name = "clap" -version = "4.4.2" +version = "4.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6" +checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136" dependencies = [ "clap_builder", "clap_derive", @@ -201,9 +232,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.2" +version = "4.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" +checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56" dependencies = [ "anstream", "anstyle", @@ -216,9 +247,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.4.0" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "586a385f7ef2f8b4d86bddaa0c094794e7ccbfe5ffef1f434fe928143fc783a5" +checksum = "4110a1e6af615a9e6d0a36f805d5c99099f8bab9b8042f5bc1fa220a4a89e36f" dependencies = [ "clap", ] @@ -286,6 +317,25 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "config" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" +dependencies = [ + "async-trait", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml 0.5.11", + "yaml-rust", +] + [[package]] name = "console" version = "0.15.7" @@ -304,6 +354,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + [[package]] name = "crossterm" version = "0.27.0" @@ -331,6 +390,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "darling" version = "0.14.4" @@ -366,6 +435,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "deranged" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" + [[package]] name = "derive_builder" version = "0.12.0" @@ -397,12 +472,33 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_deref" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdbcee2d9941369faba772587a565f4f534e42cb8d17e5295871de730163b2b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "directories" version = "5.0.1" @@ -433,6 +529,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + [[package]] name = "either" version = "1.9.0" @@ -505,24 +607,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" dependencies = [ "cfg-if", - "rustix 0.38.8", + "rustix", "windows-sys 0.48.0", ] -[[package]] -name = "figment" -version = "0.10.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4547e226f4c9ab860571e070a9034192b3175580ecea38da34fcdb53a018c9a5" -dependencies = [ - "atomic", - "pear", - "serde", - "toml", - "uncased", - "version_check", -] - [[package]] name = "fnv" version = "1.0.7" @@ -627,6 +715,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.10" @@ -644,6 +742,15 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.14.0" @@ -683,7 +790,7 @@ dependencies = [ "os_info", "serde", "serde_derive", - "toml", + "toml 0.7.6", "uuid", ] @@ -739,7 +846,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.0", ] [[package]] @@ -748,23 +855,6 @@ version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c785eefb63ebd0e33416dfcb8d6da0bf27ce752843a45632a67bf10d4d4b5c4" -[[package]] -name = "inlinable_string" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "itertools" version = "0.11.0" @@ -789,6 +879,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -802,10 +903,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] -name = "linux-raw-sys" -version = "0.3.8" +name = "linked-hash-map" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" @@ -931,6 +1032,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "object" version = "0.32.0" @@ -952,6 +1062,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown 0.12.3", +] + [[package]] name = "os_info" version = "3.7.0" @@ -1011,27 +1131,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" [[package]] -name = "pear" -version = "0.2.7" +name = "pathdiff" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a386cd715229d399604b50d1361683fe687066f42d56f54be995bc6868f71c" -dependencies = [ - "inlinable_string", - "pear_codegen", - "yansi 1.0.0-rc.1", -] - -[[package]] -name = "pear_codegen" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f0f13dac8069c139e8300a6510e3f4143ecf5259c60b116a9b271b4ca0d54" -dependencies = [ - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn 2.0.29", -] +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "percent-encoding" @@ -1039,6 +1142,51 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "pest" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a4d085fd991ac8d5b05a147b437791b4260b76326baf0fc60cf7c9c27ecd33" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bee7be22ce7918f641a33f08e3f43388c7656772244e2bbb2477f44cc9021a" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1511785c5e98d79a05e8a6bc34b4ac2168a0e3e92161862030ad84daa223141" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "pest_meta" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42f0394d3123e33353ca5e1e89092e533d2cc490389f2bd6131c43c634ebc5f" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "pin-project-lite" version = "0.2.12" @@ -1064,7 +1212,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" dependencies = [ "diff", - "yansi 0.5.1", + "yansi", ] [[package]] @@ -1076,19 +1224,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "proc-macro2-diagnostics" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.29", - "version_check", - "yansi 1.0.0-rc.1", -] - [[package]] name = "quote" version = "1.0.33" @@ -1151,6 +1286,7 @@ dependencies = [ "itertools", "paste", "strum", + "time 0.3.28", "unicode-segmentation", "unicode-width", ] @@ -1228,26 +1364,33 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64", + "bitflags 1.3.2", + "serde", +] + +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" -[[package]] -name = "rustix" -version = "0.37.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - [[package]] name = "rustix" version = "0.38.8" @@ -1257,7 +1400,7 @@ dependencies = [ "bitflags 2.4.0", "errno", "libc", - "linux-raw-sys 0.4.5", + "linux-raw-sys", "windows-sys 0.48.0", ] @@ -1354,6 +1497,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -1529,12 +1683,14 @@ dependencies = [ "clap", "clap_complete", "color-eyre", + "config", "crossterm", + "derive_deref", "directories", - "figment", "futures", "human-panic", "itertools", + "json5", "lazy_static", "libc", "log", @@ -1554,7 +1710,7 @@ dependencies = [ "task-hookrs", "tokio", "tokio-util", - "toml", + "toml 0.8.0", "tracing", "tracing-error", "tracing-subscriber", @@ -1568,11 +1724,11 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ - "rustix 0.37.23", + "rustix", "windows-sys 0.48.0", ] @@ -1617,6 +1773,25 @@ dependencies = [ "winapi", ] +[[package]] +name = "time" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" +dependencies = [ + "deranged", + "libc", + "num_threads", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + [[package]] name = "tinyvec" version = "1.6.0" @@ -1675,6 +1850,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.7.6" @@ -1684,7 +1868,19 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.19.14", +] + +[[package]] +name = "toml" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c226a7bba6d859b63c92c4b4fe69c5b6b72d0cb897dbc8e6012298e6154cb56e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.20.0", ] [[package]] @@ -1709,6 +1905,19 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_edit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ff63e60a958cefbb518ae1fd6566af80d9d4be430a33f3723dfc47d1d411d95" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tracing" version = "0.1.37" @@ -1792,13 +2001,16 @@ dependencies = [ ] [[package]] -name = "uncased" -version = "0.9.9" +name = "typenum" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68" -dependencies = [ - "version_check", -] +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" [[package]] name = "unicase" @@ -2158,14 +2370,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yansi" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" - -[[package]] -name = "yansi" -version = "1.0.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" diff --git a/Cargo.toml b/Cargo.toml index 4012fd9..3c8b09a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,21 +16,23 @@ categories = ["command-line-utilities"] better-panic = "0.3.0" cassowary = "0.3.0" chrono = "0.4.28" -clap = { version = "4.4.2", features = ["std", "color", "help", "usage", "error-context", "suggestions", "derive", "cargo", "wrap_help", "unicode", "string", "unstable-styles"] } +clap = { version = "4.4.4", features = ["derive", "cargo", "wrap_help", "unicode", "string", "unstable-styles"] } color-eyre = { version = "0.6.2", features = ["issue-url"] } +config = "0.13.3" crossterm = { version = "0.27.0", features = ["event-stream", "serde"] } +derive_deref = "1.1.1" directories = "5.0.1" -figment = { version = "0.10.10", features = ["toml", "env"] } futures = "0.3.28" human-panic = "1.2.0" itertools = "0.11.0" +json5 = "0.4.1" lazy_static = "1.4.0" libc = "0.2.147" log = "0.4.20" path-clean = "1.0.1" pretty_assertions = "1.4.0" rand = "0.8.5" -ratatui = "0.23.0" +ratatui = { version = "0.23.0", features = ["all-widgets"] } regex = "1.9.5" rustyline = { version = "12.0.0", features = ["with-file-history", "derive"] } serde = { version = "1.0.188", features = ["derive"] } @@ -43,7 +45,7 @@ strip-ansi-escapes = "0.2.0" task-hookrs = "0.9.0" tokio = { version = "1.32.0", features = ["full"] } tokio-util = "0.7.8" -toml = "0.7.6" +toml = "0.8.0" tracing = "0.1.37" tracing-error = "0.2.0" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } @@ -68,6 +70,6 @@ incremental = true lto = true [build-dependencies] -clap = { version = "4.4.2", features = ["derive"] } -clap_complete = "4.4.0" +clap = { version = "4.4.4", features = ["derive"] } +clap_complete = "4.4.1" shlex = "1.1.0" diff --git a/build.rs b/build.rs index 02f2e60..b7686f4 100644 --- a/build.rs +++ b/build.rs @@ -19,14 +19,9 @@ fn run_pandoc() -> Result { } fn get_commit_hash() { - let git_output = std::process::Command::new("git") - .args(["rev-parse", "--git-dir"]) - .output() - .ok(); + let git_output = std::process::Command::new("git").args(["rev-parse", "--git-dir"]).output().ok(); let git_dir = git_output.as_ref().and_then(|output| { - std::str::from_utf8(&output.stdout) - .ok() - .and_then(|s| s.strip_suffix('\n').or_else(|| s.strip_suffix("\r\n"))) + std::str::from_utf8(&output.stdout).ok().and_then(|s| s.strip_suffix('\n').or_else(|| s.strip_suffix("\r\n"))) }); // Tell cargo to rebuild if the head or any relevant refs change. @@ -47,13 +42,9 @@ fn get_commit_hash() { } } - let git_output = std::process::Command::new("git") - .args(["describe", "--always", "--tags", "--long", "--dirty"]) - .output() - .ok(); - let git_info = git_output - .as_ref() - .and_then(|output| std::str::from_utf8(&output.stdout).ok().map(str::trim)); + let git_output = + std::process::Command::new("git").args(["describe", "--always", "--tags", "--long", "--dirty"]).output().ok(); + let git_info = git_output.as_ref().and_then(|output| std::str::from_utf8(&output.stdout).ok().map(str::trim)); let cargo_pkg_version = env!("CARGO_PKG_VERSION"); // Default git_describe to cargo_pkg_version @@ -76,14 +67,13 @@ fn get_commit_hash() { fn main() { get_commit_hash(); - let mut app = generate_cli_app(); - let name = app.get_name().to_string(); - let outdir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("completions/"); - dbg!(&outdir); - generate_to(Bash, &mut app, &name, &outdir).unwrap(); - generate_to(Zsh, &mut app, &name, &outdir).unwrap(); - generate_to(Fish, &mut app, &name, &outdir).unwrap(); - generate_to(PowerShell, &mut app, &name, &outdir).unwrap(); + // let mut app = generate_cli_app(); + // let name = app.get_name().to_string(); + // let outdir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("completions/"); + // generate_to(Bash, &mut app, &name, &outdir).unwrap(); + // generate_to(Zsh, &mut app, &name, &outdir).unwrap(); + // generate_to(Fish, &mut app, &name, &outdir).unwrap(); + // generate_to(PowerShell, &mut app, &name, &outdir).unwrap(); if run_pandoc().is_err() { dbg!("Unable to run pandoc to generate man page documentation"); } diff --git a/completions/_taskwarrior-tui b/completions/_taskwarrior-tui index d93eeed..8d7940d 100644 --- a/completions/_taskwarrior-tui +++ b/completions/_taskwarrior-tui @@ -27,6 +27,7 @@ _taskwarrior-tui() { '--help[Print help]' \ '-V[Print version]' \ '--version[Print version]' \ +'::tick-rate:' \ && ret=0 } diff --git a/completions/taskwarrior-tui.bash b/completions/taskwarrior-tui.bash index aefc9b9..1a59296 100644 --- a/completions/taskwarrior-tui.bash +++ b/completions/taskwarrior-tui.bash @@ -19,7 +19,7 @@ _taskwarrior-tui() { case "${cmd}" in taskwarrior__tui) - opts="-d -c -r -h -V --data --config --taskdata --taskrc --report --help --version" + opts="-d -c -r -h -V --data --config --taskdata --taskrc --report --help --version [FLOAT]" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/docs/configuration/advanced.md b/docs/configuration/advanced.md deleted file mode 100644 index 3928193..0000000 --- a/docs/configuration/advanced.md +++ /dev/null @@ -1,113 +0,0 @@ -# Advanced configuration - -`taskwarrior-tui` parses the output of `task show` to get configuration data. This allows -`taskwarrior-tui` to use the same defaults as `taskwarrior` and configure additional options as -required. - -## `taskrc` config file options: - -Other `taskwarrior-tui` configuration options are possible using the user defined attribute feature -of `taskwarrior`. All `taskwarrior-tui` specific configuration options will begin with -`uda.taskwarrior-tui.`. The following is a full list of all the options available and their default -values implemented by `taskwarrior-tui` if not defined in your `taskrc` file. - -```plaintext -uda.taskwarrior-tui.selection.indicator=• -uda.taskwarrior-tui.selection.bold=yes -uda.taskwarrior-tui.selection.italic=no -uda.taskwarrior-tui.selection.dim=no -uda.taskwarrior-tui.selection.blink=no -uda.taskwarrior-tui.selection.reverse=no -uda.taskwarrior-tui.mark.indicator=✔ -uda.taskwarrior-tui.unmark.indicator= -uda.taskwarrior-tui.mark-selection.indicator=⦿ -uda.taskwarrior-tui.unmark-selection.indicator=⦾ -uda.taskwarrior-tui.calendar.months-per-row=4 -uda.taskwarrior-tui.task-report.show-info=true -uda.taskwarrior-tui.task-report.looping=true -uda.taskwarrior-tui.task-report.jump-on-task-add=true -uda.taskwarrior-tui.task-report.prompt-on-undo=false -uda.taskwarrior-tui.task-report.prompt-on-delete=false -uda.taskwarrior-tui.task-report.prompt-on-done=false -uda.taskwarrior-tui.style.report.selection= -uda.taskwarrior-tui.style.context.active=black on rgb444 -uda.taskwarrior-tui.style.calendar.title=black on rgb444 -uda.taskwarrior-tui.style.report.scrollbar=black -uda.taskwarrior-tui.scrollbar.indicator=█ -uda.taskwarrior-tui.style.report.scrollbar.area=white -uda.taskwarrior-tui.scrollbar.area=║ -uda.taskwarrior-tui.task-report.next.filter=$(task show report.next.filter) -uda.taskwarrior-tui.task-report.auto-insert-double-quotes-on-add=true -uda.taskwarrior-tui.task-report.auto-insert-double-quotes-on-annotate=true -uda.taskwarrior-tui.task-report.auto-insert-double-quotes-on-log=true -uda.taskwarrior-tui.task-report.reset-filter-on-esc=true -uda.taskwarrior-tui.context-menu.select-on-move=false -uda.taskwarrior-tui.tabs.change-focus-rotate=false -``` - -The `uda.taskwarrior-tui.task-report.next.filter` variable defines the default view at program -startup. Set this to any preconfigured report (`task reports`), or create your own report in -taskwarrior and specify its name here. - -## commandline options: - -`-r`: specify a report to be shown, overrides `uda.taskwarrior-tui.task-report.next.filter` for this -instance - -## Configure user defined shortcuts: - -You can configure shortcuts to execute custom commands from your `taskwarrior`'s `taskrc` file -(default: `~/.taskrc`). You can do this by mapping a shortcut to an executable file: - -```plaintext -uda.taskwarrior-tui.shortcuts.1=~/.config/taskwarrior-tui/shortcut-scripts/add-personal-tag.sh -uda.taskwarrior-tui.shortcuts.2=~/.config/taskwarrior-tui/shortcut-scripts/sync.sh -... -``` - -The executable file can be placed in any location. - -To make a file executable: - -1. Run `chmod +x /path/to/script` to modify the executable flag. -2. Add `#!/usr/bin/env bash`, `#!/usr/bin/env python` or whatever is appropriate for your script. - -By default, keys `1`-`9` are available to run shortcuts. - -When you hit the shortcut, the script will be executed with the `selected_tasks_uuid` as an -argument: - -```bash -~/.config/taskwarrior-tui/shortcut-scripts/add-personal-tag.sh $selected_tasks_uuid -``` - -For example, you can add the `personal` tag to the currently selected task with the following script -in `~/.config/taskwarrior-tui/shortcut-scripts/add-personal-tag.sh` : - -```plaintext -task rc.bulk=0 rc.confirmation=off rc.dependency.confirmation=off rc.recurrence.confirmation=off "$@" modify +personal -``` - -By default, shortcuts are linked to the `1-9` number row keys. They can be customized as any other -keys through `uda.taskwarrior-tui.keyconfig.shortcut1=`. For example: - -```plaintext -uda.taskwarrior-tui.keyconfig.shortcut1=n -``` - -You can set up shortcuts to run `task sync` or any custom bash script that you'd like. - -## Configure one background task - -You can configure one background task to run periodically: - -```plaintext -uda.taskwarrior-tui.background_process=task sync -uda.taskwarrior-tui.background_process_period=60 -``` - -This will run `task sync` every 60 seconds. If the `background_process` is an empty string -(default), then no process will be run. Only if the `background_process` is defined and if the -`background_process` runs successfully, it'll be run every `background_process_period` number of -seconds (default: 60 seconds). However, if it fails even once it won't be run again till -`taskwarrior-tui` is restarted. diff --git a/docs/configuration/colors.md b/docs/configuration/colors.md deleted file mode 100644 index 4da45ac..0000000 --- a/docs/configuration/colors.md +++ /dev/null @@ -1,26 +0,0 @@ -# Color configuration - -`taskwarrior-tui` reads values from your `taskwarrior`'s `taskrc` file (default: `~/.taskrc`). - -![](https://user-images.githubusercontent.com/1813121/96684390-bf173e80-1338-11eb-971c-ae64233d142e.png) - -For example, `color.active` is used to style the active task. -If you would like to try it, open your `taskrc` file and change `color.active=white on blue`. - -So `color.active` will take precedence over `color.overdue`. You can see what `color.active` is by running `task show color.active` in your favorite shell prompt. - -The following color attributes are supported: - -```plaintext -color.deleted -color.completed -color.active -color.overdue -color.scheduled -color.due.today -color.due -color.blocked -color.blocking -color.recurring -color.tagged -``` diff --git a/docs/configuration/keys.md b/docs/configuration/keys.md deleted file mode 100644 index 4d150ce..0000000 --- a/docs/configuration/keys.md +++ /dev/null @@ -1,32 +0,0 @@ -# Key configuration - -Configure `taskwarrior-tui` using `~/.taskrc`: - -`taskwarrior-tui` reads values from your `taskwarrior`'s `taskrc` file (default: `~/.taskrc`). - -```plaintext -uda.taskwarrior-tui.keyconfig.quit=q -uda.taskwarrior-tui.keyconfig.refresh=r -uda.taskwarrior-tui.keyconfig.go-to-bottom=G -uda.taskwarrior-tui.keyconfig.go-to-top=g -uda.taskwarrior-tui.keyconfig.down=j -uda.taskwarrior-tui.keyconfig.up=k -uda.taskwarrior-tui.keyconfig.page-down=J -uda.taskwarrior-tui.keyconfig.page-up=K -uda.taskwarrior-tui.keyconfig.delete=x -uda.taskwarrior-tui.keyconfig.done=d -uda.taskwarrior-tui.keyconfig.start-stop=s -uda.taskwarrior-tui.keyconfig.quick-tag=t -uda.taskwarrior-tui.keyconfig.undo=u -uda.taskwarrior-tui.keyconfig.edit=e -uda.taskwarrior-tui.keyconfig.modify=m -uda.taskwarrior-tui.keyconfig.shell=! -uda.taskwarrior-tui.keyconfig.log=l -uda.taskwarrior-tui.keyconfig.add=a -uda.taskwarrior-tui.keyconfig.annotate=A -uda.taskwarrior-tui.keyconfig.filter=/ -uda.taskwarrior-tui.keyconfig.zoom=z -uda.taskwarrior-tui.keyconfig.context-menu=c -uda.taskwarrior-tui.keyconfig.next-tab=] -uda.taskwarrior-tui.keyconfig.previous-tab=[ -``` diff --git a/docs/developer/guide.md b/docs/developer/guide.md deleted file mode 100644 index 25c98e4..0000000 --- a/docs/developer/guide.md +++ /dev/null @@ -1,60 +0,0 @@ -# Developer guide - -## Running tests - -```bash -git clone https://github.com/kdheepak/taskwarrior-tui -cd taskwarrior-tui - -git clone github.com/kdheepak/taskwarrior-testdata tests/data -source .env - -cargo test -``` - -## Running debug build - -```bash -cargo run -``` - -## Running release build - -```bash -cargo run --release -``` - -## Testing individual function - -If you want to test the `test_taskwarrior_timing` function in `src/app.rs`: - -```bash -cargo test -- app::tests::test_taskwarrior_timing --nocapture -``` - -## Getting logs - -```bash -export TASKWARRIOR_TUI_LOG_LEVEL=debug -taskwarrior-tui - -# OR - -export TASKWARRIOR_TUI_LOG_LEVEL=trace -cargo run -``` - -## Contributing to documentation - -See `docs/` folder in the repository: - -When you make a PR to the repository, a preview of the documentation is rendered and a link is posted to the PR. - -## Internals of `taskwarrior-tui` - -`taskwarrior-tui` is a state driven terminal user interface. -Keyboard events are read asynchronously and is communicated using channels. -Most of the logic is implemented in `src/app.rs`. -The difference between the previous state and the current state of the TUI is rendered every `Tick` by `tui-rs`. -`app.draw_...` functions are responsible for rendering the UI. -Actions for key presses are taken in [`app.handle_input(&mut self, input: Key)`](https://github.com/kdheepak/taskwarrior-tui/blob/f7f89cbff180f81a3b27112d676d6101b0b552d8/src/app.rs#L1893). diff --git a/docs/faqs.md b/docs/faqs.md deleted file mode 100644 index 8705254..0000000 --- a/docs/faqs.md +++ /dev/null @@ -1,25 +0,0 @@ -# Frequently Asked Questions (FAQs) - -### Does `taskwarrior-tui` show error messages when running shell commands or shortcuts - -`taskwarrior-tui` shows an error prompt for shell if: - -1. the subprocess fails -2. the subprocess succeeds but prints to stdout -3. the subprocess is empty - -`taskwarrior-tui` shows an error prompt for shortcuts if: - -1. the shortcut fails - -If `taskwarrior-tui` encounters a prompt by the subprocess or the shortcut, `taskwarrior-tui` will not prompt the user for input again. -This means, if you want to run a `taskwarrior` command as a shell command, you may want to pass `rc.confirmation=off` in the command. -See the following screencast as an example: - - - -```bash -task rc.confirmation=off context define test project:work -``` - -If you don't add `rc.confirmation=off` in the shell command, `taskwarrior-tui` will command the command but it'll fail because it won't receive any prompt. diff --git a/docs/installation.md b/docs/installation.md deleted file mode 100644 index 4b60d29..0000000 --- a/docs/installation.md +++ /dev/null @@ -1,56 +0,0 @@ - -# Installation - -Unless otherwise specified, you will need to install `taskwarrior` first. See for more information. - -**Manual** ( _Recommended_ ) [![](https://img.shields.io/github/v/tag/kdheepak/taskwarrior-tui)](https://github.com/kdheepak/taskwarrior-tui/releases/latest) [![](https://img.shields.io/github/downloads/kdheepak/taskwarrior-tui/total)](https://github.com/kdheepak/taskwarrior-tui/releases/latest) - -1. Download the tar.gz file for your OS from [the latest release](https://github.com/kdheepak/taskwarrior-tui/releases/latest). -2. Unzip the tar.gz file -3. Run with `./taskwarrior-tui`. - -**Install from source** [![](https://img.shields.io/badge/branch-main-red)](https://github.com/kdheepak/taskwarrior-tui) - -```bash -git clone https://github.com/kdheepak/taskwarrior-tui.git -cd taskwarrior-tui -cargo build --release -``` - -**Using [`brew`](https://brew.sh/)** [![](https://img.shields.io/homebrew/v/taskwarrior-tui)](https://formulae.brew.sh/formula/taskwarrior-tui) [![](https://img.shields.io/homebrew/installs/dy/taskwarrior-tui)](https://formulae.brew.sh/formula/taskwarrior-tui) - - -This installs `task` from `homebrew` as well. - -```bash -brew install taskwarrior-tui -``` - -**Installation for Arch Linux** [![](https://img.shields.io/archlinux/v/community/x86_64/taskwarrior-tui)](https://archlinux.org/packages/community/x86_64/taskwarrior-tui/) [![](https://img.shields.io/aur/version/taskwarrior-tui-git)](https://aur.archlinux.org/packages/taskwarrior-tui-git/) - -Use [pacman](https://wiki.archlinux.org/index.php/Pacman) to install it from the [community repository](https://archlinux.org/packages/community/x86_64/taskwarrior-tui/): - -```bash -pacman -S taskwarrior-tui -``` - -Or use your favorite [AUR helper](https://wiki.archlinux.org/index.php/AUR_helpers) to download the [git](https://aur.archlinux.org/packages/taskwarrior-tui-git/) package maintained by [**@loki7990**](https://github.com/loki7990). For example: - -```bash -yay -S taskwarrior-tui-git # build from source -``` - -**Using [`snap`](https://snapcraft.io/)** [![](https://snapcraft.io/taskwarrior-tui/badge.svg)](https://snapcraft.io/taskwarrior-tui) - -```bash -snap install taskwarrior-tui -``` - -**Using [`zdharma-continuum/zinit`](https://github.com/zdharma-continuum/zinit)** [![](https://img.shields.io/github/v/tag/kdheepak/taskwarrior-tui)](https://github.com/kdheepak/taskwarrior-tui/releases/latest) - -Add the following to your `~/.zshrc`: - -```zsh -zinit ice wait:2 lucid extract"" from"gh-r" as"command" mv"taskwarrior-tui* -> tt" -zinit load kdheepak/taskwarrior-tui -``` diff --git a/docs/keybindings.md b/docs/keybindings.md deleted file mode 100644 index 3f46856..0000000 --- a/docs/keybindings.md +++ /dev/null @@ -1,121 +0,0 @@ -# Default Keybindings - -Keybindings: - - Esc: - Exit current action - - ]: Next view - Go to next view - - [: Previous view - Go to previous view - -Keybindings for task report: - - /: task {string} - Filter task report - - a: task add {string} - Add new task - - d: task {selected} done - Mark task as done - - e: task {selected} edit - Open selected task in editor - - j: {selected+=1} - Move down in task report - - k: {selected-=1} - Move up in task report - - J: {selected+=pageheight} - Move page down in task report - - K: {selected-=pageheight} - Move page up in task report - - g: {selected=first} - Go to top - - G: {selected=last} - Go to bottom - - l: task log {string} - Log new task - - m: task {selected} modify {string} - Modify selected task - - q: exit - Quit - - s: task {selected} start/stop - Toggle start and stop - - t: task {selected} +{tag}/-{tag} - Toggle {uda.taskwarrior-tui.quick-tag.name} (default: `next`) - - u: task undo - Undo - - v: {toggle mark on selected} - Toggle mark on selected - - V: {toggle marks on all tasks} - Toggle marks on all tasks in current filter report - - x: task {selected} delete - Delete - - z: toggle task info - Toggle task info view - - A: task {selected} annotate {string} - Annotate current task - - Ctrl-e: scroll down task details - Scroll task details view down one line - - Ctrl-y: scroll up task details - Scroll task details view up one line - - !: {string} - Custom shell command - - 1-9: {string} - Run user defined shortcuts - - :: {task id} - Jump to task id - - c: context switcher menu - Open context switcher menu - - ?: help - Help menu - -Keybindings for filter / command prompt: - - Ctrl + f | Right: move forward - Move forward one character - - Ctrl + b | Left: move backward - Move backward one character - - Ctrl + h | Backspace: backspace - Delete one character back - - Ctrl + d | Delete: delete - Delete one character forward - - Ctrl + a | Home: home - Go to the beginning of line - - Ctrl + e | End: end - Go to the end of line - - Ctrl + k: delete to end - Delete to the end of line - - Ctrl + u: delete to beginning - Delete to the beginning of line - - Ctrl + w: delete previous word - Delete previous word - - Alt + d: delete next word - Delete next word - - Alt + b: move to previous word - Move to previous word - - Alt + f: move to next word - Move to next word - - Alt + t: transpose words - Transpose words - - Up: scroll history - Go backward in history matching from beginning of line to cursor - - Down: scroll history - Go forward in history matching from beginning of line to cursor - - TAB | Ctrl + n: tab complete - Open tab completion and selection first element OR cycle to next element - - BACKTAB | Ctrl + p: tab complete - Cycle to previous element - -Keybindings for context switcher: - - j: {selected+=1} - Move forward a context - - k: {selected-=1} - Move back a context - - Enter: task context {selected} - Select highlighted context - -Keybindings for calendar: - - j: {selected+=1} - Move forward a year in calendar - - k: {selected-=1} - Move back a year in calendar - - J: {selected+=10} - Move forward a decade in calendar - - K: {selected-=10} - Move back a decade in calendar diff --git a/docs/quick_start.md b/docs/quick_start.md deleted file mode 100644 index 75050af..0000000 --- a/docs/quick_start.md +++ /dev/null @@ -1,16 +0,0 @@ -# Quick Start - -1. Install `taskwarrior` and `taskwarrior-tui`. -2. Run the following in a shell. - `$ taskwarrior-tui` -3. Use `vim` like keys to navigate your task list. Press `?` for more information. - -_Tip_: Alias `tt` to `taskwarrior-tui`. - -Add the following to your dotfiles (e.g. `~/.bashrc`, `~/.zshrc`): - -``` -alias tt="taskwarrior-tui" -``` - -![](https://user-images.githubusercontent.com/1813121/89620056-4ed64200-d84c-11ea-9153-9e08bc26d3b4.gif) diff --git a/docs/taskwarrior-tui.1 b/docs/taskwarrior-tui.1 deleted file mode 100644 index 942e5e0..0000000 --- a/docs/taskwarrior-tui.1 +++ /dev/null @@ -1,230 +0,0 @@ -.\" Automatically generated by Pandoc 3.1.6 -.\" -.\" Define V font for inline verbatim, using C font in formats -.\" that render this, and otherwise B font. -.ie "\f[CB]x\f[]"x" \{\ -. ftr V B -. ftr VI BI -. ftr VB B -. ftr VBI BI -.\} -.el \{\ -. ftr V CR -. ftr VI CI -. ftr VB CB -. ftr VBI CBI -.\} -.TH "taskwarrior-tui" "1" "" "" "" -.hy -.SH NAME -.PP -taskwarrior-tui \[em] A terminal user interface for taskwarrior -(https://github.com/kdheepak/taskwarrior-tui) -.SH SYNOPSIS -.PP -\f[V]taskwarrior-tui\f[R] -.PP -\f[B]\f[VB]taskwarrior-tui\f[B]\f[R] is a terminal user interface for -\f[V]taskwarrior\f[R]. -.SH EXAMPLES -.TP -\f[V]taskwarrior-tui\f[R] -Starts a terminal user interface for \f[V]taskwarrior\f[R]. -.TP -\f[V]alias tt=taskwarrior-tui\f[R] -Add the above to your dotfiles to use \f[V]tt\f[R] to start -\f[V]taskwarrior-tui\f[R]. -.SH KEYBINDINGS -.PP -Keybindings: -.TP -\f[V]Esc\f[R] -Exit current action -.TP -\f[V]]\f[R] -Next view - Go to next view -.TP -\f[V][\f[R] -Previous view - Go to previous view -.PP -Keybindings for task report: -.TP -\f[V]/\f[R] -task {string} - Filter task report -.TP -\f[V]a\f[R] -task add {string} - Add new task -.TP -\f[V]d\f[R] -task {selected} done - Mark task as done -.TP -\f[V]e\f[R] -task {selected} edit - Open selected task in editor -.TP -\f[V]j\f[R] -{selected+=1} - Move down in task report -.TP -\f[V]k\f[R] -{selected-=1} - Move up in task report -.TP -\f[V]J\f[R] -{selected+=pageheight} - Move page down in task report -.TP -\f[V]K\f[R] -{selected-=pageheight} - Move page up in task report -.TP -\f[V]g\f[R] -{selected=first} - Go to top -.TP -\f[V]G\f[R] -{selected=last} - Go to bottom -.TP -\f[V]l\f[R] -task log {string} - Log new task -.TP -\f[V]m\f[R] -task {selected} modify {string} - Modify selected task -.TP -\f[V]q\f[R] -exit - Quit -.TP -\f[V]s\f[R] -task {selected} start/stop - Toggle start and stop -.TP -\f[V]t\f[R] -task {selected} +{tag}/-{tag} - Toggle -{uda.taskwarrior-tui.quick-tag.name} (default: \f[V]next\f[R]) -.TP -\f[V]u\f[R] -task undo - Undo -.TP -\f[V]v\f[R] -{toggle mark on selected} - Toggle mark on selected -.TP -\f[V]V\f[R] -{toggle marks on all tasks} - Toggle marks on all tasks in current -filter report -.TP -\f[V]x\f[R] -task delete {selected} - Delete -.TP -\f[V]z\f[R] -toggle task info - Toggle task info view -.TP -\f[V]A\f[R] -task {selected} annotate {string} - Annotate current task -.TP -Ctrl-e -scroll down task details - Scroll task details view down one line -.TP -Ctrl-y -scroll up task details - Scroll task details view up one line -.TP -\f[V]!\f[R] -{string} - Custom shell command -.TP -\f[V]1-9\f[R] -{string} - Run user defined shortcuts -.TP -\f[V]:\f[R] -{task id} - Jump to task id -.TP -\f[V]c\f[R] -context switcher menu - Open context switcher menu -.TP -\f[V]?\f[R] -help - Help menu -.PP -Keybindings for filter / command prompt: -.TP -\f[V]Ctrl + f | Right\f[R] -move forward - Move forward one character -.TP -\f[V]Ctrl + b | Left\f[R] -move backward - Move backward one character -.TP -\f[V]Ctrl + h | Backspace\f[R] -backspace - Delete one character back -.TP -\f[V]Ctrl + d | Delete\f[R] -delete - Delete one character forward -.TP -\f[V]Ctrl + a | Home\f[R] -home - Go to the beginning of line -.TP -\f[V]Ctrl + e | End\f[R] -end - Go to the end of line -.TP -\f[V]Ctrl + k\f[R] -delete to end - Delete to the end of line -.TP -\f[V]Ctrl + u\f[R] -delete to beginning - Delete to the beginning of line -.TP -\f[V]Ctrl + w\f[R] -delete previous word - Delete previous word -.TP -\f[V]Alt + d\f[R] -delete next word - Delete next word -.TP -\f[V]Alt + b\f[R] -move to previous word - Move to previous word -.TP -\f[V]Alt + f\f[R] -move to next word - Move to next word -.TP -\f[V]Alt + t\f[R] -transpose words - Transpose words -.TP -\f[V]Up\f[R] -scroll history - Go backward in history matching from beginning of line -to cursor -.TP -\f[V]Down\f[R] -scroll history - Go forward in history matching from beginning of line -to cursor -.TP -\f[V]TAB | Ctrl + n\f[R] -tab complete - Open tab completion and selection first element OR cycle -to next element -.TP -\f[V]BACKTAB | Ctrl + p\f[R] -tab complete - Cycle to previous element -.PP -Keybindings for context switcher: -.TP -\f[V]j\f[R] -{selected+=1} - Move forward a context -.TP -\f[V]k\f[R] -{selected-=1} - Move back a context -.PP -Keybindings for calendar: -.TP -\f[V]j\f[R] -{selected+=1} - Move forward a year in calendar -.TP -\f[V]k\f[R] -{selected-=1} - Move back a year in calendar -.TP -\f[V]J\f[R] -{selected+=10} - Move forward a decade in calendar -.TP -\f[V]K\f[R] -{selected-=10} - Move back a decade in calendar -.SH EXIT STATUSES -.TP -0 -If everything goes OK. -.SH AUTHOR -.PP -\f[V]taskwarrior-tui\f[R] is maintained by Dheepak `kdheepak' -Krishnamurthy and other contributors. -.PP -\f[B]Source code:\f[R] -\f[V]https://github.com/kdheepak/taskwarrior-tui/\f[R] -.PD 0 -.P -.PD -\f[B]Contributors:\f[R] -\f[V]https://github.com/kdheepak/taskwarrior-tui/graphs/contributors\f[R] diff --git a/docs/taskwarrior-tui.1.md b/docs/taskwarrior-tui.1.md deleted file mode 100644 index b9aa862..0000000 --- a/docs/taskwarrior-tui.1.md +++ /dev/null @@ -1,225 +0,0 @@ -% taskwarrior-tui(1) - - - - - - -NAME -==== - -taskwarrior-tui — A terminal user interface for taskwarrior (https://github.com/kdheepak/taskwarrior-tui) - - -SYNOPSIS -======== - -`taskwarrior-tui` - -**`taskwarrior-tui`** is a terminal user interface for `taskwarrior`. - - -EXAMPLES -======== - -`taskwarrior-tui` -: Starts a terminal user interface for `taskwarrior`. - -`alias tt=taskwarrior-tui` -: Add the above to your dotfiles to use `tt` to start `taskwarrior-tui`. - -KEYBINDINGS -=========== - - -Keybindings: - -`Esc` -: Exit current action - -`]` -: Next view - Go to next view - -`[` -: Previous view - Go to previous view - - -Keybindings for task report: - -`/` -: task {string} - Filter task report - -`a` -: task add {string} - Add new task - -`d` -: task {selected} done - Mark task as done - -`e` -: task {selected} edit - Open selected task in editor - -`j` -: {selected+=1} - Move down in task report - -`k` -: {selected-=1} - Move up in task report - -`J` -: {selected+=pageheight} - Move page down in task report - -`K` -: {selected-=pageheight} - Move page up in task report - -`g` -: {selected=first} - Go to top - -`G` -: {selected=last} - Go to bottom - -`l` -: task log {string} - Log new task - -`m` -: task {selected} modify {string} - Modify selected task - -`q` -: exit - Quit - -`s` -: task {selected} start/stop - Toggle start and stop - -`t` -: task {selected} +{tag}/-{tag} - Toggle {uda.taskwarrior-tui.quick-tag.name} (default: `next`) - -`u` -: task undo - Undo - -`v` -: {toggle mark on selected} - Toggle mark on selected - -`V` -: {toggle marks on all tasks} - Toggle marks on all tasks in current filter report - -`x` -: task delete {selected} - Delete - -`z` -: toggle task info - Toggle task info view - -`A` -: task {selected} annotate {string} - Annotate current task - -Ctrl-e -: scroll down task details - Scroll task details view down one line - -Ctrl-y -: scroll up task details - Scroll task details view up one line - - -`!` -: {string} - Custom shell command - -`1-9` -: {string} - Run user defined shortcuts - -`:` -: {task id} - Jump to task id - -`c` -: context switcher menu - Open context switcher menu - -`?` -: help - Help menu - - -Keybindings for filter / command prompt: - -`Ctrl + f | Right` -: move forward - Move forward one character - -`Ctrl + b | Left` -: move backward - Move backward one character - -`Ctrl + h | Backspace` -: backspace - Delete one character back - -`Ctrl + d | Delete` -: delete - Delete one character forward - -`Ctrl + a | Home` -: home - Go to the beginning of line - -`Ctrl + e | End` -: end - Go to the end of line - -`Ctrl + k` -: delete to end - Delete to the end of line - -`Ctrl + u` -: delete to beginning - Delete to the beginning of line - -`Ctrl + w` -: delete previous word - Delete previous word - -`Alt + d` -: delete next word - Delete next word - -`Alt + b` -: move to previous word - Move to previous word - -`Alt + f` -: move to next word - Move to next word - -`Alt + t` -: transpose words - Transpose words - -`Up` -: scroll history - Go backward in history matching from beginning of line to cursor - -`Down` -: scroll history - Go forward in history matching from beginning of line to cursor - -`TAB | Ctrl + n` -: tab complete - Open tab completion and selection first element OR cycle to next element - -`BACKTAB | Ctrl + p` -: tab complete - Cycle to previous element - - -Keybindings for context switcher: - -`j` -: {selected+=1} - Move forward a context - -`k` -: {selected-=1} - Move back a context - - -Keybindings for calendar: - -`j` -: {selected+=1} - Move forward a year in calendar - -`k` -: {selected-=1} - Move back a year in calendar - -`J` -: {selected+=10} - Move forward a decade in calendar - -`K` -: {selected-=10} - Move back a decade in calendar - -EXIT STATUSES -============= - -0 -: If everything goes OK. - - -AUTHOR -====== - -`taskwarrior-tui` is maintained by Dheepak ‘kdheepak’ Krishnamurthy and other contributors. - -**Source code:** `https://github.com/kdheepak/taskwarrior-tui/` \ -**Contributors:** `https://github.com/kdheepak/taskwarrior-tui/graphs/contributors` diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 0a57413..0000000 --- a/mkdocs.yml +++ /dev/null @@ -1,57 +0,0 @@ -site_name: taskwarrior-tui - A terminal user interface for taskwarrior -site_url: https://kdheepak.com/taskwarrior-tui -site_author: Dheepak Krishnamurthy - -# Source code repository -repo_name: kdheepak/taskwarrior-tui -repo_url: https://github.com/kdheepak/taskwarrior-tui - -# Copyright -copyright: Copyright © 2021 Dheepak Krishnamurthy - -markdown_extensions: - - admonition - - attr_list - - footnotes - - pymdownx.highlight - - pymdownx.superfences - -# Plugins -plugins: - - search - - exclude: - glob: - - '*.1' - -# Theme -theme: - name: material - favicon: assets/favicon.png - features: - - navigation.tabs - include_search_page: false - language: en - palette: - primary: deep blue - search_index_only: true - -# Customization -extra: - social: - - icon: fontawesome/brands/github - link: https://github.com/kdheepak - -# Page tree -nav: - - Home: index.md - - Getting Started: - - Installation: installation.md - - Quick Start: quick_start.md - - Keybindings: keybindings.md - - FAQs: faqs.md - - Configuration: - - Key configuration: configuration/keys.md - - Color configuration: configuration/colors.md - - Advanced: configuration/advanced.md - - Developer Guide: - - Guide: developer/guide.md diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml deleted file mode 100644 index 9cdd8e6..0000000 --- a/snap/snapcraft.yaml +++ /dev/null @@ -1,33 +0,0 @@ -name: taskwarrior-tui -base: core20 -version: git -summary: A terminal user interface for taskwarrior -description: | - A terminal user interface for taskwarrior to manage your tasks efficiently. - -grade: stable -confinement: strict - -apps: - task: - command: usr/local/bin/task - environment: - PATH: $SNAP/usr/bin:$SNAP/usr/local/bin/:$PATH:$SNAP/bin/:$PATH - - taskwarrior-tui: - command: bin/taskwarrior-tui - environment: - PATH: $SNAP/usr/bin:$SNAP/usr/local/bin/:$PATH:$SNAP/bin/:$PATH - -parts: - task: - plugin: cmake - source: https://github.com/GothenburgBitFactory/taskwarrior.git - source-type: git - build-packages: - - build-essential - - gnutls-dev - - uuid-dev - taskwarrior-tui: - plugin: rust - source: . diff --git a/src/action.rs b/src/action.rs deleted file mode 100644 index 697ceae..0000000 --- a/src/action.rs +++ /dev/null @@ -1,39 +0,0 @@ -use serde_derive::{Deserialize, Serialize}; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum Action { - Tick, - Error(String), - Quit, - Refresh, - GotoBottom, - GotoTop, - GotoPageBottom, - GotoPageTop, - Down, - Up, - PageDown, - PageUp, - Delete, - Done, - ToggleStartStop, - ToggleMark, - ToggleMarkAll, - QuickTag, - Select, - SelectAll, - Undo, - Edit, - Shell, - Help, - ToggleZoom, - Context, - Next, - Previous, - Shortcut(usize), - Modify, - Log, - Annotate, - Filter, - Add, -} diff --git a/src/app.rs b/src/app.rs deleted file mode 100644 index 950fee4..0000000 --- a/src/app.rs +++ /dev/null @@ -1,2752 +0,0 @@ -use std::{ - borrow::Borrow, - cmp::Ordering, - collections::{HashMap, HashSet}, - convert::TryInto, - fs, io, - io::{Read, Write}, - path::{Path, PathBuf}, - sync::{mpsc, Arc, Mutex}, - time::{Duration, Instant, SystemTime}, -}; - -use chrono::{DateTime, Datelike, FixedOffset, Local, NaiveDate, NaiveDateTime, TimeZone, Timelike}; -use color_eyre::eyre::{anyhow, Context, Result}; -use crossterm::{ - event::{DisableMouseCapture, EnableMouseCapture, KeyCode, KeyEvent, KeyModifiers}, - execute, - style::style, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, -}; -use futures::SinkExt; -use lazy_static::lazy_static; -use log::{debug, error, info, log_enabled, trace, warn, Level, LevelFilter}; -use ratatui::{ - backend::{Backend, CrosstermBackend}, - layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}, - style::{Color, Modifier, Style}, - symbols::bar::FULL, - terminal::Frame, - text::{Line, Span, Text}, - widgets::{Block, BorderType, Borders, Clear, Gauge, LineGauge, List, ListItem, Paragraph, Tabs, Wrap}, - Terminal, -}; -use regex::Regex; -use rustyline::{history::SearchDirection as HistoryDirection, At, Editor, Word}; -use task_hookrs::{date::Date, import::import, project::Project, status::TaskStatus, task::Task}; -use tui_input::{backend::crossterm::EventHandler, Input}; -use unicode_segmentation::{Graphemes, UnicodeSegmentation}; -use unicode_width::UnicodeWidthStr; -use uuid::Uuid; -use versions::Versioning; - -use crate::{ - action::Action, - calendar::Calendar, - completion::{get_start_word_under_cursor, CompletionList}, - config, - config::Config, - help::Help, - history::HistoryContext, - keyconfig::KeyConfig, - pane::{ - context::{ContextDetails, ContextsState}, - project::ProjectsState, - Pane, - }, - scrollbar::Scrollbar, - table::{Row, Table, TableMode, TableState}, - task_report::TaskReportTable, - trace_dbg, - traits::TaskwarriorTuiTask, - tui::{self, Event}, - ui, - utils::{self, get_data_dir}, -}; - -const MAX_LINE: usize = 4096; - -lazy_static! { - static ref START_TIME: Instant = Instant::now(); - static ref TASKWARRIOR_VERSION_SUPPORTED: Versioning = Versioning::new("2.6.0").unwrap(); -} - -#[derive(Debug)] -pub enum DateState { - BeforeToday, - EarlierToday, - LaterToday, - AfterToday, - NotDue, -} - -pub fn get_date_state(reference: &Date, due: usize) -> DateState { - let now = Local::now(); - let reference = TimeZone::from_utc_datetime(now.offset(), reference); - let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc()); - - if reference.date_naive() < now.date_naive() { - return DateState::BeforeToday; - } - - if reference.date_naive() == now.date_naive() { - return if reference.time() < now.time() { - DateState::EarlierToday - } else { - DateState::LaterToday - }; - } - - if reference <= now + chrono::Duration::days(7) { - DateState::AfterToday - } else { - DateState::NotDue - } -} - -fn get_offset_hour_minute() -> (&'static str, i32, i32) { - let off = Local::now().offset().local_minus_utc(); - let sym = if off >= 0 { "+" } else { "-" }; - let off = off.abs(); - let h = if off > 60 * 60 { off / 60 / 60 } else { 0 }; - let m = if (off - ((off / 60 / 60) * 60 * 60)) > 60 { - (off - ((off / 60 / 60) * 60 * 60)) / 60 - } else { - 0 - }; - (sym, h, m) -} - -fn get_formatted_datetime(date: &Date) -> String { - let now = Local::now(); - let date = TimeZone::from_utc_datetime(now.offset(), date); - let (sym, h, m) = get_offset_hour_minute(); - format!( - "'{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{:02}:{:02}'", - date.year(), - date.month(), - date.day(), - date.hour(), - date.minute(), - date.second(), - sym, - h, - m, - ) -} - -fn centered_rect(r: Rect, percent_x: u16, percent_y: u16) -> Rect { - let popup_layout = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Percentage((100 - percent_y) / 2), - Constraint::Percentage(percent_y), - Constraint::Percentage((100 - percent_y) / 2), - ] - .as_ref(), - ) - .split(r); - - Layout::default() - .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Percentage((100 - percent_x) / 2), - Constraint::Percentage(percent_x), - Constraint::Percentage((100 - percent_x) / 2), - ] - .as_ref(), - ) - .split(popup_layout[1])[1] -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Mode { - TaskReport, - TaskFilter, - TaskAdd, - TaskAnnotate, - TaskSubprocess, - TaskLog, - TaskModify, - TaskHelpPopup, - TaskContextMenu, - TaskJump, - TaskDeletePrompt, - TaskUndoPrompt, - TaskDonePrompt, - TaskError, - Projects, - Calendar, -} - -pub struct TaskwarriorTui { - pub tick_rate: u64, - pub should_quit: bool, - pub dirty: bool, - pub task_table_state: TableState, - pub current_context_filter: String, - pub current_context: String, - pub command: Input, - pub filter: Input, - pub modify: Input, - pub tasks: Vec, - pub all_tasks: Vec, - pub task_details: HashMap, - pub marked: HashSet, - pub current_selection: usize, - pub current_selection_uuid: Option, - pub current_selection_id: Option, - pub task_report_table: TaskReportTable, - pub calendar_year: i32, - pub mode: Mode, - pub previous_mode: Option, - pub config: Config, - pub task_report_show_info: bool, - pub task_report_height: u16, - pub task_details_scroll: u16, - pub help_popup: Help, - pub last_export: Option, - pub keyconfig: KeyConfig, - pub terminal_width: u16, - pub terminal_height: u16, - pub filter_history: HistoryContext, - pub command_history: HistoryContext, - pub history_status: Option, - pub completion_list: CompletionList, - pub show_completion_pane: bool, - pub report: String, - pub projects: ProjectsState, - pub contexts: ContextsState, - pub task_version: Versioning, - pub error: Option, - pub requires_redraw: bool, - pub changes: utils::Changeset, -} - -impl TaskwarriorTui { - pub fn new(report: &str) -> Result { - let output = std::process::Command::new("task") - .arg("rc.color=off") - .arg("rc._forcecolor=off") - .arg("rc.defaultwidth=0") - .arg("show") - .output() - .context("Unable to run `task show`.")?; - - if !output.status.success() { - let output = std::process::Command::new("task") - .arg("diagnostics") - .output() - .context("Unable to run `task diagnostics`.")?; - return Err(anyhow!( - "Unable to run `task show`.\n{}\n{}\nPlease check your configuration or open a issue on github.", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )); - } - - let data = String::from_utf8_lossy(&output.stdout); - let c = Config::new()?; - let kc = KeyConfig::new(&data)?; - - let output = std::process::Command::new("task") - .arg("--version") - .output() - .context("Unable to run `task --version`")?; - - let task_version = - Versioning::new(String::from_utf8_lossy(&output.stdout).trim()).ok_or(anyhow!("Unable to get version string"))?; - - let (w, h) = crossterm::terminal::size().unwrap_or((50, 15)); - - let data_dir = get_data_dir(); - - let mut app = Self { - tick_rate: c.uda_tick_rate, - should_quit: false, - dirty: true, - task_table_state: TableState::default(), - tasks: vec![], - all_tasks: vec![], - task_details: HashMap::new(), - marked: HashSet::new(), - current_selection: 0, - current_selection_uuid: None, - current_selection_id: None, - current_context_filter: "".to_string(), - current_context: "".to_string(), - command: Input::default(), - filter: Input::default(), - modify: Input::default(), - mode: Mode::TaskReport, - previous_mode: None, - task_report_height: 0, - task_details_scroll: 0, - task_report_show_info: c.uda_task_report_show_info, - config: c, - task_report_table: TaskReportTable::new(&data, report)?, - calendar_year: Local::now().year(), - help_popup: Help::new(), - last_export: None, - keyconfig: kc, - terminal_width: w, - terminal_height: h, - filter_history: HistoryContext::new("filter.history", data_dir.clone()), - command_history: HistoryContext::new("command.history", data_dir.clone()), - history_status: None, - completion_list: CompletionList::with_items(vec![]), - show_completion_pane: false, - report: report.to_string(), - projects: ProjectsState::new(), - contexts: ContextsState::new(), - task_version, - error: None, - requires_redraw: false, - changes: utils::Changeset::default(), - }; - - app.filter = app.filter.with_value(app.config.filter.clone()); - - app.task_report_table.date_time_vague_precise = app.config.uda_task_report_date_time_vague_more_precise; - - // app.update(true)?; - - app.filter_history.load()?; - app.filter_history.add(app.filter.value()); - app.command_history.load()?; - app.task_background(); - - if app.task_version < *TASKWARRIOR_VERSION_SUPPORTED { - app.error = Some(format!( - "Found taskwarrior version {} but taskwarrior-tui works with taskwarrior>={}", - app.task_version, *TASKWARRIOR_VERSION_SUPPORTED - )); - app.mode = Mode::TaskError; - } - - Ok(app) - } - - pub async fn run(&mut self) -> Result<()> { - let mut tui = tui::Tui::new()?; - tui.tick_rate((self.tick_rate as usize, self.tick_rate as usize)); - tui.enter()?; - - let mut events: Vec = Vec::new(); - // let mut ticker = 0; - - loop { - if self.requires_redraw { - let s = tui.size()?; - tui.resize(s)?; - self.requires_redraw = false; - } - if let Some(event) = tui.next().await { - let mut maybe_action = match event { - Event::Quit => Some(Action::Quit), - Event::Error => Some(Action::Error("Received event error".into())), - Event::Closed => Some(Action::Quit), - Event::Tick => { - events.clear(); - Some(Action::Tick) - } - Event::Key(key_event) => { - events.push(key_event); - self.handle_event(&events)? - } - Event::Mouse(_) => None, - Event::Resize(x, y) => None, - Event::Render => { - tui.draw(|f| self.draw(f))?; - None - } - Event::FocusGained => None, - Event::FocusLost => None, - Event::Paste(s) => None, - }; - while let Some(action) = maybe_action { - maybe_action = self.update(action)?; - } - } - - if self.should_quit { - break; - } - } - tui.exit()?; - Ok(()) - } - - pub fn update(&mut self, action: Action) -> Result> { - if let Action::Quit = action { - self.should_quit = true; - return Ok(None); - } - Ok(None) - } - - pub fn reset_command(&mut self) { - self.command.reset() - } - - pub fn get_context(&mut self) -> Result<()> { - let output = std::process::Command::new("task") - .arg("_get") - .arg("rc.context") - .output()?; - self.current_context = String::from_utf8_lossy(&output.stdout).to_string(); - self.current_context = self.current_context.strip_suffix('\n').unwrap_or("").to_string(); - - // support new format for context - let output = std::process::Command::new("task") - .arg("_get") - .arg(format!("rc.context.{}.read", self.current_context)) - .output()?; - self.current_context_filter = String::from_utf8_lossy(&output.stdout).to_string(); - self.current_context_filter = self.current_context_filter.strip_suffix('\n').unwrap_or("").to_string(); - - // If new format is not used, check if old format is used - if self.current_context_filter.is_empty() { - let output = std::process::Command::new("task") - .arg("_get") - .arg(format!("rc.context.{}", self.current_context)) - .output()?; - self.current_context_filter = String::from_utf8_lossy(&output.stdout).to_string(); - self.current_context_filter = self.current_context_filter.strip_suffix('\n').unwrap_or("").to_string(); - } - Ok(()) - } - - pub fn draw(&mut self, f: &mut Frame) { - let rect = f.size(); - self.terminal_width = rect.width; - self.terminal_height = rect.height; - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(1), Constraint::Min(0)]) - .split(f.size()); - - let tab_layout = chunks[0]; - let main_layout = chunks[1]; - - self.draw_tabs(f, tab_layout); - match self.mode { - Mode::Calendar => self.draw_calendar(f, main_layout), - Mode::Projects => self.draw_projects(f, main_layout), - _ => self.draw_task(f, main_layout), - } - } - - fn draw_tabs(&self, f: &mut Frame, layout: Rect) { - let titles: Vec<&str> = vec!["Tasks", "Projects", "Calendar"]; - let tab_names: Vec<_> = titles.into_iter().map(Line::from).collect(); - let selected_tab = match self.mode { - Mode::Projects => 1, - Mode::Calendar => 2, - _ => 0, - }; - let navbar_block = Block::default().style(*self.config.uda_style_navbar); - let context = Line::from(vec![ - Span::from("["), - Span::from(if self.current_context.is_empty() { - "none" - } else { - &self.current_context - }), - Span::from("]"), - ]); - let tabs = Tabs::new(tab_names) - .block(navbar_block.clone()) - .select(selected_tab) - .divider(" ") - .highlight_style(Style::default().add_modifier(Modifier::BOLD)); - let rects = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Min(0), Constraint::Length(context.width() as u16)]) - .split(layout); - - f.render_widget(tabs, rects[0]); - f.render_widget(Paragraph::new(Text::from(context)).block(navbar_block), rects[1]); - } - - pub fn draw_debug(&mut self, f: &mut Frame) { - let area = centered_rect(f.size(), 50, 50); - f.render_widget(Clear, area); - let t = format!("{}", self.current_selection); - let p = - Paragraph::new(Text::from(t)).block(Block::default().borders(Borders::ALL).border_type(BorderType::Rounded)); - f.render_widget(p, area); - } - - pub fn draw_projects(&mut self, f: &mut Frame, rect: Rect) { - let data = self.projects.data.clone(); - let p = Paragraph::new(Text::from(&data[..])); - f.render_widget(p, rect); - } - - fn style_for_project(&self, project: &[String]) -> Style { - let virtual_tag_names_in_precedence = &self.config.rule_precedence_color; - let mut style = Style::default(); - for tag_name in virtual_tag_names_in_precedence.iter().rev() { - match tag_name.as_str() { - "project." => { - let s = self - .config - .color - .get(&format!("color.project.{}", project[0])) - .copied() - .unwrap_or_default(); - style = style.patch(*s); - } - &_ => {} - } - } - style - } - - pub fn draw_calendar(&mut self, f: &mut Frame, layout: Rect) { - let mut c = Calendar::default() - .today_style(*self.config.uda_style_calendar_today) - .year(self.calendar_year) - .date_style(self.get_dates_with_styles()) - .months_per_row(self.config.uda_calendar_months_per_row) - .start_on_monday(self.config.weekstart); - c.title_background_color = self.config.uda_style_calendar_title.bg.unwrap_or(Color::Reset); - f.render_widget(c, layout); - } - - pub fn draw_task(&mut self, f: &mut Frame, layout: Rect) { - let rects = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(0), Constraint::Length(2)].as_ref()) - .split(layout); - - // render task report and task details if required - if self.task_report_show_info { - let split_task_layout = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(rects[0]); - - self.task_report_height = split_task_layout[0].height; - self.draw_task_report(f, split_task_layout[0]); - self.draw_task_details(f, split_task_layout[1]); - } else { - self.task_report_height = rects[0].height; - self.draw_task_report(f, rects[0]); - } - - // calculate selected tasks - let selected = self.current_selection; - let task_ids = if self.tasks.is_empty() { - vec!["0".to_string()] - } else { - match self.task_table_state.mode() { - TableMode::SingleSelection => vec![self.tasks[selected].id().unwrap_or_default().to_string()], - TableMode::MultipleSelection => { - let mut tids = vec![]; - for uuid in &self.marked { - if let Some(t) = self.task_by_uuid(*uuid) { - tids.push(t.id().unwrap_or_default().to_string()); - } - } - tids - } - } - }; - - // render task mode - self.draw_task_mode_action(f, &rects, &task_ids); - } - - fn draw_task_mode_action(&mut self, f: &mut Frame, rects: &[Rect], task_ids: &[String]) { - match self.mode { - Mode::TaskError => { - self.draw_command( - f, - rects[1], - "Press any key to continue.", - ( - Span::styled("Error", Style::default().add_modifier(Modifier::BOLD)), - None, - ), - 0, - false, - self.error.clone(), - ); - let text = self.error.clone().unwrap_or_else(|| "Unknown error.".to_string()); - let title = vec![Span::styled("Error", Style::default().add_modifier(Modifier::BOLD))]; - let rect = centered_rect(f.size(), 90, 60); - f.render_widget(Clear, rect); - let p = Paragraph::new(Text::from(text)) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .title(title), - ) - .wrap(Wrap { trim: true }); - f.render_widget(p, rect); - // draw error pop up - let rects = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(0)].as_ref()) - .split(f.size()); - } - Mode::TaskReport => { - // reset error when entering Action::Report - self.previous_mode = None; - self.error = None; - let position = self.command.visual_cursor(); - self.draw_command( - f, - rects[1], - self.filter.value(), - (Span::raw("Filter Tasks"), self.history_status.as_ref().map(Span::raw)), - self.filter.visual_cursor(), - false, - self.error.clone(), - ); - } - Mode::TaskJump => { - let position = self.command.visual_cursor(); - self.draw_command( - f, - rects[1], - self.command.value(), - ( - Span::styled("Jump to Task", Style::default().add_modifier(Modifier::BOLD)), - None, - ), - position, - true, - self.error.clone(), - ); - } - Mode::TaskFilter => { - let position = self.filter.visual_cursor(); - if self.show_completion_pane { - self.draw_completion_pop_up(f, rects[1], position); - } - self.draw_command( - f, - rects[1], - self.filter.value(), - ( - Span::styled("Filter Tasks", Style::default().add_modifier(Modifier::BOLD)), - self - .history_status - .as_ref() - .map(|s| Span::styled(s, Style::default().add_modifier(Modifier::BOLD))), - ), - position, - true, - self.error.clone(), - ); - } - Mode::TaskLog => { - if self.config.uda_auto_insert_double_quotes_on_log && self.command.value().is_empty() { - self.command = self.command.clone().with_value(r#""""#.to_string()); - }; - let position = self.command.visual_cursor(); - if self.show_completion_pane { - self.draw_completion_pop_up(f, rects[1], position); - } - self.draw_command( - f, - rects[1], - self.command.value(), - ( - Span::styled("Log Task", Style::default().add_modifier(Modifier::BOLD)), - self - .history_status - .as_ref() - .map(|s| Span::styled(s, Style::default().add_modifier(Modifier::BOLD))), - ), - position, - true, - self.error.clone(), - ); - } - Mode::TaskSubprocess => { - let position = self.command.visual_cursor(); - self.draw_command( - f, - rects[1], - self.command.value(), - ( - Span::styled("Shell Command", Style::default().add_modifier(Modifier::BOLD)), - None, - ), - position, - true, - self.error.clone(), - ); - } - Mode::TaskModify => { - let position = self.modify.visual_cursor(); - if self.show_completion_pane { - self.draw_completion_pop_up(f, rects[1], position); - } - let label = if task_ids.len() > 1 { - format!("Modify Tasks {}", task_ids.join(",")) - } else { - format!("Modify Task {}", task_ids.join(",")) - }; - self.draw_command( - f, - rects[1], - self.modify.value(), - ( - Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), - self - .history_status - .as_ref() - .map(|s| Span::styled(s, Style::default().add_modifier(Modifier::BOLD))), - ), - position, - true, - self.error.clone(), - ); - } - Mode::TaskAnnotate => { - if self.config.uda_auto_insert_double_quotes_on_annotate && self.command.value().is_empty() { - self.command = self.command.clone().with_value(r#""""#.to_string()); - }; - let position = self.command.visual_cursor(); - if self.show_completion_pane { - self.draw_completion_pop_up(f, rects[1], position); - } - let label = if task_ids.len() > 1 { - format!("Annotate Tasks {}", task_ids.join(",")) - } else { - format!("Annotate Task {}", task_ids.join(",")) - }; - self.draw_command( - f, - rects[1], - self.command.value(), - ( - Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), - self - .history_status - .as_ref() - .map(|s| Span::styled(s, Style::default().add_modifier(Modifier::BOLD))), - ), - position, - true, - self.error.clone(), - ); - } - Mode::TaskAdd => { - if self.config.uda_auto_insert_double_quotes_on_add && self.command.value().is_empty() { - self.command = self.command.clone().with_value(r#""""#.to_string()); - }; - let position = self.command.visual_cursor(); - if self.show_completion_pane { - self.draw_completion_pop_up(f, rects[1], position); - } - self.draw_command( - f, - rects[1], - self.command.value(), - ( - Span::styled("Add Task", Style::default().add_modifier(Modifier::BOLD)), - self - .history_status - .as_ref() - .map(|s| Span::styled(s, Style::default().add_modifier(Modifier::BOLD))), - ), - position, - true, - self.error.clone(), - ); - } - Mode::TaskHelpPopup => { - self.draw_command( - f, - rects[1], - self.filter.value(), - ("Filter Tasks".into(), None), - self.filter.visual_cursor(), - false, - self.error.clone(), - ); - self.draw_help_popup(f, 80, 90); - } - Mode::TaskContextMenu => { - self.draw_command( - f, - rects[1], - self.filter.value(), - ("Filter Tasks".into(), None), - self.filter.visual_cursor(), - false, - self.error.clone(), - ); - self.draw_context_menu(f, 80, 50); - } - Mode::TaskDonePrompt => { - let label = if task_ids.len() > 1 { - format!("Done Tasks {}?", task_ids.join(",")) - } else { - format!("Done Task {}?", task_ids.join(",")) - }; - let x = match self.keyconfig.done { - KeyCode::Char(c) => c.to_string(), - _ => "Enter".to_string(), - }; - let q = match self.keyconfig.quit { - KeyCode::Char(c) => c.to_string(), - _ => "Esc".to_string(), - }; - self.draw_command( - f, - rects[1], - &format!("Press <{}> to confirm or <{}> to abort.", x, q), - (Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), None), - 0, - false, - self.error.clone(), - ); - } - Mode::TaskDeletePrompt => { - let label = if task_ids.len() > 1 { - format!("Delete Tasks {}?", task_ids.join(",")) - } else { - format!("Delete Task {}?", task_ids.join(",")) - }; - let x = match self.keyconfig.delete { - KeyCode::Char(c) => c.to_string(), - _ => "Enter".to_string(), - }; - let q = match self.keyconfig.quit { - KeyCode::Char(c) => c.to_string(), - _ => "Esc".to_string(), - }; - self.draw_command( - f, - rects[1], - &format!("Press <{}> to confirm or <{}> to abort.", x, q), - (Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), None), - 0, - false, - self.error.clone(), - ); - } - Mode::TaskUndoPrompt => { - let label = "Run `task undo`?"; - let k = match self.keyconfig.undo { - KeyCode::Char(c) => c.to_string(), - _ => "Enter".to_string(), - }; - let q = match self.keyconfig.quit { - KeyCode::Char(c) => c.to_string(), - _ => "Esc".to_string(), - }; - self.draw_command( - f, - rects[1], - &format!("Press <{}> to confirm or <{}> to abort.", k, q), - (Span::styled(label, Style::default().add_modifier(Modifier::BOLD)), None), - 0, - false, - self.error.clone(), - ); - } - _ => {} - } - } - - pub fn get_dates_with_styles(&self) -> Vec<(chrono::NaiveDate, Style)> { - if !self.tasks.is_empty() { - let tasks = &self.tasks; - tasks - .iter() - .filter_map(|t| t.due().map(|d| (d.clone(), self.style_for_task(t)))) - .map(|(d, t)| { - let now = Local::now(); - let reference = TimeZone::from_utc_datetime(now.offset(), &d); - (reference.date_naive(), t) - }) - .collect() - } else { - vec![] - } - } - - fn draw_help_popup(&mut self, f: &mut Frame, percent_x: u16, percent_y: u16) { - let area = centered_rect(f.size(), percent_x, percent_y); - f.render_widget(Clear, area); - - let chunks = Layout::default() - .constraints([Constraint::Max(area.height - 1), Constraint::Max(1)].as_ref()) - .margin(0) - .split(area); - - self.help_popup.scroll = std::cmp::min( - self.help_popup.scroll, - (self.help_popup.text_height as u16).saturating_sub(chunks[0].height - 3), - ); - - let ratio = ((self.help_popup.scroll + chunks[0].height) as f64 / self.help_popup.text_height as f64).min(1.0); - - let gauge = LineGauge::default() - .block(Block::default()) - .gauge_style(Style::default().fg(Color::Gray)) - .ratio(ratio); - - f.render_widget(gauge, chunks[1]); - f.render_widget(&self.help_popup, chunks[0]); - } - - fn draw_context_menu(&mut self, f: &mut Frame, percent_x: u16, percent_y: u16) { - let rects = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(0)].as_ref()) - .split(f.size()); - - let area = centered_rect(f.size(), percent_x, percent_y); - - f.render_widget( - Clear, - area.inner(&Margin { - vertical: 0, - horizontal: 0, - }), - ); - - let (contexts, headers) = self.get_all_contexts(); - - let maximum_column_width = area.width; - let widths = self.calculate_widths(&contexts, &headers, maximum_column_width); - - let selected = self.contexts.table_state.current_selection().unwrap_or_default(); - let header = headers.iter(); - let mut rows = vec![]; - let mut highlight_style = Style::default(); - for (i, context) in contexts.iter().enumerate() { - let mut style = Style::default(); - if &self.contexts.rows[i].active == "yes" { - style = *self.config.uda_style_context_active; - } - rows.push(Row::StyledData(context.iter(), style)); - if i == self.contexts.table_state.current_selection().unwrap_or_default() { - highlight_style = style; - } - } - - let constraints: Vec = widths - .iter() - .map(|i| Constraint::Length((*i).try_into().unwrap_or(maximum_column_width))) - .collect(); - - let highlight_style = highlight_style.add_modifier(Modifier::BOLD); - let t = Table::new(header, rows.into_iter()) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .title(Line::from(vec![Span::styled( - "Context", - Style::default().add_modifier(Modifier::BOLD), - )])), - ) - .header_style( - self - .config - .color - .get("color.label") - .copied() - .unwrap_or_default() - .add_modifier(Modifier::UNDERLINED), - ) - .highlight_style(highlight_style) - .highlight_symbol(&self.config.uda_selection_indicator) - .widths(&constraints); - - f.render_stateful_widget(t, area, &mut self.contexts.table_state); - } - - fn draw_completion_pop_up(&mut self, f: &mut Frame, rect: Rect, cursor_position: usize) { - if self.completion_list.candidates().is_empty() { - self.show_completion_pane = false; - return; - } - // Iterate through all elements in the `items` app and append some debug text to it. - let items: Vec = self - .completion_list - .candidates() - .iter() - .map(|p| { - let lines = vec![Line::from(vec![ - Span::styled(p.3.clone(), Style::default().add_modifier(Modifier::BOLD)), - Span::from(p.4.clone()), - ])]; - ListItem::new(lines) - }) - .collect(); - - // Create a List from all list items and highlight the currently selected one - let items = List::new(items) - .block(Block::default().borders(Borders::NONE).title("")) - .style(*self.config.uda_style_report_completion_pane) - .highlight_style(*self.config.uda_style_report_completion_pane_highlight) - .highlight_symbol(&self.config.uda_selection_indicator); - - let area = f.size(); - - let mut rect = rect; - rect.height = std::cmp::min(area.height / 2, self.completion_list.len() as u16 + 2); - rect.width = std::cmp::min( - area.width / 2, - self - .completion_list - .max_width() - .unwrap_or(40) - .try_into() - .unwrap_or(area.width / 2), - ); - rect.y = rect.y.saturating_sub(rect.height); - if cursor_position as u16 + rect.width >= area.width { - rect.x = area.width - rect.width; - } else { - rect.x = cursor_position as u16; - } - - // We can now render the item list - f.render_widget(Clear, rect); - f.render_stateful_widget(items, rect, &mut self.completion_list.state); - } - - fn draw_command( - &self, - f: &mut Frame, - rect: Rect, - text: &str, - title: (Span, Option), - position: usize, - cursor: bool, - error: Option, - ) { - // f.render_widget(Clear, rect); - if cursor { - f.set_cursor( - std::cmp::min(rect.x + position as u16, rect.x + rect.width.saturating_sub(2)), - rect.y + 1, - ); - } - let rects = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(1), Constraint::Min(0)].as_ref()) - .split(rect); - - // render command title - let mut style = self.config.uda_style_command.0; - if error.is_some() { - style = style.fg(Color::Red); - }; - let title_spans = if let Some(subtitle) = title.1 { - Line::from(vec![title.0, Span::from(" ["), subtitle, Span::from("]")]) - } else { - Line::from(vec![title.0]) - }; - let title = Paragraph::new(Text::from(title_spans)).style(style); - f.render_widget(title, rects[0]); - - // render command - let p = Paragraph::new(Text::from(text)).scroll((0, ((position + 2) as u16).saturating_sub(rects[1].width))); - f.render_widget(p, rects[1]); - } - - fn draw_task_details(&mut self, f: &mut Frame, rect: Rect) { - if self.tasks.is_empty() { - let p = Paragraph::new(Text::from("Task not found")).block(Block::default().borders(Borders::TOP)); - f.render_widget(p, rect); - return; - } - let selected = self.current_selection; - let task_id = self.tasks[selected].id().unwrap_or_default(); - let task_uuid = *self.tasks[selected].uuid(); - - let data = match self.task_details.get(&task_uuid) { - Some(s) => s.clone(), - None => "Loading task details ...".to_string(), - }; - self.task_details_scroll = std::cmp::min( - (data.lines().count() as u16) - .saturating_sub(rect.height) - .saturating_add(2), - self.task_details_scroll, - ); - let p = Paragraph::new(Text::from(&data[..])) - .block(Block::default().borders(Borders::TOP)) - .scroll((self.task_details_scroll, 0)); - f.render_widget(p, rect); - } - - fn task_details_scroll_up(&mut self) { - self.task_details_scroll = self.task_details_scroll.saturating_sub(1); - } - - fn task_details_scroll_down(&mut self) { - self.task_details_scroll = self.task_details_scroll.saturating_add(1); - } - - fn task_by_index(&self, i: usize) -> Option { - let tasks = &self.tasks; - if i >= tasks.len() { - None - } else { - Some(tasks[i].clone()) - } - } - - fn task_by_uuid(&self, uuid: Uuid) -> Option { - let tasks = &self.tasks; - let m = tasks.iter().find(|t| *t.uuid() == uuid); - m.cloned() - } - - fn task_by_id(&self, id: u64) -> Option { - let tasks = &self.tasks; - let m = tasks.iter().find(|t| t.id() == Some(id)); - m.cloned() - } - - fn task_index_by_id(&self, id: u64) -> Option { - let tasks = &self.tasks; - let m = tasks.iter().position(|t| t.id() == Some(id)); - m - } - - fn task_index_by_uuid(&self, uuid: Uuid) -> Option { - let tasks = &self.tasks; - let m = tasks.iter().position(|t| *t.uuid() == uuid); - m - } - - fn style_for_task(&self, task: &Task) -> Style { - let virtual_tag_names_in_precedence = &self.config.rule_precedence_color; - - let mut style = Style::default(); - - for tag_name in virtual_tag_names_in_precedence.iter().rev() { - if tag_name == "uda." || tag_name == "priority" { - if let Some(p) = task.priority() { - let s = self - .config - .color - .get(&format!("color.uda.priority.{}", p)) - .copied() - .unwrap_or_default(); - style = style.patch(s.0); - } - } else if tag_name == "tag." { - if let Some(tags) = task.tags() { - for t in tags { - let color_tag_name = format!("color.tag.{}", t); - let s = self.config.color.get(&color_tag_name).copied().unwrap_or_default(); - style = style.patch(s.0); - } - } - } else if tag_name == "project." { - if let Some(p) = task.project() { - let s = self - .config - .color - .get(&format!("color.project.{}", p)) - .copied() - .unwrap_or_default(); - style = style.patch(s.0); - } - } else if task - .tags() - .unwrap_or(&vec![]) - .contains(&tag_name.to_string().replace('.', "").to_uppercase()) - { - let color_tag_name = format!("color.{}", tag_name); - let s = self.config.color.get(&color_tag_name).copied().unwrap_or_default(); - style = style.patch(s.0); - } - } - - style - } - - pub fn calculate_widths(&self, tasks: &[Vec], headers: &[String], maximum_column_width: u16) -> Vec { - // naive implementation of calculate widths - let mut widths = headers.iter().map(String::len).collect::>(); - - for row in tasks.iter() { - for (i, cell) in row.iter().enumerate() { - widths[i] = std::cmp::max(cell.len(), widths[i]); - } - } - - for (i, header) in headers.iter().enumerate() { - if header == "Description" || header == "Definition" { - // always give description or definition the most room to breath - widths[i] = maximum_column_width as usize; - break; - } - } - for (i, header) in headers.iter().enumerate() { - if i == 0 { - // always give ID a couple of extra for indicator - widths[i] += self.config.uda_selection_indicator.as_str().width(); - // if let TableMode::MultipleSelection = self.task_table_state.mode() { - // widths[i] += 2 - // }; - } - } - - // now start trimming - while (widths.iter().sum::() as u16) >= maximum_column_width - (headers.len()) as u16 { - let index = widths - .iter() - .position(|i| i == widths.iter().max().unwrap_or(&0)) - .unwrap_or_default(); - if widths[index] == 1 { - break; - } - widths[index] -= 1; - } - - widths - } - - fn draw_task_report(&mut self, f: &mut Frame, rect: Rect) { - let (tasks, headers) = self.get_task_report(); - - if tasks.is_empty() { - if !self.current_context.is_empty() { - let context_style = Style::default(); - context_style.add_modifier(Modifier::ITALIC); - } - - f.render_widget(Block::default(), rect); - return; - } - - let maximum_column_width = rect.width; - let widths = self.calculate_widths(&tasks, &headers, maximum_column_width); - - for (i, header) in headers.iter().enumerate() { - if header == "Description" || header == "Definition" { - self.task_report_table.description_width = widths[i] - 1; - break; - } - } - let selected = self.current_selection; - let header = headers.iter(); - let mut rows = vec![]; - let mut highlight_style = Style::default(); - let mut pos = 0; - for (i, task) in tasks.iter().enumerate() { - let style = self.style_for_task(&self.tasks[i]); - if i == selected { - pos = i; - highlight_style = style.patch(self.config.uda_style_report_selection.0); - if self.config.uda_selection_bold { - highlight_style = highlight_style.add_modifier(Modifier::BOLD); - } - if self.config.uda_selection_italic { - highlight_style = highlight_style.add_modifier(Modifier::ITALIC); - } - if self.config.uda_selection_dim { - highlight_style = highlight_style.add_modifier(Modifier::DIM); - } - if self.config.uda_selection_blink { - highlight_style = highlight_style.add_modifier(Modifier::SLOW_BLINK); - } - if self.config.uda_selection_reverse { - highlight_style = highlight_style.add_modifier(Modifier::REVERSED); - } - } - rows.push(Row::StyledData(task.iter(), style)); - } - - let constraints: Vec = widths - .iter() - .map(|i| Constraint::Length((*i).try_into().unwrap_or(maximum_column_width))) - .collect(); - - let t = Table::new(header, rows.into_iter()) - .header_style( - self - .config - .color - .get("color.label") - .copied() - .unwrap_or_default() - .add_modifier(Modifier::UNDERLINED), - ) - .highlight_style(highlight_style) - .highlight_symbol(&self.config.uda_selection_indicator) - .mark_symbol(&self.config.uda_mark_indicator) - .unmark_symbol(&self.config.uda_unmark_indicator) - .widths(&constraints); - - f.render_stateful_widget(t, rect, &mut self.task_table_state); - if tasks.iter().len() as u16 > rect.height.saturating_sub(4) { - let mut widget = Scrollbar::new(pos, tasks.iter().len()); - widget.pos_style = self.config.uda_style_report_scrollbar.0; - widget.pos_symbol = self.config.uda_scrollbar_indicator.clone(); - widget.area_style = self.config.uda_style_report_scrollbar_area.0; - widget.area_symbol = self.config.uda_scrollbar_area.clone(); - f.render_widget(widget, rect); - } - } - - fn get_all_contexts(&self) -> (Vec>, Vec) { - let contexts = self - .contexts - .rows - .iter() - .filter(|c| &c.type_ == "read") - .map(|c| vec![c.name.clone(), c.definition.clone(), c.active.clone()]) - .collect(); - let headers = vec!["Name".to_string(), "Definition".to_string(), "Active".to_string()]; - (contexts, headers) - } - - fn get_task_report(&mut self) -> (Vec>, Vec) { - self.task_report_table.generate_table(&self.tasks); - let (tasks, headers) = self.task_report_table.simplify_table(); - (tasks, headers) - } - - // pub fn update(&mut self, force: bool) -> Result<()> { - // trace!("self.update({:?});", force); - // if force || self.dirty || self.tasks_changed_since(self.last_export).unwrap_or(true) { - // self.get_context()?; - // let task_uuids = self.selected_task_uuids(); - // if self.current_selection_uuid.is_none() && self.current_selection_id.is_none() && task_uuids.len() == 1 { - // if let Some(uuid) = task_uuids.get(0) { - // self.current_selection_uuid = Some(*uuid); - // } - // } - // - // self.last_export = Some(std::time::SystemTime::now()); - // self.task_report_table.export_headers(None, &self.report)?; - // self.export_tasks()?; - // if self.config.uda_task_report_use_all_tasks_for_completion { - // self.export_all_tasks()?; - // } - // self.contexts.update_data()?; - // self.projects.update_data()?; - // self.update_tags(); - // self.task_details.clear(); - // self.dirty = false; - // self.save_history()?; - // } - // self.cursor_fix(); - // self.update_task_table_state(); - // if self.task_report_show_info { - // self.update_task_details()?; - // } - // self.selection_fix(); - // - // Ok(()) - // } - - pub fn selection_fix(&mut self) { - if let (Some(t), Some(id)) = (self.task_current(), self.current_selection_id) { - if t.id() != Some(id) { - if let Some(i) = self.task_index_by_id(id) { - self.current_selection = i; - self.current_selection_id = None; - } - } - } - - if let (Some(t), Some(uuid)) = (self.task_current(), self.current_selection_uuid) { - if t.uuid() != &uuid { - if let Some(i) = self.task_index_by_uuid(uuid) { - self.current_selection = i; - self.current_selection_uuid = None; - } - } - } - } - - pub fn save_history(&mut self) -> Result<()> { - self.filter_history.write()?; - self.command_history.write()?; - Ok(()) - } - - pub fn cursor_fix(&mut self) { - while !self.tasks.is_empty() && self.current_selection >= self.tasks.len() { - self.task_report_previous(); - } - } - - pub fn update_task_details(&mut self) -> Result<()> { - if self.tasks.is_empty() { - return Ok(()); - } - - // remove task_details of tasks not in task report - let mut to_delete = vec![]; - for k in self.task_details.keys() { - if !self.tasks.iter().map(Task::uuid).any(|x| x == k) { - to_delete.push(*k); - } - } - for k in to_delete { - self.task_details.remove(&k); - } - - let selected = self.current_selection; - if selected >= self.tasks.len() { - return Ok(()); - } - let current_task_uuid = *self.tasks[selected].uuid(); - - let mut l = vec![selected]; - - for s in 1..=self.config.uda_task_detail_prefetch { - l.insert(0, std::cmp::min(selected.saturating_sub(s), self.tasks.len() - 1)); - l.push(std::cmp::min(selected + s, self.tasks.len() - 1)); - } - - l.dedup(); - - let (tx, rx) = std::sync::mpsc::channel(); - let tasks = self.tasks.clone(); - let defaultwidth = self.terminal_width.saturating_sub(2); - for s in &l { - if tasks.is_empty() { - return Ok(()); - } - if s >= &tasks.len() { - break; - } - let task_uuid = *tasks[*s].uuid(); - if !self.task_details.contains_key(&task_uuid) || task_uuid == current_task_uuid { - debug!("Running task details for {}", task_uuid); - let _tx = tx.clone(); - tokio::spawn(async move { - let output = tokio::process::Command::new("task") - .arg("rc.color=off") - .arg("rc._forcecolor=off") - .arg(format!("rc.defaultwidth={}", defaultwidth)) - .arg(format!("{}", task_uuid)) - .output() - .await; - if let Ok(output) = output { - let data = String::from_utf8_lossy(&output.stdout).to_string(); - _tx.send(Some((task_uuid, data))).unwrap(); - } - }); - } - } - drop(tx); - while let Some((task_uuid, data)) = rx.recv()? { - self.task_details.insert(task_uuid, data); - } - Ok(()) - } - - pub fn update_task_table_state(&mut self) { - trace!("self.update_task_table_state()"); - self.task_table_state.select(Some(self.current_selection)); - - for uuid in self.marked.clone() { - if self.task_by_uuid(uuid).is_none() { - self.marked.remove(&uuid); - } - } - - if self.marked.is_empty() { - self.task_table_state.single_selection(); - } - - self.task_table_state.clear(); - - for uuid in &self.marked { - self.task_table_state.mark(self.task_index_by_uuid(*uuid)); - } - } - - pub fn context_next(&mut self) { - let i = match self.contexts.table_state.current_selection() { - Some(i) => { - if i >= self.contexts.len() - 1 { - 0 - } else { - i + 1 - } - } - None => 0, - }; - self.contexts.table_state.select(Some(i)); - } - - pub fn context_previous(&mut self) { - let i = match self.contexts.table_state.current_selection() { - Some(i) => { - if i == 0 { - self.contexts.len() - 1 - } else { - i - 1 - } - } - None => 0, - }; - self.contexts.table_state.select(Some(i)); - } - - pub fn context_select(&mut self) -> Result<()> { - let i = self.contexts.table_state.current_selection().unwrap_or_default(); - let mut command = std::process::Command::new("task"); - command.arg("context").arg(&self.contexts.rows[i].name); - command.output()?; - Ok(()) - } - - pub fn task_report_top(&mut self) { - if self.tasks.is_empty() { - return; - } - self.current_selection = 0; - self.current_selection_id = None; - self.current_selection_uuid = None; - } - - pub fn task_report_bottom(&mut self) { - if self.tasks.is_empty() { - return; - } - self.current_selection = self.tasks.len() - 1; - self.current_selection_id = None; - self.current_selection_uuid = None; - } - - pub fn task_report_next(&mut self) { - if self.tasks.is_empty() { - return; - } - let i = { - if self.current_selection >= self.tasks.len() - 1 { - if self.config.uda_task_report_looping { - 0 - } else { - self.current_selection - } - } else { - self.current_selection + 1 - } - }; - self.current_selection = i; - self.current_selection_id = None; - self.current_selection_uuid = None; - } - - pub fn task_report_previous(&mut self) { - if self.tasks.is_empty() { - return; - } - let i = { - if self.current_selection == 0 { - if self.config.uda_task_report_looping { - self.tasks.len() - 1 - } else { - 0 - } - } else { - self.current_selection - 1 - } - }; - self.current_selection = i; - self.current_selection_id = None; - self.current_selection_uuid = None; - } - - pub fn task_report_next_page(&mut self) { - if self.tasks.is_empty() { - return; - } - let i = { - if self.current_selection == self.tasks.len() - 1 { - if self.config.uda_task_report_looping { - 0 - } else { - self.tasks.len() - 1 - } - } else { - std::cmp::min( - self - .current_selection - .checked_add(self.task_report_height as usize) - .unwrap_or(self.tasks.len() - 1), - self.tasks.len() - 1, - ) - } - }; - self.current_selection = i; - self.current_selection_id = None; - self.current_selection_uuid = None; - } - - pub fn task_report_previous_page(&mut self) { - if self.tasks.is_empty() { - return; - } - let i = { - if self.current_selection == 0 { - if self.config.uda_task_report_looping { - self.tasks.len() - 1 - } else { - 0 - } - } else { - self.current_selection.saturating_sub(self.task_report_height as usize) - } - }; - self.current_selection = i; - self.current_selection_id = None; - self.current_selection_uuid = None; - } - - pub fn task_report_jump(&mut self) -> Result<()> { - if self.tasks.is_empty() { - return Ok(()); - } - let i = self.command.value().parse::()?; - if let Some(task) = self.task_by_id(i as u64) { - let j = self.task_index_by_uuid(*task.uuid()).unwrap_or_default(); - self.current_selection = j; - self.current_selection_id = None; - self.current_selection_uuid = None; - Ok(()) - } else { - Err(anyhow!("Cannot locate task id {} in report", i)) - } - } - - fn get_task_files_max_mtime(&self) -> Result { - let data_dir = shellexpand::tilde(&self.config.data_location).into_owned(); - ["backlog.data", "completed.data", "pending.data"] - .iter() - .map(|n| fs::metadata(Path::new(&data_dir).join(n)).map(|m| m.modified())) - .filter_map(Result::ok) - .filter_map(Result::ok) - .max() - .ok_or_else(|| anyhow!("Unable to get task files max time")) - } - - pub fn tasks_changed_since(&mut self, prev: Option) -> Result { - if let Some(prev) = prev { - let mtime = self.get_task_files_max_mtime()?; - if mtime > prev { - Ok(true) - } else { - // Unfortunately, we can not use std::time::Instant which is guaranteed to be monotonic, - // because we need to compare it to a file mtime as SystemTime, so as a safety for unexpected - // time shifts, cap maximum wait to 1 min - let now = SystemTime::now(); - let max_delta = Duration::from_secs(60); - Ok(now.duration_since(prev)? > max_delta) - } - } else { - Ok(true) - } - } - - pub fn export_all_tasks(&mut self) -> Result<()> { - let mut task = std::process::Command::new("task"); - - task - .arg("rc.json.array=on") - .arg("rc.confirmation=off") - .arg("rc.json.depends.array=on") - .arg("rc.color=off") - .arg("rc._forcecolor=off"); - // .arg("rc.verbose:override=false"); - - task.arg("export"); - - task.arg("all"); - - info!("Running `{:?}`", task); - let output = task.output()?; - let data = String::from_utf8_lossy(&output.stdout); - let error = String::from_utf8_lossy(&output.stderr); - - if output.status.success() { - if let Ok(imported) = import(data.as_bytes()) { - self.all_tasks = imported; - info!("Imported {} tasks", self.tasks.len()); - self.error = None; - if self.mode == Mode::TaskError { - self.mode = self.previous_mode.clone().unwrap_or(Mode::TaskReport); - self.previous_mode = None; - } - } else { - self.error = Some(format!("Unable to parse output of `{:?}`:\n`{:?}`", task, data)); - self.mode = Mode::TaskError; - debug!("Unable to parse output: {:?}", data); - } - } else { - self.error = Some(format!( - "Cannot run `{:?}` - ({}) error:\n{}", - &task, output.status, error - )); - } - - Ok(()) - } - - pub fn export_tasks(&mut self) -> Result<()> { - let mut task = std::process::Command::new("task"); - - task - .arg("rc.json.array=on") - .arg("rc.confirmation=off") - .arg("rc.json.depends.array=on") - .arg("rc.color=off") - .arg("rc._forcecolor=off"); - // .arg("rc.verbose:override=false"); - - if let Some(args) = - shlex::split(format!(r#"rc.report.{}.filter='{}'"#, self.report, self.filter.value().trim()).trim()) - { - for arg in args { - task.arg(arg); - } - } - - if !self.current_context_filter.trim().is_empty() && self.task_version >= *TASKWARRIOR_VERSION_SUPPORTED { - if let Some(args) = shlex::split(&self.current_context_filter) { - for arg in args { - task.arg(arg); - } - } - } else if !self.current_context_filter.trim().is_empty() { - task.arg(format!("'\\({}\\)'", self.current_context_filter)); - } - - task.arg("export"); - - if self.task_version >= *TASKWARRIOR_VERSION_SUPPORTED { - task.arg(&self.report); - } - - info!("Running `{:?}`", task); - let output = task.output()?; - let data = String::from_utf8_lossy(&output.stdout); - let error = String::from_utf8_lossy(&output.stderr); - - if output.status.success() { - if let Ok(imported) = import(data.as_bytes()) { - self.tasks = imported; - info!("Imported {} tasks", self.tasks.len()); - self.error = None; - if self.mode == Mode::TaskError { - self.mode = self.previous_mode.clone().unwrap_or(Mode::TaskReport); - self.previous_mode = None; - } - } else { - self.error = Some(format!("Unable to parse output of `{:?}`:\n`{:?}`", task, data)); - self.mode = Mode::TaskError; - debug!("Unable to parse output: {:?}", data); - } - } else { - self.error = Some(format!( - "Cannot run `{:?}` - ({}) error:\n{}", - &task, output.status, error - )); - } - - Ok(()) - } - - pub fn selected_task_uuids(&self) -> Vec { - let selected = match self.task_table_state.mode() { - TableMode::SingleSelection => vec![self.current_selection], - TableMode::MultipleSelection => self.task_table_state.marked().copied().collect::>(), - }; - - let mut task_uuids = vec![]; - - for s in selected { - if self.tasks.is_empty() { - break; - } - let task_id = self.tasks[s].id().unwrap_or_default(); - let task_uuid = *self.tasks[s].uuid(); - task_uuids.push(task_uuid); - } - - task_uuids - } - - pub fn task_subprocess(&mut self) -> Result<(), String> { - let task_uuids = if self.tasks.is_empty() { - vec![] - } else { - self.selected_task_uuids() - }; - - let shell = self.command.value(); - - let r = match shlex::split(shell) { - Some(cmd) => { - if cmd.is_empty() { - Err(format!("Shell command empty: {}", shell)) - } else { - // first argument must be a binary - let mut command = std::process::Command::new(&cmd[0]); - // remaining arguments are args - for (i, s) in cmd.iter().enumerate() { - if i == 0 { - continue; - } - command.arg(s); - } - let output = command.output(); - match output { - Ok(o) => { - let output = String::from_utf8_lossy(&o.stdout); - if !output.is_empty() { - Err(format!( - r#"Shell command `{}` ran successfully but printed the following output: - - {} - - Suppress output of shell commands to prevent the error prompt from showing up."#, - shell, output - )) - } else { - Ok(()) - } - } - Err(_) => Err(format!("Shell command `{}` exited with non-zero output", shell)), - } - } - } - None => Err(format!("Cannot run subprocess. Unable to shlex split `{}`", shell)), - }; - - if task_uuids.len() == 1 { - if let Some(uuid) = task_uuids.get(0) { - self.current_selection_uuid = Some(*uuid); - } - } - - r - } - - pub fn task_log(&mut self) -> Result<(), String> { - let mut command = std::process::Command::new("task"); - - command.arg("log"); - - let shell = self.command.value(); - - match shlex::split(shell) { - Some(cmd) => { - for s in cmd { - command.arg(&s); - } - let output = command.output(); - match output { - Ok(_) => Ok(()), - Err(_) => Err(format!( - "Cannot run `task log {}`. Check documentation for more information", - shell - )), - } - } - None => Err(format!( - "Unable to run `{:?}`: shlex::split(`{}`) failed.", - command, shell - )), - } - } - - pub fn task_background(&mut self) { - let shell = self.config.uda_background_process.clone(); - if shell.is_empty() { - return; - } - let shell = shellexpand::tilde(&shell).into_owned(); - let period = self.config.uda_background_process_period; - std::thread::spawn(move || loop { - std::thread::sleep(Duration::from_secs(period as u64)); - match shlex::split(&shell) { - Some(cmd) => { - let mut command = std::process::Command::new(&cmd[0]); - for s in cmd.iter().skip(1) { - command.arg(s); - } - if let Ok(output) = command.output() { - if !output.status.success() { - break; - } - } else { - break; - } - } - None => break, - }; - }); - } - - pub fn task_shortcut(&mut self, s: usize) -> Result<(), String> { - // self.pause_tui().await.unwrap(); - - let task_uuids = if self.tasks.is_empty() { - vec![] - } else { - self.selected_task_uuids() - }; - - let shell = &self.config.uda_shortcuts[s]; - - if shell.is_empty() { - // self.resume_tui().await.unwrap(); - return Err("Trying to run empty shortcut.".to_string()); - } - - let shell = format!( - "{} {}", - shell, - task_uuids - .iter() - .map(ToString::to_string) - .collect::>() - .join(" ") - ); - - let shell = shellexpand::tilde(&shell).into_owned(); - let r = match shlex::split(&shell) { - Some(cmd) => { - let mut command = std::process::Command::new(&cmd[0]); - for i in cmd.iter().skip(1) { - command.arg(i); - } - match command.spawn() { - Ok(child) => { - let output = child.wait_with_output(); - match output { - Ok(o) => { - if o.status.success() { - Ok(()) - } else { - Err(format!( - "Unable to run shortcut {}. Status Code: {} - stdout: {} stderr: {}", - s, - o.status.code().unwrap_or_default(), - String::from_utf8_lossy(&o.stdout), - String::from_utf8_lossy(&o.stderr), - )) - } - } - Err(s) => Err(format!("`{}` failed to wait with output: {}", shell, s)), - } - } - Err(err) => Err(format!( - "`{}` failed: Unable to spawn shortcut number {} - Error: {}", - shell, s, err - )), - } - } - None => Err(format!( - "Unable to run shortcut number {}: shlex::split(`{}`) failed.", - s, shell - )), - }; - - if task_uuids.len() == 1 { - if let Some(uuid) = task_uuids.get(0) { - self.current_selection_uuid = Some(*uuid); - } - } - - // self.resume_tui().await.unwrap(); - - r - } - - pub fn task_modify(&mut self) -> Result<(), String> { - if self.tasks.is_empty() { - return Ok(()); - } - - let task_uuids = self.selected_task_uuids(); - - let mut command = std::process::Command::new("task"); - command.arg("rc.bulk=0"); - command.arg("rc.confirmation=off"); - command.arg("rc.dependency.confirmation=off"); - command.arg("rc.recurrence.confirmation=off"); - for task_uuid in &task_uuids { - command.arg(task_uuid.to_string()); - } - command.arg("modify"); - - let shell = self.modify.value(); - - let r = match shlex::split(shell) { - Some(cmd) => { - for s in cmd { - command.arg(&s); - } - let output = command.output(); - match output { - Ok(o) => { - if o.status.success() { - Ok(()) - } else { - Err(format!("Modify failed. {}", String::from_utf8_lossy(&o.stdout))) - } - } - Err(_) => Err(format!( - "Cannot run `task {:?} modify {}`. Check documentation for more information", - task_uuids, shell, - )), - } - } - None => Err(format!("Cannot shlex split `{}`", shell)), - }; - - if task_uuids.len() == 1 { - if let Some(uuid) = task_uuids.get(0) { - self.current_selection_uuid = Some(*uuid); - } - } - - r - } - - pub fn task_annotate(&mut self) -> Result<(), String> { - if self.tasks.is_empty() { - return Ok(()); - } - - let task_uuids = self.selected_task_uuids(); - - let mut command = std::process::Command::new("task"); - command.arg("rc.bulk=0"); - command.arg("rc.confirmation=off"); - command.arg("rc.dependency.confirmation=off"); - command.arg("rc.recurrence.confirmation=off"); - for task_uuid in &task_uuids { - command.arg(task_uuid.to_string()); - } - command.arg("annotate"); - - let shell = self.command.value(); - - let r = match shlex::split(shell) { - Some(cmd) => { - for s in cmd { - command.arg(&s); - } - let output = command.output(); - match output { - Ok(o) => { - if o.status.success() { - Ok(()) - } else { - Err(format!("Annotate failed. {}", String::from_utf8_lossy(&o.stdout))) - } - } - Err(_) => Err(format!( - "Cannot run `task {} annotate {}`. Check documentation for more information", - task_uuids - .iter() - .map(ToString::to_string) - .collect::>() - .join(" "), - shell - )), - } - } - None => Err(format!("Cannot shlex split `{}`", shell)), - }; - - if task_uuids.len() == 1 { - if let Some(uuid) = task_uuids.get(0) { - self.current_selection_uuid = Some(*uuid); - } - } - r - } - - pub fn task_add(&mut self) -> Result<(), String> { - let mut command = std::process::Command::new("task"); - command.arg("add"); - - let shell = self.command.value(); - - match shlex::split(shell) { - Some(cmd) => { - for s in cmd { - command.arg(&s); - } - let output = command.output(); - match output { - Ok(output) => { - if output.status.code() == Some(0) { - let data = String::from_utf8_lossy(&output.stdout); - let re = Regex::new(r"^Created task (?P\d+).\n$").unwrap(); - if self.config.uda_task_report_jump_to_task_on_add { - if let Some(caps) = re.captures(&data) { - self.current_selection_id = Some(caps["task_id"].parse::().unwrap_or_default()); - } - } - Ok(()) - } else { - Err(format!("Error: {}", String::from_utf8_lossy(&output.stderr))) - } - } - Err(e) => Err(format!("Cannot run `{:?}`: {}", command, e)), - } - } - None => Err(format!( - "Unable to run `{:?}`: shlex::split(`{}`) failed.", - command, shell - )), - } - } - - pub fn task_virtual_tags(task_uuid: Uuid) -> Result { - let output = std::process::Command::new("task") - .arg(format!("{}", task_uuid)) - .output(); - - match output { - Ok(output) => { - let data = String::from_utf8_lossy(&output.stdout); - for line in data.split('\n') { - for prefix in &["Virtual tags", "Virtual"] { - if line.starts_with(prefix) { - let line = line.to_string(); - let line = line.replace(prefix, ""); - return Ok(line); - } - } - } - Err(format!( - "Cannot find any tags for `task {}`. Check documentation for more information", - task_uuid - )) - } - Err(_) => Err(format!( - "Cannot run `task {}`. Check documentation for more information", - task_uuid - )), - } - } - - pub fn task_start_stop(&mut self) -> Result<(), String> { - if self.tasks.is_empty() { - return Ok(()); - } - - let task_uuids = self.selected_task_uuids(); - - for task_uuid in &task_uuids { - let mut command = "start"; - for tag in TaskwarriorTui::task_virtual_tags(*task_uuid) - .unwrap_or_default() - .split(' ') - { - if tag == "ACTIVE" { - command = "stop"; - } - } - - let output = std::process::Command::new("task") - .arg(task_uuid.to_string()) - .arg(command) - .output(); - if output.is_err() { - return Err(format!("Error running `task {}` for task `{}`.", command, task_uuid)); - } - } - - if task_uuids.len() == 1 { - if let Some(uuid) = task_uuids.get(0) { - self.current_selection_uuid = Some(*uuid); - } - } - - Ok(()) - } - - pub fn task_quick_tag(&mut self) -> Result<(), String> { - let tag_name = &self.config.uda_quick_tag_name; - let ptag_name = format!("+{}", tag_name); - let ntag_name = format!("-{}", tag_name); - if self.tasks.is_empty() { - return Ok(()); - } - - let task_uuids = self.selected_task_uuids(); - - for task_uuid in &task_uuids { - if let Some(task) = self.task_by_uuid(*task_uuid) { - let mut tag_to_set = &ptag_name; - for tag in task.tags().unwrap() { - if tag == tag_name { - tag_to_set = &ntag_name; - } - } - - let output = std::process::Command::new("task") - .arg(task_uuid.to_string()) - .arg("modify") - .arg(tag_to_set) - .output(); - - if output.is_err() { - return Err(format!( - "Error running `task modify {}` for task `{}`.", - tag_to_set, task_uuid, - )); - } - } - } - - if task_uuids.len() == 1 { - if let Some(uuid) = task_uuids.get(0) { - self.current_selection_uuid = Some(*uuid); - } - } - - Ok(()) - } - - pub fn task_delete(&mut self) -> Result<(), String> { - if self.tasks.is_empty() { - return Ok(()); - } - - let task_uuids = self.selected_task_uuids(); - - let mut cmd = std::process::Command::new("task"); - cmd - .arg("rc.bulk=0") - .arg("rc.confirmation=off") - .arg("rc.dependency.confirmation=off") - .arg("rc.recurrence.confirmation=off"); - for task_uuid in &task_uuids { - cmd.arg(task_uuid.to_string()); - } - cmd.arg("delete"); - let output = cmd.output(); - let r = match output { - Ok(_) => Ok(()), - Err(_) => Err(format!( - "Cannot run `task delete` for tasks `{}`. Check documentation for more information", - task_uuids - .iter() - .map(ToString::to_string) - .collect::>() - .join(" ") - )), - }; - self.current_selection_uuid = None; - self.current_selection_id = None; - r - } - - pub fn task_done(&mut self) -> Result<(), String> { - if self.tasks.is_empty() { - return Ok(()); - } - let task_uuids = self.selected_task_uuids(); - let mut cmd = std::process::Command::new("task"); - cmd - .arg("rc.bulk=0") - .arg("rc.confirmation=off") - .arg("rc.dependency.confirmation=off") - .arg("rc.recurrence.confirmation=off"); - for task_uuid in &task_uuids { - cmd.arg(task_uuid.to_string()); - } - cmd.arg("done"); - let output = cmd.output(); - let r = match output { - Ok(_) => Ok(()), - Err(_) => Err(format!( - "Cannot run `task done` for task `{}`. Check documentation for more information", - task_uuids - .iter() - .map(ToString::to_string) - .collect::>() - .join(" ") - )), - }; - self.current_selection_uuid = None; - self.current_selection_id = None; - r - } - - pub fn task_undo(&mut self) -> Result<(), String> { - let output = std::process::Command::new("task") - .arg("rc.confirmation=off") - .arg("undo") - .output(); - - match output { - Ok(output) => { - let data = String::from_utf8_lossy(&output.stdout); - let re = - Regex::new(r"(?P[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})") - .unwrap(); - if let Some(caps) = re.captures(&data) { - if let Ok(uuid) = Uuid::parse_str(&caps["task_uuid"]) { - self.current_selection_uuid = Some(uuid); - } - } - Ok(()) - } - Err(_) => Err("Cannot run `task undo`. Check documentation for more information".to_string()), - } - } - - pub fn task_edit(&mut self) -> Result<(), String> { - if self.tasks.is_empty() { - return Ok(()); - } - - // self.pause_tui().await.unwrap(); - - let selected = self.current_selection; - let task_id = self.tasks[selected].id().unwrap_or_default(); - let task_uuid = *self.tasks[selected].uuid(); - - let r = std::process::Command::new("task") - .arg(format!("{}", task_uuid)) - .arg("edit") - .spawn(); - - let r = match r { - Ok(child) => { - let output = child.wait_with_output(); - match output { - Ok(output) => { - if output.status.success() { - Ok(()) - } else { - Err(format!( - "`task edit` for task `{}` failed. {}{}", - task_uuid, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - )) - } - } - Err(err) => Err(format!("Cannot run `task edit` for task `{}`. {}", task_uuid, err)), - } - } - _ => Err(format!( - "Cannot start `task edit` for task `{}`. Check documentation for more information", - task_uuid - )), - }; - - self.current_selection_uuid = Some(task_uuid); - - // self.resume_tui().await.unwrap(); - - r - } - - pub fn task_current(&self) -> Option { - if self.tasks.is_empty() { - return None; - } - let selected = self.current_selection; - Some(self.tasks[selected].clone()) - } - - pub fn update_tags(&mut self) { - let tasks = &mut self.tasks; - - // dependency scan - for l_i in 0..tasks.len() { - let default_deps = vec![]; - let deps = tasks[l_i].depends().unwrap_or(&default_deps).clone(); - tasks[l_i].add_tag("UNBLOCKED".to_string()); - for dep in deps { - for r_i in 0..tasks.len() { - if tasks[r_i].uuid() == &dep { - let l_status = tasks[l_i].status(); - let r_status = tasks[r_i].status(); - if l_status != &TaskStatus::Completed - && l_status != &TaskStatus::Deleted - && r_status != &TaskStatus::Completed - && r_status != &TaskStatus::Deleted - { - tasks[l_i].remove_tag("UNBLOCKED"); - tasks[l_i].add_tag("BLOCKED".to_string()); - tasks[r_i].add_tag("BLOCKING".to_string()); - } - break; - } - } - } - } - - // other virtual tags - // TODO: support all virtual tags that taskwarrior supports - for task in tasks.iter_mut() { - match task.status() { - TaskStatus::Waiting => task.add_tag("WAITING".to_string()), - TaskStatus::Completed => task.add_tag("COMPLETED".to_string()), - TaskStatus::Pending => task.add_tag("PENDING".to_string()), - TaskStatus::Deleted => task.add_tag("DELETED".to_string()), - TaskStatus::Recurring => (), - } - if task.start().is_some() { - task.add_tag("ACTIVE".to_string()); - } - if task.scheduled().is_some() { - task.add_tag("SCHEDULED".to_string()); - } - if task.parent().is_some() { - task.add_tag("INSTANCE".to_string()); - } - if task.until().is_some() { - task.add_tag("UNTIL".to_string()); - } - if task.annotations().is_some() { - task.add_tag("ANNOTATED".to_string()); - } - let virtual_tags = self.task_report_table.virtual_tags.clone(); - if task.tags().is_some() && task.tags().unwrap().iter().any(|s| !virtual_tags.contains(s)) { - task.add_tag("TAGGED".to_string()); - } - if !task.uda().is_empty() { - task.add_tag("UDA".to_string()); - } - if task.mask().is_some() { - task.add_tag("TEMPLATE".to_string()); - } - if task.project().is_some() { - task.add_tag("PROJECT".to_string()); - } - if task.priority().is_some() { - task.add_tag("PRIORITY".to_string()); - } - if task.recur().is_some() { - task.add_tag("RECURRING".to_string()); - let r = task.recur().unwrap(); - } - if let Some(d) = task.due() { - let status = task.status(); - // due today - if status != &TaskStatus::Completed && status != &TaskStatus::Deleted { - let now = Local::now(); - let reference = TimeZone::from_utc_datetime(now.offset(), d); - let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc()); - let d = d.clone(); - if (reference - chrono::Duration::nanoseconds(1)).month() == now.month() { - task.add_tag("MONTH".to_string()); - } - if (reference - chrono::Duration::nanoseconds(1)).month() % 4 == now.month() % 4 { - task.add_tag("QUARTER".to_string()); - } - if reference.year() == now.year() { - task.add_tag("YEAR".to_string()); - } - match get_date_state(&d, self.config.due) { - DateState::EarlierToday | DateState::LaterToday => { - task.add_tag("DUE".to_string()); - task.add_tag("TODAY".to_string()); - task.add_tag("DUETODAY".to_string()); - } - DateState::AfterToday => { - task.add_tag("DUE".to_string()); - if reference.date_naive() == (now + chrono::Duration::days(1)).date_naive() { - task.add_tag("TOMORROW".to_string()); - } - } - _ => (), - } - } - } - if let Some(d) = task.due() { - let status = task.status(); - // overdue - if status != &TaskStatus::Completed && status != &TaskStatus::Deleted && status != &TaskStatus::Recurring { - let now = Local::now().naive_utc(); - let d = NaiveDateTime::new(d.date(), d.time()); - if d < now { - task.add_tag("OVERDUE".to_string()); - } - } - } - } - } - - pub fn toggle_mark(&mut self) { - if !self.tasks.is_empty() { - let selected = self.current_selection; - let task_id = self.tasks[selected].id().unwrap_or_default(); - let task_uuid = *self.tasks[selected].uuid(); - - if !self.marked.insert(task_uuid) { - self.marked.remove(&task_uuid); - } - } - } - - pub fn toggle_mark_all(&mut self) { - for task in &self.tasks { - if !self.marked.insert(*task.uuid()) { - self.marked.remove(task.uuid()); - } - } - } - - pub fn escape(s: &str) -> String { - let mut es = String::with_capacity(s.len() + 2); - es.push('"'); - for ch in s.chars() { - match ch { - '"' => { - es.push('\\'); - es.push(ch); - } - _ => es.push(ch), - } - } - es.push('"'); - es - } - - pub fn handle_event(&mut self, input: &Vec) -> Result> { - match self.mode { - Mode::Projects => { - ProjectsState::handle_input(self, *input.first().unwrap())?; - // self.update(false)?; - } - Mode::Calendar => { - // if input == self.keyconfig.quit { - // self.should_quit = true; - // } else if input == self.keyconfig.next_tab { - // if self.config.uda_change_focus_rotate { - // self.mode = Mode::Tasks(Action::Report); - // } - // } else if input == self.keyconfig.previous_tab { - // self.mode = Mode::Projects; - // } else if input == KeyCode::Up || input == self.keyconfig.up { - // if self.calendar_year > 0 { - // self.calendar_year -= 1; - // } - // } else if input == KeyCode::Down || input == self.keyconfig.down { - // self.calendar_year += 1; - // } else if input == KeyCode::PageUp || input == self.keyconfig.page_up { - // self.task_report_previous_page(); - // } else if input == KeyCode::PageDown || input == self.keyconfig.page_down { - // self.calendar_year += 10; - // // } else if input == KeyCode::Ctrl('e') { - // // self.task_details_scroll_down(); - // // } else if input == KeyCode::Ctrl('y') { - // // self.task_details_scroll_up(); - // } else if input == self.keyconfig.done { - // if self.config.uda_task_report_prompt_on_done { - // self.mode = Mode::Tasks(Action::DonePrompt); - // if self.task_current().is_none() { - // self.mode = Mode::Tasks(Action::Report); - // } - // } else { - // match self.task_done() { - // Ok(_) => self.update(true).await?, - // Err(e) => { - // self.error = Some(e); - // self.mode = Mode::Tasks(Action::Error); - // } - // } - // if self.calendar_year > 0 { - // self.calendar_year -= 10; - // } - // } - // } - } - _ => { - return self.handle_input_by_task_mode(input); - } - } - self.update_task_table_state(); - Ok(None) - } - - fn handle_input_by_task_mode(&mut self, input: &Vec) -> Result> { - match self.mode { - Mode::TaskReport => { - if let Some(keymap) = self.config.keymap.get("task-report") { - log::info!("Received input: {:?}", &input); - log::info!("Action {:?}", keymap.get(input)); - if let Some(action) = keymap.get(input) { - log::info!("Got action: {:?}", &action); - return Ok(Some(action.clone())); - } - } - } - _ => {} - } - Ok(None) - } - - pub fn update_completion_list(&mut self) { - self.completion_list.clear(); - - let tasks = if self.config.uda_task_report_use_all_tasks_for_completion { - &self.all_tasks - } else { - &self.tasks - }; - - if let Mode::TaskModify | Mode::TaskFilter | Mode::TaskAnnotate | Mode::TaskAdd | Mode::TaskLog = self.mode { - for s in [ - "project:".to_string(), - "priority:".to_string(), - "due:".to_string(), - "scheduled:".to_string(), - "wait:".to_string(), - "depends:".to_string(), - ] { - self.completion_list.insert(("attribute".to_string(), s)); - } - } - - if let Mode::TaskModify | Mode::TaskFilter | Mode::TaskAnnotate | Mode::TaskAdd | Mode::TaskLog = self.mode { - for s in [ - ".before:", - ".under:", - ".below:", - ".after:", - ".over:", - ".above:", - ".by:", - ".none:", - ".any:", - ".is:", - ".equals:", - ".isnt:", - ".not:", - ".has:", - ".contains:", - ".hasnt:", - ".startswith:", - ".left:", - ".endswith:", - ".right:", - ".word:", - ".noword:", - ] { - self.completion_list.insert(("modifier".to_string(), s.to_string())); - } - } - - if let Mode::TaskModify | Mode::TaskFilter | Mode::TaskAnnotate | Mode::TaskAdd | Mode::TaskLog = self.mode { - for priority in &self.config.uda_priority_values { - let p = priority.to_string(); - self.completion_list.insert(("priority".to_string(), p)); - } - let virtual_tags = self.task_report_table.virtual_tags.clone(); - for task in tasks { - if let Some(tags) = task.tags() { - for tag in tags { - if !virtual_tags.contains(tag) { - self - .completion_list - .insert(("tag".to_string(), format!("tag:{}", &tag))); - } - } - } - } - for task in tasks { - if let Some(tags) = task.tags() { - for tag in tags { - if !virtual_tags.contains(tag) { - self.completion_list.insert(("+".to_string(), format!("+{}", &tag))); - } - } - } - } - for task in tasks { - if let Some(project) = task.project() { - let p = if project.contains(' ') { - format!(r#""{}""#, &project) - } else { - project.to_string() - }; - self.completion_list.insert(("project".to_string(), p)); - } - } - for task in tasks { - if let Some(date) = task.due() { - self - .completion_list - .insert(("due".to_string(), get_formatted_datetime(date))); - } - } - for task in tasks { - if let Some(date) = task.wait() { - self - .completion_list - .insert(("wait".to_string(), get_formatted_datetime(date))); - } - } - for task in tasks { - if let Some(date) = task.scheduled() { - self - .completion_list - .insert(("scheduled".to_string(), get_formatted_datetime(date))); - } - } - for task in tasks { - if let Some(date) = task.end() { - self - .completion_list - .insert(("end".to_string(), get_formatted_datetime(date))); - } - } - } - - if self.mode == Mode::TaskFilter { - self.completion_list.insert(("status".to_string(), "pending".into())); - self.completion_list.insert(("status".to_string(), "completed".into())); - self.completion_list.insert(("status".to_string(), "deleted".into())); - self.completion_list.insert(("status".to_string(), "recurring".into())); - } - } - - pub fn update_input_for_completion(&mut self) { - match self.mode { - Mode::TaskAdd | Mode::TaskAnnotate | Mode::TaskLog => { - let i = get_start_word_under_cursor(self.command.value(), self.command.cursor()); - let input = self.command.value()[i..self.command.cursor()].to_string(); - self.completion_list.input(input, "".to_string()); - } - Mode::TaskModify => { - let i = get_start_word_under_cursor(self.modify.value(), self.modify.cursor()); - let input = self.modify.value()[i..self.modify.cursor()].to_string(); - self.completion_list.input(input, "".to_string()); - } - Mode::TaskFilter => { - let i = get_start_word_under_cursor(self.filter.value(), self.filter.cursor()); - let input = self.filter.value()[i..self.filter.cursor()].to_string(); - self.completion_list.input(input, "".to_string()); - } - _ => {} - } - } -} - -#[cfg(test)] -mod tests {} diff --git a/src/calendar.rs b/src/calendar.rs deleted file mode 100644 index bd06510..0000000 --- a/src/calendar.rs +++ /dev/null @@ -1,277 +0,0 @@ -// Based on https://gist.github.com/diwic/5c20a283ca3a03752e1a27b0f3ebfa30 -// See https://old.reddit.com/r/rust/comments/4xneq5/the_calendar_example_challenge_ii_why_eddyb_all/ - -use std::fmt; - -const COL_WIDTH: usize = 21; - -use std::cmp::min; - -use chrono::{ - format::Fixed, DateTime, Datelike, Duration, FixedOffset, Local, Month, NaiveDate, NaiveDateTime, TimeZone, -}; -use ratatui::{ - buffer::Buffer, - layout::Rect, - style::{Color, Modifier, Style}, - symbols, - widgets::{Block, Widget}, -}; - -#[derive(Debug, Clone)] -pub struct Calendar<'a> { - pub block: Option>, - pub year: i32, - pub month: u32, - pub style: Style, - pub months_per_row: usize, - pub date_style: Vec<(NaiveDate, Style)>, - pub today_style: Style, - pub start_on_monday: bool, - pub title_background_color: Color, -} - -impl<'a> Default for Calendar<'a> { - fn default() -> Calendar<'a> { - let year = Local::now().year(); - let month = Local::now().month(); - Calendar { - block: None, - style: Style::default(), - months_per_row: 0, - year, - month, - date_style: vec![], - today_style: Style::default(), - start_on_monday: false, - title_background_color: Color::Reset, - } - } -} - -impl<'a> Calendar<'a> { - pub fn block(mut self, block: Block<'a>) -> Self { - self.block = Some(block); - self - } - - pub fn style(mut self, style: Style) -> Self { - self.style = style; - self - } - - pub fn year(mut self, year: i32) -> Self { - self.year = year; - if self.year < 0 { - self.year = 0; - } - self - } - - pub fn month(mut self, month: u32) -> Self { - self.month = month; - self - } - - pub fn date_style(mut self, date_style: Vec<(NaiveDate, Style)>) -> Self { - self.date_style = date_style; - self - } - - pub fn today_style(mut self, today_style: Style) -> Self { - self.today_style = today_style; - self - } - - pub fn months_per_row(mut self, months_per_row: usize) -> Self { - self.months_per_row = months_per_row; - self - } - - pub fn start_on_monday(mut self, start_on_monday: bool) -> Self { - self.start_on_monday = start_on_monday; - self - } -} - -impl<'a> Widget for Calendar<'a> { - fn render(mut self, area: Rect, buf: &mut Buffer) { - let month_names = Self::generate_month_names(); - buf.set_style(area, self.style); - - let area = match self.block.take() { - Some(b) => { - let inner_area = b.inner(area); - b.render(area, buf); - inner_area - } - None => area, - }; - - if area.height < 7 { - return; - } - - let style = self.style; - let today = Local::now(); - - let year = self.year; - let month = self.month; - - let months: Vec<_> = (0..12).collect(); - - let mut days: Vec<(NaiveDate, NaiveDate)> = months - .iter() - .map(|i| { - let first = NaiveDate::from_ymd_opt(year, i + 1, 1).unwrap(); - let num_days = if self.start_on_monday { - first.weekday().num_days_from_monday() - } else { - first.weekday().num_days_from_sunday() - }; - (first, first - Duration::days(i64::from(num_days))) - }) - .collect(); - - let mut start_m = 0_usize; - if self.months_per_row > area.width as usize / 8 / 3 || self.months_per_row == 0 { - self.months_per_row = area.width as usize / 8 / 3; - } - let mut y = area.y; - y += 1; - - let x = area.x; - let s = format!("{year:^width$}", year = year, width = area.width as usize); - - let mut new_year = 0; - let style = Style::default().add_modifier(Modifier::UNDERLINED); - if self.year + new_year as i32 == today.year() { - buf.set_string(x, y, &s, self.today_style.add_modifier(Modifier::UNDERLINED)); - } else { - buf.set_string(x, y, &s, style); - } - - let start_x = (area.width - 3 * 7 * self.months_per_row as u16 - self.months_per_row as u16) / 2; - y += 2; - loop { - let endm = std::cmp::min(start_m + self.months_per_row, 12); - let mut x = area.x + start_x; - for (c, d) in days.iter_mut().enumerate().take(endm).skip(start_m) { - if c > start_m { - x += 1; - } - let m = d.0.month() as usize; - let s = format!("{:^20}", month_names[m - 1]); - let style = Style::default().bg(self.title_background_color); - if m == today.month() as usize && self.year + new_year as i32 == today.year() { - buf.set_string(x, y, &s, self.today_style); - } else { - buf.set_string(x, y, &s, style); - } - x += s.len() as u16 + 1; - } - y += 1; - let mut x = area.x + start_x; - for d in days.iter_mut().take(endm).skip(start_m) { - let m = d.0.month() as usize; - let style = Style::default().bg(self.title_background_color); - let days_string = if self.start_on_monday { - "Mo Tu We Th Fr Sa Su" - } else { - "Su Mo Tu We Th Fr Sa" - }; - buf.set_string(x, y, days_string, style.add_modifier(Modifier::UNDERLINED)); - x += 21 + 1; - } - y += 1; - loop { - let mut moredays = false; - let mut x = area.x + start_x; - for c in start_m..endm { - if c > start_m { - x += 1; - } - let d = &mut days[c + new_year * 12]; - for _ in 0..7 { - let s = if d.0.month() == d.1.month() { - format!("{:>2}", d.1.day()) - } else { - " ".to_string() - }; - let mut style = Style::default(); - let index = self.date_style.iter().position(|(date, style)| d.1 == *date); - if let Some(i) = index { - style = self.date_style[i].1; - } - if d.1 == Local::now().date_naive() { - buf.set_string(x, y, s, self.today_style); - } else { - buf.set_string(x, y, s, style); - } - x += 3; - d.1 += Duration::days(1); - } - moredays |= d.0.month() == d.1.month() || d.1 < d.0; - } - y += 1; - if !moredays { - break; - } - } - start_m += self.months_per_row; - y += 2; - if y + 8 > area.height { - break; - } else if start_m >= 12 { - start_m = 0; - new_year += 1; - days.append( - &mut months - .iter() - .map(|i| { - let first = NaiveDate::from_ymd_opt(self.year + new_year as i32, i + 1, 1).unwrap(); - ( - first, - first - Duration::days(i64::from(first.weekday().num_days_from_sunday())), - ) - }) - .collect::>(), - ); - - let x = area.x; - let s = format!( - "{year:^width$}", - year = self.year as usize + new_year, - width = area.width as usize - ); - let mut style = Style::default().add_modifier(Modifier::UNDERLINED); - if self.year + new_year as i32 == today.year() { - style = style.add_modifier(Modifier::BOLD); - } - buf.set_string(x, y, &s, style); - y += 1; - } - y += 1; - } - } -} - -impl<'a> Calendar<'a> { - fn generate_month_names() -> [&'a str; 12] { - let month_names = [ - Month::January.name(), - Month::February.name(), - Month::March.name(), - Month::April.name(), - Month::May.name(), - Month::June.name(), - Month::July.name(), - Month::August.name(), - Month::September.name(), - Month::October.name(), - Month::November.name(), - Month::December.name(), - ]; - month_names - } -} diff --git a/src/cli.rs b/src/cli.rs index dbf0a92..0be05c3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,52 +1,36 @@ -use clap::Arg; +use std::path::PathBuf; -const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); -const APP_NAME: &str = env!("CARGO_PKG_NAME"); +use clap::Parser; -pub fn generate_cli_app() -> clap::Command { - let mut app = clap::Command::new(APP_NAME) - .version(APP_VERSION) - .author("Dheepak Krishnamurthy <@kdheepak>") - .about("A taskwarrior terminal user interface") - .arg( - Arg::new("data") - .short('d') - .long("data") - .value_name("FOLDER") - .help("Sets the data folder for taskwarrior-tui") - .action(clap::ArgAction::Set), - ) - .arg( - Arg::new("config") - .short('c') - .long("config") - .value_name("FOLDER") - .help("Sets the config folder for taskwarrior-tui") - .action(clap::ArgAction::Set), - ) - .arg( - Arg::new("taskdata") - .long("taskdata") - .value_name("FOLDER") - .help("Sets the .task folder using the TASKDATA environment variable for taskwarrior") - .action(clap::ArgAction::Set), - ) - .arg( - Arg::new("taskrc") - .long("taskrc") - .value_name("FILE") - .help("Sets the .taskrc file using the TASKRC environment variable for taskwarrior") - .action(clap::ArgAction::Set), - ) - .arg( - Arg::new("report") - .short('r') - .long("report") - .value_name("STRING") - .help("Sets default report") - .action(clap::ArgAction::Set), - ); +#[derive(Parser, Debug)] +#[command(author, version, about)] +pub struct Cli { + #[arg(short, long, value_name = "FOLDER", help = "Sets the data folder for taskwarrior-tui")] + pub data: Option, - app.set_bin_name(APP_NAME); - app + #[arg(short, long, value_name = "FOLDER", help = "Sets the config folder for taskwarrior-tui")] + pub config: Option, + + #[arg( + long, + value_name = "FOLDER", + help = "Sets the .task folder using the TASKDATA environment variable for taskwarrior" + )] + pub taskdata: Option, + + #[arg( + long, + value_name = "FILE", + help = "Sets the .taskrc file using the TASKRC environment variable for taskwarrior" + )] + pub taskrc: Option, + + #[arg(value_name = "FLOAT", help = "Tick rate", default_value_t = 4.0)] + pub tick_rate: f64, + + #[arg(value_name = "FLOAT", help = "Frame rate", default_value_t = 60.0)] + pub frame_rate: f64, + + #[arg(short, long, value_name = "STRING", help = "Sets default report")] + pub report: Option, } diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..a87a1f0 --- /dev/null +++ b/src/command.rs @@ -0,0 +1,46 @@ +use serde_derive::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum Command { + Tick, + Render, + Resize(u16, u16), + Suspend, + Resume, + Quit, + Refresh, + Error(String), + Help, + MoveDown, + MoveUp, + MoveBottom, + MoveTop, + MoveLeft, + MoveRight, + MoveHome, + MoveEnd, + ToggleMark, + ToggleMarkAll, + Select, + SelectAll, + ToggleZoom, + Context, + RunShortcut(usize), + RunShell, + Task, + ShowTaskReport, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum Task { + Undo, + Edit, + Tag, + Start, + Stop, + Modify, + Log, + Annotate, + Filter, + Add, +} diff --git a/src/completion.rs b/src/completion.rs deleted file mode 100644 index 3e413b8..0000000 --- a/src/completion.rs +++ /dev/null @@ -1,206 +0,0 @@ -use std::{error::Error, io}; - -use log::{debug, error, info, log_enabled, trace, warn, Level, LevelFilter}; -use ratatui::{ - layout::{Constraint, Corner, Direction, Layout}, - style::{Color, Modifier, Style}, - text::{Line, Span}, - widgets::{Block, Borders, List, ListItem, ListState}, - Terminal, -}; -use rustyline::{ - error::ReadlineError, - highlight::{Highlighter, MatchingBracketHighlighter}, - hint::Hinter, - history::FileHistory, - line_buffer::LineBuffer, - Context, -}; -use unicode_segmentation::{Graphemes, UnicodeSegmentation}; -use unicode_width::UnicodeWidthStr; - -pub fn get_start_word_under_cursor(line: &str, cursor_pos: usize) -> usize { - let mut chars = line[..cursor_pos].chars(); - let mut res = cursor_pos; - while let Some(c) = chars.next_back() { - if c == ' ' || c == '(' || c == ')' { - break; - } - res -= c.len_utf8(); - } - // if iter == None, res == 0. - res -} - -pub struct TaskwarriorTuiCompletionHelper { - pub candidates: Vec<(String, String)>, - pub context: String, - pub input: String, -} - -type Completion = (String, String, String, String, String); - -impl TaskwarriorTuiCompletionHelper { - fn complete(&self, word: &str, pos: usize, _ctx: &Context) -> rustyline::Result<(usize, Vec)> { - let candidates: Vec = self - .candidates - .iter() - .filter_map(|(context, candidate)| { - if context == &self.context - && (candidate.starts_with(&word[..pos]) || candidate.to_lowercase().starts_with(&word[..pos].to_lowercase())) - && (!self.input.contains(candidate) || !self.input.to_lowercase().contains(&candidate.to_lowercase())) - { - Some(( - candidate.clone(), // display - candidate.to_string(), // replacement - word[..pos].to_string(), // original - candidate[..pos].to_string(), - candidate[pos..].to_string(), - )) - } else { - None - } - }) - .collect(); - Ok((pos, candidates)) - } -} - -pub struct CompletionList { - pub state: ListState, - pub current: String, - pub pos: usize, - pub helper: TaskwarriorTuiCompletionHelper, -} - -impl CompletionList { - pub fn new() -> CompletionList { - CompletionList { - state: ListState::default(), - current: String::new(), - pos: 0, - helper: TaskwarriorTuiCompletionHelper { - candidates: vec![], - context: String::new(), - input: String::new(), - }, - } - } - - pub fn with_items(items: Vec<(String, String)>) -> CompletionList { - let mut candidates = vec![]; - for i in items { - if !candidates.contains(&i) { - candidates.push(i); - } - } - let context = String::new(); - let input = String::new(); - CompletionList { - state: ListState::default(), - current: String::new(), - pos: 0, - helper: TaskwarriorTuiCompletionHelper { - candidates, - context, - input, - }, - } - } - - pub fn insert(&mut self, item: (String, String)) { - if !self.helper.candidates.contains(&item) { - self.helper.candidates.push(item); - } - } - - pub fn next(&mut self) { - let i = match self.state.selected() { - Some(i) => { - if i >= self.candidates().len() - 1 { - 0 - } else { - i + 1 - } - } - None => 0, - }; - self.state.select(Some(i)); - } - - pub fn previous(&mut self) { - let i = match self.state.selected() { - Some(i) => { - if i == 0 { - self.candidates().len() - 1 - } else { - i - 1 - } - } - None => 0, - }; - self.state.select(Some(i)); - } - - pub fn unselect(&mut self) { - self.state.select(None); - } - - pub fn clear(&mut self) { - self.helper.candidates.clear(); - self.state.select(None); - } - - pub fn len(&self) -> usize { - self.candidates().len() - } - - pub fn max_width(&self) -> Option { - self.candidates().iter().map(|p| p.1.width() + 4).max() - } - - pub fn get(&self, i: usize) -> Option { - let candidates = self.candidates(); - if i < candidates.len() { - Some(candidates[i].clone()) - } else { - None - } - } - - pub fn selected(&self) -> Option<(usize, Completion)> { - self.state.selected().and_then(|i| self.get(i)).map(|s| (self.pos, s)) - } - - pub fn is_empty(&self) -> bool { - self.candidates().is_empty() - } - - pub fn candidates(&self) -> Vec { - let hist = FileHistory::new(); - let ctx = rustyline::Context::new(&hist); - let (pos, candidates) = self.helper.complete(&self.current, self.pos, &ctx).unwrap(); - candidates - } - - pub fn input(&mut self, current: String, i: String) { - self.helper.input = i; - if current.contains('.') && current.contains(':') { - self.current = current.split_once(':').unwrap().1.to_string(); - self.helper.context = current.split_once('.').unwrap().0.to_string(); - } else if current.contains('.') { - self.current = format!(".{}", current.split_once('.').unwrap().1); - self.helper.context = "modifier".to_string(); - } else if current.contains(':') { - self.current = current.split_once(':').unwrap().1.to_string(); - self.helper.context = current.split_once(':').unwrap().0.to_string(); - } else if current.contains('+') { - self.current = format!("+{}", current.split_once('+').unwrap().1); - self.helper.context = "+".to_string(); - } else { - self.current = current; - self.helper.context = "attribute".to_string(); - } - self.pos = self.current.len(); - } -} diff --git a/src/components.rs b/src/components.rs new file mode 100644 index 0000000..d1651dd --- /dev/null +++ b/src/components.rs @@ -0,0 +1,42 @@ +use color_eyre::eyre::Result; +use crossterm::event::{KeyEvent, MouseEvent}; +use ratatui::layout::Rect; +use tokio::sync::mpsc::UnboundedSender; + +use crate::{ + command::Command, + tui::{Event, Frame}, +}; + +pub mod app; + +pub trait Component { + #[allow(unused_variables)] + fn register_command_handler(&mut self, tx: UnboundedSender) -> Result<()> { + Ok(()) + } + fn init(&mut self) -> Result<()> { + Ok(()) + } + fn handle_events(&mut self, event: Option) -> Result> { + let r = match event { + Some(Event::Key(key_event)) => self.handle_key_events(key_event)?, + Some(Event::Mouse(mouse_event)) => self.handle_mouse_events(mouse_event)?, + _ => None, + }; + Ok(r) + } + #[allow(unused_variables)] + fn handle_key_events(&mut self, key: KeyEvent) -> Result> { + Ok(None) + } + #[allow(unused_variables)] + fn handle_mouse_events(&mut self, mouse: MouseEvent) -> Result> { + Ok(None) + } + #[allow(unused_variables)] + fn update(&mut self, command: Command) -> Result> { + Ok(None) + } + fn draw(&mut self, f: &mut Frame<'_>, rect: Rect) -> Result<()>; +} diff --git a/src/components/app.rs b/src/components/app.rs new file mode 100644 index 0000000..a03b8e5 --- /dev/null +++ b/src/components/app.rs @@ -0,0 +1,141 @@ +use std::{collections::HashMap, time::Duration}; + +use color_eyre::eyre::Result; +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{prelude::*, widgets::*}; +use serde_derive::{Deserialize, Serialize}; +use task_hookrs::{import::import, task::Task}; +use tokio::sync::mpsc::UnboundedSender; +use tui_input::backend::crossterm::EventHandler; +use uuid::Uuid; + +use super::{Component, Frame}; +use crate::{command::Command, config::KeyBindings}; + +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum Mode { + #[default] + TaskReport, + TaskContext, + Calendar, + Error, +} + +#[derive(Default)] +pub struct App { + pub mode: Mode, + pub command_tx: Option>, + pub keybindings: KeyBindings, + pub last_export: Option, + pub report: String, + pub filter: String, + pub current_context_filter: String, + pub tasks: Vec, +} + +impl App { + pub fn new() -> Self { + Self::default() + } + + pub fn keybindings(mut self, keybindings: KeyBindings) -> Self { + self.keybindings = keybindings; + self + } + + pub fn refresh(&mut self) -> Result<()> { + self.last_export = Some(std::time::SystemTime::now()); + Ok(()) + } + + pub fn send_command(&self, command: Command) -> Result<()> { + if let Some(ref tx) = self.command_tx { + tx.send(command)?; + } + Ok(()) + } + + pub fn task_export(&mut self) -> Result<()> { + let mut task = std::process::Command::new("task"); + + task + .arg("rc.json.array=on") + .arg("rc.confirmation=off") + .arg("rc.json.depends.array=on") + .arg("rc.color=off") + .arg("rc._forcecolor=off"); + // .arg("rc.verbose:override=false"); + + if let Some(args) = shlex::split(format!(r#"rc.report.{}.filter='{}'"#, self.report, self.filter.trim()).trim()) { + for arg in args { + task.arg(arg); + } + } + + if !self.current_context_filter.trim().is_empty() { + if let Some(args) = shlex::split(&self.current_context_filter) { + for arg in args { + task.arg(arg); + } + } + } + + task.arg("export"); + + task.arg(&self.report); + + log::info!("Running `{:?}`", task); + let output = task.output()?; + let data = String::from_utf8_lossy(&output.stdout); + let error = String::from_utf8_lossy(&output.stderr); + + if output.status.success() { + if let Ok(imported) = import(data.as_bytes()) { + self.tasks = imported; + log::info!("Imported {} tasks", self.tasks.len()); + if self.mode == Mode::Error { + self.send_command(Command::ShowTaskReport)?; + }; + // } else { + // self.error = Some(format!("Unable to parse output of `{:?}`:\n`{:?}`", task, data)); + // self.mode = Mode::Tasks(Action::Error); + // debug!("Unable to parse output: {:?}", data); + } + } else { + // self.error = Some(format!("Cannot run `{:?}` - ({}) error:\n{}", &task, output.status, error)); + } + + Ok(()) + } +} + +impl Component for App { + fn register_command_handler(&mut self, tx: UnboundedSender) -> Result<()> { + self.command_tx = Some(tx); + Ok(()) + } + + fn handle_key_events(&mut self, key: KeyEvent) -> Result> { + let command = if let Some(keymap) = self.keybindings.get(&self.mode) { + if let Some(command) = keymap.get(&vec![key]) { + command + } else { + return Ok(None); + } + } else { + return Ok(None); + }; + Ok(Some(command.clone())) + } + + fn update(&mut self, command: Command) -> Result> { + match command { + _ => (), + } + Ok(None) + } + + fn draw(&mut self, f: &mut Frame<'_>, rect: Rect) -> Result<()> { + Ok(()) + } +} diff --git a/src/config.rs b/src/config.rs index 5947811..42fcd8d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,102 +1,322 @@ -use std::{collections::HashMap, error::Error, path::PathBuf, str}; +use std::{collections::HashMap, fmt, path::PathBuf}; -use color_eyre::eyre::{eyre, Context, Result}; +use color_eyre::eyre::Result; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use figment::{ - providers::{Env, Format, Serialized, Toml}, - Figment, -}; -use ratatui::{ - style::{Color, Modifier, Style}, - symbols::line::DOUBLE_VERTICAL, -}; -use serde::{ - de::{self, Deserialize, Deserializer, MapAccess, Visitor}, - ser::{self, Serialize, Serializer}, -}; -use serde_derive::{Deserialize, Serialize}; +use derive_deref::{Deref, DerefMut}; +use ratatui::style::{Color, Modifier, Style}; +use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor}; +use serde_derive::Deserialize; -use crate::{action::Action, keyevent::parse_key_sequence, keymap::KeyMap, utils::get_config_dir}; +use crate::{command::Command, components::app::Mode}; -#[derive(Default, Clone, Debug, Copy)] -pub struct SerdeStyle(pub Style); +const CONFIG: &'static str = include_str!("../.config/config.json5"); -impl std::ops::Deref for SerdeStyle { - type Target = Style; - - fn deref(&self) -> &Self::Target { - &self.0 - } +#[derive(Clone, Debug, Deserialize, Default)] +pub struct AppConfig { + #[serde(default)] + pub _data_dir: PathBuf, + #[serde(default)] + pub _config_dir: PathBuf, } -impl std::ops::DerefMut for SerdeStyle { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 + +#[derive(Clone, Debug, Default, Deserialize)] +pub struct Config { + #[serde(default, flatten)] + pub config: AppConfig, + #[serde(default)] + pub keybindings: KeyBindings, + #[serde(default)] + pub styles: Styles, +} + +impl Config { + pub fn new() -> Result { + let default_config: Config = json5::from_str(&CONFIG).unwrap(); + let data_dir = crate::utils::get_data_dir(); + let config_dir = crate::utils::get_config_dir(); + let mut builder = config::Config::builder() + .set_default("_data_dir", data_dir.to_str().unwrap())? + .set_default("_config_dir", config_dir.to_str().unwrap())?; + + builder = builder + .add_source(config::File::from(config_dir.join("config.json5")).format(config::FileFormat::Json5).required(false)) + .add_source(config::File::from(config_dir.join("config.json")).format(config::FileFormat::Json).required(false)) + .add_source(config::File::from(config_dir.join("config.yaml")).format(config::FileFormat::Yaml).required(false)) + .add_source(config::File::from(config_dir.join("config.toml")).format(config::FileFormat::Toml).required(false)) + .add_source(config::File::from(config_dir.join("config.ini")).format(config::FileFormat::Ini).required(false)); + + let mut cfg: Self = builder.build()?.try_deserialize()?; + + for (mode, default_bindings) in default_config.keybindings.iter() { + let user_bindings = cfg.keybindings.entry(*mode).or_default(); + for (key, cmd) in default_bindings.iter() { + user_bindings.entry(key.clone()).or_insert_with(|| cmd.clone()); + } + } + + Ok(cfg) } } -impl<'de> Deserialize<'de> for SerdeStyle { +#[derive(Clone, Debug, Default, Deref, DerefMut)] +pub struct KeyBindings(pub HashMap, Command>>); + +impl<'de> Deserialize<'de> for KeyBindings { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { - struct StyleVisitor; + let parsed_map = HashMap::>::deserialize(deserializer)?; - impl<'de> Visitor<'de> for StyleVisitor { - type Value = SerdeStyle; + let keybindings = parsed_map + .into_iter() + .map(|(mode, inner_map)| { + let converted_inner_map = + inner_map.into_iter().map(|(key_str, cmd)| (parse_key_sequence(&key_str).unwrap(), cmd)).collect(); + (mode, converted_inner_map) + }) + .collect(); - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a string representation of tui::style::Style") - } - - fn visit_str(self, v: &str) -> Result { - Ok(SerdeStyle(get_tcolor(v))) - } - } - - deserializer.deserialize_str(StyleVisitor) + Ok(KeyBindings(keybindings)) } } -pub fn get_tcolor(line: &str) -> Style { +fn parse_key_event(raw: &str) -> Result { + let raw_lower = raw.to_ascii_lowercase(); + let (remaining, modifiers) = extract_modifiers(&raw_lower); + parse_key_code_with_modifiers(remaining, modifiers) +} + +fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) { + let mut modifiers = KeyModifiers::empty(); + let mut current = raw; + + loop { + match current { + rest if rest.starts_with("ctrl-") => { + modifiers.insert(KeyModifiers::CONTROL); + current = &rest[5..]; + }, + rest if rest.starts_with("alt-") => { + modifiers.insert(KeyModifiers::ALT); + current = &rest[4..]; + }, + rest if rest.starts_with("shift-") => { + modifiers.insert(KeyModifiers::SHIFT); + current = &rest[6..]; + }, + _ => break, // break out of the loop if no known prefix is detected + }; + } + + (current, modifiers) +} + +fn parse_key_code_with_modifiers(raw: &str, mut modifiers: KeyModifiers) -> Result { + let c = match raw { + "esc" => KeyCode::Esc, + "enter" => KeyCode::Enter, + "left" => KeyCode::Left, + "right" => KeyCode::Right, + "up" => KeyCode::Up, + "down" => KeyCode::Down, + "home" => KeyCode::Home, + "end" => KeyCode::End, + "pageup" => KeyCode::PageUp, + "pagedown" => KeyCode::PageDown, + "backtab" => { + modifiers.insert(KeyModifiers::SHIFT); + KeyCode::BackTab + }, + "backspace" => KeyCode::Backspace, + "delete" => KeyCode::Delete, + "insert" => KeyCode::Insert, + "f1" => KeyCode::F(1), + "f2" => KeyCode::F(2), + "f3" => KeyCode::F(3), + "f4" => KeyCode::F(4), + "f5" => KeyCode::F(5), + "f6" => KeyCode::F(6), + "f7" => KeyCode::F(7), + "f8" => KeyCode::F(8), + "f9" => KeyCode::F(9), + "f10" => KeyCode::F(10), + "f11" => KeyCode::F(11), + "f12" => KeyCode::F(12), + "space" => KeyCode::Char(' '), + "hyphen" => KeyCode::Char('-'), + "minus" => KeyCode::Char('-'), + "tab" => KeyCode::Tab, + c if c.len() == 1 => { + let mut c = c.chars().next().unwrap(); + if modifiers.contains(KeyModifiers::SHIFT) { + c = c.to_ascii_uppercase(); + } + KeyCode::Char(c) + }, + _ => return Err(format!("Unable to parse {raw}")), + }; + Ok(KeyEvent::new(c, modifiers)) +} + +pub fn key_event_to_string(key_event: &KeyEvent) -> String { + let char; + let key_code = match key_event.code { + KeyCode::Backspace => "backspace", + KeyCode::Enter => "enter", + KeyCode::Left => "left", + KeyCode::Right => "right", + KeyCode::Up => "up", + KeyCode::Down => "down", + KeyCode::Home => "home", + KeyCode::End => "end", + KeyCode::PageUp => "pageup", + KeyCode::PageDown => "pagedown", + KeyCode::Tab => "tab", + KeyCode::BackTab => "backtab", + KeyCode::Delete => "delete", + KeyCode::Insert => "insert", + KeyCode::F(c) => { + char = format!("f({})", c.to_string()); + &char + }, + KeyCode::Char(c) if c == ' ' => "space", + KeyCode::Char(c) => { + char = c.to_string(); + &char + }, + KeyCode::Esc => "esc", + KeyCode::Null => "", + KeyCode::CapsLock => "", + KeyCode::Menu => "", + KeyCode::ScrollLock => "", + KeyCode::Media(_) => "", + KeyCode::NumLock => "", + KeyCode::PrintScreen => "", + KeyCode::Pause => "", + KeyCode::KeypadBegin => "", + KeyCode::Modifier(_) => "", + }; + + let mut modifiers = Vec::with_capacity(3); + + if key_event.modifiers.intersects(KeyModifiers::CONTROL) { + modifiers.push("ctrl"); + } + + if key_event.modifiers.intersects(KeyModifiers::SHIFT) { + modifiers.push("shift"); + } + + if key_event.modifiers.intersects(KeyModifiers::ALT) { + modifiers.push("alt"); + } + + let mut key = modifiers.join("-"); + + if !key.is_empty() { + key.push('-'); + } + key.push_str(key_code); + + key +} + +pub fn parse_key_sequence(raw: &str) -> Result, String> { + if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() { + return Err(format!("Unable to parse `{}`", raw)); + } + let raw = if !raw.contains("><") { + let raw = raw.strip_prefix("<").unwrap_or(raw); + let raw = raw.strip_prefix(">").unwrap_or(raw); + raw + } else { + raw + }; + let sequences = raw + .split("><") + .map(|seq| { + if seq.starts_with('<') { + &seq[1..] + } else if seq.ends_with('>') { + &seq[..seq.len() - 1] + } else { + seq + } + }) + .collect::>(); + + sequences.into_iter().map(parse_key_event).collect() +} + +#[derive(Clone, Debug, Default, Deref, DerefMut)] +pub struct Styles(pub HashMap>); + +impl<'de> Deserialize<'de> for Styles { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let parsed_map = HashMap::>::deserialize(deserializer)?; + + let styles = parsed_map + .into_iter() + .map(|(mode, inner_map)| { + let converted_inner_map = inner_map.into_iter().map(|(str, style)| (str, parse_style(&style))).collect(); + (mode, converted_inner_map) + }) + .collect(); + + Ok(Styles(styles)) + } +} + +pub fn parse_style(line: &str) -> Style { let (foreground, background) = line.split_at(line.to_lowercase().find("on ").unwrap_or(line.len())); - let (mut foreground, mut background) = (String::from(foreground), String::from(background)); - background = background.replace("on ", ""); - let mut modifiers = Modifier::empty(); - if foreground.contains("bright") { - foreground = foreground.replace("bright ", ""); - background = background.replace("bright ", ""); - background.insert_str(0, "bright "); - } - foreground = foreground.replace("grey", "gray"); - background = background.replace("grey", "gray"); - if foreground.contains("underline") { - modifiers |= Modifier::UNDERLINED; - } - let foreground = foreground.replace("underline ", ""); - // TODO: use bold, bright boolean flags - if foreground.contains("bold") { - modifiers |= Modifier::BOLD; - } - // let foreground = foreground.replace("bold ", ""); - if foreground.contains("inverse") { - modifiers |= Modifier::REVERSED; - } - let foreground = foreground.replace("inverse ", ""); + let foreground = process_color_string(foreground); + let background = process_color_string(&background.replace("on ", "")); + let mut style = Style::default(); - if let Some(fg) = get_color_foreground(foreground.as_str()) { + if let Some(fg) = parse_color(&foreground.0) { style = style.fg(fg); } - if let Some(bg) = get_color_background(background.as_str()) { + if let Some(bg) = parse_color(&background.0) { style = style.bg(bg); } - style = style.add_modifier(modifiers); + style = style.add_modifier(foreground.1 | background.1); style } -fn get_color_foreground(s: &str) -> Option { +fn process_color_string(color_str: &str) -> (String, Modifier) { + let color = color_str + .replace("grey", "gray") + .replace("bright ", "") + .replace("bold ", "") + .replace("underline ", "") + .replace("inverse ", ""); + + let mut modifiers = Modifier::empty(); + if color_str.contains("underline") { + modifiers |= Modifier::UNDERLINED; + } + if color_str.contains("bold") { + modifiers |= Modifier::BOLD; + } + if color_str.contains("inverse") { + modifiers |= Modifier::REVERSED; + } + + (color, modifiers) +} + +fn parse_color(s: &str) -> Option { let s = s.trim_start(); let s = s.trim_end(); - if s.contains("color") { + if s.contains("bright color") { + let s = s.trim_start_matches("bright "); + let c = s.trim_start_matches("color").parse::().unwrap_or_default(); + Some(Color::Indexed(c.wrapping_shl(8))) + } else if s.contains("color") { let c = s.trim_start_matches("color").parse::().unwrap_or_default(); Some(Color::Indexed(c)) } else if s.contains("gray") { @@ -145,372 +365,118 @@ fn get_color_foreground(s: &str) -> Option { } } -fn get_color_background(s: &str) -> Option { - let s = s.trim_start(); - let s = s.trim_end(); - if s.contains("bright color") { - let s = s.trim_start_matches("bright "); - let c = s.trim_start_matches("color").parse::().unwrap_or_default(); - Some(Color::Indexed(c.wrapping_shl(8))) - } else if s.contains("color") { - let c = s.trim_start_matches("color").parse::().unwrap_or_default(); - Some(Color::Indexed(c)) - } else if s.contains("gray") { - let s = s.trim_start_matches("bright "); - let c = 232 + s.trim_start_matches("gray").parse::().unwrap_or_default(); - Some(Color::Indexed(c.wrapping_shl(8))) - } else if s.contains("rgb") { - let s = s.trim_start_matches("bright "); - let red = (s.as_bytes()[3] as char).to_digit(10).unwrap_or_default() as u8; - let green = (s.as_bytes()[4] as char).to_digit(10).unwrap_or_default() as u8; - let blue = (s.as_bytes()[5] as char).to_digit(10).unwrap_or_default() as u8; - let c = 16 + red * 36 + green * 6 + blue; - Some(Color::Indexed(c.wrapping_shl(8))) - } else if s == "bright black" { - Some(Color::Indexed(8)) - } else if s == "bright red" { - Some(Color::Indexed(9)) - } else if s == "bright green" { - Some(Color::Indexed(10)) - } else if s == "bright yellow" { - Some(Color::Indexed(11)) - } else if s == "bright blue" { - Some(Color::Indexed(12)) - } else if s == "bright magenta" { - Some(Color::Indexed(13)) - } else if s == "bright cyan" { - Some(Color::Indexed(14)) - } else if s == "bright white" { - Some(Color::Indexed(15)) - } else if s == "black" { - Some(Color::Indexed(0)) - } else if s == "red" { - Some(Color::Indexed(1)) - } else if s == "green" { - Some(Color::Indexed(2)) - } else if s == "yellow" { - Some(Color::Indexed(3)) - } else if s == "blue" { - Some(Color::Indexed(4)) - } else if s == "magenta" { - Some(Color::Indexed(5)) - } else if s == "cyan" { - Some(Color::Indexed(6)) - } else if s == "white" { - Some(Color::Indexed(7)) - } else { - None - } -} - -impl Serialize for SerdeStyle { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - // Getting the foreground color string - let fg_str = color_to_string(self.0.fg.unwrap_or_default()); - - // Getting the background color string - let mut bg_str = color_to_string(self.0.bg.unwrap_or_default()); - - // If the background is not default, prepend with "on " - if bg_str != "" { - bg_str.insert_str(0, "on "); - } - - // Building the modifier string - let mut mod_str = String::new(); - let mod_val = self.0.add_modifier; - if mod_val.contains(Modifier::BOLD) { - mod_str.push_str("bold "); - } - if mod_val.contains(Modifier::UNDERLINED) { - mod_str.push_str("underline "); - } - if mod_val.contains(Modifier::REVERSED) { - mod_str.push_str("inverse "); - } - - // Constructing the final style string - let style_str = format!("{}{} {}", mod_str, fg_str, bg_str).trim().to_string(); - - serializer.serialize_str(&style_str) - } -} - -fn color_to_string(color: Color) -> String { - match color { - Color::Black => "black".to_string(), - Color::Red => "red".to_string(), - Color::Green => "green".to_string(), - Color::Reset => "reset".to_string(), - Color::Yellow => "yellow".to_string(), - Color::Blue => "blue".to_string(), - Color::Magenta => "magenta".to_string(), - Color::Cyan => "cyan".to_string(), - Color::Gray => "gray".to_string(), - Color::DarkGray => "darkgray".to_string(), - Color::LightRed => "lightred".to_string(), - Color::LightGreen => "lightgreen".to_string(), - Color::LightYellow => "lightyellow".to_string(), - Color::LightBlue => "lightblue".to_string(), - Color::LightMagenta => "lightmagenta".to_string(), - Color::LightCyan => "lightcyan".to_string(), - Color::White => "white".to_string(), - Color::Rgb(r, g, b) => format!("#{}{}{}", r, g, b), - Color::Indexed(u) => format!("#{}", u), - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Config { - pub tick_rate: usize, - pub keymap: HashMap, - pub enabled: bool, - pub color: HashMap, - pub filter: String, - pub data_location: String, - pub obfuscate: bool, - pub print_empty_columns: bool, - pub due: usize, - pub weekstart: bool, - pub rule_precedence_color: Vec, - pub uda_priority_values: Vec, - pub uda_tick_rate: u64, - pub uda_auto_insert_double_quotes_on_add: bool, - pub uda_auto_insert_double_quotes_on_annotate: bool, - pub uda_auto_insert_double_quotes_on_log: bool, - pub uda_prefill_task_metadata: bool, - pub uda_reset_filter_on_esc: bool, - pub uda_task_detail_prefetch: usize, - pub uda_task_report_use_all_tasks_for_completion: bool, - pub uda_task_report_show_info: bool, - pub uda_task_report_looping: bool, - pub uda_task_report_jump_to_task_on_add: bool, - pub uda_selection_indicator: String, - pub uda_mark_indicator: String, - pub uda_unmark_indicator: String, - pub uda_scrollbar_indicator: String, - pub uda_scrollbar_area: String, - pub uda_style_report_scrollbar: SerdeStyle, - pub uda_style_report_scrollbar_area: SerdeStyle, - pub uda_selection_bold: bool, - pub uda_selection_italic: bool, - pub uda_selection_dim: bool, - pub uda_selection_blink: bool, - pub uda_selection_reverse: bool, - pub uda_calendar_months_per_row: usize, - pub uda_style_context_active: SerdeStyle, - pub uda_style_report_selection: SerdeStyle, - pub uda_style_calendar_title: SerdeStyle, - pub uda_style_calendar_today: SerdeStyle, - pub uda_style_navbar: SerdeStyle, - pub uda_style_command: SerdeStyle, - pub uda_style_report_completion_pane: SerdeStyle, - pub uda_style_report_completion_pane_highlight: SerdeStyle, - pub uda_shortcuts: Vec, - pub uda_change_focus_rotate: bool, - pub uda_background_process: String, - pub uda_background_process_period: usize, - pub uda_quick_tag_name: String, - pub uda_task_report_prompt_on_undo: bool, - pub uda_task_report_prompt_on_delete: bool, - pub uda_task_report_prompt_on_done: bool, - pub uda_task_report_date_time_vague_more_precise: bool, - pub uda_context_menu_select_on_move: bool, -} - -impl Default for Config { - fn default() -> Self { - let tick_rate = 250; - - let mut task_report_keymap: KeyMap = Default::default(); - task_report_keymap.insert(parse_key_sequence("q").unwrap(), Action::Quit); - task_report_keymap.insert(parse_key_sequence("r").unwrap(), Action::Refresh); - task_report_keymap.insert(parse_key_sequence("G").unwrap(), Action::GotoBottom); - task_report_keymap.insert(parse_key_sequence("").unwrap(), Action::GotoTop); - task_report_keymap.insert(parse_key_sequence("").unwrap(), Action::GotoPageBottom); - task_report_keymap.insert(parse_key_sequence("").unwrap(), Action::GotoPageTop); - task_report_keymap.insert(parse_key_sequence("").unwrap(), Action::GotoBottom); - task_report_keymap.insert(parse_key_sequence("j").unwrap(), Action::Down); - task_report_keymap.insert(parse_key_sequence("k").unwrap(), Action::Up); - task_report_keymap.insert(parse_key_sequence("J").unwrap(), Action::PageDown); - task_report_keymap.insert(parse_key_sequence("K").unwrap(), Action::PageUp); - task_report_keymap.insert(parse_key_sequence("").unwrap(), Action::Delete); - task_report_keymap.insert(parse_key_sequence("").unwrap(), Action::Done); - task_report_keymap.insert(parse_key_sequence("s").unwrap(), Action::ToggleStartStop); - task_report_keymap.insert(parse_key_sequence("t").unwrap(), Action::QuickTag); - task_report_keymap.insert(parse_key_sequence("v").unwrap(), Action::Select); - task_report_keymap.insert(parse_key_sequence("V").unwrap(), Action::SelectAll); - task_report_keymap.insert(parse_key_sequence("u").unwrap(), Action::Undo); - task_report_keymap.insert(parse_key_sequence("e").unwrap(), Action::Edit); - task_report_keymap.insert(parse_key_sequence("m").unwrap(), Action::Modify); - task_report_keymap.insert(parse_key_sequence("!").unwrap(), Action::Shell); - task_report_keymap.insert(parse_key_sequence("l").unwrap(), Action::Log); - task_report_keymap.insert(parse_key_sequence("a").unwrap(), Action::Add); - task_report_keymap.insert(parse_key_sequence("A").unwrap(), Action::Annotate); - task_report_keymap.insert(parse_key_sequence("?").unwrap(), Action::Help); - task_report_keymap.insert(parse_key_sequence("/").unwrap(), Action::Filter); - task_report_keymap.insert(parse_key_sequence("z").unwrap(), Action::ToggleZoom); - task_report_keymap.insert(parse_key_sequence("c").unwrap(), Action::Context); - task_report_keymap.insert(parse_key_sequence("]").unwrap(), Action::Next); - task_report_keymap.insert(parse_key_sequence("[").unwrap(), Action::Previous); - task_report_keymap.insert(parse_key_sequence("1").unwrap(), Action::Shortcut(1)); - task_report_keymap.insert(parse_key_sequence("2").unwrap(), Action::Shortcut(2)); - task_report_keymap.insert(parse_key_sequence("3").unwrap(), Action::Shortcut(3)); - task_report_keymap.insert(parse_key_sequence("4").unwrap(), Action::Shortcut(4)); - task_report_keymap.insert(parse_key_sequence("5").unwrap(), Action::Shortcut(5)); - task_report_keymap.insert(parse_key_sequence("6").unwrap(), Action::Shortcut(6)); - task_report_keymap.insert(parse_key_sequence("7").unwrap(), Action::Shortcut(7)); - task_report_keymap.insert(parse_key_sequence("8").unwrap(), Action::Shortcut(8)); - task_report_keymap.insert(parse_key_sequence("9").unwrap(), Action::Shortcut(9)); - - let mut keymap: HashMap = Default::default(); - keymap.insert("task-report".into(), task_report_keymap); - - let enabled = true; - let color = Default::default(); - let filter = Default::default(); - let data_location = Default::default(); - let obfuscate = false; - let print_empty_columns = false; - let due = 7; // due 7 days - let weekstart = true; // starts on monday - let rule_precedence_color = Default::default(); - let uda_priority_values = Default::default(); - let uda_tick_rate = 250; - let uda_auto_insert_double_quotes_on_add = true; - let uda_auto_insert_double_quotes_on_annotate = true; - let uda_auto_insert_double_quotes_on_log = true; - let uda_prefill_task_metadata = Default::default(); - let uda_reset_filter_on_esc = true; - let uda_task_detail_prefetch = 10; - let uda_task_report_use_all_tasks_for_completion = Default::default(); - let uda_task_report_show_info = true; - let uda_task_report_looping = true; - let uda_task_report_jump_to_task_on_add = true; - let uda_selection_indicator = "\u{2022} ".to_string(); - let uda_mark_indicator = "\u{2714} ".to_string(); - let uda_unmark_indicator = " ".to_string(); - let uda_scrollbar_indicator = "█".to_string(); - let uda_scrollbar_area = "║".to_string(); - let uda_style_report_scrollbar = Default::default(); - let uda_style_report_scrollbar_area = Default::default(); - let uda_selection_bold = true; - let uda_selection_italic = Default::default(); - let uda_selection_dim = Default::default(); - let uda_selection_blink = Default::default(); - let uda_selection_reverse = Default::default(); - let uda_calendar_months_per_row = 4; - let uda_style_context_active = Default::default(); - let uda_style_report_selection = Default::default(); - let uda_style_calendar_title = Default::default(); - let uda_style_calendar_today = Default::default(); - let uda_style_navbar = Default::default(); - let uda_style_command = Default::default(); - let uda_style_report_completion_pane = Default::default(); - let uda_style_report_completion_pane_highlight = Default::default(); - let uda_shortcuts = Default::default(); - let uda_change_focus_rotate = Default::default(); - let uda_background_process = Default::default(); - let uda_background_process_period = Default::default(); - let uda_quick_tag_name = Default::default(); - let uda_task_report_prompt_on_undo = Default::default(); - let uda_task_report_prompt_on_delete = Default::default(); - let uda_task_report_prompt_on_done = Default::default(); - let uda_task_report_date_time_vague_more_precise = Default::default(); - let uda_context_menu_select_on_move = Default::default(); - - Self { - tick_rate, - keymap, - enabled, - color, - filter, - data_location, - obfuscate, - print_empty_columns, - due, - weekstart, - rule_precedence_color, - uda_priority_values, - uda_tick_rate, - uda_auto_insert_double_quotes_on_add, - uda_auto_insert_double_quotes_on_annotate, - uda_auto_insert_double_quotes_on_log, - uda_prefill_task_metadata, - uda_reset_filter_on_esc, - uda_task_detail_prefetch, - uda_task_report_use_all_tasks_for_completion, - uda_task_report_show_info, - uda_task_report_looping, - uda_task_report_jump_to_task_on_add, - uda_selection_indicator, - uda_mark_indicator, - uda_unmark_indicator, - uda_scrollbar_indicator, - uda_scrollbar_area, - uda_style_report_scrollbar, - uda_style_report_scrollbar_area, - uda_selection_bold, - uda_selection_italic, - uda_selection_dim, - uda_selection_blink, - uda_selection_reverse, - uda_calendar_months_per_row, - uda_style_context_active, - uda_style_report_selection, - uda_style_calendar_title, - uda_style_calendar_today, - uda_style_navbar, - uda_style_command, - uda_style_report_completion_pane, - uda_style_report_completion_pane_highlight, - uda_shortcuts, - uda_change_focus_rotate, - uda_background_process, - uda_background_process_period, - uda_quick_tag_name, - uda_task_report_prompt_on_undo, - uda_task_report_prompt_on_delete, - uda_task_report_prompt_on_done, - uda_task_report_date_time_vague_more_precise, - uda_context_menu_select_on_move, - } - } -} - -impl Config { - pub fn new() -> Result { - let config: Self = Figment::from(Serialized::defaults(Config::default())) - .merge(Toml::file(get_config_dir().join("config.toml"))) - .merge(Env::prefixed("TASKWARRIOR_TUI_")) - .extract()?; - Ok(config) - } - - pub fn write(&self, path: PathBuf) -> Result<()> { - let content = toml::to_string(&self)?; - std::fs::write(&path, content)?; - std::fs::File::open(&path)?.sync_data()?; - Ok(()) - } -} - #[cfg(test)] mod tests { + use pretty_assertions::assert_eq; + use super::*; #[test] - fn test_read_config() { - let config = Config::new().unwrap(); - dbg!(config); + fn test_parse_style_default() { + let style = parse_style(""); + assert_eq!(style, Style::default()); } - // #[test] - // fn test_write_config() { - // let config: Config = Default::default(); - // config.write("tests/data/test.toml".into()).unwrap(); - // } + #[test] + fn test_parse_style_foreground() { + let style = parse_style("red"); + assert_eq!(style.fg, Some(Color::Indexed(1))); + } + + #[test] + fn test_parse_style_background() { + let style = parse_style("on blue"); + assert_eq!(style.bg, Some(Color::Indexed(4))); + } + + #[test] + fn test_parse_style_modifiers() { + let style = parse_style("underline red on blue"); + assert_eq!(style.fg, Some(Color::Indexed(1))); + assert_eq!(style.bg, Some(Color::Indexed(4))); + } + + #[test] + fn test_process_color_string() { + let (color, modifiers) = process_color_string("underline bold inverse gray"); + assert_eq!(color, "gray"); + assert!(modifiers.contains(Modifier::UNDERLINED)); + assert!(modifiers.contains(Modifier::BOLD)); + assert!(modifiers.contains(Modifier::REVERSED)); + } + + #[test] + fn test_parse_color_rgb() { + let color = parse_color("rgb123"); + let expected = 16 + 1 * 36 + 2 * 6 + 3; + assert_eq!(color, Some(Color::Indexed(expected))); + } + + #[test] + fn test_parse_color_unknown() { + let color = parse_color("unknown"); + assert_eq!(color, None); + } + + #[test] + fn test_config() -> Result<()> { + let c = Config::new()?; + assert_eq!( + c.keybindings.get(&Mode::TaskReport).unwrap().get(&parse_key_sequence("").unwrap_or_default()).unwrap(), + &Command::Quit + ); + Ok(()) + } + + #[test] + fn test_simple_keys() { + assert_eq!(parse_key_event("a").unwrap(), KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())); + + assert_eq!(parse_key_event("enter").unwrap(), KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())); + + assert_eq!(parse_key_event("esc").unwrap(), KeyEvent::new(KeyCode::Esc, KeyModifiers::empty())); + } + + #[test] + fn test_with_modifiers() { + assert_eq!(parse_key_event("ctrl-a").unwrap(), KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)); + + assert_eq!(parse_key_event("alt-enter").unwrap(), KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)); + + assert_eq!(parse_key_event("shift-esc").unwrap(), KeyEvent::new(KeyCode::Esc, KeyModifiers::SHIFT)); + } + + #[test] + fn test_multiple_modifiers() { + assert_eq!( + parse_key_event("ctrl-alt-a").unwrap(), + KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL | KeyModifiers::ALT) + ); + + assert_eq!( + parse_key_event("ctrl-shift-enter").unwrap(), + KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL | KeyModifiers::SHIFT) + ); + } + + #[test] + fn test_reverse_multiple_modifiers() { + assert_eq!( + key_event_to_string(&KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL | KeyModifiers::ALT)), + "ctrl-alt-a".to_string() + ); + } + + #[test] + fn test_invalid_keys() { + assert!(parse_key_event("invalid-key").is_err()); + assert!(parse_key_event("ctrl-invalid-key").is_err()); + } + + #[test] + fn test_case_insensitivity() { + assert_eq!(parse_key_event("CTRL-a").unwrap(), KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)); + + assert_eq!(parse_key_event("AlT-eNtEr").unwrap(), KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)); + } } diff --git a/src/event.rs b/src/event.rs deleted file mode 100644 index 139597f..0000000 --- a/src/event.rs +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/src/help.rs b/src/help.rs deleted file mode 100644 index 01bc213..0000000 --- a/src/help.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::cmp; - -use ratatui::{ - buffer::Buffer, - layout::{Alignment, Rect}, - style::{Modifier, Style}, - text::{Line, Span, Text}, - widgets::{Block, BorderType, Borders, Clear, Paragraph, Widget}, -}; - -const TEXT: &str = include_str!("../docs/keybindings.md"); - -pub struct Help { - pub title: String, - pub scroll: u16, - pub text_height: usize, -} - -impl Help { - pub fn new() -> Self { - Self { - title: "Help".to_string(), - scroll: 0, - text_height: TEXT.lines().count(), - } - } -} - -impl Default for Help { - fn default() -> Self { - Self::new() - } -} - -impl Widget for &Help { - fn render(self, area: Rect, buf: &mut Buffer) { - let text: Vec = TEXT.lines().map(|line| Line::from(format!("{}\n", line))).collect(); - Paragraph::new(text) - .block( - Block::default() - .title(Span::styled(&self.title, Style::default().add_modifier(Modifier::BOLD))) - .borders(Borders::ALL) - .border_type(BorderType::Rounded), - ) - .alignment(Alignment::Left) - .scroll((self.scroll, 0)) - .render(area, buf); - } -} diff --git a/src/history.rs b/src/history.rs deleted file mode 100644 index 5f9bc17..0000000 --- a/src/history.rs +++ /dev/null @@ -1,132 +0,0 @@ -use std::{ - fs::File, - path::{Path, PathBuf}, -}; - -use color_eyre::eyre::{anyhow, Result}; -use rustyline::{ - error::ReadlineError, - history::{DefaultHistory, History, SearchDirection}, -}; - -pub struct HistoryContext { - history: DefaultHistory, - history_index: Option, - data_path: PathBuf, -} - -impl HistoryContext { - pub fn new(filename: &str, data_path: PathBuf) -> Self { - let history = DefaultHistory::new(); - - std::fs::create_dir_all(&data_path) - .unwrap_or_else(|_| panic!("Unable to create configuration directory in {:?}", &data_path)); - - let data_path = data_path.join(filename); - - Self { - history, - history_index: None, - data_path, - } - } - - pub fn load(&mut self) -> Result<()> { - if self.data_path.exists() { - self.history.load(&self.data_path)?; - } else { - self.history.save(&self.data_path)?; - } - self.history_index = None; - log::debug!("Loading history of length {}", self.history.len()); - Ok(()) - } - - pub fn write(&mut self) -> Result<()> { - self.history.save(&self.data_path)?; - Ok(()) - } - - pub fn history(&self) -> &DefaultHistory { - &self.history - } - - pub fn history_index(&self) -> Option { - self.history_index - } - - pub fn history_search(&mut self, buf: &str, dir: SearchDirection) -> Option { - log::debug!( - "Searching history for {:?} in direction {:?} with history index = {:?} and history len = {:?}", - buf, - dir, - self.history_index(), - self.history.len(), - ); - - if self.history.is_empty() { - log::debug!("History is empty"); - return None; - } - - let history_index = if self.history_index().is_none() { - log::debug!("History index is none"); - match dir { - SearchDirection::Forward => return None, - SearchDirection::Reverse => self.history_index = Some(self.history_len().saturating_sub(1)), - } - self.history_index.unwrap() - } else { - let hi = self.history_index().unwrap(); - - if hi == self.history.len().saturating_sub(1) && dir == SearchDirection::Forward - || hi == 0 && dir == SearchDirection::Reverse - { - return None; - } - - match dir { - SearchDirection::Reverse => hi.saturating_sub(1), - SearchDirection::Forward => hi.saturating_add(1).min(self.history_len().saturating_sub(1)), - } - }; - - log::debug!("Using history index = {} for searching", history_index); - return if let Some(history_index) = self.history.starts_with(buf, history_index, dir).unwrap() { - log::debug!("Found index {:?}", history_index); - log::debug!("Previous index {:?}", self.history_index); - self.history_index = Some(history_index.idx); - Some(history_index.entry.to_string()) - } else if buf.is_empty() { - self.history_index = Some(history_index); - Some( - self - .history - .get(history_index, SearchDirection::Forward) - .unwrap() - .unwrap() - .entry - .to_string(), - ) - } else { - log::debug!("History index = {}. Found no match.", history_index); - None - }; - } - - pub fn add(&mut self, buf: &str) { - if let Ok(x) = self.history.add(buf) { - if x { - self.reset(); - } - } - } - - pub fn reset(&mut self) { - self.history_index = None - } - - pub fn history_len(&self) -> usize { - self.history.len() - } -} diff --git a/src/keyconfig.rs b/src/keyconfig.rs deleted file mode 100644 index 1484fb1..0000000 --- a/src/keyconfig.rs +++ /dev/null @@ -1,231 +0,0 @@ -use std::{collections::HashSet, error::Error, hash::Hash}; - -use color_eyre::eyre::{anyhow, Result}; -use crossterm::event::KeyCode; -use log::{error, info, warn}; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Debug)] -pub struct KeyConfig { - 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: 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'), - } - } -} - -impl KeyConfig { - pub fn new(data: &str) -> Result { - let mut kc = Self::default(); - kc.update(data)?; - Ok(kc) - } - - pub fn update(&mut self, data: &str) -> Result<()> { - let quit = Self::get_config("uda.taskwarrior-tui.keyconfig.quit", data); - let refresh = Self::get_config("uda.taskwarrior-tui.keyconfig.refresh", data); - let go_to_bottom = Self::get_config("uda.taskwarrior-tui.keyconfig.go-to-bottom", data); - let go_to_top = Self::get_config("uda.taskwarrior-tui.keyconfig.go-to-top", data); - let down = Self::get_config("uda.taskwarrior-tui.keyconfig.down", data); - let up = Self::get_config("uda.taskwarrior-tui.keyconfig.up", data); - let page_down = Self::get_config("uda.taskwarrior-tui.keyconfig.page-down", data); - let page_up = Self::get_config("uda.taskwarrior-tui.keyconfig.page-up", data); - let delete = Self::get_config("uda.taskwarrior-tui.keyconfig.delete", data); - let done = Self::get_config("uda.taskwarrior-tui.keyconfig.done", data); - let start_stop = Self::get_config("uda.taskwarrior-tui.keyconfig.start-stop", data); - let quick_tag = Self::get_config("uda.taskwarrior-tui.keyconfig.quick-tag", data); - let select = Self::get_config("uda.taskwarrior-tui.keyconfig.select", data); - let select_all = Self::get_config("uda.taskwarrior-tui.keyconfig.select-all", data); - let undo = Self::get_config("uda.taskwarrior-tui.keyconfig.undo", data); - let edit = Self::get_config("uda.taskwarrior-tui.keyconfig.edit", data); - let modify = Self::get_config("uda.taskwarrior-tui.keyconfig.modify", data); - let shell = Self::get_config("uda.taskwarrior-tui.keyconfig.shell", data); - let log = Self::get_config("uda.taskwarrior-tui.keyconfig.log", data); - let add = Self::get_config("uda.taskwarrior-tui.keyconfig.add", data); - let annotate = Self::get_config("uda.taskwarrior-tui.keyconfig.annotate", data); - let filter = Self::get_config("uda.taskwarrior-tui.keyconfig.filter", data); - let zoom = Self::get_config("uda.taskwarrior-tui.keyconfig.zoom", data); - let context_menu = Self::get_config("uda.taskwarrior-tui.keyconfig.context-menu", data); - let next_tab = Self::get_config("uda.taskwarrior-tui.keyconfig.next-tab", data); - let previous_tab = Self::get_config("uda.taskwarrior-tui.keyconfig.previous-tab", data); - - self.quit = quit.unwrap_or(self.quit); - self.refresh = refresh.unwrap_or(self.refresh); - self.go_to_bottom = go_to_bottom.unwrap_or(self.go_to_bottom); - self.go_to_top = go_to_top.unwrap_or(self.go_to_top); - self.down = down.unwrap_or(self.down); - self.up = up.unwrap_or(self.up); - self.page_down = page_down.unwrap_or(self.page_down); - self.page_up = page_up.unwrap_or(self.page_up); - self.delete = delete.unwrap_or(self.delete); - self.done = done.unwrap_or(self.done); - self.start_stop = start_stop.unwrap_or(self.start_stop); - self.quick_tag = quick_tag.unwrap_or(self.quick_tag); - self.select = select.unwrap_or(self.select); - self.select_all = select_all.unwrap_or(self.select_all); - self.undo = undo.unwrap_or(self.undo); - self.edit = edit.unwrap_or(self.edit); - self.modify = modify.unwrap_or(self.modify); - self.shell = shell.unwrap_or(self.shell); - self.log = log.unwrap_or(self.log); - self.add = add.unwrap_or(self.add); - self.annotate = annotate.unwrap_or(self.annotate); - self.filter = filter.unwrap_or(self.filter); - self.zoom = zoom.unwrap_or(self.zoom); - self.context_menu = context_menu.unwrap_or(self.context_menu); - self.next_tab = next_tab.unwrap_or(self.next_tab); - self.previous_tab = previous_tab.unwrap_or(self.previous_tab); - - self.check() - } - - pub fn check(&self) -> Result<()> { - let mut elements = vec![ - &self.quit, - &self.refresh, - &self.go_to_bottom, - &self.go_to_top, - &self.down, - &self.up, - &self.page_down, - &self.page_up, - &self.delete, - &self.done, - &self.select, - &self.select_all, - &self.start_stop, - &self.quick_tag, - &self.undo, - &self.edit, - &self.modify, - &self.shell, - &self.log, - &self.add, - &self.annotate, - &self.help, - &self.filter, - &self.zoom, - &self.context_menu, - &self.next_tab, - &self.previous_tab, - ]; - let l = elements.len(); - elements.dedup(); - if l == elements.len() { - Ok(()) - } else { - Err(anyhow!("Duplicate keys found in key config")) - } - } - - 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 has_just_one_char(&line) { - return Some(KeyCode::Char(line.chars().next().unwrap())); - } else { - error!("Found multiple characters in {} for {}", line, config); - } - } else if line.starts_with(&config.replace('-', "_")) { - let line = line - .trim_start_matches(&config.replace('-', "_")) - .trim_start() - .trim_end() - .to_string(); - if has_just_one_char(&line) { - return Some(KeyCode::Char(line.chars().next().unwrap())); - } else { - error!("Found multiple characters in {} for {}", line, config); - } - } - } - None - } -} - -fn has_just_one_char(s: &str) -> bool { - let mut chars = s.chars(); - chars.next().is_some() && chars.next().is_none() -} - -#[cfg(test)] -mod tests { - use super::*; -} diff --git a/src/keyevent.rs b/src/keyevent.rs deleted file mode 100644 index ce24b39..0000000 --- a/src/keyevent.rs +++ /dev/null @@ -1,307 +0,0 @@ -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MediaKeyCode, ModifierKeyCode}; - -fn parse_key_event(raw: &str) -> Result { - let raw_lower = raw.to_ascii_lowercase(); - let (remaining, modifiers) = extract_modifiers(&raw_lower); - parse_key_code_with_modifiers(remaining, modifiers) -} - -fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) { - let mut modifiers = KeyModifiers::empty(); - let mut current = raw; - - loop { - match current { - rest if rest.starts_with("ctrl-") => { - modifiers.insert(KeyModifiers::CONTROL); - current = &rest[5..]; - } - rest if rest.starts_with("alt-") => { - modifiers.insert(KeyModifiers::ALT); - current = &rest[4..]; - } - rest if rest.starts_with("shift-") => { - modifiers.insert(KeyModifiers::SHIFT); - current = &rest[6..]; - } - _ => break, // break out of the loop if no known prefix is detected - }; - } - - (current, modifiers) -} - -fn parse_key_code_with_modifiers(raw: &str, mut modifiers: KeyModifiers) -> Result { - let c = match raw { - "esc" => KeyCode::Esc, - "enter" => KeyCode::Enter, - "left" => KeyCode::Left, - "right" => KeyCode::Right, - "up" => KeyCode::Up, - "down" => KeyCode::Down, - "home" => KeyCode::Home, - "end" => KeyCode::End, - "pageup" => KeyCode::PageUp, - "pagedown" => KeyCode::PageDown, - "backtab" => { - modifiers.insert(KeyModifiers::SHIFT); - KeyCode::BackTab - } - "backspace" => KeyCode::Backspace, - "delete" => KeyCode::Delete, - "insert" => KeyCode::Insert, - "f1" => KeyCode::F(1), - "f2" => KeyCode::F(2), - "f3" => KeyCode::F(3), - "f4" => KeyCode::F(4), - "f5" => KeyCode::F(5), - "f6" => KeyCode::F(6), - "f7" => KeyCode::F(7), - "f8" => KeyCode::F(8), - "f9" => KeyCode::F(9), - "f10" => KeyCode::F(10), - "f11" => KeyCode::F(11), - "f12" => KeyCode::F(12), - "space" => KeyCode::Char(' '), - "tab" => KeyCode::Tab, - c if c.len() == 1 => { - let mut c = c.chars().next().unwrap(); - if modifiers.contains(KeyModifiers::SHIFT) { - c = c.to_ascii_uppercase(); - } - KeyCode::Char(c) - } - _ => return Err(format!("Unable to parse {raw}")), - }; - Ok(KeyEvent::new(c, modifiers)) -} - -pub fn parse_key_sequence(raw: &str) -> Result, String> { - if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() { - return Err(format!("Unable to parse `{}`", raw)); - } - let raw = if !raw.contains("><") { - let raw = raw.strip_prefix("<").unwrap_or(raw); - let raw = raw.strip_prefix(">").unwrap_or(raw); - raw - } else { - raw - }; - let sequences = raw - .split("><") - .map(|seq| { - if seq.starts_with('<') { - &seq[1..] - } else if seq.ends_with('>') { - &seq[..seq.len() - 1] - } else { - seq - } - }) - .collect::>(); - - sequences.into_iter().map(parse_key_event).collect() -} - -pub fn key_event_to_string(event: KeyEvent) -> String { - let mut result = String::new(); - - result.push('<'); - - // Add modifiers - if event.modifiers.contains(KeyModifiers::CONTROL) { - result.push_str("ctrl-"); - } - if event.modifiers.contains(KeyModifiers::ALT) { - result.push_str("alt-"); - } - if event.modifiers.contains(KeyModifiers::SHIFT) { - result.push_str("shift-"); - } - - match event.code { - KeyCode::Char(' ') => result.push_str("space"), - KeyCode::Char(c) => result.push(c), - KeyCode::Enter => result.push_str("enter"), - KeyCode::Esc => result.push_str("esc"), - KeyCode::Left => result.push_str("left"), - KeyCode::Right => result.push_str("right"), - KeyCode::Up => result.push_str("up"), - KeyCode::Down => result.push_str("down"), - KeyCode::Home => result.push_str("home"), - KeyCode::End => result.push_str("end"), - KeyCode::PageUp => result.push_str("pageup"), - KeyCode::PageDown => result.push_str("pagedown"), - KeyCode::BackTab => result.push_str("backtab"), - KeyCode::Delete => result.push_str("delete"), - KeyCode::Insert => result.push_str("insert"), - KeyCode::F(n) => result.push_str(&format!("f{}", n)), - KeyCode::Backspace => result.push_str("backspace"), - KeyCode::Tab => result.push_str("tab"), - KeyCode::Null => result.push_str("null"), - KeyCode::CapsLock => result.push_str("capslock"), - KeyCode::ScrollLock => result.push_str("scrolllock"), - KeyCode::NumLock => result.push_str("numlock"), - KeyCode::PrintScreen => result.push_str("printscreen"), - KeyCode::Pause => result.push_str("pause"), - KeyCode::Menu => result.push_str("menu"), - KeyCode::KeypadBegin => result.push_str("keypadbegin"), - KeyCode::Media(media) => match media { - MediaKeyCode::Play => result.push_str("play"), - MediaKeyCode::Pause => result.push_str("pause"), - MediaKeyCode::PlayPause => result.push_str("playpause"), - MediaKeyCode::Reverse => result.push_str("reverse"), - MediaKeyCode::Stop => result.push_str("stop"), - MediaKeyCode::FastForward => result.push_str("fastforward"), - MediaKeyCode::Rewind => result.push_str("rewind"), - MediaKeyCode::TrackNext => result.push_str("tracknext"), - MediaKeyCode::TrackPrevious => result.push_str("trackprevious"), - MediaKeyCode::Record => result.push_str("record"), - MediaKeyCode::LowerVolume => result.push_str("lowervolume"), - MediaKeyCode::RaiseVolume => result.push_str("raisevolume"), - MediaKeyCode::MuteVolume => result.push_str("mutevolume"), - }, - KeyCode::Modifier(keycode) => match keycode { - ModifierKeyCode::LeftShift => result.push_str("leftshift"), - ModifierKeyCode::LeftControl => result.push_str("leftcontrol"), - ModifierKeyCode::LeftAlt => result.push_str("leftalt"), - ModifierKeyCode::LeftSuper => result.push_str("leftsuper"), - ModifierKeyCode::LeftHyper => result.push_str("lefthyper"), - ModifierKeyCode::LeftMeta => result.push_str("leftmeta"), - ModifierKeyCode::RightShift => result.push_str("rightshift"), - ModifierKeyCode::RightControl => result.push_str("rightcontrol"), - ModifierKeyCode::RightAlt => result.push_str("rightalt"), - ModifierKeyCode::RightSuper => result.push_str("rightsuper"), - ModifierKeyCode::RightHyper => result.push_str("righthyper"), - ModifierKeyCode::RightMeta => result.push_str("rightmeta"), - ModifierKeyCode::IsoLevel3Shift => result.push_str("isolevel3shift"), - ModifierKeyCode::IsoLevel5Shift => result.push_str("isolevel5shift"), - }, - } - - result.push('>'); - - result -} - -#[cfg(test)] -mod tests { - use pretty_assertions::assert_eq; - - use super::*; - - fn test_event_to_string() { - let event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL); - println!("{}", key_event_to_string(event)); // Outputs: ctrl-a - } - - #[test] - fn test_single_key_sequence() { - let result = parse_key_sequence("a"); - assert_eq!( - result.unwrap(), - vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())] - ); - - let result = parse_key_sequence(""); - assert_eq!( - result.unwrap(), - vec![ - KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()), - KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()) - ] - ); - - let result = parse_key_sequence(""); - assert_eq!( - result.unwrap(), - vec![ - KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL), - KeyEvent::new(KeyCode::Char('b'), KeyModifiers::ALT) - ] - ); - let result = parse_key_sequence(""); - assert_eq!( - result.unwrap(), - vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),] - ); - let result = parse_key_sequence(""); - assert_eq!( - result.unwrap(), - vec![KeyEvent::new( - KeyCode::Char('a'), - KeyModifiers::CONTROL | KeyModifiers::ALT - ),] - ); - assert!(parse_key_sequence("Ctrl-a>").is_err()); - assert!(parse_key_sequence(", Action>); - -impl Deref for KeyMap { - type Target = std::collections::HashMap, Action>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for KeyMap { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl KeyMap { - pub fn validate(&self) -> Result<(), String> { - let mut sorted_sequences: Vec<_> = self.keys().collect(); - sorted_sequences.sort_by_key(|seq| seq.len()); - - for i in 0..sorted_sequences.len() { - for j in i + 1..sorted_sequences.len() { - if sorted_sequences[j].starts_with(sorted_sequences[i]) { - return Err(format!( - "Conflict detected: Sequence {:?} is a prefix of sequence {:?}", - sorted_sequences[i], sorted_sequences[j] - )); - } - } - } - - Ok(()) - } -} - -impl Serialize for KeyMap { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - // Begin serializing a map. - let mut map = serializer.serialize_map(Some(self.0.len()))?; - - for (key_sequence, action) in &self.0 { - let key_string = key_sequence - .iter() - .map(|key_event| key_event_to_string(*key_event)) - .collect::>() - .join(""); - - map.serialize_entry(&key_string, action)?; - } - - // End serialization. - map.end() - } -} - -impl<'de> Deserialize<'de> for KeyMap { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct KeyMapVisitor; - - impl<'de> Visitor<'de> for KeyMapVisitor { - type Value = KeyMap; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a keymap with string representation of KeyEvent as key and Action as value") - } - - fn visit_map(self, mut access: M) -> Result - where - M: MapAccess<'de>, - { - let mut keymap = std::collections::HashMap::new(); - - // While there are entries in the map, read them - while let Some((key_sequence_str, action)) = access.next_entry::()? { - let key_sequence = parse_key_sequence(&key_sequence_str).map_err(de::Error::custom)?; - - if let Some(old_action) = keymap.insert(key_sequence, action.clone()) { - if old_action != action { - return Err(format!( - "Found a {:?} for both {:?} and {:?}", - key_sequence_str, old_action, action - )) - .map_err(de::Error::custom); - } - } - } - - Ok(KeyMap(keymap)) - } - } - deserializer.deserialize_map(KeyMapVisitor) - } -} - -#[cfg(test)] -mod validate_tests { - use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; - - use super::*; - - #[test] - fn test_no_conflict() { - let mut map = std::collections::HashMap::new(); - map.insert( - vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())], - Action::Quit, - ); - map.insert( - vec![KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty())], - Action::Quit, - ); - let keymap = KeyMap(map); - - assert!(keymap.validate().is_ok()); - } - - #[test] - fn test_conflict_prefix() { - let mut map = std::collections::HashMap::new(); - map.insert( - vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())], - Action::Quit, - ); - map.insert( - vec![ - KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()), - KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()), - ], - Action::Quit, - ); - let keymap = KeyMap(map); - - assert!(keymap.validate().is_err()); - } - - #[test] - fn test_no_conflict_different_modifiers() { - let mut map = std::collections::HashMap::new(); - map.insert( - vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)], - Action::Quit, - ); - map.insert(vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::ALT)], Action::Quit); - let keymap = KeyMap(map); - - assert!(keymap.validate().is_ok()); - } - - #[test] - fn test_no_conflict_multiple_keys() { - let mut map = std::collections::HashMap::new(); - map.insert( - vec![ - KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()), - KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()), - ], - Action::Quit, - ); - map.insert( - vec![ - KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()), - KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()), - ], - Action::Quit, - ); - let keymap = KeyMap(map); - - assert!(keymap.validate().is_ok()); - } - - #[test] - fn test_conflict_three_keys() { - let mut map = std::collections::HashMap::new(); - map.insert( - vec![ - KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()), - KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()), - KeyEvent::new(KeyCode::Char('c'), KeyModifiers::empty()), - ], - Action::Quit, - ); - map.insert( - vec![ - KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()), - KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()), - ], - Action::Quit, - ); - let keymap = KeyMap(map); - - assert!(keymap.validate().is_err()); - } -} diff --git a/src/main.rs b/src/main.rs index cd9ece4..e03cdaf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,111 +1,38 @@ #![allow(dead_code)] #![allow(unused_imports)] #![allow(unused_variables)] -#![allow(clippy::too_many_arguments)] -mod action; -mod app; -mod calendar; -mod cli; -mod completion; -mod config; -mod help; -mod history; -mod keyconfig; -mod keyevent; -mod keymap; -mod pane; -mod scrollbar; -mod table; -mod task_report; -mod traits; -mod tui; -mod ui; -mod utils; +pub mod cli; +pub mod command; +pub mod components; +pub mod config; +pub mod runner; +pub mod tui; +pub mod utils; -use std::{ - env, - error::Error, - io::{self, Write}, - panic, - path::{Path, PathBuf}, - time::Duration, +use clap::Parser; +use cli::Cli; +use color_eyre::eyre::Result; + +use crate::{ + runner::Runner, + utils::{initialize_logging, initialize_panic_handler, version}, }; -// use app::{Mode, TaskwarriorTui}; -use color_eyre::eyre::Result; -use utils::{absolute_path, get_config_dir, get_data_dir, initialize_logging, initialize_panic_handler}; -// use crossterm::{ -// cursor, -// event::{DisableMouseCapture, EnableMouseCapture, EventStream}, -// execute, -// terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}, -// }; -// use futures::stream::{FuturesUnordered, StreamExt}; -// use log::{debug, error, info, log_enabled, trace, warn, Level, LevelFilter}; -// use ratatui::{backend::CrosstermBackend, Terminal}; -// use utils::{get_config_dir, get_data_dir}; +async fn tokio_main() -> Result<()> { + initialize_logging()?; -// use crate::{ -// action::Action, -// keyconfig::KeyConfig, -// utils::{initialize_logging, initialize_panic_handler}, -// }; -// -// const LOG_PATTERN: &str = "{d(%Y-%m-%d %H:%M:%S)} | {l} | {f}:{L} | {m}{n}"; + initialize_panic_handler()?; + + let args = Cli::parse(); + let mut runner = Runner::new(args.tick_rate, args.frame_rate)?; + runner.run().await?; + + Ok(()) +} #[tokio::main] async fn main() -> Result<()> { - let matches = cli::generate_cli_app().get_matches(); - - let config = matches.get_one::("config"); - let data = matches.get_one::("data"); - let taskrc = matches.get_one::("taskrc"); - let taskdata = matches.get_one::("taskdata"); - let binding = String::from("next"); - let report = matches.get_one::("report").unwrap_or(&binding); - - let config_dir = config.map(PathBuf::from).unwrap_or_else(get_config_dir); - let data_dir = data.map(PathBuf::from).unwrap_or_else(get_data_dir); - - if let Some(e) = taskrc { - if env::var("TASKRC").is_err() { - // if environment variable is not set, this env::var returns an error - env::set_var( - "TASKRC", - absolute_path(PathBuf::from(e)).expect("Unable to get path for taskrc"), - ) - } else { - log::warn!("TASKRC environment variable cannot be set.") - } - } - - if let Some(e) = taskdata { - if env::var("TASKDATA").is_err() { - // if environment variable is not set, this env::var returns an error - env::set_var( - "TASKDATA", - absolute_path(PathBuf::from(e)).expect("Unable to get path for taskdata"), - ) - } else { - log::warn!("TASKDATA environment variable cannot be set.") - } - } - - initialize_logging()?; - initialize_panic_handler()?; - - log::info!("getting matches from clap..."); - log::debug!("report = {:?}", &report); - log::debug!("config = {:?}", &config); - - let mut app = app::TaskwarriorTui::new(report)?; - - let r = app.run().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); - } + tokio_main().await.unwrap(); Ok(()) } diff --git a/src/pane/context.rs b/src/pane/context.rs deleted file mode 100644 index 786f786..0000000 --- a/src/pane/context.rs +++ /dev/null @@ -1,148 +0,0 @@ -use std::fmt; - -use color_eyre::eyre::{anyhow, Context as AnyhowContext, Result}; - -const NAME: &str = "Name"; -const TYPE: &str = "Remaining"; -const DEFINITION: &str = "Avg age"; -const ACTIVE: &str = "Complete"; - -use std::{ - cmp, - cmp::min, - collections::{HashMap, HashSet}, - error::Error, - process::{Command, Output}, -}; - -use chrono::{Datelike, Duration, Local, Month, NaiveDate, NaiveDateTime, TimeZone}; -use itertools::Itertools; -use ratatui::{ - buffer::Buffer, - layout::{Alignment, Rect}, - style::{Color, Modifier, Style}, - symbols, - text::{Line, Span, Text}, - widgets::{Block, BorderType, Borders, Clear, Paragraph, StatefulWidget, Widget}, -}; -use uuid::Uuid; - -use crate::{ - action::Action, - app::{Mode, TaskwarriorTui}, - pane::Pane, - table::TableState, -}; - -#[derive(Debug, Clone, Default)] -pub struct ContextDetails { - pub name: String, - pub definition: String, - pub active: String, - pub type_: String, -} - -impl ContextDetails { - pub fn new(name: String, definition: String, active: String, type_: String) -> Self { - Self { - name, - definition, - active, - type_, - } - } -} - -pub struct ContextsState { - pub table_state: TableState, - pub report_height: u16, - pub columns: Vec, - pub rows: Vec, -} - -impl ContextsState { - pub(crate) fn new() -> Self { - Self { - table_state: TableState::default(), - report_height: 0, - columns: vec![ - NAME.to_string(), - TYPE.to_string(), - DEFINITION.to_string(), - ACTIVE.to_string(), - ], - rows: vec![], - } - } - - pub fn simplified_view(&mut self) -> (Vec>, Vec) { - let rows = self - .rows - .iter() - .map(|c| vec![c.name.clone(), c.type_.clone(), c.definition.clone(), c.active.clone()]) - .collect(); - let headers = self.columns.clone(); - (rows, headers) - } - - pub fn len(&self) -> usize { - self.rows.len() - } - - pub fn update_data(&mut self) -> Result<()> { - let output = Command::new("task").arg("context").output()?; - let data = String::from_utf8_lossy(&output.stdout); - - self.rows = vec![]; - for (i, line) in data.trim().split('\n').enumerate() { - if line.starts_with(" ") && line.trim().starts_with("write") { - continue; - } - if line.starts_with(" ") && !(line.trim().ends_with("yes") || line.trim().ends_with("no")) { - let definition = line.trim(); - if let Some(c) = self.rows.last_mut() { - c.definition = format!("{} {}", c.definition, definition); - } - continue; - } - let line = line.trim(); - if line.is_empty() || line == "Use 'task context none' to unset the current context." { - continue; - } - if i == 0 || i == 1 { - continue; - } - let mut s = line.split_whitespace(); - let name = s.next().unwrap_or_default(); - let typ = s.next().unwrap_or_default(); - let active = s.last().unwrap_or_default(); - let definition = line.replacen(name, "", 1); - let definition = definition.replacen(typ, "", 1); - let definition = definition.strip_suffix(active).unwrap_or_default(); - let context = ContextDetails::new( - name.to_string(), - definition.trim().to_string(), - active.to_string(), - typ.to_string(), - ); - self.rows.push(context); - } - if self.rows.iter().any(|r| r.active != "no") { - self.rows.insert( - 0, - ContextDetails::new("none".to_string(), "".to_string(), "no".to_string(), "read".to_string()), - ); - } else { - self.rows.insert( - 0, - ContextDetails::new( - "none".to_string(), - "".to_string(), - "yes".to_string(), - "read".to_string(), - ), - ); - } - Ok(()) - } -} diff --git a/src/pane/mod.rs b/src/pane/mod.rs deleted file mode 100644 index 2fc6827..0000000 --- a/src/pane/mod.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::ops::Index; - -use color_eyre::eyre::Result; -use crossterm::event::KeyEvent; - -use crate::{ - action::Action, - app::{Mode, TaskwarriorTui}, - tui::Event, -}; - -pub mod context; -pub mod project; - -pub trait Pane { - fn handle_input(app: &mut TaskwarriorTui, input: KeyEvent) -> Result<()>; - fn change_focus_to_left_pane(app: &mut TaskwarriorTui) { - match app.mode { - Mode::Projects => app.mode = Mode::TaskReport, - Mode::Calendar => { - app.mode = Mode::Projects; - } - _ => { - if app.config.uda_change_focus_rotate { - app.mode = Mode::Calendar; - } - } - } - } - fn change_focus_to_right_pane(app: &mut TaskwarriorTui) { - match app.mode { - Mode::Projects => app.mode = Mode::Calendar, - Mode::Calendar => { - if app.config.uda_change_focus_rotate { - app.mode = Mode::TaskReport; - } - } - _ => app.mode = Mode::Projects, - } - } -} diff --git a/src/pane/project.rs b/src/pane/project.rs deleted file mode 100644 index 2940070..0000000 --- a/src/pane/project.rs +++ /dev/null @@ -1,202 +0,0 @@ -use std::fmt; - -use color_eyre::eyre::{anyhow, Context as AnyhowContext, Result}; -use crossterm::event::{KeyCode, KeyEvent}; - -const COL_WIDTH: usize = 21; -const PROJECT_HEADER: &str = "Name"; -const REMAINING_TASK_HEADER: &str = "Remaining"; -const AVG_AGE_HEADER: &str = "Avg age"; -const COMPLETE_HEADER: &str = "Complete"; - -use std::{ - cmp::min, - collections::{HashMap, HashSet}, - error::Error, - process::{Command, Output}, -}; - -use chrono::{Datelike, Duration, Local, Month, NaiveDate, NaiveDateTime, TimeZone}; -use itertools::Itertools; -use ratatui::{ - buffer::Buffer, - layout::Rect, - style::{Color, Modifier, Style}, - symbols, - widgets::{Block, Widget}, -}; -use task_hookrs::project::Project; -use uuid::Uuid; - -use crate::{ - action::Action, - app::{Mode, TaskwarriorTui}, - pane::Pane, - table::TableState, - utils::Changeset, -}; - -pub struct ProjectsState { - pub(crate) list: Vec, - pub table_state: TableState, - pub current_selection: usize, - pub marked: HashSet, - pub columns: Vec, - pub rows: Vec, - pub data: String, -} - -#[derive(Debug, Clone, Default)] -pub struct ProjectDetails { - name: Project, - remaining: usize, - avg_age: String, - complete: String, -} - -impl ProjectsState { - pub(crate) fn new() -> Self { - Self { - list: Vec::default(), - table_state: TableState::default(), - current_selection: 0, - marked: HashSet::default(), - columns: vec![ - PROJECT_HEADER.to_string(), - REMAINING_TASK_HEADER.to_string(), - AVG_AGE_HEADER.to_string(), - COMPLETE_HEADER.to_string(), - ], - data: Default::default(), - rows: vec![], - } - } - - fn pattern_by_marked(app: &mut TaskwarriorTui) -> String { - let mut project_pattern = String::new(); - if !app.projects.marked.is_empty() { - for (idx, project) in app.projects.marked.clone().iter().enumerate() { - let mut input: String = String::from(project); - if input.as_str() == "(none)" { - input = " ".to_string(); - } - if idx == 0 { - project_pattern = format!("\'(project:{}", input); - } else { - project_pattern = format!("{} or project:{}", project_pattern, input); - } - } - project_pattern = format!("{})\'", project_pattern); - } - project_pattern - } - - pub fn toggle_mark(&mut self) { - if !self.list.is_empty() { - let selected = self.current_selection; - if !self.marked.insert(self.list[selected].clone()) { - self.marked.remove(self.list[selected].as_str()); - } - } - } - - pub fn simplified_view(&mut self) -> (Vec>, Vec) { - let rows = self - .rows - .iter() - .map(|c| { - vec![ - c.name.clone(), - c.remaining.to_string(), - c.avg_age.to_string(), - c.complete.clone(), - ] - }) - .collect(); - let headers = self.columns.clone(); - (rows, headers) - } - - pub fn last_line(&self, line: &str) -> bool { - let words = line.trim().split(' ').map(|s| s.trim()).collect::>(); - return words.len() == 2 - && words[0].chars().map(|c| c.is_numeric()).all(|b| b) - && (words[1] == "project" || words[1] == "projects"); - } - - pub fn update_data(&mut self) -> Result<()> { - self.list.clear(); - self.rows.clear(); - let output = Command::new("task") - .arg("summary") - .output() - .context("Unable to run `task summary`") - .unwrap(); - let data = String::from_utf8_lossy(&output.stdout); - self.data = data.into(); - Ok(()) - } - - fn update_table_state(&mut self) { - self.table_state.select(Some(self.current_selection)); - if self.marked.is_empty() { - self.table_state.single_selection(); - } else { - self.table_state.multiple_selection(); - self.table_state.clear(); - for project in &self.marked { - let index = self.list.iter().position(|x| x == project); - self.table_state.mark(index); - } - } - } -} - -impl Pane for ProjectsState { - fn handle_input(app: &mut TaskwarriorTui, input: KeyEvent) -> Result<()> { - if input.code == app.keyconfig.quit { - // || input == KeyCode::Ctrl('c') { - app.should_quit = true; - } else if input.code == app.keyconfig.next_tab { - Self::change_focus_to_right_pane(app); - } else if input.code == app.keyconfig.previous_tab { - Self::change_focus_to_left_pane(app); - } else if input.code == KeyCode::Down || input.code == app.keyconfig.down { - self::focus_on_next_project(app); - } else if input.code == KeyCode::Up || input.code == app.keyconfig.up { - self::focus_on_previous_project(app); - } else if input.code == app.keyconfig.select { - self::update_task_filter_by_selection(app)?; - } - app.projects.update_table_state(); - Ok(()) - } -} - -fn focus_on_next_project(app: &mut TaskwarriorTui) { - if app.projects.current_selection < app.projects.list.len().saturating_sub(1) { - app.projects.current_selection += 1; - app.projects.table_state.select(Some(app.projects.current_selection)); - } -} - -fn focus_on_previous_project(app: &mut TaskwarriorTui) { - if app.projects.current_selection >= 1 { - app.projects.current_selection -= 1; - app.projects.table_state.select(Some(app.projects.current_selection)); - } -} - -fn update_task_filter_by_selection(app: &mut TaskwarriorTui) -> Result<()> { - app.projects.table_state.multiple_selection(); - let last_project_pattern = ProjectsState::pattern_by_marked(app); - app.projects.toggle_mark(); - let new_project_pattern = ProjectsState::pattern_by_marked(app); - let current_filter = app.filter.value(); - app.filter_history.add(current_filter); - - let mut filter = current_filter.replace(&last_project_pattern, ""); - filter = format!("{}{}", filter, new_project_pattern); - app.filter = app.filter.clone().with_value(filter); - Ok(()) -} diff --git a/src/runner.rs b/src/runner.rs new file mode 100644 index 0000000..32954e4 --- /dev/null +++ b/src/runner.rs @@ -0,0 +1,109 @@ +use color_eyre::eyre::Result; +use tokio::sync::mpsc; + +use crate::{ + command::Command, + components::{app::App, Component}, + config::Config, + tui, +}; + +pub struct Runner { + pub config: Config, + pub tick_rate: f64, + pub frame_rate: f64, + pub components: Vec>, + pub should_quit: bool, + pub should_suspend: bool, +} + +impl Runner { + pub fn new(tick_rate: f64, frame_rate: f64) -> Result { + let app = App::new(); + let config = Config::new()?; + let app = app.keybindings(config.keybindings.clone()); + Ok(Self { + tick_rate, + frame_rate, + components: vec![Box::new(app)], + should_quit: false, + should_suspend: false, + config, + }) + } + + pub async fn run(&mut self) -> Result<()> { + let (command_tx, mut command_rx) = mpsc::unbounded_channel(); + + let mut tui = tui::Tui::new()?; + tui.tick_rate(self.tick_rate); + tui.frame_rate(self.frame_rate); + tui.enter()?; + + for component in self.components.iter_mut() { + component.register_command_handler(command_tx.clone())?; + } + + for component in self.components.iter_mut() { + component.init()?; + } + + loop { + if let Some(e) = tui.next().await { + match e { + tui::Event::Quit => command_tx.send(Command::Quit)?, + tui::Event::Tick => command_tx.send(Command::Tick)?, + tui::Event::Render => command_tx.send(Command::Render)?, + tui::Event::Resize(x, y) => command_tx.send(Command::Resize(x, y))?, + e => { + for component in self.components.iter_mut() { + if let Some(command) = component.handle_events(Some(e.clone()))? { + command_tx.send(command)?; + } + } + }, + } + } + + while let Ok(command) = command_rx.try_recv() { + if command != Command::Tick && command != Command::Render { + log::debug!("{command:?}"); + } + match command { + Command::Quit => self.should_quit = true, + Command::Suspend => self.should_suspend = true, + Command::Resume => self.should_suspend = false, + Command::Render => { + tui.draw(|f| { + for component in self.components.iter_mut() { + let r = component.draw(f, f.size()); + if let Err(e) = r { + command_tx.send(Command::Error(format!("Failed to draw: {:?}", e))).unwrap(); + } + } + })?; + }, + _ => {}, + } + for component in self.components.iter_mut() { + if let Some(command) = component.update(command.clone())? { + command_tx.send(command)? + }; + } + } + if self.should_suspend { + tui.suspend()?; + command_tx.send(Command::Resume)?; + tui = tui::Tui::new()?; + tui.tick_rate(self.tick_rate); + tui.frame_rate(self.frame_rate); + tui.enter()?; + } else if self.should_quit { + tui.stop()?; + break; + } + } + tui.exit()?; + Ok(()) + } +} diff --git a/src/scrollbar.rs b/src/scrollbar.rs deleted file mode 100644 index 4149d91..0000000 --- a/src/scrollbar.rs +++ /dev/null @@ -1,63 +0,0 @@ -use ratatui::{ - backend::Backend, - buffer::Buffer, - layout::{Margin, Rect}, - style::{Color, Style}, - symbols::{block::FULL, line::DOUBLE_VERTICAL}, - widgets::Widget, - Frame, -}; - -pub struct Scrollbar { - pub pos: u16, - pub len: u16, - pub pos_style: Style, - pub pos_symbol: String, - pub area_style: Style, - pub area_symbol: String, -} - -impl Scrollbar { - pub fn new(pos: usize, len: usize) -> Self { - Self { - pos: pos as u16, - len: len as u16, - pos_style: Style::default(), - pos_symbol: FULL.to_string(), - area_style: Style::default(), - area_symbol: DOUBLE_VERTICAL.to_string(), - } - } -} - -impl Widget for Scrollbar { - fn render(self, area: Rect, buf: &mut Buffer) { - if area.height <= 2 { - return; - } - - if self.len == 0 { - return; - } - - let right = area.right().saturating_sub(1); - - if right <= area.left() { - return; - }; - - let (top, height) = { (area.top() + 3, area.height.saturating_sub(4)) }; - - for y in top..(top + height) { - buf.set_string(right, y, self.area_symbol.clone(), self.area_style); - } - - let progress = self.pos as f64 / self.len as f64; - let progress = if progress > 1.0 { 1.0 } else { progress }; - let pos = height as f64 * progress; - - let pos = pos as i64 as u16; - - buf.set_string(right, top + pos, self.pos_symbol, self.pos_style); - } -} diff --git a/src/table.rs b/src/table.rs deleted file mode 100644 index 9777fac..0000000 --- a/src/table.rs +++ /dev/null @@ -1,537 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - fmt::Display, - iter::{self, Iterator}, -}; - -use cassowary::{ - strength::{MEDIUM, REQUIRED, WEAK}, - Expression, Solver, - WeightedRelation::{EQ, GE, LE}, -}; -use ratatui::{ - buffer::Buffer, - layout::{Constraint, Rect}, - style::Style, - widgets::{Block, StatefulWidget, Widget}, -}; -use unicode_segmentation::{Graphemes, UnicodeSegmentation}; -use unicode_width::UnicodeWidthStr; - -#[derive(Debug, Clone)] -pub enum TableMode { - SingleSelection, - MultipleSelection, -} - -#[derive(Clone)] -pub struct TableState { - offset: usize, - current_selection: Option, - marked: HashSet, - mode: TableMode, -} - -impl Default for TableState { - fn default() -> TableState { - TableState { - offset: 0, - current_selection: Some(0), - marked: HashSet::new(), - mode: TableMode::SingleSelection, - } - } -} - -impl TableState { - pub fn mode(&self) -> TableMode { - self.mode.clone() - } - - pub fn multiple_selection(&mut self) { - self.mode = TableMode::MultipleSelection; - } - - pub fn single_selection(&mut self) { - self.mode = TableMode::SingleSelection; - } - - pub fn current_selection(&self) -> Option { - self.current_selection - } - - pub fn select(&mut self, index: Option) { - self.current_selection = index; - if index.is_none() { - self.offset = 0; - } - } - - pub fn mark(&mut self, index: Option) { - if let Some(i) = index { - self.marked.insert(i); - } - } - - pub fn unmark(&mut self, index: Option) { - if let Some(i) = index { - self.marked.remove(&i); - } - } - - pub fn toggle_mark(&mut self, index: Option) { - if let Some(i) = index { - if !self.marked.insert(i) { - self.marked.remove(&i); - } - } - } - - pub fn marked(&self) -> std::collections::hash_set::Iter { - self.marked.iter() - } - - pub fn clear(&mut self) { - self.marked.drain().for_each(drop); - } -} - -/// Holds data to be displayed in a Table widget -#[derive(Debug, Clone)] -pub enum Row -where - D: Iterator, - D::Item: Display, -{ - Data(D), - StyledData(D, Style), -} - -/// A widget to display data in formatted columns -/// -/// # Examples -/// -/// ```rust -/// # use ratatui::widgets::{Block, Borders, Table, Row}; -/// # use ratatui::layout::Constraint; -/// # use ratatui::style::{Style, Color}; -/// let row_style = Style::default().fg(Color::White); -/// Table::new( -/// ["Col1", "Col2", "Col3"].into_iter(), -/// vec![ -/// Row::StyledData(["Row11", "Row12", "Row13"].into_iter(), row_style), -/// Row::StyledData(["Row21", "Row22", "Row23"].into_iter(), row_style), -/// Row::StyledData(["Row31", "Row32", "Row33"].into_iter(), row_style), -/// Row::Data(["Row41", "Row42", "Row43"].into_iter()) -/// ].into_iter() -/// ) -/// .block(Block::default().title("Table")) -/// .header_style(Style::default().fg(Color::Yellow)) -/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)]) -/// .style(Style::default().fg(Color::White)) -/// .column_spacing(1); -/// ``` -#[derive(Debug, Clone)] -pub struct Table<'a, H, R> { - /// A block to wrap the widget in - block: Option>, - /// Base style for the widget - style: Style, - /// Header row for all columns - header: H, - /// Style for the header - header_style: Style, - /// Width constraints for each column - widths: &'a [Constraint], - /// Space between each column - column_spacing: u16, - /// Space between the header and the rows - header_gap: u16, - /// Style used to render the selected row - highlight_style: Style, - /// Symbol in front of the selected row - highlight_symbol: Option<&'a str>, - /// Symbol in front of the marked row - mark_symbol: Option<&'a str>, - /// Symbol in front of the unmarked row - unmark_symbol: Option<&'a str>, - /// Symbol in front of the marked and selected row - mark_highlight_symbol: Option<&'a str>, - /// Symbol in front of the unmarked and selected row - unmark_highlight_symbol: Option<&'a str>, - /// Data to display in each row - rows: R, -} - -impl<'a, H, R> Default for Table<'a, H, R> -where - H: Iterator + Default, - R: Iterator + Default, -{ - fn default() -> Table<'a, H, R> { - Table { - block: None, - style: Style::default(), - header: H::default(), - header_style: Style::default(), - widths: &[], - column_spacing: 1, - header_gap: 1, - highlight_style: Style::default(), - highlight_symbol: None, - mark_symbol: None, - unmark_symbol: None, - mark_highlight_symbol: None, - unmark_highlight_symbol: None, - rows: R::default(), - } - } -} -impl<'a, H, D, R> Table<'a, H, R> -where - H: Iterator, - D: Iterator, - D::Item: Display, - R: Iterator>, -{ - pub fn new(header: H, rows: R) -> Table<'a, H, R> { - Table { - block: None, - style: Style::default(), - header, - header_style: Style::default(), - widths: &[], - column_spacing: 1, - header_gap: 1, - highlight_style: Style::default(), - highlight_symbol: None, - mark_symbol: None, - unmark_symbol: None, - mark_highlight_symbol: None, - unmark_highlight_symbol: None, - rows, - } - } - pub fn block(mut self, block: Block<'a>) -> Table<'a, H, R> { - self.block = Some(block); - self - } - - pub fn header(mut self, header: II) -> Table<'a, H, R> - where - II: IntoIterator, - { - self.header = header.into_iter(); - self - } - - pub fn header_style(mut self, style: Style) -> Table<'a, H, R> { - self.header_style = style; - self - } - - pub fn widths(mut self, widths: &'a [Constraint]) -> Table<'a, H, R> { - let between_0_and_100 = |&w| match w { - Constraint::Percentage(p) => p <= 100, - _ => true, - }; - assert!( - widths.iter().all(between_0_and_100), - "Percentages should be between 0 and 100 inclusively." - ); - self.widths = widths; - self - } - - pub fn rows(mut self, rows: II) -> Table<'a, H, R> - where - II: IntoIterator, IntoIter = R>, - { - self.rows = rows.into_iter(); - self - } - - pub fn style(mut self, style: Style) -> Table<'a, H, R> { - self.style = style; - self - } - - pub fn mark_symbol(mut self, mark_symbol: &'a str) -> Table<'a, H, R> { - self.mark_symbol = Some(mark_symbol); - self - } - - pub fn unmark_symbol(mut self, unmark_symbol: &'a str) -> Table<'a, H, R> { - self.unmark_symbol = Some(unmark_symbol); - self - } - - pub fn mark_highlight_symbol(mut self, mark_highlight_symbol: &'a str) -> Table<'a, H, R> { - self.mark_highlight_symbol = Some(mark_highlight_symbol); - self - } - - pub fn unmark_highlight_symbol(mut self, unmark_highlight_symbol: &'a str) -> Table<'a, H, R> { - self.unmark_highlight_symbol = Some(unmark_highlight_symbol); - self - } - - pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Table<'a, H, R> { - self.highlight_symbol = Some(highlight_symbol); - self - } - - pub fn highlight_style(mut self, highlight_style: Style) -> Table<'a, H, R> { - self.highlight_style = highlight_style; - self - } - - pub fn column_spacing(mut self, spacing: u16) -> Table<'a, H, R> { - self.column_spacing = spacing; - self - } - - pub fn header_gap(mut self, gap: u16) -> Table<'a, H, R> { - self.header_gap = gap; - self - } -} - -impl<'a, H, D, R> StatefulWidget for Table<'a, H, R> -where - H: Iterator, - H::Item: Display, - D: Iterator, - D::Item: Display, - R: Iterator>, -{ - type State = TableState; - - fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { - buf.set_style(area, self.style); - - // Render block if necessary and get the drawing area - let table_area = match self.block.take() { - Some(b) => { - let inner_area = b.inner(area); - b.render(area, buf); - inner_area - } - None => area, - }; - - let mut solver = Solver::new(); - let mut var_indices = HashMap::new(); - let mut ccs = Vec::new(); - let mut variables = Vec::new(); - for i in 0..self.widths.len() { - let var = cassowary::Variable::new(); - variables.push(var); - var_indices.insert(var, i); - } - for (i, constraint) in self.widths.iter().enumerate() { - ccs.push(variables[i] | GE(WEAK) | 0.); - ccs.push(match *constraint { - Constraint::Length(v) => variables[i] | EQ(MEDIUM) | f64::from(v), - Constraint::Percentage(v) => variables[i] | EQ(WEAK) | (f64::from(v * area.width) / 100.0), - Constraint::Ratio(n, d) => variables[i] | EQ(WEAK) | (f64::from(area.width) * f64::from(n) / f64::from(d)), - Constraint::Min(v) => variables[i] | GE(WEAK) | f64::from(v), - Constraint::Max(v) => variables[i] | LE(WEAK) | f64::from(v), - }); - } - solver - .add_constraint( - variables.iter().fold(Expression::from_constant(0.), |acc, v| acc + *v) - | LE(REQUIRED) - | f64::from(area.width - 2 - (self.column_spacing * (variables.len() as u16 - 1))), - ) - .unwrap(); - solver.add_constraints(&ccs).unwrap(); - let mut solved_widths = vec![0; variables.len()]; - for &(var, value) in solver.fetch_changes() { - let index = var_indices[&var]; - let value = if value.is_sign_negative() { 0 } else { value as u16 }; - solved_widths[index] = value; - } - - let mut y = table_area.top(); - let mut x = table_area.left(); - - // Draw header - let mut header_index = usize::MAX; - let mut index = 0; - if y < table_area.bottom() { - for (w, t) in solved_widths.iter().zip(self.header.by_ref()) { - buf.set_stringn( - x, - y, - format!("{symbol:>width$}", symbol = " ", width = *w as usize), - *w as usize, - self.header_style, - ); - if t.to_string() == "ID" { - buf.set_stringn( - x, - y, - format!("{symbol:>width$}", symbol = t, width = *w as usize), - *w as usize, - self.header_style, - ); - header_index = index; - } else { - buf.set_stringn(x, y, format!("{}", t), *w as usize, self.header_style); - } - x += *w + self.column_spacing; - index += 1; - } - } - y += 1 + self.header_gap; - - // Use highlight_style only if something is selected - let (selected, highlight_style) = if state.current_selection().is_some() { - (state.current_selection(), self.highlight_style) - } else { - (None, self.style) - }; - - let highlight_symbol = match state.mode { - TableMode::MultipleSelection => { - let s = self.highlight_symbol.unwrap_or("\u{2022}").trim_end(); - format!("{} ", s) - } - TableMode::SingleSelection => self.highlight_symbol.unwrap_or("").to_string(), - }; - - let mark_symbol = match state.mode { - TableMode::MultipleSelection => { - let s = self.mark_symbol.unwrap_or("\u{2714}").trim_end(); - format!("{} ", s) - } - TableMode::SingleSelection => self.highlight_symbol.unwrap_or("").to_string(), - }; - - let blank_symbol = match state.mode { - TableMode::MultipleSelection => { - let s = self.unmark_symbol.unwrap_or(" ").trim_end(); - format!("{} ", s) - } - TableMode::SingleSelection => " ".repeat(highlight_symbol.width()), - }; - - let mark_highlight_symbol = { - let s = self.mark_highlight_symbol.unwrap_or("\u{29bf}").trim_end(); - format!("{} ", s) - }; - - let unmark_highlight_symbol = { - let s = self.unmark_highlight_symbol.unwrap_or("\u{29be}").trim_end(); - format!("{} ", s) - }; - - // Draw rows - let default_style = Style::default(); - if y < table_area.bottom() { - let remaining = (table_area.bottom() - y) as usize; - - // Make sure the table shows the selected item - state.offset = selected.map_or(0, |s| { - if s >= remaining + state.offset - 1 { - s + 1 - remaining - } else if s < state.offset { - s - } else { - state.offset - } - }); - for (i, row) in self.rows.skip(state.offset).take(remaining).enumerate() { - let (data, style, symbol) = match row { - Row::Data(d) | Row::StyledData(d, _) if Some(i) == state.current_selection().map(|s| s - state.offset) => { - match state.mode { - TableMode::MultipleSelection => { - if state.marked.contains(&(i + state.offset)) { - (d, highlight_style, mark_highlight_symbol.to_string()) - } else { - (d, highlight_style, unmark_highlight_symbol.to_string()) - } - } - TableMode::SingleSelection => (d, highlight_style, highlight_symbol.to_string()), - } - } - Row::Data(d) => { - if state.marked.contains(&(i + state.offset)) { - (d, default_style, mark_symbol.to_string()) - } else { - (d, default_style, blank_symbol.to_string()) - } - } - Row::StyledData(d, s) => { - if state.marked.contains(&(i + state.offset)) { - (d, s, mark_symbol.to_string()) - } else { - (d, s, blank_symbol.to_string()) - } - } - }; - x = table_area.left(); - for (c, (w, elt)) in solved_widths.iter().zip(data).enumerate() { - let s = if c == 0 { - buf.set_stringn( - x, - y + i as u16, - format!("{symbol:^width$}", symbol = "", width = area.width as usize), - *w as usize, - style, - ); - if c == header_index { - let symbol = match state.mode { - TableMode::SingleSelection | TableMode::MultipleSelection => &symbol, - }; - format!( - "{symbol}{elt:>width$}", - symbol = symbol, - elt = elt, - width = (*w as usize).saturating_sub(symbol.to_string().width()) - ) - } else { - format!( - "{symbol}{elt:width$}", elt = elt, width = *w as usize) - } else { - format!("{elt: Widget for Table<'a, H, R> -where - H: Iterator, - H::Item: Display, - D: Iterator, - D::Item: Display, - R: Iterator>, -{ - fn render(self, area: Rect, buf: &mut Buffer) { - let mut state = TableState::default(); - StatefulWidget::render(self, area, buf, &mut state); - } -} diff --git a/src/task_report.rs b/src/task_report.rs deleted file mode 100644 index e2b9923..0000000 --- a/src/task_report.rs +++ /dev/null @@ -1,481 +0,0 @@ -use std::{error::Error, process::Command}; - -use chrono::{DateTime, Datelike, Local, NaiveDate, NaiveDateTime, TimeZone}; -use color_eyre::eyre::Result; -use itertools::join; -use task_hookrs::{task::Task, uda::UDAValue}; -use unicode_truncate::UnicodeTruncateStr; -use unicode_width::UnicodeWidthStr; - -pub fn format_date_time(dt: NaiveDateTime) -> String { - let dt = Local.from_local_datetime(&dt).unwrap(); - dt.format("%Y-%m-%d %H:%M:%S").to_string() -} - -pub fn format_date(dt: NaiveDateTime) -> String { - let offset = Local.offset_from_utc_datetime(&dt); - let dt = DateTime::::from_naive_utc_and_offset(dt, offset); - dt.format("%Y-%m-%d").to_string() -} - -pub fn vague_format_date_time(from_dt: NaiveDateTime, to_dt: NaiveDateTime, with_remainder: bool) -> String { - let to_dt = Local.from_local_datetime(&to_dt).unwrap(); - let from_dt = Local.from_local_datetime(&from_dt).unwrap(); - let mut seconds = (to_dt - from_dt).num_seconds(); - let minus = if seconds < 0 { - seconds *= -1; - "-" - } else { - "" - }; - - let year = 60 * 60 * 24 * 365; - let month = 60 * 60 * 24 * 30; - let week = 60 * 60 * 24 * 7; - let day = 60 * 60 * 24; - let hour = 60 * 60; - let minute = 60; - - if seconds >= 60 * 60 * 24 * 365 { - return if with_remainder { - format!( - "{}{}y{}mo", - minus, - seconds / year, - (seconds - year * (seconds / year)) / month - ) - } else { - format!("{}{}y", minus, seconds / year) - }; - } else if seconds >= 60 * 60 * 24 * 90 { - return if with_remainder { - format!( - "{}{}mo{}w", - minus, - seconds / month, - (seconds - month * (seconds / month)) / week - ) - } else { - format!("{}{}mo", minus, seconds / month) - }; - } else if seconds >= 60 * 60 * 24 * 14 { - return if with_remainder { - format!( - "{}{}w{}d", - minus, - seconds / week, - (seconds - week * (seconds / week)) / day - ) - } else { - format!("{}{}w", minus, seconds / week) - }; - } else if seconds >= 60 * 60 * 24 { - return if with_remainder { - format!( - "{}{}d{}h", - minus, - seconds / day, - (seconds - day * (seconds / day)) / hour - ) - } else { - format!("{}{}d", minus, seconds / day) - }; - } else if seconds >= 60 * 60 { - return if with_remainder { - format!( - "{}{}h{}min", - minus, - seconds / hour, - (seconds - hour * (seconds / hour)) / minute - ) - } else { - format!("{}{}h", minus, seconds / hour) - }; - } else if seconds >= 60 { - return if with_remainder { - format!( - "{}{}min{}s", - minus, - seconds / minute, - (seconds - minute * (seconds / minute)) - ) - } else { - format!("{}{}min", minus, seconds / minute) - }; - } - format!("{}{}s", minus, seconds) -} - -pub struct TaskReportTable { - pub labels: Vec, - pub columns: Vec, - pub tasks: Vec>, - pub virtual_tags: Vec, - pub description_width: usize, - pub date_time_vague_precise: bool, -} - -impl TaskReportTable { - pub fn new(data: &str, report: &str) -> Result { - let virtual_tags = vec![ - "PROJECT", - "BLOCKED", - "UNBLOCKED", - "BLOCKING", - "DUE", - "DUETODAY", - "TODAY", - "OVERDUE", - "WEEK", - "MONTH", - "QUARTER", - "YEAR", - "ACTIVE", - "SCHEDULED", - "PARENT", - "CHILD", - "UNTIL", - "WAITING", - "ANNOTATED", - "READY", - "YESTERDAY", - "TOMORROW", - "TAGGED", - "PENDING", - "COMPLETED", - "DELETED", - "UDA", - "ORPHAN", - "PRIORITY", - "PROJECT", - "LATEST", - "RECURRING", - "INSTANCE", - "TEMPLATE", - ]; - let mut task_report_table = Self { - labels: vec![], - columns: vec![], - tasks: vec![vec![]], - virtual_tags: virtual_tags.iter().map(ToString::to_string).collect::>(), - description_width: 100, - date_time_vague_precise: false, - }; - task_report_table.export_headers(Some(data), report)?; - Ok(task_report_table) - } - - pub fn export_headers(&mut self, data: Option<&str>, report: &str) -> Result<()> { - self.columns = vec![]; - self.labels = vec![]; - - let data = if let Some(s) = data { - s.to_string() - } else { - let output = Command::new("task") - .arg("show") - .arg("rc.defaultwidth=0") - .arg(format!("report.{}.columns", report)) - .output()?; - String::from_utf8_lossy(&output.stdout).into_owned() - }; - - for line in data.split('\n') { - if line.starts_with(format!("report.{}.columns", report).as_str()) { - let column_names = line.split_once(' ').unwrap().1; - for column in column_names.split(',') { - self.columns.push(column.to_string()); - } - } - } - - let output = Command::new("task") - .arg("show") - .arg("rc.defaultwidth=0") - .arg(format!("report.{}.labels", report)) - .output()?; - let data = String::from_utf8_lossy(&output.stdout); - - for line in data.split('\n') { - if line.starts_with(format!("report.{}.labels", report).as_str()) { - let label_names = line.split_once(' ').unwrap().1; - for label in label_names.split(',') { - self.labels.push(label.to_string()); - } - } - } - - if self.labels.is_empty() { - for label in &self.columns { - let label = label.split('.').collect::>()[0]; - let label = if label == "id" { "ID" } else { label }; - let mut c = label.chars(); - let label = match c.next() { - None => String::new(), - Some(f) => f.to_uppercase().collect::() + c.as_str(), - }; - if !label.is_empty() { - self.labels.push(label); - } - } - } - let num_labels = self.labels.len(); - let num_columns = self.columns.len(); - assert!(num_labels == num_columns, "Must have the same number of labels (currently {}) and columns (currently {}). Compare their values as shown by \"task show report.{}.\" and fix your taskwarrior config.", num_labels, num_columns, report); - - Ok(()) - } - - pub fn generate_table(&mut self, tasks: &[Task]) { - self.tasks = vec![]; - - // get all tasks as their string representation - for task in tasks { - if self.columns.is_empty() { - break; - } - let mut item = vec![]; - for name in &self.columns { - let s = self.get_string_attribute(name, task, tasks); - item.push(s); - } - self.tasks.push(item); - } - } - - pub fn simplify_table(&mut self) -> (Vec>, Vec) { - // find which columns are empty - if self.tasks.is_empty() { - return (vec![], vec![]); - } - - let mut null_columns = vec![0; self.tasks[0].len()]; - - for task in &self.tasks { - for (i, s) in task.iter().enumerate() { - null_columns[i] += s.len(); - } - } - - // filter out columns where everything is empty - let mut tasks = vec![]; - for task in &self.tasks { - let t = task.clone(); - let t: Vec = t - .iter() - .enumerate() - .filter(|&(i, _)| null_columns[i] != 0) - .map(|(_, e)| e.clone()) - .collect(); - tasks.push(t); - } - - // filter out header where all columns are empty - let headers: Vec = self - .labels - .iter() - .enumerate() - .filter(|&(i, _)| null_columns[i] != 0) - .map(|(_, e)| e.clone()) - .collect(); - - (tasks, headers) - } - - pub fn get_string_attribute(&self, attribute: &str, task: &Task, tasks: &[Task]) -> String { - match attribute { - "id" => task.id().unwrap_or_default().to_string(), - "scheduled.relative" => match task.scheduled() { - Some(v) => vague_format_date_time( - Local::now().naive_utc(), - NaiveDateTime::new(v.date(), v.time()), - self.date_time_vague_precise, - ), - None => "".to_string(), - }, - "due.relative" => match task.due() { - Some(v) => vague_format_date_time( - Local::now().naive_utc(), - NaiveDateTime::new(v.date(), v.time()), - self.date_time_vague_precise, - ), - None => "".to_string(), - }, - "due" => match task.due() { - Some(v) => format_date(NaiveDateTime::new(v.date(), v.time())), - None => "".to_string(), - }, - "until.remaining" => match task.until() { - Some(v) => vague_format_date_time( - Local::now().naive_utc(), - NaiveDateTime::new(v.date(), v.time()), - self.date_time_vague_precise, - ), - None => "".to_string(), - }, - "until" => match task.until() { - Some(v) => format_date(NaiveDateTime::new(v.date(), v.time())), - None => "".to_string(), - }, - "entry.age" => vague_format_date_time( - NaiveDateTime::new(task.entry().date(), task.entry().time()), - Local::now().naive_utc(), - self.date_time_vague_precise, - ), - "entry" => format_date(NaiveDateTime::new(task.entry().date(), task.entry().time())), - "start.age" => match task.start() { - Some(v) => vague_format_date_time( - NaiveDateTime::new(v.date(), v.time()), - Local::now().naive_utc(), - self.date_time_vague_precise, - ), - None => "".to_string(), - }, - "start" => match task.start() { - Some(v) => format_date(NaiveDateTime::new(v.date(), v.time())), - None => "".to_string(), - }, - "end.age" => match task.end() { - Some(v) => vague_format_date_time( - NaiveDateTime::new(v.date(), v.time()), - Local::now().naive_utc(), - self.date_time_vague_precise, - ), - None => "".to_string(), - }, - "end" => match task.end() { - Some(v) => format_date(NaiveDateTime::new(v.date(), v.time())), - None => "".to_string(), - }, - "status.short" => task.status().to_string().chars().next().unwrap().to_string(), - "status" => task.status().to_string(), - "priority" => match task.priority() { - Some(p) => p.clone(), - None => "".to_string(), - }, - "project" => match task.project() { - Some(p) => p.to_string(), - None => "".to_string(), - }, - "depends.count" => match task.depends() { - Some(v) => { - if v.is_empty() { - "".to_string() - } else { - format!("{}", v.len()) - } - } - None => "".to_string(), - }, - "depends" => match task.depends() { - Some(v) => { - if v.is_empty() { - "".to_string() - } else { - let mut dt = vec![]; - for u in v { - if let Some(t) = tasks.iter().find(|t| t.uuid() == u) { - dt.push(t.id().unwrap()); - } - } - join(dt.iter().map(ToString::to_string), " ") - } - } - None => "".to_string(), - }, - "tags.count" => match task.tags() { - Some(v) => { - let t = v.iter().filter(|t| !self.virtual_tags.contains(t)).count(); - if t == 0 { - "".to_string() - } else { - t.to_string() - } - } - None => "".to_string(), - }, - "tags" => match task.tags() { - Some(v) => v - .iter() - .filter(|t| !self.virtual_tags.contains(t)) - .cloned() - .collect::>() - .join(","), - None => "".to_string(), - }, - "recur" => match task.recur() { - Some(v) => v.clone(), - None => "".to_string(), - }, - "wait" => match task.wait() { - Some(v) => vague_format_date_time( - NaiveDateTime::new(v.date(), v.time()), - Local::now().naive_utc(), - self.date_time_vague_precise, - ), - None => "".to_string(), - }, - "wait.remaining" => match task.wait() { - Some(v) => vague_format_date_time( - Local::now().naive_utc(), - NaiveDateTime::new(v.date(), v.time()), - self.date_time_vague_precise, - ), - None => "".to_string(), - }, - "description.count" => { - let c = if let Some(a) = task.annotations() { - format!("[{}]", a.len()) - } else { - Default::default() - }; - format!("{} {}", task.description(), c) - } - "description.truncated_count" => { - let c = if let Some(a) = task.annotations() { - format!("[{}]", a.len()) - } else { - Default::default() - }; - let d = task.description().to_string(); - let mut available_width = self.description_width; - if self.description_width >= c.len() { - available_width = self.description_width - c.len(); - } - let (d, _) = d.unicode_truncate(available_width); - let mut d = d.to_string(); - if d != *task.description() { - d = format!("{}\u{2026}", d); - } - format!("{}{}", d, c) - } - "description.truncated" => { - let d = task.description().to_string(); - let available_width = self.description_width; - let (d, _) = d.unicode_truncate(available_width); - let mut d = d.to_string(); - if d != *task.description() { - d = format!("{}\u{2026}", d); - } - d - } - "description.desc" | "description" => task.description().to_string(), - "urgency" => match &task.urgency() { - Some(f) => format!("{:.2}", *f), - None => "0.00".to_string(), - }, - s => { - let u = &task.uda(); - let v = u.get(s); - if v.is_none() { - return "".to_string(); - } - match v.unwrap() { - UDAValue::Str(s) => s.to_string(), - UDAValue::F64(f) => f.to_string(), - UDAValue::U64(u) => u.to_string(), - } - } - } - } -} diff --git a/src/traits.rs b/src/traits.rs deleted file mode 100644 index 28d8ca9..0000000 --- a/src/traits.rs +++ /dev/null @@ -1,24 +0,0 @@ -use task_hookrs::task::Task; - -pub trait TaskwarriorTuiTask { - fn add_tag(&mut self, tag: String); - - fn remove_tag(&mut self, tag: &str); -} - -impl TaskwarriorTuiTask for Task { - fn add_tag(&mut self, tag: String) { - match self.tags_mut() { - Some(t) => t.push(tag), - None => self.set_tags(Some(vec![tag])), - } - } - - fn remove_tag(&mut self, tag: &str) { - if let Some(t) = self.tags_mut() { - if let Some(index) = t.iter().position(|x| *x == tag) { - t.remove(index); - } - } - } -} diff --git a/src/tui.rs b/src/tui.rs index a60a149..e967a4e 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -1,4 +1,3 @@ - use std::{ ops::{Deref, DerefMut}, time::Duration, @@ -23,6 +22,7 @@ pub type Frame<'a> = ratatui::Frame<'a, Backend>; #[derive(Clone, Debug, Serialize, Deserialize)] pub enum Event { + Init, Quit, Error, Closed, @@ -42,36 +42,43 @@ pub struct Tui { pub cancellation_token: CancellationToken, pub event_rx: UnboundedReceiver, pub event_tx: UnboundedSender, - pub tick_rate: (usize, usize), + pub frame_rate: f64, + pub tick_rate: f64, } impl Tui { pub fn new() -> Result { - let tick_rate = (1000, 100); + let tick_rate = 4.0; + let frame_rate = 60.0; let terminal = ratatui::Terminal::new(Backend::new(std::io::stderr()))?; let (event_tx, event_rx) = mpsc::unbounded_channel(); let cancellation_token = CancellationToken::new(); let task = tokio::spawn(async {}); - Ok(Self { terminal, task, cancellation_token, event_rx, event_tx, tick_rate }) + Ok(Self { terminal, task, cancellation_token, event_rx, event_tx, frame_rate, tick_rate }) } - pub fn tick_rate(&mut self, tick_rate: (usize, usize)) { + pub fn tick_rate(&mut self, tick_rate: f64) { self.tick_rate = tick_rate; } + pub fn frame_rate(&mut self, frame_rate: f64) { + self.frame_rate = frame_rate; + } + pub fn start(&mut self) { - let tick_rate = std::time::Duration::from_millis(self.tick_rate.0 as u64); - let render_tick_rate = std::time::Duration::from_millis(self.tick_rate.1 as u64); + let tick_delay = std::time::Duration::from_secs_f64(1.0 / self.tick_rate); + let render_delay = std::time::Duration::from_secs_f64(1.0 / self.frame_rate); self.cancel(); self.cancellation_token = CancellationToken::new(); let _cancellation_token = self.cancellation_token.clone(); let _event_tx = self.event_tx.clone(); self.task = tokio::spawn(async move { let mut reader = crossterm::event::EventStream::new(); - let mut interval = tokio::time::interval(tick_rate); - let mut render_interval = tokio::time::interval(render_tick_rate); + let mut tick_interval = tokio::time::interval(tick_delay); + let mut render_interval = tokio::time::interval(render_delay); + _event_tx.send(Event::Init).unwrap(); loop { - let delay = interval.tick(); + let tick_delay = tick_interval.tick(); let render_delay = render_interval.tick(); let crossterm_event = reader.next().fuse(); tokio::select! { @@ -110,7 +117,7 @@ impl Tui { None => {}, } }, - _ = delay => { + _ = tick_delay => { _event_tx.send(Event::Tick).unwrap(); }, _ = render_delay => { @@ -132,7 +139,7 @@ impl Tui { } if counter > 100 { log::error!("Failed to abort task in 100 milliseconds for unknown reason"); - return Err(color_eyre::eyre::eyre!("Unable to abort task")); + break; } } Ok(()) @@ -145,10 +152,13 @@ impl Tui { Ok(()) } - pub fn exit(&self) -> Result<()> { + pub fn exit(&mut self) -> Result<()> { self.stop()?; - crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, cursor::Show)?; - crossterm::terminal::disable_raw_mode()?; + if crossterm::terminal::is_raw_mode_enabled()? { + self.flush()?; + crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, cursor::Show)?; + crossterm::terminal::disable_raw_mode()?; + } Ok(()) } @@ -156,7 +166,7 @@ impl Tui { self.cancellation_token.cancel(); } - pub fn suspend(&self) -> Result<()> { + pub fn suspend(&mut self) -> Result<()> { self.exit()?; #[cfg(not(windows))] signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?; diff --git a/src/ui.rs b/src/ui.rs deleted file mode 100644 index ba18ab4..0000000 --- a/src/ui.rs +++ /dev/null @@ -1,37 +0,0 @@ -use ratatui::{ - backend::Backend, - layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, - symbols, - text::{Line, Span}, - 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), - ) -} diff --git a/src/utils.rs b/src/utils.rs index 46d626a..9dee9f8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,92 +1,64 @@ -use path_clean::PathClean; -use rustyline::line_buffer::{ChangeListener, DeleteListener, Direction}; - -/// Undo manager -#[derive(Default)] -pub struct Changeset {} - -impl DeleteListener for Changeset { - fn delete(&mut self, idx: usize, string: &str, _: Direction) {} -} - -impl ChangeListener for Changeset { - fn insert_char(&mut self, idx: usize, c: char) {} - - fn insert_str(&mut self, idx: usize, string: &str) {} - - fn replace(&mut self, idx: usize, old: &str, new: &str) {} -} - -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use color_eyre::eyre::Result; use directories::ProjectDirs; use lazy_static::lazy_static; use tracing::error; use tracing_error::ErrorLayer; -use tracing_subscriber::{ - self, filter::EnvFilter, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, Layer, -}; - -use crate::tui::Tui; +use tracing_subscriber::{self, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, Layer}; lazy_static! { pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().to_string(); - pub static ref DATA_FOLDER: Option = std::env::var(format!("{}_DATA", PROJECT_NAME.clone())) - .ok() - .map(PathBuf::from); - pub static ref CONFIG_FOLDER: Option = std::env::var(format!("{}_CONFIG", PROJECT_NAME.clone())) - .ok() - .map(PathBuf::from); + pub static ref DATA_FOLDER: Option = + std::env::var(format!("{}_DATA", PROJECT_NAME.clone())).ok().map(PathBuf::from); + pub static ref CONFIG_FOLDER: Option = + std::env::var(format!("{}_CONFIG", PROJECT_NAME.clone())).ok().map(PathBuf::from); pub static ref GIT_COMMIT_HASH: String = - std::env::var(format!("{}_GIT_INFO", PROJECT_NAME.clone())).unwrap_or_else(|_| String::from("Unknown")); - pub static ref LOG_LEVEL: String = std::env::var(format!("{}_LOG_LEVEL", PROJECT_NAME.clone())).unwrap_or_default(); - pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME").to_lowercase()); + std::env::var(format!("{}_GIT_INFO", PROJECT_NAME.clone())).unwrap_or_else(|_| String::from("UNKNOWN")); + pub static ref LOG_ENV: String = format!("{}_LOGLEVEL", PROJECT_NAME.clone()); + pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME")); } fn project_directory() -> Option { - ProjectDirs::from("com", "kdheepak", PROJECT_NAME.clone().to_lowercase().as_str()) + ProjectDirs::from("com", "kdheepak", env!("CARGO_PKG_NAME")) } pub fn initialize_panic_handler() -> Result<()> { let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() - .panic_section(format!( - "This is a bug. Consider reporting it at {}", - env!("CARGO_PKG_REPOSITORY") - )) - .display_location_section(true) - .display_env_section(true) - .issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new")) - .add_issue_metadata("version", env!("CARGO_PKG_VERSION")) - .add_issue_metadata("os", std::env::consts::OS) - .add_issue_metadata("arch", std::env::consts::ARCH) + .panic_section(format!("This is a bug. Consider reporting it at {}", env!("CARGO_PKG_REPOSITORY"))) + .capture_span_trace_by_default(false) + .display_location_section(false) + .display_env_section(false) .into_hooks(); eyre_hook.install()?; std::panic::set_hook(Box::new(move |panic_info| { - if let Ok(t) = Tui::new() { + if let Ok(mut t) = crate::tui::Tui::new() { if let Err(r) = t.exit() { error!("Unable to exit Terminal: {:?}", r); } } + #[cfg(not(debug_assertions))] + { + use human_panic::{handle_dump, print_msg, Metadata}; + let meta = Metadata { + version: env!("CARGO_PKG_VERSION").into(), + name: env!("CARGO_PKG_NAME").into(), + authors: env!("CARGO_PKG_AUTHORS").replace(':', ", ").into(), + homepage: env!("CARGO_PKG_HOMEPAGE").into(), + }; + + let file_path = handle_dump(&meta, panic_info); + // prints human-panic message + print_msg(file_path, &meta).expect("human-panic: printing error message to console failed"); + eprintln!("{}", panic_hook.panic_report(panic_info)); // prints color-eyre stack trace to stderr + } let msg = format!("{}", panic_hook.panic_report(panic_info)); - eprintln!("{}", msg); log::error!("Error: {}", strip_ansi_escapes::strip_str(msg)); - use human_panic::{handle_dump, print_msg, Metadata}; - let meta = Metadata { - version: env!("CARGO_PKG_VERSION").into(), - name: env!("CARGO_PKG_NAME").into(), - authors: env!("CARGO_PKG_AUTHORS").replace(':', ", ").into(), - homepage: env!("CARGO_PKG_HOMEPAGE").into(), - }; - - let file_path = handle_dump(&meta, panic_info); - print_msg(file_path, &meta).expect("human-panic: printing error message to console failed"); - - // Better Panic. Only enabled *when* debugging. #[cfg(debug_assertions)] { + // Better Panic stacktrace that is only enabled when debugging. better_panic::Settings::auto() .most_recent_first(false) .lineno_suffix(true) @@ -126,30 +98,20 @@ pub fn initialize_logging() -> Result<()> { std::fs::create_dir_all(directory.clone())?; let log_path = directory.join(LOG_FILE.clone()); let log_file = std::fs::File::create(log_path)?; + std::env::set_var( + "RUST_LOG", + std::env::var("RUST_LOG") + .or_else(|_| std::env::var(LOG_ENV.clone())) + .unwrap_or_else(|_| format!("{}=info", env!("CARGO_CRATE_NAME"))), + ); let file_subscriber = tracing_subscriber::fmt::layer() .with_file(true) .with_line_number(true) .with_writer(log_file) .with_target(false) .with_ansi(false) - .with_filter(EnvFilter::from_default_env()); - tracing_subscriber::registry() - .with(file_subscriber) - // .with(tui_logger::tracing_subscriber_layer()) - .with(ErrorLayer::default()) - .init(); - - // let default_level = match LOG_LEVEL.clone().to_lowercase().as_str() { - // "off" => log::LevelFilter::Off, - // "error" => log::LevelFilter::Error, - // "warn" => log::LevelFilter::Warn, - // "info" => log::LevelFilter::Info, - // "debug" => log::LevelFilter::Debug, - // "trace" => log::LevelFilter::Trace, - // _ => log::LevelFilter::Info, - // }; - // tui_logger::set_default_level(default_level); - + .with_filter(tracing_subscriber::filter::EnvFilter::from_default_env()); + tracing_subscriber::registry().with(file_subscriber).with(ErrorLayer::default()).init(); Ok(()) } @@ -198,16 +160,3 @@ Config directory: {config_dir_path} Data directory: {data_dir_path}" ) } - -pub fn absolute_path(path: impl AsRef) -> std::io::Result { - let path = path.as_ref(); - - let absolute_path = if path.is_absolute() { - path.to_path_buf() - } else { - std::env::current_dir()?.join(path) - } - .clean(); - - Ok(absolute_path) -}