A file-based task manager

Compare changes

Choose any two refs to compare.

+1879 -872
+1
.envrc
···
··· 1 + use flake
+2 -1
.gitignore
··· 1 /target 2 - .tsk/
··· 1 /target 2 + index.html 3 + flake.lock
+2
.tsk/archive/tsk-1.tsk
···
··· 1 + implement searching task bodies 2 +
+2
.tsk/archive/tsk-10.tsk
···
··· 1 + foreign workspaces 2 +
+2
.tsk/archive/tsk-11.tsk
···
··· 1 + Use symlinks for tasks/ 2 +
+2
.tsk/archive/tsk-12.tsk
···
··· 1 + Update readme with changes to linking 2 +
+2
.tsk/archive/tsk-13.tsk
···
··· 1 + user-defined labels 2 +
+14
.tsk/archive/tsk-14.tsk
···
··· 1 + parse internal links from body 2 + 3 + This is some !test bold text!. 4 + here's some =highlighted text= 5 + 6 + and finally some *italics!* 7 + 8 + here's [a link](https://ngp.computer). 9 + 10 + and an internal link: [[tsk-11]]. This should add a backlink 11 + 12 + and some _underlined text_ 13 + 14 + some ~strikethrough~.
+11
.tsk/archive/tsk-15.tsk
···
··· 1 + Add link identification to tasks 2 + 3 + [This crate](https://docs.rs/linkify/latest/linkify/) should be helpful for 4 + that, though I've only done a cursory search. 5 + 6 + The intent here is to provide a command that allows you to list and open a link 7 + from a provided task and optionall use a system-handler to open the link. 8 + 9 + Something along the lines of `tsk hyperlinks -t 12 -s`, which will scan TSK-12 for 10 + hyperlinks (or email addresses?) and pipe them to `fzf` for selection and 11 + opening (-s flag) or simply print the link if no option is specified.
+3
.tsk/archive/tsk-16.tsk
···
··· 1 + Add ability to search archived tasks with find command 2 + 3 + Probably want to use `-a` flag or something
+5
.tsk/archive/tsk-17.tsk
···
··· 1 + Add reopen command 2 + 3 + Reopen will allow selecting an *archived* task (note: this needs to be 4 + restricted to archived tasks) and recreating the symlink in tasks/ to mark it as 5 + open.
+2
.tsk/archive/tsk-18.tsk
···
··· 1 + Add reindex command 2 +
+7
.tsk/archive/tsk-19.tsk
···
··· 1 + add "raw" output option for show 2 + 3 + Should probably be some variant of `tsk show -x` or something to skip the parsing step, 4 + just display the body of the text directly. 5 + 6 + This does suggest I should add a `format` subcommand that takes in a body and outputs 7 + the parsed + styled form, could be useful for editor plugins
+2
.tsk/archive/tsk-2.tsk
···
··· 1 + fix -C 2 +
+5
.tsk/archive/tsk-20.tsk
···
··· 1 + fix issue where links use absolute paths 2 + 3 + MacOS 4 + 5 +
+7
.tsk/archive/tsk-21.tsk
···
··· 1 + Add command to setup git stuff 2 + 3 + Will want to prompt to add `.tsk` to the `.git/info/exclude` file (or 4 + .gitignore/globally) and *probably* set up 5 + [metastore](https://github.com/przemoc/metastore) 6 + 7 + What else should we do?
+3
.tsk/archive/tsk-22.tsk
···
··· 1 + Figure out why link parsing isn't working in tsk-21 2 + 3 + Actually, it appears *all* styling is broken somehow
+2
.tsk/archive/tsk-23.tsk
···
··· 1 + Allow selecting which task to follow links from 2 +
+2
.tsk/archive/tsk-24.tsk
···
··· 1 + properly handle removing links from task 2 +
+2
.tsk/archive/tsk-25.tsk
···
··· 1 + fix duplicate backrefs on edit 2 +
+2
.tsk/archive/tsk-26.tsk
···
··· 1 + fix -T tsk-id parsing 2 +
+2
.tsk/archive/tsk-27.tsk
···
··· 1 + Fix -F doc on find 2 +
+17
.tsk/archive/tsk-28.tsk
···
··· 1 + Add tool to clean up old tasks not in index 2 + 3 + Previously the `drop` command did not remove the symlink in .tsk/tasks when a 4 + task was dropped, only removing it from the index. I don't recall if this was 5 + because my original design expected the index to be the source of truth on 6 + whether a task was prioritized or not or if I simply forgot to add it. Either 7 + way, drop now properly removes the symlink but basically every existing 8 + workspace has a bunch of junk in it. I can write a simple script that deletes 9 + all symlinks that aren't present in the index. 10 + 11 + This does suggest that I should add a reindex command to add tasks that are not 12 + present in the index but are in the tasks folder to the index at the *bottom*, 13 + preserving the existing order of the index. 14 + 15 + 16 + How about just adding a `fixup` command that does this and reindex as laid out in 17 + [[tsk-18]].
+2
.tsk/archive/tsk-29.tsk
···
··· 1 + style titles in list command output 2 +
+4
.tsk/archive/tsk-3.tsk
···
··· 1 + An example task with a body 2 + 3 + This is an example body! 4 + It has multiple lines!
+2
.tsk/archive/tsk-30.tsk
···
··· 1 + Add flag to only print IDs in list command 2 +
+8
.tsk/archive/tsk-31.tsk
···
··· 1 + DO THE THING 2 + 3 + 4 + remember to do part1 5 + 6 + and part2 7 + 8 + and part3
+4
.tsk/archive/tsk-4.tsk
···
··· 1 + Add basic metadata 2 + 3 + Currently have basic metadata reading done. There's nothing *writing* metadata, but 4 + we'll get there next.
+2
.tsk/archive/tsk-5.tsk
···
··· 1 + Add links 2 +
+6
.tsk/archive/tsk-6.tsk
···
··· 1 + automatically add backlinks 2 + 3 + I need to parse on save/edit/create for outgoing internal links. If any exist and their 4 + corresponding task exists, update the targetted task with a backlink reference 5 + 6 + Using [[tsk-11]] as my test.
+2
.tsk/archive/tsk-7.tsk
···
··· 1 + allow for creating tasks that don't go to top of stack 2 +
+2
.tsk/archive/tsk-8.tsk
···
··· 1 + IMAP4-based sync 2 +
+5
.tsk/archive/tsk-9.tsk
···
··· 1 + fix timestamp storage and parsing 2 + 3 + It looks like timestamps aren't being stored or parsed from the index anymore. 4 + I'm not quite sure how this broke, but it's like an issue in `StackItem`'s 5 + FromStr and Display implementations.
+12
.tsk/index
···
··· 1 + tsk-30 Add flag to only print IDs in list command 1763257109 2 + tsk-28 Add tool to clean up old tasks not in index 1735006519 3 + tsk-10 foreign workspaces 1732594198 4 + tsk-21 Add command to setup git stuff 1732594198 5 + tsk-8 IMAP4-based sync 1767469318 6 + tsk-17 Add reopen command 1732594198 7 + tsk-16 Add ability to search archived tasks with find command 1767466011 8 + tsk-15 Add link identification to tasks 1732594198 9 + tsk-9 fix timestamp storage and parsing 1732594198 10 + tsk-7 allow for creating tasks that don't go to top of stack 1732594198 11 + tsk-13 user-defined labels 1732594198 12 + tsk-18 Add reindex command 1735006716
+1
.tsk/next
···
··· 1 + 32
+1
.tsk/tasks/tsk-10.tsk
···
··· 1 + ../archive/tsk-10.tsk
+1
.tsk/tasks/tsk-11.tsk
···
··· 1 + ../archive/tsk-11.tsk
+1
.tsk/tasks/tsk-12.tsk
···
··· 1 + ../archive/tsk-12.tsk
+1
.tsk/tasks/tsk-13.tsk
···
··· 1 + ../archive/tsk-13.tsk
+1
.tsk/tasks/tsk-14.tsk
···
··· 1 + ../archive/tsk-14.tsk
+1
.tsk/tasks/tsk-15.tsk
···
··· 1 + ../archive/tsk-15.tsk
+1
.tsk/tasks/tsk-16.tsk
···
··· 1 + ../archive/tsk-16.tsk
+1
.tsk/tasks/tsk-17.tsk
···
··· 1 + ../archive/tsk-17.tsk
+1
.tsk/tasks/tsk-18.tsk
···
··· 1 + ../archive/tsk-18.tsk
+1
.tsk/tasks/tsk-19.tsk
···
··· 1 + ../archive/tsk-19.tsk
+1
.tsk/tasks/tsk-20.tsk
···
··· 1 + ../archive/tsk-20.tsk
+1
.tsk/tasks/tsk-21.tsk
···
··· 1 + ../archive/tsk-21.tsk
+1
.tsk/tasks/tsk-22.tsk
···
··· 1 + ../archive/tsk-22.tsk
+1
.tsk/tasks/tsk-23.tsk
···
··· 1 + ../archive/tsk-23.tsk
+1
.tsk/tasks/tsk-24.tsk
···
··· 1 + ../archive/tsk-24.tsk
+1
.tsk/tasks/tsk-25.tsk
···
··· 1 + ../archive/tsk-25.tsk
+1
.tsk/tasks/tsk-26.tsk
···
··· 1 + ../archive/tsk-26.tsk
+1
.tsk/tasks/tsk-27.tsk
···
··· 1 + ../archive/tsk-27.tsk
+1
.tsk/tasks/tsk-28.tsk
···
··· 1 + ../archive/tsk-28.tsk
+1
.tsk/tasks/tsk-30.tsk
···
··· 1 + ../archive/tsk-30.tsk
+1
.tsk/tasks/tsk-4.tsk
···
··· 1 + ../archive/tsk-4.tsk
+1
.tsk/tasks/tsk-5.tsk
···
··· 1 + ../archive/tsk-5.tsk
+1
.tsk/tasks/tsk-6.tsk
···
··· 1 + ../archive/tsk-6.tsk
+1
.tsk/tasks/tsk-7.tsk
···
··· 1 + ../archive/tsk-7.tsk
+1
.tsk/tasks/tsk-8.tsk
···
··· 1 + ../archive/tsk-8.tsk
+1
.tsk/tasks/tsk-9.tsk
···
··· 1 + ../archive/tsk-9.tsk
+434 -714
Cargo.lock
··· 1 # This file is automatically @generated by Cargo. 2 # It is not intended for manual editing. 3 - version = 3 4 5 [[package]] 6 name = "anstream" 7 - version = "0.6.15" 8 source = "registry+https://github.com/rust-lang/crates.io-index" 9 - checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" 10 dependencies = [ 11 "anstyle", 12 "anstyle-parse", ··· 19 20 [[package]] 21 name = "anstyle" 22 - version = "1.0.8" 23 source = "registry+https://github.com/rust-lang/crates.io-index" 24 - checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 25 26 [[package]] 27 name = "anstyle-parse" 28 - version = "0.2.5" 29 source = "registry+https://github.com/rust-lang/crates.io-index" 30 - checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" 31 dependencies = [ 32 "utf8parse", 33 ] 34 35 [[package]] 36 name = "anstyle-query" 37 - version = "1.1.1" 38 source = "registry+https://github.com/rust-lang/crates.io-index" 39 - checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" 40 dependencies = [ 41 - "windows-sys 0.52.0", 42 ] 43 44 [[package]] 45 name = "anstyle-wincon" 46 - version = "3.0.4" 47 source = "registry+https://github.com/rust-lang/crates.io-index" 48 - checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" 49 dependencies = [ 50 "anstyle", 51 - "windows-sys 0.52.0", 52 - ] 53 - 54 - [[package]] 55 - name = "any_key" 56 - version = "0.1.1" 57 - source = "registry+https://github.com/rust-lang/crates.io-index" 58 - checksum = "d21bb2cdab8087ed9d69411dd99c608dbede1df847c255b4d609f0399a3cb452" 59 - dependencies = [ 60 - "debugit", 61 - "mopa", 62 - ] 63 - 64 - [[package]] 65 - name = "arrayvec" 66 - version = "0.7.6" 67 - source = "registry+https://github.com/rust-lang/crates.io-index" 68 - checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 69 - 70 - [[package]] 71 - name = "async-channel" 72 - version = "2.3.1" 73 - source = "registry+https://github.com/rust-lang/crates.io-index" 74 - checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" 75 - dependencies = [ 76 - "concurrent-queue", 77 - "event-listener-strategy", 78 - "futures-core", 79 - "pin-project-lite", 80 - ] 81 - 82 - [[package]] 83 - name = "async-executor" 84 - version = "1.13.1" 85 - source = "registry+https://github.com/rust-lang/crates.io-index" 86 - checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" 87 - dependencies = [ 88 - "async-task", 89 - "concurrent-queue", 90 - "fastrand", 91 - "futures-lite", 92 - "slab", 93 - ] 94 - 95 - [[package]] 96 - name = "async-fs" 97 - version = "2.1.2" 98 - source = "registry+https://github.com/rust-lang/crates.io-index" 99 - checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" 100 - dependencies = [ 101 - "async-lock", 102 - "blocking", 103 - "futures-lite", 104 - ] 105 - 106 - [[package]] 107 - name = "async-io" 108 - version = "2.3.4" 109 - source = "registry+https://github.com/rust-lang/crates.io-index" 110 - checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" 111 - dependencies = [ 112 - "async-lock", 113 - "cfg-if", 114 - "concurrent-queue", 115 - "futures-io", 116 - "futures-lite", 117 - "parking", 118 - "polling", 119 - "rustix", 120 - "slab", 121 - "tracing", 122 - "windows-sys 0.59.0", 123 - ] 124 - 125 - [[package]] 126 - name = "async-lock" 127 - version = "3.4.0" 128 - source = "registry+https://github.com/rust-lang/crates.io-index" 129 - checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" 130 - dependencies = [ 131 - "event-listener", 132 - "event-listener-strategy", 133 - "pin-project-lite", 134 - ] 135 - 136 - [[package]] 137 - name = "async-net" 138 - version = "2.0.0" 139 - source = "registry+https://github.com/rust-lang/crates.io-index" 140 - checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" 141 - dependencies = [ 142 - "async-io", 143 - "blocking", 144 - "futures-lite", 145 - ] 146 - 147 - [[package]] 148 - name = "async-process" 149 - version = "2.3.0" 150 - source = "registry+https://github.com/rust-lang/crates.io-index" 151 - checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" 152 - dependencies = [ 153 - "async-channel", 154 - "async-io", 155 - "async-lock", 156 - "async-signal", 157 - "async-task", 158 - "blocking", 159 - "cfg-if", 160 - "event-listener", 161 - "futures-lite", 162 - "rustix", 163 - "tracing", 164 ] 165 166 [[package]] 167 - name = "async-signal" 168 - version = "0.2.10" 169 - source = "registry+https://github.com/rust-lang/crates.io-index" 170 - checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" 171 - dependencies = [ 172 - "async-io", 173 - "async-lock", 174 - "atomic-waker", 175 - "cfg-if", 176 - "futures-core", 177 - "futures-io", 178 - "rustix", 179 - "signal-hook-registry", 180 - "slab", 181 - "windows-sys 0.59.0", 182 - ] 183 - 184 - [[package]] 185 - name = "async-task" 186 - version = "4.7.1" 187 - source = "registry+https://github.com/rust-lang/crates.io-index" 188 - checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" 189 - 190 - [[package]] 191 - name = "atomic-waker" 192 - version = "1.1.2" 193 - source = "registry+https://github.com/rust-lang/crates.io-index" 194 - checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 195 - 196 - [[package]] 197 - name = "autocfg" 198 - version = "1.4.0" 199 - source = "registry+https://github.com/rust-lang/crates.io-index" 200 - checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 201 - 202 - [[package]] 203 name = "bitflags" 204 - version = "2.6.0" 205 - source = "registry+https://github.com/rust-lang/crates.io-index" 206 - checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 207 - 208 - [[package]] 209 - name = "blocking" 210 - version = "1.6.1" 211 source = "registry+https://github.com/rust-lang/crates.io-index" 212 - checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" 213 - dependencies = [ 214 - "async-channel", 215 - "async-task", 216 - "futures-io", 217 - "futures-lite", 218 - "piper", 219 - ] 220 221 [[package]] 222 name = "cfg-if" 223 - version = "1.0.0" 224 source = "registry+https://github.com/rust-lang/crates.io-index" 225 - checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 226 227 [[package]] 228 name = "cfg_aliases" ··· 232 233 [[package]] 234 name = "clap" 235 - version = "4.5.19" 236 source = "registry+https://github.com/rust-lang/crates.io-index" 237 - checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" 238 dependencies = [ 239 "clap_builder", 240 "clap_derive", ··· 242 243 [[package]] 244 name = "clap_builder" 245 - version = "4.5.19" 246 source = "registry+https://github.com/rust-lang/crates.io-index" 247 - checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" 248 dependencies = [ 249 "anstream", 250 "anstyle", ··· 254 255 [[package]] 256 name = "clap_complete" 257 - version = "4.5.32" 258 source = "registry+https://github.com/rust-lang/crates.io-index" 259 - checksum = "74a01f4f9ee6c066d42a1c8dedf0dcddad16c72a8981a309d6398de3a75b0c39" 260 dependencies = [ 261 "clap", 262 ] 263 264 [[package]] 265 name = "clap_derive" 266 - version = "4.5.18" 267 source = "registry+https://github.com/rust-lang/crates.io-index" 268 - checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 269 dependencies = [ 270 "heck", 271 "proc-macro2", ··· 275 276 [[package]] 277 name = "clap_lex" 278 - version = "0.7.2" 279 source = "registry+https://github.com/rust-lang/crates.io-index" 280 - checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" 281 282 [[package]] 283 name = "clap_mangen" 284 - version = "0.2.23" 285 source = "registry+https://github.com/rust-lang/crates.io-index" 286 - checksum = "f17415fd4dfbea46e3274fcd8d368284519b358654772afb700dc2e8d2b24eeb" 287 dependencies = [ 288 "clap", 289 "roff", ··· 291 292 [[package]] 293 name = "colorchoice" 294 - version = "1.0.2" 295 source = "registry+https://github.com/rust-lang/crates.io-index" 296 - checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" 297 298 [[package]] 299 - name = "concurrent-queue" 300 - version = "2.5.0" 301 source = "registry+https://github.com/rust-lang/crates.io-index" 302 - checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 303 dependencies = [ 304 - "crossbeam-utils", 305 ] 306 307 [[package]] 308 - name = "crossbeam-utils" 309 - version = "0.8.20" 310 source = "registry+https://github.com/rust-lang/crates.io-index" 311 - checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 312 - 313 - [[package]] 314 - name = "crossterm" 315 - version = "0.28.1" 316 - source = "registry+https://github.com/rust-lang/crates.io-index" 317 - checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 318 dependencies = [ 319 - "bitflags", 320 - "crossterm_winapi", 321 - "futures-core", 322 - "mio", 323 - "parking_lot", 324 - "rustix", 325 - "signal-hook", 326 - "signal-hook-mio", 327 - "winapi", 328 - ] 329 - 330 - [[package]] 331 - name = "crossterm_winapi" 332 - version = "0.9.1" 333 - source = "registry+https://github.com/rust-lang/crates.io-index" 334 - checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 335 - dependencies = [ 336 - "winapi", 337 - ] 338 - 339 - [[package]] 340 - name = "debugit" 341 - version = "0.1.2" 342 - source = "registry+https://github.com/rust-lang/crates.io-index" 343 - checksum = "63c2f7e3034df2b09f750327e23c1adfe33301e6b7388f05bb4fcc0fa46825e3" 344 - dependencies = [ 345 - "version_check 0.1.5", 346 ] 347 348 [[package]] ··· 357 358 [[package]] 359 name = "either" 360 - version = "1.13.0" 361 source = "registry+https://github.com/rust-lang/crates.io-index" 362 - checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 363 - 364 - [[package]] 365 - name = "equivalent" 366 - version = "1.0.1" 367 - source = "registry+https://github.com/rust-lang/crates.io-index" 368 - checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 369 370 [[package]] 371 name = "errno" 372 - version = "0.3.9" 373 source = "registry+https://github.com/rust-lang/crates.io-index" 374 - checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 375 dependencies = [ 376 "libc", 377 - "windows-sys 0.52.0", 378 - ] 379 - 380 - [[package]] 381 - name = "event-listener" 382 - version = "5.3.1" 383 - source = "registry+https://github.com/rust-lang/crates.io-index" 384 - checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" 385 - dependencies = [ 386 - "concurrent-queue", 387 - "parking", 388 - "pin-project-lite", 389 - ] 390 - 391 - [[package]] 392 - name = "event-listener-strategy" 393 - version = "0.5.2" 394 - source = "registry+https://github.com/rust-lang/crates.io-index" 395 - checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" 396 - dependencies = [ 397 - "event-listener", 398 - "pin-project-lite", 399 ] 400 401 [[package]] 402 name = "fastrand" 403 - version = "2.1.1" 404 source = "registry+https://github.com/rust-lang/crates.io-index" 405 - checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" 406 407 [[package]] 408 - name = "futures" 409 - version = "0.3.31" 410 source = "registry+https://github.com/rust-lang/crates.io-index" 411 - checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 412 dependencies = [ 413 - "futures-channel", 414 - "futures-core", 415 - "futures-executor", 416 - "futures-io", 417 - "futures-sink", 418 - "futures-task", 419 - "futures-util", 420 ] 421 422 [[package]] 423 - name = "futures-channel" 424 - version = "0.3.31" 425 source = "registry+https://github.com/rust-lang/crates.io-index" 426 - checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 427 dependencies = [ 428 - "futures-core", 429 - "futures-sink", 430 ] 431 432 [[package]] 433 - name = "futures-core" 434 - version = "0.3.31" 435 source = "registry+https://github.com/rust-lang/crates.io-index" 436 - checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 437 438 [[package]] 439 - name = "futures-executor" 440 - version = "0.3.31" 441 source = "registry+https://github.com/rust-lang/crates.io-index" 442 - checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 443 dependencies = [ 444 - "futures-core", 445 - "futures-task", 446 - "futures-util", 447 ] 448 449 [[package]] 450 - name = "futures-io" 451 - version = "0.3.31" 452 source = "registry+https://github.com/rust-lang/crates.io-index" 453 - checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 454 - 455 - [[package]] 456 - name = "futures-lite" 457 - version = "2.3.0" 458 - source = "registry+https://github.com/rust-lang/crates.io-index" 459 - checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" 460 dependencies = [ 461 - "fastrand", 462 - "futures-core", 463 - "futures-io", 464 - "parking", 465 - "pin-project-lite", 466 ] 467 468 [[package]] 469 - name = "futures-macro" 470 - version = "0.3.31" 471 source = "registry+https://github.com/rust-lang/crates.io-index" 472 - checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 473 dependencies = [ 474 - "proc-macro2", 475 - "quote", 476 - "syn", 477 ] 478 479 [[package]] 480 - name = "futures-sink" 481 - version = "0.3.31" 482 source = "registry+https://github.com/rust-lang/crates.io-index" 483 - checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 484 485 [[package]] 486 - name = "futures-task" 487 - version = "0.3.31" 488 source = "registry+https://github.com/rust-lang/crates.io-index" 489 - checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 490 491 [[package]] 492 - name = "futures-util" 493 - version = "0.3.31" 494 source = "registry+https://github.com/rust-lang/crates.io-index" 495 - checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 496 dependencies = [ 497 - "futures-channel", 498 - "futures-core", 499 - "futures-io", 500 - "futures-macro", 501 - "futures-sink", 502 - "futures-task", 503 - "memchr", 504 - "pin-project-lite", 505 - "pin-utils", 506 - "slab", 507 ] 508 509 [[package]] 510 - name = "generational-box" 511 - version = "0.5.6" 512 source = "registry+https://github.com/rust-lang/crates.io-index" 513 - checksum = "557cf2cbacd0504c6bf8c29f52f8071e0de1d9783346713dc6121d7fa1e5d0e0" 514 - dependencies = [ 515 - "parking_lot", 516 - ] 517 518 [[package]] 519 - name = "getrandom" 520 - version = "0.2.15" 521 source = "registry+https://github.com/rust-lang/crates.io-index" 522 - checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 523 dependencies = [ 524 - "cfg-if", 525 - "libc", 526 - "wasi", 527 ] 528 529 [[package]] 530 - name = "hashbrown" 531 - version = "0.15.0" 532 - source = "registry+https://github.com/rust-lang/crates.io-index" 533 - checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" 534 - 535 - [[package]] 536 - name = "heck" 537 - version = "0.5.0" 538 - source = "registry+https://github.com/rust-lang/crates.io-index" 539 - checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 540 - 541 - [[package]] 542 - name = "hermit-abi" 543 - version = "0.3.9" 544 - source = "registry+https://github.com/rust-lang/crates.io-index" 545 - checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 546 - 547 - [[package]] 548 - name = "hermit-abi" 549 - version = "0.4.0" 550 source = "registry+https://github.com/rust-lang/crates.io-index" 551 - checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" 552 - 553 - [[package]] 554 - name = "home" 555 - version = "0.5.9" 556 - source = "registry+https://github.com/rust-lang/crates.io-index" 557 - checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 558 dependencies = [ 559 - "windows-sys 0.52.0", 560 ] 561 562 [[package]] 563 - name = "indexmap" 564 - version = "2.6.0" 565 source = "registry+https://github.com/rust-lang/crates.io-index" 566 - checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" 567 dependencies = [ 568 - "equivalent", 569 - "hashbrown", 570 ] 571 572 [[package]] 573 - name = "iocraft" 574 - version = "0.2.3" 575 source = "registry+https://github.com/rust-lang/crates.io-index" 576 - checksum = "2a35ac1085a4234a6193f443b09de3ede720013201f6cf0d122d1513c5f6eaaf" 577 dependencies = [ 578 - "any_key", 579 - "bitflags", 580 - "crossterm", 581 - "futures", 582 - "generational-box", 583 - "indexmap", 584 - "iocraft-macros", 585 - "taffy", 586 - "textwrap", 587 - "unicode-width", 588 - "uuid", 589 ] 590 591 [[package]] 592 - name = "iocraft-macros" 593 - version = "0.1.6" 594 source = "registry+https://github.com/rust-lang/crates.io-index" 595 - checksum = "1c23693fa666552feadf75efc3e0da9d144f3bea30b6a703d3f0b8adf8619434" 596 dependencies = [ 597 - "proc-macro2", 598 - "quote", 599 - "syn", 600 - "uuid", 601 ] 602 603 [[package]] ··· 607 checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 608 609 [[package]] 610 - name = "libc" 611 - version = "0.2.159" 612 - source = "registry+https://github.com/rust-lang/crates.io-index" 613 - checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" 614 - 615 - [[package]] 616 - name = "linux-raw-sys" 617 - version = "0.4.14" 618 - source = "registry+https://github.com/rust-lang/crates.io-index" 619 - checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 620 - 621 - [[package]] 622 - name = "lock_api" 623 - version = "0.4.12" 624 source = "registry+https://github.com/rust-lang/crates.io-index" 625 - checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 626 dependencies = [ 627 - "autocfg", 628 - "scopeguard", 629 ] 630 631 [[package]] 632 - name = "log" 633 - version = "0.4.22" 634 source = "registry+https://github.com/rust-lang/crates.io-index" 635 - checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 636 637 [[package]] 638 - name = "memchr" 639 - version = "2.7.4" 640 source = "registry+https://github.com/rust-lang/crates.io-index" 641 - checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 642 643 [[package]] 644 - name = "mio" 645 - version = "1.0.2" 646 source = "registry+https://github.com/rust-lang/crates.io-index" 647 - checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 648 - dependencies = [ 649 - "hermit-abi 0.3.9", 650 - "libc", 651 - "log", 652 - "wasi", 653 - "windows-sys 0.52.0", 654 - ] 655 656 [[package]] 657 - name = "mopa" 658 - version = "0.2.2" 659 source = "registry+https://github.com/rust-lang/crates.io-index" 660 - checksum = "a785740271256c230f57462d3b83e52f998433a7062fc18f96d5999474a9f915" 661 662 [[package]] 663 name = "nix" 664 - version = "0.29.0" 665 source = "registry+https://github.com/rust-lang/crates.io-index" 666 - checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 667 dependencies = [ 668 "bitflags", 669 "cfg-if", ··· 672 ] 673 674 [[package]] 675 - name = "num-traits" 676 - version = "0.2.19" 677 - source = "registry+https://github.com/rust-lang/crates.io-index" 678 - checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 679 - dependencies = [ 680 - "autocfg", 681 - ] 682 - 683 - [[package]] 684 name = "once_cell" 685 - version = "1.20.2" 686 - source = "registry+https://github.com/rust-lang/crates.io-index" 687 - checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 688 - 689 - [[package]] 690 - name = "parking" 691 - version = "2.2.1" 692 source = "registry+https://github.com/rust-lang/crates.io-index" 693 - checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 694 695 [[package]] 696 - name = "parking_lot" 697 - version = "0.12.3" 698 source = "registry+https://github.com/rust-lang/crates.io-index" 699 - checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 700 - dependencies = [ 701 - "lock_api", 702 - "parking_lot_core", 703 - ] 704 705 [[package]] 706 - name = "parking_lot_core" 707 - version = "0.9.10" 708 source = "registry+https://github.com/rust-lang/crates.io-index" 709 - checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 710 dependencies = [ 711 - "cfg-if", 712 "libc", 713 - "redox_syscall", 714 - "smallvec", 715 - "windows-targets", 716 ] 717 718 [[package]] 719 - name = "pin-project-lite" 720 - version = "0.2.14" 721 - source = "registry+https://github.com/rust-lang/crates.io-index" 722 - checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 723 - 724 - [[package]] 725 - name = "pin-utils" 726 - version = "0.1.0" 727 source = "registry+https://github.com/rust-lang/crates.io-index" 728 - checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 729 730 [[package]] 731 - name = "piper" 732 - version = "0.2.4" 733 source = "registry+https://github.com/rust-lang/crates.io-index" 734 - checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" 735 - dependencies = [ 736 - "atomic-waker", 737 - "fastrand", 738 - "futures-io", 739 - ] 740 741 [[package]] 742 - name = "polling" 743 - version = "3.7.3" 744 source = "registry+https://github.com/rust-lang/crates.io-index" 745 - checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" 746 dependencies = [ 747 - "cfg-if", 748 - "concurrent-queue", 749 - "hermit-abi 0.4.0", 750 - "pin-project-lite", 751 - "rustix", 752 - "tracing", 753 - "windows-sys 0.59.0", 754 ] 755 756 [[package]] 757 name = "proc-macro2" 758 - version = "1.0.86" 759 source = "registry+https://github.com/rust-lang/crates.io-index" 760 - checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 761 dependencies = [ 762 "unicode-ident", 763 ] 764 765 [[package]] 766 name = "quote" 767 - version = "1.0.37" 768 source = "registry+https://github.com/rust-lang/crates.io-index" 769 - checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 770 dependencies = [ 771 "proc-macro2", 772 ] 773 774 [[package]] 775 - name = "redox_syscall" 776 - version = "0.5.7" 777 source = "registry+https://github.com/rust-lang/crates.io-index" 778 - checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" 779 - dependencies = [ 780 - "bitflags", 781 - ] 782 783 [[package]] 784 name = "roff" ··· 788 789 [[package]] 790 name = "rustix" 791 - version = "0.38.37" 792 source = "registry+https://github.com/rust-lang/crates.io-index" 793 - checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" 794 dependencies = [ 795 "bitflags", 796 "errno", 797 "libc", 798 - "linux-raw-sys", 799 - "windows-sys 0.52.0", 800 ] 801 802 [[package]] 803 - name = "scopeguard" 804 - version = "1.2.0" 805 source = "registry+https://github.com/rust-lang/crates.io-index" 806 - checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 807 808 [[package]] 809 name = "serde" 810 - version = "1.0.210" 811 source = "registry+https://github.com/rust-lang/crates.io-index" 812 - checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 813 dependencies = [ 814 "serde_derive", 815 ] 816 817 [[package]] 818 name = "serde_derive" 819 - version = "1.0.210" 820 source = "registry+https://github.com/rust-lang/crates.io-index" 821 - checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 822 dependencies = [ 823 "proc-macro2", 824 "quote", ··· 826 ] 827 828 [[package]] 829 - name = "signal-hook" 830 - version = "0.3.17" 831 - source = "registry+https://github.com/rust-lang/crates.io-index" 832 - checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 833 - dependencies = [ 834 - "libc", 835 - "signal-hook-registry", 836 - ] 837 - 838 - [[package]] 839 - name = "signal-hook-mio" 840 - version = "0.2.4" 841 - source = "registry+https://github.com/rust-lang/crates.io-index" 842 - checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 843 - dependencies = [ 844 - "libc", 845 - "mio", 846 - "signal-hook", 847 - ] 848 - 849 - [[package]] 850 - name = "signal-hook-registry" 851 - version = "1.4.2" 852 - source = "registry+https://github.com/rust-lang/crates.io-index" 853 - checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 854 - dependencies = [ 855 - "libc", 856 - ] 857 - 858 - [[package]] 859 - name = "slab" 860 - version = "0.4.9" 861 - source = "registry+https://github.com/rust-lang/crates.io-index" 862 - checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 863 - dependencies = [ 864 - "autocfg", 865 - ] 866 - 867 - [[package]] 868 - name = "slotmap" 869 - version = "1.0.7" 870 - source = "registry+https://github.com/rust-lang/crates.io-index" 871 - checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" 872 - dependencies = [ 873 - "version_check 0.9.5", 874 - ] 875 - 876 - [[package]] 877 - name = "smallstr" 878 - version = "0.3.0" 879 - source = "registry+https://github.com/rust-lang/crates.io-index" 880 - checksum = "63b1aefdf380735ff8ded0b15f31aab05daf1f70216c01c02a12926badd1df9d" 881 - dependencies = [ 882 - "serde", 883 - "smallvec", 884 - ] 885 - 886 - [[package]] 887 name = "smallvec" 888 - version = "1.13.2" 889 source = "registry+https://github.com/rust-lang/crates.io-index" 890 - checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 891 892 [[package]] 893 - name = "smawk" 894 - version = "0.3.2" 895 source = "registry+https://github.com/rust-lang/crates.io-index" 896 - checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" 897 - 898 - [[package]] 899 - name = "smol" 900 - version = "2.0.2" 901 - source = "registry+https://github.com/rust-lang/crates.io-index" 902 - checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" 903 - dependencies = [ 904 - "async-channel", 905 - "async-executor", 906 - "async-fs", 907 - "async-io", 908 - "async-lock", 909 - "async-net", 910 - "async-process", 911 - "blocking", 912 - "futures-lite", 913 - ] 914 915 [[package]] 916 name = "strsim" ··· 920 921 [[package]] 922 name = "syn" 923 - version = "2.0.79" 924 source = "registry+https://github.com/rust-lang/crates.io-index" 925 - checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" 926 dependencies = [ 927 "proc-macro2", 928 "quote", ··· 930 ] 931 932 [[package]] 933 - name = "tabwriter" 934 - version = "1.4.0" 935 source = "registry+https://github.com/rust-lang/crates.io-index" 936 - checksum = "a327282c4f64f6dc37e3bba4c2b6842cc3a992f204fa58d917696a89f691e5f6" 937 dependencies = [ 938 - "unicode-width", 939 - ] 940 - 941 - [[package]] 942 - name = "taffy" 943 - version = "0.5.2" 944 - source = "registry+https://github.com/rust-lang/crates.io-index" 945 - checksum = "9cb893bff0f80ae17d3a57e030622a967b8dbc90e38284d9b4b1442e23873c94" 946 - dependencies = [ 947 - "arrayvec", 948 - "num-traits", 949 - "slotmap", 950 ] 951 952 [[package]] 953 name = "tempfile" 954 - version = "3.13.0" 955 source = "registry+https://github.com/rust-lang/crates.io-index" 956 - checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" 957 dependencies = [ 958 - "cfg-if", 959 "fastrand", 960 "once_cell", 961 - "rustix", 962 - "windows-sys 0.59.0", 963 - ] 964 - 965 - [[package]] 966 - name = "textwrap" 967 - version = "0.16.1" 968 - source = "registry+https://github.com/rust-lang/crates.io-index" 969 - checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" 970 - dependencies = [ 971 - "smawk", 972 - "unicode-linebreak", 973 - "unicode-width", 974 ] 975 976 [[package]] 977 name = "thiserror" 978 - version = "1.0.64" 979 source = "registry+https://github.com/rust-lang/crates.io-index" 980 - checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" 981 dependencies = [ 982 "thiserror-impl", 983 ] 984 985 [[package]] 986 name = "thiserror-impl" 987 - version = "1.0.64" 988 source = "registry+https://github.com/rust-lang/crates.io-index" 989 - checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" 990 dependencies = [ 991 "proc-macro2", 992 "quote", ··· 994 ] 995 996 [[package]] 997 - name = "tracing" 998 - version = "0.1.40" 999 source = "registry+https://github.com/rust-lang/crates.io-index" 1000 - checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1001 dependencies = [ 1002 - "pin-project-lite", 1003 - "tracing-core", 1004 ] 1005 1006 [[package]] 1007 - name = "tracing-core" 1008 - version = "0.1.32" 1009 - source = "registry+https://github.com/rust-lang/crates.io-index" 1010 - checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1011 - 1012 - [[package]] 1013 - name = "tsk" 1014 - version = "0.1.0" 1015 dependencies = [ 1016 "clap", 1017 "clap_complete", 1018 "clap_mangen", 1019 "edit", 1020 - "iocraft", 1021 "nix", 1022 - "smallstr", 1023 - "smol", 1024 - "tabwriter", 1025 "thiserror", 1026 "xattr", 1027 ] 1028 1029 [[package]] 1030 name = "unicode-ident" 1031 - version = "1.0.13" 1032 source = "registry+https://github.com/rust-lang/crates.io-index" 1033 - checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 1034 1035 [[package]] 1036 - name = "unicode-linebreak" 1037 - version = "0.1.5" 1038 source = "registry+https://github.com/rust-lang/crates.io-index" 1039 - checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" 1040 1041 [[package]] 1042 - name = "unicode-width" 1043 - version = "0.1.14" 1044 source = "registry+https://github.com/rust-lang/crates.io-index" 1045 - checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 1046 1047 [[package]] 1048 name = "utf8parse" ··· 1051 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1052 1053 [[package]] 1054 - name = "uuid" 1055 - version = "1.10.0" 1056 source = "registry+https://github.com/rust-lang/crates.io-index" 1057 - checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" 1058 dependencies = [ 1059 - "getrandom", 1060 ] 1061 1062 [[package]] 1063 - name = "version_check" 1064 - version = "0.1.5" 1065 - source = "registry+https://github.com/rust-lang/crates.io-index" 1066 - checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" 1067 - 1068 - [[package]] 1069 - name = "version_check" 1070 - version = "0.9.5" 1071 - source = "registry+https://github.com/rust-lang/crates.io-index" 1072 - checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1073 - 1074 - [[package]] 1075 - name = "wasi" 1076 - version = "0.11.0+wasi-snapshot-preview1" 1077 - source = "registry+https://github.com/rust-lang/crates.io-index" 1078 - checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1079 - 1080 - [[package]] 1081 name = "which" 1082 version = "4.4.2" 1083 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1086 "either", 1087 "home", 1088 "once_cell", 1089 - "rustix", 1090 - ] 1091 - 1092 - [[package]] 1093 - name = "winapi" 1094 - version = "0.3.9" 1095 - source = "registry+https://github.com/rust-lang/crates.io-index" 1096 - checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1097 - dependencies = [ 1098 - "winapi-i686-pc-windows-gnu", 1099 - "winapi-x86_64-pc-windows-gnu", 1100 ] 1101 1102 [[package]] 1103 - name = "winapi-i686-pc-windows-gnu" 1104 - version = "0.4.0" 1105 - source = "registry+https://github.com/rust-lang/crates.io-index" 1106 - checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1107 - 1108 - [[package]] 1109 - name = "winapi-x86_64-pc-windows-gnu" 1110 - version = "0.4.0" 1111 source = "registry+https://github.com/rust-lang/crates.io-index" 1112 - checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1113 1114 [[package]] 1115 name = "windows-sys" 1116 - version = "0.52.0" 1117 source = "registry+https://github.com/rust-lang/crates.io-index" 1118 - checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1119 dependencies = [ 1120 - "windows-targets", 1121 ] 1122 1123 [[package]] 1124 name = "windows-sys" 1125 - version = "0.59.0" 1126 source = "registry+https://github.com/rust-lang/crates.io-index" 1127 - checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1128 dependencies = [ 1129 - "windows-targets", 1130 ] 1131 1132 [[package]] ··· 1135 source = "registry+https://github.com/rust-lang/crates.io-index" 1136 checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1137 dependencies = [ 1138 - "windows_aarch64_gnullvm", 1139 - "windows_aarch64_msvc", 1140 - "windows_i686_gnu", 1141 - "windows_i686_gnullvm", 1142 - "windows_i686_msvc", 1143 - "windows_x86_64_gnu", 1144 - "windows_x86_64_gnullvm", 1145 - "windows_x86_64_msvc", 1146 ] 1147 1148 [[package]] ··· 1152 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1153 1154 [[package]] 1155 name = "windows_aarch64_msvc" 1156 version = "0.52.6" 1157 source = "registry+https://github.com/rust-lang/crates.io-index" 1158 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1159 1160 [[package]] 1161 name = "windows_i686_gnu" 1162 version = "0.52.6" 1163 source = "registry+https://github.com/rust-lang/crates.io-index" 1164 checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1165 1166 [[package]] 1167 name = "windows_i686_gnullvm" 1168 version = "0.52.6" 1169 source = "registry+https://github.com/rust-lang/crates.io-index" 1170 checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1171 1172 [[package]] 1173 name = "windows_i686_msvc" 1174 version = "0.52.6" 1175 source = "registry+https://github.com/rust-lang/crates.io-index" 1176 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1177 1178 [[package]] 1179 name = "windows_x86_64_gnu" 1180 version = "0.52.6" 1181 source = "registry+https://github.com/rust-lang/crates.io-index" 1182 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1183 1184 [[package]] 1185 name = "windows_x86_64_gnullvm" 1186 version = "0.52.6" 1187 source = "registry+https://github.com/rust-lang/crates.io-index" 1188 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1189 1190 [[package]] 1191 name = "windows_x86_64_msvc" 1192 version = "0.52.6" 1193 source = "registry+https://github.com/rust-lang/crates.io-index" 1194 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1195 1196 [[package]] 1197 name = "xattr" 1198 - version = "1.3.1" 1199 source = "registry+https://github.com/rust-lang/crates.io-index" 1200 - checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" 1201 dependencies = [ 1202 "libc", 1203 - "linux-raw-sys", 1204 - "rustix", 1205 ]
··· 1 # This file is automatically @generated by Cargo. 2 # It is not intended for manual editing. 3 + version = 4 4 5 [[package]] 6 name = "anstream" 7 + version = "0.6.20" 8 source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" 10 dependencies = [ 11 "anstyle", 12 "anstyle-parse", ··· 19 20 [[package]] 21 name = "anstyle" 22 + version = "1.0.11" 23 source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 25 26 [[package]] 27 name = "anstyle-parse" 28 + version = "0.2.7" 29 source = "registry+https://github.com/rust-lang/crates.io-index" 30 + checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 31 dependencies = [ 32 "utf8parse", 33 ] 34 35 [[package]] 36 name = "anstyle-query" 37 + version = "1.1.4" 38 source = "registry+https://github.com/rust-lang/crates.io-index" 39 + checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 40 dependencies = [ 41 + "windows-sys 0.60.2", 42 ] 43 44 [[package]] 45 name = "anstyle-wincon" 46 + version = "3.0.10" 47 source = "registry+https://github.com/rust-lang/crates.io-index" 48 + checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 49 dependencies = [ 50 "anstyle", 51 + "once_cell_polyfill", 52 + "windows-sys 0.60.2", 53 ] 54 55 [[package]] 56 name = "bitflags" 57 + version = "2.9.4" 58 source = "registry+https://github.com/rust-lang/crates.io-index" 59 + checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" 60 61 [[package]] 62 name = "cfg-if" 63 + version = "1.0.3" 64 source = "registry+https://github.com/rust-lang/crates.io-index" 65 + checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 66 67 [[package]] 68 name = "cfg_aliases" ··· 72 73 [[package]] 74 name = "clap" 75 + version = "4.5.47" 76 source = "registry+https://github.com/rust-lang/crates.io-index" 77 + checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" 78 dependencies = [ 79 "clap_builder", 80 "clap_derive", ··· 82 83 [[package]] 84 name = "clap_builder" 85 + version = "4.5.47" 86 source = "registry+https://github.com/rust-lang/crates.io-index" 87 + checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" 88 dependencies = [ 89 "anstream", 90 "anstyle", ··· 94 95 [[package]] 96 name = "clap_complete" 97 + version = "4.5.57" 98 source = "registry+https://github.com/rust-lang/crates.io-index" 99 + checksum = "4d9501bd3f5f09f7bbee01da9a511073ed30a80cd7a509f1214bb74eadea71ad" 100 dependencies = [ 101 "clap", 102 ] 103 104 [[package]] 105 name = "clap_derive" 106 + version = "4.5.47" 107 source = "registry+https://github.com/rust-lang/crates.io-index" 108 + checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" 109 dependencies = [ 110 "heck", 111 "proc-macro2", ··· 115 116 [[package]] 117 name = "clap_lex" 118 + version = "0.7.5" 119 source = "registry+https://github.com/rust-lang/crates.io-index" 120 + checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 121 122 [[package]] 123 name = "clap_mangen" 124 + version = "0.2.29" 125 source = "registry+https://github.com/rust-lang/crates.io-index" 126 + checksum = "27b4c3c54b30f0d9adcb47f25f61fcce35c4dd8916638c6b82fbd5f4fb4179e2" 127 dependencies = [ 128 "clap", 129 "roff", ··· 131 132 [[package]] 133 name = "colorchoice" 134 + version = "1.0.4" 135 source = "registry+https://github.com/rust-lang/crates.io-index" 136 + checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 137 138 [[package]] 139 + name = "colored" 140 + version = "3.0.0" 141 source = "registry+https://github.com/rust-lang/crates.io-index" 142 + checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" 143 dependencies = [ 144 + "windows-sys 0.59.0", 145 ] 146 147 [[package]] 148 + name = "displaydoc" 149 + version = "0.2.5" 150 source = "registry+https://github.com/rust-lang/crates.io-index" 151 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 152 dependencies = [ 153 + "proc-macro2", 154 + "quote", 155 + "syn", 156 ] 157 158 [[package]] ··· 167 168 [[package]] 169 name = "either" 170 + version = "1.15.0" 171 source = "registry+https://github.com/rust-lang/crates.io-index" 172 + checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 173 174 [[package]] 175 name = "errno" 176 + version = "0.3.13" 177 source = "registry+https://github.com/rust-lang/crates.io-index" 178 + checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 179 dependencies = [ 180 "libc", 181 + "windows-sys 0.60.2", 182 ] 183 184 [[package]] 185 name = "fastrand" 186 + version = "2.3.0" 187 source = "registry+https://github.com/rust-lang/crates.io-index" 188 + checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 189 190 [[package]] 191 + name = "form_urlencoded" 192 + version = "1.2.2" 193 source = "registry+https://github.com/rust-lang/crates.io-index" 194 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 195 dependencies = [ 196 + "percent-encoding", 197 ] 198 199 [[package]] 200 + name = "getrandom" 201 + version = "0.3.3" 202 source = "registry+https://github.com/rust-lang/crates.io-index" 203 + checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 204 dependencies = [ 205 + "cfg-if", 206 + "libc", 207 + "r-efi", 208 + "wasi", 209 ] 210 211 [[package]] 212 + name = "heck" 213 + version = "0.5.0" 214 source = "registry+https://github.com/rust-lang/crates.io-index" 215 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 216 217 [[package]] 218 + name = "home" 219 + version = "0.5.11" 220 source = "registry+https://github.com/rust-lang/crates.io-index" 221 + checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 222 dependencies = [ 223 + "windows-sys 0.59.0", 224 ] 225 226 [[package]] 227 + name = "icu_collections" 228 + version = "2.0.0" 229 source = "registry+https://github.com/rust-lang/crates.io-index" 230 + checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 231 dependencies = [ 232 + "displaydoc", 233 + "potential_utf", 234 + "yoke", 235 + "zerofrom", 236 + "zerovec", 237 ] 238 239 [[package]] 240 + name = "icu_locale_core" 241 + version = "2.0.0" 242 source = "registry+https://github.com/rust-lang/crates.io-index" 243 + checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 244 dependencies = [ 245 + "displaydoc", 246 + "litemap", 247 + "tinystr", 248 + "writeable", 249 + "zerovec", 250 ] 251 252 [[package]] 253 + name = "icu_normalizer" 254 + version = "2.0.0" 255 source = "registry+https://github.com/rust-lang/crates.io-index" 256 + checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 257 + dependencies = [ 258 + "displaydoc", 259 + "icu_collections", 260 + "icu_normalizer_data", 261 + "icu_properties", 262 + "icu_provider", 263 + "smallvec", 264 + "zerovec", 265 + ] 266 267 [[package]] 268 + name = "icu_normalizer_data" 269 + version = "2.0.0" 270 source = "registry+https://github.com/rust-lang/crates.io-index" 271 + checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 272 273 [[package]] 274 + name = "icu_properties" 275 + version = "2.0.1" 276 source = "registry+https://github.com/rust-lang/crates.io-index" 277 + checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 278 dependencies = [ 279 + "displaydoc", 280 + "icu_collections", 281 + "icu_locale_core", 282 + "icu_properties_data", 283 + "icu_provider", 284 + "potential_utf", 285 + "zerotrie", 286 + "zerovec", 287 ] 288 289 [[package]] 290 + name = "icu_properties_data" 291 + version = "2.0.1" 292 source = "registry+https://github.com/rust-lang/crates.io-index" 293 + checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 294 295 [[package]] 296 + name = "icu_provider" 297 + version = "2.0.0" 298 source = "registry+https://github.com/rust-lang/crates.io-index" 299 + checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 300 dependencies = [ 301 + "displaydoc", 302 + "icu_locale_core", 303 + "stable_deref_trait", 304 + "tinystr", 305 + "writeable", 306 + "yoke", 307 + "zerofrom", 308 + "zerotrie", 309 + "zerovec", 310 ] 311 312 [[package]] 313 + name = "idna" 314 + version = "1.1.0" 315 source = "registry+https://github.com/rust-lang/crates.io-index" 316 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 317 dependencies = [ 318 + "idna_adapter", 319 + "smallvec", 320 + "utf8_iter", 321 ] 322 323 [[package]] 324 + name = "idna_adapter" 325 + version = "1.2.1" 326 source = "registry+https://github.com/rust-lang/crates.io-index" 327 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 328 dependencies = [ 329 + "icu_normalizer", 330 + "icu_properties", 331 ] 332 333 [[package]] 334 + name = "is-docker" 335 + version = "0.2.0" 336 source = "registry+https://github.com/rust-lang/crates.io-index" 337 + checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" 338 dependencies = [ 339 + "once_cell", 340 ] 341 342 [[package]] 343 + name = "is-wsl" 344 + version = "0.4.0" 345 source = "registry+https://github.com/rust-lang/crates.io-index" 346 + checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" 347 dependencies = [ 348 + "is-docker", 349 + "once_cell", 350 ] 351 352 [[package]] ··· 356 checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 357 358 [[package]] 359 + name = "itertools" 360 + version = "0.14.0" 361 source = "registry+https://github.com/rust-lang/crates.io-index" 362 + checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 363 dependencies = [ 364 + "either", 365 ] 366 367 [[package]] 368 + name = "libc" 369 + version = "0.2.175" 370 source = "registry+https://github.com/rust-lang/crates.io-index" 371 + checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" 372 373 [[package]] 374 + name = "linux-raw-sys" 375 + version = "0.4.15" 376 source = "registry+https://github.com/rust-lang/crates.io-index" 377 + checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 378 379 [[package]] 380 + name = "linux-raw-sys" 381 + version = "0.9.4" 382 source = "registry+https://github.com/rust-lang/crates.io-index" 383 + checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 384 385 [[package]] 386 + name = "litemap" 387 + version = "0.8.0" 388 source = "registry+https://github.com/rust-lang/crates.io-index" 389 + checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 390 391 [[package]] 392 name = "nix" 393 + version = "0.30.1" 394 source = "registry+https://github.com/rust-lang/crates.io-index" 395 + checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 396 dependencies = [ 397 "bitflags", 398 "cfg-if", ··· 401 ] 402 403 [[package]] 404 name = "once_cell" 405 + version = "1.21.3" 406 source = "registry+https://github.com/rust-lang/crates.io-index" 407 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 408 409 [[package]] 410 + name = "once_cell_polyfill" 411 + version = "1.70.1" 412 source = "registry+https://github.com/rust-lang/crates.io-index" 413 + checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 414 415 [[package]] 416 + name = "open" 417 + version = "5.3.2" 418 source = "registry+https://github.com/rust-lang/crates.io-index" 419 + checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" 420 dependencies = [ 421 + "is-wsl", 422 "libc", 423 + "pathdiff", 424 ] 425 426 [[package]] 427 + name = "pathdiff" 428 + version = "0.2.3" 429 source = "registry+https://github.com/rust-lang/crates.io-index" 430 + checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" 431 432 [[package]] 433 + name = "percent-encoding" 434 + version = "2.3.2" 435 source = "registry+https://github.com/rust-lang/crates.io-index" 436 + checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 437 438 [[package]] 439 + name = "potential_utf" 440 + version = "0.1.3" 441 source = "registry+https://github.com/rust-lang/crates.io-index" 442 + checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" 443 dependencies = [ 444 + "zerovec", 445 ] 446 447 [[package]] 448 name = "proc-macro2" 449 + version = "1.0.101" 450 source = "registry+https://github.com/rust-lang/crates.io-index" 451 + checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 452 dependencies = [ 453 "unicode-ident", 454 ] 455 456 [[package]] 457 name = "quote" 458 + version = "1.0.40" 459 source = "registry+https://github.com/rust-lang/crates.io-index" 460 + checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 461 dependencies = [ 462 "proc-macro2", 463 ] 464 465 [[package]] 466 + name = "r-efi" 467 + version = "5.3.0" 468 source = "registry+https://github.com/rust-lang/crates.io-index" 469 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 470 471 [[package]] 472 name = "roff" ··· 476 477 [[package]] 478 name = "rustix" 479 + version = "0.38.44" 480 source = "registry+https://github.com/rust-lang/crates.io-index" 481 + checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 482 dependencies = [ 483 "bitflags", 484 "errno", 485 "libc", 486 + "linux-raw-sys 0.4.15", 487 + "windows-sys 0.59.0", 488 ] 489 490 [[package]] 491 + name = "rustix" 492 + version = "1.0.8" 493 source = "registry+https://github.com/rust-lang/crates.io-index" 494 + checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" 495 + dependencies = [ 496 + "bitflags", 497 + "errno", 498 + "libc", 499 + "linux-raw-sys 0.9.4", 500 + "windows-sys 0.60.2", 501 + ] 502 503 [[package]] 504 name = "serde" 505 + version = "1.0.219" 506 source = "registry+https://github.com/rust-lang/crates.io-index" 507 + checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 508 dependencies = [ 509 "serde_derive", 510 ] 511 512 [[package]] 513 name = "serde_derive" 514 + version = "1.0.219" 515 source = "registry+https://github.com/rust-lang/crates.io-index" 516 + checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 517 dependencies = [ 518 "proc-macro2", 519 "quote", ··· 521 ] 522 523 [[package]] 524 name = "smallvec" 525 + version = "1.15.1" 526 source = "registry+https://github.com/rust-lang/crates.io-index" 527 + checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 528 529 [[package]] 530 + name = "stable_deref_trait" 531 + version = "1.2.0" 532 source = "registry+https://github.com/rust-lang/crates.io-index" 533 + checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 534 535 [[package]] 536 name = "strsim" ··· 540 541 [[package]] 542 name = "syn" 543 + version = "2.0.106" 544 source = "registry+https://github.com/rust-lang/crates.io-index" 545 + checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 546 dependencies = [ 547 "proc-macro2", 548 "quote", ··· 550 ] 551 552 [[package]] 553 + name = "synstructure" 554 + version = "0.13.2" 555 source = "registry+https://github.com/rust-lang/crates.io-index" 556 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 557 dependencies = [ 558 + "proc-macro2", 559 + "quote", 560 + "syn", 561 ] 562 563 [[package]] 564 name = "tempfile" 565 + version = "3.21.0" 566 source = "registry+https://github.com/rust-lang/crates.io-index" 567 + checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" 568 dependencies = [ 569 "fastrand", 570 + "getrandom", 571 "once_cell", 572 + "rustix 1.0.8", 573 + "windows-sys 0.60.2", 574 ] 575 576 [[package]] 577 name = "thiserror" 578 + version = "2.0.16" 579 source = "registry+https://github.com/rust-lang/crates.io-index" 580 + checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" 581 dependencies = [ 582 "thiserror-impl", 583 ] 584 585 [[package]] 586 name = "thiserror-impl" 587 + version = "2.0.16" 588 source = "registry+https://github.com/rust-lang/crates.io-index" 589 + checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" 590 dependencies = [ 591 "proc-macro2", 592 "quote", ··· 594 ] 595 596 [[package]] 597 + name = "tinystr" 598 + version = "0.8.1" 599 source = "registry+https://github.com/rust-lang/crates.io-index" 600 + checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 601 dependencies = [ 602 + "displaydoc", 603 + "zerovec", 604 ] 605 606 [[package]] 607 + name = "tsk-cli" 608 + version = "0.4.0" 609 dependencies = [ 610 "clap", 611 "clap_complete", 612 "clap_mangen", 613 + "colored", 614 "edit", 615 + "itertools", 616 "nix", 617 + "open", 618 "thiserror", 619 + "url", 620 "xattr", 621 ] 622 623 [[package]] 624 name = "unicode-ident" 625 + version = "1.0.18" 626 source = "registry+https://github.com/rust-lang/crates.io-index" 627 + checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 628 629 [[package]] 630 + name = "url" 631 + version = "2.5.7" 632 source = "registry+https://github.com/rust-lang/crates.io-index" 633 + checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 634 + dependencies = [ 635 + "form_urlencoded", 636 + "idna", 637 + "percent-encoding", 638 + "serde", 639 + ] 640 641 [[package]] 642 + name = "utf8_iter" 643 + version = "1.0.4" 644 source = "registry+https://github.com/rust-lang/crates.io-index" 645 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 646 647 [[package]] 648 name = "utf8parse" ··· 651 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 652 653 [[package]] 654 + name = "wasi" 655 + version = "0.14.4+wasi-0.2.4" 656 source = "registry+https://github.com/rust-lang/crates.io-index" 657 + checksum = "88a5f4a424faf49c3c2c344f166f0662341d470ea185e939657aaff130f0ec4a" 658 dependencies = [ 659 + "wit-bindgen", 660 ] 661 662 [[package]] 663 name = "which" 664 version = "4.4.2" 665 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 668 "either", 669 "home", 670 "once_cell", 671 + "rustix 0.38.44", 672 ] 673 674 [[package]] 675 + name = "windows-link" 676 + version = "0.1.3" 677 source = "registry+https://github.com/rust-lang/crates.io-index" 678 + checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 679 680 [[package]] 681 name = "windows-sys" 682 + version = "0.59.0" 683 source = "registry+https://github.com/rust-lang/crates.io-index" 684 + checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 685 dependencies = [ 686 + "windows-targets 0.52.6", 687 ] 688 689 [[package]] 690 name = "windows-sys" 691 + version = "0.60.2" 692 source = "registry+https://github.com/rust-lang/crates.io-index" 693 + checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 694 dependencies = [ 695 + "windows-targets 0.53.3", 696 ] 697 698 [[package]] ··· 701 source = "registry+https://github.com/rust-lang/crates.io-index" 702 checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 703 dependencies = [ 704 + "windows_aarch64_gnullvm 0.52.6", 705 + "windows_aarch64_msvc 0.52.6", 706 + "windows_i686_gnu 0.52.6", 707 + "windows_i686_gnullvm 0.52.6", 708 + "windows_i686_msvc 0.52.6", 709 + "windows_x86_64_gnu 0.52.6", 710 + "windows_x86_64_gnullvm 0.52.6", 711 + "windows_x86_64_msvc 0.52.6", 712 + ] 713 + 714 + [[package]] 715 + name = "windows-targets" 716 + version = "0.53.3" 717 + source = "registry+https://github.com/rust-lang/crates.io-index" 718 + checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" 719 + dependencies = [ 720 + "windows-link", 721 + "windows_aarch64_gnullvm 0.53.0", 722 + "windows_aarch64_msvc 0.53.0", 723 + "windows_i686_gnu 0.53.0", 724 + "windows_i686_gnullvm 0.53.0", 725 + "windows_i686_msvc 0.53.0", 726 + "windows_x86_64_gnu 0.53.0", 727 + "windows_x86_64_gnullvm 0.53.0", 728 + "windows_x86_64_msvc 0.53.0", 729 ] 730 731 [[package]] ··· 735 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 736 737 [[package]] 738 + name = "windows_aarch64_gnullvm" 739 + version = "0.53.0" 740 + source = "registry+https://github.com/rust-lang/crates.io-index" 741 + checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 742 + 743 + [[package]] 744 name = "windows_aarch64_msvc" 745 version = "0.52.6" 746 source = "registry+https://github.com/rust-lang/crates.io-index" 747 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 748 749 [[package]] 750 + name = "windows_aarch64_msvc" 751 + version = "0.53.0" 752 + source = "registry+https://github.com/rust-lang/crates.io-index" 753 + checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 754 + 755 + [[package]] 756 name = "windows_i686_gnu" 757 version = "0.52.6" 758 source = "registry+https://github.com/rust-lang/crates.io-index" 759 checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 760 761 [[package]] 762 + name = "windows_i686_gnu" 763 + version = "0.53.0" 764 + source = "registry+https://github.com/rust-lang/crates.io-index" 765 + checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 766 + 767 + [[package]] 768 name = "windows_i686_gnullvm" 769 version = "0.52.6" 770 source = "registry+https://github.com/rust-lang/crates.io-index" 771 checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 772 773 [[package]] 774 + name = "windows_i686_gnullvm" 775 + version = "0.53.0" 776 + source = "registry+https://github.com/rust-lang/crates.io-index" 777 + checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 778 + 779 + [[package]] 780 name = "windows_i686_msvc" 781 version = "0.52.6" 782 source = "registry+https://github.com/rust-lang/crates.io-index" 783 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 784 785 [[package]] 786 + name = "windows_i686_msvc" 787 + version = "0.53.0" 788 + source = "registry+https://github.com/rust-lang/crates.io-index" 789 + checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 790 + 791 + [[package]] 792 name = "windows_x86_64_gnu" 793 version = "0.52.6" 794 source = "registry+https://github.com/rust-lang/crates.io-index" 795 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 796 797 [[package]] 798 + name = "windows_x86_64_gnu" 799 + version = "0.53.0" 800 + source = "registry+https://github.com/rust-lang/crates.io-index" 801 + checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 802 + 803 + [[package]] 804 name = "windows_x86_64_gnullvm" 805 version = "0.52.6" 806 source = "registry+https://github.com/rust-lang/crates.io-index" 807 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 808 809 [[package]] 810 + name = "windows_x86_64_gnullvm" 811 + version = "0.53.0" 812 + source = "registry+https://github.com/rust-lang/crates.io-index" 813 + checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 814 + 815 + [[package]] 816 name = "windows_x86_64_msvc" 817 version = "0.52.6" 818 source = "registry+https://github.com/rust-lang/crates.io-index" 819 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 820 821 [[package]] 822 + name = "windows_x86_64_msvc" 823 + version = "0.53.0" 824 + source = "registry+https://github.com/rust-lang/crates.io-index" 825 + checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 826 + 827 + [[package]] 828 + name = "wit-bindgen" 829 + version = "0.45.1" 830 + source = "registry+https://github.com/rust-lang/crates.io-index" 831 + checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" 832 + 833 + [[package]] 834 + name = "writeable" 835 + version = "0.6.1" 836 + source = "registry+https://github.com/rust-lang/crates.io-index" 837 + checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 838 + 839 + [[package]] 840 name = "xattr" 841 + version = "1.5.1" 842 source = "registry+https://github.com/rust-lang/crates.io-index" 843 + checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" 844 dependencies = [ 845 "libc", 846 + "rustix 1.0.8", 847 + ] 848 + 849 + [[package]] 850 + name = "yoke" 851 + version = "0.8.0" 852 + source = "registry+https://github.com/rust-lang/crates.io-index" 853 + checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 854 + dependencies = [ 855 + "serde", 856 + "stable_deref_trait", 857 + "yoke-derive", 858 + "zerofrom", 859 + ] 860 + 861 + [[package]] 862 + name = "yoke-derive" 863 + version = "0.8.0" 864 + source = "registry+https://github.com/rust-lang/crates.io-index" 865 + checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 866 + dependencies = [ 867 + "proc-macro2", 868 + "quote", 869 + "syn", 870 + "synstructure", 871 + ] 872 + 873 + [[package]] 874 + name = "zerofrom" 875 + version = "0.1.6" 876 + source = "registry+https://github.com/rust-lang/crates.io-index" 877 + checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 878 + dependencies = [ 879 + "zerofrom-derive", 880 + ] 881 + 882 + [[package]] 883 + name = "zerofrom-derive" 884 + version = "0.1.6" 885 + source = "registry+https://github.com/rust-lang/crates.io-index" 886 + checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 887 + dependencies = [ 888 + "proc-macro2", 889 + "quote", 890 + "syn", 891 + "synstructure", 892 + ] 893 + 894 + [[package]] 895 + name = "zerotrie" 896 + version = "0.2.2" 897 + source = "registry+https://github.com/rust-lang/crates.io-index" 898 + checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 899 + dependencies = [ 900 + "displaydoc", 901 + "yoke", 902 + "zerofrom", 903 + ] 904 + 905 + [[package]] 906 + name = "zerovec" 907 + version = "0.11.4" 908 + source = "registry+https://github.com/rust-lang/crates.io-index" 909 + checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" 910 + dependencies = [ 911 + "yoke", 912 + "zerofrom", 913 + "zerovec-derive", 914 + ] 915 + 916 + [[package]] 917 + name = "zerovec-derive" 918 + version = "0.11.1" 919 + source = "registry+https://github.com/rust-lang/crates.io-index" 920 + checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 921 + dependencies = [ 922 + "proc-macro2", 923 + "quote", 924 + "syn", 925 ]
+26 -13
Cargo.toml
··· 1 [package] 2 name = "tsk" 3 - version = "0.1.0" 4 - edition = "2021" 5 6 [dependencies] 7 - clap = { version = "4.5.18", features = ["derive", "env"] } 8 - clap_complete = "4.5.29" 9 - clap_mangen = "0.2.23" 10 - edit = "0.1.5" 11 - iocraft = "0.2.3" 12 - nix = { version = "0.29.0", features = ["fs"] } 13 - smallstr = { version = "0.3.0", features = ["std"] } 14 - smol = "2.0.2" 15 - tabwriter = "1.4.0" 16 - thiserror = "1.0.64" 17 - xattr = "1.3.1"
··· 1 [package] 2 + name = "tsk-cli" 3 + version = "0.4.0" 4 + edition = "2024" 5 + publish = true 6 + license-file = "LICENSE" 7 + description = "A command-line first, file-system based task manager" 8 + repository = "https://codeberg.org/ngp/tsk" 9 + homepage = "https://tsk.ngp.computer/" 10 + readme = "readme" 11 + authors = ["Noah Pederson <noah@packetlost.dev>"] 12 + 13 + [[bin]] 14 name = "tsk" 15 + path = "src/main.rs" 16 17 [dependencies] 18 + clap = { version = "4", features = ["derive", "env"] } 19 + clap_complete = "4" 20 + edit = "0" 21 + nix = { version = "0", features = ["fs"] } 22 + thiserror = "2" 23 + url = "2" 24 + xattr = "1" 25 + colored = "3" 26 + open = "5" 27 + itertools = "0" 28 + 29 + [build-dependencies] 30 + clap_mangen = "0"
+21
LICENSE
···
··· 1 + MIT License 2 + 3 + Copyright (c) 2024 Noah Pederson 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+41
flake.nix
···
··· 1 + { 2 + inputs = { 3 + naersk.url = "github:nix-community/naersk/master"; 4 + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 5 + utils.url = "github:numtide/flake-utils"; 6 + }; 7 + 8 + outputs = 9 + { 10 + self, 11 + nixpkgs, 12 + utils, 13 + naersk, 14 + }: 15 + utils.lib.eachDefaultSystem ( 16 + system: 17 + let 18 + pkgs = import nixpkgs { inherit system; }; 19 + naersk-lib = pkgs.callPackage naersk { }; 20 + in 21 + { 22 + defaultPackage = naersk-lib.buildPackage ./.; 23 + devShell = 24 + with pkgs; 25 + mkShell { 26 + buildInputs = [ 27 + libiconv 28 + cargo 29 + rustc 30 + rustfmt 31 + rust-analyzer 32 + rustPackages.clippy 33 + plan9port 34 + pandoc 35 + codeberg-cli 36 + ]; 37 + RUST_SRC_PATH = rustPlatform.rustLibSrc; 38 + }; 39 + } 40 + ); 41 + }
+7
mkfile
···
··· 1 + 2 + index.html: readme 3 + pandoc -s -f markdown -t html5 -o $target $prereq 4 + 5 + deploy:V: index.html readme 6 + rsync -rv $prereq pgs.sh:/tsk/ 7 +
+212
readme
···
··· 1 + tsk 2 + === 3 + 4 + A fast & simple CLI task manager 5 + -------------------------------- 6 + 7 + The motivation for tsk is simple: make managing tasks as fast and easy as 8 + possible with a focus on small, ephemeral tasks. 9 + 10 + Secondary goals include: 11 + 12 + - Provide the minimum amount of features necessary to be usable by a team 13 + - Support local and non-local workspaces 14 + - Be adaptable to almost any environment, even employer-mandated JIRA use 15 + - Be flexible, within reason 16 + 17 + tsk takes inspiration from git and FORTH and is expected to be used alongside 18 + the former. 19 + 20 + Dependencies 21 + ------------ 22 + 23 + tsk is written in Rust. To compile from source, a recent Rust toolchain is 24 + required. As of now, it is developed using Rust 1.81.0. 25 + 26 + Additionally, for fuzzy-finding functionality, the fzf command must be installed 27 + and in the shell's PATH. 28 + 29 + https://github.com/junegunn/fzf 30 + 31 + tsk workspaces must be created on filesystems that support symlinking. 32 + 33 + Task-level metadata requires Linux's xattr(7) API and a filesystem that supports 34 + it. Patches that implement this for other operating systems are welcome. 35 + 36 + tsk expects to run on POSIX-like systems. Microsoft Windows and other 37 + non-UNIX-ey operating systems will never be directly supported. 38 + 39 + 40 + Installation 41 + ------------ 42 + 43 + ```sh 44 + cargo install --locked tsk-cli 45 + ``` 46 + 47 + 48 + Building 49 + -------- 50 + 51 + ```sh 52 + cargo install --path . 53 + ``` 54 + 55 + Make sure ~/.cargo/bin is in your PATH. 56 + 57 + Overview 58 + -------- 59 + 60 + A summary of commands and their functionality can be seen with: 61 + 62 + tsk help 63 + 64 + tsk uses plain text files for all of its functionality. A workspace is a folder 65 + that contains a .tsk/ directory created with the `tsk init` command. The 66 + presence of a .tsk/ folder is searched recursively upwards until a filesystem 67 + boundary or root is encountered. This means you can nest workspaces and use 68 + folders to namespace tasks while also using tsk commands at any location within 69 + a workspace. 70 + 71 + New tasks are created with the `tsk push` command. A title is always required, 72 + but can be modified later. A unique identifier is selected automatically and a 73 + file with the title and any body contents supplied are stored in the 74 + .tsk/archive folder. A symlink is then created in the .tsk/tasks folder marking 75 + the task as "open." The task is then added to the top of the "stack" by having 76 + its tsk-ID and title added to the .tsk/index file. 77 + 78 + The contents of the stack may be printed using the `tsk list` command. 79 + 80 + Tasks are marked as "completed" and removed from the index with the `tsk drop` 81 + command. They will remain in the .tsk/archive folder, but are excluded from 82 + fuzzy searches by default. 83 + 84 + The priority of a task may be manipulated in any of several ways: 85 + 86 + `tsk swap` swaps the top two task on the stack 87 + 88 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 89 + โ”‚ tsk-100 โ”‚ โ”‚ tsk-102 โ”‚ 90 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ–ฒโ”€โ”˜ 91 + โ”‚ โ”‚ 92 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ” 93 + โ”‚ tsk-102 โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ tsk-100 โ”‚ 94 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 95 + 96 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 97 + โ”‚ tsk-108 โ”‚ โ”‚ tsk-108 โ”‚ 98 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 99 + 100 + `tsk rot` moves the 3rd task on the stack to the top of the stack and shifts 101 + the first and second down 102 + 103 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 104 + โ”‚ tsk-100 โ”‚ โ”‚ tsk-108 โ—„โ”€โ” 105 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜ โ”‚ 106 + โ”‚ โ”‚ 107 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ” โ”‚ 108 + โ”‚ tsk-102 โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ tsk-100 โ”‚ โ”‚ 109 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜ โ”‚ 110 + โ”‚ โ”‚ 111 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ” โ”‚ 112 + โ”‚ tsk-108 โ”‚ โ”‚ tsk-102 โ”œโ”€โ”˜ 113 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 114 + 115 + `tsk tor` moves the task on the top of the stack behind the third, shifting the 116 + second and third tasks up. 117 + 118 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 119 + โ”‚ tsk-100 โ”‚ โ”‚ tsk-102 โ”œโ”€โ” 120 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ–ฒโ”€โ”€โ”€โ”€โ”˜ โ”‚ 121 + โ”‚ โ”‚ 122 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ” โ”‚ 123 + โ”‚ tsk-102 โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ tsk-108 โ”‚ โ”‚ 124 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ–ฒโ”€โ”€โ”€โ”€โ”˜ โ”‚ 125 + โ”‚ โ”‚ 126 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ” โ”‚ 127 + โ”‚ tsk-108 โ”‚ โ”‚ tsk-100 โ—„โ”€โ”˜ 128 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 129 + 130 + `tsk prioritize` will take a selected task and move it to the top of the stack 131 + from any other position in the stack. It is selected either by ID or using fuzzy 132 + finding. 133 + 134 + `tsk deprioritize` moves a selected task to the bottom of the stack from any 135 + position. 136 + 137 + Roadmap 138 + ------- 139 + 140 + - Configurable workspace-scoped prefix tags (tsk- vs example-) 141 + - Extended Attribute-based Metadata 142 + - Task Linking 143 + - IMAP4/SMTP-based synchronization and sharing 144 + - Export + sync with external task managers 145 + - GitLab 146 + - GitHub 147 + - JIRA 148 + - Obsidian 149 + - More? 150 + - tsk -> html export 151 + - Editor plugins 152 + - nvim-telescope 153 + - nvim 154 + - others? 155 + - Man page 156 + 157 + Format 158 + ------ 159 + 160 + The tsk text format can be thought of as a derivative of Markdown and scdoc, but 161 + not quite either. Markdown is a great baseline for rich-text while scdoc 162 + restricts itself to rich text formatting that can be displayed effectively in a 163 + terminal. As tsk's primary goal is to be a fast, terminal-centric task manager, 164 + this property is a *must.* 165 + 166 + Additionally, it should be similar enough to Markdown such that it is easy to 167 + export to other applications, as outlined above in the roadmap. 168 + 169 + Meanwhile, both Markdown and scdoc have some limitations and make choices that, 170 + while appropriate for their domain, are not appropriate for tsk. Some notable 171 + differences from both: 172 + 173 + - There is only one way to do any type of formatting 174 + - Hard line breaks are real, not imaginary 175 + - Inline formatting control characters must be surrounded by space, newline, or 176 + common punctuation 177 + 178 + A core feature of the format is *linking*. That is, references to other tasks 179 + utilizing wiki-link style links: `[[]]`. The content within the link is mapped 180 + to the local workspace if the `tsk-` prefix is used, or a mapped non-local 181 + workspace if another prefix is used. These mappings are specified using a text 182 + file within the .tsk folder. 183 + 184 + A quick overview of the format: 185 + 186 + - \!Bolded\! text is surrounded by exclamation marks (!) 187 + - \*Italicized\* text is surrounded by single asterisks (*) 188 + - \_Underlined\_ text is surrounded by underscores (_) 189 + - \~Strikethrough\~ text is surrounded by tildes (~) 190 + - \=Highlighted\= text is surrounded by equals signs (=) 191 + - \`Inline code\` is surrounded by backticks (`) 192 + 193 + Links like in Markdown, along with the wiki-style links documented above. 194 + Raw links can also be written as \<https://example.com\>. 195 + 196 + Misc 197 + ---- 198 + 199 + tsk is heavily inspired by git. It mimics its folder structure and some 200 + commands. The concept of the stack is inspired by FORTH and the observation that 201 + most of the time, only the top 3 priorities at any given moment matter and tasks 202 + tend to be created when they are most important. This facilitates small, 203 + frequent creation of tasks that help both document problems and manage 204 + fast-paced work environments. 205 + 206 + tsk is not intended to be checked into git, however there is not a reason that it 207 + cannot be. This repository's development is managed using tsk itself. 208 + 209 + Git does *not* track extended filesystem attributes. If you wish to avoid constantly 210 + re-indexing, use something like metastore: 211 + 212 + https://github.com/przemoc/metastore
+61
src/attrs.rs
···
··· 1 + use std::collections::BTreeMap; 2 + use std::collections::btree_map::Entry; 3 + use std::collections::btree_map::{IntoIter as BTreeIntoIter, Iter as BTreeMapIter}; 4 + use std::iter::Chain; 5 + 6 + type Map = BTreeMap<String, String>; 7 + 8 + #[allow(dead_code)] 9 + /// Holds xattributes in a way that allows for differentiating between attributes that have been 10 + /// added/modified or that were present when reading the file. This is an *optimization* over 11 + /// infrequently modified values. 12 + #[derive(Default, Clone, Debug)] 13 + pub(crate) struct Attrs { 14 + pub written: Map, 15 + pub updated: Map, 16 + } 17 + 18 + impl IntoIterator for Attrs { 19 + type Item = (String, String); 20 + 21 + type IntoIter = Chain<BTreeIntoIter<String, String>, BTreeIntoIter<String, String>>; 22 + 23 + fn into_iter(self) -> Self::IntoIter { 24 + self.written.into_iter().chain(self.updated) 25 + } 26 + } 27 + 28 + #[allow(dead_code)] 29 + impl Attrs { 30 + pub(crate) fn from_written(written: Map) -> Self { 31 + Self { 32 + written, 33 + ..Default::default() 34 + } 35 + } 36 + 37 + pub(crate) fn get(&self, key: &str) -> Option<&String> { 38 + self.updated.get(key).or_else(|| self.written.get(key)) 39 + } 40 + 41 + pub(crate) fn insert(&mut self, key: String, value: String) -> Option<String> { 42 + match self.updated.entry(key.clone()) { 43 + Entry::Occupied(mut e) => Some(e.insert(value)), 44 + Entry::Vacant(e) => { 45 + e.insert(value); 46 + let maybe_old_value = self.written.get(&key); 47 + maybe_old_value.cloned() 48 + } 49 + } 50 + } 51 + 52 + pub(crate) fn is_empty(&self) -> bool { 53 + self.updated.is_empty() && self.written.is_empty() 54 + } 55 + 56 + pub(crate) fn iter( 57 + &self, 58 + ) -> Chain<BTreeMapIter<'_, String, String>, BTreeMapIter<'_, String, String>> { 59 + self.written.iter().chain(self.updated.iter()) 60 + } 61 + }
+2
src/errors.rs
··· 27 #[allow(dead_code)] 28 #[error("An unexpected error occurred: {0}")] 29 Oops(Box<dyn std::error::Error>), 30 } 31 32 impl From<Infallible> for Error {
··· 27 #[allow(dead_code)] 28 #[error("An unexpected error occurred: {0}")] 29 Oops(Box<dyn std::error::Error>), 30 + #[error("System time/clock error: {0}")] 31 + SystemTime(#[from] std::time::SystemTimeError), 32 } 33 34 impl From<Infallible> for Error {
+14 -6
src/fzf.rs
··· 1 use crate::errors::{Error, Result}; 2 use std::fmt::Display; 3 use std::io::Write; 4 use std::process::{Command, Stdio}; ··· 6 7 /// Sends each item as a line to stdin to the `fzf` command and returns the selected item's string 8 /// representation as output 9 - pub fn select<I>(input: impl IntoIterator<Item = I>) -> Result<Option<I>> 10 where 11 - I: Display + FromStr, 12 - Error: From<<I as FromStr>::Err>, 13 { 14 - let mut child = Command::new("fzf") 15 - .args(["-d", "\t"]) 16 .stderr(Stdio::inherit()) 17 .stdin(Stdio::piped()) 18 .stdout(Stdio::piped()) ··· 20 // unwrap: this can never fail 21 let child_in = child.stdin.as_mut().unwrap(); 22 for item in input.into_iter() { 23 - write!(child_in, "{item}\n")?; 24 } 25 let output = child.wait_with_output()?; 26 if output.stdout.is_empty() {
··· 1 use crate::errors::{Error, Result}; 2 + use std::ffi::OsStr; 3 use std::fmt::Display; 4 use std::io::Write; 5 use std::process::{Command, Stdio}; ··· 7 8 /// Sends each item as a line to stdin to the `fzf` command and returns the selected item's string 9 /// representation as output 10 + pub fn select<I, O, S>( 11 + input: impl IntoIterator<Item = I>, 12 + extra: impl IntoIterator<Item = S>, 13 + ) -> Result<Option<O>> 14 where 15 + O: FromStr, 16 + I: Display, 17 + Error: From<<O as FromStr>::Err>, 18 + S: AsRef<OsStr>, 19 { 20 + let mut command = Command::new("fzf"); 21 + let mut child = command 22 + .args(extra) 23 + .arg("--read0") 24 .stderr(Stdio::inherit()) 25 .stdin(Stdio::piped()) 26 .stdout(Stdio::piped()) ··· 28 // unwrap: this can never fail 29 let child_in = child.stdin.as_mut().unwrap(); 30 for item in input.into_iter() { 31 + write!(child_in, "{item}\0")?; 32 } 33 let output = child.wait_with_output()?; 34 if output.stdout.is_empty() {
+234 -54
src/main.rs
··· 1 mod errors; 2 mod fzf; 3 mod stack; 4 mod util; 5 mod workspace; 6 - use clap_complete::{generate, Shell}; 7 use errors::Result; 8 - use std::io; 9 use std::path::PathBuf; 10 use std::process::exit; 11 use std::{env::current_dir, io::Read}; 12 - use workspace::{Id, TaskIdentifier, Workspace}; 13 14 //use smol; 15 //use iocraft::prelude::*; 16 - use clap::{value_parser, Args, CommandFactory, Parser, Subcommand}; 17 use edit::edit as open_editor; 18 19 fn default_dir() -> PathBuf { 20 current_dir().unwrap() 21 } 22 23 #[derive(Parser)] 24 // TODO: add long_about 25 #[command(version, about)] 26 struct Cli { 27 #[arg(short = 'C', env = "TSK_ROOT", value_name = "DIR")] 28 dir: Option<PathBuf>, 29 // TODO: other global options ··· 52 #[command(flatten)] 53 title: Title, 54 }, 55 /// Print the task stack. This will include just TSK-IDs and the title. 56 List { 57 /// Whether to list all tasks in the task stack. If specified, -c / count is ignored. ··· 81 Find { 82 #[command(flatten)] 83 args: FindArgs, 84 - /// Whether to print the full TSK-ID (instead of just an integer) 85 - #[arg(short = 'F', default_value_t = true)] 86 - full_id: bool, 87 }, 88 89 /// Drops the task on the top of the stack and archives it. 90 - Drop, 91 92 /// Moves the 3rd item on the stack to the front of the stack, shifting everything else down by 93 /// one. If there are less than 3 tasks on the stack, has no effect. ··· 96 /// task up. 97 Tor, 98 99 - /// Reprioritizes an arbitrary task to the top of the stack. 100 - Reprioritize { 101 /// The [TSK-]ID to prioritize. If it exists, it is moved to the top of the stack. 102 #[command(flatten)] 103 task_id: TaskId, 104 }, ··· 124 id: Option<u32>, 125 126 /// The ID of the task to select with the 'tsk-' prefix. 127 - #[arg(short = 'T', value_name = "TSK-ID", value_parser = value_parser!(String))] 128 tsk_id: Option<Id>, 129 130 /// Selects a task relative to the top of the stack. ··· 141 #[derive(Args)] 142 #[group(required = false, multiple = true)] 143 struct Find { 144 #[arg(short = 'f', value_name = "FIND", default_value_t = false)] 145 find: bool, 146 #[command(flatten)] ··· 150 #[derive(Args)] 151 #[group(required = false, multiple = false)] 152 struct FindArgs { 153 - /// Include the contents of tasks in the search criteria. 154 #[arg(short = 'b', default_value_t = false)] 155 - search_body: bool, 156 /* TODO: implement this 157 /// Include archived tasks in the search criteria. Combine with `-b` to include archived 158 /// bodies in the search criteria. ··· 165 fn from(value: TaskId) -> Self { 166 if let Some(id) = value.id.map(Id::from).or(value.tsk_id) { 167 TaskIdentifier::Id(id) 168 - } else { 169 - if value.find.find { 170 - TaskIdentifier::Find { 171 - search_body: value.find.args.search_body, 172 - archived: false, 173 - } 174 - } else { 175 - TaskIdentifier::Relative(value.relative_id) 176 } 177 } 178 } 179 } ··· 181 fn main() { 182 let cli = Cli::parse(); 183 let dir = cli.dir.unwrap_or(default_dir()); 184 - let result = match cli.command { 185 Commands::Init => command_init(dir), 186 Commands::Push { edit, body, title } => command_push(dir, edit, body, title), 187 Commands::List { all, count } => command_list(dir, all, count), 188 Commands::Swap => command_swap(dir), 189 Commands::Edit { task_id } => command_edit(dir, task_id), 190 Commands::Completion { shell } => command_completion(shell), 191 - Commands::Drop => command_drop(dir), 192 - Commands::Find { args, full_id } => command_find(dir, full_id, args), 193 Commands::Rot => Workspace::from_path(dir).unwrap().rot(), 194 Commands::Tor => Workspace::from_path(dir).unwrap().tor(), 195 - Commands::Reprioritize { task_id } => command_reprioritize(dir, task_id), 196 }; 197 match result { 198 Ok(_) => exit(0), 199 Err(e) => { ··· 203 } 204 } 205 206 fn command_init(dir: PathBuf) -> Result<()> { 207 Workspace::init(dir) 208 } 209 210 - fn command_push(dir: PathBuf, edit: bool, body: Option<String>, title: Title) -> Result<()> { 211 - let workspace = Workspace::from_path(dir)?; 212 let mut title = if let Some(title) = title.title { 213 title 214 } else if let Some(title) = title.title_simple { 215 - let joined = title.join(" "); 216 - joined 217 } else { 218 "".to_string() 219 }; 220 - let mut body = body.unwrap_or_default(); 221 if body == "-" { 222 // add newline so you can type directly in the shell 223 - eprintln!(""); 224 body.clear(); 225 std::io::stdin().read_to_string(&mut body)?; 226 } ··· 231 body = content.1.to_string(); 232 } 233 } 234 let task = workspace.new_task(title, body)?; 235 workspace.push_task(task) 236 } 237 238 fn command_list(dir: PathBuf, all: bool, count: usize) -> Result<()> { 239 let workspace = Workspace::from_path(dir)?; 240 - let stack = if all { 241 - workspace.read_stack()? 242 - } else { 243 - workspace.read_stack()? 244 - }; 245 if stack.empty() { 246 println!("*No tasks*"); 247 exit(0); 248 - } else { 249 - if !all { 250 - for stack_item in stack.into_iter().take(count) { 251 - println!("{stack_item}"); 252 - } 253 } else { 254 - for stack_item in stack.into_iter() { 255 - println!("{stack_item}"); 256 - } 257 } 258 } 259 Ok(()) ··· 269 let workspace = Workspace::from_path(dir)?; 270 let id: TaskIdentifier = id.into(); 271 let mut task = workspace.task(id)?; 272 let new_content = open_editor(format!("{}\n\n{}", task.title.trim(), task.body.trim()))?; 273 if let Some((title, body)) = new_content.split_once("\n") { 274 - task.title = title.to_string(); 275 task.body = body.to_string(); 276 task.save()?; 277 } 278 Ok(()) ··· 283 Ok(()) 284 } 285 286 - fn command_drop(dir: PathBuf) -> Result<()> { 287 - if let Some(id) = Workspace::from_path(dir)?.drop()? { 288 eprint!("Dropped "); 289 println!("{id}"); 290 } else { ··· 294 Ok(()) 295 } 296 297 - fn command_find(dir: PathBuf, full_id: bool, find_args: FindArgs) -> Result<()> { 298 - let id = Workspace::from_path(dir)?.search(None, find_args.search_body, false)?; 299 if let Some(id) = id { 300 - if full_id { 301 - println!("{id}"); 302 - } else { 303 // print as integer 304 println!("{}", id.0); 305 } 306 } else { 307 eprintln!("No task selected."); ··· 310 Ok(()) 311 } 312 313 - fn command_reprioritize(dir: PathBuf, task_id: TaskId) -> Result<()> { 314 - Workspace::from_path(dir)?.reprioritize(task_id.into()) 315 }
··· 1 + mod attrs; 2 mod errors; 3 mod fzf; 4 mod stack; 5 + mod task; 6 mod util; 7 mod workspace; 8 + use clap_complete::{Shell, generate}; 9 use errors::Result; 10 + use std::io::{self, Write}; 11 use std::path::PathBuf; 12 use std::process::exit; 13 + use std::str::FromStr as _; 14 use std::{env::current_dir, io::Read}; 15 + use task::ParsedLink; 16 + use workspace::{Id, Task, TaskIdentifier, Workspace}; 17 18 //use smol; 19 //use iocraft::prelude::*; 20 + use clap::{Args, CommandFactory, Parser, Subcommand}; 21 use edit::edit as open_editor; 22 23 fn default_dir() -> PathBuf { 24 current_dir().unwrap() 25 } 26 27 + fn parse_id(s: &str) -> std::result::Result<Id, &'static str> { 28 + Id::from_str(s).map_err(|_| "Unable to parse tsk- ID") 29 + } 30 + 31 #[derive(Parser)] 32 // TODO: add long_about 33 #[command(version, about)] 34 struct Cli { 35 + /// Override the tsk root directory. 36 #[arg(short = 'C', env = "TSK_ROOT", value_name = "DIR")] 37 dir: Option<PathBuf>, 38 // TODO: other global options ··· 61 #[command(flatten)] 62 title: Title, 63 }, 64 + /// Creates a new task just like `push`, but instead of putting it at the top of the stack, it 65 + /// puts it at the bottom 66 + Append { 67 + /// Whether to open $EDITOR to edit the content of the task. The first line if the 68 + /// resulting file will be the task's title. The body follows the title after two newlines, 69 + /// similr to the format of a commit message. 70 + #[arg(short = 'e', default_value_t = false)] 71 + edit: bool, 72 + 73 + /// The body of the task. It may be specified as either a string using quotes or the 74 + /// special character '-' to read from stdin. 75 + #[arg(short = 'b')] 76 + body: Option<String>, 77 + 78 + /// The title of the task as a raw string. It mus be proceeded by two dashes (--). 79 + #[command(flatten)] 80 + title: Title, 81 + }, 82 /// Print the task stack. This will include just TSK-IDs and the title. 83 List { 84 /// Whether to list all tasks in the task stack. If specified, -c / count is ignored. ··· 108 Find { 109 #[command(flatten)] 110 args: FindArgs, 111 + /// Whether to print the a shortened tsk ID (just the integer portion). Defaults to *false* 112 + #[arg(short = 'f', default_value_t = false)] 113 + short_id: bool, 114 + }, 115 + 116 + /// Prints the contents of a task, parsing the body as rich text and formatting it using ANSI 117 + /// escape sequences. 118 + Show { 119 + /// Shows raw file attributes for the file 120 + #[arg(short = 'x', default_value_t = false)] 121 + show_attrs: bool, 122 + 123 + #[arg(short = 'R', default_value_t = false)] 124 + raw: bool, 125 + /// The [TSK-]ID of the task to display 126 + #[command(flatten)] 127 + task_id: TaskId, 128 + }, 129 + 130 + /// Follow a link that is parsed from a task body. It may be an internal or external link (ie. 131 + /// a url or a wiki-style link using double square brackets). When using the `tsk show` 132 + /// command, links that are successfully parsed get a numeric superscript that may be used to 133 + /// address the link. That number should be supplied to the -l/link_index where it will be 134 + /// subsequently followed opened or shown. 135 + Follow { 136 + /// The task whose body will be searched for links. 137 + #[command(flatten)] 138 + task_id: TaskId, 139 + /// The index of the link to open. Must be supplied. 140 + #[arg(short = 'l', default_value_t = 1)] 141 + link_index: usize, 142 + /// When opening an internal link, whether to show or edit the addressed task. 143 + #[arg(short = 'e', default_value_t = false)] 144 + edit: bool, 145 }, 146 147 /// Drops the task on the top of the stack and archives it. 148 + Drop { 149 + /// The [TSK-]ID of the task to drop. 150 + #[command(flatten)] 151 + task_id: TaskId, 152 + }, 153 154 /// Moves the 3rd item on the stack to the front of the stack, shifting everything else down by 155 /// one. If there are less than 3 tasks on the stack, has no effect. ··· 158 /// task up. 159 Tor, 160 161 + /// Prioritizes an arbitrary task to the top of the stack. 162 + Prioritize { 163 /// The [TSK-]ID to prioritize. If it exists, it is moved to the top of the stack. 164 + #[command(flatten)] 165 + task_id: TaskId, 166 + }, 167 + 168 + /// Deprioritizes a task to the bottom of the stack. 169 + Deprioritize { 170 + /// The [TSK-]ID to deprioritize. If it exists, it is moved to the bottom of the stack. 171 #[command(flatten)] 172 task_id: TaskId, 173 }, ··· 193 id: Option<u32>, 194 195 /// The ID of the task to select with the 'tsk-' prefix. 196 + #[arg(short = 'T', value_name = "TSK-ID", value_parser = parse_id)] 197 tsk_id: Option<Id>, 198 199 /// Selects a task relative to the top of the stack. ··· 210 #[derive(Args)] 211 #[group(required = false, multiple = true)] 212 struct Find { 213 + /// Use fuzzy finding to select a task. 214 #[arg(short = 'f', value_name = "FIND", default_value_t = false)] 215 find: bool, 216 #[command(flatten)] ··· 220 #[derive(Args)] 221 #[group(required = false, multiple = false)] 222 struct FindArgs { 223 + /// Exclude the contents of tasks in the search criteria. 224 #[arg(short = 'b', default_value_t = false)] 225 + exclude_body: bool, 226 /* TODO: implement this 227 /// Include archived tasks in the search criteria. Combine with `-b` to include archived 228 /// bodies in the search criteria. ··· 235 fn from(value: TaskId) -> Self { 236 if let Some(id) = value.id.map(Id::from).or(value.tsk_id) { 237 TaskIdentifier::Id(id) 238 + } else if value.find.find { 239 + TaskIdentifier::Find { 240 + exclude_body: value.find.args.exclude_body, 241 + archived: false, 242 } 243 + } else { 244 + TaskIdentifier::Relative(value.relative_id) 245 } 246 } 247 } ··· 249 fn main() { 250 let cli = Cli::parse(); 251 let dir = cli.dir.unwrap_or(default_dir()); 252 + let var_name = match cli.command { 253 Commands::Init => command_init(dir), 254 Commands::Push { edit, body, title } => command_push(dir, edit, body, title), 255 + Commands::Append { edit, body, title } => command_append(dir, edit, body, title), 256 Commands::List { all, count } => command_list(dir, all, count), 257 Commands::Swap => command_swap(dir), 258 + Commands::Show { 259 + task_id, 260 + raw, 261 + show_attrs, 262 + } => command_show(dir, task_id, show_attrs, raw), 263 + Commands::Follow { 264 + task_id, 265 + link_index, 266 + edit, 267 + } => command_follow(dir, task_id, link_index, edit), 268 Commands::Edit { task_id } => command_edit(dir, task_id), 269 Commands::Completion { shell } => command_completion(shell), 270 + Commands::Drop { task_id } => command_drop(dir, task_id), 271 + Commands::Find { args, short_id } => command_find(dir, short_id, args), 272 Commands::Rot => Workspace::from_path(dir).unwrap().rot(), 273 Commands::Tor => Workspace::from_path(dir).unwrap().tor(), 274 + Commands::Prioritize { task_id } => command_prioritize(dir, task_id), 275 + Commands::Deprioritize { task_id } => command_deprioritize(dir, task_id), 276 }; 277 + let result = var_name; 278 match result { 279 Ok(_) => exit(0), 280 Err(e) => { ··· 284 } 285 } 286 287 + fn taskid_from_tsk_id(tsk_id: Id) -> TaskId { 288 + TaskId { 289 + tsk_id: Some(tsk_id), 290 + id: None, 291 + relative_id: 0, 292 + find: Find { 293 + find: false, 294 + args: FindArgs { exclude_body: true }, 295 + }, 296 + } 297 + } 298 + 299 fn command_init(dir: PathBuf) -> Result<()> { 300 Workspace::init(dir) 301 } 302 303 + fn create_task( 304 + workspace: &mut Workspace, 305 + edit: bool, 306 + body: Option<String>, 307 + title: Title, 308 + ) -> Result<Task> { 309 let mut title = if let Some(title) = title.title { 310 title 311 } else if let Some(title) = title.title_simple { 312 + title.join(" ") 313 } else { 314 "".to_string() 315 }; 316 + // If no body was explicitly provided and the title contains newlines, 317 + // treat the first line as the title and the rest as the body (like git commit -m) 318 + let mut body = if body.is_none() { 319 + if let Some((first_line, rest)) = title.split_once('\n') { 320 + let extracted_body = rest.to_string(); 321 + title = first_line.to_string(); 322 + extracted_body 323 + } else { 324 + String::new() 325 + } 326 + } else { 327 + // Body was explicitly provided, so strip any newlines from title 328 + title = title.replace(['\n', '\r'], " "); 329 + body.unwrap_or_default() 330 + }; 331 if body == "-" { 332 // add newline so you can type directly in the shell 333 + //eprintln!(""); 334 body.clear(); 335 std::io::stdin().read_to_string(&mut body)?; 336 } ··· 341 body = content.1.to_string(); 342 } 343 } 344 + // Ensure title never contains newlines (invariant for index file format) 345 + title = title.replace(['\n', '\r'], " "); 346 let task = workspace.new_task(title, body)?; 347 + workspace.handle_metadata(&task, None)?; 348 + Ok(task) 349 + } 350 + 351 + fn command_push(dir: PathBuf, edit: bool, body: Option<String>, title: Title) -> Result<()> { 352 + let mut workspace = Workspace::from_path(dir)?; 353 + let task = create_task(&mut workspace, edit, body, title)?; 354 workspace.push_task(task) 355 } 356 357 + fn command_append(dir: PathBuf, edit: bool, body: Option<String>, title: Title) -> Result<()> { 358 + let mut workspace = Workspace::from_path(dir)?; 359 + let task = create_task(&mut workspace, edit, body, title)?; 360 + workspace.append_task(task) 361 + } 362 + 363 fn command_list(dir: PathBuf, all: bool, count: usize) -> Result<()> { 364 let workspace = Workspace::from_path(dir)?; 365 + let stack = workspace.read_stack()?; 366 + 367 if stack.empty() { 368 println!("*No tasks*"); 369 exit(0); 370 + } 371 + 372 + for (_, stack_item) in stack 373 + .into_iter() 374 + .enumerate() 375 + .take_while(|(idx, _)| all || idx < &count) 376 + { 377 + if let Some(parsed) = task::parse(&stack_item.title) { 378 + println!("{}\t{}", stack_item.id, parsed.content.trim()); 379 } else { 380 + println!("{stack_item}"); 381 } 382 } 383 Ok(()) ··· 393 let workspace = Workspace::from_path(dir)?; 394 let id: TaskIdentifier = id.into(); 395 let mut task = workspace.task(id)?; 396 + let pre_links = task::parse(&task.to_string()).map(|pt| pt.intenal_links()); 397 let new_content = open_editor(format!("{}\n\n{}", task.title.trim(), task.body.trim()))?; 398 if let Some((title, body)) = new_content.split_once("\n") { 399 + // Ensure title never contains newlines (invariant for index file format) 400 + task.title = title.replace(['\n', '\r'], " "); 401 task.body = body.to_string(); 402 + workspace.handle_metadata(&task, pre_links)?; 403 task.save()?; 404 } 405 Ok(()) ··· 410 Ok(()) 411 } 412 413 + fn command_drop(dir: PathBuf, task_id: TaskId) -> Result<()> { 414 + if let Some(id) = Workspace::from_path(dir)?.drop(task_id.into())? { 415 eprint!("Dropped "); 416 println!("{id}"); 417 } else { ··· 421 Ok(()) 422 } 423 424 + fn command_find(dir: PathBuf, short_id: bool, find_args: FindArgs) -> Result<()> { 425 + let id = Workspace::from_path(dir)?.search(None, !find_args.exclude_body, false)?; 426 if let Some(id) = id { 427 + if short_id { 428 // print as integer 429 println!("{}", id.0); 430 + } else { 431 + println!("{id}"); 432 } 433 } else { 434 eprintln!("No task selected."); ··· 437 Ok(()) 438 } 439 440 + fn command_prioritize(dir: PathBuf, task_id: TaskId) -> Result<()> { 441 + Workspace::from_path(dir)?.prioritize(task_id.into()) 442 + } 443 + 444 + fn command_deprioritize(dir: PathBuf, task_id: TaskId) -> Result<()> { 445 + Workspace::from_path(dir)?.deprioritize(task_id.into()) 446 + } 447 + 448 + fn command_show(dir: PathBuf, task_id: TaskId, show_attrs: bool, raw: bool) -> Result<()> { 449 + let task = Workspace::from_path(dir)?.task(task_id.into())?; 450 + // YAML front-matter style. YAML is gross, but it's what everyone uses! 451 + if show_attrs && !task.attributes.is_empty() { 452 + println!("---"); 453 + for (attr, value) in task.attributes.iter() { 454 + println!("{attr}: \"{value}\""); 455 + } 456 + println!("---"); 457 + } 458 + match task::parse(&task.to_string()) { 459 + Some(styled_task) if !raw => { 460 + writeln!(io::stdout(), "{}", styled_task.content)?; 461 + } 462 + _ => { 463 + println!("{task}"); 464 + } 465 + } 466 + Ok(()) 467 + } 468 + 469 + fn command_follow(dir: PathBuf, task_id: TaskId, link_index: usize, edit: bool) -> Result<()> { 470 + let task = Workspace::from_path(dir.clone())?.task(task_id.into())?; 471 + if let Some(parsed_task) = task::parse(&task.to_string()) { 472 + if link_index == 0 || link_index > parsed_task.links.len() { 473 + eprintln!("Link index out of bounds."); 474 + exit(1); 475 + } 476 + let link = &parsed_task.links[link_index - 1]; 477 + match link { 478 + ParsedLink::External(url) => { 479 + open::that_detached(url.as_str())?; 480 + Ok(()) 481 + } 482 + ParsedLink::Internal(id) => { 483 + let taskid = taskid_from_tsk_id(*id); 484 + if edit { 485 + command_edit(dir, taskid) 486 + } else { 487 + command_show(dir, taskid, false, false) 488 + } 489 + } 490 + } 491 + } else { 492 + eprintln!("Unable to parse any links from body."); 493 + exit(1); 494 + } 495 }
+24 -17
src/stack.rs
··· 4 5 use crate::errors::{Error, Result}; 6 use crate::util; 7 - use std::collections::vec_deque::Iter; 8 use std::collections::VecDeque; 9 use std::fmt::Display; 10 use std::io::{self, BufRead, BufReader, Seek, Write}; 11 use std::str::FromStr; 12 use std::time::{Duration, SystemTime, UNIX_EPOCH}; 13 - use std::{fs::File, path::PathBuf}; 14 15 use nix::fcntl::{Flock, FlockArg}; 16 ··· 67 let mut parts = s.trim().split("\t"); 68 let id: Id = parts 69 .next() 70 - .ok_or(Error::Parse(format!( 71 - "Incomplete index line. Missing tsk ID" 72 - )))? 73 .parse()?; 74 let title: String = parts 75 .next() 76 - .ok_or(Error::Parse(format!( 77 - "Incomplete index line. Missing title." 78 - )))? 79 .trim() 80 .to_string(); 81 // parse the timestamp as an integer ··· 96 97 impl StackItem { 98 /// Parses a [`StackItem`] from a string. The expected format is a tab-delimited line with the 99 - /// files: task id title 100 - fn from_line(workspace_path: &PathBuf, line: String) -> Result<Self> { 101 let mut stack_item: StackItem = line.parse()?; 102 103 let task = util::flopen( 104 workspace_path 105 .join(TASKSFOLDER) 106 - .join(stack_item.id.to_filename()), 107 FlockArg::LockExclusive, 108 )?; 109 let task_modify_time = task.metadata()?.modified()?; ··· 125 } 126 127 impl TaskStack { 128 - pub fn from_tskdir(workspace_path: &PathBuf) -> Result<Self> { 129 let file = util::flopen(workspace_path.join(INDEXFILE), FlockArg::LockExclusive)?; 130 let index = BufReader::new(&*file).lines(); 131 let mut all = VecDeque::new(); ··· 142 self.file.seek(std::io::SeekFrom::Start(0))?; 143 self.file.set_len(0)?; 144 for item in self.all.iter() { 145 - self.file.write_all(format!("{item}\n").as_bytes())?; 146 } 147 Ok(()) 148 } ··· 151 self.all.push_front(item); 152 } 153 154 pub fn pop(&mut self) -> Option<StackItem> { 155 self.all.pop_front() 156 } ··· 158 pub fn swap(&mut self) { 159 let tip = self.all.pop_front(); 160 let second = self.all.pop_front(); 161 - if tip.is_some() && second.is_some() { 162 - self.all.push_front(tip.unwrap()); 163 - self.all.push_front(second.unwrap()); 164 } 165 } 166 ··· 172 self.all.remove(index) 173 } 174 175 - pub fn iter(&self) -> Iter<StackItem> { 176 self.all.iter() 177 } 178
··· 4 5 use crate::errors::{Error, Result}; 6 use crate::util; 7 use std::collections::VecDeque; 8 + use std::collections::vec_deque::Iter; 9 use std::fmt::Display; 10 + use std::fs::File; 11 use std::io::{self, BufRead, BufReader, Seek, Write}; 12 + use std::path::Path; 13 use std::str::FromStr; 14 use std::time::{Duration, SystemTime, UNIX_EPOCH}; 15 16 use nix::fcntl::{Flock, FlockArg}; 17 ··· 68 let mut parts = s.trim().split("\t"); 69 let id: Id = parts 70 .next() 71 + .ok_or(Error::Parse( 72 + "Incomplete index line. Missing tsk ID".to_owned(), 73 + ))? 74 .parse()?; 75 let title: String = parts 76 .next() 77 + .ok_or(Error::Parse( 78 + "Incomplete index line. Missing title.".to_owned(), 79 + ))? 80 .trim() 81 .to_string(); 82 // parse the timestamp as an integer ··· 97 98 impl StackItem { 99 /// Parses a [`StackItem`] from a string. The expected format is a tab-delimited line with the 100 + /// files: task id title 101 + fn from_line(workspace_path: &Path, line: String) -> Result<Self> { 102 let mut stack_item: StackItem = line.parse()?; 103 104 let task = util::flopen( 105 workspace_path 106 .join(TASKSFOLDER) 107 + .join(stack_item.id.filename()), 108 FlockArg::LockExclusive, 109 )?; 110 let task_modify_time = task.metadata()?.modified()?; ··· 126 } 127 128 impl TaskStack { 129 + pub fn from_tskdir(workspace_path: &Path) -> Result<Self> { 130 let file = util::flopen(workspace_path.join(INDEXFILE), FlockArg::LockExclusive)?; 131 let index = BufReader::new(&*file).lines(); 132 let mut all = VecDeque::new(); ··· 143 self.file.seek(std::io::SeekFrom::Start(0))?; 144 self.file.set_len(0)?; 145 for item in self.all.iter() { 146 + let time = item.modify_time.duration_since(UNIX_EPOCH)?.as_secs(); 147 + self.file 148 + .write_all(format!("{item}\t{}\n", time).as_bytes())?; 149 } 150 Ok(()) 151 } ··· 154 self.all.push_front(item); 155 } 156 157 + pub fn push_back(&mut self, item: StackItem) { 158 + self.all.push_back(item); 159 + } 160 + 161 pub fn pop(&mut self) -> Option<StackItem> { 162 self.all.pop_front() 163 } ··· 165 pub fn swap(&mut self) { 166 let tip = self.all.pop_front(); 167 let second = self.all.pop_front(); 168 + if let Some((tip, second)) = tip.zip(second) { 169 + self.all.push_front(tip); 170 + self.all.push_front(second); 171 } 172 } 173 ··· 179 self.all.remove(index) 180 } 181 182 + pub fn iter(&self) -> Iter<'_, StackItem> { 183 self.all.iter() 184 } 185
+413
src/task.rs
···
··· 1 + #![allow(dead_code)] 2 + 3 + use std::{collections::HashSet, str::FromStr}; 4 + use url::Url; 5 + 6 + use crate::workspace::Id; 7 + use colored::Colorize; 8 + 9 + /// Returns true if the character is a word boundary (whitespace or punctuation) 10 + fn is_boundary(c: char) -> bool { 11 + c.is_whitespace() || c.is_ascii_punctuation() 12 + } 13 + 14 + #[derive(Debug, Eq, PartialEq, Clone, Copy)] 15 + enum ParserState { 16 + // Started by ` =`, terminated by `= 17 + Highlight(usize, usize), 18 + // Started by ` [`, terminated by `](` 19 + Linktext(usize, usize), 20 + // Started by `](`, terminated by `) `, must immedately follow a Linktext 21 + Link(usize, usize), 22 + RawLink(usize, usize), 23 + // Started by ` [[`, terminated by `]] ` 24 + InternalLink(usize, usize), 25 + // Started by ` *`, terminated by `* ` 26 + Italics(usize, usize), 27 + // Started by ` !`, termianted by `!` 28 + Bold(usize, usize), 29 + // Started by ` _`, terminated by `_ ` 30 + Underline(usize, usize), 31 + // Started by ` -`, terminated by `- ` 32 + Strikethrough(usize, usize), 33 + 34 + // TODO: implement these. 35 + // Started by `_ `, terminated by `_` 36 + UnorderedList(usize, u8), 37 + // Started by `^\w+1.`, terminated by `\n` 38 + OrderedList(usize, u8), 39 + // Started by `^`````, terminated by a [`ParserOpcode::BlockEnd`] 40 + BlockStart(usize), 41 + // Started by `$````, is terminal itself. It must appear on its own line and be preceeded by a 42 + // `\n` and followed by a `\n` 43 + BlockEnd(usize), 44 + // Started by ` ``, terminated by `` ` or `\n` 45 + InlineBlock(usize, usize), 46 + // Started by `^\w+>`, terminated by `\n` 47 + Blockquote(usize), 48 + } 49 + 50 + #[derive(Debug, Eq, PartialEq, Clone)] 51 + pub(crate) enum ParsedLink { 52 + Internal(Id), 53 + External(Url), 54 + } 55 + 56 + pub(crate) struct ParsedTask { 57 + pub(crate) content: String, 58 + pub(crate) links: Vec<ParsedLink>, 59 + } 60 + 61 + impl ParsedTask { 62 + pub(crate) fn intenal_links(&self) -> HashSet<Id> { 63 + let mut out = HashSet::with_capacity(self.links.len()); 64 + for link in &self.links { 65 + if let ParsedLink::Internal(id) = link { 66 + out.insert(*id); 67 + } 68 + } 69 + out 70 + } 71 + } 72 + 73 + pub(crate) fn parse(s: &str) -> Option<ParsedTask> { 74 + let mut state: Vec<ParserState> = Vec::new(); 75 + let mut out = String::with_capacity(s.len()); 76 + let mut stream = s.char_indices().peekable(); 77 + let mut links = Vec::new(); 78 + let mut last = '\0'; 79 + use ParserState::*; 80 + loop { 81 + let state_last = state.last().cloned(); 82 + match stream.next() { 83 + // there will always be an op code in the stack 84 + Some((char_pos, c)) => { 85 + out.push(c); 86 + let end = out.len() - 1; 87 + match (last, c, state_last) { 88 + ('[', '[', _) => { 89 + state.push(InternalLink(end, char_pos)); 90 + } 91 + (']', ']', Some(InternalLink(il, s_pos))) => { 92 + state.pop(); 93 + let contents = s.get(s_pos + 1..char_pos - 1)?; 94 + if let Ok(id) = Id::from_str(contents) { 95 + let linktext = format!( 96 + "{}{}", 97 + contents.purple(), 98 + super_num(links.len() + 1).purple() 99 + ); 100 + out.replace_range(il - 1..out.len(), &linktext); 101 + links.push(ParsedLink::Internal(id)); 102 + } else { 103 + panic!("Internal link is not a valid id: {contents}"); 104 + } 105 + } 106 + (last, '[', _) if is_boundary(last) => { 107 + state.push(Linktext(end, char_pos)); 108 + } 109 + (']', '(', Some(Linktext(_, _))) => { 110 + state.push(Link(end, char_pos)); 111 + } 112 + (')', c, Some(Link(_, _))) if is_boundary(c) => { 113 + // TODO: this needs to be updated to use `s` instead of `out` for position 114 + // parsing 115 + let linkpos = if let Link(lp, _) = state.pop().unwrap() { 116 + lp 117 + } else { 118 + // remove the linktext state, it is always present. 119 + state.pop(); 120 + continue; 121 + }; 122 + let linktextpos = if let Linktext(lt, _) = state.pop().unwrap() { 123 + lt 124 + } else { 125 + continue; 126 + }; 127 + let linktext = format!( 128 + "{}{}", 129 + out.get(linktextpos + 1..linkpos - 1)?.blue(), 130 + super_num(links.len() + 1).purple() 131 + ); 132 + let link = out.get(linkpos + 1..end - 1)?; 133 + if let Ok(url) = Url::parse(link) { 134 + links.push(ParsedLink::External(url)); 135 + out.replace_range(linktextpos..end, &linktext); 136 + } 137 + } 138 + ('>', c, Some(RawLink(hl, s_pos))) 139 + if is_boundary(c) && s_pos != char_pos - 1 => 140 + { 141 + state.pop(); 142 + let link = s.get(s_pos + 1..char_pos - 1)?; 143 + if let Ok(url) = Url::parse(link) { 144 + let linktext = 145 + format!("{}{}", link.blue(), super_num(links.len() + 1).purple()); 146 + links.push(ParsedLink::External(url)); 147 + out.replace_range(hl..end, &linktext); 148 + } 149 + } 150 + (last, '<', _) if is_boundary(last) => { 151 + state.push(RawLink(end, char_pos)); 152 + } 153 + ('=', c, Some(Highlight(hl, s_pos))) 154 + if is_boundary(c) && s_pos != char_pos - 1 => 155 + { 156 + state.pop(); 157 + out.replace_range( 158 + hl..end, 159 + &s.get(s_pos + 1..char_pos - 1)?.reversed().to_string(), 160 + ); 161 + } 162 + (last, '=', _) if is_boundary(last) => { 163 + state.push(Highlight(end, char_pos)); 164 + } 165 + (last, '*', _) if is_boundary(last) => { 166 + state.push(Italics(end, char_pos)); 167 + } 168 + ('*', c, Some(Italics(il, s_pos))) 169 + if is_boundary(c) && s_pos != char_pos - 1 => 170 + { 171 + state.pop(); 172 + out.replace_range( 173 + il..end, 174 + &s.get(s_pos + 1..char_pos - 1)?.italic().to_string(), 175 + ); 176 + } 177 + (last, '!', _) if is_boundary(last) => { 178 + state.push(Bold(end, char_pos)); 179 + } 180 + ('!', c, Some(Bold(il, s_pos))) if is_boundary(c) && s_pos != char_pos - 1 => { 181 + state.pop(); 182 + out.replace_range( 183 + il..end, 184 + &s.get(s_pos + 1..char_pos - 1)?.bold().to_string(), 185 + ); 186 + } 187 + (last, '_', _) if is_boundary(last) => { 188 + state.push(Underline(end, char_pos)); 189 + } 190 + ('_', c, Some(Underline(il, s_pos))) 191 + if is_boundary(c) && s_pos != char_pos - 1 => 192 + { 193 + state.pop(); 194 + out.replace_range( 195 + il..end, 196 + &s.get(s_pos + 1..char_pos - 1)?.underline().to_string(), 197 + ); 198 + } 199 + (last, '~', _) if is_boundary(last) => { 200 + state.push(Strikethrough(end, char_pos)); 201 + } 202 + ('~', c, Some(Strikethrough(il, s_pos))) 203 + if is_boundary(c) && s_pos != char_pos - 1 => 204 + { 205 + state.pop(); 206 + out.replace_range( 207 + il..end, 208 + &s.get(s_pos + 1..char_pos - 1)?.strikethrough().to_string(), 209 + ); 210 + } 211 + ('`', c, Some(InlineBlock(hl, s_pos))) 212 + if is_boundary(c) && s_pos != char_pos - 1 => 213 + { 214 + out.replace_range( 215 + hl..end, 216 + &s.get(s_pos + 1..char_pos - 1)?.green().to_string(), 217 + ); 218 + } 219 + (last, '`', _) if is_boundary(last) => { 220 + state.push(InlineBlock(end, char_pos)); 221 + } 222 + _ => (), 223 + } 224 + if c == '\n' || c == '\r' { 225 + state.clear(); 226 + } 227 + last = c; 228 + } 229 + None => break, 230 + } 231 + } 232 + Some(ParsedTask { 233 + content: out, 234 + links, 235 + }) 236 + } 237 + 238 + /// Converts a unsigned integer into a superscripted string 239 + fn super_num(num: usize) -> String { 240 + let num_str = num.to_string(); 241 + let mut out = String::with_capacity(num_str.len()); 242 + for char in num_str.chars() { 243 + out.push(match char { 244 + '0' => 'โฐ', 245 + '1' => 'ยน', 246 + '2' => 'ยฒ', 247 + '3' => 'ยณ', 248 + '4' => 'โด', 249 + '5' => 'โต', 250 + '6' => 'โถ', 251 + '7' => 'โท', 252 + '8' => 'โธ', 253 + '9' => 'โน', 254 + _ => unreachable!(), 255 + }); 256 + } 257 + out 258 + } 259 + 260 + #[cfg(test)] 261 + mod test { 262 + use super::*; 263 + #[test] 264 + fn test_highlight() { 265 + let input = "hello =world=\n"; 266 + let output = parse(input).expect("parse to work"); 267 + assert_eq!("hello \u{1b}[7mworld\u{1b}[0m\n", output.content); 268 + } 269 + 270 + #[test] 271 + fn test_highlight_bad() { 272 + let input = "hello =world\n"; 273 + let output = parse(input).expect("parse to work"); 274 + assert_eq!(input, output.content); 275 + } 276 + 277 + #[test] 278 + fn test_link() { 279 + let input = "hello [world](https://ngp.computer)\n"; 280 + let output = parse(input).expect("parse to work"); 281 + assert_eq!( 282 + &[ParsedLink::External( 283 + Url::parse("https://ngp.computer").unwrap() 284 + )], 285 + output.links.as_slice() 286 + ); 287 + assert_eq!( 288 + "hello \u{1b}[34mworld\u{1b}[0m\u{1b}[35mยน\u{1b}[0m\n", 289 + output.content 290 + ); 291 + } 292 + 293 + #[test] 294 + fn test_link_no_terminal_link() { 295 + let input = "hello [world](https://ngp.computer\n"; 296 + let output = parse(input).expect("parse to work"); 297 + assert!(output.links.is_empty()); 298 + assert_eq!(input, output.content); 299 + } 300 + #[test] 301 + fn test_link_bad_no_start_link() { 302 + let input = "hello [world]https://ngp.computer)\n"; 303 + let output = parse(input).expect("parse to work"); 304 + assert!(output.links.is_empty()); 305 + assert_eq!(input, output.content); 306 + } 307 + #[test] 308 + fn test_link_bad_no_link() { 309 + let input = "hello [world]\n"; 310 + let output = parse(input).expect("parse to work"); 311 + assert!(output.links.is_empty()); 312 + assert_eq!(input, output.content); 313 + } 314 + 315 + #[test] 316 + fn test_internal_link_good() { 317 + let input = "hello [[tsk-123]]\n"; 318 + let output = parse(input).expect("parse to work"); 319 + assert_eq!(&[ParsedLink::Internal(Id(123))], output.links.as_slice()); 320 + assert_eq!( 321 + "hello \u{1b}[35mtsk-123\u{1b}[0m\u{1b}[35mยน\u{1b}[0m\n", 322 + output.content 323 + ); 324 + } 325 + 326 + #[test] 327 + fn test_internal_link_bad() { 328 + let input = "hello [[tsk-123"; 329 + let output = parse(input).expect("parse to work"); 330 + assert!(output.links.is_empty()); 331 + assert_eq!(input, output.content); 332 + } 333 + 334 + #[test] 335 + fn test_italics() { 336 + let input = "hello *world*\n"; 337 + let output = parse(input).expect("parse to work"); 338 + assert_eq!("hello \u{1b}[3mworld\u{1b}[0m\n", output.content); 339 + } 340 + 341 + #[test] 342 + fn test_italics_bad() { 343 + let input = "hello *world"; 344 + let output = parse(input).expect("parse to work"); 345 + assert_eq!(input, output.content); 346 + } 347 + 348 + #[test] 349 + fn test_bold() { 350 + let input = "hello !world!\n"; 351 + let output = parse(input).expect("parse to work"); 352 + assert_eq!("hello \u{1b}[1mworld\u{1b}[0m\n", output.content); 353 + } 354 + 355 + #[test] 356 + fn test_bold_bad() { 357 + let input = "hello !world\n"; 358 + let output = parse(input).expect("parse to work"); 359 + assert_eq!(input, output.content); 360 + } 361 + 362 + #[test] 363 + fn test_underline() { 364 + let input = "hello _world_\n"; 365 + let output = parse(input).expect("parse to work"); 366 + assert_eq!("hello \u{1b}[4mworld\u{1b}[0m\n", output.content); 367 + } 368 + 369 + #[test] 370 + fn test_underline_bad() { 371 + let input = "hello _world\n"; 372 + let output = parse(input).expect("parse to work"); 373 + assert_eq!(input, output.content); 374 + } 375 + 376 + #[test] 377 + fn test_strikethrough() { 378 + let input = "hello ~world~\n"; 379 + let output = parse(input).expect("parse to work"); 380 + assert_eq!("hello \u{1b}[9mworld\u{1b}[0m\n", output.content); 381 + } 382 + 383 + #[test] 384 + fn test_strikethrough_bad() { 385 + let input = "hello ~world\n"; 386 + let output = parse(input).expect("parse to work"); 387 + assert_eq!(input, output.content); 388 + } 389 + 390 + #[test] 391 + fn test_inlineblock() { 392 + let input = "hello `world`\n"; 393 + let output = parse(input).expect("parse to work"); 394 + assert_eq!("hello \u{1b}[32mworld\u{1b}[0m\n", output.content); 395 + } 396 + 397 + #[test] 398 + fn test_inlineblock_bad() { 399 + let input = "hello `world\n"; 400 + let output = parse(input).expect("parse to work"); 401 + assert_eq!(input, output.content); 402 + } 403 + 404 + #[test] 405 + fn test_multiple_styles() { 406 + let input = "hello *italic* ~strikethrough~ !bold!\n"; 407 + let output = parse(input).expect("parse to work"); 408 + assert_eq!( 409 + "hello \u{1b}[3mitalic\u{1b}[0m \u{1b}[9mstrikethrough\u{1b}[0m \u{1b}[1mbold\u{1b}[0m\n", 410 + output.content 411 + ); 412 + } 413 + }
+2 -1
src/util.rs
··· 13 .read(true) 14 .write(true) 15 .create(true) 16 .open(path)?; 17 - Ok(Flock::lock(file, mode).map_err(|(_, errno)| Error::Lock(errno))?) 18 } 19 20 /// Recursively searches upwards for a directory
··· 13 .read(true) 14 .write(true) 15 .create(true) 16 + .truncate(false) 17 .open(path)?; 18 + Flock::lock(file, mode).map_err(|(_, errno)| Error::Lock(errno)) 19 } 20 21 /// Recursively searches upwards for a directory
+213 -66
src/workspace.rs
··· 1 #![allow(dead_code)] 2 use nix::fcntl::{Flock, FlockArg}; 3 4 use crate::errors::{Error, Result}; 5 use crate::stack::{StackItem, TaskStack}; 6 use crate::{fzf, util}; 7 - use std::collections::vec_deque; 8 use std::fmt::Display; 9 - use std::fs::{self, File}; 10 use std::io::{BufRead as _, BufReader, Read, Seek, SeekFrom}; 11 use std::path::PathBuf; 12 use std::str::FromStr; 13 use std::{fs::OpenOptions, io::Write}; 14 15 const INDEXFILE: &str = "index"; 16 const TITLECACHEFILE: &str = "cache"; 17 /// A unique identifier for a task. When referenced in text, it is prefixed with `tsk-`. 18 - #[derive(Clone, Copy, Debug, Eq, PartialEq)] 19 pub struct Id(pub u32); 20 21 impl FromStr for Id { 22 type Err = Error; 23 24 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { 25 - let s = s 26 - .strip_prefix("tsk-") 27 .ok_or(Self::Err::Parse(format!("expected tsk- prefix. Got {s}")))?; 28 Ok(Self(s.parse()?)) 29 } ··· 42 } 43 44 impl Id { 45 - pub fn to_filename(&self) -> String { 46 format!("tsk-{}.tsk", self.0) 47 } 48 } ··· 50 pub enum TaskIdentifier { 51 Id(Id), 52 Relative(u32), 53 - Find { search_body: bool, archived: bool }, 54 } 55 56 impl From<Id> for TaskIdentifier { ··· 74 } 75 std::fs::create_dir(&tsk_dir)?; 76 // Create the tasks directory 77 - std::fs::create_dir(&tsk_dir.join("tasks"))?; 78 // Create the archive directory 79 - std::fs::create_dir(&tsk_dir.join("archive"))?; 80 let mut next = OpenOptions::new() 81 .read(true) 82 .write(true) 83 .create(true) 84 .open(tsk_dir.join("next"))?; 85 next.write_all(b"1\n")?; 86 Ok(()) 87 } ··· 100 Ok(stack_item.id) 101 } 102 TaskIdentifier::Find { 103 - search_body, 104 archived, 105 } => self 106 - .search(None, search_body, archived)? 107 .ok_or(Error::NotSelected), 108 } 109 } 110 111 pub fn next_id(&self) -> Result<Id> { 112 let mut file = util::flopen(self.path.join("next"), FlockArg::LockExclusive)?; 113 let mut buf = String::new(); ··· 125 // WARN: we could improperly increment the id if the task is not written to disk/errors. 126 // But who cares 127 let id = self.next_id()?; 128 // the task goes in the archive first 129 - let task_path = self.path.join("archive").join(format!("tsk-{}.tsk", id.0)); 130 let mut file = util::flopen(task_path.clone(), FlockArg::LockExclusive)?; 131 file.write_all(format!("{title}\n\n{body}").as_bytes())?; 132 // create a hardlink to the task dir to mark it as "open" 133 - fs::hard_link( 134 - task_path, 135 - self.path.join("tasks").join(format!("tsk-{}.tsk", id.0)), 136 )?; 137 Ok(Task { 138 id, 139 title, 140 body, 141 file, 142 }) 143 } 144 ··· 155 reader.read_line(&mut title)?; 156 reader.read_to_string(&mut body)?; 157 drop(reader); 158 Ok(Task { 159 id, 160 - title, 161 - body, 162 file, 163 }) 164 } 165 166 pub fn read_stack(&self) -> Result<TaskStack> { 167 TaskStack::from_tskdir(&self.path) 168 } ··· 174 Ok(()) 175 } 176 177 pub fn swap_top(&self) -> Result<()> { 178 let mut stack = TaskStack::from_tskdir(&self.path)?; 179 stack.swap(); ··· 191 return Ok(()); 192 } 193 194 stack.push(second.unwrap()); 195 stack.push(top.unwrap()); 196 stack.push(third.unwrap()); ··· 217 Ok(()) 218 } 219 220 - pub fn drop(&self) -> Result<Option<Id>> { 221 let mut stack = self.read_stack()?; 222 - if let Some(stack_item) = stack.pop() { 223 - let task_path = self 224 - .path 225 - .join("tasks") 226 - .join(format!("{}.tsk", stack_item.id)); 227 - fs::remove_file(task_path)?; 228 stack.save()?; 229 - Ok(Some(stack_item.id)) 230 } else { 231 - Ok(None) 232 - } 233 } 234 235 pub fn search( ··· 249 workspace: self, 250 }; 251 // search the entirety of a task 252 - Ok(fzf::select(loader)?.map(|bt| bt.id)) 253 } else { 254 // just search the stack 255 - Ok(fzf::select(stack)?.map(|si| si.id)) 256 } 257 } 258 259 - pub fn reprioritize(&self, identifier: TaskIdentifier) -> Result<()> { 260 let id = self.resolve(identifier)?; 261 let mut stack = self.read_stack()?; 262 let index = &stack.iter().map(|i| i.id).position(|i| i == id); ··· 268 } 269 Ok(()) 270 } 271 } 272 273 pub struct Task { ··· 275 pub title: String, 276 pub body: String, 277 pub file: Flock<File>, 278 } 279 280 - /// A task container without a file handle 281 - pub struct BareTask { 282 - pub id: Id, 283 - pub title: String, 284 - pub body: String, 285 } 286 287 impl Task { ··· 295 Ok(()) 296 } 297 298 - fn bare(self) -> BareTask { 299 - BareTask { 300 id: self.id, 301 title: self.title, 302 body: self.body, ··· 304 } 305 } 306 307 - impl FromStr for BareTask { 308 - type Err = Error; 309 - 310 - fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { 311 - let (tsk_id, task_content) = s.split_once('\t').ok_or(Error::Parse(format!( 312 - "Missing TSK-ID or content or task parse." 313 - )))?; 314 - let (title, body) = task_content 315 - .split_once('\t') 316 - .ok_or(Error::Parse(format!("Missing body for task parse.")))?; 317 - Ok(Self { 318 - id: tsk_id.parse()?, 319 - title: title.to_string(), 320 - body: body.to_string(), 321 - }) 322 - } 323 } 324 325 - impl Display for BareTask { 326 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 327 - write!( 328 - f, 329 - "{}\t{}\t{}", 330 - self.id, 331 - self.title.trim(), 332 - self.body.replace('\n', " ").replace('\r', "") 333 - ) 334 } 335 } 336 ··· 339 workspace: &'a Workspace, 340 } 341 342 - impl<'a> Iterator for LazyTaskLoader<'a> { 343 - type Item = BareTask; 344 345 fn next(&mut self) -> Option<Self::Item> { 346 let stack_item = self.files.next()?; ··· 352 } 353 } 354 355 #[cfg(test)] 356 mod test { 357 use super::*; 358 359 #[test] 360 fn test_bare_task_display() { 361 - let task = BareTask { 362 id: Id(123), 363 title: "Hello, world".to_string(), 364 - body: "The body of the task.\nAnother line\r\nis here.".to_string(), 365 }; 366 assert_eq!( 367 - "tsk-123\tHello, world\tThe body of the task. Another line is here.", 368 task.to_string() 369 ); 370 } 371 }
··· 1 #![allow(dead_code)] 2 use nix::fcntl::{Flock, FlockArg}; 3 + use xattr::FileExt; 4 5 + use crate::attrs::Attrs; 6 use crate::errors::{Error, Result}; 7 use crate::stack::{StackItem, TaskStack}; 8 + use crate::task::parse as parse_task; 9 use crate::{fzf, util}; 10 + use std::collections::{BTreeMap, HashSet, vec_deque}; 11 + use std::ffi::OsString; 12 use std::fmt::Display; 13 + use std::fs::{File, remove_file}; 14 use std::io::{BufRead as _, BufReader, Read, Seek, SeekFrom}; 15 + use std::ops::Deref; 16 + use std::os::unix::fs::symlink; 17 use std::path::PathBuf; 18 + use std::process::{Command, Stdio}; 19 use std::str::FromStr; 20 use std::{fs::OpenOptions, io::Write}; 21 22 const INDEXFILE: &str = "index"; 23 const TITLECACHEFILE: &str = "cache"; 24 + const XATTRPREFIX: &str = "user.tsk."; 25 + const BACKREFXATTR: &str = "user.tsk.references"; 26 /// A unique identifier for a task. When referenced in text, it is prefixed with `tsk-`. 27 + #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] 28 pub struct Id(pub u32); 29 30 impl FromStr for Id { 31 type Err = Error; 32 33 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { 34 + let upper = s.to_uppercase(); 35 + let s = upper 36 + .trim() 37 + .strip_prefix("TSK-") 38 .ok_or(Self::Err::Parse(format!("expected tsk- prefix. Got {s}")))?; 39 Ok(Self(s.parse()?)) 40 } ··· 53 } 54 55 impl Id { 56 + /// Returns the filename for a task with this id. 57 + pub fn filename(&self) -> String { 58 format!("tsk-{}.tsk", self.0) 59 } 60 } ··· 62 pub enum TaskIdentifier { 63 Id(Id), 64 Relative(u32), 65 + Find { exclude_body: bool, archived: bool }, 66 } 67 68 impl From<Id> for TaskIdentifier { ··· 86 } 87 std::fs::create_dir(&tsk_dir)?; 88 // Create the tasks directory 89 + std::fs::create_dir(tsk_dir.join("tasks"))?; 90 // Create the archive directory 91 + std::fs::create_dir(tsk_dir.join("archive"))?; 92 let mut next = OpenOptions::new() 93 .read(true) 94 .write(true) 95 .create(true) 96 + .truncate(true) 97 .open(tsk_dir.join("next"))?; 98 + // initialize the next file with ID 1 99 next.write_all(b"1\n")?; 100 Ok(()) 101 } ··· 114 Ok(stack_item.id) 115 } 116 TaskIdentifier::Find { 117 + exclude_body, 118 archived, 119 } => self 120 + .search(None, !exclude_body, archived)? 121 .ok_or(Error::NotSelected), 122 } 123 } 124 125 + /// Increments the `next` counter and returns the previous value. 126 pub fn next_id(&self) -> Result<Id> { 127 let mut file = util::flopen(self.path.join("next"), FlockArg::LockExclusive)?; 128 let mut buf = String::new(); ··· 140 // WARN: we could improperly increment the id if the task is not written to disk/errors. 141 // But who cares 142 let id = self.next_id()?; 143 + let task_name = format!("tsk-{}.tsk", id.0); 144 // the task goes in the archive first 145 + let task_path = self.path.join("archive").join(&task_name); 146 let mut file = util::flopen(task_path.clone(), FlockArg::LockExclusive)?; 147 file.write_all(format!("{title}\n\n{body}").as_bytes())?; 148 // create a hardlink to the task dir to mark it as "open" 149 + symlink( 150 + PathBuf::from("../archive").join(&task_name), 151 + self.path.join("tasks").join(task_name), 152 )?; 153 Ok(Task { 154 id, 155 title, 156 body, 157 file, 158 + attributes: Default::default(), 159 }) 160 } 161 ··· 172 reader.read_line(&mut title)?; 173 reader.read_to_string(&mut body)?; 174 drop(reader); 175 + let mut read_attributes = BTreeMap::new(); 176 + if let Ok(attrs) = file.list_xattr() { 177 + for attr in attrs { 178 + if let Some((key, value)) = Self::read_xattr(&file, attr) { 179 + read_attributes.insert(key, value); 180 + } 181 + } 182 + } 183 Ok(Task { 184 id, 185 file, 186 + title: title.trim().to_string(), 187 + body: body.trim().to_string(), 188 + attributes: Attrs::from_written(read_attributes), 189 }) 190 } 191 192 + pub fn handle_metadata(&self, tsk: &Task, pre_links: Option<HashSet<Id>>) -> Result<()> { 193 + // Parse the task and update any backlinks 194 + if let Some(parsed_task) = parse_task(&tsk.to_string()) { 195 + let internal_links = parsed_task.intenal_links(); 196 + for link in &internal_links { 197 + self.add_backlink(*link, tsk.id)?; 198 + } 199 + if let Some(pre_links) = pre_links { 200 + let removed_links = pre_links.difference(&internal_links); 201 + for link in removed_links { 202 + self.remove_backlink(*link, tsk.id)?; 203 + } 204 + } 205 + } 206 + Ok(()) 207 + } 208 + 209 + fn add_backlink(&self, to: Id, from: Id) -> Result<()> { 210 + let to_task = self.task(TaskIdentifier::Id(to))?; 211 + let (_, current_backlinks_text) = 212 + Self::read_xattr(&to_task.file, BACKREFXATTR.into()).unwrap_or_default(); 213 + let mut backlinks: HashSet<Id> = current_backlinks_text 214 + .split(',') 215 + .filter_map(|s| Id::from_str(s).ok()) 216 + .collect(); 217 + backlinks.insert(from); 218 + Self::set_xattr( 219 + &to_task.file, 220 + BACKREFXATTR, 221 + &itertools::join(backlinks, ","), 222 + ) 223 + } 224 + 225 + fn remove_backlink(&self, to: Id, from: Id) -> Result<()> { 226 + let to_task = self.task(TaskIdentifier::Id(to))?; 227 + let (_, current_backlinks_text) = 228 + Self::read_xattr(&to_task.file, BACKREFXATTR.into()).unwrap_or_default(); 229 + let mut backlinks: HashSet<Id> = current_backlinks_text 230 + .split(',') 231 + .filter_map(|s| Id::from_str(s).ok()) 232 + .collect(); 233 + backlinks.remove(&from); 234 + Self::set_xattr( 235 + &to_task.file, 236 + BACKREFXATTR, 237 + &itertools::join(backlinks, ","), 238 + ) 239 + } 240 + 241 + /// Reads an xattr from a file, stripping the prefix for 242 + fn read_xattr<D: Deref<Target = File>>(file: &D, key: OsString) -> Option<(String, String)> { 243 + // this *shouldn't* allocate, but it does O(n) scan the str for UTF-8 correctness 244 + let parsedkey = key.as_os_str().to_str()?.strip_prefix(XATTRPREFIX)?; 245 + let valuebytes = file.get_xattr(&key).ok().flatten()?; 246 + Some((parsedkey.to_string(), String::from_utf8(valuebytes).ok()?)) 247 + } 248 + 249 + fn set_xattr<D: Deref<Target = File>>(file: &D, key: &str, value: &str) -> Result<()> { 250 + let key = if !key.starts_with(XATTRPREFIX) { 251 + format!("{XATTRPREFIX}.{key}") 252 + } else { 253 + key.to_string() 254 + }; 255 + Ok(file.set_xattr(key, value.as_bytes())?) 256 + } 257 + 258 pub fn read_stack(&self) -> Result<TaskStack> { 259 TaskStack::from_tskdir(&self.path) 260 } ··· 266 Ok(()) 267 } 268 269 + pub fn append_task(&self, task: Task) -> Result<()> { 270 + let mut stack = TaskStack::from_tskdir(&self.path)?; 271 + stack.push_back(task.try_into()?); 272 + stack.save()?; 273 + Ok(()) 274 + } 275 + 276 pub fn swap_top(&self) -> Result<()> { 277 let mut stack = TaskStack::from_tskdir(&self.path)?; 278 stack.swap(); ··· 290 return Ok(()); 291 } 292 293 + // unwrap is ok here because we checked above 294 stack.push(second.unwrap()); 295 stack.push(top.unwrap()); 296 stack.push(third.unwrap()); ··· 317 Ok(()) 318 } 319 320 + pub fn drop(&self, identifier: TaskIdentifier) -> Result<Option<Id>> { 321 + let id = self.resolve(identifier)?; 322 let mut stack = self.read_stack()?; 323 + let index = &stack.iter().map(|i| i.id).position(|i| i == id); 324 + // TODO: remove the softlink in .tsk/tasks 325 + let task = if let Some(index) = index { 326 + let prioritized_task = stack.remove(*index); 327 stack.save()?; 328 + prioritized_task.map(|t| t.id) 329 } else { 330 + None 331 + }; 332 + remove_file(self.path.join("tasks").join(format!("{id}.tsk")))?; 333 + Ok(task) 334 } 335 336 pub fn search( ··· 350 workspace: self, 351 }; 352 // search the entirety of a task 353 + Ok(fzf::select::<_, Id, _>( 354 + loader, 355 + [ 356 + "--no-multi-line", 357 + "--accept-nth=1", 358 + "--delimiter=\t", 359 + "--preview=tsk show -T {1}", 360 + "--preview-window=top", 361 + "--ansi", 362 + "--info-command=tsk show -T {1} | head -n1", 363 + "--info=inline-right", 364 + ], 365 + )?) 366 } else { 367 // just search the stack 368 + Ok(fzf::select::<_, Id, _>( 369 + stack, 370 + ["--delimiter=\t", "--accept-nth=1"], 371 + )?) 372 } 373 } 374 375 + pub fn prioritize(&self, identifier: TaskIdentifier) -> Result<()> { 376 let id = self.resolve(identifier)?; 377 let mut stack = self.read_stack()?; 378 let index = &stack.iter().map(|i| i.id).position(|i| i == id); ··· 384 } 385 Ok(()) 386 } 387 + 388 + pub fn deprioritize(&self, identifier: TaskIdentifier) -> Result<()> { 389 + let id = self.resolve(identifier)?; 390 + let mut stack = self.read_stack()?; 391 + let index = &stack.iter().map(|i| i.id).position(|i| i == id); 392 + if let Some(index) = index { 393 + let deprioritized_task = stack.remove(*index); 394 + // unwrap here is safe because we just searched for the index and know it exists 395 + stack.push_back(deprioritized_task.unwrap()); 396 + stack.save()?; 397 + } 398 + Ok(()) 399 + } 400 } 401 402 pub struct Task { ··· 404 pub title: String, 405 pub body: String, 406 pub file: Flock<File>, 407 + pub attributes: Attrs, 408 } 409 410 + impl Display for Task { 411 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 412 + write!(f, "{}\n\n{}", self.title, &self.body) 413 + } 414 } 415 416 impl Task { ··· 424 Ok(()) 425 } 426 427 + /// Returns a [`SearchTas`] which is plain task data with no file or attrs 428 + fn bare(self) -> SearchTask { 429 + SearchTask { 430 id: self.id, 431 title: self.title, 432 body: self.body, ··· 434 } 435 } 436 437 + /// A task container without a file handle 438 + pub struct SearchTask { 439 + pub id: Id, 440 + pub title: String, 441 + pub body: String, 442 } 443 444 + impl Display for SearchTask { 445 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 446 + write!(f, "{}\t{}", self.id, self.title.trim())?; 447 + if !self.body.is_empty() { 448 + write!(f, "\n\n{}", self.body)?; 449 + } 450 + Ok(()) 451 } 452 } 453 ··· 456 workspace: &'a Workspace, 457 } 458 459 + impl Iterator for LazyTaskLoader<'_> { 460 + type Item = SearchTask; 461 462 fn next(&mut self) -> Option<Self::Item> { 463 let stack_item = self.files.next()?; ··· 469 } 470 } 471 472 + fn select_task(input: impl IntoIterator<Item = SearchTask>) -> Result<Option<Id>> { 473 + let mut child = Command::new("cat") 474 + .stderr(Stdio::inherit()) 475 + .stdin(Stdio::piped()) 476 + .stdout(Stdio::piped()) 477 + .spawn()?; 478 + let child_in = child.stdin.as_mut().unwrap(); 479 + for item in input.into_iter() { 480 + writeln!(child_in, "{item}\0")?; 481 + } 482 + let output = child.wait_with_output()?; 483 + if output.stdout.is_empty() { 484 + Ok(None) 485 + } else { 486 + Ok(Some(String::from_utf8(output.stdout)?.parse()?)) 487 + } 488 + } 489 + 490 #[cfg(test)] 491 mod test { 492 use super::*; 493 494 #[test] 495 fn test_bare_task_display() { 496 + let task = SearchTask { 497 id: Id(123), 498 title: "Hello, world".to_string(), 499 + body: "The body of the task.\nAnother line is here.".to_string(), 500 }; 501 assert_eq!( 502 + "tsk-123\tHello, world\n\nThe body of the task.\nAnother line is here.", 503 task.to_string() 504 ); 505 + } 506 + 507 + #[test] 508 + fn test_task_display() { 509 + let task = Task { 510 + id: Id(123), 511 + title: "Hello, world".to_string(), 512 + body: "The body of the task.".to_string(), 513 + file: util::flopen("/dev/null".into(), FlockArg::LockShared).unwrap(), 514 + attributes: Default::default(), 515 + }; 516 + assert_eq!("Hello, world\n\nThe body of the task.", task.to_string()); 517 } 518 }