mirror of
https://github.com/kdheepak/taskwarrior-tui.git
synced 2025-08-24 23:46:41 +02:00
WIP
This commit is contained in:
parent
732c5b6f84
commit
1ec93c0913
46 changed files with 1233 additions and 7570 deletions
11
.config/config.json5
Normal file
11
.config/config.json5
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"keybindings": {
|
||||||
|
// KeyBindings for TaskReport
|
||||||
|
"TaskReport": {
|
||||||
|
"<q>": "Quit", // Quit the application
|
||||||
|
"<Ctrl-d>": "Quit", // Another way to quit
|
||||||
|
"<Ctrl-c>": "Quit", // Yet another way to quit
|
||||||
|
"<Ctrl-z>": "Suspend" // Suspend the application
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,16 @@
|
||||||
max_width = 120
|
max_width = 120
|
||||||
tab_spaces = 2
|
use_small_heuristics = "Max"
|
||||||
group_imports = "StdExternalCrate"
|
empty_item_single_line = false
|
||||||
|
force_multiline_blocks = true
|
||||||
|
format_code_in_doc_comments = true
|
||||||
|
match_block_trailing_comma = true
|
||||||
imports_granularity = "Crate"
|
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
|
||||||
|
|
443
Cargo.lock
generated
443
Cargo.lock
generated
|
@ -17,6 +17,17 @@ version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
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]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
|
@ -90,10 +101,15 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic"
|
name = "async-trait"
|
||||||
version = "0.5.3"
|
version = "0.1.73"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
|
checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.29",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
|
@ -116,6 +132,12 @@ dependencies = [
|
||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "better-panic"
|
name = "better-panic"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
@ -141,6 +163,15 @@ dependencies = [
|
||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.13.0"
|
version = "3.13.0"
|
||||||
|
@ -184,16 +215,16 @@ dependencies = [
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"time",
|
"time 0.1.45",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-targets 0.48.5",
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.4.2"
|
version = "4.4.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6"
|
checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
|
@ -201,9 +232,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.4.2"
|
version = "4.4.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08"
|
checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
|
@ -216,9 +247,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_complete"
|
name = "clap_complete"
|
||||||
version = "4.4.0"
|
version = "4.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "586a385f7ef2f8b4d86bddaa0c094794e7ccbfe5ffef1f434fe928143fc783a5"
|
checksum = "4110a1e6af615a9e6d0a36f805d5c99099f8bab9b8042f5bc1fa220a4a89e36f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
]
|
]
|
||||||
|
@ -286,6 +317,25 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
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]]
|
[[package]]
|
||||||
name = "console"
|
name = "console"
|
||||||
version = "0.15.7"
|
version = "0.15.7"
|
||||||
|
@ -304,6 +354,15 @@ version = "0.8.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossterm"
|
name = "crossterm"
|
||||||
version = "0.27.0"
|
version = "0.27.0"
|
||||||
|
@ -331,6 +390,16 @@ dependencies = [
|
||||||
"winapi",
|
"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]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.14.4"
|
version = "0.14.4"
|
||||||
|
@ -366,6 +435,12 @@ dependencies = [
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.3.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_builder"
|
name = "derive_builder"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
|
@ -397,12 +472,33 @@ dependencies = [
|
||||||
"syn 1.0.109",
|
"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]]
|
[[package]]
|
||||||
name = "diff"
|
name = "diff"
|
||||||
version = "0.1.13"
|
version = "0.1.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
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]]
|
[[package]]
|
||||||
name = "directories"
|
name = "directories"
|
||||||
version = "5.0.1"
|
version = "5.0.1"
|
||||||
|
@ -433,6 +529,12 @@ dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"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]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
|
@ -505,24 +607,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5"
|
checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"rustix 0.38.8",
|
"rustix",
|
||||||
"windows-sys 0.48.0",
|
"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]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
@ -627,6 +715,16 @@ dependencies = [
|
||||||
"slab",
|
"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]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.10"
|
version = "0.2.10"
|
||||||
|
@ -644,6 +742,15 @@ version = "0.28.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
|
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
|
@ -683,7 +790,7 @@ dependencies = [
|
||||||
"os_info",
|
"os_info",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"toml",
|
"toml 0.7.6",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -739,7 +846,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
|
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.14.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -748,23 +855,6 @@ version = "2.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2c785eefb63ebd0e33416dfcb8d6da0bf27ce752843a45632a67bf10d4d4b5c4"
|
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]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
|
@ -789,6 +879,17 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"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]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
@ -802,10 +903,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linked-hash-map"
|
||||||
version = "0.3.8"
|
version = "0.5.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
|
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
|
@ -931,6 +1032,15 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_threads"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.32.0"
|
version = "0.32.0"
|
||||||
|
@ -952,6 +1062,16 @@ version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
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]]
|
[[package]]
|
||||||
name = "os_info"
|
name = "os_info"
|
||||||
version = "3.7.0"
|
version = "3.7.0"
|
||||||
|
@ -1011,27 +1131,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef"
|
checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pear"
|
name = "pathdiff"
|
||||||
version = "0.2.7"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61a386cd715229d399604b50d1361683fe687066f42d56f54be995bc6868f71c"
|
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
||||||
dependencies = [
|
|
||||||
"inlinable_string",
|
|
||||||
"pear_codegen",
|
|
||||||
"yansi 1.0.0-rc.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pear_codegen"
|
|
||||||
version = "0.2.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "da9f0f13dac8069c139e8300a6510e3f4143ecf5259c60b116a9b271b4ca0d54"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"proc-macro2-diagnostics",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.29",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
|
@ -1039,6 +1142,51 @@ version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
|
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]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
|
@ -1064,7 +1212,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
|
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"diff",
|
"diff",
|
||||||
"yansi 0.5.1",
|
"yansi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1076,19 +1224,6 @@ dependencies = [
|
||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.33"
|
version = "1.0.33"
|
||||||
|
@ -1151,6 +1286,7 @@ dependencies = [
|
||||||
"itertools",
|
"itertools",
|
||||||
"paste",
|
"paste",
|
||||||
"strum",
|
"strum",
|
||||||
|
"time 0.3.28",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
@ -1228,26 +1364,33 @@ version = "0.7.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
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]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.23"
|
version = "0.1.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
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]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.8"
|
version = "0.38.8"
|
||||||
|
@ -1257,7 +1400,7 @@ dependencies = [
|
||||||
"bitflags 2.4.0",
|
"bitflags 2.4.0",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.4.5",
|
"linux-raw-sys",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1354,6 +1497,17 @@ dependencies = [
|
||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "sharded-slab"
|
name = "sharded-slab"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
|
@ -1529,12 +1683,14 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
|
"config",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
|
"derive_deref",
|
||||||
"directories",
|
"directories",
|
||||||
"figment",
|
|
||||||
"futures",
|
"futures",
|
||||||
"human-panic",
|
"human-panic",
|
||||||
"itertools",
|
"itertools",
|
||||||
|
"json5",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
|
@ -1554,7 +1710,7 @@ dependencies = [
|
||||||
"task-hookrs",
|
"task-hookrs",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"toml",
|
"toml 0.8.0",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-error",
|
"tracing-error",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
@ -1568,11 +1724,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "terminal_size"
|
name = "terminal_size"
|
||||||
version = "0.2.6"
|
version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237"
|
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustix 0.37.23",
|
"rustix",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1617,6 +1773,25 @@ dependencies = [
|
||||||
"winapi",
|
"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]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
|
@ -1675,6 +1850,15 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.5.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.7.6"
|
version = "0.7.6"
|
||||||
|
@ -1684,7 +1868,19 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"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]]
|
[[package]]
|
||||||
|
@ -1709,6 +1905,19 @@ dependencies = [
|
||||||
"winnow",
|
"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]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.37"
|
version = "0.1.37"
|
||||||
|
@ -1792,13 +2001,16 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uncased"
|
name = "typenum"
|
||||||
version = "0.9.9"
|
version = "1.17.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68"
|
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||||
dependencies = [
|
|
||||||
"version_check",
|
[[package]]
|
||||||
]
|
name = "ucd-trie"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicase"
|
name = "unicase"
|
||||||
|
@ -2158,14 +2370,17 @@ dependencies = [
|
||||||
"memchr",
|
"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]]
|
[[package]]
|
||||||
name = "yansi"
|
name = "yansi"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "yansi"
|
|
||||||
version = "1.0.0-rc.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377"
|
|
||||||
|
|
14
Cargo.toml
14
Cargo.toml
|
@ -16,21 +16,23 @@ categories = ["command-line-utilities"]
|
||||||
better-panic = "0.3.0"
|
better-panic = "0.3.0"
|
||||||
cassowary = "0.3.0"
|
cassowary = "0.3.0"
|
||||||
chrono = "0.4.28"
|
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"] }
|
color-eyre = { version = "0.6.2", features = ["issue-url"] }
|
||||||
|
config = "0.13.3"
|
||||||
crossterm = { version = "0.27.0", features = ["event-stream", "serde"] }
|
crossterm = { version = "0.27.0", features = ["event-stream", "serde"] }
|
||||||
|
derive_deref = "1.1.1"
|
||||||
directories = "5.0.1"
|
directories = "5.0.1"
|
||||||
figment = { version = "0.10.10", features = ["toml", "env"] }
|
|
||||||
futures = "0.3.28"
|
futures = "0.3.28"
|
||||||
human-panic = "1.2.0"
|
human-panic = "1.2.0"
|
||||||
itertools = "0.11.0"
|
itertools = "0.11.0"
|
||||||
|
json5 = "0.4.1"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
libc = "0.2.147"
|
libc = "0.2.147"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
path-clean = "1.0.1"
|
path-clean = "1.0.1"
|
||||||
pretty_assertions = "1.4.0"
|
pretty_assertions = "1.4.0"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
ratatui = "0.23.0"
|
ratatui = { version = "0.23.0", features = ["all-widgets"] }
|
||||||
regex = "1.9.5"
|
regex = "1.9.5"
|
||||||
rustyline = { version = "12.0.0", features = ["with-file-history", "derive"] }
|
rustyline = { version = "12.0.0", features = ["with-file-history", "derive"] }
|
||||||
serde = { version = "1.0.188", features = ["derive"] }
|
serde = { version = "1.0.188", features = ["derive"] }
|
||||||
|
@ -43,7 +45,7 @@ strip-ansi-escapes = "0.2.0"
|
||||||
task-hookrs = "0.9.0"
|
task-hookrs = "0.9.0"
|
||||||
tokio = { version = "1.32.0", features = ["full"] }
|
tokio = { version = "1.32.0", features = ["full"] }
|
||||||
tokio-util = "0.7.8"
|
tokio-util = "0.7.8"
|
||||||
toml = "0.7.6"
|
toml = "0.8.0"
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
tracing-error = "0.2.0"
|
tracing-error = "0.2.0"
|
||||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||||
|
@ -68,6 +70,6 @@ incremental = true
|
||||||
lto = true
|
lto = true
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
clap = { version = "4.4.2", features = ["derive"] }
|
clap = { version = "4.4.4", features = ["derive"] }
|
||||||
clap_complete = "4.4.0"
|
clap_complete = "4.4.1"
|
||||||
shlex = "1.1.0"
|
shlex = "1.1.0"
|
||||||
|
|
34
build.rs
34
build.rs
|
@ -19,14 +19,9 @@ fn run_pandoc() -> Result<Output, std::io::Error> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_commit_hash() {
|
fn get_commit_hash() {
|
||||||
let git_output = std::process::Command::new("git")
|
let git_output = std::process::Command::new("git").args(["rev-parse", "--git-dir"]).output().ok();
|
||||||
.args(["rev-parse", "--git-dir"])
|
|
||||||
.output()
|
|
||||||
.ok();
|
|
||||||
let git_dir = git_output.as_ref().and_then(|output| {
|
let git_dir = git_output.as_ref().and_then(|output| {
|
||||||
std::str::from_utf8(&output.stdout)
|
std::str::from_utf8(&output.stdout).ok().and_then(|s| s.strip_suffix('\n').or_else(|| s.strip_suffix("\r\n")))
|
||||||
.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.
|
// 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")
|
let git_output =
|
||||||
.args(["describe", "--always", "--tags", "--long", "--dirty"])
|
std::process::Command::new("git").args(["describe", "--always", "--tags", "--long", "--dirty"]).output().ok();
|
||||||
.output()
|
let git_info = git_output.as_ref().and_then(|output| std::str::from_utf8(&output.stdout).ok().map(str::trim));
|
||||||
.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");
|
let cargo_pkg_version = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
// Default git_describe to cargo_pkg_version
|
// Default git_describe to cargo_pkg_version
|
||||||
|
@ -76,14 +67,13 @@ fn get_commit_hash() {
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
get_commit_hash();
|
get_commit_hash();
|
||||||
let mut app = generate_cli_app();
|
// let mut app = generate_cli_app();
|
||||||
let name = app.get_name().to_string();
|
// let name = app.get_name().to_string();
|
||||||
let outdir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("completions/");
|
// let outdir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("completions/");
|
||||||
dbg!(&outdir);
|
// generate_to(Bash, &mut app, &name, &outdir).unwrap();
|
||||||
generate_to(Bash, &mut app, &name, &outdir).unwrap();
|
// generate_to(Zsh, &mut app, &name, &outdir).unwrap();
|
||||||
generate_to(Zsh, &mut app, &name, &outdir).unwrap();
|
// generate_to(Fish, &mut app, &name, &outdir).unwrap();
|
||||||
generate_to(Fish, &mut app, &name, &outdir).unwrap();
|
// generate_to(PowerShell, &mut app, &name, &outdir).unwrap();
|
||||||
generate_to(PowerShell, &mut app, &name, &outdir).unwrap();
|
|
||||||
if run_pandoc().is_err() {
|
if run_pandoc().is_err() {
|
||||||
dbg!("Unable to run pandoc to generate man page documentation");
|
dbg!("Unable to run pandoc to generate man page documentation");
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ _taskwarrior-tui() {
|
||||||
'--help[Print help]' \
|
'--help[Print help]' \
|
||||||
'-V[Print version]' \
|
'-V[Print version]' \
|
||||||
'--version[Print version]' \
|
'--version[Print version]' \
|
||||||
|
'::tick-rate:' \
|
||||||
&& ret=0
|
&& ret=0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ _taskwarrior-tui() {
|
||||||
|
|
||||||
case "${cmd}" in
|
case "${cmd}" in
|
||||||
taskwarrior__tui)
|
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
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
|
@ -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=<key>`. 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.
|
|
|
@ -1,26 +0,0 @@
|
||||||
# Color configuration
|
|
||||||
|
|
||||||
`taskwarrior-tui` reads values from your `taskwarrior`'s `taskrc` file (default: `~/.taskrc`).
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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
|
|
||||||
```
|
|
|
@ -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=[
|
|
||||||
```
|
|
|
@ -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: <https://github.com/kdheepak/taskwarrior-tui>
|
|
||||||
|
|
||||||
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).
|
|
25
docs/faqs.md
25
docs/faqs.md
|
@ -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:
|
|
||||||
|
|
||||||
<video src="https://user-images.githubusercontent.com/1813121/159824511-de66d4fc-0a59-4a65-9c74-7419c127481e.mov" data-canonical-src="https://user-images.githubusercontent.com/1813121/159824511-de66d4fc-0a59-4a65-9c74-7419c127481e.mov" controls="controls" muted="muted" class="d-block rounded-bottom-2 border-top width-fit" style="max-height:640px;"></video>
|
|
||||||
|
|
||||||
```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.
|
|
|
@ -1,56 +0,0 @@
|
||||||
|
|
||||||
# Installation
|
|
||||||
|
|
||||||
Unless otherwise specified, you will need to install `taskwarrior` first. See <https://taskwarrior.org/download/> for more information.
|
|
||||||
|
|
||||||
**Manual** ( _Recommended_ ) [](https://github.com/kdheepak/taskwarrior-tui/releases/latest) [](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://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://formulae.brew.sh/formula/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://archlinux.org/packages/community/x86_64/taskwarrior-tui/) [](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)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
snap install taskwarrior-tui
|
|
||||||
```
|
|
||||||
|
|
||||||
**Using [`zdharma-continuum/zinit`](https://github.com/zdharma-continuum/zinit)** [](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
|
|
||||||
```
|
|
|
@ -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
|
|
|
@ -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"
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
|
@ -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]
|
|
|
@ -1,225 +0,0 @@
|
||||||
% taskwarrior-tui(1)
|
|
||||||
|
|
||||||
<!-- This is the taskwarrior-tui(1) man page, written in Markdown. -->
|
|
||||||
<!-- To generate the roff version, run `just man`, -->
|
|
||||||
<!-- and the man page will appear in the ‘target’ directory. -->
|
|
||||||
|
|
||||||
|
|
||||||
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`
|
|
57
mkdocs.yml
57
mkdocs.yml
|
@ -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
|
|
|
@ -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: .
|
|
|
@ -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,
|
|
||||||
}
|
|
2752
src/app.rs
2752
src/app.rs
File diff suppressed because it is too large
Load diff
277
src/calendar.rs
277
src/calendar.rs
|
@ -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<Block<'a>>,
|
|
||||||
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::<Vec<_>>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
80
src/cli.rs
80
src/cli.rs
|
@ -1,52 +1,36 @@
|
||||||
use clap::Arg;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
use clap::Parser;
|
||||||
const APP_NAME: &str = env!("CARGO_PKG_NAME");
|
|
||||||
|
|
||||||
pub fn generate_cli_app() -> clap::Command {
|
#[derive(Parser, Debug)]
|
||||||
let mut app = clap::Command::new(APP_NAME)
|
#[command(author, version, about)]
|
||||||
.version(APP_VERSION)
|
pub struct Cli {
|
||||||
.author("Dheepak Krishnamurthy <@kdheepak>")
|
#[arg(short, long, value_name = "FOLDER", help = "Sets the data folder for taskwarrior-tui")]
|
||||||
.about("A taskwarrior terminal user interface")
|
pub data: Option<String>,
|
||||||
.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),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.set_bin_name(APP_NAME);
|
#[arg(short, long, value_name = "FOLDER", help = "Sets the config folder for taskwarrior-tui")]
|
||||||
app
|
pub config: Option<String>,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
value_name = "FOLDER",
|
||||||
|
help = "Sets the .task folder using the TASKDATA environment variable for taskwarrior"
|
||||||
|
)]
|
||||||
|
pub taskdata: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
value_name = "FILE",
|
||||||
|
help = "Sets the .taskrc file using the TASKRC environment variable for taskwarrior"
|
||||||
|
)]
|
||||||
|
pub taskrc: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
}
|
}
|
||||||
|
|
46
src/command.rs
Normal file
46
src/command.rs
Normal file
|
@ -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,
|
||||||
|
}
|
|
@ -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<Completion>)> {
|
|
||||||
let candidates: Vec<Completion> = 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<usize> {
|
|
||||||
self.candidates().iter().map(|p| p.1.width() + 4).max()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&self, i: usize) -> Option<Completion> {
|
|
||||||
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<Completion> {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
42
src/components.rs
Normal file
42
src/components.rs
Normal file
|
@ -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<Command>) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn init(&mut self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn handle_events(&mut self, event: Option<Event>) -> Result<Option<Command>> {
|
||||||
|
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<Option<Command>> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn handle_mouse_events(&mut self, mouse: MouseEvent) -> Result<Option<Command>> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn update(&mut self, command: Command) -> Result<Option<Command>> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
fn draw(&mut self, f: &mut Frame<'_>, rect: Rect) -> Result<()>;
|
||||||
|
}
|
141
src/components/app.rs
Normal file
141
src/components/app.rs
Normal file
|
@ -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<UnboundedSender<Command>>,
|
||||||
|
pub keybindings: KeyBindings,
|
||||||
|
pub last_export: Option<std::time::SystemTime>,
|
||||||
|
pub report: String,
|
||||||
|
pub filter: String,
|
||||||
|
pub current_context_filter: String,
|
||||||
|
pub tasks: Vec<Task>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Command>) -> Result<()> {
|
||||||
|
self.command_tx = Some(tx);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_key_events(&mut self, key: KeyEvent) -> Result<Option<Command>> {
|
||||||
|
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<Option<Command>> {
|
||||||
|
match command {
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&mut self, f: &mut Frame<'_>, rect: Rect) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
828
src/config.rs
828
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 crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
use figment::{
|
use derive_deref::{Deref, DerefMut};
|
||||||
providers::{Env, Format, Serialized, Toml},
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
Figment,
|
use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor};
|
||||||
};
|
use serde_derive::Deserialize;
|
||||||
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 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)]
|
const CONFIG: &'static str = include_str!("../.config/config.json5");
|
||||||
pub struct SerdeStyle(pub Style);
|
|
||||||
|
|
||||||
impl std::ops::Deref for SerdeStyle {
|
#[derive(Clone, Debug, Deserialize, Default)]
|
||||||
type Target = Style;
|
pub struct AppConfig {
|
||||||
|
#[serde(default)]
|
||||||
fn deref(&self) -> &Self::Target {
|
pub _data_dir: PathBuf,
|
||||||
&self.0
|
#[serde(default)]
|
||||||
}
|
pub _config_dir: PathBuf,
|
||||||
}
|
}
|
||||||
impl std::ops::DerefMut for SerdeStyle {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
&mut self.0
|
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<Self, config::ConfigError> {
|
||||||
|
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<Mode, HashMap<Vec<KeyEvent>, Command>>);
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for KeyBindings {
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
struct StyleVisitor;
|
let parsed_map = HashMap::<Mode, HashMap<String, Command>>::deserialize(deserializer)?;
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for StyleVisitor {
|
let keybindings = parsed_map
|
||||||
type Value = SerdeStyle;
|
.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 {
|
Ok(KeyBindings(keybindings))
|
||||||
formatter.write_str("a string representation of tui::style::Style")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E: de::Error>(self, v: &str) -> Result<SerdeStyle, E> {
|
|
||||||
Ok(SerdeStyle(get_tcolor(v)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_str(StyleVisitor)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_tcolor(line: &str) -> Style {
|
fn parse_key_event(raw: &str) -> Result<KeyEvent, String> {
|
||||||
|
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<KeyEvent, String> {
|
||||||
|
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<Vec<KeyEvent>, 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::<Vec<_>>();
|
||||||
|
|
||||||
|
sequences.into_iter().map(parse_key_event).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Deref, DerefMut)]
|
||||||
|
pub struct Styles(pub HashMap<Mode, HashMap<String, Style>>);
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Styles {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let parsed_map = HashMap::<Mode, HashMap<String, String>>::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 (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));
|
let foreground = process_color_string(foreground);
|
||||||
background = background.replace("on ", "");
|
let background = process_color_string(&background.replace("on ", ""));
|
||||||
let mut modifiers = Modifier::empty();
|
|
||||||
if foreground.contains("bright") {
|
|
||||||
foreground = foreground.replace("bright ", "");
|
|
||||||
background = background.replace("bright ", "");
|
|
||||||
background.insert_str(0, "bright ");
|
|
||||||
}
|
|
||||||
foreground = foreground.replace("grey", "gray");
|
|
||||||
background = background.replace("grey", "gray");
|
|
||||||
if foreground.contains("underline") {
|
|
||||||
modifiers |= Modifier::UNDERLINED;
|
|
||||||
}
|
|
||||||
let foreground = foreground.replace("underline ", "");
|
|
||||||
// TODO: use bold, bright boolean flags
|
|
||||||
if foreground.contains("bold") {
|
|
||||||
modifiers |= Modifier::BOLD;
|
|
||||||
}
|
|
||||||
// let foreground = foreground.replace("bold ", "");
|
|
||||||
if foreground.contains("inverse") {
|
|
||||||
modifiers |= Modifier::REVERSED;
|
|
||||||
}
|
|
||||||
let foreground = foreground.replace("inverse ", "");
|
|
||||||
let mut style = Style::default();
|
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);
|
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.bg(bg);
|
||||||
}
|
}
|
||||||
style = style.add_modifier(modifiers);
|
style = style.add_modifier(foreground.1 | background.1);
|
||||||
style
|
style
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_color_foreground(s: &str) -> Option<Color> {
|
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<Color> {
|
||||||
let s = s.trim_start();
|
let s = s.trim_start();
|
||||||
let s = s.trim_end();
|
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::<u8>().unwrap_or_default();
|
||||||
|
Some(Color::Indexed(c.wrapping_shl(8)))
|
||||||
|
} else if s.contains("color") {
|
||||||
let c = s.trim_start_matches("color").parse::<u8>().unwrap_or_default();
|
let c = s.trim_start_matches("color").parse::<u8>().unwrap_or_default();
|
||||||
Some(Color::Indexed(c))
|
Some(Color::Indexed(c))
|
||||||
} else if s.contains("gray") {
|
} else if s.contains("gray") {
|
||||||
|
@ -145,372 +365,118 @@ fn get_color_foreground(s: &str) -> Option<Color> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_color_background(s: &str) -> Option<Color> {
|
|
||||||
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::<u8>().unwrap_or_default();
|
|
||||||
Some(Color::Indexed(c.wrapping_shl(8)))
|
|
||||||
} else if s.contains("color") {
|
|
||||||
let c = s.trim_start_matches("color").parse::<u8>().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::<u8>().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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
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<String, KeyMap>,
|
|
||||||
pub enabled: bool,
|
|
||||||
pub color: HashMap<String, SerdeStyle>,
|
|
||||||
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<String>,
|
|
||||||
pub uda_priority_values: Vec<String>,
|
|
||||||
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<String>,
|
|
||||||
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("<g><g>").unwrap(), Action::GotoTop);
|
|
||||||
task_report_keymap.insert(parse_key_sequence("<g><j>").unwrap(), Action::GotoPageBottom);
|
|
||||||
task_report_keymap.insert(parse_key_sequence("<g><k>").unwrap(), Action::GotoPageTop);
|
|
||||||
task_report_keymap.insert(parse_key_sequence("<g><G>").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("<d><d>").unwrap(), Action::Delete);
|
|
||||||
task_report_keymap.insert(parse_key_sequence("<x><x>").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<String, KeyMap> = 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<Self> {
|
|
||||||
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_read_config() {
|
fn test_parse_style_default() {
|
||||||
let config = Config::new().unwrap();
|
let style = parse_style("");
|
||||||
dbg!(config);
|
assert_eq!(style, Style::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[test]
|
#[test]
|
||||||
// fn test_write_config() {
|
fn test_parse_style_foreground() {
|
||||||
// let config: Config = Default::default();
|
let style = parse_style("red");
|
||||||
// config.write("tests/data/test.toml".into()).unwrap();
|
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("<q>").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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
|
|
||||||
|
|
49
src/help.rs
49
src/help.rs
|
@ -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<Line> = 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);
|
|
||||||
}
|
|
||||||
}
|
|
132
src/history.rs
132
src/history.rs
|
@ -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<usize>,
|
|
||||||
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<usize> {
|
|
||||||
self.history_index
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn history_search(&mut self, buf: &str, dir: SearchDirection) -> Option<String> {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
231
src/keyconfig.rs
231
src/keyconfig.rs
|
@ -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<Self> {
|
|
||||||
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<KeyCode> {
|
|
||||||
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::*;
|
|
||||||
}
|
|
307
src/keyevent.rs
307
src/keyevent.rs
|
@ -1,307 +0,0 @@
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MediaKeyCode, ModifierKeyCode};
|
|
||||||
|
|
||||||
fn parse_key_event(raw: &str) -> Result<KeyEvent, String> {
|
|
||||||
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<KeyEvent, String> {
|
|
||||||
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<Vec<KeyEvent>, 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::<Vec<_>>();
|
|
||||||
|
|
||||||
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("<a><b>");
|
|
||||||
assert_eq!(
|
|
||||||
result.unwrap(),
|
|
||||||
vec![
|
|
||||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()),
|
|
||||||
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty())
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
let result = parse_key_sequence("<Ctrl-a><Alt-b>");
|
|
||||||
assert_eq!(
|
|
||||||
result.unwrap(),
|
|
||||||
vec![
|
|
||||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),
|
|
||||||
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::ALT)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
let result = parse_key_sequence("<Ctrl-a>");
|
|
||||||
assert_eq!(
|
|
||||||
result.unwrap(),
|
|
||||||
vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),]
|
|
||||||
);
|
|
||||||
let result = parse_key_sequence("<Ctrl-Alt-a>");
|
|
||||||
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("<Ctrl-a").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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_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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
220
src/keymap.rs
220
src/keymap.rs
|
@ -1,220 +0,0 @@
|
||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
error::Error,
|
|
||||||
ops::{Deref, DerefMut},
|
|
||||||
str,
|
|
||||||
};
|
|
||||||
|
|
||||||
use color_eyre::eyre::{eyre, Context, Result};
|
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
|
||||||
use serde::{
|
|
||||||
de::{self, Deserialize, Deserializer, MapAccess, Visitor},
|
|
||||||
ser::{self, Serialize, SerializeMap, Serializer},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
action::Action,
|
|
||||||
keyevent::{key_event_to_string, parse_key_sequence},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
pub struct KeyMap(pub std::collections::HashMap<Vec<KeyEvent>, Action>);
|
|
||||||
|
|
||||||
impl Deref for KeyMap {
|
|
||||||
type Target = std::collections::HashMap<Vec<KeyEvent>, 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
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::<Vec<_>>()
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
map.serialize_entry(&key_string, action)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// End serialization.
|
|
||||||
map.end()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for KeyMap {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
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<M>(self, mut access: M) -> Result<KeyMap, M::Error>
|
|
||||||
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::<String, Action>()? {
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
123
src/main.rs
123
src/main.rs
|
@ -1,111 +1,38 @@
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
#![allow(unused_imports)]
|
#![allow(unused_imports)]
|
||||||
#![allow(unused_variables)]
|
#![allow(unused_variables)]
|
||||||
#![allow(clippy::too_many_arguments)]
|
|
||||||
|
|
||||||
mod action;
|
pub mod cli;
|
||||||
mod app;
|
pub mod command;
|
||||||
mod calendar;
|
pub mod components;
|
||||||
mod cli;
|
pub mod config;
|
||||||
mod completion;
|
pub mod runner;
|
||||||
mod config;
|
pub mod tui;
|
||||||
mod help;
|
pub mod utils;
|
||||||
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;
|
|
||||||
|
|
||||||
use std::{
|
use clap::Parser;
|
||||||
env,
|
use cli::Cli;
|
||||||
error::Error,
|
use color_eyre::eyre::Result;
|
||||||
io::{self, Write},
|
|
||||||
panic,
|
use crate::{
|
||||||
path::{Path, PathBuf},
|
runner::Runner,
|
||||||
time::Duration,
|
utils::{initialize_logging, initialize_panic_handler, version},
|
||||||
};
|
};
|
||||||
|
|
||||||
// use app::{Mode, TaskwarriorTui};
|
async fn tokio_main() -> Result<()> {
|
||||||
use color_eyre::eyre::Result;
|
initialize_logging()?;
|
||||||
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};
|
|
||||||
|
|
||||||
// use crate::{
|
initialize_panic_handler()?;
|
||||||
// action::Action,
|
|
||||||
// keyconfig::KeyConfig,
|
let args = Cli::parse();
|
||||||
// utils::{initialize_logging, initialize_panic_handler},
|
let mut runner = Runner::new(args.tick_rate, args.frame_rate)?;
|
||||||
// };
|
runner.run().await?;
|
||||||
//
|
|
||||||
// const LOG_PATTERN: &str = "{d(%Y-%m-%d %H:%M:%S)} | {l} | {f}:{L} | {m}{n}";
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let matches = cli::generate_cli_app().get_matches();
|
tokio_main().await.unwrap();
|
||||||
|
|
||||||
let config = matches.get_one::<String>("config");
|
|
||||||
let data = matches.get_one::<String>("data");
|
|
||||||
let taskrc = matches.get_one::<String>("taskrc");
|
|
||||||
let taskdata = matches.get_one::<String>("taskdata");
|
|
||||||
let binding = String::from("next");
|
|
||||||
let report = matches.get_one::<String>("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);
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<String>,
|
|
||||||
pub rows: Vec<ContextDetails>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<String>>, Vec<String>) {
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<Project>,
|
|
||||||
pub table_state: TableState,
|
|
||||||
pub current_selection: usize,
|
|
||||||
pub marked: HashSet<Project>,
|
|
||||||
pub columns: Vec<String>,
|
|
||||||
pub rows: Vec<ProjectDetails>,
|
|
||||||
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<String>>, Vec<String>) {
|
|
||||||
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::<Vec<&str>>();
|
|
||||||
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(())
|
|
||||||
}
|
|
109
src/runner.rs
Normal file
109
src/runner.rs
Normal file
|
@ -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<Box<dyn Component>>,
|
||||||
|
pub should_quit: bool,
|
||||||
|
pub should_suspend: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Runner {
|
||||||
|
pub fn new(tick_rate: f64, frame_rate: f64) -> Result<Self> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
537
src/table.rs
537
src/table.rs
|
@ -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<usize>,
|
|
||||||
marked: HashSet<usize>,
|
|
||||||
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<usize> {
|
|
||||||
self.current_selection
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select(&mut self, index: Option<usize>) {
|
|
||||||
self.current_selection = index;
|
|
||||||
if index.is_none() {
|
|
||||||
self.offset = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mark(&mut self, index: Option<usize>) {
|
|
||||||
if let Some(i) = index {
|
|
||||||
self.marked.insert(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn unmark(&mut self, index: Option<usize>) {
|
|
||||||
if let Some(i) = index {
|
|
||||||
self.marked.remove(&i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toggle_mark(&mut self, index: Option<usize>) {
|
|
||||||
if let Some(i) = index {
|
|
||||||
if !self.marked.insert(i) {
|
|
||||||
self.marked.remove(&i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn marked(&self) -> std::collections::hash_set::Iter<usize> {
|
|
||||||
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<D>
|
|
||||||
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<Block<'a>>,
|
|
||||||
/// 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<Item = Row<D>>,
|
|
||||||
{
|
|
||||||
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<II>(mut self, header: II) -> Table<'a, H, R>
|
|
||||||
where
|
|
||||||
II: IntoIterator<Item = H::Item, IntoIter = H>,
|
|
||||||
{
|
|
||||||
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<II>(mut self, rows: II) -> Table<'a, H, R>
|
|
||||||
where
|
|
||||||
II: IntoIterator<Item = Row<D>, 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<Item = Row<D>>,
|
|
||||||
{
|
|
||||||
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$}",
|
|
||||||
symbol = symbol,
|
|
||||||
elt = elt,
|
|
||||||
width = (*w as usize).saturating_sub(symbol.to_string().width())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
buf.set_stringn(
|
|
||||||
x - 1,
|
|
||||||
y + i as u16,
|
|
||||||
format!("{symbol:^width$}", symbol = "", width = area.width as usize),
|
|
||||||
*w as usize + 1,
|
|
||||||
style,
|
|
||||||
);
|
|
||||||
if c == header_index {
|
|
||||||
format!("{elt:>width$}", elt = elt, width = *w as usize)
|
|
||||||
} else {
|
|
||||||
format!("{elt:<width$}", elt = elt, width = *w as usize)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
buf.set_stringn(x, y + i as u16, s, *w as usize, style);
|
|
||||||
x += *w + self.column_spacing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, H, D, R> Widget for Table<'a, H, R>
|
|
||||||
where
|
|
||||||
H: Iterator,
|
|
||||||
H::Item: Display,
|
|
||||||
D: Iterator,
|
|
||||||
D::Item: Display,
|
|
||||||
R: Iterator<Item = Row<D>>,
|
|
||||||
{
|
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
|
||||||
let mut state = TableState::default();
|
|
||||||
StatefulWidget::render(self, area, buf, &mut state);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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::<Local>::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<String>,
|
|
||||||
pub columns: Vec<String>,
|
|
||||||
pub tasks: Vec<Vec<String>>,
|
|
||||||
pub virtual_tags: Vec<String>,
|
|
||||||
pub description_width: usize,
|
|
||||||
pub date_time_vague_precise: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TaskReportTable {
|
|
||||||
pub fn new(data: &str, report: &str) -> Result<Self> {
|
|
||||||
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::<Vec<_>>(),
|
|
||||||
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::<Vec<&str>>()[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::<String>() + 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<String>>, Vec<String>) {
|
|
||||||
// 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<String> = 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<String> = 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::<Vec<_>>()
|
|
||||||
.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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
42
src/tui.rs
42
src/tui.rs
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
|
@ -23,6 +22,7 @@ pub type Frame<'a> = ratatui::Frame<'a, Backend<std::io::Stderr>>;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
|
Init,
|
||||||
Quit,
|
Quit,
|
||||||
Error,
|
Error,
|
||||||
Closed,
|
Closed,
|
||||||
|
@ -42,36 +42,43 @@ pub struct Tui {
|
||||||
pub cancellation_token: CancellationToken,
|
pub cancellation_token: CancellationToken,
|
||||||
pub event_rx: UnboundedReceiver<Event>,
|
pub event_rx: UnboundedReceiver<Event>,
|
||||||
pub event_tx: UnboundedSender<Event>,
|
pub event_tx: UnboundedSender<Event>,
|
||||||
pub tick_rate: (usize, usize),
|
pub frame_rate: f64,
|
||||||
|
pub tick_rate: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tui {
|
impl Tui {
|
||||||
pub fn new() -> Result<Self> {
|
pub fn new() -> Result<Self> {
|
||||||
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 terminal = ratatui::Terminal::new(Backend::new(std::io::stderr()))?;
|
||||||
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
||||||
let cancellation_token = CancellationToken::new();
|
let cancellation_token = CancellationToken::new();
|
||||||
let task = tokio::spawn(async {});
|
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;
|
self.tick_rate = tick_rate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn frame_rate(&mut self, frame_rate: f64) {
|
||||||
|
self.frame_rate = frame_rate;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn start(&mut self) {
|
pub fn start(&mut self) {
|
||||||
let tick_rate = std::time::Duration::from_millis(self.tick_rate.0 as u64);
|
let tick_delay = std::time::Duration::from_secs_f64(1.0 / self.tick_rate);
|
||||||
let render_tick_rate = std::time::Duration::from_millis(self.tick_rate.1 as u64);
|
let render_delay = std::time::Duration::from_secs_f64(1.0 / self.frame_rate);
|
||||||
self.cancel();
|
self.cancel();
|
||||||
self.cancellation_token = CancellationToken::new();
|
self.cancellation_token = CancellationToken::new();
|
||||||
let _cancellation_token = self.cancellation_token.clone();
|
let _cancellation_token = self.cancellation_token.clone();
|
||||||
let _event_tx = self.event_tx.clone();
|
let _event_tx = self.event_tx.clone();
|
||||||
self.task = tokio::spawn(async move {
|
self.task = tokio::spawn(async move {
|
||||||
let mut reader = crossterm::event::EventStream::new();
|
let mut reader = crossterm::event::EventStream::new();
|
||||||
let mut interval = tokio::time::interval(tick_rate);
|
let mut tick_interval = tokio::time::interval(tick_delay);
|
||||||
let mut render_interval = tokio::time::interval(render_tick_rate);
|
let mut render_interval = tokio::time::interval(render_delay);
|
||||||
|
_event_tx.send(Event::Init).unwrap();
|
||||||
loop {
|
loop {
|
||||||
let delay = interval.tick();
|
let tick_delay = tick_interval.tick();
|
||||||
let render_delay = render_interval.tick();
|
let render_delay = render_interval.tick();
|
||||||
let crossterm_event = reader.next().fuse();
|
let crossterm_event = reader.next().fuse();
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
@ -110,7 +117,7 @@ impl Tui {
|
||||||
None => {},
|
None => {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_ = delay => {
|
_ = tick_delay => {
|
||||||
_event_tx.send(Event::Tick).unwrap();
|
_event_tx.send(Event::Tick).unwrap();
|
||||||
},
|
},
|
||||||
_ = render_delay => {
|
_ = render_delay => {
|
||||||
|
@ -132,7 +139,7 @@ impl Tui {
|
||||||
}
|
}
|
||||||
if counter > 100 {
|
if counter > 100 {
|
||||||
log::error!("Failed to abort task in 100 milliseconds for unknown reason");
|
log::error!("Failed to abort task in 100 milliseconds for unknown reason");
|
||||||
return Err(color_eyre::eyre::eyre!("Unable to abort task"));
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -145,10 +152,13 @@ impl Tui {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exit(&self) -> Result<()> {
|
pub fn exit(&mut self) -> Result<()> {
|
||||||
self.stop()?;
|
self.stop()?;
|
||||||
crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, cursor::Show)?;
|
if crossterm::terminal::is_raw_mode_enabled()? {
|
||||||
crossterm::terminal::disable_raw_mode()?;
|
self.flush()?;
|
||||||
|
crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, cursor::Show)?;
|
||||||
|
crossterm::terminal::disable_raw_mode()?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,7 +166,7 @@ impl Tui {
|
||||||
self.cancellation_token.cancel();
|
self.cancellation_token.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn suspend(&self) -> Result<()> {
|
pub fn suspend(&mut self) -> Result<()> {
|
||||||
self.exit()?;
|
self.exit()?;
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?;
|
signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?;
|
||||||
|
|
37
src/ui.rs
37
src/ui.rs
|
@ -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<B>(rect: &mut Frame<B>, 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),
|
|
||||||
)
|
|
||||||
}
|
|
129
src/utils.rs
129
src/utils.rs
|
@ -1,92 +1,64 @@
|
||||||
use path_clean::PathClean;
|
use std::path::PathBuf;
|
||||||
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 color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
use tracing_error::ErrorLayer;
|
use tracing_error::ErrorLayer;
|
||||||
use tracing_subscriber::{
|
use tracing_subscriber::{self, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, Layer};
|
||||||
self, filter::EnvFilter, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, Layer,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::tui::Tui;
|
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().to_string();
|
pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().to_string();
|
||||||
pub static ref DATA_FOLDER: Option<PathBuf> = std::env::var(format!("{}_DATA", PROJECT_NAME.clone()))
|
pub static ref DATA_FOLDER: Option<PathBuf> =
|
||||||
.ok()
|
std::env::var(format!("{}_DATA", PROJECT_NAME.clone())).ok().map(PathBuf::from);
|
||||||
.map(PathBuf::from);
|
pub static ref CONFIG_FOLDER: Option<PathBuf> =
|
||||||
pub static ref CONFIG_FOLDER: Option<PathBuf> = std::env::var(format!("{}_CONFIG", PROJECT_NAME.clone()))
|
std::env::var(format!("{}_CONFIG", PROJECT_NAME.clone())).ok().map(PathBuf::from);
|
||||||
.ok()
|
|
||||||
.map(PathBuf::from);
|
|
||||||
pub static ref GIT_COMMIT_HASH: String =
|
pub static ref GIT_COMMIT_HASH: String =
|
||||||
std::env::var(format!("{}_GIT_INFO", PROJECT_NAME.clone())).unwrap_or_else(|_| String::from("Unknown"));
|
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_ENV: String = format!("{}_LOGLEVEL", PROJECT_NAME.clone());
|
||||||
pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME").to_lowercase());
|
pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn project_directory() -> Option<ProjectDirs> {
|
fn project_directory() -> Option<ProjectDirs> {
|
||||||
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<()> {
|
pub fn initialize_panic_handler() -> Result<()> {
|
||||||
let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default()
|
let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default()
|
||||||
.panic_section(format!(
|
.panic_section(format!("This is a bug. Consider reporting it at {}", env!("CARGO_PKG_REPOSITORY")))
|
||||||
"This is a bug. Consider reporting it at {}",
|
.capture_span_trace_by_default(false)
|
||||||
env!("CARGO_PKG_REPOSITORY")
|
.display_location_section(false)
|
||||||
))
|
.display_env_section(false)
|
||||||
.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)
|
|
||||||
.into_hooks();
|
.into_hooks();
|
||||||
eyre_hook.install()?;
|
eyre_hook.install()?;
|
||||||
std::panic::set_hook(Box::new(move |panic_info| {
|
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() {
|
if let Err(r) = t.exit() {
|
||||||
error!("Unable to exit Terminal: {:?}", r);
|
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));
|
let msg = format!("{}", panic_hook.panic_report(panic_info));
|
||||||
eprintln!("{}", msg);
|
|
||||||
log::error!("Error: {}", strip_ansi_escapes::strip_str(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)]
|
#[cfg(debug_assertions)]
|
||||||
{
|
{
|
||||||
|
// Better Panic stacktrace that is only enabled when debugging.
|
||||||
better_panic::Settings::auto()
|
better_panic::Settings::auto()
|
||||||
.most_recent_first(false)
|
.most_recent_first(false)
|
||||||
.lineno_suffix(true)
|
.lineno_suffix(true)
|
||||||
|
@ -126,30 +98,20 @@ pub fn initialize_logging() -> Result<()> {
|
||||||
std::fs::create_dir_all(directory.clone())?;
|
std::fs::create_dir_all(directory.clone())?;
|
||||||
let log_path = directory.join(LOG_FILE.clone());
|
let log_path = directory.join(LOG_FILE.clone());
|
||||||
let log_file = std::fs::File::create(log_path)?;
|
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()
|
let file_subscriber = tracing_subscriber::fmt::layer()
|
||||||
.with_file(true)
|
.with_file(true)
|
||||||
.with_line_number(true)
|
.with_line_number(true)
|
||||||
.with_writer(log_file)
|
.with_writer(log_file)
|
||||||
.with_target(false)
|
.with_target(false)
|
||||||
.with_ansi(false)
|
.with_ansi(false)
|
||||||
.with_filter(EnvFilter::from_default_env());
|
.with_filter(tracing_subscriber::filter::EnvFilter::from_default_env());
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry().with(file_subscriber).with(ErrorLayer::default()).init();
|
||||||
.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);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,16 +160,3 @@ Config directory: {config_dir_path}
|
||||||
Data directory: {data_dir_path}"
|
Data directory: {data_dir_path}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn absolute_path(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue