basic feature set

This commit is contained in:
aaron-jack-manning 2022-08-20 14:01:43 +10:00
commit ef7103043c
11 changed files with 1456 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

707
Cargo.lock generated Normal file
View File

@ -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"

15
Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "toru"
version = "0.1.0"
edition = "2021"
authors = ["Aaron Manning <contact@aaronmanning.net>"]
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"

30
README.md Normal file
View File

@ -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<String, Vec<Id>>`
- 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

31
src/colour.rs Normal file
View File

@ -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()
}

103
src/config.rs Normal file
View File

@ -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::<Config>("toru", self)?)
}
pub fn load() -> Result<Config, error::Error> {
Ok(confy::load::<Config>("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<path::PathBuf, error::Error> {
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 <NAME> <PATH>"));
}
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!();
}
}
}
}

57
src/error.rs Normal file
View File

@ -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<io::Error> for Error {
fn from(err : io::Error) -> Self {
Error::Io(err)
}
}
impl From<confy::ConfyError> for Error {
fn from(err : confy::ConfyError) -> Self {
Error::Confy(err)
}
}
impl From<trash::Error> for Error {
fn from(err : trash::Error) -> Self {
Error::Trash(err)
}
}
impl From<toml::de::Error> for Error {
fn from(err : toml::de::Error) -> Self {
Error::TomlDe(err)
}
}
impl From<toml::ser::Error> for Error {
fn from(err : toml::ser::Error) -> Self {
Error::TomlSer(err)
}
}

164
src/main.rs Normal file
View File

@ -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<String>,
#[clap(short, long)]
tags : Vec<String>,
#[clap(short, long)]
dependencies : Vec<tasks::Id>,
#[clap(short, long, value_enum)]
priority : Option<tasks::Priority>,
},
/// 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(())
}

86
src/state.rs Normal file
View File

@ -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<Self, error::Error> {
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::<InternalState>(&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::<Id>().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(())
}
}

157
src/tasks.rs Normal file
View File

@ -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<String>,
pub tags : HashSet<String>,
pub dependencies : HashSet<Id>,
pub priority : Priority,
//due : Option<chrono::NaiveDateTime>,
pub time_entries : Vec<TimeEntry>,
pub created : chrono::NaiveDateTime,
pub complete : bool,
pub discarded : bool,
}
impl Task {
pub fn new(name : String, info : Option<String>, tags : Vec<String>, dependencies : Vec<Id>, priority : Option<Priority>, vault_folder : &path::Path, state : &mut state::State) -> Result<Self, error::Error> {
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<Self, error::Error> {
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<path::PathBuf, error::Error> {
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(())
}
}

105
src/vault.rs Normal file
View File

@ -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(())
}