diff --git a/Cargo.lock b/Cargo.lock index df94c871b..d542dbd8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,7 +49,7 @@ dependencies = [ "actix-service", "actix-threadpool", "actix-utils", - "base64 0.13.0", + "base64", "bitflags", "brotli2", "bytes 0.5.6", @@ -78,8 +78,8 @@ dependencies = [ "regex", "serde", "serde_json", - "serde_urlencoded 0.7.0", - "sha-1 0.9.6", + "serde_urlencoded", + "sha-1", "slab", "time 0.2.27", ] @@ -245,7 +245,7 @@ dependencies = [ "regex", "serde", "serde_json", - "serde_urlencoded 0.7.0", + "serde_urlencoded", "socket2", "time 0.2.27", "tinyvec", @@ -289,21 +289,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ammonia" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee7d6eb157f337c5cedc95ddf17f0cbc36d36eb7763c8e0d1c1aeb3722f6279" -dependencies = [ - "html5ever", - "lazy_static", - "maplit", - "markup5ever_rcdom", - "matches", - "tendril", - "url", -] - [[package]] name = "ansi_term" version = "0.11.0" @@ -328,32 +313,6 @@ version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61" -[[package]] -name = "arrayref" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - -[[package]] -name = "assert_cmd" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88b6bd5df287567ffdf4ddf4d33060048e1068308e5f62d81c6f9824a045a48" -dependencies = [ - "bstr", - "doc-comment", - "predicates 1.0.8", - "predicates-core", - "predicates-tree", - "wait-timeout", -] - [[package]] name = "async-trait" version = "0.1.50" @@ -392,7 +351,7 @@ dependencies = [ "actix-http", "actix-rt", "actix-service", - "base64 0.13.0", + "base64", "bytes 0.5.6", "cfg-if 1.0.0", "derive_more", @@ -403,7 +362,7 @@ dependencies = [ "rand 0.7.3", "serde", "serde_json", - "serde_urlencoded 0.7.0", + "serde_urlencoded", ] [[package]] @@ -412,12 +371,6 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" -[[package]] -name = "base64" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" - [[package]] name = "base64" version = "0.13.0" @@ -445,57 +398,13 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" -[[package]] -name = "bitvec" -version = "0.19.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "blake2b_simd" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" -dependencies = [ - "arrayref", - "arrayvec", - "constant_time_eq", -] - -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -dependencies = [ - "block-padding", - "byte-tools", - "byteorder", - "generic-array 0.12.4", -] - [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.4", -] - -[[package]] -name = "block-padding" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -dependencies = [ - "byte-tools", + "generic-array", ] [[package]] @@ -518,40 +427,12 @@ dependencies = [ "libc", ] -[[package]] -name = "bstr" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279" -dependencies = [ - "lazy_static", - "memchr", - "regex-automata", - "serde", -] - -[[package]] -name = "built" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f346b6890a0dfa7266974910e7df2d5088120dd54721b9b0e5aae1ae5e05715" -dependencies = [ - "cargo-lock", - "git2", -] - [[package]] name = "bumpalo" version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" -[[package]] -name = "byte-tools" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" - [[package]] name = "byteorder" version = "1.4.3" @@ -579,18 +460,6 @@ dependencies = [ "bytes 1.0.1", ] -[[package]] -name = "cargo-lock" -version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19807e9f4f8af2c8ece4236ed7d229b9179da1f3f2ba44e765c7ba934748f99" -dependencies = [ - "semver 1.0.3", - "serde", - "toml", - "url", -] - [[package]] name = "cbindgen" version = "0.20.0" @@ -615,9 +484,6 @@ name = "cc" version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" -dependencies = [ - "jobserver", -] [[package]] name = "cfg-if" @@ -661,48 +527,17 @@ dependencies = [ "atty", "bitflags", "strsim", - "textwrap 0.11.0", + "textwrap", "unicode-width", "vec_map", ] -[[package]] -name = "combine" -version = "4.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4369b5e4c0cddf64ad8981c0111e7df4f7078f4d6ba98fb31f2e17c4c57b7e" -dependencies = [ - "bytes 1.0.1", - "memchr", -] - -[[package]] -name = "console" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "regex", - "terminal_size", - "unicode-width", - "winapi 0.3.9", -] - [[package]] name = "const_fn" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7" -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - [[package]] name = "convert_case" version = "0.4.0" @@ -744,38 +579,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "crossbeam-utils" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" -dependencies = [ - "cfg-if 1.0.0", - "lazy_static", -] - -[[package]] -name = "csv" -version = "1.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" -dependencies = [ - "bstr", - "csv-core", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "csv-core" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" -dependencies = [ - "memchr", -] - [[package]] name = "ctor" version = "0.1.21" @@ -798,83 +601,19 @@ dependencies = [ "syn", ] -[[package]] -name = "dialoguer" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8c8ae48e400addc32a8710c8d62d55cb84249a7d58ac4cd959daecfbaddc545" -dependencies = [ - "console", - "tempfile", - "zeroize", -] - [[package]] name = "diff" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" -[[package]] -name = "difference" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" - -[[package]] -name = "difflib" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" - -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array 0.12.4", -] - [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.4", -] - -[[package]] -name = "dirs" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" -dependencies = [ - "libc", - "redox_users 0.3.5", - "winapi 0.3.9", -] - -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if 1.0.0", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users 0.4.0", - "winapi 0.3.9", + "generic-array", ] [[package]] @@ -883,45 +622,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - -[[package]] -name = "dtoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" - [[package]] name = "either" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" -[[package]] -name = "elasticlunr-rs" -version = "2.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f8cf73b19a7aece6942f5745a2fc1ae3c8b0533569707d596b5d6baa7d6c600" -dependencies = [ - "lazy_static", - "regex", - "serde", - "serde_derive", - "serde_json", - "strum 0.18.0", - "strum_macros 0.18.0", -] - -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - [[package]] name = "encoding_rs" version = "0.8.28" @@ -943,19 +649,6 @@ dependencies = [ "syn", ] -[[package]] -name = "env_logger" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" -dependencies = [ - "atty", - "humantime 1.3.0", - "log", - "regex", - "termcolor", -] - [[package]] name = "env_logger" version = "0.8.4" @@ -963,18 +656,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" dependencies = [ "atty", - "humantime 2.1.0", + "humantime", "log", "regex", "termcolor", ] -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - [[package]] name = "fallible-iterator" version = "0.2.0" @@ -987,18 +674,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" -[[package]] -name = "filetime" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "redox_syscall 0.2.8", - "winapi 0.3.9", -] - [[package]] name = "flate2" version = "1.0.20" @@ -1011,15 +686,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" -dependencies = [ - "num-traits", -] - [[package]] name = "fnv" version = "1.0.7" @@ -1036,25 +702,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fsevent" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6" -dependencies = [ - "bitflags", - "fsevent-sys", -] - -[[package]] -name = "fsevent-sys" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0" -dependencies = [ - "libc", -] - [[package]] name = "fuchsia-zircon" version = "0.3.3" @@ -1071,22 +718,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" -[[package]] -name = "funty" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" - -[[package]] -name = "futf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b" -dependencies = [ - "mac", - "new_debug_unreachable", -] - [[package]] name = "futures" version = "0.3.15" @@ -1190,15 +821,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "generic-array" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" -dependencies = [ - "typenum", -] - [[package]] name = "generic-array" version = "0.14.4" @@ -1209,15 +831,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "getopts" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" -dependencies = [ - "unicode-width", -] - [[package]] name = "getrandom" version = "0.1.16" @@ -1240,34 +853,6 @@ dependencies = [ "wasi 0.10.2+wasi-snapshot-preview1", ] -[[package]] -name = "git2" -version = "0.13.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9831e983241f8c5591ed53f17d874833e2fa82cac2625f3888c50cbfe136cba" -dependencies = [ - "bitflags", - "libc", - "libgit2-sys", - "log", - "url", -] - -[[package]] -name = "gitignore" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78aa90e4620c1498ac434c06ba6e521b525794bbdacf085d490cc794b4a2f9a4" -dependencies = [ - "glob", -] - -[[package]] -name = "glob" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" - [[package]] name = "h2" version = "0.2.7" @@ -1288,20 +873,6 @@ dependencies = [ "tracing-futures", ] -[[package]] -name = "handlebars" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28f0fe89affef47e2c9729030a8f6e79df34cb66b8a44ecf91dad43f31150559" -dependencies = [ - "log", - "pest", - "pest_derive", - "quick-error 2.0.1", - "serde", - "serde_json", -] - [[package]] name = "hashbrown" version = "0.9.1" @@ -1326,31 +897,6 @@ dependencies = [ "hashbrown 0.11.2", ] -[[package]] -name = "headers" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0b7591fb62902706ae8e7aaff416b1b0fa2c0fd0878b46dc13baa3712d8a855" -dependencies = [ - "base64 0.13.0", - "bitflags", - "bytes 1.0.1", - "headers-core", - "http", - "mime", - "sha-1 0.9.6", - "time 0.1.43", -] - -[[package]] -name = "headers-core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" -dependencies = [ - "http", -] - [[package]] name = "heck" version = "0.3.3" @@ -1380,20 +926,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "html5ever" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafcf38a1a36118242d29b92e1b08ef84e67e4a5ed06e0a80be20e6a32bfed6b" -dependencies = [ - "log", - "mac", - "markup5ever", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "http" version = "0.2.4" @@ -1405,67 +937,18 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" -dependencies = [ - "bytes 0.5.6", - "http", -] - [[package]] name = "httparse" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" -[[package]] -name = "httpdate" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" - -[[package]] -name = "humantime" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" -dependencies = [ - "quick-error 1.2.3", -] - [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" -[[package]] -name = "hyper" -version = "0.13.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a6f157065790a3ed2f88679250419b5cdd96e714a0d65f7797fd337186e96bb" -dependencies = [ - "bytes 0.5.6", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project 1.0.7", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "idna" version = "0.2.3" @@ -1487,35 +970,6 @@ dependencies = [ "hashbrown 0.9.1", ] -[[package]] -name = "inotify" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f" -dependencies = [ - "bitflags", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - -[[package]] -name = "input_buffer" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a8a95243d5a0398cae618ec29477c6e3cb631152be5c19481f80bc71559754" -dependencies = [ - "bytes 0.5.6", -] - [[package]] name = "instant" version = "0.1.9" @@ -1533,7 +987,7 @@ dependencies = [ "actix-web", "anyhow", "cc", - "env_logger 0.8.4", + "env_logger", "lazy_static", "log", "pretty_assertions", @@ -1564,39 +1018,12 @@ dependencies = [ "winreg", ] -[[package]] -name = "iso8601-duration" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b51dd97fa24074214b9eb14da518957573f4dec3189112610ae1ccec9ac464" -dependencies = [ - "nom 5.1.2", -] - -[[package]] -name = "itertools" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" -[[package]] -name = "jobserver" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "972f5ae5d1cb9c6ae417789196c803205313edde988685da5e3aae0827b9e7fd" -dependencies = [ - "libc", -] - [[package]] name = "js-sys" version = "0.3.51" @@ -1628,43 +1055,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - -[[package]] -name = "lexical-core" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" -dependencies = [ - "arrayvec", - "bitflags", - "cfg-if 1.0.0", - "ryu", - "static_assertions", -] - [[package]] name = "libc" version = "0.2.113" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eef78b64d87775463c549fbd80e19249ef436ea3bf1de2a1eb7e717ec7fab1e9" -[[package]] -name = "libgit2-sys" -version = "0.12.21+1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86271bacd72b2b9e854c3dcfb82efd538f15f870e4c11af66900effb462f6825" -dependencies = [ - "cc", - "libc", - "libz-sys", - "pkg-config", -] - [[package]] name = "libsqlite3-sys" version = "0.22.2" @@ -1676,18 +1072,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "libz-sys" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "linked-hash-map" version = "0.5.4" @@ -1721,44 +1105,6 @@ dependencies = [ "linked-hash-map", ] -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - -[[package]] -name = "markup5ever" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" -dependencies = [ - "log", - "phf", - "phf_codegen", - "string_cache", - "string_cache_codegen", - "tendril", -] - -[[package]] -name = "markup5ever_rcdom" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f015da43bcd8d4f144559a3423f4591d69b8ce0652c905374da7205df336ae2b" -dependencies = [ - "html5ever", - "markup5ever", - "tendril", - "xml5ever", -] - [[package]] name = "match_cfg" version = "0.1.0" @@ -1771,38 +1117,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" -[[package]] -name = "mdbook" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6da0e609de0d4a7e0d42367d91b87117e3dce74d3d1699efeda1fefb2a6fa85" -dependencies = [ - "ammonia", - "anyhow", - "chrono", - "clap", - "elasticlunr-rs", - "env_logger 0.7.1", - "futures-util", - "gitignore", - "handlebars", - "lazy_static", - "log", - "memchr", - "notify", - "open", - "pulldown-cmark", - "regex", - "serde", - "serde_derive", - "serde_json", - "shlex", - "tempfile", - "tokio", - "toml", - "warp", -] - [[package]] name = "memchr" version = "2.4.0" @@ -1815,16 +1129,6 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" -[[package]] -name = "mime_guess" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" -dependencies = [ - "mime", - "unicase", -] - [[package]] name = "miniz_oxide" version = "0.4.4" @@ -1854,18 +1158,6 @@ dependencies = [ "winapi 0.2.8", ] -[[package]] -name = "mio-extras" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" -dependencies = [ - "lazycell", - "log", - "mio", - "slab", -] - [[package]] name = "mio-uds" version = "0.6.8" @@ -1900,60 +1192,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "new_debug_unreachable" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" - -[[package]] -name = "nom" -version = "5.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" -dependencies = [ - "lexical-core", - "memchr", - "version_check", -] - -[[package]] -name = "nom" -version = "6.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" -dependencies = [ - "bitvec", - "funty", - "lexical-core", - "memchr", - "version_check", -] - -[[package]] -name = "normalize-line-endings" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" - -[[package]] -name = "notify" -version = "4.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257" -dependencies = [ - "bitflags", - "filetime", - "fsevent", - "fsevent-sys", - "inotify", - "libc", - "mio", - "mio-extras", - "walkdir", - "winapi 0.3.9", -] - [[package]] name = "num-integer" version = "0.1.44" @@ -1989,28 +1227,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" -[[package]] -name = "opaque-debug" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" - [[package]] name = "opaque-debug" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" -[[package]] -name = "open" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1711eb4b31ce4ad35b0f316d8dfba4fe5c7ad601c448446d84aae7a896627b20" -dependencies = [ - "which", - "winapi 0.3.9", -] - [[package]] name = "output_vt100" version = "0.1.2" @@ -2040,7 +1262,7 @@ dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall 0.2.8", + "redox_syscall", "smallvec", "winapi 0.3.9", ] @@ -2060,78 +1282,6 @@ dependencies = [ "ucd-trie", ] -[[package]] -name = "pest_derive" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" -dependencies = [ - "maplit", - "pest", - "sha-1 0.8.2", -] - -[[package]] -name = "phf" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_codegen" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" -dependencies = [ - "phf_shared", - "rand 0.7.3", -] - -[[package]] -name = "phf_shared" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" -dependencies = [ - "siphasher", -] - [[package]] name = "pin-project" version = "0.4.28" @@ -2202,52 +1352,6 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - -[[package]] -name = "predicates" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49cfaf7fdaa3bfacc6fa3e7054e65148878354a5cfddcf661df4c851f8021df" -dependencies = [ - "difference", - "predicates-core", -] - -[[package]] -name = "predicates" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c" -dependencies = [ - "difflib", - "float-cmp", - "itertools", - "normalize-line-endings", - "predicates-core", - "regex", -] - -[[package]] -name = "predicates-core" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e35a3326b75e49aa85f5dc6ec15b41108cf5aee58eabb1f274dd18b73c2451" - -[[package]] -name = "predicates-tree" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f553275e5721409451eb85e15fd9a860a6e5ab4496eb215987502b5f5391f2" -dependencies = [ - "predicates-core", - "treeline", -] - [[package]] name = "pretty_assertions" version = "1.0.0" @@ -2260,20 +1364,6 @@ dependencies = [ "output_vt100", ] -[[package]] -name = "prettytable-rs" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fd04b170004fa2daccf418a7f8253aaf033c27760b5f225889024cf66d7ac2e" -dependencies = [ - "atty", - "csv", - "encode_unicode", - "lazy_static", - "term", - "unicode-width", -] - [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -2315,18 +1405,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "pulldown-cmark" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca36dea94d187597e104a5c8e4b07576a8a45aa5db48a65e12940d3eb7461f55" -dependencies = [ - "bitflags", - "getopts", - "memchr", - "unicase", -] - [[package]] name = "quick-error" version = "1.2.3" @@ -2348,12 +1426,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" - [[package]] name = "rand" version = "0.7.3" @@ -2365,7 +1437,6 @@ dependencies = [ "rand_chacha 0.2.2", "rand_core 0.5.1", "rand_hc 0.2.0", - "rand_pcg", ] [[package]] @@ -2436,15 +1507,6 @@ dependencies = [ "rand_core 0.6.2", ] -[[package]] -name = "rand_pcg" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core 0.5.1", -] - [[package]] name = "rand_xorshift" version = "0.3.0" @@ -2454,12 +1516,6 @@ dependencies = [ "rand_core 0.6.2", ] -[[package]] -name = "redox_syscall" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" - [[package]] name = "redox_syscall" version = "0.2.8" @@ -2469,27 +1525,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "redox_users" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" -dependencies = [ - "getrandom 0.1.16", - "redox_syscall 0.1.57", - "rust-argon2", -] - -[[package]] -name = "redox_users" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" -dependencies = [ - "getrandom 0.2.3", - "redox_syscall 0.2.8", -] - [[package]] name = "regex" version = "1.5.5" @@ -2501,12 +1536,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" - [[package]] name = "regex-syntax" version = "0.6.25" @@ -2575,18 +1604,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "rust-argon2" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" -dependencies = [ - "base64 0.13.0", - "blake2b_simd", - "constant_time_eq", - "crossbeam-utils", -] - [[package]] name = "rustc_version" version = "0.2.3" @@ -2611,7 +1628,7 @@ version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" dependencies = [ - "base64 0.13.0", + "base64", "log", "ring", "sct", @@ -2636,21 +1653,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scoped-tls" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" - [[package]] name = "scopeguard" version = "1.1.0" @@ -2685,15 +1687,6 @@ dependencies = [ "semver-parser 0.10.2", ] -[[package]] -name = "semver" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f3aac57ee7f3272d8395c6e4f502f434f0e289fcd62876f70daa008c20dcabe" -dependencies = [ - "serde", -] - [[package]] name = "semver-parser" version = "0.7.0" @@ -2740,18 +1733,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_urlencoded" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" -dependencies = [ - "dtoa", - "itoa", - "serde", - "url", -] - [[package]] name = "serde_urlencoded" version = "0.7.0" @@ -2764,29 +1745,17 @@ dependencies = [ "serde", ] -[[package]] -name = "sha-1" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" -dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", -] - [[package]] name = "sha-1" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c4cfa741c5832d0ef7fab46cabed29c2aae926db0b11bb2069edd8db5e64e16" dependencies = [ - "block-buffer 0.9.0", + "block-buffer", "cfg-if 1.0.0", "cpufeatures", - "digest 0.9.0", - "opaque-debug 0.3.0", + "digest", + "opaque-debug", ] [[package]] @@ -2795,12 +1764,6 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" -[[package]] -name = "shlex" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a568c8f2cd051a4d283bd6eb0343ac214c1b0f1ac19f93e1175b2dee38c73d" - [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -2810,12 +1773,6 @@ dependencies = [ "libc", ] -[[package]] -name = "siphasher" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbce6d4507c7e4a3962091436e56e95290cb71fa302d0d270e32130b75fbff27" - [[package]] name = "slab" version = "0.4.3" @@ -2828,12 +1785,6 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" -[[package]] -name = "smawk" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" - [[package]] name = "socket2" version = "0.3.19" @@ -2860,12 +1811,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "stdweb" version = "0.4.20" @@ -2915,61 +1860,18 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" -[[package]] -name = "string_cache" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a" -dependencies = [ - "lazy_static", - "new_debug_unreachable", - "phf_shared", - "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", -] - [[package]] name = "strsim" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" -[[package]] -name = "strum" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" - [[package]] name = "strum" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" -[[package]] -name = "strum_macros" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "strum_macros" version = "0.21.1" @@ -2993,12 +1895,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "taskchampion" version = "0.4.1" @@ -3015,45 +1911,14 @@ dependencies = [ "rusqlite", "serde", "serde_json", - "strum 0.21.0", - "strum_macros 0.21.1", + "strum", + "strum_macros", "tempfile", "thiserror", "ureq", "uuid", ] -[[package]] -name = "taskchampion-cli" -version = "0.4.1" -dependencies = [ - "anyhow", - "assert_cmd", - "atty", - "built", - "dialoguer", - "dirs-next", - "env_logger 0.8.4", - "iso8601-duration", - "lazy_static", - "log", - "mdbook", - "nom 6.1.2", - "predicates 2.1.1", - "pretty_assertions", - "prettytable-rs", - "rstest", - "serde", - "serde_json", - "taskchampion", - "tempfile", - "termcolor", - "textwrap 0.13.4", - "thiserror", - "toml", - "toml_edit", -] - [[package]] name = "taskchampion-lib" version = "0.1.0" @@ -3073,7 +1938,7 @@ dependencies = [ "anyhow", "chrono", "clap", - "env_logger 0.8.4", + "env_logger", "futures", "log", "pretty_assertions", @@ -3094,33 +1959,11 @@ dependencies = [ "cfg-if 1.0.0", "libc", "rand 0.8.3", - "redox_syscall 0.2.8", + "redox_syscall", "remove_dir_all", "winapi 0.3.9", ] -[[package]] -name = "tendril" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9ef557cb397a4f0a5a3a628f06515f78563f2209e64d47055d9dc6052bf5e33" -dependencies = [ - "futf", - "mac", - "utf-8", -] - -[[package]] -name = "term" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" -dependencies = [ - "byteorder", - "dirs", - "winapi 0.3.9", -] - [[package]] name = "termcolor" version = "1.1.2" @@ -3130,16 +1973,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "terminal_size" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" -dependencies = [ - "libc", - "winapi 0.3.9", -] - [[package]] name = "textwrap" version = "0.11.0" @@ -3149,17 +1982,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "textwrap" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd05616119e612a8041ef58f2b578906cc2531a6069047ae092cfb86a325d835" -dependencies = [ - "smawk", - "terminal_size", - "unicode-width", -] - [[package]] name = "thiserror" version = "1.0.25" @@ -3259,7 +2081,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" dependencies = [ "bytes 0.5.6", - "fnv", "futures-core", "iovec", "lazy_static", @@ -3270,34 +2091,9 @@ dependencies = [ "pin-project-lite 0.1.12", "signal-hook-registry", "slab", - "tokio-macros", "winapi 0.3.9", ] -[[package]] -name = "tokio-macros" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9e878ad426ca286e4dcae09cbd4e1973a7f8987d97570e2469703dd7f5720c" -dependencies = [ - "futures-util", - "log", - "pin-project 0.4.28", - "tokio", - "tungstenite", -] - [[package]] name = "tokio-util" version = "0.3.1" @@ -3321,23 +2117,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_edit" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbbdcf4f749dd33b1f1ea19b547bf789d87442ec40767d6015e5e2d39158d69a" -dependencies = [ - "chrono", - "combine", - "linked-hash-map", -] - -[[package]] -name = "tower-service" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" - [[package]] name = "tracing" version = "0.1.26" @@ -3369,12 +2148,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "treeline" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" - [[package]] name = "trust-dns-proto" version = "0.19.7" @@ -3414,31 +2187,6 @@ dependencies = [ "trust-dns-proto", ] -[[package]] -name = "try-lock" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" - -[[package]] -name = "tungstenite" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0308d80d86700c5878b9ef6321f020f29b1bb9d5ff3cab25e75e23f3a492a23" -dependencies = [ - "base64 0.12.3", - "byteorder", - "bytes 0.5.6", - "http", - "httparse", - "input_buffer", - "log", - "rand 0.7.3", - "sha-1 0.9.6", - "url", - "utf-8", -] - [[package]] name = "typenum" version = "1.13.0" @@ -3451,15 +2199,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" -[[package]] -name = "unicase" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" -dependencies = [ - "version_check", -] - [[package]] name = "unicode-bidi" version = "0.3.5" @@ -3508,7 +2247,7 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2475a6781e9bc546e7b64f4013d2f4032c8c6a40fcffd7c6f4ee734a890972ab" dependencies = [ - "base64 0.13.0", + "base64", "chunked_transfer", "log", "once_cell", @@ -3530,18 +2269,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "urlencoding" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a1f0175e03a0973cf4afd476bef05c26e228520400eb1fd473ad417b1c00ffb" - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "uuid" version = "0.8.2" @@ -3579,54 +2306,6 @@ dependencies = [ "libc", ] -[[package]] -name = "walkdir" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" -dependencies = [ - "same-file", - "winapi 0.3.9", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" -dependencies = [ - "log", - "try-lock", -] - -[[package]] -name = "warp" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f41be6df54c97904af01aa23e613d4521eed7ab23537cede692d4058f6449407" -dependencies = [ - "bytes 0.5.6", - "futures", - "headers", - "http", - "hyper", - "log", - "mime", - "mime_guess", - "pin-project 0.4.28", - "scoped-tls", - "serde", - "serde_json", - "serde_urlencoded 0.6.1", - "tokio", - "tokio-tungstenite", - "tower-service", - "tracing", - "tracing-futures", - "urlencoding", -] - [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -3722,16 +2401,6 @@ dependencies = [ "webpki", ] -[[package]] -name = "which" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55551e42cbdf2ce2bedd2203d0cc08dba002c27510f86dab6d0ce304cba3dfe" -dependencies = [ - "either", - "libc", -] - [[package]] name = "widestring" version = "0.4.3" @@ -3800,24 +2469,6 @@ dependencies = [ "winapi-build", ] -[[package]] -name = "wyz" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" - -[[package]] -name = "xml5ever" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b1b52e6e8614d4a58b8e70cf51ec0cc21b256ad8206708bcff8139b5bbd6a59" -dependencies = [ - "log", - "mac", - "markup5ever", - "time 0.1.43", -] - [[package]] name = "xtask" version = "0.4.1" @@ -3825,9 +2476,3 @@ dependencies = [ "anyhow", "cbindgen", ] - -[[package]] -name = "zeroize" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" diff --git a/Cargo.toml b/Cargo.toml index 32e10dd3d..51108cbb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ members = [ "rust/taskchampion", - "rust/cli", "rust/sync-server", "rust/lib", "rust/integration-tests", diff --git a/rust/cli/Cargo.toml b/rust/cli/Cargo.toml deleted file mode 100644 index ae7d812c6..000000000 --- a/rust/cli/Cargo.toml +++ /dev/null @@ -1,57 +0,0 @@ -[package] -authors = ["Dustin J. Mitchell "] -edition = "2018" -name = "taskchampion-cli" -version = "0.4.1" - -build = "build.rs" - -# Run 'ta' when doing 'cargo run' at repo root -default-run = "ta" - -[dependencies] -dirs-next = "^2.0.0" -env_logger = "^0.8.3" -anyhow = "1.0" -thiserror = "1.0" -log = "^0.4.17" -nom = "^6.1.2" -prettytable-rs = "^0.8.0" -textwrap = { version="^0.13.4", features=["terminal_size"] } -termcolor = "^1.1.2" -atty = "^0.2.14" -toml = "^0.5.8" -toml_edit = "^0.2.0" -serde = { version = "^1.0.125", features = ["derive"] } -serde_json = "^1.0" -lazy_static = "1" -iso8601-duration = "0.1" -dialoguer = "0.10" - -# only needed for usage-docs -# if the mdbook version changes, change it in .github/workflows/publish-docs.yml and .github/workflows/checks.yml as well -mdbook = { version = "0.4.10", optional = true } - -[dependencies.taskchampion] -path = "../taskchampion" - -[build-dependencies] -built = { version = "0.5", features = ["git2"] } - -[dev-dependencies] -assert_cmd = "^1.0.3" -predicates = "^2.1.1" -tempfile = "3" -rstest = "0.10" -pretty_assertions = "1" - -[features] -usage-docs = [ "mdbook" ] - -[[bin]] -name = "ta" - -[[bin]] -# this is an mdbook plugin and only needed when running `mdbook` -name = "usage-docs" -required-features = [ "usage-docs" ] diff --git a/rust/cli/build.rs b/rust/cli/build.rs deleted file mode 100644 index d8f91cb91..000000000 --- a/rust/cli/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - built::write_built_file().expect("Failed to acquire build-time information"); -} diff --git a/rust/cli/src/argparse/args/arg_matching.rs b/rust/cli/src/argparse/args/arg_matching.rs deleted file mode 100644 index a95e4ec1e..000000000 --- a/rust/cli/src/argparse/args/arg_matching.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::argparse::ArgList; -use nom::{ - error::{Error, ErrorKind}, - Err, IResult, -}; - -/// Consume a single argument from an argument list that matches the given string parser (one -/// of the other functions in this module). The given parser must consume the entire input. -pub(crate) fn arg_matching<'a, O, F>(f: F) -> impl Fn(ArgList<'a>) -> IResult -where - F: Fn(&'a str) -> IResult<&'a str, O>, -{ - move |input: ArgList<'a>| { - if let Some(arg) = input.get(0) { - return match f(arg) { - Ok(("", rv)) => Ok((&input[1..], rv)), - // single-arg parsers must consume the entire arg, so consider unconsumed - // output to be an error. - Ok((_, _)) => Err(Err::Error(Error { - input, - code: ErrorKind::Eof, - })), - // single-arg parsers are all complete parsers - Err(Err::Incomplete(_)) => unreachable!(), - // for error and failure, rewrite to an error at this position in the arugment list - Err(Err::Error(Error { input: _, code })) => Err(Err::Error(Error { input, code })), - Err(Err::Failure(Error { input: _, code })) => { - Err(Err::Failure(Error { input, code })) - } - }; - } - - Err(Err::Error(Error { - input, - // since we're using nom's built-in Error, our choices here are limited, but tihs - // occurs when there's no argument where one is expected, so Eof seems appropriate - code: ErrorKind::Eof, - })) - } -} - -#[cfg(test)] -mod test { - use super::super::*; - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_arg_matching() { - assert_eq!( - arg_matching(plus_tag)(argv!["+foo", "bar"]).unwrap(), - (argv!["bar"], tag!("foo")) - ); - assert!(arg_matching(plus_tag)(argv!["foo", "bar"]).is_err()); - } - - #[test] - fn test_partial_arg_matching() { - assert!(arg_matching(wait_colon)(argv!["wait:UNRECOGNIZED"]).is_err()); - } -} diff --git a/rust/cli/src/argparse/args/colon.rs b/rust/cli/src/argparse/args/colon.rs deleted file mode 100644 index bca014081..000000000 --- a/rust/cli/src/argparse/args/colon.rs +++ /dev/null @@ -1,98 +0,0 @@ -use super::{any, id_list, timestamp, TaskId}; -use crate::argparse::NOW; -use nom::bytes::complete::tag as nomtag; -use nom::{branch::*, character::complete::*, combinator::*, sequence::*, IResult}; -use taskchampion::chrono::prelude::*; -use taskchampion::Status; - -/// Recognizes up to the colon of the common `:...` syntax -fn colon_prefix(prefix: &'static str) -> impl Fn(&str) -> IResult<&str, &str> { - fn to_suffix<'a>(input: (&'a str, char, &'a str)) -> Result<&'a str, ()> { - Ok(input.2) - } - move |input: &str| { - map_res( - all_consuming(tuple((nomtag(prefix), char(':'), any))), - to_suffix, - )(input) - } -} - -/// Recognizes `status:{pending,completed,deleted}` -pub(crate) fn status_colon(input: &str) -> IResult<&str, Status> { - fn to_status(input: &str) -> Result { - match input { - "pending" => Ok(Status::Pending), - "completed" => Ok(Status::Completed), - "deleted" => Ok(Status::Deleted), - _ => Err(()), - } - } - map_res(colon_prefix("status"), to_status)(input) -} - -/// Recognizes `wait:` to None and `wait:` to `Some(ts)` -pub(crate) fn wait_colon(input: &str) -> IResult<&str, Option>> { - fn to_wait(input: DateTime) -> Result>, ()> { - Ok(Some(input)) - } - fn to_none(_: &str) -> Result>, ()> { - Ok(None) - } - preceded( - nomtag("wait:"), - alt(( - map_res(timestamp(*NOW, Local), to_wait), - map_res(nomtag(""), to_none), - )), - )(input) -} - -/// Recognizes `depends:` to `(true, )` and `depends:-` to `(false, )`. -pub(crate) fn depends_colon(input: &str) -> IResult<&str, (bool, Vec)> { - fn to_bool(maybe_minus: Option) -> Result { - Ok(maybe_minus.is_none()) // None -> true, Some -> false - } - preceded( - nomtag("depends:"), - pair(map_res(opt(char('-')), to_bool), id_list), - )(input) -} - -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - use taskchampion::chrono::Duration; - - #[test] - fn test_colon_prefix() { - assert_eq!(colon_prefix("foo")("foo:abc").unwrap().1, "abc"); - assert_eq!(colon_prefix("foo")("foo:").unwrap().1, ""); - assert!(colon_prefix("foo")("foo").is_err()); - } - - #[test] - fn test_status_colon() { - assert_eq!(status_colon("status:pending").unwrap().1, Status::Pending); - assert_eq!( - status_colon("status:completed").unwrap().1, - Status::Completed - ); - assert_eq!(status_colon("status:deleted").unwrap().1, Status::Deleted); - assert!(status_colon("status:foo").is_err()); - assert!(status_colon("status:complete").is_err()); - assert!(status_colon("status").is_err()); - } - - #[test] - fn test_wait() { - assert_eq!(wait_colon("wait:").unwrap(), ("", None)); - - let one_day = *NOW + Duration::days(1); - assert_eq!(wait_colon("wait:1d").unwrap(), ("", Some(one_day))); - - let one_day = *NOW + Duration::days(1); - assert_eq!(wait_colon("wait:1d2").unwrap(), ("2", Some(one_day))); - } -} diff --git a/rust/cli/src/argparse/args/idlist.rs b/rust/cli/src/argparse/args/idlist.rs deleted file mode 100644 index a7ea71e0e..000000000 --- a/rust/cli/src/argparse/args/idlist.rs +++ /dev/null @@ -1,140 +0,0 @@ -use nom::{branch::*, character::complete::*, combinator::*, multi::*, sequence::*, IResult}; -use taskchampion::Uuid; - -/// A task identifier, as given in a filter command-line expression -#[derive(Debug, PartialEq, Eq, Hash, Clone)] -pub(crate) enum TaskId { - /// A small integer identifying a working-set task - WorkingSetId(usize), - - /// A full Uuid specifically identifying a task - Uuid(Uuid), - - /// A prefix of a Uuid - PartialUuid(String), -} - -/// Recognizes a comma-separated list of TaskIds -pub(crate) fn id_list(input: &str) -> IResult<&str, Vec> { - fn hex_n(n: usize) -> impl Fn(&str) -> IResult<&str, &str> { - move |input: &str| recognize(many_m_n(n, n, one_of(&b"0123456789abcdefABCDEF"[..])))(input) - } - fn uuid(input: &str) -> Result { - Ok(TaskId::Uuid(Uuid::parse_str(input).map_err(|_| ())?)) - } - fn partial_uuid(input: &str) -> Result { - Ok(TaskId::PartialUuid(input.to_owned())) - } - fn working_set_id(input: &str) -> Result { - Ok(TaskId::WorkingSetId(input.parse().map_err(|_| ())?)) - } - all_consuming(separated_list1( - char(','), - alt(( - map_res( - recognize(tuple(( - hex_n(8), - char('-'), - hex_n(4), - char('-'), - hex_n(4), - char('-'), - hex_n(4), - char('-'), - hex_n(12), - ))), - uuid, - ), - map_res( - recognize(tuple(( - hex_n(8), - char('-'), - hex_n(4), - char('-'), - hex_n(4), - char('-'), - hex_n(4), - ))), - partial_uuid, - ), - map_res( - recognize(tuple((hex_n(8), char('-'), hex_n(4), char('-'), hex_n(4)))), - partial_uuid, - ), - map_res( - recognize(tuple((hex_n(8), char('-'), hex_n(4)))), - partial_uuid, - ), - map_res(hex_n(8), partial_uuid), - // note that an 8-decimal-digit value will be treated as a UUID - map_res(digit1, working_set_id), - )), - ))(input) -} - -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_id_list_single() { - assert_eq!(id_list("123").unwrap().1, vec![TaskId::WorkingSetId(123)]); - } - - #[test] - fn test_id_list_uuids() { - assert_eq!( - id_list("12341234").unwrap().1, - vec![TaskId::PartialUuid(s!("12341234"))] - ); - assert_eq!( - id_list("1234abcd").unwrap().1, - vec![TaskId::PartialUuid(s!("1234abcd"))] - ); - assert_eq!( - id_list("abcd1234").unwrap().1, - vec![TaskId::PartialUuid(s!("abcd1234"))] - ); - assert_eq!( - id_list("abcd1234-1234").unwrap().1, - vec![TaskId::PartialUuid(s!("abcd1234-1234"))] - ); - assert_eq!( - id_list("abcd1234-1234-2345").unwrap().1, - vec![TaskId::PartialUuid(s!("abcd1234-1234-2345"))] - ); - assert_eq!( - id_list("abcd1234-1234-2345-3456").unwrap().1, - vec![TaskId::PartialUuid(s!("abcd1234-1234-2345-3456"))] - ); - assert_eq!( - id_list("abcd1234-1234-2345-3456-0123456789ab").unwrap().1, - vec![TaskId::Uuid( - Uuid::parse_str("abcd1234-1234-2345-3456-0123456789ab").unwrap() - )] - ); - } - - #[test] - fn test_id_list_invalid_partial_uuids() { - assert!(id_list("abcd123").is_err()); - assert!(id_list("abcd12345").is_err()); - assert!(id_list("abcd1234-").is_err()); - assert!(id_list("abcd1234-123").is_err()); - assert!(id_list("abcd1234-1234-").is_err()); - assert!(id_list("abcd1234-12345-").is_err()); - assert!(id_list("abcd1234-1234-2345-3456-0123456789ab-").is_err()); - } - - #[test] - fn test_id_list_uuids_mixed() { - assert_eq!(id_list("abcd1234,abcd1234-1234,abcd1234-1234-2345,abcd1234-1234-2345-3456,abcd1234-1234-2345-3456-0123456789ab").unwrap().1, - vec![TaskId::PartialUuid(s!("abcd1234")), - TaskId::PartialUuid(s!("abcd1234-1234")), - TaskId::PartialUuid(s!("abcd1234-1234-2345")), - TaskId::PartialUuid(s!("abcd1234-1234-2345-3456")), - TaskId::Uuid(Uuid::parse_str("abcd1234-1234-2345-3456-0123456789ab").unwrap()), - ]); - } -} diff --git a/rust/cli/src/argparse/args/misc.rs b/rust/cli/src/argparse/args/misc.rs deleted file mode 100644 index 5cb957f10..000000000 --- a/rust/cli/src/argparse/args/misc.rs +++ /dev/null @@ -1,42 +0,0 @@ -use nom::bytes::complete::tag as nomtag; -use nom::{character::complete::*, combinator::*, sequence::*, IResult}; - -/// Recognizes any argument -pub(crate) fn any(input: &str) -> IResult<&str, &str> { - rest(input) -} - -/// Recognizes a report name -pub(crate) fn report_name(input: &str) -> IResult<&str, &str> { - all_consuming(recognize(pair(alpha1, alphanumeric0)))(input) -} - -/// Recognizes a literal string -pub(crate) fn literal(literal: &'static str) -> impl Fn(&str) -> IResult<&str, &str> { - move |input: &str| all_consuming(nomtag(literal))(input) -} - -#[cfg(test)] -mod test { - use super::super::*; - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_arg_matching() { - assert_eq!( - arg_matching(plus_tag)(argv!["+foo", "bar"]).unwrap(), - (argv!["bar"], tag!("foo")) - ); - assert!(arg_matching(plus_tag)(argv!["foo", "bar"]).is_err()); - } - - #[test] - fn test_literal() { - assert_eq!(literal("list")("list").unwrap().1, "list"); - assert!(literal("list")("listicle").is_err()); - assert!(literal("list")(" list ").is_err()); - assert!(literal("list")("LiSt").is_err()); - assert!(literal("list")("denylist").is_err()); - } -} diff --git a/rust/cli/src/argparse/args/mod.rs b/rust/cli/src/argparse/args/mod.rs deleted file mode 100644 index b9c20e9cd..000000000 --- a/rust/cli/src/argparse/args/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Parsers for single arguments (strings) - -mod arg_matching; -mod colon; -mod idlist; -mod misc; -mod tags; -mod time; - -pub(crate) use arg_matching::arg_matching; -pub(crate) use colon::{depends_colon, status_colon, wait_colon}; -pub(crate) use idlist::{id_list, TaskId}; -pub(crate) use misc::{any, literal, report_name}; -pub(crate) use tags::{minus_tag, plus_tag}; -#[allow(unused_imports)] -pub(crate) use time::{duration, timestamp}; diff --git a/rust/cli/src/argparse/args/tags.rs b/rust/cli/src/argparse/args/tags.rs deleted file mode 100644 index 1dbea5ee2..000000000 --- a/rust/cli/src/argparse/args/tags.rs +++ /dev/null @@ -1,35 +0,0 @@ -use nom::{character::complete::*, combinator::*, sequence::*, IResult}; -use std::convert::TryFrom; -use taskchampion::Tag; - -/// Recognizes a tag prefixed with `+` and returns the tag value -pub(crate) fn plus_tag(input: &str) -> IResult<&str, Tag> { - preceded(char('+'), map_res(rest, Tag::try_from))(input) -} - -/// Recognizes a tag prefixed with `-` and returns the tag value -pub(crate) fn minus_tag(input: &str) -> IResult<&str, Tag> { - preceded(char('-'), map_res(rest, Tag::try_from))(input) -} - -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_plus_tag() { - assert_eq!(plus_tag("+abc").unwrap().1, tag!("abc")); - assert_eq!(plus_tag("+abc123").unwrap().1, tag!("abc123")); - assert!(plus_tag("-abc123").is_err()); - assert!(plus_tag("+1abc").is_err()); - } - - #[test] - fn test_minus_tag() { - assert_eq!(minus_tag("-abc").unwrap().1, tag!("abc")); - assert_eq!(minus_tag("-abc123").unwrap().1, tag!("abc123")); - assert!(minus_tag("+abc123").is_err()); - assert!(minus_tag("-1abc").is_err()); - } -} diff --git a/rust/cli/src/argparse/args/time.rs b/rust/cli/src/argparse/args/time.rs deleted file mode 100644 index 4848e3888..000000000 --- a/rust/cli/src/argparse/args/time.rs +++ /dev/null @@ -1,492 +0,0 @@ -use iso8601_duration::Duration as IsoDuration; -use lazy_static::lazy_static; -use nom::{ - branch::*, - bytes::complete::*, - character::complete::*, - character::*, - combinator::*, - error::{Error, ErrorKind}, - multi::*, - sequence::*, - Err, IResult, -}; -use std::str::FromStr; -use taskchampion::chrono::{self, prelude::*, Duration}; - -// https://taskwarrior.org/docs/dates.html -// https://taskwarrior.org/docs/named_dates.html -// https://taskwarrior.org/docs/durations.html - -/// A case for matching durations. If `.3` is true, then the value can be used -/// without a prefix, e.g., `minute`. If false, it cannot, e.g., `minutes` -#[derive(Debug)] -struct DurationCase(&'static str, Duration, bool); - -// https://github.com/GothenburgBitFactory/libshared/blob/9a5f24e2acb38d05afb8f8e316a966dee196a42a/src/Duration.cpp#L50 -// TODO: use const when chrono supports it -lazy_static! { - static ref DURATION_CASES: Vec = vec![ - DurationCase("days", Duration::days(1), false), - DurationCase("day", Duration::days(1), true), - DurationCase("d", Duration::days(1), false), - DurationCase("hours", Duration::hours(1), false), - DurationCase("hour", Duration::hours(1), true), - DurationCase("h", Duration::hours(1), false), - DurationCase("minutes", Duration::minutes(1), false), - DurationCase("minute", Duration::minutes(1), true), - DurationCase("mins", Duration::minutes(1), false), - DurationCase("min", Duration::minutes(1), true), - DurationCase("months", Duration::days(30), false), - DurationCase("month", Duration::days(30), true), - DurationCase("mo", Duration::days(30), true), - DurationCase("seconds", Duration::seconds(1), false), - DurationCase("second", Duration::seconds(1), true), - DurationCase("s", Duration::seconds(1), false), - DurationCase("weeks", Duration::days(7), false), - DurationCase("week", Duration::days(7), true), - DurationCase("w", Duration::days(7), false), - DurationCase("years", Duration::days(365), false), - DurationCase("year", Duration::days(365), true), - DurationCase("y", Duration::days(365), false), - ]; -} - -/// Parses suffixes like 'min', and 'd'; standalone is true if there is no numeric prefix, in which -/// case plurals (like `days`) are not matched. -fn duration_suffix(has_prefix: bool) -> impl Fn(&str) -> IResult<&str, Duration> { - move |input: &str| { - // Rust wants this to have a default value, but it is not actually used - // because DURATION_CASES has at least one case with case.2 == `true` - let mut res = Err(Err::Failure(Error::new(input, ErrorKind::Tag))); - for case in DURATION_CASES.iter() { - if !case.2 && !has_prefix { - // this case requires a prefix, and input does not have one - continue; - } - res = tag(case.0)(input); - match res { - Ok((i, _)) => { - return Ok((i, case.1)); - } - Err(Err::Error(_)) => { - // recoverable error - continue; - } - Err(e) => { - // irrecoverable error - return Err(e); - } - } - } - - // return the last error - Err(res.unwrap_err()) - } -} -/// Calculate the multiplier for a decimal prefix; this uses integer math -/// where possible, falling back to floating-point math on seconds -fn decimal_prefix_multiplier(input: &str) -> IResult<&str, f64> { - map_res( - // recognize NN or NN.NN - alt((recognize(tuple((digit1, char('.'), digit1))), digit1)), - |input: &str| -> Result::Err> { - let mul = input.parse::()?; - Ok(mul) - }, - )(input) -} - -/// Parse an iso8601 duration, converting it to a [`chrono::Duration`] on the assumption -/// that a year is 365 days and a month is 30 days. -fn iso8601_dur(input: &str) -> IResult<&str, Duration> { - if let Ok(iso_dur) = IsoDuration::parse(input) { - // iso8601_duration uses f32, but f32 underflows seconds for values as small as - // a year. So we upgrade to f64 immediately. f64 has a 53-bit mantissa which can - // represent almost 300 million years without underflow, so it should be adequate. - let days = iso_dur.year as f64 * 365.0 + iso_dur.month as f64 * 30.0 + iso_dur.day as f64; - let hours = days * 24.0 + iso_dur.hour as f64; - let mins = hours * 60.0 + iso_dur.minute as f64; - let secs = mins * 60.0 + iso_dur.second as f64; - let dur = Duration::seconds(secs as i64); - Ok((&input[input.len()..], dur)) - } else { - Err(Err::Error(Error::new(input, ErrorKind::Tag))) - } -} - -/// Recognizes durations -pub(crate) fn duration(input: &str) -> IResult<&str, Duration> { - alt(( - map_res( - tuple(( - decimal_prefix_multiplier, - multispace0, - duration_suffix(true), - )), - |input: (f64, &str, Duration)| -> Result { - // `as i64` is saturating, so for large offsets this will - // just pick an imprecise very-futuristic date - let secs = (input.0 * input.2.num_seconds() as f64) as i64; - Ok(Duration::seconds(secs)) - }, - ), - duration_suffix(false), - iso8601_dur, - ))(input) -} - -/// Parse a rfc3339 datestamp -fn rfc3339_timestamp(input: &str) -> IResult<&str, DateTime> { - if let Ok(dt) = DateTime::parse_from_rfc3339(input) { - // convert to UTC and truncate seconds - let dt = dt.with_timezone(&Utc).trunc_subsecs(0); - Ok((&input[input.len()..], dt)) - } else { - Err(Err::Error(Error::new(input, ErrorKind::Tag))) - } -} - -fn named_date( - now: DateTime, - local: Tz, -) -> impl Fn(&str) -> IResult<&str, DateTime> { - move |input: &str| { - let local_today = now.with_timezone(&local).date(); - let remaining = &input[input.len()..]; - let day_index = local_today.weekday().num_days_from_monday(); - match input { - "yesterday" => Ok((remaining, local_today - Duration::days(1))), - "today" => Ok((remaining, local_today)), - "tomorrow" => Ok((remaining, local_today + Duration::days(1))), - // TODO: lots more! - "eod" => Ok((remaining, local_today + Duration::days(1))), - "sod" => Ok((remaining, local_today)), - "eow" => Ok(( - remaining, - local_today + Duration::days((6 - day_index).into()), - )), - "eoww" => Ok(( - remaining, - local_today + Duration::days((5 - day_index).into()), - )), - "sow" => Ok(( - remaining, - local_today + Duration::days((6 - day_index).into()), - )), - "soww" => Ok(( - remaining, - local_today + Duration::days((7 - day_index).into()), - )), - _ => Err(Err::Error(Error::new(input, ErrorKind::Tag))), - } - .map(|(rem, dt)| (rem, dt.and_hms(0, 0, 0).with_timezone(&Utc))) - } -} - -/// recognize a digit -fn digit(input: &str) -> IResult<&str, char> { - satisfy(|c| is_digit(c as u8))(input) -} - -/// Parse yyyy-mm-dd as the given date, at the local midnight -fn yyyy_mm_dd(local: Tz) -> impl Fn(&str) -> IResult<&str, DateTime> { - move |input: &str| { - fn parse_int(input: &str) -> Result::Err> { - input.parse::() - } - map_res( - tuple(( - map_res(recognize(count(digit, 4)), parse_int::), - char('-'), - map_res(recognize(many_m_n(1, 2, digit)), parse_int::), - char('-'), - map_res(recognize(many_m_n(1, 2, digit)), parse_int::), - )), - |input: (i32, char, u32, char, u32)| -> Result, ()> { - // try to convert, handling out-of-bounds months or days as an error - let ymd = match local.ymd_opt(input.0, input.2, input.4) { - chrono::LocalResult::Single(ymd) => Ok(ymd), - _ => Err(()), - }?; - Ok(ymd.and_hms(0, 0, 0).with_timezone(&Utc)) - }, - )(input) - } -} - -/// Recognizes timestamps -pub(crate) fn timestamp( - now: DateTime, - local: Tz, -) -> impl Fn(&str) -> IResult<&str, DateTime> { - move |input: &str| { - alt(( - // relative time - map_res( - duration, - |duration: Duration| -> Result, ()> { Ok(now + duration) }, - ), - rfc3339_timestamp, - yyyy_mm_dd(local), - value(now, tag("now")), - named_date(now, local), - ))(input) - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::argparse::NOW; - use pretty_assertions::assert_eq; - use rstest::rstest; - - const M: i64 = 60; - const H: i64 = M * 60; - const DAY: i64 = H * 24; - const MONTH: i64 = DAY * 30; - const YEAR: i64 = DAY * 365; - - // TODO: use const when chrono supports it - lazy_static! { - // India standard time (not an even multiple of hours) - static ref IST: FixedOffset = FixedOffset::east(5 * 3600 + 30 * 60); - // Utc, but as a FixedOffset TimeZone impl - static ref UTC_FO: FixedOffset = FixedOffset::east(0); - // Hawaii - static ref HST: FixedOffset = FixedOffset::west(10 * 3600); - } - - /// test helper to ensure that the entire input is consumed - fn complete_duration(input: &str) -> IResult<&str, Duration> { - all_consuming(duration)(input) - } - - /// test helper to ensure that the entire input is consumed - fn complete_timestamp( - now: DateTime, - local: Tz, - ) -> impl Fn(&str) -> IResult<&str, DateTime> { - move |input: &str| all_consuming(timestamp(now, local))(input) - } - - /// Shorthand day and time - fn dt(y: i32, m: u32, d: u32, hh: u32, mm: u32, ss: u32) -> DateTime { - Utc.ymd(y, m, d).and_hms(hh, mm, ss) - } - - /// Local day and time, parameterized on the timezone - fn ldt( - y: i32, - m: u32, - d: u32, - hh: u32, - mm: u32, - ss: u32, - ) -> Box DateTime> { - Box::new(move |tz| tz.ymd(y, m, d).and_hms(hh, mm, ss).with_timezone(&Utc)) - } - - fn ld(y: i32, m: u32, d: u32) -> Box DateTime> { - ldt(y, m, d, 0, 0, 0) - } - - #[rstest] - #[case::rel_hours_0(dt(2021, 5, 29, 1, 30, 0), "0h", dt(2021, 5, 29, 1, 30, 0))] - #[case::rel_hours_05(dt(2021, 5, 29, 1, 30, 0), "0.5h", dt(2021, 5, 29, 2, 0, 0))] - #[case::rel_hours_no_prefix(dt(2021, 5, 29, 1, 30, 0), "hour", dt(2021, 5, 29, 2, 30, 0))] - #[case::rel_hours_5(dt(2021, 5, 29, 1, 30, 0), "5h", dt(2021, 5, 29, 6, 30, 0))] - #[case::rel_days_0(dt(2021, 5, 29, 1, 30, 0), "0d", dt(2021, 5, 29, 1, 30, 0))] - #[case::rel_days_10(dt(2021, 5, 29, 1, 30, 0), "10d", dt(2021, 6, 8, 1, 30, 0))] - #[case::rfc3339_datetime(*NOW, "2019-10-12T07:20:50.12Z", dt(2019, 10, 12, 7, 20, 50))] - #[case::now(*NOW, "now", *NOW)] - /// Cases where the `local` parameter is ignored - fn test_nonlocal_timestamp( - #[case] now: DateTime, - #[case] input: &'static str, - #[case] output: DateTime, - ) { - let (_, res) = complete_timestamp(now, *IST)(input).unwrap(); - assert_eq!(res, output, "parsing {:?}", input); - } - - #[rstest] - /// Cases where the `local` parameter matters - #[case::yyyy_mm_dd(ld(2000, 1, 1), "2021-01-01", ld(2021, 1, 1))] - #[case::yyyy_m_d(ld(2000, 1, 1), "2021-1-1", ld(2021, 1, 1))] - #[case::yesterday(ld(2021, 3, 1), "yesterday", ld(2021, 2, 28))] - #[case::yesterday_from_evening(ldt(2021, 3, 1, 21, 30, 30), "yesterday", ld(2021, 2, 28))] - #[case::today(ld(2021, 3, 1), "today", ld(2021, 3, 1))] - #[case::today_from_evening(ldt(2021, 3, 1, 21, 30, 30), "today", ld(2021, 3, 1))] - #[case::tomorrow(ld(2021, 3, 1), "tomorrow", ld(2021, 3, 2))] - #[case::tomorow_from_evening(ldt(2021, 3, 1, 21, 30, 30), "tomorrow", ld(2021, 3, 2))] - #[case::end_of_week(ld(2021, 8, 25,), "eow", ld(2021, 8, 29))] - #[case::end_of_work_week(ld(2021, 8, 25), "eoww", ld(2021, 8, 28))] - #[case::start_of_week(ld(2021, 8, 25), "sow", ld(2021, 8, 29))] - #[case::start_of_work_week(ld(2021, 8, 25), "soww", ld(2021, 8, 30))] - #[case::end_of_today(ld(2021, 8, 25), "eod", ld(2021, 8, 26))] - #[case::start_of_today(ld(2021, 8, 25), "sod", ld(2021, 8, 25))] - fn test_local_timestamp( - #[case] now: Box DateTime>, - #[values(*IST, *UTC_FO, *HST)] tz: FixedOffset, - #[case] input: &str, - #[case] output: Box DateTime>, - ) { - let now = now(tz); - let output = output(tz); - let (_, res) = complete_timestamp(now, tz)(input).unwrap(); - assert_eq!( - res, output, - "parsing {:?} relative to {:?} in timezone {:?}", - input, now, tz - ); - } - - #[rstest] - #[case::rfc3339_datetime_bad_month(*NOW, "2019-10-99T07:20:50.12Z")] - #[case::yyyy_mm_dd_bad_month(*NOW, "2019-10-99")] - fn test_timestamp_err(#[case] now: DateTime, #[case] input: &'static str) { - let res = complete_timestamp(now, Utc)(input); - assert!( - res.is_err(), - "expected error parsing {:?}, got {:?}", - input, - res.unwrap() - ); - } - - // All test cases from - // https://github.com/GothenburgBitFactory/libshared/blob/9a5f24e2acb38d05afb8f8e316a966dee196a42a/test/duration.t.cpp#L136 - #[rstest] - #[case("0seconds", 0)] - #[case("2 seconds", 2)] - #[case("10seconds", 10)] - #[case("1.5seconds", 1)] - #[case("0second", 0)] - #[case("2 second", 2)] - #[case("10second", 10)] - #[case("1.5second", 1)] - #[case("0s", 0)] - #[case("2 s", 2)] - #[case("10s", 10)] - #[case("1.5s", 1)] - #[case("0minutes", 0)] - #[case("2 minutes", 2 * M)] - #[case("10minutes", 10 * M)] - #[case("1.5minutes", M + 30)] - #[case("0minute", 0)] - #[case("2 minute", 2 * M)] - #[case("10minute", 10 * M)] - #[case("1.5minute", M + 30)] - #[case("0min", 0)] - #[case("2 min", 2 * M)] - #[case("10min", 10 * M)] - #[case("1.5min", M + 30)] - #[case("0hours", 0)] - #[case("2 hours", 2 * H)] - #[case("10hours", 10 * H)] - #[case("1.5hours", H + 30 * M)] - #[case("0hour", 0)] - #[case("2 hour", 2 * H)] - #[case("10hour", 10 * H)] - #[case("1.5hour", H + 30 * M)] - #[case("0h", 0)] - #[case("2 h", 2 * H)] - #[case("10h", 10 * H)] - #[case("1.5h", H + 30 * M)] - #[case("0days", 0)] - #[case("2 days", 2 * DAY)] - #[case("10days", 10 * DAY)] - #[case("1.5days", DAY + 12 * H)] - #[case("0day", 0)] - #[case("2 day", 2 * DAY)] - #[case("10day", 10 * DAY)] - #[case("1.5day", DAY + 12 * H)] - #[case("0d", 0)] - #[case("2 d", 2 * DAY)] - #[case("10d", 10 * DAY)] - #[case("1.5d", DAY + 12 * H)] - #[case("0weeks", 0)] - #[case("2 weeks", 14 * DAY)] - #[case("10weeks", 70 * DAY)] - #[case("1.5weeks", 10 * DAY + 12 * H)] - #[case("0week", 0)] - #[case("2 week", 14 * DAY)] - #[case("10week", 70 * DAY)] - #[case("1.5week", 10 * DAY + 12 * H)] - #[case("0w", 0)] - #[case("2 w", 14 * DAY)] - #[case("10w", 70 * DAY)] - #[case("1.5w", 10 * DAY + 12 * H)] - #[case("0months", 0)] - #[case("2 months", 60 * DAY)] - #[case("10months", 300 * DAY)] - #[case("1.5months", 45 * DAY)] - #[case("0month", 0)] - #[case("2 month", 60 * DAY)] - #[case("10month", 300 * DAY)] - #[case("1.5month", 45 * DAY)] - #[case("0mo", 0)] - #[case("2 mo", 60 * DAY)] - #[case("10mo", 300 * DAY)] - #[case("1.5mo", 45 * DAY)] - #[case("0years", 0)] - #[case("2 years", 2 * YEAR)] - #[case("10years", 10 * YEAR)] - #[case("1.5years", 547 * DAY + 12 * H)] - #[case("0year", 0)] - #[case("2 year", 2 * YEAR)] - #[case("10year", 10 * YEAR)] - #[case("1.5year", 547 * DAY + 12 * H)] - #[case("0y", 0)] - #[case("2 y", 2 * YEAR)] - #[case("10y", 10 * YEAR)] - #[case("1.5y", 547 * DAY + 12 * H)] - fn test_duration_units(#[case] input: &'static str, #[case] seconds: i64) { - let (_, res) = complete_duration(input).expect(input); - assert_eq!(res.num_seconds(), seconds, "parsing {}", input); - } - - #[rstest] - #[case("years")] - #[case("minutes")] - #[case("eons")] - #[case("P1S")] // missing T - #[case("p1y")] // lower-case - fn test_duration_errors(#[case] input: &'static str) { - let res = complete_duration(input); - assert!( - res.is_err(), - "did not get expected error parsing duration {:?}; got {:?}", - input, - res.unwrap() - ); - } - - // https://github.com/GothenburgBitFactory/libshared/blob/9a5f24e2acb38d05afb8f8e316a966dee196a42a/test/duration.t.cpp#L115 - #[rstest] - #[case("P1Y", YEAR)] - #[case("P1M", MONTH)] - #[case("P1D", DAY)] - #[case("P1Y1M", YEAR + MONTH)] - #[case("P1Y1D", YEAR + DAY)] - #[case("P1M1D", MONTH + DAY)] - #[case("P1Y1M1D", YEAR + MONTH + DAY)] - #[case("PT1H", H)] - #[case("PT1M", M)] - #[case("PT1S", 1)] - #[case("PT1H1M", H + M)] - #[case("PT1H1S", H + 1)] - #[case("PT1M1S", M + 1)] - #[case("PT1H1M1S", H + M + 1)] - #[case("P1Y1M1DT1H1M1S", YEAR + MONTH + DAY + H + M + 1)] - #[case("PT24H", DAY)] - #[case("PT40000000S", 40000000)] - #[case("PT3600S", H)] - #[case("PT60M", H)] - fn test_duration_8601(#[case] input: &'static str, #[case] seconds: i64) { - let (_, res) = complete_duration(input).expect(input); - assert_eq!(res.num_seconds(), seconds, "parsing {}", input); - } -} diff --git a/rust/cli/src/argparse/command.rs b/rust/cli/src/argparse/command.rs deleted file mode 100644 index 712114f21..000000000 --- a/rust/cli/src/argparse/command.rs +++ /dev/null @@ -1,86 +0,0 @@ -use super::args::*; -use super::{ArgList, Subcommand}; -use nom::{combinator::*, sequence::*, Err, IResult}; - -/// A command is the overall command that the CLI should execute. -/// -/// It consists of some information common to all commands and a `Subcommand` identifying the -/// particular kind of behavior desired. -#[derive(Debug, PartialEq)] -pub(crate) struct Command { - pub(crate) command_name: String, - pub(crate) subcommand: Subcommand, -} - -impl Command { - pub(super) fn parse(input: ArgList) -> IResult { - fn to_command(input: (&str, Subcommand)) -> Result { - // Clean up command name, so `./target/bin/ta` to `ta` etc - let command_name: String = std::path::PathBuf::from(&input.0) - .file_name() - // Convert to string, very unlikely to contain non-UTF8 - .map(|x| x.to_string_lossy().to_string()) - .unwrap_or_else(|| input.0.to_owned()); - - let command = Command { - command_name, - subcommand: input.1, - }; - Ok(command) - } - map_res( - all_consuming(tuple((arg_matching(any), Subcommand::parse))), - to_command, - )(input) - } - - /// Parse a command from the given list of strings. - pub fn from_argv(argv: &[&str]) -> Result { - match Command::parse(argv) { - Ok((&[], cmd)) => Ok(cmd), - Ok((trailing, _)) => Err(crate::Error::for_arguments(format!( - "command line has trailing arguments: {:?}", - trailing - ))), - Err(Err::Incomplete(_)) => unreachable!(), - Err(Err::Error(e)) => Err(crate::Error::for_arguments(format!( - "command line not recognized: {:?}", - e - ))), - Err(Err::Failure(e)) => Err(crate::Error::for_arguments(format!( - "command line not recognized: {:?}", - e - ))), - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - - // NOTE: most testing of specific subcommands is handled in `subcommand.rs`. - - #[test] - fn test_version() { - assert_eq!( - Command::from_argv(argv!["ta", "version"]).unwrap(), - Command { - subcommand: Subcommand::Version, - command_name: s!("ta"), - } - ); - } - - #[test] - fn test_cleaning_command_name() { - assert_eq!( - Command::from_argv(argv!["/tmp/ta", "version"]).unwrap(), - Command { - subcommand: Subcommand::Version, - command_name: s!("ta"), - } - ); - } -} diff --git a/rust/cli/src/argparse/config.rs b/rust/cli/src/argparse/config.rs deleted file mode 100644 index 209000a4e..000000000 --- a/rust/cli/src/argparse/config.rs +++ /dev/null @@ -1,44 +0,0 @@ -use super::args::{any, arg_matching, literal}; -use super::ArgList; -use crate::usage; -use nom::{branch::alt, combinator::*, sequence::*, IResult}; - -#[derive(Debug, PartialEq)] -/// A config operation -pub(crate) enum ConfigOperation { - /// Set a configuration value - Set(String, String), - /// Show configuration path - Path, -} - -impl ConfigOperation { - pub(super) fn parse(input: ArgList) -> IResult { - fn set_to_op(input: (&str, &str, &str)) -> Result { - Ok(ConfigOperation::Set(input.1.to_owned(), input.2.to_owned())) - } - fn path_to_op(_: &str) -> Result { - Ok(ConfigOperation::Path) - } - alt(( - map_res( - tuple(( - arg_matching(literal("set")), - arg_matching(any), - arg_matching(any), - )), - set_to_op, - ), - map_res(arg_matching(literal("path")), path_to_op), - ))(input) - } - - pub(super) fn get_usage(u: &mut usage::Usage) { - u.subcommands.push(usage::Subcommand { - name: "config set", - syntax: "config set ", - summary: "Set a configuration value", - description: "Update Taskchampion configuration file to set key = value", - }); - } -} diff --git a/rust/cli/src/argparse/filter.rs b/rust/cli/src/argparse/filter.rs deleted file mode 100644 index 2895c03b7..000000000 --- a/rust/cli/src/argparse/filter.rs +++ /dev/null @@ -1,400 +0,0 @@ -use super::args::{arg_matching, id_list, literal, minus_tag, plus_tag, status_colon, TaskId}; -use super::ArgList; -use crate::usage; -use anyhow::bail; -use nom::{ - branch::alt, - combinator::*, - multi::{fold_many0, fold_many1}, - IResult, -}; -use taskchampion::{Status, Tag}; - -/// A filter represents a selection of a particular set of tasks. -/// -/// A filter has a "universe" of tasks that might match, and a list of conditions -/// all of which tasks must match. The universe can be a set of task IDs, or just -/// pending tasks, or all tasks. -#[derive(Debug, PartialEq, Default, Clone)] -pub(crate) struct Filter { - /// A set of filter conditions, all of which must match a task in order for that task to be - /// selected. - pub(crate) conditions: Vec, -} - -/// A condition which tasks must match to be accepted by the filter. -#[derive(Debug, PartialEq, Clone)] -pub(crate) enum Condition { - /// Task has the given tag - HasTag(Tag), - - /// Task does not have the given tag - NoTag(Tag), - - /// Task has the given status - Status(Status), - - /// Task has one of the given IDs - IdList(Vec), -} - -impl Condition { - fn parse(input: ArgList) -> IResult { - alt(( - Self::parse_id_list, - Self::parse_plus_tag, - Self::parse_minus_tag, - Self::parse_status, - ))(input) - } - - /// Parse a single condition string - pub(crate) fn parse_str(input: &str) -> anyhow::Result { - let input = &[input]; - Ok(match Condition::parse(input) { - Ok((&[], cond)) => cond, - Ok(_) => unreachable!(), // input only has one element - Err(nom::Err::Incomplete(_)) => unreachable!(), - Err(nom::Err::Error(e)) => bail!("invalid filter condition: {:?}", e), - Err(nom::Err::Failure(e)) => bail!("invalid filter condition: {:?}", e), - }) - } - - fn parse_id_list(input: ArgList) -> IResult { - fn to_condition(input: Vec) -> Result { - Ok(Condition::IdList(input)) - } - map_res(arg_matching(id_list), to_condition)(input) - } - - fn parse_plus_tag(input: ArgList) -> IResult { - fn to_condition(input: Tag) -> Result { - Ok(Condition::HasTag(input)) - } - map_res(arg_matching(plus_tag), to_condition)(input) - } - - fn parse_minus_tag(input: ArgList) -> IResult { - fn to_condition(input: Tag) -> Result { - Ok(Condition::NoTag(input)) - } - map_res(arg_matching(minus_tag), to_condition)(input) - } - - fn parse_status(input: ArgList) -> IResult { - fn to_condition(input: Status) -> Result { - Ok(Condition::Status(input)) - } - map_res(arg_matching(status_colon), to_condition)(input) - } -} - -impl Filter { - /// Parse a filter that can include an empty set of args (meaning - /// all tasks) - pub(super) fn parse0(input: ArgList) -> IResult { - fold_many0( - Condition::parse, - Filter { - ..Default::default() - }, - |acc, arg| acc.with_arg(arg), - )(input) - } - - /// Parse a filter that must have at least one arg, which can be `all` - /// to mean all tasks - pub(super) fn parse1(input: ArgList) -> IResult { - alt(( - Filter::parse_all, - fold_many1( - Condition::parse, - Filter { - ..Default::default() - }, - |acc, arg| acc.with_arg(arg), - ), - ))(input) - } - - fn parse_all(input: ArgList) -> IResult { - fn to_filter(_: &str) -> Result { - Ok(Filter { - ..Default::default() - }) - } - map_res(arg_matching(literal("all")), to_filter)(input) - } - - /// fold multiple filter args into a single Filter instance - fn with_arg(mut self, cond: Condition) -> Filter { - if let Condition::IdList(mut id_list) = cond { - // If there is already an IdList condition, concatenate this one - // to it. Thus multiple IdList command-line args represent an OR - // operation. This assumes that the filter is still being built - // from command-line arguments and thus has at most one IdList - // condition. - if let Some(Condition::IdList(existing)) = self - .conditions - .iter_mut() - .find(|c| matches!(c, Condition::IdList(_))) - { - existing.append(&mut id_list); - } else { - self.conditions.push(Condition::IdList(id_list)); - } - } else { - // all other command-line conditions are AND'd together - self.conditions.push(cond); - } - self - } - - /// combine this filter with another filter in an AND operation - pub(crate) fn intersect(mut self, mut other: Filter) -> Filter { - // simply concatenate the conditions - self.conditions.append(&mut other.conditions); - - self - } - - // usage - - pub(super) fn get_usage(u: &mut usage::Usage) { - u.filters.push(usage::Filter { - syntax: "TASKID[,TASKID,..]", - summary: "Specific tasks", - description: " - Select only specific tasks. Multiple tasks can be specified either separated by - commas or as separate arguments. Each task may be specfied by its working-set - index (a small number) or by its UUID. Partial UUIDs, broken on a hyphen, are - also supported, such as `b5664ef8-423d` or `b5664ef8`.", - }); - u.filters.push(usage::Filter { - syntax: "+TAG", - summary: "Tagged tasks", - description: " - Select tasks with the given tag.", - }); - u.filters.push(usage::Filter { - syntax: "-TAG", - summary: "Un-tagged tasks", - description: " - Select tasks that do not have the given tag.", - }); - u.filters.push(usage::Filter { - syntax: "status:pending, status:completed, status:deleted", - summary: "Task status", - description: " - Select tasks with the given status.", - }); - u.filters.push(usage::Filter { - syntax: "all", - summary: "All tasks", - description: " - When specified alone for task-modification commands, `all` matches all tasks. - For example, `task all done` will mark all tasks as done.", - }); - } -} - -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_empty_parse0() { - let (input, filter) = Filter::parse0(argv![]).unwrap(); - assert_eq!(input.len(), 0); - assert_eq!( - filter, - Filter { - ..Default::default() - } - ); - } - - #[test] - fn test_empty_parse1() { - // parse1 does not allow empty input - assert!(Filter::parse1(argv![]).is_err()); - } - - #[test] - fn test_all_parse0() { - let (input, _) = Filter::parse0(argv!["all"]).unwrap(); - assert_eq!(input.len(), 1); // did not parse "all" - } - - #[test] - fn test_all_parse1() { - let (input, filter) = Filter::parse1(argv!["all"]).unwrap(); - assert_eq!(input.len(), 0); - assert_eq!( - filter, - Filter { - ..Default::default() - } - ); - } - - #[test] - fn test_all_with_other_stuff() { - let (input, filter) = Filter::parse1(argv!["all", "+foo"]).unwrap(); - // filter ends after `all` - assert_eq!(input.len(), 1); - assert_eq!( - filter, - Filter { - ..Default::default() - } - ); - } - - #[test] - fn test_id_list_single() { - let (input, filter) = Filter::parse0(argv!["1"]).unwrap(); - assert_eq!(input.len(), 0); - assert_eq!( - filter, - Filter { - conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(1)])], - } - ); - } - - #[test] - fn test_id_list_commas() { - let (input, filter) = Filter::parse0(argv!["1,2,3"]).unwrap(); - assert_eq!(input.len(), 0); - assert_eq!( - filter, - Filter { - conditions: vec![Condition::IdList(vec![ - TaskId::WorkingSetId(1), - TaskId::WorkingSetId(2), - TaskId::WorkingSetId(3), - ])], - } - ); - } - - #[test] - fn test_id_list_multi_arg() { - let (input, filter) = Filter::parse0(argv!["1,2", "3,4"]).unwrap(); - assert_eq!(input.len(), 0); - assert_eq!( - filter, - Filter { - conditions: vec![Condition::IdList(vec![ - TaskId::WorkingSetId(1), - TaskId::WorkingSetId(2), - TaskId::WorkingSetId(3), - TaskId::WorkingSetId(4), - ])], - } - ); - } - - #[test] - fn test_id_list_uuids() { - let (input, filter) = Filter::parse0(argv!["1,abcd1234"]).unwrap(); - assert_eq!(input.len(), 0); - assert_eq!( - filter, - Filter { - conditions: vec![Condition::IdList(vec![ - TaskId::WorkingSetId(1), - TaskId::PartialUuid(s!("abcd1234")), - ])], - } - ); - } - - #[test] - fn test_tags() { - let (input, filter) = Filter::parse0(argv!["1", "+yes", "-no"]).unwrap(); - assert_eq!(input.len(), 0); - assert_eq!( - filter, - Filter { - conditions: vec![ - Condition::IdList(vec![TaskId::WorkingSetId(1),]), - Condition::HasTag(tag!("yes")), - Condition::NoTag(tag!("no")), - ], - } - ); - } - - #[test] - fn test_status() { - let (input, filter) = Filter::parse0(argv!["status:completed", "status:pending"]).unwrap(); - assert_eq!(input.len(), 0); - assert_eq!( - filter, - Filter { - conditions: vec![ - Condition::Status(Status::Completed), - Condition::Status(Status::Pending), - ], - } - ); - } - - #[test] - fn intersect_idlist_idlist() { - let left = Filter::parse0(argv!["1,2", "+yes"]).unwrap().1; - let right = Filter::parse0(argv!["2,3", "+no"]).unwrap().1; - let both = left.intersect(right); - assert_eq!( - both, - Filter { - conditions: vec![ - // from first filter - Condition::IdList(vec![TaskId::WorkingSetId(1), TaskId::WorkingSetId(2),]), - Condition::HasTag(tag!("yes")), - // from second filter - Condition::IdList(vec![TaskId::WorkingSetId(2), TaskId::WorkingSetId(3)]), - Condition::HasTag(tag!("no")), - ], - } - ); - } - - #[test] - fn intersect_idlist_alltasks() { - let left = Filter::parse0(argv!["1,2", "+yes"]).unwrap().1; - let right = Filter::parse0(argv!["+no"]).unwrap().1; - let both = left.intersect(right); - assert_eq!( - both, - Filter { - conditions: vec![ - // from first filter - Condition::IdList(vec![TaskId::WorkingSetId(1), TaskId::WorkingSetId(2),]), - Condition::HasTag(tag!("yes")), - // from second filter - Condition::HasTag(tag!("no")), - ], - } - ); - } - - #[test] - fn intersect_alltasks_alltasks() { - let left = Filter::parse0(argv!["+yes"]).unwrap().1; - let right = Filter::parse0(argv!["+no"]).unwrap().1; - let both = left.intersect(right); - assert_eq!( - both, - Filter { - conditions: vec![ - Condition::HasTag(tag!("yes")), - Condition::HasTag(tag!("no")), - ], - } - ); - } -} diff --git a/rust/cli/src/argparse/mod.rs b/rust/cli/src/argparse/mod.rs deleted file mode 100644 index 678c9424e..000000000 --- a/rust/cli/src/argparse/mod.rs +++ /dev/null @@ -1,48 +0,0 @@ -// Nested functions that always return Ok(..) are used as callbacks in a context where a Result is -// expected, so the unnecessary_wraps clippy lint is not useful here. - -#![allow(clippy::unnecessary_wraps)] - -/*! - -This module is responsible for parsing command lines (`Arglist`, an alias for `&[&str]`) into `Command` instances. -It removes some redundancy from the command line, for example combining the multiple ways to modify a task into a single `Modification` struct. - -The module is organized as a nom parser over ArgList, and each struct has a `parse` method to parse such a list. - -The exception to this rule is the `args` sub-module, which contains string parsers that are applied to indivdual command-line elements. - -All of the structs produced by this module are fully-owned, data-only structs. -That is, they contain no references, and have no methods to aid in their execution -- that is the `invocation` module's job. - -*/ -mod args; -mod command; -mod config; -mod filter; -mod modification; -mod subcommand; - -pub(crate) use args::TaskId; -pub(crate) use command::Command; -pub(crate) use config::ConfigOperation; -pub(crate) use filter::{Condition, Filter}; -pub(crate) use modification::{DescriptionMod, Modification}; -pub(crate) use subcommand::Subcommand; - -use crate::usage::Usage; -use lazy_static::lazy_static; -use taskchampion::chrono::prelude::*; - -lazy_static! { - // A static value of NOW to make tests easier - pub(crate) static ref NOW: DateTime = Utc::now(); -} - -type ArgList<'a> = &'a [&'a str]; - -pub(crate) fn get_usage(usage: &mut Usage) { - Subcommand::get_usage(usage); - Filter::get_usage(usage); - Modification::get_usage(usage); -} diff --git a/rust/cli/src/argparse/modification.rs b/rust/cli/src/argparse/modification.rs deleted file mode 100644 index 21ddeef8e..000000000 --- a/rust/cli/src/argparse/modification.rs +++ /dev/null @@ -1,342 +0,0 @@ -use super::args::{any, arg_matching, depends_colon, minus_tag, plus_tag, wait_colon, TaskId}; -use super::ArgList; -use crate::usage; -use nom::{branch::alt, combinator::*, multi::fold_many0, IResult}; -use std::collections::HashSet; -use taskchampion::chrono::prelude::*; -use taskchampion::{Status, Tag}; - -#[derive(Debug, PartialEq, Clone)] -pub enum DescriptionMod { - /// do not change the description - None, - - /// Prepend the given value to the description, with a space separator - Prepend(String), - - /// Append the given value to the description, with a space separator - Append(String), - - /// Set the description - Set(String), -} - -impl Default for DescriptionMod { - fn default() -> Self { - Self::None - } -} - -/// A modification represents a change to a task: adding or removing tags, setting the -/// description, and so on. -#[derive(Debug, PartialEq, Clone, Default)] -pub(crate) struct Modification { - /// Change the description - pub(crate) description: DescriptionMod, - - /// Set the status - pub(crate) status: Option, - - /// Set (or, with `Some(None)`, clear) the wait timestamp - pub(crate) wait: Option>>, - - /// Set the "active" state, that is, start (true) or stop (false) the task. - pub(crate) active: Option, - - /// Add tags - pub(crate) add_tags: HashSet, - - /// Remove tags - pub(crate) remove_tags: HashSet, - - /// Add dependencies - pub(crate) add_dependencies: HashSet, - - /// Remove dependencies - pub(crate) remove_dependencies: HashSet, - - /// Add annotation - pub(crate) annotate: Option, -} - -/// A single argument that is part of a modification, used internally to this module -enum ModArg<'a> { - Description(&'a str), - PlusTag(Tag), - MinusTag(Tag), - Wait(Option>), - AddDependencies(Vec), - RemoveDependencies(Vec), -} - -impl Modification { - pub(super) fn parse(input: ArgList) -> IResult { - fn fold(mut acc: Modification, mod_arg: ModArg) -> Modification { - match mod_arg { - ModArg::Description(description) => { - if let DescriptionMod::Set(existing) = acc.description { - acc.description = - DescriptionMod::Set(format!("{} {}", existing, description)); - } else { - acc.description = DescriptionMod::Set(description.to_string()); - } - } - ModArg::PlusTag(tag) => { - acc.add_tags.insert(tag); - } - ModArg::MinusTag(tag) => { - acc.remove_tags.insert(tag); - } - ModArg::Wait(wait) => { - acc.wait = Some(wait); - } - ModArg::AddDependencies(task_ids) => { - for tid in task_ids { - acc.add_dependencies.insert(tid); - } - } - ModArg::RemoveDependencies(task_ids) => { - for tid in task_ids { - acc.remove_dependencies.insert(tid); - } - } - } - acc - } - fold_many0( - alt(( - Self::plus_tag, - Self::minus_tag, - Self::wait, - Self::dependencies, - // this must come last - Self::description, - )), - Modification { - ..Default::default() - }, - fold, - )(input) - } - - fn description(input: ArgList) -> IResult { - fn to_modarg(input: &str) -> Result { - Ok(ModArg::Description(input)) - } - map_res(arg_matching(any), to_modarg)(input) - } - - fn plus_tag(input: ArgList) -> IResult { - fn to_modarg(input: Tag) -> Result, ()> { - Ok(ModArg::PlusTag(input)) - } - map_res(arg_matching(plus_tag), to_modarg)(input) - } - - fn minus_tag(input: ArgList) -> IResult { - fn to_modarg(input: Tag) -> Result, ()> { - Ok(ModArg::MinusTag(input)) - } - map_res(arg_matching(minus_tag), to_modarg)(input) - } - - fn wait(input: ArgList) -> IResult { - fn to_modarg(input: Option>) -> Result, ()> { - Ok(ModArg::Wait(input)) - } - map_res(arg_matching(wait_colon), to_modarg)(input) - } - - fn dependencies(input: ArgList) -> IResult { - fn to_modarg(input: (bool, Vec)) -> Result, ()> { - Ok(if input.0 { - ModArg::AddDependencies(input.1) - } else { - ModArg::RemoveDependencies(input.1) - }) - } - map_res(arg_matching(depends_colon), to_modarg)(input) - } - - pub(super) fn get_usage(u: &mut usage::Usage) { - u.modifications.push(usage::Modification { - syntax: "DESCRIPTION", - summary: "Set description/annotation", - description: " - Set the task description (or the task annotation for `ta annotate`). Multiple - arguments are combined into a single space-separated description. To avoid - surprises from shell quoting, prefer to use a single quoted argument, for example - `ta 19 modify \"return library books\"`", - }); - u.modifications.push(usage::Modification { - syntax: "+TAG", - summary: "Tag task", - description: "Add the given tag to the task.", - }); - u.modifications.push(usage::Modification { - syntax: "-TAG", - summary: "Un-tag task", - description: "Remove the given tag from the task.", - }); - u.modifications.push(usage::Modification { - syntax: "status:{pending,completed,deleted}", - summary: "Set the task's status", - description: "Set the status of the task explicitly.", - }); - u.modifications.push(usage::Modification { - syntax: "wait:", - summary: "Set or unset the task's wait time", - description: " - Set the time before which the task is not actionable and should not be shown in - reports, e.g., `wait:3day` to wait for three days. With `wait:`, the time is - un-set. See the documentation for the timestamp syntax.", - }); - u.modifications.push(usage::Modification { - syntax: "depends:", - summary: "Add task dependencies", - description: " - Add a dependency of this task on the given tasks. The tasks can be specified - in the same syntax as for filters, e.g., `depends:13,94500c95`.", - }); - u.modifications.push(usage::Modification { - syntax: "depends:-", - summary: "Remove task dependencies", - description: " - Remove the dependency of this task on the given tasks.", - }); - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::argparse::NOW; - use pretty_assertions::assert_eq; - use taskchampion::chrono::Duration; - - #[test] - fn test_empty() { - let (input, modification) = Modification::parse(argv![]).unwrap(); - assert_eq!(input.len(), 0); - assert_eq!( - modification, - Modification { - ..Default::default() - } - ); - } - - #[test] - fn test_single_arg_description() { - let (input, modification) = Modification::parse(argv!["newdesc"]).unwrap(); - assert_eq!(input.len(), 0); - assert_eq!( - modification, - Modification { - description: DescriptionMod::Set(s!("newdesc")), - ..Default::default() - } - ); - } - - #[test] - fn test_add_tags() { - let (input, modification) = Modification::parse(argv!["+abc", "+def"]).unwrap(); - assert_eq!(input.len(), 0); - assert_eq!( - modification, - Modification { - add_tags: set![tag!("abc"), tag!("def")], - ..Default::default() - } - ); - } - - #[test] - fn test_set_wait() { - let (input, modification) = Modification::parse(argv!["wait:2d"]).unwrap(); - assert_eq!(input.len(), 0); - assert_eq!( - modification, - Modification { - wait: Some(Some(*NOW + Duration::days(2))), - ..Default::default() - } - ); - } - - #[test] - fn test_add_deps() { - let (input, modification) = Modification::parse(argv!["depends:13,e72b73d1-9e88"]).unwrap(); - assert_eq!(input.len(), 0); - let mut deps = HashSet::new(); - deps.insert(TaskId::WorkingSetId(13)); - deps.insert(TaskId::PartialUuid("e72b73d1-9e88".into())); - assert_eq!( - modification, - Modification { - add_dependencies: deps, - ..Default::default() - } - ); - } - - #[test] - fn test_remove_deps() { - let (input, modification) = - Modification::parse(argv!["depends:-13,e72b73d1-9e88"]).unwrap(); - assert_eq!(input.len(), 0); - let mut deps = HashSet::new(); - deps.insert(TaskId::WorkingSetId(13)); - deps.insert(TaskId::PartialUuid("e72b73d1-9e88".into())); - assert_eq!( - modification, - Modification { - remove_dependencies: deps, - ..Default::default() - } - ); - } - - #[test] - fn test_unset_wait() { - let (input, modification) = Modification::parse(argv!["wait:"]).unwrap(); - assert_eq!(input.len(), 0); - assert_eq!( - modification, - Modification { - wait: Some(None), - ..Default::default() - } - ); - } - - #[test] - fn test_multi_arg_description() { - let (input, modification) = Modification::parse(argv!["new", "desc", "fun"]).unwrap(); - assert_eq!(input.len(), 0); - assert_eq!( - modification, - Modification { - description: DescriptionMod::Set(s!("new desc fun")), - ..Default::default() - } - ); - } - - #[test] - fn test_multi_arg_description_and_tags() { - let (input, modification) = - Modification::parse(argv!["new", "+next", "desc", "-daytime", "fun"]).unwrap(); - assert_eq!(input.len(), 0); - assert_eq!( - modification, - Modification { - description: DescriptionMod::Set(s!("new desc fun")), - add_tags: set![tag!("next")], - remove_tags: set![tag!("daytime")], - ..Default::default() - } - ); - } -} diff --git a/rust/cli/src/argparse/subcommand.rs b/rust/cli/src/argparse/subcommand.rs deleted file mode 100644 index 553350740..000000000 --- a/rust/cli/src/argparse/subcommand.rs +++ /dev/null @@ -1,948 +0,0 @@ -use super::args::*; -use super::{ArgList, ConfigOperation, DescriptionMod, Filter, Modification}; -use crate::usage; -use nom::{branch::alt, combinator::*, sequence::*, IResult}; -use taskchampion::Status; - -// IMPLEMENTATION NOTE: -// -// For each variant of Subcommand, there is a private, empty type of the same name with a `parse` -// method and a `get_usage` method. The parse methods may handle several subcommands, but always -// produce the variant of the same name as the type. -// -// This organization helps to gather the parsing and usage information into -// comprehensible chunks of code, to ensure that everything is documented. - -/// A subcommand is the specific operation that the CLI should execute. -#[derive(Debug, PartialEq)] -pub(crate) enum Subcommand { - /// Display the tool version - Version, - - /// Display the help output - Help { - /// Give the summary help (fitting on a few lines) - summary: bool, - }, - - /// Manipulate configuration - Config { - config_operation: ConfigOperation, - }, - - /// Add a new task - Add { - modification: Modification, - }, - - /// Modify existing tasks - Modify { - filter: Filter, - modification: Modification, - }, - - /// Lists (reports) - Report { - /// The name of the report to show - report_name: String, - - /// Additional filter terms beyond those in the report - filter: Filter, - }, - - /// Per-task information (typically one task) - Info { - filter: Filter, - debug: bool, - }, - - /// Basic operations without args - Gc, - Sync, - ImportTW, - ImportTDB2 { - path: String, - }, - Undo, -} - -impl Subcommand { - pub(super) fn parse(input: ArgList) -> IResult { - all_consuming(alt(( - Version::parse, - Help::parse, - Config::parse, - Add::parse, - Modify::parse, - Info::parse, - Gc::parse, - Sync::parse, - ImportTW::parse, - ImportTDB2::parse, - Undo::parse, - // This must come last since it accepts arbitrary report names - Report::parse, - )))(input) - } - - pub(super) fn get_usage(u: &mut usage::Usage) { - Version::get_usage(u); - Help::get_usage(u); - Config::get_usage(u); - Add::get_usage(u); - Modify::get_usage(u); - Info::get_usage(u); - Gc::get_usage(u); - Sync::get_usage(u); - ImportTW::get_usage(u); - ImportTDB2::get_usage(u); - Undo::get_usage(u); - Report::get_usage(u); - } -} - -struct Version; - -impl Version { - fn parse(input: ArgList) -> IResult { - fn to_subcommand(_: &str) -> Result { - Ok(Subcommand::Version) - } - map_res( - alt(( - arg_matching(literal("version")), - arg_matching(literal("--version")), - )), - to_subcommand, - )(input) - } - - fn get_usage(u: &mut usage::Usage) { - u.subcommands.push(usage::Subcommand { - name: "version", - syntax: "version", - summary: "Show the TaskChampion version", - description: "Show the version of the TaskChampion binary", - }); - } -} - -struct Help; - -impl Help { - fn parse(input: ArgList) -> IResult { - fn to_subcommand(input: &str) -> Result { - Ok(Subcommand::Help { - summary: input == "-h", - }) - } - map_res( - alt(( - arg_matching(literal("help")), - arg_matching(literal("--help")), - arg_matching(literal("-h")), - )), - to_subcommand, - )(input) - } - - fn get_usage(_u: &mut usage::Usage) {} -} - -struct Config; - -impl Config { - fn parse(input: ArgList) -> IResult { - fn to_subcommand(input: (&str, ConfigOperation)) -> Result { - Ok(Subcommand::Config { - config_operation: input.1, - }) - } - map_res( - tuple((arg_matching(literal("config")), ConfigOperation::parse)), - to_subcommand, - )(input) - } - - fn get_usage(u: &mut usage::Usage) { - ConfigOperation::get_usage(u); - } -} - -struct Add; - -impl Add { - fn parse(input: ArgList) -> IResult { - fn to_subcommand(input: (&str, Modification)) -> Result { - Ok(Subcommand::Add { - modification: input.1, - }) - } - map_res( - pair(arg_matching(literal("add")), Modification::parse), - to_subcommand, - )(input) - } - - fn get_usage(u: &mut usage::Usage) { - u.subcommands.push(usage::Subcommand { - name: "add", - syntax: "add [modification]", - summary: "Add a new task", - description: " - Add a new, pending task to the list of tasks. The modification must include a - description.", - }); - } -} - -struct Modify; - -impl Modify { - fn parse(input: ArgList) -> IResult { - fn to_subcommand(input: (Filter, &str, Modification)) -> Result { - let filter = input.0; - let mut modification = input.2; - - match input.1 { - "prepend" => { - if let DescriptionMod::Set(s) = modification.description { - modification.description = DescriptionMod::Prepend(s) - } - } - "append" => { - if let DescriptionMod::Set(s) = modification.description { - modification.description = DescriptionMod::Append(s) - } - } - "start" => modification.active = Some(true), - "stop" => modification.active = Some(false), - "done" => modification.status = Some(Status::Completed), - "delete" => modification.status = Some(Status::Deleted), - "annotate" => { - // what would be parsed as a description is, here, used as the annotation - if let DescriptionMod::Set(s) = modification.description { - modification.description = DescriptionMod::None; - modification.annotate = Some(s); - } - } - _ => {} - } - - Ok(Subcommand::Modify { - filter, - modification, - }) - } - map_res( - tuple(( - Filter::parse1, - alt(( - arg_matching(literal("modify")), - arg_matching(literal("prepend")), - arg_matching(literal("append")), - arg_matching(literal("start")), - arg_matching(literal("stop")), - arg_matching(literal("done")), - arg_matching(literal("delete")), - arg_matching(literal("annotate")), - )), - Modification::parse, - )), - to_subcommand, - )(input) - } - - fn get_usage(u: &mut usage::Usage) { - u.subcommands.push(usage::Subcommand { - name: "modify", - syntax: " modify [modification]", - summary: "Modify tasks", - description: " - Modify all tasks matching the required filter.", - }); - u.subcommands.push(usage::Subcommand { - name: "prepend", - syntax: " prepend [modification]", - summary: "Prepend task description", - description: " - Modify all tasks matching the required filter by inserting the given description before each - task's description.", - }); - u.subcommands.push(usage::Subcommand { - name: "append", - syntax: " append [modification]", - summary: "Append task description", - description: " - Modify all tasks matching the required filter by adding the given description to the end - of each task's description.", - }); - u.subcommands.push(usage::Subcommand { - name: "start", - syntax: " start [modification]", - summary: "Start tasks", - description: " - Start all tasks matching the required filter, additionally applying any given modifications." - }); - u.subcommands.push(usage::Subcommand { - name: "stop", - syntax: " stop [modification]", - summary: "Stop tasks", - description: " - Stop all tasks matching the required filter, additionally applying any given modifications.", - }); - u.subcommands.push(usage::Subcommand { - name: "done", - syntax: " done [modification]", - summary: "Mark tasks as completed", - description: " - Mark all tasks matching the required filter as completed, additionally applying any given - modifications.", - }); - u.subcommands.push(usage::Subcommand { - name: "delete", - syntax: " delete [modification]", - summary: "Mark tasks as deleted", - description: " - Mark all tasks matching the required filter as deleted, additionally applying any given - modifications. Deleted tasks remain until they are expired in a 'ta gc' operation at - least six months after their last modification.", - }); - u.subcommands.push(usage::Subcommand { - name: "annotate", - syntax: " annotate [modification]", - summary: "Annotate a task", - description: " - Add an annotation to all tasks matching the required filter.", - }); - } -} - -struct Report; - -impl Report { - fn parse(input: ArgList) -> IResult { - fn to_subcommand(filter: Filter, report_name: &str) -> Result { - Ok(Subcommand::Report { - filter, - report_name: report_name.to_owned(), - }) - } - // allow the filter expression before or after the report name - alt(( - map_res(pair(arg_matching(report_name), Filter::parse0), |input| { - to_subcommand(input.1, input.0) - }), - map_res(pair(Filter::parse0, arg_matching(report_name)), |input| { - to_subcommand(input.0, input.1) - }), - // default to a "next" report - map_res(Filter::parse0, |input| to_subcommand(input, "next")), - ))(input) - } - - fn get_usage(u: &mut usage::Usage) { - u.subcommands.push(usage::Subcommand { - name: "report", - syntax: "[filter] [report-name] *or* [report-name] [filter]", - summary: "Show a report", - description: " - Show the named report, including only tasks matching the filter", - }); - u.subcommands.push(usage::Subcommand { - name: "next", - syntax: "[filter]", - summary: "Show the 'next' report", - description: " - Show the report named 'next', including only tasks matching the filter", - }); - } -} - -struct Info; - -impl Info { - fn parse(input: ArgList) -> IResult { - fn to_subcommand(input: (Filter, &str)) -> Result { - let debug = input.1 == "debug"; - Ok(Subcommand::Info { - filter: input.0, - debug, - }) - } - map_res( - pair( - Filter::parse1, - alt(( - arg_matching(literal("info")), - arg_matching(literal("debug")), - )), - ), - to_subcommand, - )(input) - } - - fn get_usage(u: &mut usage::Usage) { - u.subcommands.push(usage::Subcommand { - name: "info", - syntax: "[filter] info", - summary: "Show tasks", - description: " Show information about all tasks matching the fiter.", - }); - u.subcommands.push(usage::Subcommand { - name: "debug", - syntax: "[filter] debug", - summary: "Show task debug details", - description: " Show all key/value properties of the tasks matching the fiter.", - }); - } -} - -struct Gc; - -impl Gc { - fn parse(input: ArgList) -> IResult { - fn to_subcommand(_: &str) -> Result { - Ok(Subcommand::Gc) - } - map_res(arg_matching(literal("gc")), to_subcommand)(input) - } - - fn get_usage(u: &mut usage::Usage) { - u.subcommands.push(usage::Subcommand { - name: "gc", - syntax: "gc", - summary: "Perform 'garbage collection'", - description: " - Perform 'garbage collection'. This refreshes the list of pending tasks - and their short id's.", - }); - } -} - -struct Sync; - -impl Sync { - fn parse(input: ArgList) -> IResult { - fn to_subcommand(_: &str) -> Result { - Ok(Subcommand::Sync) - } - map_res(arg_matching(literal("sync")), to_subcommand)(input) - } - - fn get_usage(u: &mut usage::Usage) { - u.subcommands.push(usage::Subcommand { - name: "sync", - syntax: "sync", - summary: "Synchronize this replica", - description: " - Synchronize this replica locally or against a remote server, as configured. - - Synchronization is a critical part of maintaining the task database, and should - be done regularly, even if only locally. It is typically run in a crontask.", - }) - } -} - -struct ImportTW; - -impl ImportTW { - fn parse(input: ArgList) -> IResult { - fn to_subcommand(_: &str) -> Result { - Ok(Subcommand::ImportTW) - } - map_res(arg_matching(literal("import-tw")), to_subcommand)(input) - } - - fn get_usage(u: &mut usage::Usage) { - u.subcommands.push(usage::Subcommand { - name: "import-tw", - syntax: "import-tw", - summary: "Import tasks from TaskWarrior export", - description: " - Import tasks into this replica. - - The tasks must be provided in the TaskWarrior JSON format on stdin. If tasks - in the import already exist, they are 'merged'. - - Because TaskChampion lacks the information about the types of UDAs that is stored - in the TaskWarrior configuration, UDA values are imported as simple strings, in the - format they appear in the JSON export. This may cause undesirable results. - ", - }) - } -} - -struct ImportTDB2; - -impl ImportTDB2 { - fn parse(input: ArgList) -> IResult { - fn to_subcommand(input: (&str, &str)) -> Result { - Ok(Subcommand::ImportTDB2 { - path: input.1.into(), - }) - } - map_res( - pair(arg_matching(literal("import-tdb2")), arg_matching(any)), - to_subcommand, - )(input) - } - - fn get_usage(u: &mut usage::Usage) { - u.subcommands.push(usage::Subcommand { - name: "import-tdb2", - syntax: "import-tdb2 ", - summary: "Import tasks from the TaskWarrior data directory", - description: " - Import tasks into this replica from a TaskWarrior data directory. If tasks in the - import already exist, they are 'merged'. This mode of import supports UDAs better - than the `import` subcommand, but requires access to the \"raw\" TaskWarrior data. - - This command supports task directories written by TaskWarrior-2.6.1 or later. - ", - }) - } -} - -struct Undo; - -impl Undo { - fn parse(input: ArgList) -> IResult { - fn to_subcommand(_: &str) -> Result { - Ok(Subcommand::Undo) - } - map_res(arg_matching(literal("undo")), to_subcommand)(input) - } - - fn get_usage(u: &mut usage::Usage) { - u.subcommands.push(usage::Subcommand { - name: "undo", - syntax: "undo", - summary: "Undo the latest change made on this replica", - description: " - Undo the latest change made on this replica. - - Changes cannot be undone once they have been synchronized.", - }) - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::argparse::Condition; - use pretty_assertions::assert_eq; - - const EMPTY: Vec<&str> = vec![]; - - #[test] - fn test_version() { - assert_eq!( - Subcommand::parse(argv!["version"]).unwrap(), - (&EMPTY[..], Subcommand::Version) - ); - } - - #[test] - fn test_dd_version() { - assert_eq!( - Subcommand::parse(argv!["--version"]).unwrap(), - (&EMPTY[..], Subcommand::Version) - ); - } - - #[test] - fn test_d_h() { - assert_eq!( - Subcommand::parse(argv!["-h"]).unwrap(), - (&EMPTY[..], Subcommand::Help { summary: true }) - ); - } - - #[test] - fn test_help() { - assert_eq!( - Subcommand::parse(argv!["help"]).unwrap(), - (&EMPTY[..], Subcommand::Help { summary: false }) - ); - } - - #[test] - fn test_dd_help() { - assert_eq!( - Subcommand::parse(argv!["--help"]).unwrap(), - (&EMPTY[..], Subcommand::Help { summary: false }) - ); - } - - #[test] - fn test_config_set() { - assert_eq!( - Subcommand::parse(argv!["config", "set", "x", "y"]).unwrap(), - ( - &EMPTY[..], - Subcommand::Config { - config_operation: ConfigOperation::Set("x".to_owned(), "y".to_owned()) - } - ) - ); - } - - #[test] - fn test_add_description() { - let subcommand = Subcommand::Add { - modification: Modification { - description: DescriptionMod::Set(s!("foo")), - ..Default::default() - }, - }; - assert_eq!( - Subcommand::parse(argv!["add", "foo"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_add_description_multi() { - let subcommand = Subcommand::Add { - modification: Modification { - description: DescriptionMod::Set(s!("foo bar")), - ..Default::default() - }, - }; - assert_eq!( - Subcommand::parse(argv!["add", "foo", "bar"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_modify_description_multi() { - let subcommand = Subcommand::Modify { - filter: Filter { - conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])], - }, - modification: Modification { - description: DescriptionMod::Set(s!("foo bar")), - ..Default::default() - }, - }; - assert_eq!( - Subcommand::parse(argv!["123", "modify", "foo", "bar"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_append() { - let subcommand = Subcommand::Modify { - filter: Filter { - conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])], - }, - modification: Modification { - description: DescriptionMod::Append(s!("foo bar")), - ..Default::default() - }, - }; - assert_eq!( - Subcommand::parse(argv!["123", "append", "foo", "bar"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_prepend() { - let subcommand = Subcommand::Modify { - filter: Filter { - conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])], - }, - modification: Modification { - description: DescriptionMod::Prepend(s!("foo bar")), - ..Default::default() - }, - }; - assert_eq!( - Subcommand::parse(argv!["123", "prepend", "foo", "bar"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_done() { - let subcommand = Subcommand::Modify { - filter: Filter { - conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])], - }, - modification: Modification { - status: Some(Status::Completed), - ..Default::default() - }, - }; - assert_eq!( - Subcommand::parse(argv!["123", "done"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_done_modify() { - let subcommand = Subcommand::Modify { - filter: Filter { - conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])], - }, - modification: Modification { - description: DescriptionMod::Set(s!("now-finished")), - status: Some(Status::Completed), - ..Default::default() - }, - }; - assert_eq!( - Subcommand::parse(argv!["123", "done", "now-finished"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_start() { - let subcommand = Subcommand::Modify { - filter: Filter { - conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])], - }, - modification: Modification { - active: Some(true), - ..Default::default() - }, - }; - assert_eq!( - Subcommand::parse(argv!["123", "start"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_start_modify() { - let subcommand = Subcommand::Modify { - filter: Filter { - conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])], - }, - modification: Modification { - active: Some(true), - description: DescriptionMod::Set(s!("mod")), - ..Default::default() - }, - }; - assert_eq!( - Subcommand::parse(argv!["123", "start", "mod"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_stop() { - let subcommand = Subcommand::Modify { - filter: Filter { - conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])], - }, - modification: Modification { - active: Some(false), - ..Default::default() - }, - }; - assert_eq!( - Subcommand::parse(argv!["123", "stop"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_stop_modify() { - let subcommand = Subcommand::Modify { - filter: Filter { - conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])], - }, - modification: Modification { - description: DescriptionMod::Set(s!("mod")), - active: Some(false), - ..Default::default() - }, - }; - assert_eq!( - Subcommand::parse(argv!["123", "stop", "mod"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_delete() { - let subcommand = Subcommand::Modify { - filter: Filter { - conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])], - }, - modification: Modification { - status: Some(Status::Deleted), - ..Default::default() - }, - }; - assert_eq!( - Subcommand::parse(argv!["123", "delete"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_annotate() { - let subcommand = Subcommand::Modify { - filter: Filter { - conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])], - }, - modification: Modification { - annotate: Some("sent invoice".into()), - ..Default::default() - }, - }; - assert_eq!( - Subcommand::parse(argv!["123", "annotate", "sent", "invoice"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_report() { - let subcommand = Subcommand::Report { - filter: Default::default(), - report_name: "myreport".to_owned(), - }; - assert_eq!( - Subcommand::parse(argv!["myreport"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_report_filter_before() { - let subcommand = Subcommand::Report { - filter: Filter { - conditions: vec![Condition::IdList(vec![ - TaskId::WorkingSetId(12), - TaskId::WorkingSetId(13), - ])], - }, - report_name: "foo".to_owned(), - }; - assert_eq!( - Subcommand::parse(argv!["12,13", "foo"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_report_filter_after() { - let subcommand = Subcommand::Report { - filter: Filter { - conditions: vec![Condition::IdList(vec![ - TaskId::WorkingSetId(12), - TaskId::WorkingSetId(13), - ])], - }, - report_name: "foo".to_owned(), - }; - assert_eq!( - Subcommand::parse(argv!["foo", "12,13"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_report_filter_next() { - let subcommand = Subcommand::Report { - filter: Filter { - conditions: vec![Condition::IdList(vec![ - TaskId::WorkingSetId(12), - TaskId::WorkingSetId(13), - ])], - }, - report_name: "next".to_owned(), - }; - assert_eq!( - Subcommand::parse(argv!["12,13"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_report_next() { - let subcommand = Subcommand::Report { - filter: Filter { - ..Default::default() - }, - report_name: "next".to_owned(), - }; - assert_eq!( - Subcommand::parse(argv![]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_info_filter() { - let subcommand = Subcommand::Info { - debug: false, - filter: Filter { - conditions: vec![Condition::IdList(vec![ - TaskId::WorkingSetId(12), - TaskId::WorkingSetId(13), - ])], - }, - }; - assert_eq!( - Subcommand::parse(argv!["12,13", "info"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_debug_filter() { - let subcommand = Subcommand::Info { - debug: true, - filter: Filter { - conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(12)])], - }, - }; - assert_eq!( - Subcommand::parse(argv!["12", "debug"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_gc() { - let subcommand = Subcommand::Gc; - assert_eq!( - Subcommand::parse(argv!["gc"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_gc_extra_args() { - assert!(Subcommand::parse(argv!["gc", "foo"]).is_err()); - } - - #[test] - fn test_sync() { - let subcommand = Subcommand::Sync; - assert_eq!( - Subcommand::parse(argv!["sync"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } - - #[test] - fn test_undo() { - let subcommand = Subcommand::Undo; - assert_eq!( - Subcommand::parse(argv!["undo"]).unwrap(), - (&EMPTY[..], subcommand) - ); - } -} diff --git a/rust/cli/src/bin/ta.rs b/rust/cli/src/bin/ta.rs deleted file mode 100644 index efdee99da..000000000 --- a/rust/cli/src/bin/ta.rs +++ /dev/null @@ -1,11 +0,0 @@ -use std::process::exit; - -pub fn main() { - match taskchampion_cli::main() { - Ok(_) => exit(0), - Err(e) => { - eprintln!("{:?}", e); - exit(e.exit_status()); - } - } -} diff --git a/rust/cli/src/bin/usage-docs.rs b/rust/cli/src/bin/usage-docs.rs deleted file mode 100644 index cf3998b5b..000000000 --- a/rust/cli/src/bin/usage-docs.rs +++ /dev/null @@ -1,53 +0,0 @@ -use mdbook::book::{Book, BookItem}; -use mdbook::errors::Error; -use mdbook::preprocess::{CmdPreprocessor, PreprocessorContext}; -use std::io; -use std::process; -use taskchampion_cli::Usage; - -/// This is a simple mdbook preprocessor designed to substitute information from the usage -/// into the documentation. -fn main() -> anyhow::Result<()> { - // cheap way to detect the "supports" arg - if std::env::args().len() > 1 { - // sure, whatever, we support it all - process::exit(0); - } - - let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?; - - if ctx.mdbook_version != mdbook::MDBOOK_VERSION { - eprintln!( - "Warning: This mdbook preprocessor was built against version {} of mdbook, \ - but we're being called from version {}", - mdbook::MDBOOK_VERSION, - ctx.mdbook_version - ); - } - - let processed_book = process(&ctx, book)?; - serde_json::to_writer(io::stdout(), &processed_book)?; - - Ok(()) -} - -fn process(_ctx: &PreprocessorContext, mut book: Book) -> Result { - let usage = Usage::new(); - - book.for_each_mut(|sect| { - if let BookItem::Chapter(ref mut chapter) = sect { - let new_content = usage.substitute_docs(&chapter.content).unwrap(); - if new_content != chapter.content { - eprintln!( - "Substituting usage in {:?}", - chapter - .source_path - .as_ref() - .unwrap_or_else(|| chapter.path.as_ref().unwrap()) - ); - } - chapter.content = new_content; - } - }); - Ok(book) -} diff --git a/rust/cli/src/errors.rs b/rust/cli/src/errors.rs deleted file mode 100644 index 6da512136..000000000 --- a/rust/cli/src/errors.rs +++ /dev/null @@ -1,60 +0,0 @@ -use taskchampion::Error as TcError; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum Error { - #[error("Command-Line Syntax Error: {0}")] - Arguments(String), - - #[error(transparent)] - TaskChampion(#[from] TcError), - - #[error(transparent)] - Other(#[from] anyhow::Error), -} - -impl Error { - /// Construct a new command-line argument error - pub(crate) fn for_arguments(msg: S) -> Self { - Error::Arguments(msg.to_string()) - } - - /// Determine the exit status for this error, as documented. - pub fn exit_status(&self) -> i32 { - match *self { - Error::Arguments(_) => 3, - _ => 1, - } - } -} - -impl From for Error { - fn from(err: std::io::Error) -> Self { - let err: anyhow::Error = err.into(); - Error::Other(err) - } -} - -#[cfg(test)] -mod test { - use super::*; - use anyhow::anyhow; - use pretty_assertions::assert_eq; - - #[test] - fn test_exit_status() { - let mut err: Error; - - err = anyhow!("uhoh").into(); - assert_eq!(err.exit_status(), 1); - - err = Error::Arguments("uhoh".to_string()); - assert_eq!(err.exit_status(), 3); - - err = std::io::Error::last_os_error().into(); - assert_eq!(err.exit_status(), 1); - - err = TcError::Database("uhoh".to_string()).into(); - assert_eq!(err.exit_status(), 1); - } -} diff --git a/rust/cli/src/invocation/cmd/add.rs b/rust/cli/src/invocation/cmd/add.rs deleted file mode 100644 index 5dfd5c215..000000000 --- a/rust/cli/src/invocation/cmd/add.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::argparse::DescriptionMod; -use crate::invocation::{apply_modification, ResolvedModification}; -use taskchampion::{Replica, Status}; -use termcolor::WriteColor; - -pub(in crate::invocation) fn execute( - w: &mut W, - replica: &mut Replica, - mut modification: ResolvedModification, -) -> Result<(), crate::Error> { - // extract the description from the modification to handle it specially - let description = match modification.0.description { - DescriptionMod::Set(ref s) => s.clone(), - _ => "(no description)".to_owned(), - }; - modification.0.description = DescriptionMod::None; - - let task = replica.new_task(Status::Pending, description).unwrap(); - let mut task = task.into_mut(replica); - apply_modification(&mut task, &modification)?; - writeln!(w, "added task {}", task.get_uuid())?; - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::argparse::Modification; - use crate::invocation::test::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_add() { - let mut w = test_writer(); - let mut replica = test_replica(); - let modification = ResolvedModification(Modification { - description: DescriptionMod::Set(s!("my description")), - ..Default::default() - }); - execute(&mut w, &mut replica, modification).unwrap(); - - // check that the task appeared.. - let working_set = replica.working_set().unwrap(); - let task = replica - .get_task(working_set.by_index(1).unwrap()) - .unwrap() - .unwrap(); - assert_eq!(task.get_description(), "my description"); - assert_eq!(task.get_status(), Status::Pending); - - assert_eq!(w.into_string(), format!("added task {}\n", task.get_uuid())); - } - - #[test] - fn test_add_with_tags() { - let mut w = test_writer(); - let mut replica = test_replica(); - let modification = ResolvedModification(Modification { - description: DescriptionMod::Set(s!("my description")), - add_tags: vec![tag!("tag1")].drain(..).collect(), - ..Default::default() - }); - execute(&mut w, &mut replica, modification).unwrap(); - - // check that the task appeared.. - let working_set = replica.working_set().unwrap(); - let task = replica - .get_task(working_set.by_index(1).unwrap()) - .unwrap() - .unwrap(); - assert_eq!(task.get_description(), "my description"); - assert_eq!(task.get_status(), Status::Pending); - assert!(task.has_tag(&tag!("tag1"))); - - assert_eq!(w.into_string(), format!("added task {}\n", task.get_uuid())); - } -} diff --git a/rust/cli/src/invocation/cmd/completed.data b/rust/cli/src/invocation/cmd/completed.data deleted file mode 100644 index 3a48b9cd1..000000000 --- a/rust/cli/src/invocation/cmd/completed.data +++ /dev/null @@ -1 +0,0 @@ -[description:"&open;TEST&close; foo" entry:"1554074416" modified:"1554074416" priority:"M" status:"completed" uuid:"4578fb67-359b-4483-afe4-fef15925ccd6"] diff --git a/rust/cli/src/invocation/cmd/config.rs b/rust/cli/src/invocation/cmd/config.rs deleted file mode 100644 index ce4d0f27a..000000000 --- a/rust/cli/src/invocation/cmd/config.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::argparse::ConfigOperation; -use crate::settings::Settings; -use termcolor::{ColorSpec, WriteColor}; - -pub(crate) fn execute( - w: &mut W, - config_operation: ConfigOperation, - settings: &Settings, -) -> Result<(), crate::Error> { - match config_operation { - ConfigOperation::Set(key, value) => { - let filename = settings.set(&key, &value)?; - write!(w, "Set configuration value ")?; - w.set_color(ColorSpec::new().set_bold(true))?; - write!(w, "{}", &key)?; - w.set_color(ColorSpec::new().set_bold(false))?; - write!(w, " in ")?; - w.set_color(ColorSpec::new().set_bold(true))?; - writeln!(w, "{:?}.", filename)?; - w.set_color(ColorSpec::new().set_bold(false))?; - } - ConfigOperation::Path => { - if let Some(ref filename) = settings.filename { - writeln!(w, "{}", filename.to_string_lossy())?; - } else { - return Err(anyhow::anyhow!("No configuration filename found").into()); - } - } - } - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::invocation::test::*; - use pretty_assertions::assert_eq; - use std::fs; - use tempfile::TempDir; - - #[test] - fn test_config_set() { - let cfg_dir = TempDir::new().unwrap(); - let cfg_file = cfg_dir.path().join("foo.toml"); - fs::write( - cfg_file.clone(), - "# store data everywhere\ndata_dir = \"/nowhere\"\n", - ) - .unwrap(); - - let settings = Settings::load_from_file(cfg_file.clone(), true).unwrap(); - - let mut w = test_writer(); - - execute( - &mut w, - ConfigOperation::Set("data_dir".to_owned(), "/somewhere".to_owned()), - &settings, - ) - .unwrap(); - assert!(w.into_string().starts_with("Set configuration value ")); - - let updated_toml = fs::read_to_string(cfg_file.clone()).unwrap(); - assert_eq!( - updated_toml, - "# store data everywhere\ndata_dir = \"/somewhere\"\n" - ); - } -} diff --git a/rust/cli/src/invocation/cmd/gc.rs b/rust/cli/src/invocation/cmd/gc.rs deleted file mode 100644 index e9b8a2828..000000000 --- a/rust/cli/src/invocation/cmd/gc.rs +++ /dev/null @@ -1,26 +0,0 @@ -use taskchampion::Replica; -use termcolor::WriteColor; - -pub(crate) fn execute(w: &mut W, replica: &mut Replica) -> Result<(), crate::Error> { - log::debug!("rebuilding working set"); - replica.rebuild_working_set(true)?; - log::debug!("expiring old tasks"); - replica.expire_tasks()?; - writeln!(w, "garbage collected.")?; - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::invocation::test::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_gc() { - let mut w = test_writer(); - let mut replica = test_replica(); - execute(&mut w, &mut replica).unwrap(); - assert_eq!(&w.into_string(), "garbage collected.\n") - } -} diff --git a/rust/cli/src/invocation/cmd/help.rs b/rust/cli/src/invocation/cmd/help.rs deleted file mode 100644 index 2f81a08f8..000000000 --- a/rust/cli/src/invocation/cmd/help.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::usage::Usage; -use termcolor::WriteColor; - -pub(crate) fn execute( - w: &mut W, - command_name: String, - summary: bool, -) -> Result<(), crate::Error> { - let usage = Usage::new(); - usage.write_help(w, command_name.as_ref(), summary)?; - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::invocation::test::*; - - #[test] - fn test_summary() { - let mut w = test_writer(); - execute(&mut w, s!("ta"), true).unwrap(); - } - - #[test] - fn test_long() { - let mut w = test_writer(); - execute(&mut w, s!("ta"), false).unwrap(); - } -} diff --git a/rust/cli/src/invocation/cmd/import_tdb2.rs b/rust/cli/src/invocation/cmd/import_tdb2.rs deleted file mode 100644 index c3eb9ba5f..000000000 --- a/rust/cli/src/invocation/cmd/import_tdb2.rs +++ /dev/null @@ -1,142 +0,0 @@ -use crate::tdb2; -use anyhow::anyhow; -use std::fs; -use std::path::PathBuf; -use taskchampion::{Replica, Uuid}; -use termcolor::{Color, ColorSpec, WriteColor}; - -pub(crate) fn execute( - w: &mut W, - replica: &mut Replica, - path: &str, -) -> Result<(), crate::Error> { - let path: PathBuf = path.into(); - - let mut count = 0; - for file in &["pending.data", "completed.data"] { - let file = path.join(file); - w.set_color(ColorSpec::new().set_bold(true))?; - writeln!(w, "Importing tasks from {:?}.", file)?; - w.reset()?; - - let data = fs::read_to_string(file)?; - let content = - tdb2::File::from_str(&data).map_err(|_| anyhow!("Could not parse TDB2 file format"))?; - count += content.lines.len(); - for line in content.lines { - import_task(w, replica, line)?; - } - } - w.set_color(ColorSpec::new().set_bold(true))?; - writeln!(w, "{} tasks imported.", count)?; - w.reset()?; - - Ok(()) -} - -fn import_task( - w: &mut W, - replica: &mut Replica, - mut line: tdb2::Line, -) -> anyhow::Result<()> { - let mut uuid = None; - for attr in line.attrs.iter() { - if &attr.name == "uuid" { - uuid = Some(Uuid::parse_str(&attr.value)?); - break; - } - } - let uuid = uuid.ok_or_else(|| anyhow!("task has no uuid"))?; - replica.import_task_with_uuid(uuid)?; - - let mut description = None; - for attr in line.attrs.drain(..) { - // oddly, TaskWarrior represents [ and ] with their HTML entity equivalents - let value = attr.value.replace("&open;", "[").replace("&close;", "]"); - match attr.name.as_ref() { - // `uuid` was already handled - "uuid" => {} - - // everything else is inserted directly - _ => { - if attr.name == "description" { - // keep a copy of the description for console output - description = Some(value.clone()); - } - replica.update_task(uuid, attr.name, Some(value))?; - } - } - } - - w.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?; - write!(w, "{}", uuid)?; - w.reset()?; - writeln!( - w, - " {}", - description.unwrap_or_else(|| "(no description)".into()) - )?; - - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::invocation::test::*; - use pretty_assertions::assert_eq; - use std::convert::TryInto; - use taskchampion::chrono::{TimeZone, Utc}; - use taskchampion::Status; - use tempfile::TempDir; - - #[test] - fn test_import() -> anyhow::Result<()> { - let mut w = test_writer(); - let mut replica = test_replica(); - let tmp_dir = TempDir::new()?; - - fs::write( - tmp_dir.path().join("pending.data"), - include_bytes!("pending.data"), - )?; - fs::write( - tmp_dir.path().join("completed.data"), - include_bytes!("completed.data"), - )?; - - execute(&mut w, &mut replica, tmp_dir.path().to_str().unwrap())?; - - let task = replica - .get_task(Uuid::parse_str("f19086c2-1f8d-4a6c-9b8d-f94901fb8e62").unwrap()) - .unwrap() - .unwrap(); - assert_eq!(task.get_description(), "snake 🐍"); - assert_eq!(task.get_status(), Status::Pending); - assert_eq!(task.get_priority(), "M"); - assert_eq!(task.get_wait(), None); - assert_eq!( - task.get_modified(), - Some(Utc.ymd(2022, 1, 8).and_hms(19, 33, 5)) - ); - assert!(task.has_tag(&"reptile".try_into().unwrap())); - assert!(!task.has_tag(&"COMPLETED".try_into().unwrap())); - - let task = replica - .get_task(Uuid::parse_str("4578fb67-359b-4483-afe4-fef15925ccd6").unwrap()) - .unwrap() - .unwrap(); - assert_eq!(task.get_description(), "[TEST] foo"); - assert_eq!(task.get_status(), Status::Completed); - assert_eq!(task.get_priority(), "M".to_string()); - assert_eq!(task.get_wait(), None); - assert_eq!( - task.get_modified(), - Some(Utc.ymd(2019, 3, 31).and_hms(23, 20, 16)) - ); - assert!(!task.has_tag(&"reptile".try_into().unwrap())); - assert!(task.has_tag(&"COMPLETED".try_into().unwrap())); - - Ok(()) - } -} diff --git a/rust/cli/src/invocation/cmd/import_tw.rs b/rust/cli/src/invocation/cmd/import_tw.rs deleted file mode 100644 index aaee30d90..000000000 --- a/rust/cli/src/invocation/cmd/import_tw.rs +++ /dev/null @@ -1,265 +0,0 @@ -use anyhow::{anyhow, bail}; -use serde::{self, Deserialize, Deserializer}; -use serde_json::Value; -use std::collections::HashMap; -use taskchampion::chrono::{DateTime, TimeZone, Utc}; -use taskchampion::{Replica, Uuid}; -use termcolor::{Color, ColorSpec, WriteColor}; - -pub(crate) fn execute(w: &mut W, replica: &mut Replica) -> Result<(), crate::Error> { - w.set_color(ColorSpec::new().set_bold(true))?; - writeln!(w, "Importing tasks from stdin.")?; - w.reset()?; - - let mut tasks: Vec> = - serde_json::from_reader(std::io::stdin()).map_err(|_| anyhow!("Invalid JSON"))?; - - for task_json in tasks.drain(..) { - import_task(w, replica, task_json)?; - } - - w.set_color(ColorSpec::new().set_bold(true))?; - writeln!(w, "{} tasks imported.", tasks.len())?; - w.reset()?; - - Ok(()) -} - -/// Convert the given value to a string, failing on compound types (arrays -/// and objects). -fn stringify(v: Value) -> anyhow::Result { - Ok(match v { - Value::String(s) => s, - Value::Number(n) => n.to_string(), - Value::Bool(true) => "true".to_string(), - Value::Bool(false) => "false".to_string(), - Value::Null => "null".to_string(), - _ => bail!("{:?} cannot be converted to a string", v), - }) -} - -pub fn deserialize_tw_datetime<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - const FORMAT: &str = "%Y%m%dT%H%M%SZ"; - let s = String::deserialize(deserializer)?; - Utc.datetime_from_str(&s, FORMAT) - .map_err(serde::de::Error::custom) -} - -/// Deserialize a string in the TaskWarrior format into a DateTime -#[derive(Deserialize)] -struct TwDateTime(#[serde(deserialize_with = "deserialize_tw_datetime")] DateTime); - -impl TwDateTime { - /// Generate the data-model style UNIX timestamp for this DateTime - fn tc_timestamp(&self) -> String { - self.0.timestamp().to_string() - } -} - -#[derive(Deserialize)] -struct Annotation { - entry: TwDateTime, - description: String, -} - -fn import_task( - w: &mut W, - replica: &mut Replica, - mut task_json: HashMap, -) -> anyhow::Result<()> { - let uuid = task_json - .get("uuid") - .ok_or_else(|| anyhow!("task has no uuid"))?; - let uuid = uuid - .as_str() - .ok_or_else(|| anyhow!("uuid is not a string"))?; - let uuid = Uuid::parse_str(uuid)?; - replica.import_task_with_uuid(uuid)?; - - let mut description = None; - for (k, v) in task_json.drain() { - match k.as_ref() { - // `id` is the working-set ID and is not stored - "id" => {} - - // `urgency` is also calculated and not stored - "urgency" => {} - - // `uuid` was already handled - "uuid" => {} - - // `annotations` is a sub-aray - "annotations" => { - let annotations: Vec = serde_json::from_value(v)?; - for ann in annotations { - let k = format!("annotation_{}", ann.entry.tc_timestamp()); - replica.update_task(uuid, k, Some(ann.description))?; - } - } - - // `depends` is a sub-aray - "depends" => { - let deps: Vec = serde_json::from_value(v)?; - for dep in deps { - let k = format!("dep_{}", dep); - replica.update_task(uuid, k, Some("".to_owned()))?; - } - } - - // `tags` is a sub-aray - "tags" => { - let tags: Vec = serde_json::from_value(v)?; - for tag in tags { - let k = format!("tag_{}", tag); - replica.update_task(uuid, k, Some("".to_owned()))?; - } - } - - // convert all datetimes -> epoch integers - "end" | "entry" | "modified" | "wait" | "due" => { - let v: TwDateTime = serde_json::from_value(v)?; - replica.update_task(uuid, k, Some(v.tc_timestamp()))?; - } - - // everything else is inserted directly - _ => { - let v = stringify(v)?; - if k == "description" { - // keep a copy of the description for console output - description = Some(v.clone()); - } - replica.update_task(uuid, k, Some(v))?; - } - } - } - - w.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?; - write!(w, "{}", uuid)?; - w.reset()?; - writeln!( - w, - " {}", - description.unwrap_or_else(|| "(no description)".into()) - )?; - - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::invocation::test::*; - use pretty_assertions::assert_eq; - use serde_json::json; - use std::convert::TryInto; - use taskchampion::chrono::{TimeZone, Utc}; - use taskchampion::Status; - - #[test] - fn stringify_string() { - assert_eq!(stringify(json!("foo")).unwrap(), "foo".to_string()); - } - - #[test] - fn stringify_number() { - assert_eq!(stringify(json!(2.14)).unwrap(), "2.14".to_string()); - } - - #[test] - fn stringify_bool() { - assert_eq!(stringify(json!(true)).unwrap(), "true".to_string()); - assert_eq!(stringify(json!(false)).unwrap(), "false".to_string()); - } - - #[test] - fn stringify_null() { - assert_eq!(stringify(json!(null)).unwrap(), "null".to_string()); - } - - #[test] - fn stringify_invalid() { - assert!(stringify(json!([1])).is_err()); - assert!(stringify(json!({"a": 1})).is_err()); - } - - #[test] - fn test_import() -> anyhow::Result<()> { - let mut w = test_writer(); - let mut replica = test_replica(); - - let task_json = serde_json::from_value(json!({ - "id": 0, - "description": "repair window", - "end": "20211231T175614Z", // TODO (#327) - "entry": "20211117T022410Z", // TODO (#326) - "modified": "20211231T175614Z", - "priority": "M", - "status": "completed", - "uuid": "fa01e916-1587-4c7d-a646-f7be62be8ee7", - "wait": "20211225T001523Z", - "due": "20211225T040000Z", // TODO (#82) - - // TODO: recurrence (#81) - "imask": 2, - "recur": "monthly", - "rtype": "periodic", - "mask": "+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--", - - // (legacy) UDAs - "githubcreatedon": "20211110T175919Z", - "githubnamespace": "djmitche", - "githubnumber": 228, - - "tags": [ - "house" - ], - "depends": [ // TODO (#84) - "4f71035d-1704-47f0-885c-6f9134bcefb2" - ], - "annotations": [ - { - "entry": "20211223T142031Z", - "description": "ordered from website" - } - ], - "urgency": 4.16849 - }))?; - import_task(&mut w, &mut replica, task_json)?; - - let task = replica - .get_task(Uuid::parse_str("fa01e916-1587-4c7d-a646-f7be62be8ee7").unwrap()) - .unwrap() - .unwrap(); - assert_eq!(task.get_description(), "repair window"); - assert_eq!(task.get_status(), Status::Completed); - assert_eq!(task.get_priority(), "M".to_string()); - assert_eq!( - task.get_wait(), - Some(Utc.ymd(2021, 12, 25).and_hms(00, 15, 23)) - ); - assert_eq!( - task.get_modified(), - Some(Utc.ymd(2021, 12, 31).and_hms(17, 56, 14)) - ); - assert!(task.has_tag(&"house".try_into().unwrap())); - assert!(!task.has_tag(&"PENDING".try_into().unwrap())); - assert_eq!( - task.get_annotations().collect::>(), - vec![taskchampion::Annotation { - entry: Utc.ymd(2021, 12, 23).and_hms(14, 20, 31), - description: "ordered from website".into(), - }] - ); - assert_eq!( - task.get_legacy_uda("githubcreatedon"), - Some("20211110T175919Z") - ); - assert_eq!(task.get_legacy_uda("githubnamespace"), Some("djmitche")); - assert_eq!(task.get_legacy_uda("githubnumber"), Some("228")); - - Ok(()) - } -} diff --git a/rust/cli/src/invocation/cmd/info.rs b/rust/cli/src/invocation/cmd/info.rs deleted file mode 100644 index 2766e6267..000000000 --- a/rust/cli/src/invocation/cmd/info.rs +++ /dev/null @@ -1,117 +0,0 @@ -use crate::argparse::Filter; -use crate::invocation::filtered_tasks; -use crate::table; -use prettytable::{cell, row, Table}; -use taskchampion::{Replica, Status}; -use termcolor::WriteColor; - -pub(crate) fn execute( - w: &mut W, - replica: &mut Replica, - filter: Filter, - debug: bool, -) -> Result<(), crate::Error> { - let working_set = replica.working_set()?; - - for task in filtered_tasks(replica, &filter)? { - let uuid = task.get_uuid(); - - let mut t = Table::new(); - t.set_format(table::format()); - if debug { - t.set_titles(row![b->"key", b->"value"]); - for (k, v) in task.get_taskmap().iter() { - t.add_row(row![k, v]); - } - } else { - t.add_row(row![b->"Uuid", uuid]); - if let Some(i) = working_set.by_uuid(uuid) { - t.add_row(row![b->"Id", i]); - } - t.add_row(row![b->"Description", task.get_description()]); - t.add_row(row![b->"Status", task.get_status()]); - t.add_row(row![b->"Active", task.is_active()]); - let mut tags: Vec<_> = task.get_tags().map(|t| format!("+{}", t)).collect(); - if !tags.is_empty() { - tags.sort(); - t.add_row(row![b->"Tags", tags.join(" ")]); - } - if let Some(wait) = task.get_wait() { - t.add_row(row![b->"Wait", wait]); - } - let mut annotations: Vec<_> = task.get_annotations().collect(); - annotations.sort(); - for ann in annotations { - t.add_row(row![b->"Annotation", format!("{}: {}", ann.entry, ann.description)]); - } - - let mut deps: Vec<_> = task.get_dependencies().collect(); - deps.sort(); - for dep in deps { - let mut descr = None; - if let Some(task) = replica.get_task(dep)? { - if task.get_status() == Status::Pending { - if let Some(i) = working_set.by_uuid(dep) { - descr = Some(format!("{} - {}", i, task.get_description())) - } else { - descr = Some(format!("{} - {}", dep, task.get_description())) - } - } - } - - if let Some(descr) = descr { - t.add_row(row![b->"Depends On", descr]); - } - } - } - t.print(w)?; - } - - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::argparse::{Condition, TaskId}; - use crate::invocation::test::*; - - use taskchampion::Status; - - #[test] - fn test_info() { - let mut w = test_writer(); - let mut replica = test_replica(); - replica.new_task(Status::Pending, s!("my task")).unwrap(); - - let filter = Filter { - ..Default::default() - }; - let debug = false; - execute(&mut w, &mut replica, filter, debug).unwrap(); - assert!(w.into_string().contains("my task")); - } - - #[test] - fn test_deps() { - let mut w = test_writer(); - let mut replica = test_replica(); - let t1 = replica.new_task(Status::Pending, s!("my task")).unwrap(); - let t2 = replica - .new_task(Status::Pending, s!("dunno, depends")) - .unwrap(); - let mut t2 = t2.into_mut(&mut replica); - t2.add_dependency(t1.get_uuid()).unwrap(); - let t2 = t2.into_immut(); - - let filter = Filter { - conditions: vec![Condition::IdList(vec![TaskId::Uuid(t2.get_uuid())])], - }; - let debug = false; - execute(&mut w, &mut replica, filter, debug).unwrap(); - let s = w.into_string(); - // length of whitespace between these two strings is not important - assert!(s.contains("Depends On")); - assert!(s.contains("1 - my task")); - } -} diff --git a/rust/cli/src/invocation/cmd/mod.rs b/rust/cli/src/invocation/cmd/mod.rs deleted file mode 100644 index b5d1a21d6..000000000 --- a/rust/cli/src/invocation/cmd/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Responsible for executing commands as parsed by [`crate::argparse`]. - -pub(crate) mod add; -pub(crate) mod config; -pub(crate) mod gc; -pub(crate) mod help; -pub(crate) mod import_tdb2; -pub(crate) mod import_tw; -pub(crate) mod info; -pub(crate) mod modify; -pub(crate) mod report; -pub(crate) mod sync; -pub(crate) mod undo; -pub(crate) mod version; diff --git a/rust/cli/src/invocation/cmd/modify.rs b/rust/cli/src/invocation/cmd/modify.rs deleted file mode 100644 index cb8eaf5b1..000000000 --- a/rust/cli/src/invocation/cmd/modify.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::argparse::Filter; -use crate::invocation::util::{confirm, summarize_task}; -use crate::invocation::{apply_modification, filtered_tasks, ResolvedModification}; -use crate::settings::Settings; -use taskchampion::Replica; -use termcolor::WriteColor; - -/// confirm modification of more than `modificationt_count_prompt` tasks, defaulting to 3 -fn check_modification( - w: &mut W, - settings: &Settings, - affected_tasks: usize, -) -> Result { - let setting = settings.modification_count_prompt.unwrap_or(3); - if setting == 0 || affected_tasks <= setting as usize { - return Ok(true); - } - - let prompt = format!("Operation will modify {} tasks; continue?", affected_tasks,); - if confirm(&prompt)? { - return Ok(true); - } - - writeln!(w, "Cancelled")?; - - // only show this help if the setting is not set - if settings.modification_count_prompt.is_none() { - writeln!( - w, - "Set the `modification_count_prompt` setting to avoid this prompt:" - )?; - writeln!( - w, - " ta config set modification_count_prompt {}", - affected_tasks + 1 - )?; - writeln!(w, "Set it to 0 to disable the prompt entirely")?; - } - Ok(false) -} - -pub(in crate::invocation) fn execute( - w: &mut W, - replica: &mut Replica, - settings: &Settings, - filter: Filter, - modification: ResolvedModification, -) -> Result<(), crate::Error> { - let tasks = filtered_tasks(replica, &filter)?; - - if !check_modification(w, settings, tasks.size_hint().0)? { - return Ok(()); - } - - for task in tasks { - let mut task = task.into_mut(replica); - - apply_modification(&mut task, &modification)?; - - let task = task.into_immut(); - let summary = summarize_task(replica, &task)?; - writeln!(w, "modified task {}", summary)?; - } - - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::argparse::{DescriptionMod, Modification}; - use crate::invocation::test::test_replica; - use crate::invocation::test::*; - use pretty_assertions::assert_eq; - use taskchampion::Status; - - #[test] - fn test_modify() { - let mut w = test_writer(); - let mut replica = test_replica(); - let settings = Settings::default(); - - let task = replica - .new_task(Status::Pending, s!("old description")) - .unwrap(); - - let filter = Filter { - ..Default::default() - }; - let modification = ResolvedModification(Modification { - description: DescriptionMod::Set(s!("new description")), - ..Default::default() - }); - execute(&mut w, &mut replica, &settings, filter, modification).unwrap(); - - // check that the task appeared.. - let task = replica.get_task(task.get_uuid()).unwrap().unwrap(); - assert_eq!(task.get_description(), "new description"); - assert_eq!(task.get_status(), Status::Pending); - - assert_eq!( - w.into_string(), - format!("modified task 1 - new description\n") - ); - } -} diff --git a/rust/cli/src/invocation/cmd/pending.data b/rust/cli/src/invocation/cmd/pending.data deleted file mode 100644 index 5f5590945..000000000 --- a/rust/cli/src/invocation/cmd/pending.data +++ /dev/null @@ -1 +0,0 @@ -[description:"snake 🐍" entry:"1641670385" modified:"1641670385" priority:"M" status:"pending" tag_reptile:"" uuid:"f19086c2-1f8d-4a6c-9b8d-f94901fb8e62"] diff --git a/rust/cli/src/invocation/cmd/report.rs b/rust/cli/src/invocation/cmd/report.rs deleted file mode 100644 index 9be4030a7..000000000 --- a/rust/cli/src/invocation/cmd/report.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::argparse::Filter; -use crate::invocation::display_report; -use crate::settings::Settings; -use taskchampion::Replica; -use termcolor::WriteColor; - -pub(crate) fn execute( - w: &mut W, - replica: &mut Replica, - settings: &Settings, - report_name: String, - filter: Filter, -) -> Result<(), crate::Error> { - display_report(w, replica, settings, report_name, filter) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::argparse::Filter; - use crate::invocation::test::*; - - use taskchampion::Status; - - #[test] - fn test_report() { - let mut w = test_writer(); - let mut replica = test_replica(); - replica.new_task(Status::Pending, s!("my task")).unwrap(); - - // The function being tested is only one line long, so this is sort of an integration test - // for display_report. - - let settings = Default::default(); - let report_name = "next".to_owned(); - let filter = Filter { - ..Default::default() - }; - - execute(&mut w, &mut replica, &settings, report_name, filter).unwrap(); - assert!(w.into_string().contains("my task")); - } -} diff --git a/rust/cli/src/invocation/cmd/sync.rs b/rust/cli/src/invocation/cmd/sync.rs deleted file mode 100644 index 7a3708421..000000000 --- a/rust/cli/src/invocation/cmd/sync.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::settings::Settings; -use taskchampion::{server::Server, Error as TCError, Replica}; -use termcolor::WriteColor; - -pub(crate) fn execute( - w: &mut W, - replica: &mut Replica, - settings: &Settings, - server: &mut Box, -) -> Result<(), crate::Error> { - match replica.sync(server, settings.avoid_snapshots) { - Ok(()) => { - writeln!(w, "sync complete.")?; - Ok(()) - } - Err(e) => match e.downcast() { - Ok(TCError::OutOfSync) => { - writeln!(w, "This replica cannot be synchronized with the server.")?; - writeln!( - w, - "It may be too old, or some other failure may have occurred." - )?; - writeln!( - w, - "To start fresh, remove the local task database and run `ta sync` again." - )?; - writeln!( - w, - "Note that doing so will lose any un-synchronized local changes." - )?; - Ok(()) - } - Ok(e) => Err(e.into()), - Err(e) => Err(e.into()), - }, - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::invocation::test::*; - use pretty_assertions::assert_eq; - use tempfile::TempDir; - - #[test] - fn test_add() { - let mut w = test_writer(); - let mut replica = test_replica(); - let server_dir = TempDir::new().unwrap(); - let mut server = test_server(&server_dir); - let settings = Settings::default(); - - // Note that the details of the actual sync are tested thoroughly in the taskchampion crate - execute(&mut w, &mut replica, &settings, &mut server).unwrap(); - assert_eq!(&w.into_string(), "sync complete.\n") - } -} diff --git a/rust/cli/src/invocation/cmd/undo.rs b/rust/cli/src/invocation/cmd/undo.rs deleted file mode 100644 index d3f688e7a..000000000 --- a/rust/cli/src/invocation/cmd/undo.rs +++ /dev/null @@ -1,28 +0,0 @@ -use taskchampion::Replica; -use termcolor::WriteColor; - -pub(crate) fn execute(w: &mut W, replica: &mut Replica) -> Result<(), crate::Error> { - if replica.undo()? { - writeln!(w, "Undo successful.")?; - } else { - writeln!(w, "Nothing to undo.")?; - } - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::invocation::test::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_undo() { - let mut w = test_writer(); - let mut replica = test_replica(); - - // Note that the details of the actual undo operation are tested thoroughly in the taskchampion crate - execute(&mut w, &mut replica).unwrap(); - assert_eq!(&w.into_string(), "Nothing to undo.\n") - } -} diff --git a/rust/cli/src/invocation/cmd/version.rs b/rust/cli/src/invocation/cmd/version.rs deleted file mode 100644 index af1f77c56..000000000 --- a/rust/cli/src/invocation/cmd/version.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::built_info; -use termcolor::{ColorSpec, WriteColor}; - -pub(crate) fn execute(w: &mut W) -> Result<(), crate::Error> { - write!(w, "TaskChampion ")?; - w.set_color(ColorSpec::new().set_bold(true))?; - write!(w, "{}", built_info::PKG_VERSION)?; - w.reset()?; - - if let (Some(version), Some(dirty)) = (built_info::GIT_VERSION, built_info::GIT_DIRTY) { - if dirty { - write!(w, " (git version: {} with un-committed changes)", version)?; - } else { - write!(w, " (git version: {})", version)?; - }; - } - writeln!(w)?; - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::invocation::test::*; - - #[test] - fn test_version() { - let mut w = test_writer(); - execute(&mut w).unwrap(); - assert!(w.into_string().starts_with("TaskChampion ")); - } -} diff --git a/rust/cli/src/invocation/filter.rs b/rust/cli/src/invocation/filter.rs deleted file mode 100644 index 6e2378ae4..000000000 --- a/rust/cli/src/invocation/filter.rs +++ /dev/null @@ -1,325 +0,0 @@ -use crate::argparse::{Condition, Filter, TaskId}; -use std::collections::HashSet; -use taskchampion::{Replica, Status, Task, Uuid, WorkingSet}; - -fn match_task(filter: &Filter, task: &Task, uuid: Uuid, working_set: &WorkingSet) -> bool { - for cond in &filter.conditions { - match cond { - Condition::HasTag(ref tag) => { - if !task.has_tag(tag) { - return false; - } - } - Condition::NoTag(ref tag) => { - if task.has_tag(tag) { - return false; - } - } - Condition::Status(status) => { - if task.get_status() != *status { - return false; - } - } - Condition::IdList(ids) => { - let uuid_str = uuid.to_string(); - let mut found = false; - let working_set_id = working_set.by_uuid(uuid); - - for id in ids { - if match id { - TaskId::WorkingSetId(i) => Some(*i) == working_set_id, - TaskId::PartialUuid(partial) => uuid_str.starts_with(partial), - TaskId::Uuid(i) => *i == uuid, - } { - found = true; - break; - } - } - if !found { - return false; - } - } - } - } - true -} - -// the universe of tasks we must consider -enum Universe { - /// Scan all the tasks - AllTasks, - /// Scan the working set (for pending tasks) - WorkingSet, - /// Scan an explicit set of tasks, "Absolute" meaning either full UUID or a working set - /// index - AbsoluteIdList(Vec), -} - -/// Determine the universe for the given filter; avoiding the need to scan all tasks in most cases. -fn universe_for_filter(filter: &Filter) -> Universe { - /// If there is a condition with Status::Pending, return true - fn has_pending_condition(filter: &Filter) -> bool { - filter - .conditions - .iter() - .any(|cond| matches!(cond, Condition::Status(Status::Pending))) - } - - /// If there is a condition with an IdList containing no partial UUIDs, - /// return that. - fn absolute_id_list_condition(filter: &Filter) -> Option> { - filter - .conditions - .iter() - .find(|cond| { - if let Condition::IdList(ids) = cond { - !ids.iter().any(|id| matches!(id, TaskId::PartialUuid(_))) - } else { - false - } - }) - .map(|cond| { - if let Condition::IdList(ids) = cond { - ids.to_vec() - } else { - unreachable!() // any condition found above must be an IdList(_) - } - }) - } - - if let Some(ids) = absolute_id_list_condition(filter) { - Universe::AbsoluteIdList(ids) - } else if has_pending_condition(filter) { - Universe::WorkingSet - } else { - Universe::AllTasks - } -} - -/// Return the tasks matching the given filter. This will return each matching -/// task once, even if the user specified the same task multiple times on the -/// command line. -pub(super) fn filtered_tasks( - replica: &mut Replica, - filter: &Filter, -) -> anyhow::Result> { - let mut res = vec![]; - - log::debug!("Applying filter {:?}", filter); - - let working_set = replica.working_set()?; - - // We will enumerate the universe of tasks for this filter, checking - // each resulting task with match_task - match universe_for_filter(filter) { - // A list of IDs, but some are partial so we need to iterate over - // all tasks and pattern-match their Uuids - Universe::AbsoluteIdList(ref ids) => { - log::debug!("Scanning only the tasks specified in the filter"); - // this is the only case where we might accidentally return the same task - // several times, so we must track the seen tasks. - let mut seen = HashSet::new(); - for id in ids { - let task = match id { - TaskId::WorkingSetId(id) => working_set - .by_index(*id) - .map(|uuid| replica.get_task(uuid)) - .transpose()? - .flatten(), - TaskId::PartialUuid(_) => unreachable!(), // not present in absolute id list - TaskId::Uuid(id) => replica.get_task(*id)?, - }; - - if let Some(task) = task { - // if we have already seen this task, skip ahead.. - let uuid = task.get_uuid(); - if seen.contains(&uuid) { - continue; - } - seen.insert(uuid); - - if match_task(filter, &task, uuid, &working_set) { - res.push(task); - } - } - } - } - - // All tasks -- iterate over the full set - Universe::AllTasks => { - log::debug!("Scanning all tasks in the task database"); - for (uuid, task) in replica.all_tasks()?.drain() { - if match_task(filter, &task, uuid, &working_set) { - res.push(task); - } - } - } - Universe::WorkingSet => { - log::debug!("Scanning only the working set (pending tasks)"); - for (_, uuid) in working_set.iter() { - if let Some(task) = replica.get_task(uuid)? { - if match_task(filter, &task, uuid, &working_set) { - res.push(task); - } - } - } - } - } - Ok(res.into_iter()) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::invocation::test::*; - use pretty_assertions::assert_eq; - use taskchampion::Status; - - #[test] - fn exact_ids() { - let mut replica = test_replica(); - - let t1 = replica.new_task(Status::Pending, s!("A")).unwrap(); - let t2 = replica.new_task(Status::Completed, s!("B")).unwrap(); - let _t = replica.new_task(Status::Pending, s!("C")).unwrap(); - replica.rebuild_working_set(true).unwrap(); - - let t1uuid = t1.get_uuid(); - - let filter = Filter { - conditions: vec![Condition::IdList(vec![ - TaskId::Uuid(t1uuid), // A - TaskId::WorkingSetId(1), // A (again, dups filtered) - TaskId::Uuid(t2.get_uuid()), // B - ])], - }; - let mut filtered: Vec<_> = filtered_tasks(&mut replica, &filter) - .unwrap() - .map(|t| t.get_description().to_owned()) - .collect(); - filtered.sort(); - assert_eq!(vec![s!("A"), s!("B")], filtered); - } - - #[test] - fn partial_ids() { - let mut replica = test_replica(); - - let t1 = replica.new_task(Status::Pending, s!("A")).unwrap(); - let t2 = replica.new_task(Status::Completed, s!("B")).unwrap(); - let _t = replica.new_task(Status::Pending, s!("C")).unwrap(); - replica.rebuild_working_set(true).unwrap(); - - let t1uuid = t1.get_uuid(); - let t2uuid = t2.get_uuid().to_string(); - let t2partial = t2uuid[..13].to_owned(); - - let filter = Filter { - conditions: vec![Condition::IdList(vec![ - TaskId::Uuid(t1uuid), // A - TaskId::WorkingSetId(1), // A (again, dups filtered) - TaskId::PartialUuid(t2partial), // B - ])], - }; - let mut filtered: Vec<_> = filtered_tasks(&mut replica, &filter) - .unwrap() - .map(|t| t.get_description().to_owned()) - .collect(); - filtered.sort(); - assert_eq!(vec![s!("A"), s!("B")], filtered); - } - - #[test] - fn all_tasks() { - let mut replica = test_replica(); - - replica.new_task(Status::Pending, s!("A")).unwrap(); - replica.new_task(Status::Completed, s!("B")).unwrap(); - replica.new_task(Status::Deleted, s!("C")).unwrap(); - replica.rebuild_working_set(true).unwrap(); - - let filter = Filter { conditions: vec![] }; - let mut filtered: Vec<_> = filtered_tasks(&mut replica, &filter) - .unwrap() - .map(|t| t.get_description().to_owned()) - .collect(); - filtered.sort(); - assert_eq!(vec![s!("A"), s!("B"), s!("C")], filtered); - } - - #[test] - fn tag_filtering() -> anyhow::Result<()> { - let mut replica = test_replica(); - let yes = tag!("yes"); - let no = tag!("no"); - - let mut t1 = replica - .new_task(Status::Pending, s!("A"))? - .into_mut(&mut replica); - t1.add_tag(&yes)?; - let mut t2 = replica - .new_task(Status::Pending, s!("B"))? - .into_mut(&mut replica); - t2.add_tag(&yes)?; - t2.add_tag(&no)?; - let mut t3 = replica - .new_task(Status::Pending, s!("C"))? - .into_mut(&mut replica); - t3.add_tag(&no)?; - let _t4 = replica.new_task(Status::Pending, s!("D"))?; - - // look for just "yes" (A and B) - let filter = Filter { - conditions: vec![Condition::HasTag(tag!("yes"))], - }; - let mut filtered: Vec<_> = filtered_tasks(&mut replica, &filter)? - .map(|t| t.get_description().to_owned()) - .collect(); - filtered.sort(); - assert_eq!(vec![s!("A"), s!("B")], filtered); - - // look for tags without "no" (A, D) - let filter = Filter { - conditions: vec![Condition::NoTag(tag!("no"))], - }; - let mut filtered: Vec<_> = filtered_tasks(&mut replica, &filter)? - .map(|t| t.get_description().to_owned()) - .collect(); - filtered.sort(); - assert_eq!(vec![s!("A"), s!("D")], filtered); - - // look for tags with "yes" and "no" (B) - let filter = Filter { - conditions: vec![ - Condition::HasTag(tag!("yes")), - Condition::HasTag(tag!("no")), - ], - }; - let filtered: Vec<_> = filtered_tasks(&mut replica, &filter)? - .map(|t| t.get_description().to_owned()) - .collect(); - assert_eq!(vec![s!("B")], filtered); - - Ok(()) - } - - #[test] - fn pending_tasks() { - let mut replica = test_replica(); - - replica.new_task(Status::Pending, s!("A")).unwrap(); - replica.new_task(Status::Completed, s!("B")).unwrap(); - replica.new_task(Status::Deleted, s!("C")).unwrap(); - replica.rebuild_working_set(true).unwrap(); - - let filter = Filter { - conditions: vec![Condition::Status(Status::Pending)], - }; - let mut filtered: Vec<_> = filtered_tasks(&mut replica, &filter) - .unwrap() - .map(|t| t.get_description().to_owned()) - .collect(); - filtered.sort(); - assert_eq!(vec![s!("A")], filtered); - } -} diff --git a/rust/cli/src/invocation/mod.rs b/rust/cli/src/invocation/mod.rs deleted file mode 100644 index 0b75799f4..000000000 --- a/rust/cli/src/invocation/mod.rs +++ /dev/null @@ -1,179 +0,0 @@ -//! The invocation module handles invoking the commands parsed by the argparse module. - -use crate::argparse::{Command, Subcommand}; -use crate::settings::Settings; -use taskchampion::{Replica, Server, ServerConfig, StorageConfig, Uuid}; -use termcolor::{ColorChoice, StandardStream}; - -mod cmd; -mod filter; -mod modify; -mod report; -mod util; - -#[cfg(test)] -mod test; - -use filter::filtered_tasks; -use modify::{apply_modification, resolve_modification, ResolvedModification}; -use report::display_report; - -/// Invoke the given Command in the context of the given settings -#[allow(clippy::needless_return)] -pub(crate) fn invoke(command: Command, settings: Settings) -> Result<(), crate::Error> { - log::debug!("command: {:?}", command); - log::debug!("settings: {:?}", settings); - - let mut w = get_writer(); - - // This function examines the command and breaks out the necessary bits to call one of the - // `execute` functions in a submodule of `cmd`. - - // match the subcommands that do not require a replica first, before - // getting the replica - match command { - Command { - subcommand: Subcommand::Help { summary }, - command_name, - } => return cmd::help::execute(&mut w, command_name, summary), - Command { - subcommand: Subcommand::Config { config_operation }, - .. - } => return cmd::config::execute(&mut w, config_operation, &settings), - Command { - subcommand: Subcommand::Version, - .. - } => return cmd::version::execute(&mut w), - _ => {} - }; - - let mut replica = get_replica(&settings)?; - match command { - Command { - subcommand: Subcommand::Add { modification }, - .. - } => { - let modification = resolve_modification(modification, &mut replica)?; - return cmd::add::execute(&mut w, &mut replica, modification); - } - - Command { - subcommand: - Subcommand::Modify { - filter, - modification, - }, - .. - } => { - let modification = resolve_modification(modification, &mut replica)?; - return cmd::modify::execute(&mut w, &mut replica, &settings, filter, modification); - } - - Command { - subcommand: - Subcommand::Report { - report_name, - filter, - }, - .. - } => return cmd::report::execute(&mut w, &mut replica, &settings, report_name, filter), - - Command { - subcommand: Subcommand::Info { filter, debug }, - .. - } => return cmd::info::execute(&mut w, &mut replica, filter, debug), - - Command { - subcommand: Subcommand::Gc, - .. - } => return cmd::gc::execute(&mut w, &mut replica), - - Command { - subcommand: Subcommand::Sync, - .. - } => { - let mut server = get_server(&settings)?; - return cmd::sync::execute(&mut w, &mut replica, &settings, &mut server); - } - - Command { - subcommand: Subcommand::ImportTW, - .. - } => { - return cmd::import_tw::execute(&mut w, &mut replica); - } - - Command { - subcommand: Subcommand::ImportTDB2 { path }, - .. - } => { - return cmd::import_tdb2::execute(&mut w, &mut replica, path.as_ref()); - } - - Command { - subcommand: Subcommand::Undo, - .. - } => { - return cmd::undo::execute(&mut w, &mut replica); - } - - // handled in the first match, but here to ensure this match is exhaustive - Command { - subcommand: Subcommand::Help { .. }, - .. - } => unreachable!(), - Command { - subcommand: Subcommand::Config { .. }, - .. - } => unreachable!(), - Command { - subcommand: Subcommand::Version, - .. - } => unreachable!(), - }; -} - -// utilities for invoke - -/// Get the replica for this invocation -fn get_replica(settings: &Settings) -> anyhow::Result { - let taskdb_dir = settings.data_dir.clone(); - log::debug!("Replica data_dir: {:?}", taskdb_dir); - let storage_config = StorageConfig::OnDisk { taskdb_dir }; - Ok(Replica::new(storage_config.into_storage()?)) -} - -/// Get the server for this invocation -fn get_server(settings: &Settings) -> anyhow::Result> { - // if server_client_key and server_origin are both set, use - // the remote server - let config = if let (Some(client_key), Some(origin), Some(encryption_secret)) = ( - settings.server_client_key.as_ref(), - settings.server_origin.as_ref(), - settings.encryption_secret.as_ref(), - ) { - let client_key = Uuid::parse_str(client_key)?; - - log::debug!("Using sync-server with origin {}", origin); - log::debug!("Sync client ID: {}", client_key); - ServerConfig::Remote { - origin: origin.clone(), - client_key, - encryption_secret: encryption_secret.as_bytes().to_vec(), - } - } else { - let server_dir = settings.server_dir.clone(); - log::debug!("Using local sync-server at `{:?}`", server_dir); - ServerConfig::Local { server_dir } - }; - config.into_server() -} - -/// Get a WriteColor implementation based on whether the output is a tty. -fn get_writer() -> StandardStream { - StandardStream::stdout(if atty::is(atty::Stream::Stdout) { - ColorChoice::Auto - } else { - ColorChoice::Never - }) -} diff --git a/rust/cli/src/invocation/modify.rs b/rust/cli/src/invocation/modify.rs deleted file mode 100644 index 934ab1bc2..000000000 --- a/rust/cli/src/invocation/modify.rs +++ /dev/null @@ -1,247 +0,0 @@ -use crate::argparse::{DescriptionMod, Modification, TaskId}; -use std::collections::HashSet; -use taskchampion::chrono::Utc; -use taskchampion::{Annotation, Replica, TaskMut}; - -/// A wrapper for Modification, promising that all TaskId instances are of variant TaskId::Uuid. -pub(super) struct ResolvedModification(pub(super) Modification); - -/// Resolve a Modification to a ResolvedModification, based on access to a Replica. -/// -/// This is not automatically done in `apply_modification` because, by that time, the TaskMut being -/// modified has an exclusive reference to the Replica, so it is impossible to search for matching -/// tasks. -pub(super) fn resolve_modification( - unres: Modification, - replica: &mut Replica, -) -> anyhow::Result { - Ok(ResolvedModification(Modification { - description: unres.description, - status: unres.status, - wait: unres.wait, - active: unres.active, - add_tags: unres.add_tags, - remove_tags: unres.remove_tags, - add_dependencies: resolve_task_ids(replica, unres.add_dependencies)?, - remove_dependencies: resolve_task_ids(replica, unres.remove_dependencies)?, - annotate: unres.annotate, - })) -} - -/// Convert a set of arbitrary TaskId's into TaskIds containing only TaskId::Uuid. -fn resolve_task_ids( - replica: &mut Replica, - task_ids: HashSet, -) -> anyhow::Result> { - // already all UUIDs (or empty)? - if task_ids.iter().all(|tid| matches!(tid, TaskId::Uuid(_))) { - return Ok(task_ids); - } - - let mut result = HashSet::new(); - let mut working_set = None; - let mut all_tasks = None; - for tid in task_ids { - match tid { - TaskId::WorkingSetId(i) => { - let ws = match working_set { - Some(ref ws) => ws, - None => { - working_set = Some(replica.working_set()?); - working_set.as_ref().unwrap() - } - }; - if let Some(u) = ws.by_index(i) { - result.insert(TaskId::Uuid(u)); - } - } - TaskId::PartialUuid(partial) => { - let ts = match all_tasks { - Some(ref ts) => ts, - None => { - all_tasks = Some( - replica - .all_task_uuids()? - .drain(..) - .map(|u| (u, u.to_string())) - .collect::>(), - ); - all_tasks.as_ref().unwrap() - } - }; - for (u, ustr) in ts { - if ustr.starts_with(&partial) { - result.insert(TaskId::Uuid(*u)); - } - } - } - TaskId::Uuid(u) => { - result.insert(TaskId::Uuid(u)); - } - } - } - - Ok(result) -} - -/// Apply the given modification -pub(super) fn apply_modification( - task: &mut TaskMut, - modification: &ResolvedModification, -) -> anyhow::Result<()> { - // unwrap the "Resolved" promise - let modification = &modification.0; - - match modification.description { - DescriptionMod::Set(ref description) => task.set_description(description.clone())?, - DescriptionMod::Prepend(ref description) => { - task.set_description(format!("{} {}", description, task.get_description()))? - } - DescriptionMod::Append(ref description) => { - task.set_description(format!("{} {}", task.get_description(), description))? - } - DescriptionMod::None => {} - } - - if let Some(ref status) = modification.status { - task.set_status(status.clone())?; - } - - if let Some(true) = modification.active { - task.start()?; - } - - if let Some(false) = modification.active { - task.stop()?; - } - - for tag in modification.add_tags.iter() { - task.add_tag(tag)?; - } - - for tag in modification.remove_tags.iter() { - task.remove_tag(tag)?; - } - - if let Some(wait) = modification.wait { - task.set_wait(wait)?; - } - - if let Some(ref ann) = modification.annotate { - task.add_annotation(Annotation { - entry: Utc::now(), - description: ann.into(), - })?; - } - - for tid in &modification.add_dependencies { - if let TaskId::Uuid(u) = tid { - task.add_dependency(*u)?; - } else { - // this Modification is resolved, so all TaskIds should - // be the Uuid variant. - unreachable!(); - } - } - - for tid in &modification.remove_dependencies { - if let TaskId::Uuid(u) = tid { - task.remove_dependency(*u)?; - } else { - // this Modification is resolved, so all TaskIds should - // be the Uuid variant. - unreachable!(); - } - } - - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::invocation::test::*; - use pretty_assertions::assert_eq; - use taskchampion::{Status, Uuid}; - - #[test] - fn test_resolve_modifications() { - let mut replica = test_replica(); - let u1 = Uuid::new_v4(); - let t1 = replica.new_task(Status::Pending, "a task".into()).unwrap(); - replica.rebuild_working_set(true).unwrap(); - - let modi = Modification { - add_dependencies: set![TaskId::Uuid(u1), TaskId::WorkingSetId(1)], - ..Default::default() - }; - - let res = resolve_modification(modi, &mut replica).unwrap(); - - assert_eq!( - res.0.add_dependencies, - set![TaskId::Uuid(u1), TaskId::Uuid(t1.get_uuid())], - ); - } - - #[test] - fn test_resolve_task_ids_empty() { - let mut replica = test_replica(); - - assert_eq!( - resolve_task_ids(&mut replica, HashSet::new()).unwrap(), - HashSet::new() - ); - } - - #[test] - fn test_resolve_task_ids_all_uuids() { - let mut replica = test_replica(); - let uuid = Uuid::new_v4(); - let tids = set![TaskId::Uuid(uuid)]; - assert_eq!(resolve_task_ids(&mut replica, tids.clone()).unwrap(), tids); - } - - #[test] - fn test_resolve_task_ids_working_set_not_found() { - let mut replica = test_replica(); - let tids = set![TaskId::WorkingSetId(13)]; - assert_eq!( - resolve_task_ids(&mut replica, tids.clone()).unwrap(), - HashSet::new() - ); - } - - #[test] - fn test_resolve_task_ids_working_set() { - let mut replica = test_replica(); - let t1 = replica.new_task(Status::Pending, "a task".into()).unwrap(); - let t2 = replica - .new_task(Status::Pending, "another task".into()) - .unwrap(); - replica.rebuild_working_set(true).unwrap(); - let tids = set![TaskId::WorkingSetId(1), TaskId::WorkingSetId(2)]; - let resolved = set![TaskId::Uuid(t1.get_uuid()), TaskId::Uuid(t2.get_uuid())]; - assert_eq!(resolve_task_ids(&mut replica, tids).unwrap(), resolved); - } - - #[test] - fn test_resolve_task_ids_partial_not_found() { - let mut replica = test_replica(); - let tids = set![TaskId::PartialUuid("abcd".into())]; - assert_eq!( - resolve_task_ids(&mut replica, tids.clone()).unwrap(), - HashSet::new() - ); - } - - #[test] - fn test_resolve_task_ids_partial() { - let mut replica = test_replica(); - let t1 = replica.new_task(Status::Pending, "a task".into()).unwrap(); - let uuid_str = t1.get_uuid().to_string(); - let tids = set![TaskId::PartialUuid(uuid_str[..8].into())]; - let resolved = set![TaskId::Uuid(t1.get_uuid())]; - assert_eq!(resolve_task_ids(&mut replica, tids).unwrap(), resolved); - } -} diff --git a/rust/cli/src/invocation/report.rs b/rust/cli/src/invocation/report.rs deleted file mode 100644 index 81b4c8622..000000000 --- a/rust/cli/src/invocation/report.rs +++ /dev/null @@ -1,417 +0,0 @@ -use crate::argparse::Filter; -use crate::invocation::filtered_tasks; -use crate::settings::{Column, Property, Report, Settings, SortBy}; -use crate::table; -use anyhow::anyhow; -use prettytable::{Row, Table}; -use std::cmp::Ordering; -use taskchampion::{Replica, Task, WorkingSet}; -use termcolor::WriteColor; - -/// Sort tasks for the given report. -fn sort_tasks(tasks: &mut Vec, report: &Report, working_set: &WorkingSet) { - tasks.sort_by(|a, b| { - for s in &report.sort { - let ord = match s.sort_by { - SortBy::Id => { - let a_uuid = a.get_uuid(); - let b_uuid = b.get_uuid(); - let a_id = working_set.by_uuid(a_uuid); - let b_id = working_set.by_uuid(b_uuid); - match (a_id, b_id) { - (Some(a_id), Some(b_id)) => a_id.cmp(&b_id), - (Some(_), None) => Ordering::Less, - (None, Some(_)) => Ordering::Greater, - (None, None) => a_uuid.cmp(&b_uuid), - } - } - SortBy::Uuid => a.get_uuid().cmp(&b.get_uuid()), - SortBy::Description => a.get_description().cmp(b.get_description()), - SortBy::Wait => a.get_wait().cmp(&b.get_wait()), - }; - // If this sort property is equal, go on to the next.. - if ord == Ordering::Equal { - continue; - } - // Reverse order if not ascending - if s.ascending { - return ord; - } else { - return ord.reverse(); - } - } - Ordering::Equal - }); -} - -/// Generate the string representation for the given task and column. -fn task_column(task: &Task, column: &Column, working_set: &WorkingSet) -> String { - match column.property { - Property::Id => { - let uuid = task.get_uuid(); - let mut id = uuid.to_string(); - if let Some(i) = working_set.by_uuid(uuid) { - id = i.to_string(); - } - id - } - Property::Uuid => { - let uuid = task.get_uuid(); - uuid.to_string() - } - Property::Active => match task.is_active() { - true => "*".to_owned(), - false => "".to_owned(), - }, - Property::Description => task.get_description().to_owned(), - Property::Tags => { - let mut tags = task - .get_tags() - .map(|t| format!("+{}", t)) - .collect::>(); - tags.sort(); - tags.join(" ") - } - Property::Wait => { - if task.is_waiting() { - task.get_wait().unwrap().format("%Y-%m-%d").to_string() - } else { - "".to_owned() - } - } - } -} - -pub(super) fn display_report( - w: &mut W, - replica: &mut Replica, - settings: &Settings, - report_name: String, - filter: Filter, -) -> Result<(), crate::Error> { - let mut t = Table::new(); - let working_set = replica.working_set()?; - - // Get the report from settings - let mut report = settings - .reports - .get(&report_name) - .ok_or_else(|| anyhow!("report `{}` not defined", report_name))? - .clone(); - - // include any user-supplied filter conditions - report.filter = report.filter.intersect(filter); - - // Get the tasks from the filter - let mut tasks: Vec<_> = filtered_tasks(replica, &report.filter)?.collect(); - - // ..sort them as desired - sort_tasks(&mut tasks, &report, &working_set); - - // ..set up the column titles - t.set_format(table::format()); - t.set_titles(report.columns.iter().map(|col| col.label.clone()).into()); - - // ..insert the data - for task in &tasks { - let row: Row = report - .columns - .iter() - .map(|col| task_column(task, col, &working_set)) - .collect::(); - t.add_row(row); - } - - // ..and display it - t.print(w)?; - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::invocation::test::*; - use crate::settings::Sort; - use pretty_assertions::assert_eq; - use std::convert::TryInto; - use taskchampion::chrono::{prelude::*, Duration}; - use taskchampion::{Status, Uuid}; - - fn create_tasks(replica: &mut Replica) -> [Uuid; 3] { - let t1 = replica.new_task(Status::Pending, s!("A")).unwrap(); - let t2 = replica.new_task(Status::Pending, s!("B")).unwrap(); - let t3 = replica.new_task(Status::Pending, s!("C")).unwrap(); - - // t2 is comleted and not in the working set - let mut t2 = t2.into_mut(replica); - t2.set_status(Status::Completed).unwrap(); - let t2 = t2.into_immut(); - - replica.rebuild_working_set(true).unwrap(); - - [t1.get_uuid(), t2.get_uuid(), t3.get_uuid()] - } - - #[test] - fn sorting_by_descr() { - let mut replica = test_replica(); - create_tasks(&mut replica); - let working_set = replica.working_set().unwrap(); - let mut report = Report { - sort: vec![Sort { - ascending: true, - sort_by: SortBy::Description, - }], - ..Default::default() - }; - - // ascending - let mut tasks: Vec<_> = replica.all_tasks().unwrap().values().cloned().collect(); - sort_tasks(&mut tasks, &report, &working_set); - let descriptions: Vec<_> = tasks.iter().map(|t| t.get_description()).collect(); - assert_eq!(descriptions, vec!["A", "B", "C"]); - - // ascending - report.sort[0].ascending = false; - let mut tasks: Vec<_> = replica.all_tasks().unwrap().values().cloned().collect(); - sort_tasks(&mut tasks, &report, &working_set); - let descriptions: Vec<_> = tasks.iter().map(|t| t.get_description()).collect(); - assert_eq!(descriptions, vec!["C", "B", "A"]); - } - - #[test] - fn sorting_by_id() { - let mut replica = test_replica(); - create_tasks(&mut replica); - let working_set = replica.working_set().unwrap(); - let mut report = Report { - sort: vec![Sort { - ascending: true, - sort_by: SortBy::Id, - }], - ..Default::default() - }; - - // ascending - let mut tasks: Vec<_> = replica.all_tasks().unwrap().values().cloned().collect(); - sort_tasks(&mut tasks, &report, &working_set); - let descriptions: Vec<_> = tasks.iter().map(|t| t.get_description()).collect(); - assert_eq!(descriptions, vec!["A", "C", "B"]); - - // ascending - report.sort[0].ascending = false; - let mut tasks: Vec<_> = replica.all_tasks().unwrap().values().cloned().collect(); - sort_tasks(&mut tasks, &report, &working_set); - let descriptions: Vec<_> = tasks.iter().map(|t| t.get_description()).collect(); - assert_eq!(descriptions, vec!["B", "C", "A"]); - } - - #[test] - fn sorting_by_uuid() { - let mut replica = test_replica(); - let uuids = create_tasks(&mut replica); - let working_set = replica.working_set().unwrap(); - let report = Report { - sort: vec![Sort { - ascending: true, - sort_by: SortBy::Uuid, - }], - ..Default::default() - }; - - let mut tasks: Vec<_> = replica.all_tasks().unwrap().values().cloned().collect(); - sort_tasks(&mut tasks, &report, &working_set); - let got_uuids: Vec<_> = tasks.iter().map(|t| t.get_uuid()).collect(); - let mut exp_uuids = uuids.to_vec(); - exp_uuids.sort(); - assert_eq!(got_uuids, exp_uuids); - } - - #[test] - fn sorting_by_wait() { - let mut replica = test_replica(); - let uuids = create_tasks(&mut replica); - - replica - .get_task(uuids[0]) - .unwrap() - .unwrap() - .into_mut(&mut replica) - .set_wait(Some(Utc::now() + Duration::days(2))) - .unwrap(); - - replica - .get_task(uuids[1]) - .unwrap() - .unwrap() - .into_mut(&mut replica) - .set_wait(Some(Utc::now() + Duration::days(3))) - .unwrap(); - - let working_set = replica.working_set().unwrap(); - - let report = Report { - sort: vec![Sort { - ascending: true, - sort_by: SortBy::Wait, - }], - ..Default::default() - }; - - let mut tasks: Vec<_> = replica.all_tasks().unwrap().values().cloned().collect(); - sort_tasks(&mut tasks, &report, &working_set); - let got_uuids: Vec<_> = tasks.iter().map(|t| t.get_uuid()).collect(); - - let exp_uuids = vec![ - uuids[2], // no wait - uuids[0], // wait:2d - uuids[1], // wait:3d - ]; - - assert_eq!(got_uuids, exp_uuids); - } - - #[test] - fn sorting_by_multiple() { - let mut replica = test_replica(); - create_tasks(&mut replica); - - // make a second task named A with a larger ID than the first - let t = replica.new_task(Status::Pending, s!("A")).unwrap(); - t.into_mut(&mut replica) - .add_tag(&("second".try_into().unwrap())) - .unwrap(); - - let working_set = replica.working_set().unwrap(); - let report = Report { - sort: vec![ - Sort { - ascending: false, - sort_by: SortBy::Description, - }, - Sort { - ascending: true, - sort_by: SortBy::Id, - }, - ], - ..Default::default() - }; - - let mut tasks: Vec<_> = replica.all_tasks().unwrap().values().cloned().collect(); - sort_tasks(&mut tasks, &report, &working_set); - let descriptions: Vec<_> = tasks.iter().map(|t| t.get_description()).collect(); - assert_eq!(descriptions, vec!["C", "B", "A", "A"]); - assert!(tasks[3].has_tag(&("second".try_into().unwrap()))); - } - - #[test] - fn task_column_id() { - let mut replica = test_replica(); - let uuids = create_tasks(&mut replica); - let working_set = replica.working_set().unwrap(); - - let task = replica.get_task(uuids[0]).unwrap().unwrap(); - let column = Column { - label: s!(""), - property: Property::Id, - }; - assert_eq!(task_column(&task, &column, &working_set), s!("1")); - - // get the task that's not in the working set, which should show - // a uuid for its id column - let task = replica.get_task(uuids[1]).unwrap().unwrap(); - assert_eq!( - task_column(&task, &column, &working_set), - uuids[1].to_string() - ); - } - - #[test] - fn task_column_uuid() { - let mut replica = test_replica(); - let uuids = create_tasks(&mut replica); - let working_set = replica.working_set().unwrap(); - - let task = replica.get_task(uuids[0]).unwrap().unwrap(); - let column = Column { - label: s!(""), - property: Property::Uuid, - }; - assert_eq!( - task_column(&task, &column, &working_set), - task.get_uuid().to_string() - ); - } - - #[test] - fn task_column_active() { - let mut replica = test_replica(); - let uuids = create_tasks(&mut replica); - let working_set = replica.working_set().unwrap(); - - // make task A active - replica - .get_task(uuids[0]) - .unwrap() - .unwrap() - .into_mut(&mut replica) - .start() - .unwrap(); - - let column = Column { - label: s!(""), - property: Property::Active, - }; - - let task = replica.get_task(uuids[0]).unwrap().unwrap(); - assert_eq!(task_column(&task, &column, &working_set), s!("*")); - let task = replica.get_task(uuids[2]).unwrap().unwrap(); - assert_eq!(task_column(&task, &column, &working_set), s!("")); - } - - #[test] - fn task_column_description() { - let mut replica = test_replica(); - let uuids = create_tasks(&mut replica); - let working_set = replica.working_set().unwrap(); - - let task = replica.get_task(uuids[2]).unwrap().unwrap(); - let column = Column { - label: s!(""), - property: Property::Description, - }; - assert_eq!(task_column(&task, &column, &working_set), s!("C")); - } - - #[test] - fn task_column_tags() { - let mut replica = test_replica(); - let uuids = create_tasks(&mut replica); - let working_set = replica.working_set().unwrap(); - - // add some tags to task A - let mut t1 = replica - .get_task(uuids[0]) - .unwrap() - .unwrap() - .into_mut(&mut replica); - t1.add_tag(&("foo".try_into().unwrap())).unwrap(); - t1.add_tag(&("bar".try_into().unwrap())).unwrap(); - - let column = Column { - label: s!(""), - property: Property::Tags, - }; - - let task = replica.get_task(uuids[0]).unwrap().unwrap(); - assert_eq!( - task_column(&task, &column, &working_set), - s!("+PENDING +UNBLOCKED +bar +foo") - ); - let task = replica.get_task(uuids[2]).unwrap().unwrap(); - assert_eq!( - task_column(&task, &column, &working_set), - s!("+PENDING +UNBLOCKED") - ); - } -} diff --git a/rust/cli/src/invocation/test.rs b/rust/cli/src/invocation/test.rs deleted file mode 100644 index 72f11e137..000000000 --- a/rust/cli/src/invocation/test.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::io; -use taskchampion::{storage, Replica, Server, ServerConfig}; -use tempfile::TempDir; - -pub(super) fn test_replica() -> Replica { - let storage = storage::InMemoryStorage::new(); - Replica::new(Box::new(storage)) -} - -pub(super) fn test_server(dir: &TempDir) -> Box { - ServerConfig::Local { - server_dir: dir.path().to_path_buf(), - } - .into_server() - .unwrap() -} - -pub(super) struct TestWriter { - data: Vec, -} - -impl TestWriter { - pub(super) fn into_string(self) -> String { - String::from_utf8(self.data).unwrap() - } -} - -impl io::Write for TestWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.data.write(buf) - } - fn flush(&mut self) -> io::Result<()> { - self.data.flush() - } -} - -impl termcolor::WriteColor for TestWriter { - fn supports_color(&self) -> bool { - false - } - fn set_color(&mut self, _spec: &termcolor::ColorSpec) -> io::Result<()> { - Ok(()) - } - fn reset(&mut self) -> io::Result<()> { - Ok(()) - } -} - -pub(super) fn test_writer() -> TestWriter { - TestWriter { data: vec![] } -} diff --git a/rust/cli/src/invocation/util.rs b/rust/cli/src/invocation/util.rs deleted file mode 100644 index 12f4535e8..000000000 --- a/rust/cli/src/invocation/util.rs +++ /dev/null @@ -1,22 +0,0 @@ -use dialoguer::Confirm; -use taskchampion::{Replica, Task}; - -/// Print the prompt and ask the user to answer yes or no. If input is not from a terminal, the -/// answer is assumed to be true. -pub(super) fn confirm>(prompt: S) -> anyhow::Result { - if !atty::is(atty::Stream::Stdin) { - return Ok(true); - } - Ok(Confirm::new().with_prompt(prompt).interact()?) -} - -/// Summarize a task in a single line -pub(super) fn summarize_task(replica: &mut Replica, task: &Task) -> anyhow::Result { - let ws = replica.working_set()?; - let uuid = task.get_uuid(); - if let Some(id) = ws.by_uuid(uuid) { - Ok(format!("{} - {}", id, task.get_description())) - } else { - Ok(format!("{} - {}", uuid, task.get_description())) - } -} diff --git a/rust/cli/src/lib.rs b/rust/cli/src/lib.rs deleted file mode 100644 index 3b3258f21..000000000 --- a/rust/cli/src/lib.rs +++ /dev/null @@ -1,79 +0,0 @@ -#![deny(clippy::all)] -#![allow(clippy::unnecessary_wraps)] // for Rust 1.50, https://github.com/rust-lang/rust-clippy/pull/6765 -#![allow(clippy::module_inception)] // we use re-exports to shorten stuttering paths like settings::settings::Settings -/*! -This crate implements the command-line interface to TaskChampion. - -## Design - -The crate is split into two parts: argument parsing (`argparse`) and command invocation (`invocation`). -Both are fairly complex operations, and the split serves both to isolate that complexity and to facilitate testing. - -### Argparse - -The TaskChampion command line API is modeled on TaskWarrior's API, which is far from that of a typical UNIX command. -Tools like `clap` and `structopt` are not flexible enough to handle this syntax. - -Instead, the `argparse` module uses [nom](https://crates.io/crates/nom) to parse command lines as a sequence of words. -These parsers act on a list of strings, `&[&str]`, and at the top level return a `crate::argparse::Command`. -This is a wholly-owned repesentation of the command line's meaning, but with some interpretation. -For example, `task start`, `task stop`, and `task append` all map to a `crate::argparse::Subcommand::Modify` variant. - -### Invocation - -The `invocation` module executes a `Command`, given some settings and other ancillary data. -Most of its functionality is in common functions to handle filtering tasks, modifying tasks, and so on. - -## Rust API - -Note that this crate does not expose a Rust API for use from other crates. -For the public TaskChampion Rust API, see the `taskchampion` crate. - -*/ - -use std::ffi::OsString; - -// NOTE: it's important that this 'mod' comes first so that the macros can be used in other modules -mod macros; - -mod argparse; -mod errors; -mod invocation; -mod settings; -mod table; -mod tdb2; -mod usage; - -/// See https://docs.rs/built -pub(crate) mod built_info { - include!(concat!(env!("OUT_DIR"), "/built.rs")); -} - -pub(crate) use errors::Error; -use settings::Settings; - -// used by the `generate` command -pub use usage::Usage; - -/// The main entry point for the command-line interface. This builds an Invocation -/// from the particulars of the operating-system interface, and then executes it. -pub fn main() -> Result<(), Error> { - env_logger::init(); - - // parse the command line into a vector of &str, failing if - // there are invalid utf-8 sequences. - let argv: Vec = std::env::args_os() - .map(|oss| oss.into_string()) - .collect::>() - .map_err(|_| Error::for_arguments("arguments must be valid utf-8"))?; - let argv: Vec<&str> = argv.iter().map(|s| s.as_ref()).collect(); - - // parse the command line - let command = argparse::Command::from_argv(&argv[..])?; - - // load the application settings - let settings = Settings::read()?; - - invocation::invoke(command, settings)?; - Ok(()) -} diff --git a/rust/cli/src/macros.rs b/rust/cli/src/macros.rs deleted file mode 100644 index f2cbe803b..000000000 --- a/rust/cli/src/macros.rs +++ /dev/null @@ -1,40 +0,0 @@ -#![macro_use] - -/// create a &[&str] from vec notation -#[cfg(test)] -macro_rules! argv { - () => ( - &[][..] - ); - ($($x:expr),* $(,)?) => ( - &[$($x),*][..] - ); -} - -/// Create a hashset, similar to vec! -// NOTE: in Rust 1.56.0, this can be changed to HashSet::from([..]) -#[cfg(test)] -macro_rules! set( - { $($key:expr),* $(,)? } => { - { - #[allow(unused_mut)] - let mut s = ::std::collections::HashSet::new(); - $( - s.insert($key); - )* - s - } - }; -); - -/// Create a String from an &str; just a testing shorthand -#[cfg(test)] -macro_rules! s( - { $s:expr } => { $s.to_owned() }; -); - -/// Create a Tag from an &str; just a testing shorthand -#[cfg(test)] -macro_rules! tag( - { $s:expr } => { { use std::convert::TryFrom; taskchampion::Tag::try_from($s).unwrap() } }; -); diff --git a/rust/cli/src/settings/mod.rs b/rust/cli/src/settings/mod.rs deleted file mode 100644 index c6a6ddd2f..000000000 --- a/rust/cli/src/settings/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Support for the CLI's configuration file, including default settings. -//! -//! Configuration is stored in a "parsed" format, meaning that any syntax errors will be caught on -//! startup and not just when those values are used. - -mod report; -mod settings; -mod util; - -pub(crate) use report::{get_usage, Column, Property, Report, Sort, SortBy}; -pub(crate) use settings::Settings; diff --git a/rust/cli/src/settings/report.rs b/rust/cli/src/settings/report.rs deleted file mode 100644 index 3cc5e3e2c..000000000 --- a/rust/cli/src/settings/report.rs +++ /dev/null @@ -1,580 +0,0 @@ -//! This module contains the data structures used to define reports. - -use crate::argparse::{Condition, Filter}; -use crate::settings::util::table_with_keys; -use crate::usage::{self, Usage}; -use anyhow::{anyhow, bail, Result}; -use std::convert::{TryFrom, TryInto}; - -/// A report specifies a filter as well as a sort order and information about which -/// task attributes to display -#[derive(Clone, Debug, PartialEq, Default)] -pub(crate) struct Report { - /// Columns to display in this report - pub columns: Vec, - /// Sort order for this report - pub sort: Vec, - /// Filter selecting tasks for this report - pub filter: Filter, -} - -/// A column to display in a report -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct Column { - /// The label for this column - pub label: String, - - /// The property to display - pub property: Property, -} - -/// Task property to display in a report -#[derive(Clone, Debug, PartialEq)] -pub(crate) enum Property { - // NOTE: when adding a property here, add it to get_usage, below, as well. - /// The task's ID, either working-set index or Uuid if not in the working set - Id, - - /// The task's full UUID - Uuid, - - /// Whether the task is active or not - Active, - - /// The task's description - Description, - - /// The task's tags - Tags, - - /// The task's wait date - Wait, -} - -/// A sorting criterion for a sort operation. -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct Sort { - /// True if the sort should be "ascending" (a -> z, 0 -> 9, etc.) - pub ascending: bool, - - /// The property to sort on - pub sort_by: SortBy, -} - -/// Task property to sort by -#[derive(Clone, Debug, PartialEq)] -pub(crate) enum SortBy { - // NOTE: when adding a property here, add it to get_usage, below, as well. - /// The task's ID, either working-set index or a UUID prefix; working - /// set tasks sort before others. - Id, - - /// The task's full UUID - Uuid, - - /// The task's description - Description, - - /// The task's wait date - Wait, -} - -// Conversions from settings::Settings. - -impl TryFrom for Report { - type Error = anyhow::Error; - - fn try_from(cfg: toml::Value) -> Result { - Report::try_from(&cfg) - } -} - -impl TryFrom<&toml::Value> for Report { - type Error = anyhow::Error; - - /// Create a Report from a toml value. This should be the `report.` value. - /// The error message begins with any additional path information, e.g., `.sort[1].sort_by: - /// ..`. - fn try_from(cfg: &toml::Value) -> Result { - let keys = ["sort", "columns", "filter"]; - let table = table_with_keys(cfg, &keys).map_err(|e| anyhow!(": {}", e))?; - - let sort = match table.get("sort") { - Some(v) => v - .as_array() - .ok_or_else(|| anyhow!(".sort: not an array"))? - .iter() - .enumerate() - .map(|(i, v)| v.try_into().map_err(|e| anyhow!(".sort[{}]{}", i, e))) - .collect::>>()?, - None => vec![], - }; - - let columns = match table.get("columns") { - Some(v) => v - .as_array() - .ok_or_else(|| anyhow!(".columns: not an array"))? - .iter() - .enumerate() - .map(|(i, v)| v.try_into().map_err(|e| anyhow!(".columns[{}]{}", i, e))) - .collect::>>()?, - None => bail!(": `columns` property is required"), - }; - - let conditions = match table.get("filter") { - Some(v) => v - .as_array() - .ok_or_else(|| anyhow!(".filter: not an array"))? - .iter() - .enumerate() - .map(|(i, v)| { - v.as_str() - .ok_or_else(|| anyhow!(".filter[{}]: not a string", i)) - .and_then(Condition::parse_str) - .map_err(|e| anyhow!(".filter[{}]: {}", i, e)) - }) - .collect::>>()?, - None => vec![], - }; - - Ok(Report { - columns, - sort, - filter: Filter { conditions }, - }) - } -} - -impl TryFrom<&toml::Value> for Column { - type Error = anyhow::Error; - - fn try_from(cfg: &toml::Value) -> Result { - let keys = ["label", "property"]; - let table = table_with_keys(cfg, &keys).map_err(|e| anyhow!(": {}", e))?; - - let label = match table.get("label") { - Some(v) => v - .as_str() - .ok_or_else(|| anyhow!(".label: not a string"))? - .to_owned(), - None => bail!(": `label` property is required"), - }; - - let property = match table.get("property") { - Some(v) => v.try_into().map_err(|e| anyhow!(".property{}", e))?, - None => bail!(": `property` property is required"), - }; - - Ok(Column { label, property }) - } -} - -impl TryFrom<&toml::Value> for Property { - type Error = anyhow::Error; - - fn try_from(cfg: &toml::Value) -> Result { - let s = cfg.as_str().ok_or_else(|| anyhow!(": not a string"))?; - Ok(match s { - "id" => Property::Id, - "uuid" => Property::Uuid, - "active" => Property::Active, - "description" => Property::Description, - "tags" => Property::Tags, - "wait" => Property::Wait, - _ => bail!(": unknown property {}", s), - }) - } -} - -impl TryFrom<&toml::Value> for Sort { - type Error = anyhow::Error; - - fn try_from(cfg: &toml::Value) -> Result { - let keys = ["ascending", "sort_by"]; - let table = table_with_keys(cfg, &keys).map_err(|e| anyhow!(": {}", e))?; - let ascending = match table.get("ascending") { - Some(v) => v - .as_bool() - .ok_or_else(|| anyhow!(".ascending: not a boolean value"))?, - None => true, // default - }; - - let sort_by = match table.get("sort_by") { - Some(v) => v.try_into().map_err(|e| anyhow!(".sort_by{}", e))?, - None => bail!(": `sort_by` property is required"), - }; - - Ok(Sort { ascending, sort_by }) - } -} - -impl TryFrom<&toml::Value> for SortBy { - type Error = anyhow::Error; - - fn try_from(cfg: &toml::Value) -> Result { - let s = cfg.as_str().ok_or_else(|| anyhow!(": not a string"))?; - Ok(match s { - "id" => SortBy::Id, - "uuid" => SortBy::Uuid, - "description" => SortBy::Description, - "wait" => SortBy::Wait, - _ => bail!(": unknown sort_by value `{}`", s), - }) - } -} - -pub(crate) fn get_usage(u: &mut Usage) { - u.report_properties.push(usage::ReportProperty { - name: "id", - as_sort_by: Some("Sort by the task's shorthand ID"), - as_column: Some("The task's shorthand ID"), - }); - u.report_properties.push(usage::ReportProperty { - name: "uuid", - as_sort_by: Some("Sort by the task's full UUID"), - as_column: Some("The task's full UUID"), - }); - u.report_properties.push(usage::ReportProperty { - name: "active", - as_sort_by: None, - as_column: Some("`*` if the task is active (started)"), - }); - u.report_properties.push(usage::ReportProperty { - name: "wait", - as_sort_by: Some("Sort by the task's wait date, with non-waiting tasks first"), - as_column: Some("Wait date of the task"), - }); - u.report_properties.push(usage::ReportProperty { - name: "description", - as_sort_by: Some("Sort by the task's description"), - as_column: Some("The task's description"), - }); - u.report_properties.push(usage::ReportProperty { - name: "tags", - as_sort_by: None, - as_column: Some("The task's tags"), - }); -} - -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - use taskchampion::Status; - use toml::toml; - - #[test] - fn test_report_ok() { - let val = toml! { - sort = [] - columns = [] - filter = ["status:pending"] - }; - let report: Report = TryInto::try_into(val).unwrap(); - assert_eq!( - report.filter, - Filter { - conditions: vec![Condition::Status(Status::Pending),], - } - ); - assert_eq!(report.columns, vec![]); - assert_eq!(report.sort, vec![]); - } - - #[test] - fn test_report_no_sort() { - let val = toml! { - filter = [] - columns = [] - }; - let report = Report::try_from(val).unwrap(); - assert_eq!(report.sort, vec![]); - } - - #[test] - fn test_report_sort_not_array() { - let val = toml! { - filter = [] - sort = true - columns = [] - }; - let err = Report::try_from(val).unwrap_err().to_string(); - assert_eq!(&err, ".sort: not an array"); - } - - #[test] - fn test_report_sort_error() { - let val = toml! { - filter = [] - sort = [ { sort_by = "id" }, true ] - columns = [] - }; - let err = Report::try_from(val).unwrap_err().to_string(); - assert!(err.starts_with(".sort[1]")); - } - - #[test] - fn test_report_unknown_prop() { - let val = toml! { - columns = [] - filter = [] - sort = [] - nosuch = true - }; - let err = Report::try_from(val).unwrap_err().to_string(); - assert_eq!(&err, ": unknown table key `nosuch`"); - } - - #[test] - fn test_report_no_columns() { - let val = toml! { - filter = [] - sort = [] - }; - let err = Report::try_from(val).unwrap_err().to_string(); - assert_eq!(&err, ": `columns` property is required"); - } - - #[test] - fn test_report_columns_not_array() { - let val = toml! { - filter = [] - sort = [] - columns = true - }; - let err = Report::try_from(val).unwrap_err().to_string(); - assert_eq!(&err, ".columns: not an array"); - } - - #[test] - fn test_report_column_error() { - let val = toml! { - filter = [] - sort = [] - - [[columns]] - label = "ID" - property = "id" - - [[columns]] - foo = 10 - }; - let err = Report::try_from(val).unwrap_err().to_string(); - assert_eq!(&err, ".columns[1]: unknown table key `foo`"); - } - - #[test] - fn test_report_filter_not_array() { - let val = toml! { - filter = "foo" - sort = [] - columns = [] - }; - let err = Report::try_from(val).unwrap_err().to_string(); - assert_eq!(&err, ".filter: not an array"); - } - - #[test] - fn test_report_filter_error() { - let val = toml! { - sort = [] - columns = [] - filter = [ "nosuchfilter" ] - }; - let err = Report::try_from(val).unwrap_err().to_string(); - assert!(err.starts_with(".filter[0]: invalid filter condition:")); - } - - #[test] - fn test_column() { - let val = toml! { - label = "ID" - property = "id" - }; - let column = Column::try_from(&val).unwrap(); - assert_eq!( - column, - Column { - label: "ID".to_owned(), - property: Property::Id, - } - ); - } - - #[test] - fn test_column_unknown_prop() { - let val = toml! { - label = "ID" - property = "id" - nosuch = "foo" - }; - assert_eq!( - &Column::try_from(&val).unwrap_err().to_string(), - ": unknown table key `nosuch`" - ); - } - - #[test] - fn test_column_no_label() { - let val = toml! { - property = "id" - }; - assert_eq!( - &Column::try_from(&val).unwrap_err().to_string(), - ": `label` property is required" - ); - } - - #[test] - fn test_column_invalid_label() { - let val = toml! { - label = [] - property = "id" - }; - assert_eq!( - &Column::try_from(&val).unwrap_err().to_string(), - ".label: not a string" - ); - } - - #[test] - fn test_column_no_property() { - let val = toml! { - label = "ID" - }; - assert_eq!( - &Column::try_from(&val).unwrap_err().to_string(), - ": `property` property is required" - ); - } - - #[test] - fn test_column_invalid_property() { - let val = toml! { - label = "ID" - property = [] - }; - assert_eq!( - &Column::try_from(&val).unwrap_err().to_string(), - ".property: not a string" - ); - } - - #[test] - fn test_property() { - let val = toml::Value::String("uuid".to_owned()); - let prop = Property::try_from(&val).unwrap(); - assert_eq!(prop, Property::Uuid); - } - - #[test] - fn test_property_invalid_type() { - let val = toml::Value::Array(vec![]); - assert_eq!( - &Property::try_from(&val).unwrap_err().to_string(), - ": not a string" - ); - } - - #[test] - fn test_sort() { - let val = toml! { - ascending = false - sort_by = "id" - }; - let sort = Sort::try_from(&val).unwrap(); - assert_eq!( - sort, - Sort { - ascending: false, - sort_by: SortBy::Id, - } - ); - } - - #[test] - fn test_sort_no_ascending() { - let val = toml! { - sort_by = "id" - }; - let sort = Sort::try_from(&val).unwrap(); - assert_eq!( - sort, - Sort { - ascending: true, - sort_by: SortBy::Id, - } - ); - } - - #[test] - fn test_sort_unknown_prop() { - let val = toml! { - sort_by = "id" - nosuch = true - }; - assert_eq!( - &Sort::try_from(&val).unwrap_err().to_string(), - ": unknown table key `nosuch`" - ); - } - - #[test] - fn test_sort_no_sort_by() { - let val = toml! { - ascending = true - }; - assert_eq!( - &Sort::try_from(&val).unwrap_err().to_string(), - ": `sort_by` property is required" - ); - } - - #[test] - fn test_sort_invalid_ascending() { - let val = toml! { - sort_by = "id" - ascending = {} - }; - assert_eq!( - &Sort::try_from(&val).unwrap_err().to_string(), - ".ascending: not a boolean value" - ); - } - - #[test] - fn test_sort_invalid_sort_by() { - let val = toml! { - sort_by = {} - }; - assert_eq!( - &Sort::try_from(&val).unwrap_err().to_string(), - ".sort_by: not a string" - ); - } - - #[test] - fn test_sort_by() { - let val = toml::Value::String("uuid".to_string()); - let prop = SortBy::try_from(&val).unwrap(); - assert_eq!(prop, SortBy::Uuid); - } - - #[test] - fn test_sort_by_unknown() { - let val = toml::Value::String("nosuch".to_string()); - assert_eq!( - &SortBy::try_from(&val).unwrap_err().to_string(), - ": unknown sort_by value `nosuch`" - ); - } - - #[test] - fn test_sort_by_invalid_type() { - let val = toml::Value::Array(vec![]); - assert_eq!( - &SortBy::try_from(&val).unwrap_err().to_string(), - ": not a string" - ); - } -} diff --git a/rust/cli/src/settings/settings.rs b/rust/cli/src/settings/settings.rs deleted file mode 100644 index 3dba86409..000000000 --- a/rust/cli/src/settings/settings.rs +++ /dev/null @@ -1,449 +0,0 @@ -use super::util::table_with_keys; -use super::{Column, Property, Report, Sort, SortBy}; -use crate::argparse::{Condition, Filter}; -use anyhow::{anyhow, bail, Context, Result}; -use std::collections::HashMap; -use std::convert::TryFrom; -use std::env; -use std::fs; -use std::path::PathBuf; -use taskchampion::Status; -use toml::value::Table; -use toml_edit::Document; - -#[derive(Debug, PartialEq)] -pub(crate) struct Settings { - /// filename from which this configuration was loaded, if any - pub(crate) filename: Option, - - /// Maximum number of tasks to modify without a confirmation prompt; `Some(0)` means to never - /// prompt, and `None` means to use the default value. - pub(crate) modification_count_prompt: Option, - - /// replica - pub(crate) data_dir: PathBuf, - pub(crate) avoid_snapshots: bool, - - /// remote sync server - pub(crate) server_client_key: Option, - pub(crate) server_origin: Option, - pub(crate) encryption_secret: Option, - - /// local sync server - pub(crate) server_dir: PathBuf, - - /// reports - pub(crate) reports: HashMap, -} - -impl Settings { - pub(crate) fn read() -> Result { - if let Some(config_file) = env::var_os("TASKCHAMPION_CONFIG") { - log::debug!("Loading configuration from {:?}", config_file); - env::remove_var("TASKCHAMPION_CONFIG"); - Self::load_from_file(config_file.into(), true) - } else if let Some(filename) = Settings::default_filename() { - log::debug!("Loading configuration from {:?} (optional)", filename); - Self::load_from_file(filename, false) - } else { - Ok(Default::default()) - } - } - - /// Get the default filename for the configuration, or None if that cannot - /// be determined. - fn default_filename() -> Option { - dirs_next::config_dir().map(|dir| dir.join("taskchampion.toml")) - } - - /// Update this settings object with the contents of the given TOML file. Top-level settings - /// are overwritten, and reports are overwritten by name. - pub(crate) fn load_from_file(config_file: PathBuf, required: bool) -> Result { - let mut settings = Self::default(); - - let config_toml = match fs::read_to_string(config_file.clone()) { - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - return if required { - Err(e.into()) - } else { - settings.filename = Some(config_file); - Ok(settings) - }; - } - Err(e) => return Err(e.into()), - Ok(s) => s, - }; - - let config_toml = config_toml - .parse::() - .with_context(|| format!("error while reading {:?}", config_file))?; - - settings.filename = Some(config_file.clone()); - settings - .update_from_toml(&config_toml) - .with_context(|| format!("error while parsing {:?}", config_file))?; - - Ok(settings) - } - - /// Update this object with configuration from the given config file. This is - /// broken out mostly for convenience in error handling - fn update_from_toml(&mut self, config_toml: &toml::Value) -> Result<()> { - let table_keys = [ - "data_dir", - "modification_count_prompt", - "avoid_snapshots", - "server_client_key", - "server_origin", - "encryption_secret", - "server_dir", - "reports", - ]; - let table = table_with_keys(config_toml, &table_keys)?; - - fn get_str_cfg( - table: &Table, - name: &'static str, - setter: F, - ) -> Result<()> { - if let Some(v) = table.get(name) { - setter( - v.as_str() - .ok_or_else(|| anyhow!(".{}: not a string", name))? - .to_owned(), - ); - } - Ok(()) - } - - fn get_i64_cfg(table: &Table, name: &'static str, setter: F) -> Result<()> { - if let Some(v) = table.get(name) { - setter( - v.as_integer() - .ok_or_else(|| anyhow!(".{}: not a number", name))?, - ); - } - Ok(()) - } - - fn get_bool_cfg( - table: &Table, - name: &'static str, - setter: F, - ) -> Result<()> { - if let Some(v) = table.get(name) { - setter( - v.as_bool() - .ok_or_else(|| anyhow!(".{}: not a boolean value", name))?, - ); - } - Ok(()) - } - - get_str_cfg(table, "data_dir", |v| { - self.data_dir = v.into(); - })?; - - get_i64_cfg(table, "modification_count_prompt", |v| { - self.modification_count_prompt = Some(v); - })?; - - get_bool_cfg(table, "avoid_snapshots", |v| { - self.avoid_snapshots = v; - })?; - - get_str_cfg(table, "server_client_key", |v| { - self.server_client_key = Some(v); - })?; - - get_str_cfg(table, "server_origin", |v| { - self.server_origin = Some(v); - })?; - - get_str_cfg(table, "encryption_secret", |v| { - self.encryption_secret = Some(v); - })?; - - get_str_cfg(table, "server_dir", |v| { - self.server_dir = v.into(); - })?; - - if let Some(v) = table.get("reports") { - let report_cfgs = v - .as_table() - .ok_or_else(|| anyhow!(".reports: not a table"))?; - for (name, cfg) in report_cfgs { - let report = Report::try_from(cfg).map_err(|e| anyhow!("reports.{}{}", name, e))?; - self.reports.insert(name.clone(), report); - } - } - - Ok(()) - } - - /// Set a value in the config file, modifying it in place. Returns the filename. The value is - /// interpreted as the appropriate type for the configuration setting. - pub(crate) fn set(&self, key: &str, value: &str) -> Result { - let allowed_keys = [ - "data_dir", - "modification_count_prompt", - "server_client_key", - "server_origin", - "encryption_secret", - "server_dir", - // reports is not allowed, since it is not a string - ]; - if !allowed_keys.contains(&key) { - bail!("No such configuration key {}", key); - } - - let filename = if let Some(ref f) = self.filename { - f.clone() - } else { - Settings::default_filename() - .ok_or_else(|| anyhow!("Could not determine config file name"))? - }; - - let exists = filename.exists(); - - // try to create the parent directory if the file does not exist - if !exists { - if let Some(dir) = filename.parent() { - fs::create_dir_all(dir)?; - } - } - - // start with the existing document, or a blank document - let mut document = if exists { - fs::read_to_string(filename.clone()) - .context("Could not read existing configuration file")? - .parse::() - .context("Could not parse existing configuration file")? - } else { - Document::new() - }; - - // set the value as the correct type - match key { - // integers - "modification_count_prompt" => { - let value: i64 = value.parse()?; - document[key] = toml_edit::value(value); - } - - // most keys are strings - _ => document[key] = toml_edit::value(value), - } - - fs::write(filename.clone(), document.to_string()) - .context("Could not write updated configuration file")?; - - Ok(filename) - } -} - -impl Default for Settings { - fn default() -> Self { - let data_dir; - let server_dir; - - if let Some(dir) = dirs_next::data_local_dir() { - data_dir = dir.join("taskchampion"); - server_dir = dir.join("taskchampion-sync-server"); - } else { - // fallback - data_dir = PathBuf::from("."); - server_dir = PathBuf::from("."); - } - - // define the default reports - let mut reports = HashMap::new(); - - reports.insert( - "list".to_owned(), - Report { - sort: vec![Sort { - ascending: true, - sort_by: SortBy::Uuid, - }], - columns: vec![ - Column { - label: "id".to_owned(), - property: Property::Id, - }, - Column { - label: "description".to_owned(), - property: Property::Description, - }, - Column { - label: "active".to_owned(), - property: Property::Active, - }, - Column { - label: "tags".to_owned(), - property: Property::Tags, - }, - Column { - label: "wait".to_owned(), - property: Property::Wait, - }, - ], - filter: Default::default(), - }, - ); - - reports.insert( - "next".to_owned(), - Report { - sort: vec![ - Sort { - ascending: true, - sort_by: SortBy::Id, - }, - Sort { - ascending: true, - sort_by: SortBy::Uuid, - }, - ], - columns: vec![ - Column { - label: "id".to_owned(), - property: Property::Id, - }, - Column { - label: "description".to_owned(), - property: Property::Description, - }, - Column { - label: "active".to_owned(), - property: Property::Active, - }, - Column { - label: "tags".to_owned(), - property: Property::Tags, - }, - ], - filter: Filter { - conditions: vec![Condition::Status(Status::Pending)], - }, - }, - ); - - Self { - filename: None, - data_dir, - modification_count_prompt: None, - avoid_snapshots: false, - server_client_key: None, - server_origin: None, - encryption_secret: None, - server_dir, - reports, - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - use tempfile::TempDir; - use toml::toml; - - #[test] - fn test_load_from_file_not_required() { - let cfg_dir = TempDir::new().unwrap(); - let cfg_file = cfg_dir.path().join("foo.toml"); - - let settings = Settings::load_from_file(cfg_file.clone(), false).unwrap(); - - let mut expected = Settings::default(); - expected.filename = Some(cfg_file.clone()); - assert_eq!(settings, expected); - } - - #[test] - fn test_load_from_file_required() { - let cfg_dir = TempDir::new().unwrap(); - - assert!(Settings::load_from_file(cfg_dir.path().join("foo.toml"), true).is_err()); - } - - #[test] - fn test_load_from_file_exists() { - let cfg_dir = TempDir::new().unwrap(); - let cfg_file = cfg_dir.path().join("foo.toml"); - fs::write(cfg_file.clone(), "data_dir = \"/nowhere\"").unwrap(); - - let settings = Settings::load_from_file(cfg_file.clone(), true).unwrap(); - assert_eq!(settings.data_dir, PathBuf::from("/nowhere")); - assert_eq!(settings.filename, Some(cfg_file)); - } - - #[test] - fn test_update_from_toml_top_level_keys() { - let val = toml! { - data_dir = "/data" - modification_count_prompt = 42 - server_client_key = "sck" - server_origin = "so" - encryption_secret = "es" - server_dir = "/server" - }; - let mut settings = Settings::default(); - settings.update_from_toml(&val).unwrap(); - - assert_eq!(settings.data_dir, PathBuf::from("/data")); - assert_eq!(settings.modification_count_prompt, Some(42)); - assert_eq!(settings.server_client_key, Some("sck".to_owned())); - assert_eq!(settings.server_origin, Some("so".to_owned())); - assert_eq!(settings.encryption_secret, Some("es".to_owned())); - assert_eq!(settings.server_dir, PathBuf::from("/server")); - } - - #[test] - fn test_update_from_toml_report() { - let val = toml! { - [reports.foo] - sort = [ { sort_by = "id" } ] - columns = [ { label = "ID", property = "id" } ] - }; - let mut settings = Settings::default(); - settings.update_from_toml(&val).unwrap(); - - assert!(settings.reports.get("foo").is_some()); - // beyond existence of this report, we can rely on Report's unit tests - } - - #[test] - fn test_set_valid_key() { - let cfg_dir = TempDir::new().unwrap(); - let cfg_file = cfg_dir.path().join("foo.toml"); - fs::write(cfg_file.clone(), "server_dir = \"/srv\"").unwrap(); - - let settings = Settings::load_from_file(cfg_file.clone(), true).unwrap(); - assert_eq!(settings.filename, Some(cfg_file.clone())); - settings.set("data_dir", "/data").unwrap(); - settings.set("modification_count_prompt", "42").unwrap(); - - // load the file again and see the changes - let settings = Settings::load_from_file(cfg_file.clone(), true).unwrap(); - assert_eq!(settings.data_dir, PathBuf::from("/data")); - assert_eq!(settings.server_dir, PathBuf::from("/srv")); - assert_eq!(settings.filename, Some(cfg_file)); - assert_eq!(settings.modification_count_prompt, Some(42)); - } - - #[test] - fn test_set_invalid_key() { - let cfg_dir = TempDir::new().unwrap(); - let cfg_file = cfg_dir.path().join("foo.toml"); - fs::write(cfg_file.clone(), "server_dir = \"/srv\"").unwrap(); - - let settings = Settings::load_from_file(cfg_file.clone(), true).unwrap(); - assert_eq!(settings.filename, Some(cfg_file.clone())); - assert!(settings - .set("modification_count_prompt", "a string?") - .is_err()); - } -} diff --git a/rust/cli/src/settings/util.rs b/rust/cli/src/settings/util.rs deleted file mode 100644 index 85d2e1d52..000000000 --- a/rust/cli/src/settings/util.rs +++ /dev/null @@ -1,42 +0,0 @@ -use anyhow::{anyhow, bail, Result}; -use toml::value::Table; - -/// Check that the input is a table and contains no keys not in the given list, returning -/// the table. -pub(super) fn table_with_keys<'a>(cfg: &'a toml::Value, keys: &[&str]) -> Result<&'a Table> { - let table = cfg.as_table().ok_or_else(|| anyhow!("not a table"))?; - - for tk in table.keys() { - if !keys.iter().any(|k| k == tk) { - bail!("unknown table key `{}`", tk); - } - } - Ok(table) -} - -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - use toml::toml; - - #[test] - fn test_dissect_table_missing() { - let val = toml! { bar = true }; - let diss = table_with_keys(&val, &["foo", "bar"]).unwrap(); - assert_eq!(diss.get("bar"), Some(&toml::Value::Boolean(true))); - assert_eq!(diss.get("foo"), None); - } - - #[test] - fn test_dissect_table_extra() { - let val = toml! { nosuch = 10 }; - assert!(table_with_keys(&val, &["foo", "bar"]).is_err()); - } - - #[test] - fn test_dissect_table_not_a_table() { - let val = toml::Value::Array(vec![]); - assert!(table_with_keys(&val, &["foo", "bar"]).is_err()); - } -} diff --git a/rust/cli/src/table.rs b/rust/cli/src/table.rs deleted file mode 100644 index 9fdb9b833..000000000 --- a/rust/cli/src/table.rs +++ /dev/null @@ -1,8 +0,0 @@ -use prettytable::format; - -pub(crate) fn format() -> format::TableFormat { - format::FormatBuilder::new() - .column_separator(' ') - .borders(' ') - .build() -} diff --git a/rust/cli/src/tdb2/mod.rs b/rust/cli/src/tdb2/mod.rs deleted file mode 100644 index 0ff59a311..000000000 --- a/rust/cli/src/tdb2/mod.rs +++ /dev/null @@ -1,326 +0,0 @@ -//! TDB2 is TaskWarrior's on-disk database format. The set of tasks is represented in -//! `pending.data` and `completed.data`. There are other `.data` files as well, but those are not -//! used in TaskChampion. -use nom::{branch::*, character::complete::*, combinator::*, multi::*, sequence::*, IResult}; -use std::fmt; - -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct File { - pub(crate) lines: Vec, -} - -#[derive(Clone, PartialEq)] -pub(crate) struct Line { - pub(crate) attrs: Vec, -} - -#[derive(Clone, PartialEq)] -pub(crate) struct Attr { - pub(crate) name: String, - pub(crate) value: String, -} - -impl File { - pub(crate) fn from_str(input: &str) -> Result { - File::parse(input).map(|(_, res)| res).map_err(|_| ()) - } - - fn parse(input: &str) -> IResult<&str, File> { - all_consuming(fold_many0( - // allow windows or normal newlines - terminated(Line::parse, pair(opt(char('\r')), char('\n'))), - File { lines: vec![] }, - |mut file, line| { - file.lines.push(line); - file - }, - ))(input) - } -} - -impl Line { - /// Parse a line in a TDB2 file. See TaskWarrior's Task::Parse. - fn parse(input: &str) -> IResult<&str, Line> { - fn to_line(input: Vec) -> Result { - Ok(Line { attrs: input }) - } - map_res( - delimited( - char('['), - separated_list0(char(' '), Attr::parse), - char(']'), - ), - to_line, - )(input) - } -} - -impl fmt::Debug for Line { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("line!")?; - f.debug_list().entries(self.attrs.iter()).finish() - } -} - -impl Attr { - /// Parse an attribute (name-value pair). - fn parse(input: &str) -> IResult<&str, Attr> { - fn to_attr(input: (&str, String)) -> Result { - Ok(Attr { - name: input.0.into(), - value: input.1, - }) - } - map_res( - separated_pair(Attr::parse_name, char(':'), Attr::parse_value), - to_attr, - )(input) - } - - /// Parse an attribute name, which is composed of any character but `:`. - fn parse_name(input: &str) -> IResult<&str, &str> { - recognize(many1(none_of(":")))(input) - } - - /// Parse and interpret a quoted string. Note that this does _not_ reverse the effects of - - fn parse_value(input: &str) -> IResult<&str, String> { - // For the parsing part of the job, see Pig::getQuoted in TaskWarrior's libshared, which - // merely finds the end of a string. - // - // The interpretation is defined in json::decode in libshared. Fortunately, the data we - // are reading was created with json::encode, which does not perform unicode escaping. - - fn escaped_string_char(input: &str) -> IResult<&str, char> { - alt(( - // reverse the escaping performed in json::encode - preceded( - char('\\'), - alt(( - // some characters are simply escaped - one_of(r#""\/"#), - // others translate to control characters - value('\x08', char('b')), - value('\x0c', char('f')), - value('\n', char('n')), - value('\r', char('r')), - value('\t', char('t')), - )), - ), - // not a backslash or double-quote - none_of("\"\\"), - ))(input) - } - - let inner = fold_many0( - escaped_string_char, - String::new(), - |mut string, fragment| { - string.push(fragment); - string - }, - ); - - delimited(char('"'), inner, char('"'))(input) - } -} - -impl fmt::Debug for Attr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_fmt(format_args!("{:?} => {:?}", self.name, self.value)) - } -} - -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - - macro_rules! line { - ($($n:expr => $v:expr),* $(,)?) => ( - Line{attrs: vec![$(Attr{name: $n.into(), value: $v.into()}),*]} - ); - } - - #[test] - fn file() { - assert_eq!( - File::parse(include_str!("test.data")).unwrap(), - ( - "", - File { - lines: vec![ - line![ - "description" => "snake 🐍", - "entry" => "1641670385", - "modified" => "1641670385", - "priority" => "M", - "status" => "pending", - "uuid" => "f19086c2-1f8d-4a6c-9b8d-f94901fb8e62", - ], - line![ - "annotation_1585711454" => - "https://blog.tensorflow.org/2020/03/face-and-hand-tracking-in-browser-with-mediapipe-and-tensorflowjs.html?linkId=83993617", - "description" => "try facemesh", - "entry" => "1585711451", - "modified" => "1592947544", - "priority" => "M", - "project" => "lists", - "status" => "pending", - "tags" => "idea", - "tags_idea" => "x", - "uuid" => "ee855dc7-6f61-408c-bc95-ebb52f7d529c", - ], - line![ - "description" => "testing", - "entry" => "1554074416", - "modified" => "1554074416", - "priority" => "M", - "status" => "pending", - "uuid" => "4578fb67-359b-4483-afe4-fef15925ccd6", - ], - line![ - "description" => "testing2", - "entry" => "1576352411", - "modified" => "1576352411", - "priority" => "M", - "status" => "pending", - "uuid" => "f5982cca-2ea1-4bfd-832c-9bd571dc0743", - ], - line![ - "description" => "new-task", - "entry" => "1576352696", - "modified" => "1576352696", - "priority" => "M", - "status" => "pending", - "uuid" => "cfee3170-f153-4075-aa1d-e20bcac2841b", - ], - line![ - "description" => "foo", - "entry" => "1579398776", - "modified" => "1579398776", - "priority" => "M", - "status" => "pending", - "uuid" => "df74ea94-5122-44fa-965a-637412fbbffc", - ], - ] - } - ) - ); - } - - #[test] - fn empty_line() { - assert_eq!(Line::parse("[]").unwrap(), ("", line![])); - } - - #[test] - fn escaped_line() { - assert_eq!( - Line::parse(r#"[annotation_1585711454:"\"\\\"" abc:"xx"]"#).unwrap(), - ( - "", - line!["annotation_1585711454" => "\"\\\"", "abc" => "xx"] - ) - ); - } - - #[test] - fn escaped_line_backslash() { - assert_eq!( - Line::parse(r#"[abc:"xx" 123:"x\\x"]"#).unwrap(), - ("", line!["abc" => "xx", "123" => "x\\x"]) - ); - } - - #[test] - fn escaped_line_quote() { - assert_eq!( - Line::parse(r#"[abc:"xx" 123:"x\"x"]"#).unwrap(), - ("", line!["abc" => "xx", "123" => "x\"x"]) - ); - } - - #[test] - fn unicode_line() { - assert_eq!( - Line::parse(r#"[description:"snake 🐍" entry:"1641670385" modified:"1641670385" priority:"M" status:"pending" uuid:"f19086c2-1f8d-4a6c-9b8d-f94901fb8e62"]"#).unwrap(), - ("", line![ - "description" => "snake 🐍", - "entry" => "1641670385", - "modified" => "1641670385", - "priority" => "M", - "status" => "pending", - "uuid" => "f19086c2-1f8d-4a6c-9b8d-f94901fb8e62", - ])); - } - - #[test] - fn backslashed_attr() { - assert!(Attr::parse(r#"one:"\""#).is_err()); - assert_eq!( - Attr::parse(r#"two:"\\""#).unwrap(), - ( - "", - Attr { - name: "two".into(), - value: r#"\"#.into(), - } - ) - ); - assert!(Attr::parse(r#"three:"\\\""#).is_err()); - assert_eq!( - Attr::parse(r#"four:"\\\\""#).unwrap(), - ( - "", - Attr { - name: "four".into(), - value: r#"\\"#.into(), - } - ) - ); - } - - #[test] - fn backslash_frontslash() { - assert_eq!( - Attr::parse(r#"front:"\/""#).unwrap(), - ( - "", - Attr { - name: "front".into(), - value: r#"/"#.into(), - } - ) - ); - } - - #[test] - fn backslash_control_chars() { - assert_eq!( - Attr::parse(r#"control:"\b\f\n\r\t""#).unwrap(), - ( - "", - Attr { - name: "control".into(), - value: "\x08\x0c\x0a\x0d\x09".into(), - } - ) - ); - } - - #[test] - fn url_attr() { - assert_eq!( - Attr::parse(r#"annotation_1585711454:"https:\/\/blog.tensorflow.org\/2020\/03\/""#) - .unwrap(), - ( - "", - Attr { - name: "annotation_1585711454".into(), - value: "https://blog.tensorflow.org/2020/03/".into(), - } - ) - ); - } -} diff --git a/rust/cli/src/tdb2/test.data b/rust/cli/src/tdb2/test.data deleted file mode 100644 index f57b9101b..000000000 --- a/rust/cli/src/tdb2/test.data +++ /dev/null @@ -1,6 +0,0 @@ -[description:"snake 🐍" entry:"1641670385" modified:"1641670385" priority:"M" status:"pending" uuid:"f19086c2-1f8d-4a6c-9b8d-f94901fb8e62"] -[annotation_1585711454:"https:\/\/blog.tensorflow.org\/2020\/03\/face-and-hand-tracking-in-browser-with-mediapipe-and-tensorflowjs.html?linkId=83993617" description:"try facemesh" entry:"1585711451" modified:"1592947544" priority:"M" project:"lists" status:"pending" tags:"idea" tags_idea:"x" uuid:"ee855dc7-6f61-408c-bc95-ebb52f7d529c"] -[description:"testing" entry:"1554074416" modified:"1554074416" priority:"M" status:"pending" uuid:"4578fb67-359b-4483-afe4-fef15925ccd6"] -[description:"testing2" entry:"1576352411" modified:"1576352411" priority:"M" status:"pending" uuid:"f5982cca-2ea1-4bfd-832c-9bd571dc0743"] -[description:"new-task" entry:"1576352696" modified:"1576352696" priority:"M" status:"pending" uuid:"cfee3170-f153-4075-aa1d-e20bcac2841b"] -[description:"foo" entry:"1579398776" modified:"1579398776" priority:"M" status:"pending" uuid:"df74ea94-5122-44fa-965a-637412fbbffc"] diff --git a/rust/cli/src/usage.rs b/rust/cli/src/usage.rs deleted file mode 100644 index d341c3b52..000000000 --- a/rust/cli/src/usage.rs +++ /dev/null @@ -1,312 +0,0 @@ -//! This module handles creation of CLI usage documents (--help, manpages, etc.) in -//! a way that puts the source of that documentation near its implementation. - -use crate::argparse; -use crate::settings; -use anyhow::Result; -use std::io::Write; - -#[cfg(feature = "usage-docs")] -use std::fmt::Write as FmtWrite; - -/// A top-level structure containing usage/help information for the entire CLI. -#[derive(Debug, Default)] -pub struct Usage { - pub(crate) subcommands: Vec, - pub(crate) filters: Vec, - pub(crate) modifications: Vec, - pub(crate) report_properties: Vec, -} - -impl Usage { - /// Get a new, completely-filled-out usage object - pub fn new() -> Self { - let mut rv = Self { - ..Default::default() - }; - - argparse::get_usage(&mut rv); - settings::get_usage(&mut rv); - - rv - } - - /// Write this usage to the given output as a help string, writing a short version if `summary` - /// is true. - pub(crate) fn write_help( - &self, - mut w: W, - command_name: &str, - summary: bool, - ) -> Result<()> { - write!( - w, - "TaskChampion {}: Personal task-tracking\n\n", - env!("CARGO_PKG_VERSION") - )?; - writeln!(w, "USAGE:\n {} [args]\n", command_name)?; - writeln!(w, "TaskChampion subcommands:")?; - for subcommand in self.subcommands.iter() { - subcommand.write_help(&mut w, command_name, summary)?; - } - writeln!(w, "Filter Expressions:\n")?; - writeln!( - w, - "{}", - indented( - " - Where [filter] appears above, zero or more of the following arguments can be used - to limit the tasks addressed by the subcommand.", - "" - ) - )?; - for filter in self.filters.iter() { - filter.write_help(&mut w, command_name, summary)?; - } - writeln!(w, "Modifications:\n")?; - writeln!( - w, - "{}", - indented( - " - Where [modification] appears above, zero or more of the following arguments can be - used to modify the selected tasks.", - "" - ) - )?; - for modification in self.modifications.iter() { - modification.write_help(&mut w, command_name, summary)?; - } - if !summary { - writeln!(w, "\nSee `{} help` for more detail", command_name)?; - } - Ok(()) - } - - #[cfg(feature = "usage-docs")] - /// Substitute strings matching - /// - /// ```text - /// - /// ``` - /// - /// With the appropriate documentation. - pub fn substitute_docs(&self, content: &str) -> Result { - // this is not efficient, but it doesn't need to be - let lines = content.lines(); - let mut w = String::new(); - - const DOC_HEADER_PREFIX: &str = ""; - - for line in lines { - if line.starts_with(DOC_HEADER_PREFIX) && line.ends_with(DOC_HEADER_SUFFIX) { - let doc_type = &line[DOC_HEADER_PREFIX.len()..line.len() - DOC_HEADER_SUFFIX.len()]; - - match doc_type { - "subcommands" => { - for subcommand in self.subcommands.iter() { - subcommand.write_markdown(&mut w)?; - } - } - "filters" => { - for filter in self.filters.iter() { - filter.write_markdown(&mut w)?; - } - } - "modifications" => { - for modification in self.modifications.iter() { - modification.write_markdown(&mut w)?; - } - } - "report-columns" => { - for prop in self.report_properties.iter() { - prop.write_column_markdown(&mut w)?; - } - } - "report-sort-by" => { - for prop in self.report_properties.iter() { - prop.write_sort_by_markdown(&mut w)?; - } - } - _ => anyhow::bail!("Unkonwn doc type {}", doc_type), - } - } else { - writeln!(w, "{}", line)?; - } - } - - Ok(w) - } -} - -/// wrap an indented string -fn indented(string: &str, indent: &str) -> String { - let termwidth = textwrap::termwidth(); - let words: Vec<&str> = string.split_whitespace().collect(); - let string = words.join(" "); - textwrap::indent( - textwrap::fill(string.trim(), termwidth - indent.len()).as_ref(), - indent, - ) -} - -/// Usage documentation for a subcommand -#[derive(Debug, Default)] -pub(crate) struct Subcommand { - /// Name of the subcommand - pub(crate) name: &'static str, - - /// Syntax summary, without command_name - pub(crate) syntax: &'static str, - - /// One-line description of the subcommand. Use an initial capital and no trailing period. - pub(crate) summary: &'static str, - - /// Multi-line description of the subcommand. It's OK for this to duplicate summary, as the - /// two are not displayed together. - pub(crate) description: &'static str, -} - -impl Subcommand { - fn write_help(&self, mut w: W, command_name: &str, summary: bool) -> Result<()> { - if summary { - writeln!(w, " {} {} - {}", command_name, self.name, self.summary)?; - } else { - writeln!( - w, - " {} {}\n{}", - command_name, - self.syntax, - indented(self.description, " ") - )?; - } - Ok(()) - } - - #[cfg(feature = "usage-docs")] - fn write_markdown(&self, mut w: W) -> Result<()> { - writeln!(w, "### `ta {}` - {}", self.name, self.summary)?; - writeln!(w, "```shell\nta {}\n```", self.syntax)?; - writeln!(w, "{}", indented(self.description, ""))?; - writeln!(w)?; - Ok(()) - } -} - -/// Usage documentation for a filter argument -#[derive(Debug, Default)] -pub(crate) struct Filter { - /// Syntax summary - pub(crate) syntax: &'static str, - - /// One-line description of the filter. Use all-caps words for placeholders. - pub(crate) summary: &'static str, - - /// Multi-line description of the filter. It's OK for this to duplicate summary, as the - /// two are not displayed together. - pub(crate) description: &'static str, -} - -impl Filter { - fn write_help(&self, mut w: W, _: &str, summary: bool) -> Result<()> { - if summary { - writeln!(w, " {} - {}", self.syntax, self.summary)?; - } else { - write!( - w, - " {}\n{}\n", - self.syntax, - indented(self.description, " ") - )?; - } - Ok(()) - } - - #[cfg(feature = "usage-docs")] - fn write_markdown(&self, mut w: W) -> Result<()> { - writeln!(w, "* `{}` - {}", self.syntax, self.summary)?; - writeln!(w)?; - writeln!(w, "{}", indented(self.description, " "))?; - writeln!(w)?; - Ok(()) - } -} - -/// Usage documentation for a modification argument -#[derive(Debug, Default)] -pub(crate) struct Modification { - /// Syntax summary - pub(crate) syntax: &'static str, - - /// One-line description of the modification. Use all-caps words for placeholders. - pub(crate) summary: &'static str, - - /// Multi-line description of the modification. It's OK for this to duplicate summary, as the - /// two are not displayed together. - pub(crate) description: &'static str, -} - -impl Modification { - fn write_help(&self, mut w: W, _: &str, summary: bool) -> Result<()> { - if summary { - writeln!(w, " {} - {}", self.syntax, self.summary)?; - } else { - writeln!( - w, - " {}\n{}", - self.syntax, - indented(self.description, " ") - )?; - } - Ok(()) - } - - #[cfg(feature = "usage-docs")] - fn write_markdown(&self, mut w: W) -> Result<()> { - writeln!(w, "* `{}` - {}", self.syntax, self.summary)?; - writeln!(w)?; - writeln!(w, "{}", indented(self.description, " "))?; - writeln!(w)?; - Ok(()) - } -} - -/// Usage documentation for a report property (which may be used for sorting, as a column, or -/// both). -#[allow(dead_code)] -#[derive(Debug, Default)] -pub(crate) struct ReportProperty { - /// Name of the property - pub(crate) name: &'static str, - - /// Usage description for sorting, if any - pub(crate) as_sort_by: Option<&'static str>, - - /// Usage description as a column, if any - pub(crate) as_column: Option<&'static str>, -} - -impl ReportProperty { - #[cfg(feature = "usage-docs")] - fn write_sort_by_markdown(&self, mut w: W) -> Result<()> { - if let Some(as_sort_by) = self.as_sort_by { - writeln!(w, "* `{}`", self.name)?; - writeln!(w)?; - writeln!(w, "{}", indented(as_sort_by, " "))?; - writeln!(w)?; - } - Ok(()) - } - - #[cfg(feature = "usage-docs")] - fn write_column_markdown(&self, mut w: W) -> Result<()> { - if let Some(as_column) = self.as_column { - writeln!(w, "* `{}`", self.name)?; - writeln!(w)?; - writeln!(w, "{}", indented(as_column, " "))?; - writeln!(w)?; - } - Ok(()) - } -} diff --git a/rust/cli/tests/cli.rs b/rust/cli/tests/cli.rs deleted file mode 100644 index 6325a6d3e..000000000 --- a/rust/cli/tests/cli.rs +++ /dev/null @@ -1,63 +0,0 @@ -use assert_cmd::prelude::*; -use predicates::prelude::*; -use std::fs; -use std::process::Command; -use tempfile::TempDir; - -// NOTE: This tests that the `ta` binary is running and parsing arguments. The details of -// subcommands are handled with unit tests. - -/// These tests force config to be read via TASKCHAMPION_CONFIG so that a user's own config file -/// (in their homedir) does not interfere with tests. -fn test_cmd(dir: &TempDir) -> Result> { - let config_filename = dir.path().join("config.toml"); - fs::write( - config_filename.clone(), - format!("data_dir = {:?}", dir.path()), - )?; - - let config_filename = config_filename.to_str().unwrap(); - let mut cmd = Command::cargo_bin("ta")?; - cmd.env("TASKCHAMPION_CONFIG", config_filename); - Ok(cmd) -} - -#[test] -fn help() -> Result<(), Box> { - let dir = TempDir::new().unwrap(); - let mut cmd = test_cmd(&dir)?; - - cmd.arg("--help"); - cmd.assert() - .success() - .stdout(predicate::str::contains("Personal task-tracking")); - - Ok(()) -} - -#[test] -fn version() -> Result<(), Box> { - let dir = TempDir::new().unwrap(); - let mut cmd = test_cmd(&dir)?; - - cmd.arg("--version"); - cmd.assert() - .success() - .stdout(predicate::str::contains("TaskChampion")); - - Ok(()) -} - -#[test] -fn invalid_option() -> Result<(), Box> { - let dir = TempDir::new().unwrap(); - let mut cmd = test_cmd(&dir)?; - - cmd.arg("--no-such-option"); - cmd.assert() - .failure() - .stderr(predicate::str::contains("command line not recognized")) - .code(predicate::eq(3)); - - Ok(()) -}