From ef7103043c92b30e74a7a290d5bf782ae26eddf1 Mon Sep 17 00:00:00 2001 From: aaron-jack-manning Date: Sat, 20 Aug 2022 14:01:43 +1000 Subject: [PATCH] basic feature set --- .gitignore | 1 + Cargo.lock | 707 ++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 15 ++ README.md | 30 +++ src/colour.rs | 31 +++ src/config.rs | 103 ++++++++ src/error.rs | 57 ++++ src/main.rs | 164 ++++++++++++ src/state.rs | 86 ++++++ src/tasks.rs | 157 +++++++++++ src/vault.rs | 105 ++++++++ 11 files changed, 1456 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/colour.rs create mode 100644 src/config.rs create mode 100644 src/error.rs create mode 100644 src/main.rs create mode 100644 src/state.rs create mode 100644 src/tasks.rs create mode 100644 src/vault.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3eaf6a7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,707 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "android_system_properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e" +dependencies = [ + "libc", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "serde", + "time", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "clap" +version = "3.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29e724a68d9319343bb3328c9cc2dfde263f4b3142ee1059a9980580171c954b" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13547f7012c01ab4a0e8f8967730ada8f9fdf419e8b6c792788f39cf4e46eefa" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "colored" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +dependencies = [ + "atty", + "lazy_static", + "winapi", +] + +[[package]] +name = "confy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2913470204e9e8498a0f31f17f90a0de801ae92c8c5ac18c49af4819e6786697" +dependencies = [ + "directories", + "serde", + "toml", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "directories" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551a778172a450d7fc12e629ca3b0428d00f6afa9a43da1b630d54604e97371c" +dependencies = [ + "cfg-if 0.1.10", + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "js-sys" +version = "0.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "once_cell" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" + +[[package]] +name = "os_str_bytes" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e8e5d5b70924f74ff5c6d64d9a5acd91422117c60f48c4e07855238a254553" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d8e8de557aee63c26b85b947f5e59b690d0454c753f3adeb5cd7835ab88391" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + +[[package]] +name = "thiserror" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "toru" +version = "0.1.0" +dependencies = [ + "chrono", + "clap", + "colored", + "confy", + "serde", + "toml", + "trash", +] + +[[package]] +name = "trash" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe090367848cd40c4230ff3ce4e2ff6a2fd511c1e14ae047a4a4c37ef7965236" +dependencies = [ + "chrono", + "libc", + "log", + "objc", + "once_cell", + "scopeguard", + "url", + "windows", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" + +[[package]] +name = "unicode-normalization" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" + +[[package]] +name = "windows_i686_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" + +[[package]] +name = "windows_i686_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..313dddc --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "toru" +version = "0.1.0" +edition = "2021" +authors = ["Aaron Manning "] +description = "A command line task manager with time tracking." + +[dependencies] +chrono = { version = "0.4.22", features = ["serde"] } +clap = { version = "3.2.17", features = ["derive"] } +colored = "2.0.0" +confy = "0.4.0" +serde = { version = "1.0.143", features = ["derive"] } +toml = "0.5.9" +trash = "2.1.5" diff --git a/README.md b/README.md new file mode 100644 index 0000000..739d33e --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# Toru + +A (currently in development) to do app for the command line. + +## Planned Features and Changes: +- Viewing individual tasks in full (command: `view`) +- Options for editing additional config + - `config` + - `editor` subcommand for setting default text editor +- Editing individual tasks directly (command: `edit`) + - Create temporary file for the data + - Fork process to open the text editor + - Wait for process to return + - Open, read and then delete the temporary file + - Deserialize as a map so each value can be checked and useful errors reported +- Listing tasks in vault (command: `list`) + - Options for which field to order by, and how to order (ascending or descending) + - Options for which columns to include + - If no values given, read a set of defaults from a `list.toml` file, which can be edited from a similar command +- Git integration + - `toru git` should run the provided Git command directly at the root of the current vault +- Ability to view, edit, delete, etc. using name + - Have a file containing a serialized `HashMap>` + - Disallow numerical names and have command automatically identify if it is a name or Id + - Error on operation if two tasks exist with the same name +- Dependency tracker + - Store dependencies in a file and correctly update them upon creation and removal of notes + - Error if any circular dependencies are introduced + - Make sure dependencies written to file are only those that could be successfully created +- Automatically added recurring notes diff --git a/src/colour.rs b/src/colour.rs new file mode 100644 index 0000000..8c50182 --- /dev/null +++ b/src/colour.rs @@ -0,0 +1,31 @@ +use colored::Colorize; + +// Yellow +pub fn vault(text : &str) -> colored::ColoredString { + text.truecolor(243, 156, 18).bold() +} + +// Red +pub fn error(text : &str) -> colored::ColoredString { + text.truecolor(192, 57, 43).bold() +} + +// Purple +pub fn command(text : &str) -> colored::ColoredString { + text.truecolor(155, 89, 182).bold() +} + +// Green +pub fn task_name(text : &str) -> colored::ColoredString { + text.truecolor(39, 174, 96).bold() +} + +// Beige +pub fn file(text : &str) -> colored::ColoredString { + text.truecolor(255, 184, 184).bold() +} + +// Pink +pub fn id(text : &str) -> colored::ColoredString { + text.truecolor(232, 67, 147).bold() +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..2945e4c --- /dev/null +++ b/src/config.rs @@ -0,0 +1,103 @@ +use crate::error; + +use crate::colour; + +use std::path; + +#[derive(Debug, Default, serde::Serialize, serde::Deserialize)] +pub struct Config { + /// Paths for all vaults, ordered according to recent usage, with current at the front. + pub vaults : Vec<(String, path::PathBuf)>, +} + +impl Config { + pub fn current_vault(&self) -> Result<&(String, path::PathBuf), error::Error> { + self.vaults.get(0).ok_or_else(|| error::Error::Generic(String::from("The attempted operation requires a vault, none of which have been set up"))) + } + + pub fn save(self) -> Result<(), error::Error> { + Ok(confy::store::("toru", self)?) + } + + pub fn load() -> Result { + Ok(confy::load::("toru")?) + } + + pub fn contains_name(&self, name : &String) -> bool { + self.vaults.iter().any(|(n, _)| n == name) + } + + pub fn contains_path(&self, path : &path::PathBuf) -> bool { + self.vaults.iter().any(|(_, p)| p == path) + } + + /// Adds the vault to the configuration. + pub fn add(&mut self, name : String, path : path::PathBuf) { + debug_assert!(!self.contains_name(&name)); + debug_assert!(!self.contains_path(&path)); + + self.vaults.push((name, path)); + } + + pub fn remove(&mut self, name : &String) -> Result { + match self.vaults.iter().position(|(n, _)| n == name) { + Some(index) => { + let (_, path) = self.vaults.swap_remove(index); + Ok(path) + }, + None => { + Err(error::Error::Generic(format!("No vault by the name {} exists", colour::vault(name)))) + } + } + } + + pub fn switch(&mut self, name : &String) -> Result<(), error::Error> { + match self.vaults.iter().position(|(n, _)| n == name) { + Some(index) => { + self.vaults.swap(index, 0); + Ok(()) + }, + None => { + Err(error::Error::Generic(format!("No vault by the name {} exists", colour::vault(name)))) + } + } + } + + /// Lists all vaults to stdout. + pub fn list_vaults(&self) { + + let width = self.vaults.iter().fold(usize::MIN, |c, (n, _)| c.max(n.len())); + + if self.vaults.is_empty() { + println!("No vaults currently set up, try running: {}", colour::command("toru vault new ")); + } + else { + for (i, (name, path)) in self.vaults.iter().enumerate() { + + if i == 0 { + print!("* "); + } + else { + print!(" "); + } + + print!("{}", colour::vault(name)); + + let padding = width - name.len() + 1; + + for _ in 0..padding { + print!(" ") + } + + print!("{}", path.display()); + + println!(); + } + } + } +} + + + + + diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..e1c79ae --- /dev/null +++ b/src/error.rs @@ -0,0 +1,57 @@ +use crate::colour; + +use std::io; +use std::fmt; + +#[derive(Debug)] +pub enum Error { + Io(io::Error), + Confy(confy::ConfyError), + Trash(trash::Error), + TomlDe(toml::de::Error), + TomlSer(toml::ser::Error), + Generic(String), +} + +impl fmt::Display for Error { + fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Io(err) => write!(f, "{} {}", colour::error("Internal Error:"), err), + Error::Confy(err) => write!(f, "{} {}", colour::error("Internal Error:"), err), + Error::Trash(err) => write!(f, "{} {}", colour::error("Internal Error:"), err), + Error::TomlDe(err) => write!(f, "{} {}", colour::error("Internal Error:"), err), + Error::TomlSer(err) => write!(f, "{} {}", colour::error("Internal Error:"), err), + Error::Generic(message) => write!(f, "{}", message), + } + } +} + +impl From for Error { + fn from(err : io::Error) -> Self { + Error::Io(err) + } +} + +impl From for Error { + fn from(err : confy::ConfyError) -> Self { + Error::Confy(err) + } +} + +impl From for Error { + fn from(err : trash::Error) -> Self { + Error::Trash(err) + } +} + +impl From for Error { + fn from(err : toml::de::Error) -> Self { + Error::TomlDe(err) + } +} + +impl From for Error { + fn from(err : toml::ser::Error) -> Self { + Error::TomlSer(err) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..765db9c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,164 @@ +#![allow(dead_code, unused_variables)] + +mod vault; +mod error; +mod tasks; +mod state; +mod config; +mod colour; + +use std::path; + +#[derive(clap::Parser, Debug)] +struct Args { + #[clap(subcommand)] + command : Command, +} + +#[derive(clap::Subcommand, Debug)] +#[clap(version, about, author, global_setting = clap::AppSettings::DisableHelpSubcommand)] +enum Command { + /// Create a new task. + New { + #[clap(short, long)] + name : String, + #[clap(short, long)] + info : Option, + #[clap(short, long)] + tags : Vec, + #[clap(short, long)] + dependencies : Vec, + #[clap(short, long, value_enum)] + priority : Option, + }, + /// Delete a task completely. + Delete { + id : tasks::Id, + }, + /// Discard a task without deleting the underlying file. + Discard { + id : tasks::Id, + }, + /// Mark a task as complete. + Complete { + id : tasks::Id, + }, + /// Commands for interacting with vaults. + #[clap(subcommand)] + Vault(VaultCommand), +} + +#[derive(clap::Subcommand, Debug)] +enum VaultCommand { + /// Creates a new vault at the specified location of the given name. + New { + name : String, + path : path::PathBuf, + }, + /// Disconnects the specified vault from toru, without altering the files. + Disconnect { + name : String, + }, + /// Connects an existing fault to toru. + Connect { + name : String, + path : path::PathBuf, + }, + /// Deletes the specified vault along with all of its data. + Delete { + name : String, + }, + /// Lists all configured vaults. + List, + /// Switches to the specified vault. + Switch { + name : String, + }, +} + +fn main() { + let result = program(); + + match result { + Ok(()) => (), + Err(error::Error::Generic(message)) => { + println!("{} {}", colour::error("Error:"), message); + } + result => println!("{:?}", result), + } +} + +fn program() -> Result<(), error::Error> { + let command = { + use clap::Parser; + Args::parse().command + }; + + let mut config = config::Config::load()?; + + use Command::*; + match command { + Vault(command) => { + use VaultCommand::*; + match command { + New { name, path } => { + vault::new(name.clone(), path, &mut config)?; + println!("Created vault {}", colour::vault(&name)); + }, + Disconnect { name } => { + vault::disconnect(&name, &mut config)?; + println!("Disconnected vault {}", colour::vault(&name)); + }, + Connect { name , path } => { + vault::connect(name.clone(), path, &mut config)?; + println!("Connected vault {}", colour::vault(&name)); + }, + Delete { name } => { + vault::delete(&name, &mut config)?; + println!("Deleted vault {}", colour::vault(&name)); + }, + List => { + config.list_vaults(); + }, + Switch { name } => { + config.switch(&name)?; + println!("Switched to vault {}", colour::vault(&name)); + }, + } + } + command => { + let vault_folder = &config.current_vault()?.1; + let mut state = state::State::load(vault_folder)?; + + match command { + New { name, info, tags, dependencies, priority } => { + let task = tasks::Task::new(name, info, tags, dependencies, priority, vault_folder, &mut state)?; + println!("Created task {}", colour::task_name(&task.data.name)); + }, + Delete { id } => { + tasks::Task::delete_by_id(id, vault_folder)?; + println!("Deleted task {}", colour::id(&id.to_string())); + } + Discard { id } => { + let mut task = tasks::Task::load(id, vault_folder.clone(), false)?; + task.data.discarded = true; + task.save()?; + println!("Discarded task {}", colour::id(&id.to_string())); + }, + Complete { id } => { + let mut task = tasks::Task::load(id, vault_folder.clone(), false)?; + task.data.complete = true; + task.save()?; + println!("Marked task {} as complete", colour::id(&id.to_string())); + }, + Vault(_) => unreachable!(), + } + + state.save()?; + } + } + + config.save()?; + + Ok(()) +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..19bc20b --- /dev/null +++ b/src/state.rs @@ -0,0 +1,86 @@ +use std::fs; +use std::path; +use std::io; +use std::io::{Write, Seek}; + +use crate::error; +use crate::tasks::Id; + +pub struct State { + file : fs::File, + pub data : InternalState, +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct InternalState { + pub next_id : Id, +} + +impl State { + /// This function should be called after creating or checking that the "notes" folder exists. + pub fn load(vault_location : &path::Path) -> Result { + let path = vault_location.join("state.toml"); + + if path.exists() && path.is_file() { + // Read file before opening (and truncating). + let contents = fs::read_to_string(&path)?; + + let file = fs::File::options() + .write(true) + .create(true) + .open(&path)?; + + let data = toml::from_str::(&contents)?; + + Ok(Self { + file, + data, + }) + } + else { + + let mut max_id : i128 = -1; + + for id in vault_location.join("notes").read_dir()?.filter_map(|p| p.ok()).map(|p| p.path()).filter(|p| p.extension().map(|s| s.to_str()) == Some(Some("toml"))).filter_map(|p| p.file_stem().map(|x| x.to_str().map(|y| y.to_string()))).flatten().filter_map(|p| p.parse::().ok()) { + + if i128::try_from(id).unwrap() > max_id { + max_id = i128::from(id); + } + } + + let data = InternalState { + next_id : u64::try_from(max_id + 1).unwrap(), + }; + + let mut file = fs::File::options() + .write(true) + .create(true) + .open(&path)?; + + file.set_len(0)?; + file.seek(io::SeekFrom::Start(0))?; + file.write_all(toml::to_string(&data)?.as_bytes())?; + + let task = Self { + file, + data, + }; + + Ok(task) + } + } + + pub fn save(self) -> Result<(), error::Error> { + + let Self { + mut file, + data, + } = self; + + file.set_len(0)?; + file.seek(io::SeekFrom::Start(0))?; + file.write_all(toml::to_string(&data)?.as_bytes())?; + + Ok(()) + } +} diff --git a/src/tasks.rs b/src/tasks.rs new file mode 100644 index 0000000..06b00da --- /dev/null +++ b/src/tasks.rs @@ -0,0 +1,157 @@ +use crate::error; +use crate::state; +use crate::colour; + +use std::fs; +use std::mem; +use std::path; +use std::io; +use std::io::{Write, Seek}; +use std::collections::HashSet; + +pub type Id = u64; + +pub struct Task { + path : path::PathBuf, + file : fs::File, + pub data : InternalTask, +} + +#[derive(Default, Debug, Clone, clap::ValueEnum, serde::Serialize, serde::Deserialize)] +pub enum Priority { + #[default] + Unspecified, + Low, + Medium, + High, +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct TimeEntry { + hours : u32, + minutes : u8, +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct InternalTask { + pub id : Id, + pub name : String, + pub info : Option, + pub tags : HashSet, + pub dependencies : HashSet, + pub priority : Priority, + //due : Option, + pub time_entries : Vec, + pub created : chrono::NaiveDateTime, + pub complete : bool, + pub discarded : bool, +} + +impl Task { + pub fn new(name : String, info : Option, tags : Vec, dependencies : Vec, priority : Option, vault_folder : &path::Path, state : &mut state::State) -> Result { + + let id = state.data.next_id; + state.data.next_id += 1; + + let path = vault_folder.join("notes").join(&format!("{}.toml", id)); + + let mut file = fs::File::options() + .write(true) + .create(true) + .open(&path)?; + + let data = InternalTask { + id, + name, + info, + tags : tags.into_iter().collect(), + dependencies : dependencies.into_iter().collect(), + priority : priority.unwrap_or_default(), + time_entries : Vec::new(), + created : chrono::Utc::now().naive_local(), + complete : false, + discarded : false, + }; + + + file.set_len(0)?; + file.seek(io::SeekFrom::Start(0))?; + file.write_all(toml::to_string(&data)?.as_bytes())?; + + Ok(Task { + path, + file, + data, + }) + } + + /// The read_only flag is so that the file will not be truncated, and therefore doesn't need to + /// be saved when finished. + pub fn load(id : Id, vault_folder : path::PathBuf, read_only : bool) -> Result { + let path = Task::check_exists(id, &vault_folder)?; + + let file_contents = fs::read_to_string(&path)?; + let file = if read_only { + fs::File::open(&path)? + } + else { + fs::File::options() + .write(true) + .create(true) + .open(&path)? + }; + + let data = toml::from_str(&file_contents)?; + + Ok(Self { + path, + file, + data, + }) + } + + pub fn check_exists(id : Id, vault_folder : &path::Path) -> Result { + let path = vault_folder.join("notes").join(format!("{}.toml", id)); + if path.exists() && path.is_file() { + Ok(path) + } + else { + Err(error::Error::Generic(format!("No task with the ID {} exists", colour::id(&id.to_string())))) + } + } + + pub fn save(self) -> Result<(), error::Error> { + let Self { + path, + mut file, + data, + } = self; + + file.set_len(0)?; + file.seek(io::SeekFrom::Start(0))?; + file.write_all(toml::to_string(&data)?.as_bytes())?; + + Ok(()) + } + + pub fn delete(self) -> Result<(), error::Error> { + let Self { + path, + file, + data, + } = self; + + mem::drop(file); + fs::remove_file(&path)?; + + Ok(()) + } + + pub fn delete_by_id(id : Id, vault_folder : &path::Path) -> Result<(), error::Error> { + let path = Task::check_exists(id, vault_folder)?; + fs::remove_file(&path)?; + Ok(()) + } +} + + diff --git a/src/vault.rs b/src/vault.rs new file mode 100644 index 0000000..429e197 --- /dev/null +++ b/src/vault.rs @@ -0,0 +1,105 @@ +use crate::error; +use crate::state; +use crate::colour; +use crate::config; + +use std::fs; +use std::path; + +pub fn new(name : String, path : path::PathBuf, config : &mut config::Config) -> Result<(), error::Error> { + + fn create_all_metadata(path : &path::Path) -> Result<(), error::Error> { + fs::create_dir(path.join("notes"))?; + let state = state::State::load(path)?; + //state.save()?; + + Ok(()) + } + + // Configuration already contains a vault by the given name. + if config.contains_name(&name) { + Err(error::Error::Generic(format!("A vault named \"{}\" already exists", name))) + } + else if config.contains_path(&path) { + Err(error::Error::Generic(format!("A vault at the path {:?} already exists", path))) + } + else { + // Folder exists and contains data. + if path.exists() && path.is_dir() && path.read_dir()?.next().is_some() { + Err(error::Error::Generic(String::from("The specified folder already exists and contains other data, please provide a path to a new or empty folder"))) + } + // Folder exists and is empty, so set up the vault metadata. + else if path.exists() && path.is_dir() { + + // Create the vault metadata. + create_all_metadata(&path)?; + + config.add(name, path); + + Ok(()) + } + // Provided path is to a file, not a directory. + else if path.exists() { + Err(error::Error::Generic(String::from("The specified path already points to a file, please provide a path to a new or empty folder"))) + } + // Path does not yet exist, and should be created. + else { + fs::create_dir_all(&path)?; + + // Create the vault metadata. + create_all_metadata(&path)?; + + config.add(name, path); + + Ok(()) + } + } +} + +pub fn connect(name : String, path : path::PathBuf, config : &mut config::Config) -> Result<(), error::Error> { + // Configuration already contains a vault by the given name. + if config.contains_name(&name) { + Err(error::Error::Generic(format!("A vault named \"{}\" already exists", name))) + } + else if config.contains_path(&path) { + Err(error::Error::Generic(format!("A vault at the path {:?} is already set up", path))) + } + else { + // Folder exists and contains data. + if path.exists() && path.is_dir() { + // Vault is missing required metadata files. + if !path.join("notes").exists() { + Err(error::Error::Generic(format!("Cannot connect the vault as it is missing the {} folder", colour::file("notes")))) + } + else if !path.join("state.toml").exists() { + Err(error::Error::Generic(format!("Cannot connect the vault as it is missing the {} file", colour::file("state.toml")))) + } + // Required metadata exists, so the vault is connected. + else { + config.add(name, path); + + Ok(()) + } + } + // Provided path is to a file, not a directory. + else if path.exists() { + Err(error::Error::Generic(String::from("The specified path points to a file, not a folder"))) + } + // Path does not yet exist. + else { + Err(error::Error::Generic(format!("The path {:?} does not exist", path))) + } + } +} + +pub fn disconnect(name : &String, config : &mut config::Config) -> Result<(), error::Error> { + config.remove(name)?; + Ok(()) +} + +pub fn delete(name : &String, config : &mut config::Config) -> Result<(), error::Error> { + let path = config.remove(name)?; + trash::delete(path)?; + Ok(()) +} +