diff options
Diffstat (limited to '')
-rw-r--r-- | pkgs/by-name/ts/tskm/.envrc | 3 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/Cargo.lock | 355 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/Cargo.toml | 19 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/build.rs | 49 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/flake.lock | 6 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/flake.nix | 4 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/package.nix | 33 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/cli.rs | 166 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs | 51 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs | 9 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/interface/open/handle.rs | 115 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/main.rs | 55 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/state.rs | 45 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/task/mod.rs | 186 |
14 files changed, 809 insertions, 287 deletions
diff --git a/pkgs/by-name/ts/tskm/.envrc b/pkgs/by-name/ts/tskm/.envrc index d21a17fc..a83561cc 100644 --- a/pkgs/by-name/ts/tskm/.envrc +++ b/pkgs/by-name/ts/tskm/.envrc @@ -1,8 +1,5 @@ #!/usr/bin/env sh -SHELL_COMPLETION_DIR="$(pwd)/target/shell" -export SHELL_COMPLETION_DIR - export TSKM_PROJECT_FILE=/home/soispha/repos/nix/config/modules/common/projects.json use flake diff --git a/pkgs/by-name/ts/tskm/Cargo.lock b/pkgs/by-name/ts/tskm/Cargo.lock index 4a064858..9aa2cf2c 100644 --- a/pkgs/by-name/ts/tskm/Cargo.lock +++ b/pkgs/by-name/ts/tskm/Cargo.lock @@ -3,6 +3,24 @@ version = 4 [[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -69,9 +87,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "autocfg" @@ -92,10 +110,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] name = "cc" -version = "1.2.17" +version = "1.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" dependencies = [ "shlex", ] @@ -116,15 +140,16 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] [[package]] name = "clap" -version = "4.5.34" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff" +checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" dependencies = [ "clap_builder", "clap_derive", @@ -132,9 +157,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.34" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489" +checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" dependencies = [ "anstream", "anstyle", @@ -149,6 +174,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06f5378ea264ad4f82bbc826628b5aad714a75abf6ece087e923010eb937fb6" dependencies = [ "clap", + "clap_lex", + "is_executable", + "shlex", ] [[package]] @@ -182,6 +210,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -214,6 +251,28 @@ dependencies = [ ] [[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "flate2" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -224,13 +283,43 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown", ] [[package]] @@ -247,9 +336,9 @@ checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" [[package]] name = "iana-time-zone" -version = "0.1.62" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -420,6 +509,15 @@ dependencies = [ ] [[package]] +name = "is_executable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a1b5bad6f9072935961dfbf1cced2f3d129963d091b6f69f007fe04e758ae2" +dependencies = [ + "winapi", +] + +[[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -443,9 +541,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.171" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libredox" @@ -458,6 +556,16 @@ dependencies = [ ] [[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] name = "litemap" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -485,6 +593,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", +] + +[[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -512,10 +629,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -530,17 +653,37 @@ dependencies = [ ] [[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] name = "redox_users" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ - "getrandom", + "getrandom 0.2.16", "libredox", "thiserror", ] [[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] name = "rustversion" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -601,9 +744,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "stable_deref_trait" @@ -637,6 +780,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] +name = "strum" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" + +[[package]] +name = "strum_macros" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] name = "syn" version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -659,6 +821,26 @@ dependencies = [ ] [[package]] +name = "taskchampion" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b010f5ebe51e88ae490691ed2a43b699e3468c8e3838e244accd8526aca7751b" +dependencies = [ + "anyhow", + "byteorder", + "chrono", + "flate2", + "log", + "rusqlite", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror", + "uuid", +] + +[[package]] name = "termcolor" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -720,6 +902,7 @@ dependencies = [ "serde", "serde_json", "stderrlog", + "taskchampion", "url", "walkdir", ] @@ -771,6 +954,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.2", + "serde", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -787,6 +992,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] name = "wasm-bindgen" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -845,6 +1059,22 @@ dependencies = [ ] [[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.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -854,12 +1084,44 @@ dependencies = [ ] [[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" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ - "windows-targets", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -869,6 +1131,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + +[[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -942,6 +1222,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] name = "write16" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -978,6 +1267,26 @@ dependencies = [ ] [[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/pkgs/by-name/ts/tskm/Cargo.toml b/pkgs/by-name/ts/tskm/Cargo.toml index d2990b0c..05cd06da 100644 --- a/pkgs/by-name/ts/tskm/Cargo.toml +++ b/pkgs/by-name/ts/tskm/Cargo.toml @@ -6,14 +6,16 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0.97" -clap = { version = "4.5.34", features = ["derive"] } +anyhow = "1.0.98" +clap = { version = "4.5.37", features = ["derive"] } +clap_complete = { version = "4.5.47", features = ["unstable-dynamic"] } dirs = "6.0.0" log = "0.4.27" lz4_flex = "0.11.3" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" stderrlog = "0.6.0" +taskchampion = { version = "2.0.3", default-features = false } url = { version = "2.5.4", features = ["serde"] } walkdir = "2.5.0" @@ -74,14 +76,5 @@ style = { level = "warn", priority = -1 } complexity = { level = "warn", priority = -1 } perf = { level = "warn", priority = -1 } pedantic = { level = "warn", priority = -1 } - -[build-dependencies] -anyhow = "1.0.97" -clap = { version = "4.5.34", features = ["derive"] } -clap_complete = "4.5.47" -dirs = "6.0.0" -log = "0.4.27" -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.140" -url = "2.5.4" -walkdir = "2.5.0" +missing_panics_doc = "allow" +missing_errors_doc = "allow" diff --git a/pkgs/by-name/ts/tskm/build.rs b/pkgs/by-name/ts/tskm/build.rs deleted file mode 100644 index 8dfb213b..00000000 --- a/pkgs/by-name/ts/tskm/build.rs +++ /dev/null @@ -1,49 +0,0 @@ -use anyhow::{Context, Result}; -use clap::{CommandFactory, ValueEnum}; -use clap_complete::generate_to; -use clap_complete::Shell; - -use std::env; -use std::fs; -use std::path::PathBuf; - -use crate::cli::CliArgs; - -pub mod task { - include!("src/task/mod.rs"); -} - -pub mod interface { - pub mod input { - include!("src/interface/input/mod.rs"); - } - pub mod project { - include!("src/interface/project/mod.rs"); - } -} - -pub mod cli { - include!("src/cli.rs"); -} - -fn main() -> Result<()> { - let outdir = match env::var_os("SHELL_COMPLETION_DIR") { - None => return Ok(()), - Some(outdir) => outdir, - }; - - if !PathBuf::from(&outdir).exists() { - fs::create_dir_all(&outdir)?; - } - - let mut cmd = CliArgs::command(); - - for &shell in Shell::value_variants() { - let path = generate_to(shell, &mut cmd, "tskm", &outdir).with_context(|| { - format!("Failed to output shell completion for {shell} to {outdir:?}") - })?; - println!("cargo:warning=completion file for {shell} is generated at: {path:?}"); - } - - Ok(()) -} diff --git a/pkgs/by-name/ts/tskm/flake.lock b/pkgs/by-name/ts/tskm/flake.lock index 82448387..07eb6b21 100644 --- a/pkgs/by-name/ts/tskm/flake.lock +++ b/pkgs/by-name/ts/tskm/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1743076231, - "narHash": "sha256-yQugdVfi316qUfqzN8JMaA2vixl+45GxNm4oUfXlbgw=", + "lastModified": 1745377448, + "narHash": "sha256-jhZDfXVKdD7TSEGgzFJQvEEZ2K65UMiqW5YJ2aIqxMA=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6c5963357f3c1c840201eda129a99d455074db04", + "rev": "507b63021ada5fee621b6ca371c4fca9ca46f52c", "type": "github" }, "original": { diff --git a/pkgs/by-name/ts/tskm/flake.nix b/pkgs/by-name/ts/tskm/flake.nix index 4b1142d6..5a5f628b 100644 --- a/pkgs/by-name/ts/tskm/flake.nix +++ b/pkgs/by-name/ts/tskm/flake.nix @@ -10,6 +10,10 @@ pkgs = nixpkgs.legacyPackages."${system}"; in { devShells."${system}".default = pkgs.mkShell { + buildInputs = [ + pkgs.sqlite + ]; + packages = with pkgs; [ cargo clippy diff --git a/pkgs/by-name/ts/tskm/package.nix b/pkgs/by-name/ts/tskm/package.nix index a57dc4aa..71ef7ed6 100644 --- a/pkgs/by-name/ts/tskm/package.nix +++ b/pkgs/by-name/ts/tskm/package.nix @@ -1,8 +1,16 @@ { rustPlatform, installShellFiles, + makeWrapper, + lib, + # Dependencies + taskwarrior3, + git, + rofi, + firefox, + sqlite, }: -rustPlatform.buildRustPackage { +rustPlatform.buildRustPackage (finalAttrs: { pname = "tskm"; version = "0.1.0"; @@ -11,22 +19,31 @@ rustPlatform.buildRustPackage { lockFile = ./Cargo.lock; }; - env = { - SHELL_COMPLETION_DIR = "./shell"; - }; + buildInputs = [ + taskwarrior3 + git + rofi + firefox + sqlite + ]; nativeBuildInputs = [ installShellFiles + makeWrapper ]; postInstall = '' installShellCompletion --cmd tskm \ - --bash ./shell/tskm.bash \ - --fish ./shell/tskm.fish \ - --zsh ./shell/_tskm + --bash <(COMPLETE=bash $out/bin/tskm) \ + --fish <(COMPLETE=fish $out/bin/tskm) \ + --zsh <(COMPLETE=zsh $out/bin/tskm) + + # NOTE: We cannot clear the path, because we need access to the $EDITOR. <2025-04-04> + wrapProgram $out/bin/tskm \ + --prefix PATH : ${lib.makeBinPath finalAttrs.buildInputs} ''; meta = { mainProgram = "tskm"; }; -} +}) diff --git a/pkgs/by-name/ts/tskm/src/cli.rs b/pkgs/by-name/ts/tskm/src/cli.rs index bf0af7fb..c1eba387 100644 --- a/pkgs/by-name/ts/tskm/src/cli.rs +++ b/pkgs/by-name/ts/tskm/src/cli.rs @@ -1,10 +1,13 @@ -use std::path::PathBuf; +use std::{ffi::OsStr, path::PathBuf}; -use clap::{Parser, Subcommand}; +use anyhow::{bail, Result}; +use clap::{builder::StyledStr, ArgAction, Parser, Subcommand}; +use clap_complete::{ArgValueCompleter, CompletionCandidate}; +use url::Url; use crate::{ interface::{input::Input, project::ProjectName}, - task, + state, task, }; #[derive(Parser, Debug)] @@ -14,13 +17,23 @@ use crate::{ /// `tskm` effectively combines multiple applications together: /// - `taskwarrior` projects are raised connected to `firefox` profiles, making it possible to “open” /// a project. +/// /// - Every `taskwarrior` project has a determined `neorg` path, so that extra information for a /// `project` can be stored in this `norg` file. +/// /// - `tskm` can track inputs for you. These are URLs with optional tags which you can that /// “review” to open tasks based on them. pub struct CliArgs { #[command(subcommand)] pub command: Command, + + /// Increase message verbosity + #[arg(long="verbose", short = 'v', action = ArgAction::Count, default_value_t = 2)] + pub verbosity: u8, + + /// Silence all output + #[arg(long, short = 'q')] + pub quiet: bool, } #[derive(Subcommand, Debug)] @@ -66,7 +79,21 @@ pub enum ProjectCommand { #[derive(Subcommand, Debug, Clone, Copy)] pub enum NeorgCommand { /// Open the `neorg` project associated with id of the task. - Task { id: task::Id }, + Task { + /// The working set id of the task + #[arg(value_parser = task_from_working_set_id, add = ArgValueCompleter::new(complete_task_id))] + id: task::Task, + }, +} + +fn task_from_working_set_id(id: &str) -> Result<task::Task> { + let id: usize = id.parse()?; + let mut state = state::State::new_ro()?; + + let Some(task) = task::Task::from_working_set(id, &mut state)? else { + bail!("Working set id '{id}' is not valid!") + }; + Ok(task) } #[derive(Subcommand, Debug)] @@ -79,22 +106,28 @@ pub enum OpenCommand { /// Opens Firefox with either the supplied project or the currently active project profile. Project { /// The project to open. - #[arg(value_parser = task::Project::from_project_string)] - project: Option<task::Project>, + #[arg(value_parser = task::Project::from_project_string, add = ArgValueCompleter::new(complete_project))] + project: task::Project, + + /// The URL to open. + url: Option<Url>, }, /// Open a selected project in it's Firefox profile. /// /// This will use rofi's dmenu mode to select one project from the list of all registered /// projects. - Select, + Select { + /// The URL to open. + url: Option<Url>, + }, /// List all open tabs in the project. ListTabs { /// The project to open. - #[arg(value_parser = task::Project::from_project_string)] + #[arg(value_parser = task::Project::from_project_string, add = ArgValueCompleter::new(complete_project))] project: Option<task::Project>, - } + }, } #[derive(Subcommand, Debug)] @@ -102,7 +135,10 @@ pub enum InputCommand { /// Add URLs as inputs to be categorized. Add { inputs: Vec<Input> }, /// Remove URLs - Remove { inputs: Vec<Input> }, + Remove { + #[arg(add = ArgValueCompleter::new(complete_input_url))] + inputs: Vec<Input>, + }, /// Add all URLs in the file as inputs to be categorized. /// @@ -113,10 +149,118 @@ pub enum InputCommand { /// It takes a project in which to open the URLs. Review { /// Opens all the URLs in this project. - #[arg(value_parser = task::Project::from_project_string)] + #[arg(value_parser = task::Project::from_project_string, add = ArgValueCompleter::new(complete_project))] project: task::Project, }, /// List all the previously added inputs. List, } + +fn complete_task_id(current: &OsStr) -> Vec<CompletionCandidate> { + fn format_task( + task: task::Task, + current: &str, + state: &mut state::State, + ) -> Option<CompletionCandidate> { + let id = { + let Ok(base) = task.working_set_id(state) else { + return None; + }; + base.to_string() + }; + + if !id.starts_with(current) { + return None; + } + + let description = { + let Ok(base) = task.description(state) else { + return None; + }; + StyledStr::from(base) + }; + + Some(CompletionCandidate::new(id).help(Some(description))) + } + + let mut output = vec![]; + + let Some(current) = current.to_str() else { + return output; + }; + + let Ok(mut state) = state::State::new_ro() else { + return output; + }; + + let Ok(pending) = state.replica().pending_tasks() else { + return output; + }; + + let Ok(current_project) = task::Project::get_current() else { + return output; + }; + + if let Some(current_project) = current_project { + for t in pending { + let task = task::Task::from(&t); + if let Ok(project) = task.project(&mut state) { + if project == current_project { + if let Some(out) = format_task(task, current, &mut state) { + output.push(out); + } else { + continue; + } + } + } + } + } else { + for t in pending { + let task = task::Task::from(&t); + if let Some(out) = format_task(task, current, &mut state) { + output.push(out); + } + } + } + + output +} +fn complete_project(current: &OsStr) -> Vec<CompletionCandidate> { + let mut output = vec![]; + + let Some(current) = current.to_str() else { + return output; + }; + + let Ok(all) = task::Project::all() else { + return output; + }; + + for a in all { + if a.to_project_display().starts_with(current) { + output.push(CompletionCandidate::new(a.to_project_display())); + } + } + + output +} +fn complete_input_url(current: &OsStr) -> Vec<CompletionCandidate> { + let mut output = vec![]; + + let Some(current) = current.to_str() else { + return output; + }; + + let Ok(all) = Input::all() else { + return output; + }; + + for a in all { + if a.to_string().starts_with(current) { + output.push(CompletionCandidate::new(a.to_string())); + } + } + + output +} diff --git a/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs b/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs index a9a46ee7..d904b12e 100644 --- a/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs +++ b/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs @@ -1,33 +1,46 @@ use std::{ env, - fs::{self, read_to_string, OpenOptions}, + fs::{self, read_to_string, File, OpenOptions}, io::Write, process::Command, }; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; -use crate::cli::NeorgCommand; +use crate::{cli::NeorgCommand, state::State}; -pub fn handle(command: NeorgCommand) -> Result<()> { +pub fn handle(command: NeorgCommand, state: &mut State) -> Result<()> { match command { NeorgCommand::Task { id } => { - let project = id.project()?; - let path = dirs::data_local_dir() + let project = id.project(state)?; + let base = dirs::data_local_dir() .expect("This should exists") - .join("notes") - .join(project.get_neorg_path()?); + .join("tskm/notes"); + let path = base.join(project.get_neorg_path()?); fs::create_dir_all(path.parent().expect("This should exist"))?; { - let contents = read_to_string(&path)?; - if contents.contains(format!("% {}", id.to_uuid()?).as_str()) { + let contents = if path.exists() { + read_to_string(&path) + .with_context(|| format!("Failed to read file: '{}'", path.display()))? + } else { + File::create(&path) + .with_context(|| format!("Failed to create file: '{}'", path.display()))?; + String::new() + }; + + if !contents.contains(format!("% {}", id.uuid()).as_str()) { let mut options = OpenOptions::new(); - options.append(true).create(true); + options.append(true).create(false); let mut file = options.open(&path)?; - file.write_all(format!("* TITLE (% {})", id.to_uuid()?).as_bytes())?; + file.write_all(format!("* TITLE (% {})", id.uuid()).as_bytes()) + .with_context(|| { + format!("Failed to write task uuid to file: '{}'", path.display()) + })?; + file.flush() + .with_context(|| format!("Failed to flush file: '{}'", path.display()))?; } } @@ -36,7 +49,7 @@ pub fn handle(command: NeorgCommand) -> Result<()> { .args([ path.to_str().expect("Should be a utf-8 str"), "-c", - format!("/% {}", id.to_uuid()?).as_str(), + format!("/% {}", id.uuid()).as_str(), ]) .status()?; if !status.success() { @@ -46,7 +59,7 @@ pub fn handle(command: NeorgCommand) -> Result<()> { { let status = Command::new("git") .args(["add", "."]) - .current_dir(&path) + .current_dir(path.parent().expect("Will exist")) .status()?; if !status.success() { bail!("Git add . failed!"); @@ -56,14 +69,10 @@ pub fn handle(command: NeorgCommand) -> Result<()> { .args([ "commit", "--message", - format!( - "chore({}): Update", - path.parent().expect("Should have a parent").display() - ) - .as_str(), + format!("chore({}): Update", project.get_neorg_path()?.display()).as_str(), "--no-gpg-sign", ]) - .current_dir(&path) + .current_dir(path.parent().expect("Will exist")) .status()?; if !status.success() { bail!("Git commit failed!"); @@ -71,7 +80,7 @@ pub fn handle(command: NeorgCommand) -> Result<()> { } { - id.annotate("[neorg data]")?; + id.mark_neorg_data(state)?; } } } diff --git a/pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs b/pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs index dc5cdf19..51d58ab3 100644 --- a/pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs +++ b/pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs @@ -8,11 +8,18 @@ pub mod handle; pub use handle::handle; impl Project { + /// Return the stored neorg path of this project. + /// The returned path will never start with a slash (/). pub(super) fn get_neorg_path(&self) -> Result<PathBuf> { let project_path = run_task(&[ "_get", format!("rc.context.{}.rc.neorg_path", self.to_context_display()).as_str(), ])?; - Ok(PathBuf::from(project_path.as_str())) + + let final_path = project_path + .strip_prefix('/') + .unwrap_or(project_path.as_str()); + + Ok(PathBuf::from(final_path)) } } diff --git a/pkgs/by-name/ts/tskm/src/interface/open/handle.rs b/pkgs/by-name/ts/tskm/src/interface/open/handle.rs index dc0d165d..4d7341b2 100644 --- a/pkgs/by-name/ts/tskm/src/interface/open/handle.rs +++ b/pkgs/by-name/ts/tskm/src/interface/open/handle.rs @@ -1,17 +1,23 @@ -use std::process; +use std::{ + fs, + net::{IpAddr, Ipv4Addr}, + path::PathBuf, + process, +}; use anyhow::{bail, Context, Result}; -use log::{error, info}; +use log::{error, info, warn}; +use url::Url; -use crate::{cli::OpenCommand, rofi, task}; +use crate::{cli::OpenCommand, rofi, state::State, task}; -pub fn handle(command: OpenCommand) -> Result<()> { +pub fn handle(command: OpenCommand, state: &mut State) -> Result<()> { match command { OpenCommand::Review => { for project in task::Project::all().context("Failed to get all project files")? { if project.is_touched() { info!("Reviewing project: '{}'", project.to_project_display()); - open_in_browser(project).with_context(|| { + open_in_browser(project, state, None).with_context(|| { format!( "Failed to open project ('{}') in Firefox", project.to_project_display() @@ -26,23 +32,13 @@ pub fn handle(command: OpenCommand) -> Result<()> { } } } - OpenCommand::Project { project } => { - let project = if let Some(p) = project { - p - } else if let Some(p) = - task::Project::get_current().context("Failed to get currently focused project")? - { - p - } else { - bail!("You need to either supply a project or have a project active!"); - }; - + OpenCommand::Project { project, url } => { project.touch().context("Failed to touch project")?; - open_in_browser(&project).with_context(|| { + open_in_browser(&project, state, url).with_context(|| { format!("Failed to open project: {}", project.to_project_display()) })?; } - OpenCommand::Select => { + OpenCommand::Select { url } => { let selected_project: task::Project = task::Project::from_project_string( &rofi::select( task::Project::all() @@ -60,7 +56,7 @@ pub fn handle(command: OpenCommand) -> Result<()> { .touch() .context("Failed to touch project")?; - open_in_browser(&selected_project).context("Failed to open project")?; + open_in_browser(&selected_project, state, url).context("Failed to open project")?; } OpenCommand::ListTabs { project } => { let project = if let Some(p) = project { @@ -109,12 +105,15 @@ pub fn handle(command: OpenCommand) -> Result<()> { Ok(()) } -fn open_in_browser(selected_project: &task::Project) -> Result<()> { +fn open_in_browser( + selected_project: &task::Project, + state: &mut State, + url: Option<Url>, +) -> Result<()> { let old_project: Option<task::Project> = task::Project::get_current().context("Failed to get currently active project")?; - // We have ensured that only one task may be active - let old_task: Option<task::Id> = - task::Id::get_current().context("Failed to get currently active task")?; + let old_task: Option<task::Task> = + task::Task::get_current(state).context("Failed to get currently active task")?; selected_project.activate().with_context(|| { format!( @@ -124,7 +123,7 @@ fn open_in_browser(selected_project: &task::Project) -> Result<()> { })?; let tracking_task = { - let all_tasks = selected_project.get_tasks().with_context(|| { + let all_tasks = selected_project.get_tasks(state).with_context(|| { format!( "Failed to get assoctiated tasks for project: '{}'", selected_project.to_project_display() @@ -132,7 +131,7 @@ fn open_in_browser(selected_project: &task::Project) -> Result<()> { })?; let tracking_task = all_tasks.into_iter().find(|t| { - let maybe_desc = t.description(); + let maybe_desc = t.description(state); if let Ok(desc) = maybe_desc { desc == "tracking" } else { @@ -149,31 +148,75 @@ fn open_in_browser(selected_project: &task::Project) -> Result<()> { "Starting task {} -> tracking", selected_project.to_project_display() ); - task.start() + task.start(state) .with_context(|| format!("Failed to start task {task}"))?; } tracking_task }; - let status = process::Command::new("firefox") - .args([ - "-P", - selected_project.to_project_display().as_str(), - "about:newtab", - ]) - .status() - .context("Failed to start firefox")?; + let status = { + let mut args = vec!["-P".to_owned(), selected_project.to_project_display()]; + if let Some(url) = url { + args.push(url.to_string()); + } else { + let lock_file = dirs::home_dir() + .expect("Exists") + .join(".mozilla/firefox") + .join(selected_project.to_project_display()) + .join("lock"); + + if lock_file.exists() { + let (ip, pid): (IpAddr, u32) = { + let link = fs::read_link(&lock_file).with_context(|| { + format!("Failed to readlink lock at '{}'", lock_file.display()) + })?; + + let (ip, pid) = link + .to_str() + .expect("Should work") + .split_once(':') + .expect("The split works"); + + ( + ip.parse().expect("Should be a valid ip address"), + pid.parse().expect("Should be a valid pid"), + ) + }; + + if ip != Ipv4Addr::new(127, 0, 0, 2) { + warn!("Your ip is weird.."); + } + + if PathBuf::from("/proc").join(pid.to_string()).exists() { + // Another Firefox instance has already been started for this project + // Add a buffer URL to force Firefox to open it in the already open instance + args.push("about:newtab".to_owned()); + } else { + // This project does not yet have another Firefox instance + // We do not need to add anything to the arguments, Firefox will open a new + // instance. + } + } else { + // There is no lock file and thus no instance already open. + } + }; + + process::Command::new("firefox") + .args(args) + .status() + .context("Failed to start firefox")? + }; if !status.success() { error!("Firefox run exited with error."); } if let Some(task) = tracking_task { - task.stop() + task.stop(state) .with_context(|| format!("Failed to stop task {task}"))?; } if let Some(task) = old_task { - task.start() + task.start(state) .with_context(|| format!("Failed to start task {task}"))?; } diff --git a/pkgs/by-name/ts/tskm/src/main.rs b/pkgs/by-name/ts/tskm/src/main.rs index 7fc9c0d4..77f2dcca 100644 --- a/pkgs/by-name/ts/tskm/src/main.rs +++ b/pkgs/by-name/ts/tskm/src/main.rs @@ -1,64 +1,39 @@ -#![allow(clippy::missing_panics_doc)] -#![allow(clippy::missing_errors_doc)] - use anyhow::Result; -use clap::Parser; +use clap::{CommandFactory, Parser}; -use crate::interface::{input, neorg, open, project}; +use crate::{ + cli::{CliArgs, Command}, + interface::{input, neorg, open, project}, + state::State, +}; pub mod cli; pub mod interface; pub mod rofi; +pub mod state; pub mod task; -use crate::cli::{CliArgs, Command}; - fn main() -> Result<(), anyhow::Error> { - // TODO: Support these completions for the respective types <2025-04-04> - // - // ID_GENERATION_FUNCTION - // ```sh - // context="$(task _get rc.context)" - // if [ "$context" ]; then - // filter="project:$context" - // else - // filter="0-10000" - // fi - // tasks="$(task "$filter" _ids)" - // - // if [ "$tasks" ]; then - // echo "$tasks" | xargs task _zshids | awk -F: -v q="'" '{gsub(/'\''/, q "\\" q q ); print $1 ":" q $2 q}' - // fi - // ``` - // - // ARGUMENTS: - // ID | *([0-9]) := [[%ID_GENERATION_FUNCTION]] - // The function displays all possible IDs of the eligible tasks. - // - // WS := %ALL_WORKSPACES - // All possible workspaces. - // - // P := %ALL_PROJECTS_PIPE - // The possible project. - // - // F := [[fd . --max-depth 3]] - // A URL-Input file to use as source. + clap_complete::CompleteEnv::with_factory(CliArgs::command).complete(); + let args = CliArgs::parse(); stderrlog::new() .module(module_path!()) - .quiet(false) + .quiet(args.quiet) .show_module_names(true) .color(stderrlog::ColorChoice::Auto) - .verbosity(5) + .verbosity(usize::from(args.verbosity)) .timestamp(stderrlog::Timestamp::Off) .init() .expect("Let's just hope that this does not panic"); + let mut state = State::new_rw()?; + match args.command { Command::Inputs { command } => input::handle(command)?, - Command::Neorg { command } => neorg::handle(command)?, - Command::Open { command } => open::handle(command)?, + Command::Neorg { command } => neorg::handle(command, &mut state)?, + Command::Open { command } => open::handle(command, &mut state)?, Command::Projects { command } => project::handle(command)?, } diff --git a/pkgs/by-name/ts/tskm/src/state.rs b/pkgs/by-name/ts/tskm/src/state.rs new file mode 100644 index 00000000..175a7f03 --- /dev/null +++ b/pkgs/by-name/ts/tskm/src/state.rs @@ -0,0 +1,45 @@ +use std::path::PathBuf; + +use anyhow::Result; +use taskchampion::{storage::AccessMode, Replica, StorageConfig}; + +pub struct State { + replica: Replica, +} + +impl std::fmt::Debug for State { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "State") + } +} + +impl State { + fn taskdb_dir() -> PathBuf { + dirs::data_local_dir().expect("Should exist").join("task") + } + + fn new(taskdb_dir: PathBuf, access_mode: AccessMode) -> Result<Self> { + let storage = StorageConfig::OnDisk { + taskdb_dir, + create_if_missing: false, + access_mode, + } + .into_storage()?; + + let replica = Replica::new(storage); + + Ok(Self { replica }) + } + + pub fn new_ro() -> Result<Self> { + Self::new(Self::taskdb_dir(), AccessMode::ReadOnly) + } + pub fn new_rw() -> Result<Self> { + Self::new(Self::taskdb_dir(), AccessMode::ReadWrite) + } + + #[must_use] + pub fn replica(&mut self) -> &mut Replica { + &mut self.replica + } +} diff --git a/pkgs/by-name/ts/tskm/src/task/mod.rs b/pkgs/by-name/ts/tskm/src/task/mod.rs index c3a6d614..989f273a 100644 --- a/pkgs/by-name/ts/tskm/src/task/mod.rs +++ b/pkgs/by-name/ts/tskm/src/task/mod.rs @@ -9,109 +9,144 @@ use std::{ use anyhow::{bail, Context, Result}; use log::{debug, info, trace}; +use taskchampion::Tag; -use crate::interface::project::ProjectName; +use crate::{interface::project::ProjectName, state::State}; /// The `taskwarrior` id of a task. #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Ord, Eq)] -pub struct Id { - id: u64, +pub struct Task { + uuid: taskchampion::Uuid, } -impl Id { - /// # Errors - /// When `task` execution fails - pub fn get_current() -> Result<Option<Self>> { - // We have ensured that only one task may be active - let self_str = run_task(&["+ACTIVE", "_ids"])?; - if self_str.is_empty() { - Ok(None) +impl From<&taskchampion::Task> for Task { + fn from(value: &taskchampion::Task) -> Self { + Self { + uuid: value.get_uuid(), + } + } +} +impl From<&taskchampion::TaskData> for Task { + fn from(value: &taskchampion::TaskData) -> Self { + Self { + uuid: value.get_uuid(), + } + } +} + +impl Task { + pub fn from_working_set(id: usize, state: &mut State) -> Result<Option<Self>> { + Ok(state + .replica() + .working_set()? + .by_index(id) + .map(|uuid| Self { uuid })) + } + + pub fn get_current(state: &mut State) -> Result<Option<Self>> { + let tasks = state + .replica() + .pending_tasks()? + .into_iter() + .filter(taskchampion::Task::is_active) + .collect::<Vec<_>>(); + + assert!( + tasks.len() <= 1, + "We have ensured that only one task may be active, via a hook" + ); + if let Some(active) = tasks.first() { + Ok(Some(Self::from(active))) } else { - Self::from_str(&self_str).map(Some) + Ok(None) } } - /// # Errors - /// When `task` execution fails - pub fn to_uuid(&self) -> Result<String> { - let uuid = run_task(&[self.to_string().as_str(), "uuids"])?; + #[must_use] + pub fn uuid(&self) -> &taskchampion::Uuid { + &self.uuid + } + #[must_use] + pub fn working_set_id(&self, state: &mut State) -> Result<usize> { + Ok(state + .replica() + .working_set()? + .by_uuid(self.uuid) + .expect("The task should be in the working set")) + } - Ok(uuid) + fn as_task(&self, state: &mut State) -> Result<taskchampion::Task> { + Ok(state + .replica() + .get_task(self.uuid)? + .expect("We have the task from this replica, it should still be in it")) } - /// # Panics - /// When internal assertions fail. - /// # Errors - /// When `task` execution fails - pub fn annotate(&self, message: &str) -> Result<()> { - run_task(&["annotate", self.to_string().as_str(), "--", message])?; + /// Adds a tag to the task, to show the user that it has additional neorg data. + pub fn mark_neorg_data(&self, state: &mut State) -> Result<()> { + let mut ops = vec![]; + self.as_task(state)? + .add_tag(&Tag::from_str("neorg_data").expect("Is valid"), &mut ops)?; + state.replica().commit_operations(ops)?; Ok(()) } - /// # Panics - /// When internal assertions fail. - /// # Errors - /// When `task` execution fails - pub fn start(&self) -> Result<()> { + /// Try to start this task. + /// It will stop previously active tasks. + pub fn start(&self, state: &mut State) -> Result<()> { info!("Activating {self}"); - let output = run_task(&["start", self.to_string().as_str()])?; - assert!(output.is_empty()); + if let Some(active) = Self::get_current(state)? { + active.stop(state)?; + } + + let mut ops = vec![]; + self.as_task(state)?.start(&mut ops)?; + state.replica().commit_operations(ops)?; Ok(()) } - /// # Panics - /// When internal assertions fail. - /// # Errors - /// When `task` execution fails - pub fn stop(&self) -> Result<()> { + + /// Stops this task. + pub fn stop(&self, state: &mut State) -> Result<()> { info!("Stopping {self}"); - let output = run_task(&["stop", self.to_string().as_str()])?; - assert!(output.is_empty()); + let mut ops = vec![]; + self.as_task(state)?.stop(&mut ops)?; + state.replica().commit_operations(ops)?; Ok(()) } - /// # Panics - /// When internal assertions fail. - /// # Errors - /// When `task` execution fails - pub fn description(&self) -> Result<String> { - let output = run_task(&["rc.context=none", "_zshids", self.to_string().as_str()])?; - let (id, desc) = output - .split_once(':') - .expect("The output should always contain one colon"); - assert_eq!(id.parse::<Id>().expect("This should be a valid id"), *self); - Ok(desc.to_owned()) + pub fn description(&self, state: &mut State) -> Result<String> { + Ok(self.as_task(state)?.get_description().to_owned()) } - /// # Panics - /// When internal assertions fail. - /// # Errors - /// When `task` execution fails - pub fn project(&self) -> Result<Project> { - let output = run_task(&[ - "rc.context=none", - "_get", - format!("{self}.project").as_str(), - ])?; - let project = Project::from_project_string(output.as_str()) + pub fn project(&self, state: &mut State) -> Result<Project> { + let output = { + let task = self.as_task(state)?; + let task_data = task.into_task_data(); + task_data + .get("project") + .expect("Every task should have a project") + .to_owned() + }; + let project = Project::from_project_string(output.as_str().trim()) .expect("This comes from tw, it should be valid"); Ok(project) } } -impl Display for Id { +impl Display for Task { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.id.fmt(f) + self.uuid.fmt(f) } } -impl FromStr for Id { +impl FromStr for Task { type Err = anyhow::Error; fn from_str(s: &str) -> Result<Self, Self::Err> { - let id = u64::from_str(s)?; - Ok(Self { id }) + let uuid = taskchampion::Uuid::from_str(s)?; + Ok(Self { uuid }) } } @@ -259,21 +294,14 @@ impl Project { /// # Errors /// When `task` execution fails. - pub fn get_tasks(&self) -> Result<Vec<Id>> { - let output = run_task(&[ - "rc.context=none", - format!("project:{}", self.to_project_display()).as_str(), - "_ids", - ])?; - - if output.is_empty() { - Ok(vec![]) - } else { - output - .lines() - .map(Id::from_str) - .collect::<Result<Vec<Id>>>() - } + pub fn get_tasks(&self, state: &mut State) -> Result<Vec<Task>> { + Ok(state + .replica() + .pending_task_data()? + .into_iter() + .filter(|t| t.get("project").expect("Is set") == self.to_project_display()) + .map(|t| Task::from(&t)) + .collect()) } /// # Errors |