A file-based task manager

Compare changes

Choose any two refs to compare.

+1971 -857
+1
.envrc
··· 1 + use flake
+2 -1
.gitignore
··· 1 1 /target 2 - .tsk/ 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 -723
Cargo.lock
··· 1 1 # This file is automatically @generated by Cargo. 2 2 # It is not intended for manual editing. 3 - version = 3 3 + version = 4 4 4 5 5 [[package]] 6 6 name = "anstream" 7 - version = "0.6.15" 7 + version = "0.6.20" 8 8 source = "registry+https://github.com/rust-lang/crates.io-index" 9 - checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" 9 + checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" 10 10 dependencies = [ 11 11 "anstyle", 12 12 "anstyle-parse", ··· 19 19 20 20 [[package]] 21 21 name = "anstyle" 22 - version = "1.0.8" 22 + version = "1.0.11" 23 23 source = "registry+https://github.com/rust-lang/crates.io-index" 24 - checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 24 + checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 25 25 26 26 [[package]] 27 27 name = "anstyle-parse" 28 - version = "0.2.5" 28 + version = "0.2.7" 29 29 source = "registry+https://github.com/rust-lang/crates.io-index" 30 - checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" 30 + checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 31 31 dependencies = [ 32 32 "utf8parse", 33 33 ] 34 34 35 35 [[package]] 36 36 name = "anstyle-query" 37 - version = "1.1.1" 37 + version = "1.1.4" 38 38 source = "registry+https://github.com/rust-lang/crates.io-index" 39 - checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" 39 + checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 40 40 dependencies = [ 41 - "windows-sys 0.52.0", 41 + "windows-sys 0.60.2", 42 42 ] 43 43 44 44 [[package]] 45 45 name = "anstyle-wincon" 46 - version = "3.0.4" 46 + version = "3.0.10" 47 47 source = "registry+https://github.com/rust-lang/crates.io-index" 48 - checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" 48 + checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 49 49 dependencies = [ 50 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", 51 + "once_cell_polyfill", 52 + "windows-sys 0.60.2", 182 53 ] 183 54 184 55 [[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 56 name = "bitflags" 204 - version = "2.6.0" 57 + version = "2.9.4" 205 58 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 - ] 59 + checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" 220 60 221 61 [[package]] 222 62 name = "cfg-if" 223 - version = "1.0.0" 63 + version = "1.0.3" 224 64 source = "registry+https://github.com/rust-lang/crates.io-index" 225 - checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 65 + checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 226 66 227 67 [[package]] 228 68 name = "cfg_aliases" ··· 232 72 233 73 [[package]] 234 74 name = "clap" 235 - version = "4.5.19" 75 + version = "4.5.47" 236 76 source = "registry+https://github.com/rust-lang/crates.io-index" 237 - checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" 77 + checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" 238 78 dependencies = [ 239 79 "clap_builder", 240 80 "clap_derive", ··· 242 82 243 83 [[package]] 244 84 name = "clap_builder" 245 - version = "4.5.19" 85 + version = "4.5.47" 246 86 source = "registry+https://github.com/rust-lang/crates.io-index" 247 - checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" 87 + checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" 248 88 dependencies = [ 249 89 "anstream", 250 90 "anstyle", ··· 254 94 255 95 [[package]] 256 96 name = "clap_complete" 257 - version = "4.5.32" 97 + version = "4.5.57" 258 98 source = "registry+https://github.com/rust-lang/crates.io-index" 259 - checksum = "74a01f4f9ee6c066d42a1c8dedf0dcddad16c72a8981a309d6398de3a75b0c39" 99 + checksum = "4d9501bd3f5f09f7bbee01da9a511073ed30a80cd7a509f1214bb74eadea71ad" 260 100 dependencies = [ 261 101 "clap", 262 102 ] 263 103 264 104 [[package]] 265 105 name = "clap_derive" 266 - version = "4.5.18" 106 + version = "4.5.47" 267 107 source = "registry+https://github.com/rust-lang/crates.io-index" 268 - checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 108 + checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" 269 109 dependencies = [ 270 110 "heck", 271 111 "proc-macro2", ··· 275 115 276 116 [[package]] 277 117 name = "clap_lex" 278 - version = "0.7.2" 118 + version = "0.7.5" 279 119 source = "registry+https://github.com/rust-lang/crates.io-index" 280 - checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" 120 + checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 281 121 282 122 [[package]] 283 123 name = "clap_mangen" 284 - version = "0.2.23" 124 + version = "0.2.29" 285 125 source = "registry+https://github.com/rust-lang/crates.io-index" 286 - checksum = "f17415fd4dfbea46e3274fcd8d368284519b358654772afb700dc2e8d2b24eeb" 126 + checksum = "27b4c3c54b30f0d9adcb47f25f61fcce35c4dd8916638c6b82fbd5f4fb4179e2" 287 127 dependencies = [ 288 128 "clap", 289 129 "roff", ··· 291 131 292 132 [[package]] 293 133 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" 134 + version = "1.0.4" 310 135 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 - ] 136 + checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 329 137 330 138 [[package]] 331 - name = "crossterm_winapi" 332 - version = "0.9.1" 139 + name = "colored" 140 + version = "3.0.0" 333 141 source = "registry+https://github.com/rust-lang/crates.io-index" 334 - checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 142 + checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" 335 143 dependencies = [ 336 - "winapi", 144 + "windows-sys 0.59.0", 337 145 ] 338 146 339 147 [[package]] 340 - name = "debugit" 341 - version = "0.1.2" 148 + name = "displaydoc" 149 + version = "0.2.5" 342 150 source = "registry+https://github.com/rust-lang/crates.io-index" 343 - checksum = "63c2f7e3034df2b09f750327e23c1adfe33301e6b7388f05bb4fcc0fa46825e3" 151 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 344 152 dependencies = [ 345 - "version_check 0.1.5", 153 + "proc-macro2", 154 + "quote", 155 + "syn", 346 156 ] 347 157 348 158 [[package]] ··· 357 167 358 168 [[package]] 359 169 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" 170 + version = "1.15.0" 367 171 source = "registry+https://github.com/rust-lang/crates.io-index" 368 - checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 172 + checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 369 173 370 174 [[package]] 371 175 name = "errno" 372 - version = "0.3.9" 176 + version = "0.3.13" 373 177 source = "registry+https://github.com/rust-lang/crates.io-index" 374 - checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 178 + checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 375 179 dependencies = [ 376 180 "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", 181 + "windows-sys 0.60.2", 399 182 ] 400 183 401 184 [[package]] 402 185 name = "fastrand" 403 - version = "2.1.1" 186 + version = "2.3.0" 404 187 source = "registry+https://github.com/rust-lang/crates.io-index" 405 - checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" 188 + checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 406 189 407 190 [[package]] 408 - name = "futures" 409 - version = "0.3.30" 191 + name = "form_urlencoded" 192 + version = "1.2.2" 410 193 source = "registry+https://github.com/rust-lang/crates.io-index" 411 - checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" 194 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 412 195 dependencies = [ 413 - "futures-channel", 414 - "futures-core", 415 - "futures-executor", 416 - "futures-io", 417 - "futures-sink", 418 - "futures-task", 419 - "futures-util", 196 + "percent-encoding", 420 197 ] 421 198 422 199 [[package]] 423 - name = "futures-channel" 424 - version = "0.3.30" 200 + name = "getrandom" 201 + version = "0.3.3" 425 202 source = "registry+https://github.com/rust-lang/crates.io-index" 426 - checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 203 + checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 427 204 dependencies = [ 428 - "futures-core", 429 - "futures-sink", 205 + "cfg-if", 206 + "libc", 207 + "r-efi", 208 + "wasi", 430 209 ] 431 210 432 211 [[package]] 433 - name = "futures-core" 434 - version = "0.3.30" 212 + name = "heck" 213 + version = "0.5.0" 435 214 source = "registry+https://github.com/rust-lang/crates.io-index" 436 - checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 215 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 437 216 438 217 [[package]] 439 - name = "futures-executor" 440 - version = "0.3.30" 218 + name = "home" 219 + version = "0.5.11" 441 220 source = "registry+https://github.com/rust-lang/crates.io-index" 442 - checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" 221 + checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 443 222 dependencies = [ 444 - "futures-core", 445 - "futures-task", 446 - "futures-util", 223 + "windows-sys 0.59.0", 447 224 ] 448 225 449 226 [[package]] 450 - name = "futures-io" 451 - version = "0.3.30" 227 + name = "icu_collections" 228 + version = "2.0.0" 452 229 source = "registry+https://github.com/rust-lang/crates.io-index" 453 - checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 230 + checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 231 + dependencies = [ 232 + "displaydoc", 233 + "potential_utf", 234 + "yoke", 235 + "zerofrom", 236 + "zerovec", 237 + ] 454 238 455 239 [[package]] 456 - name = "futures-lite" 457 - version = "2.3.0" 240 + name = "icu_locale_core" 241 + version = "2.0.0" 458 242 source = "registry+https://github.com/rust-lang/crates.io-index" 459 - checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" 243 + checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 460 244 dependencies = [ 461 - "fastrand", 462 - "futures-core", 463 - "futures-io", 464 - "parking", 465 - "pin-project-lite", 245 + "displaydoc", 246 + "litemap", 247 + "tinystr", 248 + "writeable", 249 + "zerovec", 466 250 ] 467 251 468 252 [[package]] 469 - name = "futures-macro" 470 - version = "0.3.30" 253 + name = "icu_normalizer" 254 + version = "2.0.0" 471 255 source = "registry+https://github.com/rust-lang/crates.io-index" 472 - checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 256 + checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 473 257 dependencies = [ 474 - "proc-macro2", 475 - "quote", 476 - "syn", 258 + "displaydoc", 259 + "icu_collections", 260 + "icu_normalizer_data", 261 + "icu_properties", 262 + "icu_provider", 263 + "smallvec", 264 + "zerovec", 477 265 ] 478 266 479 267 [[package]] 480 - name = "futures-sink" 481 - version = "0.3.30" 482 - source = "registry+https://github.com/rust-lang/crates.io-index" 483 - checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 484 - 485 - [[package]] 486 - name = "futures-task" 487 - version = "0.3.30" 268 + name = "icu_normalizer_data" 269 + version = "2.0.0" 488 270 source = "registry+https://github.com/rust-lang/crates.io-index" 489 - checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 271 + checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 490 272 491 273 [[package]] 492 - name = "futures-util" 493 - version = "0.3.30" 274 + name = "icu_properties" 275 + version = "2.0.1" 494 276 source = "registry+https://github.com/rust-lang/crates.io-index" 495 - checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 277 + checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 496 278 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", 279 + "displaydoc", 280 + "icu_collections", 281 + "icu_locale_core", 282 + "icu_properties_data", 283 + "icu_provider", 284 + "potential_utf", 285 + "zerotrie", 286 + "zerovec", 507 287 ] 508 288 509 289 [[package]] 510 - name = "generational-box" 511 - version = "0.5.6" 290 + name = "icu_properties_data" 291 + version = "2.0.1" 512 292 source = "registry+https://github.com/rust-lang/crates.io-index" 513 - checksum = "557cf2cbacd0504c6bf8c29f52f8071e0de1d9783346713dc6121d7fa1e5d0e0" 514 - dependencies = [ 515 - "parking_lot", 516 - ] 293 + checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 517 294 518 295 [[package]] 519 - name = "getrandom" 520 - version = "0.2.15" 296 + name = "icu_provider" 297 + version = "2.0.0" 521 298 source = "registry+https://github.com/rust-lang/crates.io-index" 522 - checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 299 + checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 523 300 dependencies = [ 524 - "cfg-if", 525 - "libc", 526 - "wasi", 301 + "displaydoc", 302 + "icu_locale_core", 303 + "stable_deref_trait", 304 + "tinystr", 305 + "writeable", 306 + "yoke", 307 + "zerofrom", 308 + "zerotrie", 309 + "zerovec", 527 310 ] 528 311 529 312 [[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" 313 + name = "idna" 314 + version = "1.1.0" 556 315 source = "registry+https://github.com/rust-lang/crates.io-index" 557 - checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 316 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 558 317 dependencies = [ 559 - "windows-sys 0.52.0", 318 + "idna_adapter", 319 + "smallvec", 320 + "utf8_iter", 560 321 ] 561 322 562 323 [[package]] 563 - name = "indexmap" 564 - version = "2.6.0" 324 + name = "idna_adapter" 325 + version = "1.2.1" 565 326 source = "registry+https://github.com/rust-lang/crates.io-index" 566 - checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" 327 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 567 328 dependencies = [ 568 - "equivalent", 569 - "hashbrown", 329 + "icu_normalizer", 330 + "icu_properties", 570 331 ] 571 332 572 333 [[package]] 573 - name = "iocraft" 574 - version = "0.2.3" 334 + name = "is-docker" 335 + version = "0.2.0" 575 336 source = "registry+https://github.com/rust-lang/crates.io-index" 576 - checksum = "2a35ac1085a4234a6193f443b09de3ede720013201f6cf0d122d1513c5f6eaaf" 337 + checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" 577 338 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", 339 + "once_cell", 589 340 ] 590 341 591 342 [[package]] 592 - name = "iocraft-macros" 593 - version = "0.1.5" 343 + name = "is-wsl" 344 + version = "0.4.0" 594 345 source = "registry+https://github.com/rust-lang/crates.io-index" 595 - checksum = "b2737d46d5f3c13db67066e5055ccda0d2379f993f77ce2ca90cf1dbd92edfe4" 346 + checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" 596 347 dependencies = [ 597 - "proc-macro2", 598 - "quote", 599 - "syn", 600 - "uuid", 348 + "is-docker", 349 + "once_cell", 601 350 ] 602 351 603 352 [[package]] ··· 607 356 checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 608 357 609 358 [[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" 359 + name = "itertools" 360 + version = "0.14.0" 624 361 source = "registry+https://github.com/rust-lang/crates.io-index" 625 - checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 362 + checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 626 363 dependencies = [ 627 - "autocfg", 628 - "scopeguard", 364 + "either", 629 365 ] 630 366 631 367 [[package]] 632 - name = "log" 633 - version = "0.4.22" 368 + name = "libc" 369 + version = "0.2.175" 634 370 source = "registry+https://github.com/rust-lang/crates.io-index" 635 - checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 371 + checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" 636 372 637 373 [[package]] 638 - name = "memchr" 639 - version = "2.7.4" 374 + name = "linux-raw-sys" 375 + version = "0.4.15" 640 376 source = "registry+https://github.com/rust-lang/crates.io-index" 641 - checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 377 + checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 642 378 643 379 [[package]] 644 - name = "mio" 645 - version = "1.0.2" 380 + name = "linux-raw-sys" 381 + version = "0.9.4" 646 382 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 - ] 383 + checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 655 384 656 385 [[package]] 657 - name = "mopa" 658 - version = "0.2.2" 386 + name = "litemap" 387 + version = "0.8.0" 659 388 source = "registry+https://github.com/rust-lang/crates.io-index" 660 - checksum = "a785740271256c230f57462d3b83e52f998433a7062fc18f96d5999474a9f915" 389 + checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 661 390 662 391 [[package]] 663 392 name = "nix" 664 - version = "0.29.0" 393 + version = "0.30.1" 665 394 source = "registry+https://github.com/rust-lang/crates.io-index" 666 - checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 395 + checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 667 396 dependencies = [ 668 397 "bitflags", 669 398 "cfg-if", ··· 672 401 ] 673 402 674 403 [[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 404 name = "once_cell" 685 - version = "1.20.1" 405 + version = "1.21.3" 686 406 source = "registry+https://github.com/rust-lang/crates.io-index" 687 - checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" 688 - dependencies = [ 689 - "portable-atomic", 690 - ] 407 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 691 408 692 409 [[package]] 693 - name = "parking" 694 - version = "2.2.1" 410 + name = "once_cell_polyfill" 411 + version = "1.70.1" 695 412 source = "registry+https://github.com/rust-lang/crates.io-index" 696 - checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 413 + checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 697 414 698 415 [[package]] 699 - name = "parking_lot" 700 - version = "0.12.3" 701 - source = "registry+https://github.com/rust-lang/crates.io-index" 702 - checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 703 - dependencies = [ 704 - "lock_api", 705 - "parking_lot_core", 706 - ] 707 - 708 - [[package]] 709 - name = "parking_lot_core" 710 - version = "0.9.10" 416 + name = "open" 417 + version = "5.3.2" 711 418 source = "registry+https://github.com/rust-lang/crates.io-index" 712 - checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 419 + checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" 713 420 dependencies = [ 714 - "cfg-if", 421 + "is-wsl", 715 422 "libc", 716 - "redox_syscall", 717 - "smallvec", 718 - "windows-targets", 423 + "pathdiff", 719 424 ] 720 425 721 426 [[package]] 722 - name = "pin-project-lite" 723 - version = "0.2.14" 427 + name = "pathdiff" 428 + version = "0.2.3" 724 429 source = "registry+https://github.com/rust-lang/crates.io-index" 725 - checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 430 + checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" 726 431 727 432 [[package]] 728 - name = "pin-utils" 729 - version = "0.1.0" 433 + name = "percent-encoding" 434 + version = "2.3.2" 730 435 source = "registry+https://github.com/rust-lang/crates.io-index" 731 - checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 436 + checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 732 437 733 438 [[package]] 734 - name = "piper" 735 - version = "0.2.4" 736 - source = "registry+https://github.com/rust-lang/crates.io-index" 737 - checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" 738 - dependencies = [ 739 - "atomic-waker", 740 - "fastrand", 741 - "futures-io", 742 - ] 743 - 744 - [[package]] 745 - name = "polling" 746 - version = "3.7.3" 439 + name = "potential_utf" 440 + version = "0.1.3" 747 441 source = "registry+https://github.com/rust-lang/crates.io-index" 748 - checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" 442 + checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" 749 443 dependencies = [ 750 - "cfg-if", 751 - "concurrent-queue", 752 - "hermit-abi 0.4.0", 753 - "pin-project-lite", 754 - "rustix", 755 - "tracing", 756 - "windows-sys 0.59.0", 444 + "zerovec", 757 445 ] 758 446 759 447 [[package]] 760 - name = "portable-atomic" 761 - version = "1.9.0" 762 - source = "registry+https://github.com/rust-lang/crates.io-index" 763 - checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" 764 - 765 - [[package]] 766 448 name = "proc-macro2" 767 - version = "1.0.86" 449 + version = "1.0.101" 768 450 source = "registry+https://github.com/rust-lang/crates.io-index" 769 - checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 451 + checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 770 452 dependencies = [ 771 453 "unicode-ident", 772 454 ] 773 455 774 456 [[package]] 775 457 name = "quote" 776 - version = "1.0.37" 458 + version = "1.0.40" 777 459 source = "registry+https://github.com/rust-lang/crates.io-index" 778 - checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 460 + checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 779 461 dependencies = [ 780 462 "proc-macro2", 781 463 ] 782 464 783 465 [[package]] 784 - name = "redox_syscall" 785 - version = "0.5.7" 466 + name = "r-efi" 467 + version = "5.3.0" 786 468 source = "registry+https://github.com/rust-lang/crates.io-index" 787 - checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" 788 - dependencies = [ 789 - "bitflags", 790 - ] 469 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 791 470 792 471 [[package]] 793 472 name = "roff" ··· 797 476 798 477 [[package]] 799 478 name = "rustix" 800 - version = "0.38.37" 479 + version = "0.38.44" 801 480 source = "registry+https://github.com/rust-lang/crates.io-index" 802 - checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" 481 + checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 803 482 dependencies = [ 804 483 "bitflags", 805 484 "errno", 806 485 "libc", 807 - "linux-raw-sys", 808 - "windows-sys 0.52.0", 486 + "linux-raw-sys 0.4.15", 487 + "windows-sys 0.59.0", 809 488 ] 810 489 811 490 [[package]] 812 - name = "scopeguard" 813 - version = "1.2.0" 491 + name = "rustix" 492 + version = "1.0.8" 814 493 source = "registry+https://github.com/rust-lang/crates.io-index" 815 - checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 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 + ] 816 502 817 503 [[package]] 818 504 name = "serde" 819 - version = "1.0.210" 505 + version = "1.0.219" 820 506 source = "registry+https://github.com/rust-lang/crates.io-index" 821 - checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 507 + checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 822 508 dependencies = [ 823 509 "serde_derive", 824 510 ] 825 511 826 512 [[package]] 827 513 name = "serde_derive" 828 - version = "1.0.210" 514 + version = "1.0.219" 829 515 source = "registry+https://github.com/rust-lang/crates.io-index" 830 - checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 516 + checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 831 517 dependencies = [ 832 518 "proc-macro2", 833 519 "quote", ··· 835 521 ] 836 522 837 523 [[package]] 838 - name = "signal-hook" 839 - version = "0.3.17" 840 - source = "registry+https://github.com/rust-lang/crates.io-index" 841 - checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 842 - dependencies = [ 843 - "libc", 844 - "signal-hook-registry", 845 - ] 846 - 847 - [[package]] 848 - name = "signal-hook-mio" 849 - version = "0.2.4" 850 - source = "registry+https://github.com/rust-lang/crates.io-index" 851 - checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 852 - dependencies = [ 853 - "libc", 854 - "mio", 855 - "signal-hook", 856 - ] 857 - 858 - [[package]] 859 - name = "signal-hook-registry" 860 - version = "1.4.2" 861 - source = "registry+https://github.com/rust-lang/crates.io-index" 862 - checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 863 - dependencies = [ 864 - "libc", 865 - ] 866 - 867 - [[package]] 868 - name = "slab" 869 - version = "0.4.9" 870 - source = "registry+https://github.com/rust-lang/crates.io-index" 871 - checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 872 - dependencies = [ 873 - "autocfg", 874 - ] 875 - 876 - [[package]] 877 - name = "slotmap" 878 - version = "1.0.7" 879 - source = "registry+https://github.com/rust-lang/crates.io-index" 880 - checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" 881 - dependencies = [ 882 - "version_check 0.9.5", 883 - ] 884 - 885 - [[package]] 886 - name = "smallstr" 887 - version = "0.3.0" 888 - source = "registry+https://github.com/rust-lang/crates.io-index" 889 - checksum = "63b1aefdf380735ff8ded0b15f31aab05daf1f70216c01c02a12926badd1df9d" 890 - dependencies = [ 891 - "serde", 892 - "smallvec", 893 - ] 894 - 895 - [[package]] 896 524 name = "smallvec" 897 - version = "1.13.2" 525 + version = "1.15.1" 898 526 source = "registry+https://github.com/rust-lang/crates.io-index" 899 - checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 900 - 901 - [[package]] 902 - name = "smawk" 903 - version = "0.3.2" 904 - source = "registry+https://github.com/rust-lang/crates.io-index" 905 - checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" 527 + checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 906 528 907 529 [[package]] 908 - name = "smol" 909 - version = "2.0.2" 530 + name = "stable_deref_trait" 531 + version = "1.2.0" 910 532 source = "registry+https://github.com/rust-lang/crates.io-index" 911 - checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" 912 - dependencies = [ 913 - "async-channel", 914 - "async-executor", 915 - "async-fs", 916 - "async-io", 917 - "async-lock", 918 - "async-net", 919 - "async-process", 920 - "blocking", 921 - "futures-lite", 922 - ] 533 + checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 923 534 924 535 [[package]] 925 536 name = "strsim" ··· 929 540 930 541 [[package]] 931 542 name = "syn" 932 - version = "2.0.79" 543 + version = "2.0.106" 933 544 source = "registry+https://github.com/rust-lang/crates.io-index" 934 - checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" 545 + checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 935 546 dependencies = [ 936 547 "proc-macro2", 937 548 "quote", ··· 939 550 ] 940 551 941 552 [[package]] 942 - name = "tabwriter" 943 - version = "1.4.0" 944 - source = "registry+https://github.com/rust-lang/crates.io-index" 945 - checksum = "a327282c4f64f6dc37e3bba4c2b6842cc3a992f204fa58d917696a89f691e5f6" 946 - dependencies = [ 947 - "unicode-width", 948 - ] 949 - 950 - [[package]] 951 - name = "taffy" 952 - version = "0.5.2" 553 + name = "synstructure" 554 + version = "0.13.2" 953 555 source = "registry+https://github.com/rust-lang/crates.io-index" 954 - checksum = "9cb893bff0f80ae17d3a57e030622a967b8dbc90e38284d9b4b1442e23873c94" 556 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 955 557 dependencies = [ 956 - "arrayvec", 957 - "num-traits", 958 - "slotmap", 558 + "proc-macro2", 559 + "quote", 560 + "syn", 959 561 ] 960 562 961 563 [[package]] 962 564 name = "tempfile" 963 - version = "3.13.0" 565 + version = "3.21.0" 964 566 source = "registry+https://github.com/rust-lang/crates.io-index" 965 - checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" 567 + checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" 966 568 dependencies = [ 967 - "cfg-if", 968 569 "fastrand", 570 + "getrandom", 969 571 "once_cell", 970 - "rustix", 971 - "windows-sys 0.59.0", 972 - ] 973 - 974 - [[package]] 975 - name = "textwrap" 976 - version = "0.16.1" 977 - source = "registry+https://github.com/rust-lang/crates.io-index" 978 - checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" 979 - dependencies = [ 980 - "smawk", 981 - "unicode-linebreak", 982 - "unicode-width", 572 + "rustix 1.0.8", 573 + "windows-sys 0.60.2", 983 574 ] 984 575 985 576 [[package]] 986 577 name = "thiserror" 987 - version = "1.0.64" 578 + version = "2.0.16" 988 579 source = "registry+https://github.com/rust-lang/crates.io-index" 989 - checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" 580 + checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" 990 581 dependencies = [ 991 582 "thiserror-impl", 992 583 ] 993 584 994 585 [[package]] 995 586 name = "thiserror-impl" 996 - version = "1.0.64" 587 + version = "2.0.16" 997 588 source = "registry+https://github.com/rust-lang/crates.io-index" 998 - checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" 589 + checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" 999 590 dependencies = [ 1000 591 "proc-macro2", 1001 592 "quote", ··· 1003 594 ] 1004 595 1005 596 [[package]] 1006 - name = "tracing" 1007 - version = "0.1.40" 597 + name = "tinystr" 598 + version = "0.8.1" 1008 599 source = "registry+https://github.com/rust-lang/crates.io-index" 1009 - checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 600 + checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 1010 601 dependencies = [ 1011 - "pin-project-lite", 1012 - "tracing-core", 602 + "displaydoc", 603 + "zerovec", 1013 604 ] 1014 605 1015 606 [[package]] 1016 - name = "tracing-core" 1017 - version = "0.1.32" 1018 - source = "registry+https://github.com/rust-lang/crates.io-index" 1019 - checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1020 - 1021 - [[package]] 1022 - name = "tsk" 1023 - version = "0.1.0" 607 + name = "tsk-cli" 608 + version = "0.3.6" 1024 609 dependencies = [ 1025 610 "clap", 1026 611 "clap_complete", 1027 612 "clap_mangen", 613 + "colored", 1028 614 "edit", 1029 - "iocraft", 615 + "itertools", 1030 616 "nix", 1031 - "smallstr", 1032 - "smol", 1033 - "tabwriter", 617 + "open", 1034 618 "thiserror", 619 + "url", 1035 620 "xattr", 1036 621 ] 1037 622 1038 623 [[package]] 1039 624 name = "unicode-ident" 1040 - version = "1.0.13" 625 + version = "1.0.18" 1041 626 source = "registry+https://github.com/rust-lang/crates.io-index" 1042 - checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 627 + checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1043 628 1044 629 [[package]] 1045 - name = "unicode-linebreak" 1046 - version = "0.1.5" 630 + name = "url" 631 + version = "2.5.7" 1047 632 source = "registry+https://github.com/rust-lang/crates.io-index" 1048 - checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" 633 + checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 634 + dependencies = [ 635 + "form_urlencoded", 636 + "idna", 637 + "percent-encoding", 638 + "serde", 639 + ] 1049 640 1050 641 [[package]] 1051 - name = "unicode-width" 1052 - version = "0.1.14" 642 + name = "utf8_iter" 643 + version = "1.0.4" 1053 644 source = "registry+https://github.com/rust-lang/crates.io-index" 1054 - checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 645 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1055 646 1056 647 [[package]] 1057 648 name = "utf8parse" ··· 1060 651 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1061 652 1062 653 [[package]] 1063 - name = "uuid" 1064 - version = "1.10.0" 654 + name = "wasi" 655 + version = "0.14.4+wasi-0.2.4" 1065 656 source = "registry+https://github.com/rust-lang/crates.io-index" 1066 - checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" 657 + checksum = "88a5f4a424faf49c3c2c344f166f0662341d470ea185e939657aaff130f0ec4a" 1067 658 dependencies = [ 1068 - "getrandom", 659 + "wit-bindgen", 1069 660 ] 1070 661 1071 662 [[package]] 1072 - name = "version_check" 1073 - version = "0.1.5" 1074 - source = "registry+https://github.com/rust-lang/crates.io-index" 1075 - checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" 1076 - 1077 - [[package]] 1078 - name = "version_check" 1079 - version = "0.9.5" 1080 - source = "registry+https://github.com/rust-lang/crates.io-index" 1081 - checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1082 - 1083 - [[package]] 1084 - name = "wasi" 1085 - version = "0.11.0+wasi-snapshot-preview1" 1086 - source = "registry+https://github.com/rust-lang/crates.io-index" 1087 - checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1088 - 1089 - [[package]] 1090 663 name = "which" 1091 664 version = "4.4.2" 1092 665 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1095 668 "either", 1096 669 "home", 1097 670 "once_cell", 1098 - "rustix", 1099 - ] 1100 - 1101 - [[package]] 1102 - name = "winapi" 1103 - version = "0.3.9" 1104 - source = "registry+https://github.com/rust-lang/crates.io-index" 1105 - checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1106 - dependencies = [ 1107 - "winapi-i686-pc-windows-gnu", 1108 - "winapi-x86_64-pc-windows-gnu", 671 + "rustix 0.38.44", 1109 672 ] 1110 673 1111 674 [[package]] 1112 - name = "winapi-i686-pc-windows-gnu" 1113 - version = "0.4.0" 1114 - source = "registry+https://github.com/rust-lang/crates.io-index" 1115 - checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1116 - 1117 - [[package]] 1118 - name = "winapi-x86_64-pc-windows-gnu" 1119 - version = "0.4.0" 675 + name = "windows-link" 676 + version = "0.1.3" 1120 677 source = "registry+https://github.com/rust-lang/crates.io-index" 1121 - checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 678 + checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 1122 679 1123 680 [[package]] 1124 681 name = "windows-sys" 1125 - version = "0.52.0" 682 + version = "0.59.0" 1126 683 source = "registry+https://github.com/rust-lang/crates.io-index" 1127 - checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 684 + checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1128 685 dependencies = [ 1129 - "windows-targets", 686 + "windows-targets 0.52.6", 1130 687 ] 1131 688 1132 689 [[package]] 1133 690 name = "windows-sys" 1134 - version = "0.59.0" 691 + version = "0.60.2" 1135 692 source = "registry+https://github.com/rust-lang/crates.io-index" 1136 - checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 693 + checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 1137 694 dependencies = [ 1138 - "windows-targets", 695 + "windows-targets 0.53.3", 1139 696 ] 1140 697 1141 698 [[package]] ··· 1144 701 source = "registry+https://github.com/rust-lang/crates.io-index" 1145 702 checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1146 703 dependencies = [ 1147 - "windows_aarch64_gnullvm", 1148 - "windows_aarch64_msvc", 1149 - "windows_i686_gnu", 1150 - "windows_i686_gnullvm", 1151 - "windows_i686_msvc", 1152 - "windows_x86_64_gnu", 1153 - "windows_x86_64_gnullvm", 1154 - "windows_x86_64_msvc", 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", 1155 729 ] 1156 730 1157 731 [[package]] ··· 1161 735 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1162 736 1163 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]] 1164 744 name = "windows_aarch64_msvc" 1165 745 version = "0.52.6" 1166 746 source = "registry+https://github.com/rust-lang/crates.io-index" 1167 747 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1168 748 1169 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]] 1170 756 name = "windows_i686_gnu" 1171 757 version = "0.52.6" 1172 758 source = "registry+https://github.com/rust-lang/crates.io-index" 1173 759 checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1174 760 1175 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]] 1176 768 name = "windows_i686_gnullvm" 1177 769 version = "0.52.6" 1178 770 source = "registry+https://github.com/rust-lang/crates.io-index" 1179 771 checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1180 772 1181 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]] 1182 780 name = "windows_i686_msvc" 1183 781 version = "0.52.6" 1184 782 source = "registry+https://github.com/rust-lang/crates.io-index" 1185 783 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1186 784 1187 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]] 1188 792 name = "windows_x86_64_gnu" 1189 793 version = "0.52.6" 1190 794 source = "registry+https://github.com/rust-lang/crates.io-index" 1191 795 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1192 796 1193 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]] 1194 804 name = "windows_x86_64_gnullvm" 1195 805 version = "0.52.6" 1196 806 source = "registry+https://github.com/rust-lang/crates.io-index" 1197 807 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1198 808 1199 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]] 1200 816 name = "windows_x86_64_msvc" 1201 817 version = "0.52.6" 1202 818 source = "registry+https://github.com/rust-lang/crates.io-index" 1203 819 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1204 820 1205 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]] 1206 840 name = "xattr" 1207 - version = "1.3.1" 841 + version = "1.5.1" 1208 842 source = "registry+https://github.com/rust-lang/crates.io-index" 1209 - checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" 843 + checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" 1210 844 dependencies = [ 1211 845 "libc", 1212 - "linux-raw-sys", 1213 - "rustix", 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", 1214 925 ]
+26 -13
Cargo.toml
··· 1 1 [package] 2 + name = "tsk-cli" 3 + version = "0.3.6" 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]] 2 14 name = "tsk" 3 - version = "0.1.0" 4 - edition = "2021" 15 + path = "src/main.rs" 5 16 6 17 [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" 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 27 #[allow(dead_code)] 28 28 #[error("An unexpected error occurred: {0}")] 29 29 Oops(Box<dyn std::error::Error>), 30 + #[error("System time/clock error: {0}")] 31 + SystemTime(#[from] std::time::SystemTimeError), 30 32 } 31 33 32 34 impl From<Infallible> for Error {
+14 -6
src/fzf.rs
··· 1 1 use crate::errors::{Error, Result}; 2 + use std::ffi::OsStr; 2 3 use std::fmt::Display; 3 4 use std::io::Write; 4 5 use std::process::{Command, Stdio}; ··· 6 7 7 8 /// Sends each item as a line to stdin to the `fzf` command and returns the selected item's string 8 9 /// representation as output 9 - pub fn select<I>(input: impl IntoIterator<Item = I>) -> Result<Option<I>> 10 + pub fn select<I, O, S>( 11 + input: impl IntoIterator<Item = I>, 12 + extra: impl IntoIterator<Item = S>, 13 + ) -> Result<Option<O>> 10 14 where 11 - I: Display + FromStr, 12 - Error: From<<I as FromStr>::Err>, 15 + O: FromStr, 16 + I: Display, 17 + Error: From<<O as FromStr>::Err>, 18 + S: AsRef<OsStr>, 13 19 { 14 - let mut child = Command::new("fzf") 15 - .args(["-d", "\t"]) 20 + let mut command = Command::new("fzf"); 21 + let mut child = command 22 + .args(extra) 23 + .arg("--read0") 16 24 .stderr(Stdio::inherit()) 17 25 .stdin(Stdio::piped()) 18 26 .stdout(Stdio::piped()) ··· 20 28 // unwrap: this can never fail 21 29 let child_in = child.stdin.as_mut().unwrap(); 22 30 for item in input.into_iter() { 23 - write!(child_in, "{}\n", item.to_string())?; 31 + write!(child_in, "{item}\0")?; 24 32 } 25 33 let output = child.wait_with_output()?; 26 34 if output.stdout.is_empty() {
+264 -66
src/main.rs
··· 1 + mod attrs; 1 2 mod errors; 2 3 mod fzf; 3 4 mod stack; 5 + mod task; 4 6 mod util; 5 7 mod workspace; 6 - use clap_complete::{generate, Shell}; 8 + use clap_complete::{Shell, generate}; 7 9 use errors::Result; 8 - use std::io; 10 + use std::io::{self, Write}; 9 11 use std::path::PathBuf; 10 12 use std::process::exit; 13 + use std::str::FromStr as _; 11 14 use std::{env::current_dir, io::Read}; 12 - use workspace::{Id, TaskIdentifier, Workspace}; 15 + use task::ParsedLink; 16 + use workspace::{Id, Task, TaskIdentifier, Workspace}; 13 17 14 18 //use smol; 15 19 //use iocraft::prelude::*; 16 - use clap::{value_parser, Args, CommandFactory, Parser, Subcommand}; 20 + use clap::{Args, CommandFactory, Parser, Subcommand}; 17 21 use edit::edit as open_editor; 18 22 19 23 fn default_dir() -> PathBuf { 20 24 current_dir().unwrap() 21 25 } 22 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 + 23 31 #[derive(Parser)] 24 32 // TODO: add long_about 25 33 #[command(version, about)] 26 34 struct Cli { 35 + /// Override the tsk root directory. 27 36 #[arg(short = 'C', env = "TSK_ROOT", value_name = "DIR")] 28 37 dir: Option<PathBuf>, 29 38 // TODO: other global options ··· 52 61 #[command(flatten)] 53 62 title: Title, 54 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. 55 83 List { 56 84 /// Whether to list all tasks in the task stack. If specified, -c / count is ignored. 57 85 #[arg(short = 'a', default_value_t = false)] ··· 78 106 79 107 /// Use fuzzy finding with `fzf` to search for a task 80 108 Find { 81 - /// Include the contents of tasks in the search criteria. 82 - #[arg(short = 'b', default_value_t = false)] 83 - search_body: bool, 84 - /// Include archived tasks in the search criteria. Combine with `-b` to include archived 85 - /// bodies in the search criteria. 86 - #[arg(short = 'a', default_value_t = false)] 87 - search_archived: bool, 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 + }, 88 129 89 - #[arg(short = 't', default_value_t = true)] 90 - full_id: bool, 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, 91 145 }, 92 146 93 147 /// Drops the task on the top of the stack and archives it. 94 - Drop, 148 + Drop { 149 + /// The [TSK-]ID of the task to drop. 150 + #[command(flatten)] 151 + task_id: TaskId, 152 + }, 95 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. 96 156 Rot, 157 + /// Moves the task on the top of the stack back behind the 2nd element, shifting the next two 158 + /// task up. 97 159 Tor, 98 160 99 - Reprioritize { 161 + /// Prioritizes an arbitrary task to the top of the stack. 162 + Prioritize { 100 163 /// The [TSK-]ID to prioritize. If it exists, it is moved to the top of the stack. 101 164 #[command(flatten)] 102 165 task_id: TaskId, 103 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 + }, 104 174 } 105 175 106 176 #[derive(Args)] ··· 123 193 id: Option<u32>, 124 194 125 195 /// The ID of the task to select with the 'tsk-' prefix. 126 - #[arg(short = 'T', value_name = "TSK-ID", value_parser = value_parser!(String))] 196 + #[arg(short = 'T', value_name = "TSK-ID", value_parser = parse_id)] 127 197 tsk_id: Option<Id>, 128 198 129 199 /// Selects a task relative to the top of the stack. ··· 131 201 #[arg(short = 'r', value_name = "RELATIVE", default_value_t = 0)] 132 202 relative_id: u32, 133 203 134 - /// Use fuzzy finding to search for and select a task. 135 - /// Does not support searching task bodies or archived tasks. 204 + #[command(flatten)] 205 + find: Find, 206 + } 207 + 208 + /// Use fuzzy finding to search for and select a task. 209 + /// Does not support searching task bodies or archived tasks. 210 + #[derive(Args)] 211 + #[group(required = false, multiple = true)] 212 + struct Find { 213 + /// Use fuzzy finding to select a task. 136 214 #[arg(short = 'f', value_name = "FIND", default_value_t = false)] 137 215 find: bool, 216 + #[command(flatten)] 217 + args: FindArgs, 218 + } 219 + 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. 229 + #[arg(short = 'a', default_value_t = false)] 230 + search_archived: bool, 231 + */ 138 232 } 139 233 140 234 impl From<TaskId> for TaskIdentifier { 141 235 fn from(value: TaskId) -> Self { 142 236 if let Some(id) = value.id.map(Id::from).or(value.tsk_id) { 143 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 + } 144 243 } else { 145 - if value.find { 146 - TaskIdentifier::Find 147 - } else { 148 - TaskIdentifier::Relative(value.relative_id) 149 - } 244 + TaskIdentifier::Relative(value.relative_id) 150 245 } 151 246 } 152 247 } ··· 154 249 fn main() { 155 250 let cli = Cli::parse(); 156 251 let dir = cli.dir.unwrap_or(default_dir()); 157 - let result = match cli.command { 252 + let var_name = match cli.command { 158 253 Commands::Init => command_init(dir), 159 254 Commands::Push { edit, body, title } => command_push(dir, edit, body, title), 255 + Commands::Append { edit, body, title } => command_append(dir, edit, body, title), 160 256 Commands::List { all, count } => command_list(dir, all, count), 161 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), 162 268 Commands::Edit { task_id } => command_edit(dir, task_id), 163 269 Commands::Completion { shell } => command_completion(shell), 164 - Commands::Drop => command_drop(dir), 165 - Commands::Find { 166 - full_id, 167 - search_body, 168 - search_archived, 169 - } => command_find(dir, full_id, search_body, search_archived), 270 + Commands::Drop { task_id } => command_drop(dir, task_id), 271 + Commands::Find { args, short_id } => command_find(dir, short_id, args), 170 272 Commands::Rot => Workspace::from_path(dir).unwrap().rot(), 171 273 Commands::Tor => Workspace::from_path(dir).unwrap().tor(), 172 - Commands::Reprioritize { task_id } => command_reprioritize(dir, task_id), 274 + Commands::Prioritize { task_id } => command_prioritize(dir, task_id), 275 + Commands::Deprioritize { task_id } => command_deprioritize(dir, task_id), 173 276 }; 277 + let result = var_name; 174 278 match result { 175 279 Ok(_) => exit(0), 176 280 Err(e) => { ··· 180 284 } 181 285 } 182 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 + 183 299 fn command_init(dir: PathBuf) -> Result<()> { 184 300 Workspace::init(dir) 185 301 } 186 302 187 - fn command_push(dir: PathBuf, edit: bool, body: Option<String>, title: Title) -> Result<()> { 188 - let workspace = Workspace::from_path(dir)?; 303 + fn create_task( 304 + workspace: &mut Workspace, 305 + edit: bool, 306 + body: Option<String>, 307 + title: Title, 308 + ) -> Result<Task> { 189 309 let mut title = if let Some(title) = title.title { 190 310 title 191 311 } else if let Some(title) = title.title_simple { 192 - let joined = title.join(" "); 193 - joined 312 + title.join(" ") 194 313 } else { 195 314 "".to_string() 196 315 }; 197 - let mut body = body.unwrap_or_default(); 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 + }; 198 331 if body == "-" { 199 332 // add newline so you can type directly in the shell 200 - eprintln!(""); 333 + //eprintln!(""); 201 334 body.clear(); 202 335 std::io::stdin().read_to_string(&mut body)?; 203 336 } ··· 208 341 body = content.1.to_string(); 209 342 } 210 343 } 344 + // Ensure title never contains newlines (invariant for index file format) 345 + title = title.replace(['\n', '\r'], " "); 211 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)?; 212 354 workspace.push_task(task) 213 355 } 214 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 + 215 363 fn command_list(dir: PathBuf, all: bool, count: usize) -> Result<()> { 216 364 let workspace = Workspace::from_path(dir)?; 217 - let stack = if all { 218 - workspace.read_stack()? 219 - } else { 220 - workspace.read_stack()? 221 - }; 365 + let stack = workspace.read_stack()?; 366 + 222 367 if stack.empty() { 223 368 println!("*No tasks*"); 224 369 exit(0); 225 - } else { 226 - if !all { 227 - for stack_item in stack.into_iter().take(count) { 228 - println!("{stack_item}"); 229 - } 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()); 230 379 } else { 231 - for stack_item in stack.into_iter() { 232 - println!("{stack_item}"); 233 - } 380 + println!("{stack_item}"); 234 381 } 235 382 } 236 383 Ok(()) ··· 246 393 let workspace = Workspace::from_path(dir)?; 247 394 let id: TaskIdentifier = id.into(); 248 395 let mut task = workspace.task(id)?; 396 + let pre_links = task::parse(&task.to_string()).map(|pt| pt.intenal_links()); 249 397 let new_content = open_editor(format!("{}\n\n{}", task.title.trim(), task.body.trim()))?; 250 398 if let Some((title, body)) = new_content.split_once("\n") { 251 - task.title = title.to_string(); 399 + // Ensure title never contains newlines (invariant for index file format) 400 + task.title = title.replace(['\n', '\r'], " "); 252 401 task.body = body.to_string(); 402 + workspace.handle_metadata(&task, pre_links)?; 253 403 task.save()?; 254 404 } 255 405 Ok(()) ··· 260 410 Ok(()) 261 411 } 262 412 263 - fn command_drop(dir: PathBuf) -> Result<()> { 264 - if let Some(id) = Workspace::from_path(dir)?.drop()? { 413 + fn command_drop(dir: PathBuf, task_id: TaskId) -> Result<()> { 414 + if let Some(id) = Workspace::from_path(dir)?.drop(task_id.into())? { 265 415 eprint!("Dropped "); 266 416 println!("{id}"); 267 417 } else { ··· 271 421 Ok(()) 272 422 } 273 423 274 - fn command_find( 275 - dir: PathBuf, 276 - full_id: bool, 277 - search_body: bool, 278 - search_archived: bool, 279 - ) -> Result<()> { 280 - let id = Workspace::from_path(dir)?.search(None, search_body, search_archived)?; 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)?; 281 426 if let Some(id) = id { 282 - if full_id { 283 - println!("{id}"); 284 - } else { 427 + if short_id { 285 428 // print as integer 286 429 println!("{}", id.0); 430 + } else { 431 + println!("{id}"); 287 432 } 288 433 } else { 289 - eprintln!("No task to drop."); 434 + eprintln!("No task selected."); 290 435 exit(1); 291 436 } 292 437 Ok(()) 293 438 } 294 439 295 - fn command_reprioritize(dir: PathBuf, task_id: TaskId) -> Result<()> { 296 - Workspace::from_path(dir)?.reprioritize(task_id.into()) 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 + } 297 495 }
+24 -17
src/stack.rs
··· 4 4 5 5 use crate::errors::{Error, Result}; 6 6 use crate::util; 7 - use std::collections::vec_deque::Iter; 8 7 use std::collections::VecDeque; 8 + use std::collections::vec_deque::Iter; 9 9 use std::fmt::Display; 10 + use std::fs::File; 10 11 use std::io::{self, BufRead, BufReader, Seek, Write}; 12 + use std::path::Path; 11 13 use std::str::FromStr; 12 14 use std::time::{Duration, SystemTime, UNIX_EPOCH}; 13 - use std::{fs::File, path::PathBuf}; 14 15 15 16 use nix::fcntl::{Flock, FlockArg}; 16 17 ··· 67 68 let mut parts = s.trim().split("\t"); 68 69 let id: Id = parts 69 70 .next() 70 - .ok_or(Error::Parse(format!( 71 - "Incomplete index line. Missing tsk ID" 72 - )))? 71 + .ok_or(Error::Parse( 72 + "Incomplete index line. Missing tsk ID".to_owned(), 73 + ))? 73 74 .parse()?; 74 75 let title: String = parts 75 76 .next() 76 - .ok_or(Error::Parse(format!( 77 - "Incomplete index line. Missing title." 78 - )))? 77 + .ok_or(Error::Parse( 78 + "Incomplete index line. Missing title.".to_owned(), 79 + ))? 79 80 .trim() 80 81 .to_string(); 81 82 // parse the timestamp as an integer ··· 96 97 97 98 impl StackItem { 98 99 /// 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> { 100 + /// files: task id title 101 + fn from_line(workspace_path: &Path, line: String) -> Result<Self> { 101 102 let mut stack_item: StackItem = line.parse()?; 102 103 103 104 let task = util::flopen( 104 105 workspace_path 105 106 .join(TASKSFOLDER) 106 - .join(stack_item.id.to_filename()), 107 + .join(stack_item.id.filename()), 107 108 FlockArg::LockExclusive, 108 109 )?; 109 110 let task_modify_time = task.metadata()?.modified()?; ··· 125 126 } 126 127 127 128 impl TaskStack { 128 - pub fn from_tskdir(workspace_path: &PathBuf) -> Result<Self> { 129 + pub fn from_tskdir(workspace_path: &Path) -> Result<Self> { 129 130 let file = util::flopen(workspace_path.join(INDEXFILE), FlockArg::LockExclusive)?; 130 131 let index = BufReader::new(&*file).lines(); 131 132 let mut all = VecDeque::new(); ··· 142 143 self.file.seek(std::io::SeekFrom::Start(0))?; 143 144 self.file.set_len(0)?; 144 145 for item in self.all.iter() { 145 - self.file.write_all(format!("{item}\n").as_bytes())?; 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())?; 146 149 } 147 150 Ok(()) 148 151 } ··· 151 154 self.all.push_front(item); 152 155 } 153 156 157 + pub fn push_back(&mut self, item: StackItem) { 158 + self.all.push_back(item); 159 + } 160 + 154 161 pub fn pop(&mut self) -> Option<StackItem> { 155 162 self.all.pop_front() 156 163 } ··· 158 165 pub fn swap(&mut self) { 159 166 let tip = self.all.pop_front(); 160 167 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()); 168 + if let Some((tip, second)) = tip.zip(second) { 169 + self.all.push_front(tip); 170 + self.all.push_front(second); 164 171 } 165 172 } 166 173 ··· 172 179 self.all.remove(index) 173 180 } 174 181 175 - pub fn iter(&self) -> Iter<StackItem> { 182 + pub fn iter(&self) -> Iter<'_, StackItem> { 176 183 self.all.iter() 177 184 } 178 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 13 .read(true) 14 14 .write(true) 15 15 .create(true) 16 + .truncate(false) 16 17 .open(path)?; 17 - Ok(Flock::lock(file, mode).map_err(|(_, errno)| Error::Lock(errno))?) 18 + Flock::lock(file, mode).map_err(|(_, errno)| Error::Lock(errno)) 18 19 } 19 20 20 21 /// Recursively searches upwards for a directory
+275 -30
src/workspace.rs
··· 1 1 #![allow(dead_code)] 2 2 use nix::fcntl::{Flock, FlockArg}; 3 + use xattr::FileExt; 3 4 5 + use crate::attrs::Attrs; 4 6 use crate::errors::{Error, Result}; 5 - use crate::stack::TaskStack; 7 + use crate::stack::{StackItem, TaskStack}; 8 + use crate::task::parse as parse_task; 6 9 use crate::{fzf, util}; 10 + use std::collections::{BTreeMap, HashSet, vec_deque}; 11 + use std::ffi::OsString; 7 12 use std::fmt::Display; 8 - use std::fs::{self, File}; 13 + use std::fs::{File, remove_file}; 9 14 use std::io::{BufRead as _, BufReader, Read, Seek, SeekFrom}; 15 + use std::ops::Deref; 16 + use std::os::unix::fs::symlink; 10 17 use std::path::PathBuf; 18 + use std::process::{Command, Stdio}; 11 19 use std::str::FromStr; 12 20 use std::{fs::OpenOptions, io::Write}; 13 21 14 22 const INDEXFILE: &str = "index"; 15 23 const TITLECACHEFILE: &str = "cache"; 24 + const XATTRPREFIX: &str = "user.tsk."; 25 + const BACKREFXATTR: &str = "user.tsk.references"; 16 26 /// A unique identifier for a task. When referenced in text, it is prefixed with `tsk-`. 17 - #[derive(Clone, Copy, Debug, Eq, PartialEq)] 27 + #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] 18 28 pub struct Id(pub u32); 19 29 20 30 impl FromStr for Id { 21 31 type Err = Error; 22 32 23 33 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { 24 - let s = s 25 - .strip_prefix("tsk-") 34 + let upper = s.to_uppercase(); 35 + let s = upper 36 + .trim() 37 + .strip_prefix("TSK-") 26 38 .ok_or(Self::Err::Parse(format!("expected tsk- prefix. Got {s}")))?; 27 39 Ok(Self(s.parse()?)) 28 40 } ··· 41 53 } 42 54 43 55 impl Id { 44 - pub fn to_filename(&self) -> String { 56 + /// Returns the filename for a task with this id. 57 + pub fn filename(&self) -> String { 45 58 format!("tsk-{}.tsk", self.0) 46 59 } 47 60 } ··· 49 62 pub enum TaskIdentifier { 50 63 Id(Id), 51 64 Relative(u32), 52 - Find, 65 + Find { exclude_body: bool, archived: bool }, 53 66 } 54 67 55 68 impl From<Id> for TaskIdentifier { ··· 73 86 } 74 87 std::fs::create_dir(&tsk_dir)?; 75 88 // Create the tasks directory 76 - std::fs::create_dir(&tsk_dir.join("tasks"))?; 89 + std::fs::create_dir(tsk_dir.join("tasks"))?; 77 90 // Create the archive directory 78 - std::fs::create_dir(&tsk_dir.join("archive"))?; 91 + std::fs::create_dir(tsk_dir.join("archive"))?; 79 92 let mut next = OpenOptions::new() 80 93 .read(true) 81 94 .write(true) 82 95 .create(true) 96 + .truncate(true) 83 97 .open(tsk_dir.join("next"))?; 98 + // initialize the next file with ID 1 84 99 next.write_all(b"1\n")?; 85 100 Ok(()) 86 101 } ··· 98 113 let stack_item = stack.get(r as usize).ok_or(Error::NoTasks)?; 99 114 Ok(stack_item.id) 100 115 } 101 - TaskIdentifier::Find => self.search(None, false, false)?.ok_or(Error::NotSelected), 116 + TaskIdentifier::Find { 117 + exclude_body, 118 + archived, 119 + } => self 120 + .search(None, !exclude_body, archived)? 121 + .ok_or(Error::NotSelected), 102 122 } 103 123 } 104 124 125 + /// Increments the `next` counter and returns the previous value. 105 126 pub fn next_id(&self) -> Result<Id> { 106 127 let mut file = util::flopen(self.path.join("next"), FlockArg::LockExclusive)?; 107 128 let mut buf = String::new(); ··· 119 140 // WARN: we could improperly increment the id if the task is not written to disk/errors. 120 141 // But who cares 121 142 let id = self.next_id()?; 122 - let task_path = self.path.join("tasks").join(format!("tsk-{}.tsk", id.0)); 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); 123 146 let mut file = util::flopen(task_path.clone(), FlockArg::LockExclusive)?; 124 147 file.write_all(format!("{title}\n\n{body}").as_bytes())?; 125 - // create a hardlink to the archive dir 126 - fs::hard_link( 127 - task_path, 128 - self.path.join("archive").join(format!("tsk-{}.tsk", id.0)), 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), 129 152 )?; 130 153 Ok(Task { 131 154 id, 132 155 title, 133 156 body, 134 157 file, 158 + attributes: Default::default(), 135 159 }) 136 160 } 137 161 ··· 148 172 reader.read_line(&mut title)?; 149 173 reader.read_to_string(&mut body)?; 150 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 + } 151 183 Ok(Task { 152 184 id, 153 - title, 154 - body, 155 185 file, 186 + title: title.trim().to_string(), 187 + body: body.trim().to_string(), 188 + attributes: Attrs::from_written(read_attributes), 156 189 }) 157 190 } 158 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 + 159 258 pub fn read_stack(&self) -> Result<TaskStack> { 160 259 TaskStack::from_tskdir(&self.path) 161 260 } ··· 167 266 Ok(()) 168 267 } 169 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 + 170 276 pub fn swap_top(&self) -> Result<()> { 171 277 let mut stack = TaskStack::from_tskdir(&self.path)?; 172 278 stack.swap(); ··· 184 290 return Ok(()); 185 291 } 186 292 293 + // unwrap is ok here because we checked above 187 294 stack.push(second.unwrap()); 188 295 stack.push(top.unwrap()); 189 296 stack.push(third.unwrap()); ··· 210 317 Ok(()) 211 318 } 212 319 213 - pub fn drop(&self) -> Result<Option<Id>> { 320 + pub fn drop(&self, identifier: TaskIdentifier) -> Result<Option<Id>> { 321 + let id = self.resolve(identifier)?; 214 322 let mut stack = self.read_stack()?; 215 - if let Some(stack_item) = stack.pop() { 216 - let task_path = self 217 - .path 218 - .join("tasks") 219 - .join(format!("{}.tsk", stack_item.id)); 220 - fs::remove_file(task_path)?; 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); 221 327 stack.save()?; 222 - Ok(Some(stack_item.id)) 328 + prioritized_task.map(|t| t.id) 223 329 } else { 224 - Ok(None) 225 - } 330 + None 331 + }; 332 + remove_file(self.path.join("tasks").join(format!("{id}.tsk")))?; 333 + Ok(task) 226 334 } 227 335 228 336 pub fn search( 229 337 &self, 230 338 stack: Option<TaskStack>, 231 - _search_body: bool, 339 + search_body: bool, 232 340 _include_archived: bool, 233 341 ) -> Result<Option<Id>> { 234 342 let stack = if let Some(stack) = stack { ··· 236 344 } else { 237 345 self.read_stack()? 238 346 }; 239 - Ok(fzf::select(stack)?.map(|si| si.id)) 347 + if search_body { 348 + let loader = LazyTaskLoader { 349 + files: stack.into_iter(), 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 + } 240 373 } 241 374 242 - pub fn reprioritize(&self, identifier: TaskIdentifier) -> Result<()> { 375 + pub fn prioritize(&self, identifier: TaskIdentifier) -> Result<()> { 243 376 let id = self.resolve(identifier)?; 244 377 let mut stack = self.read_stack()?; 245 378 let index = &stack.iter().map(|i| i.id).position(|i| i == id); ··· 251 384 } 252 385 Ok(()) 253 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 + } 254 400 } 255 401 256 402 pub struct Task { ··· 258 404 pub title: String, 259 405 pub body: String, 260 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 + } 261 414 } 262 415 263 416 impl Task { ··· 269 422 self.file.write_all(b"\n\n")?; 270 423 self.file.write_all(self.body.trim().as_bytes())?; 271 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, 433 + } 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 + 454 + struct LazyTaskLoader<'a> { 455 + files: vec_deque::IntoIter<StackItem>, 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()?; 464 + let task = self 465 + .workspace 466 + .task(TaskIdentifier::Id(stack_item.id)) 467 + .ok()?; 468 + Some(task.bare()) 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()); 272 517 } 273 518 }