Compare commits

...

9 Commits

Author SHA1 Message Date
xeruf 92b4be130c feat(tasks): interpret plain numbers as minutes and strip prefixes 2024-08-15 13:16:14 +03:00
xeruf 9c0a688297 fix(tasks): prevent tracking invalid times 2024-08-15 13:12:42 +03:00
xeruf fcd5e9c0c9 feat(tasks): employ time parsing libraries for tracking offsets 2024-08-15 12:21:32 +03:00
xeruf 3e056eb2b6 feat: feedback when filter does not match 2024-08-15 10:33:52 +03:00
xeruf 930c6b9c38 fix: improve task filtering, especially with slash
- smart case
- substring match
- less movement needed
2024-08-15 10:33:06 +03:00
xeruf c93b2f2d91 feat(main): enhance prompt formatting 2024-08-15 09:31:49 +03:00
xeruf 0253b00c61 feat(tasks): display tracking since for current task 2024-08-14 22:12:43 +03:00
xeruf 957422f767 feat: localize nostr Timestamps consistently 2024-08-14 21:49:36 +03:00
xeruf 3eefbad6d5 feat: allow setting multiple tag filters at once 2024-08-14 19:42:58 +03:00
6 changed files with 705 additions and 113 deletions

524
Cargo.lock generated
View File

@ -129,6 +129,18 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "arrayref"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a"
[[package]]
name = "arrayvec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "async-trait"
version = "0.1.80"
@ -211,6 +223,12 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.7"
@ -235,6 +253,12 @@ version = "0.10.0-beta"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea"
[[package]]
name = "beef"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1"
[[package]]
name = "bip39"
version = "2.0.0"
@ -287,6 +311,29 @@ dependencies = [
"serde",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[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.10.4"
@ -344,6 +391,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "chacha20"
version = "0.9.1"
@ -382,6 +435,16 @@ dependencies = [
"windows-targets 0.52.5",
]
[[package]]
name = "chrono-english"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f73d909da7eb4a7d88c679c3f5a1bc09d965754e0adb2e7627426cef96a00d6f"
dependencies = [
"chrono",
"scanlex",
]
[[package]]
name = "cipher"
version = "0.4.4"
@ -393,6 +456,15 @@ dependencies = [
"zeroize",
]
[[package]]
name = "clipboard-win"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892"
dependencies = [
"error-code",
]
[[package]]
name = "colog"
version = "1.3.0"
@ -420,6 +492,12 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
@ -435,6 +513,12 @@ dependencies = [
"libc",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "crypto-common"
version = "0.1.6"
@ -463,12 +547,49 @@ dependencies = [
"subtle",
]
[[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",
]
[[package]]
name = "dirs"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
dependencies = [
"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 0.4.5",
"winapi",
]
[[package]]
name = "either"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
[[package]]
name = "endian-type"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
[[package]]
name = "env_filter"
version = "0.1.2"
@ -498,6 +619,33 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "error-code"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b"
[[package]]
name = "fd-lock"
version = "4.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947"
dependencies = [
"cfg-if",
"rustix",
"windows-sys 0.52.0",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -612,6 +760,17 @@ dependencies = [
"version_check",
]
[[package]]
name = "getrandom"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.2.14"
@ -621,7 +780,7 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
@ -680,6 +839,15 @@ dependencies = [
"digest",
]
[[package]]
name = "home"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "http"
version = "1.1.0"
@ -847,6 +1015,16 @@ dependencies = [
"web-sys",
]
[[package]]
name = "interim"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afd0f0bff60c0e845844b6ee665e07990541ef3b70d8cd21861cf85b69fbef4"
dependencies = [
"chrono",
"logos",
]
[[package]]
name = "ipnet"
version = "2.9.0"
@ -895,6 +1073,33 @@ version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.6.0",
"libc",
]
[[package]]
name = "linefeed"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28715d08e35c6c074f9ae6b2e6a2420bac75d050c66ecd669d7d5b98e2caa036"
dependencies = [
"dirs 1.0.5",
"mortal",
"winapi",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "lnurl-pay"
version = "0.5.0"
@ -913,6 +1118,39 @@ version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "logos"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff1ceb190eb9bdeecdd8f1ad6a71d6d632a50905948771718741b5461fb01e13"
dependencies = [
"logos-derive",
]
[[package]]
name = "logos-codegen"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90be66cb7bd40cb5cc2e9cfaf2d1133b04a3d93b72344267715010a466e0915a"
dependencies = [
"beef",
"fnv",
"lazy_static",
"proc-macro2",
"quote",
"regex-syntax",
"syn",
]
[[package]]
name = "logos-derive"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45154231e8e96586b39494029e58f12f8ffcb5ecf80333a603a13aa205ea8cbd"
dependencies = [
"logos-codegen",
]
[[package]]
name = "lru"
version = "0.12.3"
@ -934,6 +1172,12 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.7.2"
@ -950,22 +1194,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.48.0",
]
[[package]]
name = "mortal"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c624fa1b7aab6bd2aff6e9b18565cc0363b6d45cbcd7465c9ed5e3740ebf097"
dependencies = [
"bitflags 2.6.0",
"libc",
"nix 0.26.4",
"smallstr",
"terminfo",
"unicode-normalization",
"unicode-width",
"winapi",
]
[[package]]
name = "mostr"
version = "0.3.0"
dependencies = [
"chrono",
"chrono-english",
"colog",
"colored",
"env_logger",
"interim",
"itertools",
"linefeed",
"log",
"nostr-sdk",
"parse_datetime",
"regex",
"rustyline",
"tokio",
"xdg",
]
@ -976,6 +1241,48 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e664971378a3987224f7a0e10059782035e89899ae403718ee07de85bec42afe"
[[package]]
name = "nibble_vec"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
dependencies = [
"smallvec",
]
[[package]]
name = "nix"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
dependencies = [
"bitflags 1.3.2",
"cfg-if",
"libc",
]
[[package]]
name = "nix"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "nostr"
version = "0.33.0"
@ -989,7 +1296,7 @@ dependencies = [
"cbc",
"chacha20",
"chacha20poly1305",
"getrandom",
"getrandom 0.2.14",
"instant",
"js-sys",
"negentropy",
@ -1135,6 +1442,16 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "parse_datetime"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bbf4e25b13841080e018a1e666358adfe5e39b6d353f986ca5091c210b586a1"
dependencies = [
"chrono",
"regex",
]
[[package]]
name = "password-hash"
version = "0.5.0"
@ -1172,6 +1489,44 @@ dependencies = [
"rustc_version",
]
[[package]]
name = "phf"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_codegen"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a"
dependencies = [
"phf_generator",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
dependencies = [
"phf_shared",
"rand",
]
[[package]]
name = "phf_shared"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
dependencies = [
"siphasher",
]
[[package]]
name = "pin-project"
version = "1.1.5"
@ -1239,6 +1594,16 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "radix_trie"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd"
dependencies = [
"endian-type",
"nibble_vec",
]
[[package]]
name = "rand"
version = "0.8.5"
@ -1266,7 +1631,35 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
"getrandom 0.2.14",
]
[[package]]
name = "redox_syscall"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[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",
"rust-argon2",
]
[[package]]
name = "redox_users"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
dependencies = [
"getrandom 0.2.14",
"libredox",
"thiserror",
]
[[package]]
@ -1348,13 +1741,25 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [
"cc",
"cfg-if",
"getrandom",
"getrandom 0.2.14",
"libc",
"spin",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rust-argon2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
dependencies = [
"base64 0.13.1",
"blake2b_simd",
"constant_time_eq",
"crossbeam-utils",
]
[[package]]
name = "rustc-demangle"
version = "0.1.23"
@ -1370,6 +1775,19 @@ dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "0.38.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
dependencies = [
"bitflags 2.6.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
]
[[package]]
name = "rustls"
version = "0.22.4"
@ -1425,6 +1843,28 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rustyline"
version = "14.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"clipboard-win",
"fd-lock",
"home",
"libc",
"log",
"memchr",
"nix 0.28.0",
"radix_trie",
"unicode-segmentation",
"unicode-width",
"utf8parse",
"windows-sys 0.52.0",
]
[[package]]
name = "ryu"
version = "1.0.17"
@ -1440,6 +1880,12 @@ dependencies = [
"cipher",
]
[[package]]
name = "scanlex"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "088c5d71572124929ea7549a8ce98e1a6fd33d0a38367b09027b382e67c033db"
[[package]]
name = "scrypt"
version = "0.11.0"
@ -1551,6 +1997,12 @@ dependencies = [
"digest",
]
[[package]]
name = "siphasher"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]]
name = "slab"
version = "0.4.9"
@ -1560,6 +2012,15 @@ dependencies = [
"autocfg",
]
[[package]]
name = "smallstr"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e922794d168678729ffc7e07182721a14219c65814e66e91b839a272fe5ae4f"
dependencies = [
"smallvec",
]
[[package]]
name = "smallvec"
version = "1.13.2"
@ -1605,6 +2066,19 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "terminfo"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "666cd3a6681775d22b200409aad3b089c5b99fb11ecdd8a204d9d62f8148498f"
dependencies = [
"dirs 4.0.0",
"fnv",
"nom",
"phf",
"phf_codegen",
]
[[package]]
name = "thiserror"
version = "1.0.59"
@ -1831,6 +2305,18 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-width"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
[[package]]
name = "universal-hash"
version = "0.5.1"
@ -1886,6 +2372,12 @@ dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -1994,6 +2486,28 @@ dependencies = [
"rustls-pki-types",
]
[[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-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.52.0"

View File

@ -19,6 +19,13 @@ chrono = "0.4"
env_logger = "0.11"
colog = "1.3"
colored = "2.1"
nostr-sdk = "0.33"
parse_datetime = "0.5.0"
interim = { version = "0.1", features = ["chrono"] }
nostr-sdk = "0.33" # { git = "https://github.com/rust-nostr/nostr" }
tokio = { version = "1.37", features = ["rt", "rt-multi-thread", "macros"] }
regex = "1.10.5"
[dev-dependencies]
chrono-english = "0.1"
linefeed = "0.6"
rustyline = { version = "14.0", features = ["custom-bindings"] }

View File

@ -93,22 +93,23 @@ To stop time-tracking completely, simply move to the root of all tasks.
`TASK` creation syntax: `NAME: TAG1 TAG2 ...`
- `TASK` - create task (prefix with space if you want a task to start with a command character)
- `.` - clear filters and reload
- `.` - clear filters
- `.TASK`
+ activate task by id
+ match by task name prefix: if one or more tasks match, filter / activate (tries case-sensitive then case-insensitive)
+ no match: create & activate task
- `.2` - set view depth to `2`, which can be substituted for any number (how many subtask levels to show, default 1)
- `/[TEXT]` - like `.`, but never creates a task and filters beyond currently visible tasks
- `.2` - set view depth to the given number (how many subtask levels to show, default is 1)
- `/[TEXT]` - activate task or filter by smart-case substring match
- `||TASK` - create and activate a new task procedure (where subtasks automatically depend on the previously created task)
- `|[TASK]` - (un)mark current task as procedure or create a sibling task depending on the current one and move up
Dots and slashes can be repeated to move to parent tasks.
Dot or slash can be repeated to move to parent tasks before acting.
- `:[IND][PROP]` - add property column PROP at IND or end, if it already exists remove property column PROP or IND (1-indexed)
- `::[PROP]` - Sort by property PROP (multiple space-separated values allowed)
- `([TIME]` - insert timetracking with the specified offset in minutes (empty: list tracked times)
- `)[TIME]` - stop timetracking with the specified offset in minutes - convenience helper to move to root (empty: stop now)
- `([TIME]` - insert timetracking with the specified offset such as `-1d`, `-15 minutes` or `yesterday 17:20` (empty:
list tracked times)
- `)[TIME]` - stop timetracking with the specified offset - convenience helper to move to root (empty: stop now)
- `>[TEXT]` - complete active task and move up, with optional status description
- `<[TEXT]` - close active task and move up, with optional status description
- `!TEXT` - set status for current task from text and move up (empty to open)
@ -120,9 +121,9 @@ Dots and slashes can be repeated to move to parent tasks.
Property Filters:
- `#TAG` - set tag filter (empty: list all used tags)
- `#TAG1 TAG2` - set tag filter (empty: list all used tags)
- `+TAG` - add tag filter
- `-TAG` - remove tag filters
- `-TAG` - remove tag filters by prefix
- `?STATUS` - filter by status (type or description) - plain `?` to reset, `??` to show all
- `@AUTHOR` - filter by author (`@` for self, id prefix, name prefix)
- TBI: Filter by time

View File

@ -1,7 +1,10 @@
use std::fmt::Display;
use std::io::{stdin, stdout, Write};
use chrono::{Local, NaiveDateTime, TimeZone};
use chrono::LocalResult::Single;
use log::{debug, error, info, trace, warn};
use nostr_sdk::Timestamp;
pub fn some_non_empty(str: &str) -> Option<String> {
if str.is_empty() { None } else { Some(str.to_string()) }
@ -27,3 +30,39 @@ pub fn prompt(prompt: &str) -> Option<String> {
}
}
// For use in format strings but not possible, so need global find-replace
pub const MAX_TIMESTAMP_WIDTH: u8 = 15;
/// Format nostr Timestamp relative to local time with optional day specifier or full date if needed
pub fn relative_datetimestamp(stamp: &Timestamp) -> String {
match Local.timestamp_opt(stamp.as_u64() as i64, 0) {
Single(time) => {
let date = time.date_naive();
let prefix = match Local::now()
.date_naive()
.signed_duration_since(date)
.num_days()
{
-1 => "tomorrow ".into(),
0 => "".into(),
1 => "yesterday ".into(),
2..=6 => date.format("last %a ").to_string(),
_ => date.format("%y-%m-%d ").to_string(),
};
format!("{}{}", prefix, time.format("%H:%M"))
}
_ => stamp.to_human_datetime(),
}
}
pub fn local_datetimestamp(stamp: &Timestamp) -> String {
format_stamp(stamp, "%y-%m-%d %a %H:%M")
}
pub fn format_stamp(stamp: &Timestamp, format: &str) -> String {
match Local.timestamp_opt(stamp.as_u64() as i64, 0) {
Single(time) => time.format(format).to_string(),
_ => stamp.to_human_datetime(),
}
}

View File

@ -18,6 +18,7 @@ use env_logger::Builder;
use itertools::Itertools;
use log::{debug, error, info, LevelFilter, trace, warn};
use nostr_sdk::prelude::*;
use nostr_sdk::TagStandard::Hashtag;
use regex::Regex;
use xdg::BaseDirectories;
@ -303,13 +304,10 @@ async fn main() {
println!();
let tasks = selected_relay.as_ref().and_then(|url| relays.get(url)).unwrap_or(&local_tasks);
print!(
"{} {}) ",
selected_relay.as_ref().map_or("TEMP".to_string(), |url| url.to_string()).bright_black().italic(),
format!(
"{}{}",
tasks.get_task_path(tasks.get_position()),
tasks.get_prompt_suffix()
).bold()
"{} {}{}) ",
selected_relay.as_ref().map_or("TEMP".to_string(), |url| url.to_string()).bright_black(),
tasks.get_task_path(tasks.get_position()).bold(),
tasks.get_prompt_suffix().italic(),
);
stdout().flush().unwrap();
match lines.next() {
@ -383,7 +381,7 @@ async fn main() {
None => {
tasks.get_current_task().map_or_else(
|| info!("With a task selected, use ,NOTE to attach NOTE and , to list all its notes"),
|task| println!("{}", task.description_events().map(|e| format!("{} {}", e.created_at.to_human_datetime(), e.content)).join("\n")),
|task| println!("{}", task.description_events().map(|e| format!("{} {}", local_datetimestamp(&e.created_at), e.content)).join("\n")),
);
continue;
}
@ -408,13 +406,13 @@ async fn main() {
match arg {
None => match tasks.get_position() {
None => {
info!("Filtering for Procedures");
tasks.set_filter(
tasks.current_tasks().into_iter()
tasks.filtered_tasks(None)
.filter(|t| t.pure_state() == State::Procedure)
.map(|t| t.event.id)
.collect()
);
info!("Filtering for procedures");
}
Some(id) => {
tasks.set_state_for(id, "", State::Procedure);
@ -456,7 +454,7 @@ async fn main() {
Some('#') =>
match arg {
Some(arg) => tasks.set_tag(arg.to_string()),
Some(arg) => tasks.set_tags(arg.split_whitespace().map(|s| Hashtag(s.to_string()).into())),
None => {
println!("Hashtags of all known tasks:\n{}", tasks.all_hashtags().join(" "));
continue;
@ -501,8 +499,8 @@ async fn main() {
dots += 1;
pos = tasks.get_parent(pos).cloned();
}
let slice = input[dots..].trim();
let slice = input[dots..].trim();
if pos != tasks.get_position() || slice.is_empty() {
tasks.move_to(pos);
}
@ -524,21 +522,30 @@ async fn main() {
dots += 1;
pos = tasks.get_parent(pos).cloned();
}
let slice = &input[dots..].trim().to_ascii_lowercase();
let slice = input[dots..].trim();
if slice.is_empty() {
tasks.move_to(pos);
if dots > 1 {
info!("Moving up {} tasks", dots - 1)
}
} else if let Ok(depth) = slice.parse::<i8>() {
tasks.move_to(pos);
tasks.set_depth(depth);
} else {
let filtered = tasks
.children_of(pos)
.into_iter()
.filter_map(|child| tasks.get_by_id(&child))
.filter(|t| t.event.content.to_ascii_lowercase().starts_with(slice))
let mut transform: Box<dyn Fn(&str) -> String> = Box::new(|s: &str| s.to_string());
if slice.chars().find(|c| c.is_ascii_uppercase()).is_none() {
// Smart-case - case-sensitive if any uppercase char is entered
transform = Box::new(|s| s.to_ascii_lowercase());
}
let filtered = tasks.filtered_tasks(pos)
.filter(|t| {
transform(&t.event.content).contains(slice) || t.tags.iter().flatten().any(|tag|
tag.content().is_some_and(|s| transform(s).contains(slice))
)
})
.map(|t| t.event.id)
.collect::<Vec<_>>();
.collect_vec();
if filtered.len() == 1 {
tasks.move_to(filtered.into_iter().nth(0));
} else {

View File

@ -7,8 +7,7 @@ use std::str::FromStr;
use std::sync::mpsc::Sender;
use std::time::Duration;
use chrono::{DateTime, Local, TimeZone};
use chrono::LocalResult::Single;
use chrono::Local;
use colored::Colorize;
use itertools::Itertools;
use log::{debug, error, info, trace, warn};
@ -16,8 +15,8 @@ use nostr_sdk::{Event, EventBuilder, EventId, Keys, Kind, PublicKey, Tag, TagSta
use nostr_sdk::prelude::Marker;
use TagStandard::Hashtag;
use crate::{Events, EventSender, MostrMessage};
use crate::helpers::some_non_empty;
use crate::{EventSender, MostrMessage};
use crate::helpers::{format_stamp, local_datetimestamp, relative_datetimestamp, some_non_empty};
use crate::kinds::*;
use crate::task::{MARKER_DEPENDS, MARKER_PARENT, State, Task, TaskState};
@ -128,6 +127,7 @@ impl Tasks {
],
sorting: VecDeque::from([
"state".into(),
"hashtags".into(),
"rtime".into(),
"name".into(),
]),
@ -182,7 +182,7 @@ impl Tasks {
.map(|str| EventId::from_str(str).ok().map_or(str.to_string(), |id| self.get_task_title(&id)))
.join(" "));
if new != last {
full.push_str(&format!("{} {}\n", event.created_at.to_human_datetime(), new.as_ref().unwrap_or(&"---".to_string())));
full.push_str(&format!("{:>15} {}\n", relative_datetimestamp(&event.created_at), new.as_ref().unwrap_or(&"---".to_string())));
last = new;
}
}
@ -198,12 +198,21 @@ impl Tasks {
let mut vec = Vec::with_capacity(set.len() / 2);
let mut iter = timestamps(set.iter(), &ids).tuples();
while let Some(((start, _), (end, _))) = iter.next() {
vec.push(format!("{} - {} by {}", start.to_human_datetime(), end.to_human_datetime(), key))
vec.push(format!("{} - {} by {}",
local_datetimestamp(start),
// Only use full stamp when ambiguous (>1day)
if end.as_u64() - start.as_u64() > 86400 {
local_datetimestamp(end)
} else {
format_stamp(end, "%H:%M")
},
key))
}
iter.into_buffer().for_each(|(stamp, _)|
vec.push(format!("{} started by {}", stamp.to_human_datetime(), key)));
iter.into_buffer()
.for_each(|(stamp, _)|
vec.push(format!("{} started by {}", local_datetimestamp(stamp), key)));
vec
}).sorted_unstable()
}).sorted_unstable() // TODO sorting depends on timestamp format - needed to interleave different people
).join("\n")
}
}
@ -344,17 +353,9 @@ impl Tasks {
.map(|t| t.get_id())
}
pub(crate) fn current_tasks(&self) -> Vec<&Task> {
if self.depth == 0 {
return self.get_current_task().into_iter().collect();
}
let res: Vec<&Task> = self.resolve_tasks(self.view.iter());
if res.len() > 0 {
// Currently ignores filtered view when it matches nothing
return res;
}
pub(crate) fn filtered_tasks(&self, position: Option<EventId>) -> impl Iterator<Item=&Task> {
// TODO use ChildrenIterator
self.resolve_tasks(self.children_of(self.position)).into_iter()
self.resolve_tasks(self.children_of(position)).into_iter()
.filter(|t| {
// TODO apply filters in transit
self.state.matches(t) &&
@ -367,36 +368,39 @@ impl Tasks {
self.tags.iter().all(|tag| iter.any(|t| t == tag))
}))
})
.collect()
}
pub(crate) fn visible_tasks(&self) -> Vec<&Task> {
if self.depth == 0 {
return self.get_current_task().into_iter().collect();
}
if self.view.len() > 0 {
return self.resolve_tasks(self.view.iter());
}
self.filtered_tasks(self.position).collect()
}
pub(crate) fn print_tasks(&self) -> Result<(), Error> {
let mut lock = stdout().lock();
if let Some(t) = self.get_current_task() {
let state = t.state_or_default();
let now = &Timestamp::now();
let mut tracking_stamp: Option<Timestamp> = None;
for elem in
timestamps(self.history.get(&self.sender.pubkey()).into_iter().flatten(), &vec![t.get_id()])
.map(|(e, o)| e) {
if tracking_stamp.is_some() && elem > now {
break;
}
tracking_stamp = Some(elem.clone())
}
writeln!(
lock,
"{} since {} (total tracked time {}m)",
// TODO tracking since, scheduled/planned for
"Tracking since {} (total tracked time {}m) - {} since {}",
tracking_stamp.map_or("?".to_string(), |t| relative_datetimestamp(&t)),
self.time_tracked(*t.get_id()) / 60,
state.get_label(),
match Local.timestamp_opt(state.time.as_u64() as i64, 0) {
Single(time) => {
let date = time.date_naive();
let prefix = match Local::now()
.date_naive()
.signed_duration_since(date)
.num_days()
{
0 => "".into(),
1 => "yesterday ".into(),
2..=6 => date.format("%a ").to_string(),
_ => date.format("%y-%m-%d ").to_string(),
};
format!("{}{}", prefix, time.format("%H:%M"))
}
_ => state.time.to_human_datetime(),
},
self.time_tracked(*t.get_id()) / 60
relative_datetimestamp(&state.time)
)?;
writeln!(lock, "{}", t.descriptions().join("\n"))?;
}
@ -404,7 +408,7 @@ impl Tasks {
// TODO hide empty columns
writeln!(lock, "{}", self.properties.join("\t").bold())?;
let mut total_time = 0;
let mut tasks = self.current_tasks();
let mut tasks = self.visible_tasks();
let count = tasks.len();
tasks.sort_by_cached_key(|task| {
self.sorting
@ -475,6 +479,9 @@ impl Tasks {
// Movement and Selection
pub(crate) fn set_filter(&mut self, view: Vec<EventId>) {
if view.is_empty() {
warn!("No match for filter!")
}
self.view = view;
}
@ -485,10 +492,10 @@ impl Tasks {
info!("Removed all filters");
}
pub(crate) fn set_tag(&mut self, tag: String) {
pub(crate) fn set_tags(&mut self, tags: impl IntoIterator<Item=Tag>) {
self.tags_excluded.clear();
self.tags.clear();
self.add_tag(tag);
self.tags.extend(tags);
}
pub(crate) fn add_tag(&mut self, tag: String) {
@ -530,11 +537,10 @@ impl Tasks {
if let Ok(id) = EventId::parse(arg) {
return vec![id];
}
let tasks = self.current_tasks();
let mut filtered: Vec<EventId> = Vec::with_capacity(tasks.len());
let mut filtered: Vec<EventId> = Vec::with_capacity(32);
let lowercase_arg = arg.to_ascii_lowercase();
let mut filtered_more: Vec<EventId> = Vec::with_capacity(tasks.len());
for task in tasks {
let mut filtered_more: Vec<EventId> = Vec::with_capacity(32);
for task in self.filtered_tasks(self.position) {
let lowercase = task.event.content.to_ascii_lowercase();
if lowercase == lowercase_arg {
return vec![task.event.id];
@ -561,7 +567,7 @@ impl Tasks {
if arg.len() > 2 {
Some(self.make_task(arg))
} else {
warn!("Not creating task under 3 chars to avoid silly mistakes");
warn!("Name of a task needs to have at least 3 characters");
None
}
}
@ -693,21 +699,38 @@ impl Tasks {
}
/// Parse string and set tracking
/// Returns false if parsing failed
/// Returns false and prints a message if parsing failed
pub(crate) fn track_from(&mut self, str: &str) -> bool {
if let Ok(num) = str.parse::<i64>() {
// Using two libraries for better exhaustiveness, see https://github.com/uutils/parse_datetime/issues/84
let stripped = str.trim().trim_start_matches('+').trim_start_matches("in ");
if let Ok(num) = stripped.parse::<i64>() {
self.track_at(Timestamp::from(Timestamp::now().as_u64().saturating_add_signed(num * 60)));
} else if let Ok(date) = DateTime::parse_from_rfc3339(str) {
self.track_at(Timestamp::from(date.to_utc().timestamp() as u64));
} else {
warn!("Cannot parse time from {str}");
return false;
return true
}
match interim::parse_date_string(stripped, Local::now(), interim::Dialect::Us) {
Ok(date) => Some(date.to_utc()),
Err(e) => {
match parse_datetime::parse_datetime_at_date(Local::now(), str) {
Ok(date) => Some(date.to_utc()),
Err(_) => {
warn!("Could not parse time from {str}: {e}");
None
}
}
}
}.filter(|time| {
if time.timestamp() > 0 {
self.track_at(Timestamp::from(time.timestamp() as u64));
true
} else {
warn!("Can only track times after 1970!");
false
}
}).is_some()
}
pub(crate) fn track_at(&mut self, time: Timestamp) -> EventId {
info!("{} from {}", self.position.map_or(String::from("Stopping time-tracking"), |id| format!("Tracking \"{}\"", self.get_task_title(&id))), time.to_human_datetime());
info!("{} from {}", self.position.map_or(String::from("Stopping time-tracking"), |id| format!("Tracking \"{}\"", self.get_task_title(&id))), relative_datetimestamp(&time));
let pos = self.get_position();
let tracking = build_tracking(pos);
// TODO this can lead to funny deletions
@ -921,6 +944,7 @@ fn matching_tag_id<'a>(event: &'a Event, ids: &'a Vec<&'a EventId>) -> Option<&'
})
}
/// Filters out event timestamps to those that start or stop one of the given events
fn timestamps<'a>(events: impl Iterator<Item=&'a Event>, ids: &'a Vec<&'a EventId>) -> impl Iterator<Item=(&Timestamp, Option<&EventId>)> {
events.map(|event| (&event.created_at, matching_tag_id(event, ids)))
.dedup_by(|(_, e1), (_, e2)| e1 == e2)
@ -1107,59 +1131,59 @@ mod tasks_test {
assert_eq!(tasks.depth, 1);
assert_eq!(task1.pure_state(), State::Open);
debug!("{:?}", tasks);
assert_eq!(tasks.current_tasks().len(), 1);
assert_eq!(tasks.visible_tasks().len(), 1);
tasks.depth = 0;
assert_eq!(tasks.current_tasks().len(), 0);
assert_eq!(tasks.visible_tasks().len(), 0);
tasks.move_to(Some(t1));
tasks.depth = 2;
assert_eq!(tasks.current_tasks().len(), 0);
assert_eq!(tasks.visible_tasks().len(), 0);
let t2 = tasks.make_task("t2");
assert_eq!(tasks.current_tasks().len(), 1);
assert_eq!(tasks.visible_tasks().len(), 1);
assert_eq!(tasks.get_task_path(Some(t2)), "t1>t2");
assert_eq!(tasks.relative_path(t2), "t2");
let t3 = tasks.make_task("t3");
assert_eq!(tasks.current_tasks().len(), 2);
assert_eq!(tasks.visible_tasks().len(), 2);
tasks.move_to(Some(t2));
assert_eq!(tasks.current_tasks().len(), 0);
assert_eq!(tasks.visible_tasks().len(), 0);
let t4 = tasks.make_task("t4");
assert_eq!(tasks.current_tasks().len(), 1);
assert_eq!(tasks.visible_tasks().len(), 1);
assert_eq!(tasks.get_task_path(Some(t4)), "t1>t2>t4");
assert_eq!(tasks.relative_path(t4), "t4");
tasks.depth = 2;
assert_eq!(tasks.current_tasks().len(), 1);
assert_eq!(tasks.visible_tasks().len(), 1);
tasks.depth = -1;
assert_eq!(tasks.current_tasks().len(), 1);
assert_eq!(tasks.visible_tasks().len(), 1);
tasks.move_to(Some(t1));
assert_eq!(tasks.relative_path(t4), "t2>t4");
assert_eq!(tasks.current_tasks().len(), 2);
assert_eq!(tasks.visible_tasks().len(), 2);
tasks.depth = 2;
assert_eq!(tasks.current_tasks().len(), 3);
assert_eq!(tasks.visible_tasks().len(), 3);
tasks.set_filter(vec![t2]);
assert_eq!(tasks.current_tasks().len(), 2);
assert_eq!(tasks.visible_tasks().len(), 2);
tasks.depth = 1;
assert_eq!(tasks.current_tasks().len(), 1);
assert_eq!(tasks.visible_tasks().len(), 1);
tasks.depth = -1;
assert_eq!(tasks.current_tasks().len(), 1);
assert_eq!(tasks.visible_tasks().len(), 1);
tasks.set_filter(vec![t2, t3]);
assert_eq!(tasks.current_tasks().len(), 2);
assert_eq!(tasks.visible_tasks().len(), 2);
tasks.depth = 2;
assert_eq!(tasks.current_tasks().len(), 3);
assert_eq!(tasks.visible_tasks().len(), 3);
tasks.depth = 1;
assert_eq!(tasks.current_tasks().len(), 2);
assert_eq!(tasks.visible_tasks().len(), 2);
tasks.move_to(None);
assert_eq!(tasks.current_tasks().len(), 1);
assert_eq!(tasks.visible_tasks().len(), 1);
tasks.depth = 2;
assert_eq!(tasks.current_tasks().len(), 3);
assert_eq!(tasks.visible_tasks().len(), 3);
tasks.depth = 3;
assert_eq!(tasks.current_tasks().len(), 4);
assert_eq!(tasks.visible_tasks().len(), 4);
tasks.depth = 9;
assert_eq!(tasks.current_tasks().len(), 4);
assert_eq!(tasks.visible_tasks().len(), 4);
tasks.depth = -1;
assert_eq!(tasks.current_tasks().len(), 2);
assert_eq!(tasks.visible_tasks().len(), 2);
}
#[test]