A file-based task manager

Compare changes

Choose any two refs to compare.

+2152 -868
+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 -714
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", 51 + "once_cell_polyfill", 52 + "windows-sys 0.60.2", 164 53 ] 165 54 166 55 [[package]] 167 - name = "async-signal" 168 - version = "0.2.10" 169 - source = "registry+https://github.com/rust-lang/crates.io-index" 170 - checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" 171 - dependencies = [ 172 - "async-io", 173 - "async-lock", 174 - "atomic-waker", 175 - "cfg-if", 176 - "futures-core", 177 - "futures-io", 178 - "rustix", 179 - "signal-hook-registry", 180 - "slab", 181 - "windows-sys 0.59.0", 182 - ] 183 - 184 - [[package]] 185 - name = "async-task" 186 - version = "4.7.1" 187 - source = "registry+https://github.com/rust-lang/crates.io-index" 188 - checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" 189 - 190 - [[package]] 191 - name = "atomic-waker" 192 - version = "1.1.2" 193 - source = "registry+https://github.com/rust-lang/crates.io-index" 194 - checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 195 - 196 - [[package]] 197 - name = "autocfg" 198 - version = "1.4.0" 199 - source = "registry+https://github.com/rust-lang/crates.io-index" 200 - checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 201 - 202 - [[package]] 203 56 name = "bitflags" 204 - version = "2.6.0" 205 - source = "registry+https://github.com/rust-lang/crates.io-index" 206 - checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 207 - 208 - [[package]] 209 - name = "blocking" 210 - version = "1.6.1" 57 + version = "2.9.4" 211 58 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.18" 75 + version = "4.5.47" 236 76 source = "registry+https://github.com/rust-lang/crates.io-index" 237 - checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" 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.18" 85 + version = "4.5.47" 246 86 source = "registry+https://github.com/rust-lang/crates.io-index" 247 - checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" 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.29" 97 + version = "4.5.57" 258 98 source = "registry+https://github.com/rust-lang/crates.io-index" 259 - checksum = "8937760c3f4c60871870b8c3ee5f9b30771f792a7045c48bcbba999d7d6b3b8e" 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" 134 + version = "1.0.4" 295 135 source = "registry+https://github.com/rust-lang/crates.io-index" 296 - checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" 136 + checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 297 137 298 138 [[package]] 299 - name = "concurrent-queue" 300 - version = "2.5.0" 139 + name = "colored" 140 + version = "3.0.0" 301 141 source = "registry+https://github.com/rust-lang/crates.io-index" 302 - checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 142 + checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" 303 143 dependencies = [ 304 - "crossbeam-utils", 144 + "windows-sys 0.59.0", 305 145 ] 306 146 307 147 [[package]] 308 - name = "crossbeam-utils" 309 - version = "0.8.20" 148 + name = "displaydoc" 149 + version = "0.2.5" 310 150 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" 151 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 318 152 dependencies = [ 319 - "bitflags", 320 - "crossterm_winapi", 321 - "futures-core", 322 - "mio", 323 - "parking_lot", 324 - "rustix", 325 - "signal-hook", 326 - "signal-hook-mio", 327 - "winapi", 328 - ] 329 - 330 - [[package]] 331 - name = "crossterm_winapi" 332 - version = "0.9.1" 333 - source = "registry+https://github.com/rust-lang/crates.io-index" 334 - checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 335 - dependencies = [ 336 - "winapi", 337 - ] 338 - 339 - [[package]] 340 - name = "debugit" 341 - version = "0.1.2" 342 - source = "registry+https://github.com/rust-lang/crates.io-index" 343 - checksum = "63c2f7e3034df2b09f750327e23c1adfe33301e6b7388f05bb4fcc0fa46825e3" 344 - dependencies = [ 345 - "version_check 0.1.5", 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" 170 + version = "1.15.0" 361 171 source = "registry+https://github.com/rust-lang/crates.io-index" 362 - checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 363 - 364 - [[package]] 365 - name = "equivalent" 366 - version = "1.0.1" 367 - source = "registry+https://github.com/rust-lang/crates.io-index" 368 - checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 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" 454 - 455 - [[package]] 456 - name = "futures-lite" 457 - version = "2.3.0" 458 - source = "registry+https://github.com/rust-lang/crates.io-index" 459 - checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" 230 + checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 460 231 dependencies = [ 461 - "fastrand", 462 - "futures-core", 463 - "futures-io", 464 - "parking", 465 - "pin-project-lite", 232 + "displaydoc", 233 + "potential_utf", 234 + "yoke", 235 + "zerofrom", 236 + "zerovec", 466 237 ] 467 238 468 239 [[package]] 469 - name = "futures-macro" 470 - version = "0.3.30" 240 + name = "icu_locale_core" 241 + version = "2.0.0" 471 242 source = "registry+https://github.com/rust-lang/crates.io-index" 472 - checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 243 + checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 473 244 dependencies = [ 474 - "proc-macro2", 475 - "quote", 476 - "syn", 245 + "displaydoc", 246 + "litemap", 247 + "tinystr", 248 + "writeable", 249 + "zerovec", 477 250 ] 478 251 479 252 [[package]] 480 - name = "futures-sink" 481 - version = "0.3.30" 253 + name = "icu_normalizer" 254 + version = "2.0.0" 482 255 source = "registry+https://github.com/rust-lang/crates.io-index" 483 - checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 256 + checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 257 + dependencies = [ 258 + "displaydoc", 259 + "icu_collections", 260 + "icu_normalizer_data", 261 + "icu_properties", 262 + "icu_provider", 263 + "smallvec", 264 + "zerovec", 265 + ] 484 266 485 267 [[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.14.5" 532 - source = "registry+https://github.com/rust-lang/crates.io-index" 533 - checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 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" 313 + name = "idna" 314 + version = "1.1.0" 550 315 source = "registry+https://github.com/rust-lang/crates.io-index" 551 - checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" 552 - 553 - [[package]] 554 - name = "home" 555 - version = "0.5.9" 556 - source = "registry+https://github.com/rust-lang/crates.io-index" 557 - checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 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.5.0" 324 + name = "idna_adapter" 325 + version = "1.2.1" 565 326 source = "registry+https://github.com/rust-lang/crates.io-index" 566 - checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" 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.19.0" 686 - source = "registry+https://github.com/rust-lang/crates.io-index" 687 - checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 688 - 689 - [[package]] 690 - name = "parking" 691 - version = "2.2.1" 405 + version = "1.21.3" 692 406 source = "registry+https://github.com/rust-lang/crates.io-index" 693 - checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 407 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 694 408 695 409 [[package]] 696 - name = "parking_lot" 697 - version = "0.12.3" 410 + name = "once_cell_polyfill" 411 + version = "1.70.1" 698 412 source = "registry+https://github.com/rust-lang/crates.io-index" 699 - checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 700 - dependencies = [ 701 - "lock_api", 702 - "parking_lot_core", 703 - ] 413 + checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 704 414 705 415 [[package]] 706 - name = "parking_lot_core" 707 - version = "0.9.10" 416 + name = "open" 417 + version = "5.3.2" 708 418 source = "registry+https://github.com/rust-lang/crates.io-index" 709 - checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 419 + checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" 710 420 dependencies = [ 711 - "cfg-if", 421 + "is-wsl", 712 422 "libc", 713 - "redox_syscall", 714 - "smallvec", 715 - "windows-targets", 423 + "pathdiff", 716 424 ] 717 425 718 426 [[package]] 719 - name = "pin-project-lite" 720 - version = "0.2.14" 721 - source = "registry+https://github.com/rust-lang/crates.io-index" 722 - checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 723 - 724 - [[package]] 725 - name = "pin-utils" 726 - version = "0.1.0" 427 + name = "pathdiff" 428 + version = "0.2.3" 727 429 source = "registry+https://github.com/rust-lang/crates.io-index" 728 - checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 430 + checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" 729 431 730 432 [[package]] 731 - name = "piper" 732 - version = "0.2.4" 433 + name = "percent-encoding" 434 + version = "2.3.2" 733 435 source = "registry+https://github.com/rust-lang/crates.io-index" 734 - checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" 735 - dependencies = [ 736 - "atomic-waker", 737 - "fastrand", 738 - "futures-io", 739 - ] 436 + checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 740 437 741 438 [[package]] 742 - name = "polling" 743 - version = "3.7.3" 439 + name = "potential_utf" 440 + version = "0.1.3" 744 441 source = "registry+https://github.com/rust-lang/crates.io-index" 745 - checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" 442 + checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" 746 443 dependencies = [ 747 - "cfg-if", 748 - "concurrent-queue", 749 - "hermit-abi 0.4.0", 750 - "pin-project-lite", 751 - "rustix", 752 - "tracing", 753 - "windows-sys 0.59.0", 444 + "zerovec", 754 445 ] 755 446 756 447 [[package]] 757 448 name = "proc-macro2" 758 - version = "1.0.86" 449 + version = "1.0.101" 759 450 source = "registry+https://github.com/rust-lang/crates.io-index" 760 - checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 451 + checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 761 452 dependencies = [ 762 453 "unicode-ident", 763 454 ] 764 455 765 456 [[package]] 766 457 name = "quote" 767 - version = "1.0.37" 458 + version = "1.0.40" 768 459 source = "registry+https://github.com/rust-lang/crates.io-index" 769 - checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 460 + checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 770 461 dependencies = [ 771 462 "proc-macro2", 772 463 ] 773 464 774 465 [[package]] 775 - name = "redox_syscall" 776 - version = "0.5.6" 466 + name = "r-efi" 467 + version = "5.3.0" 777 468 source = "registry+https://github.com/rust-lang/crates.io-index" 778 - checksum = "355ae415ccd3a04315d3f8246e86d67689ea74d88d915576e1589a351062a13b" 779 - dependencies = [ 780 - "bitflags", 781 - ] 469 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 782 470 783 471 [[package]] 784 472 name = "roff" ··· 788 476 789 477 [[package]] 790 478 name = "rustix" 791 - version = "0.38.37" 479 + version = "0.38.44" 792 480 source = "registry+https://github.com/rust-lang/crates.io-index" 793 - checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" 481 + checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 794 482 dependencies = [ 795 483 "bitflags", 796 484 "errno", 797 485 "libc", 798 - "linux-raw-sys", 799 - "windows-sys 0.52.0", 486 + "linux-raw-sys 0.4.15", 487 + "windows-sys 0.59.0", 800 488 ] 801 489 802 490 [[package]] 803 - name = "scopeguard" 804 - version = "1.2.0" 491 + name = "rustix" 492 + version = "1.0.8" 805 493 source = "registry+https://github.com/rust-lang/crates.io-index" 806 - 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 + ] 807 502 808 503 [[package]] 809 504 name = "serde" 810 - version = "1.0.203" 505 + version = "1.0.219" 811 506 source = "registry+https://github.com/rust-lang/crates.io-index" 812 - checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" 507 + checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 813 508 dependencies = [ 814 509 "serde_derive", 815 510 ] 816 511 817 512 [[package]] 818 513 name = "serde_derive" 819 - version = "1.0.203" 514 + version = "1.0.219" 820 515 source = "registry+https://github.com/rust-lang/crates.io-index" 821 - checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" 516 + checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 822 517 dependencies = [ 823 518 "proc-macro2", 824 519 "quote", ··· 826 521 ] 827 522 828 523 [[package]] 829 - name = "signal-hook" 830 - version = "0.3.17" 831 - source = "registry+https://github.com/rust-lang/crates.io-index" 832 - checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 833 - dependencies = [ 834 - "libc", 835 - "signal-hook-registry", 836 - ] 837 - 838 - [[package]] 839 - name = "signal-hook-mio" 840 - version = "0.2.4" 841 - source = "registry+https://github.com/rust-lang/crates.io-index" 842 - checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 843 - dependencies = [ 844 - "libc", 845 - "mio", 846 - "signal-hook", 847 - ] 848 - 849 - [[package]] 850 - name = "signal-hook-registry" 851 - version = "1.4.2" 852 - source = "registry+https://github.com/rust-lang/crates.io-index" 853 - checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 854 - dependencies = [ 855 - "libc", 856 - ] 857 - 858 - [[package]] 859 - name = "slab" 860 - version = "0.4.9" 861 - source = "registry+https://github.com/rust-lang/crates.io-index" 862 - checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 863 - dependencies = [ 864 - "autocfg", 865 - ] 866 - 867 - [[package]] 868 - name = "slotmap" 869 - version = "1.0.7" 870 - source = "registry+https://github.com/rust-lang/crates.io-index" 871 - checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" 872 - dependencies = [ 873 - "version_check 0.9.5", 874 - ] 875 - 876 - [[package]] 877 - name = "smallstr" 878 - version = "0.3.0" 879 - source = "registry+https://github.com/rust-lang/crates.io-index" 880 - checksum = "63b1aefdf380735ff8ded0b15f31aab05daf1f70216c01c02a12926badd1df9d" 881 - dependencies = [ 882 - "serde", 883 - "smallvec", 884 - ] 885 - 886 - [[package]] 887 524 name = "smallvec" 888 - version = "1.13.2" 525 + version = "1.15.1" 889 526 source = "registry+https://github.com/rust-lang/crates.io-index" 890 - checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 527 + checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 891 528 892 529 [[package]] 893 - name = "smawk" 894 - version = "0.3.2" 530 + name = "stable_deref_trait" 531 + version = "1.2.0" 895 532 source = "registry+https://github.com/rust-lang/crates.io-index" 896 - checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" 897 - 898 - [[package]] 899 - name = "smol" 900 - version = "2.0.2" 901 - source = "registry+https://github.com/rust-lang/crates.io-index" 902 - checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" 903 - dependencies = [ 904 - "async-channel", 905 - "async-executor", 906 - "async-fs", 907 - "async-io", 908 - "async-lock", 909 - "async-net", 910 - "async-process", 911 - "blocking", 912 - "futures-lite", 913 - ] 533 + checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 914 534 915 535 [[package]] 916 536 name = "strsim" ··· 920 540 921 541 [[package]] 922 542 name = "syn" 923 - version = "2.0.77" 543 + version = "2.0.106" 924 544 source = "registry+https://github.com/rust-lang/crates.io-index" 925 - checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" 545 + checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 926 546 dependencies = [ 927 547 "proc-macro2", 928 548 "quote", ··· 930 550 ] 931 551 932 552 [[package]] 933 - name = "tabwriter" 934 - version = "1.4.0" 553 + name = "synstructure" 554 + version = "0.13.2" 935 555 source = "registry+https://github.com/rust-lang/crates.io-index" 936 - checksum = "a327282c4f64f6dc37e3bba4c2b6842cc3a992f204fa58d917696a89f691e5f6" 556 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 937 557 dependencies = [ 938 - "unicode-width", 939 - ] 940 - 941 - [[package]] 942 - name = "taffy" 943 - version = "0.5.2" 944 - source = "registry+https://github.com/rust-lang/crates.io-index" 945 - checksum = "9cb893bff0f80ae17d3a57e030622a967b8dbc90e38284d9b4b1442e23873c94" 946 - dependencies = [ 947 - "arrayvec", 948 - "num-traits", 949 - "slotmap", 558 + "proc-macro2", 559 + "quote", 560 + "syn", 950 561 ] 951 562 952 563 [[package]] 953 564 name = "tempfile" 954 - version = "3.12.0" 565 + version = "3.21.0" 955 566 source = "registry+https://github.com/rust-lang/crates.io-index" 956 - checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" 567 + checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" 957 568 dependencies = [ 958 - "cfg-if", 959 569 "fastrand", 570 + "getrandom", 960 571 "once_cell", 961 - "rustix", 962 - "windows-sys 0.59.0", 963 - ] 964 - 965 - [[package]] 966 - name = "textwrap" 967 - version = "0.16.1" 968 - source = "registry+https://github.com/rust-lang/crates.io-index" 969 - checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" 970 - dependencies = [ 971 - "smawk", 972 - "unicode-linebreak", 973 - "unicode-width", 572 + "rustix 1.0.8", 573 + "windows-sys 0.60.2", 974 574 ] 975 575 976 576 [[package]] 977 577 name = "thiserror" 978 - version = "1.0.64" 578 + version = "2.0.16" 979 579 source = "registry+https://github.com/rust-lang/crates.io-index" 980 - checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" 580 + checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" 981 581 dependencies = [ 982 582 "thiserror-impl", 983 583 ] 984 584 985 585 [[package]] 986 586 name = "thiserror-impl" 987 - version = "1.0.64" 587 + version = "2.0.16" 988 588 source = "registry+https://github.com/rust-lang/crates.io-index" 989 - checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" 589 + checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" 990 590 dependencies = [ 991 591 "proc-macro2", 992 592 "quote", ··· 994 594 ] 995 595 996 596 [[package]] 997 - name = "tracing" 998 - version = "0.1.40" 597 + name = "tinystr" 598 + version = "0.8.1" 999 599 source = "registry+https://github.com/rust-lang/crates.io-index" 1000 - checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 600 + checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 1001 601 dependencies = [ 1002 - "pin-project-lite", 1003 - "tracing-core", 602 + "displaydoc", 603 + "zerovec", 1004 604 ] 1005 605 1006 606 [[package]] 1007 - name = "tracing-core" 1008 - version = "0.1.32" 1009 - source = "registry+https://github.com/rust-lang/crates.io-index" 1010 - checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1011 - 1012 - [[package]] 1013 - name = "tsk" 1014 - version = "0.1.0" 607 + name = "tsk-cli" 608 + version = "0.4.0" 1015 609 dependencies = [ 1016 610 "clap", 1017 611 "clap_complete", 1018 612 "clap_mangen", 613 + "colored", 1019 614 "edit", 1020 - "iocraft", 615 + "itertools", 1021 616 "nix", 1022 - "smallstr", 1023 - "smol", 1024 - "tabwriter", 617 + "open", 1025 618 "thiserror", 619 + "url", 1026 620 "xattr", 1027 621 ] 1028 622 1029 623 [[package]] 1030 624 name = "unicode-ident" 1031 - version = "1.0.13" 625 + version = "1.0.18" 1032 626 source = "registry+https://github.com/rust-lang/crates.io-index" 1033 - checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 627 + checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1034 628 1035 629 [[package]] 1036 - name = "unicode-linebreak" 1037 - version = "0.1.5" 630 + name = "url" 631 + version = "2.5.7" 1038 632 source = "registry+https://github.com/rust-lang/crates.io-index" 1039 - checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" 633 + checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 634 + dependencies = [ 635 + "form_urlencoded", 636 + "idna", 637 + "percent-encoding", 638 + "serde", 639 + ] 1040 640 1041 641 [[package]] 1042 - name = "unicode-width" 1043 - version = "0.1.14" 642 + name = "utf8_iter" 643 + version = "1.0.4" 1044 644 source = "registry+https://github.com/rust-lang/crates.io-index" 1045 - checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 645 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1046 646 1047 647 [[package]] 1048 648 name = "utf8parse" ··· 1051 651 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1052 652 1053 653 [[package]] 1054 - name = "uuid" 1055 - version = "1.10.0" 654 + name = "wasi" 655 + version = "0.14.4+wasi-0.2.4" 1056 656 source = "registry+https://github.com/rust-lang/crates.io-index" 1057 - checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" 657 + checksum = "88a5f4a424faf49c3c2c344f166f0662341d470ea185e939657aaff130f0ec4a" 1058 658 dependencies = [ 1059 - "getrandom", 659 + "wit-bindgen", 1060 660 ] 1061 661 1062 662 [[package]] 1063 - name = "version_check" 1064 - version = "0.1.5" 1065 - source = "registry+https://github.com/rust-lang/crates.io-index" 1066 - checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" 1067 - 1068 - [[package]] 1069 - name = "version_check" 1070 - version = "0.9.5" 1071 - source = "registry+https://github.com/rust-lang/crates.io-index" 1072 - checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1073 - 1074 - [[package]] 1075 - name = "wasi" 1076 - version = "0.11.0+wasi-snapshot-preview1" 1077 - source = "registry+https://github.com/rust-lang/crates.io-index" 1078 - checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1079 - 1080 - [[package]] 1081 663 name = "which" 1082 664 version = "4.4.2" 1083 665 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1086 668 "either", 1087 669 "home", 1088 670 "once_cell", 1089 - "rustix", 1090 - ] 1091 - 1092 - [[package]] 1093 - name = "winapi" 1094 - version = "0.3.9" 1095 - source = "registry+https://github.com/rust-lang/crates.io-index" 1096 - checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1097 - dependencies = [ 1098 - "winapi-i686-pc-windows-gnu", 1099 - "winapi-x86_64-pc-windows-gnu", 671 + "rustix 0.38.44", 1100 672 ] 1101 673 1102 674 [[package]] 1103 - name = "winapi-i686-pc-windows-gnu" 1104 - version = "0.4.0" 1105 - source = "registry+https://github.com/rust-lang/crates.io-index" 1106 - checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1107 - 1108 - [[package]] 1109 - name = "winapi-x86_64-pc-windows-gnu" 1110 - version = "0.4.0" 675 + name = "windows-link" 676 + version = "0.1.3" 1111 677 source = "registry+https://github.com/rust-lang/crates.io-index" 1112 - checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 678 + checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 1113 679 1114 680 [[package]] 1115 681 name = "windows-sys" 1116 - version = "0.52.0" 682 + version = "0.59.0" 1117 683 source = "registry+https://github.com/rust-lang/crates.io-index" 1118 - checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 684 + checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1119 685 dependencies = [ 1120 - "windows-targets", 686 + "windows-targets 0.52.6", 1121 687 ] 1122 688 1123 689 [[package]] 1124 690 name = "windows-sys" 1125 - version = "0.59.0" 691 + version = "0.60.2" 1126 692 source = "registry+https://github.com/rust-lang/crates.io-index" 1127 - checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 693 + checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 1128 694 dependencies = [ 1129 - "windows-targets", 695 + "windows-targets 0.53.3", 1130 696 ] 1131 697 1132 698 [[package]] ··· 1135 701 source = "registry+https://github.com/rust-lang/crates.io-index" 1136 702 checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1137 703 dependencies = [ 1138 - "windows_aarch64_gnullvm", 1139 - "windows_aarch64_msvc", 1140 - "windows_i686_gnu", 1141 - "windows_i686_gnullvm", 1142 - "windows_i686_msvc", 1143 - "windows_x86_64_gnu", 1144 - "windows_x86_64_gnullvm", 1145 - "windows_x86_64_msvc", 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", 1146 729 ] 1147 730 1148 731 [[package]] ··· 1152 735 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1153 736 1154 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]] 1155 744 name = "windows_aarch64_msvc" 1156 745 version = "0.52.6" 1157 746 source = "registry+https://github.com/rust-lang/crates.io-index" 1158 747 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1159 748 1160 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]] 1161 756 name = "windows_i686_gnu" 1162 757 version = "0.52.6" 1163 758 source = "registry+https://github.com/rust-lang/crates.io-index" 1164 759 checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1165 760 1166 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]] 1167 768 name = "windows_i686_gnullvm" 1168 769 version = "0.52.6" 1169 770 source = "registry+https://github.com/rust-lang/crates.io-index" 1170 771 checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1171 772 1172 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]] 1173 780 name = "windows_i686_msvc" 1174 781 version = "0.52.6" 1175 782 source = "registry+https://github.com/rust-lang/crates.io-index" 1176 783 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1177 784 1178 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]] 1179 792 name = "windows_x86_64_gnu" 1180 793 version = "0.52.6" 1181 794 source = "registry+https://github.com/rust-lang/crates.io-index" 1182 795 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1183 796 1184 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]] 1185 804 name = "windows_x86_64_gnullvm" 1186 805 version = "0.52.6" 1187 806 source = "registry+https://github.com/rust-lang/crates.io-index" 1188 807 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1189 808 1190 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]] 1191 816 name = "windows_x86_64_msvc" 1192 817 version = "0.52.6" 1193 818 source = "registry+https://github.com/rust-lang/crates.io-index" 1194 819 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1195 820 1196 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]] 1197 840 name = "xattr" 1198 - version = "1.3.1" 841 + version = "1.5.1" 1199 842 source = "registry+https://github.com/rust-lang/crates.io-index" 1200 - checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" 843 + checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" 1201 844 dependencies = [ 1202 845 "libc", 1203 - "linux-raw-sys", 1204 - "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", 1205 925 ]
+26 -13
Cargo.toml
··· 1 1 [package] 2 + name = "tsk-cli" 3 + version = "0.4.0" 4 + edition = "2024" 5 + publish = true 6 + license-file = "LICENSE" 7 + description = "A command-line first, file-system based task manager" 8 + repository = "https://codeberg.org/ngp/tsk" 9 + homepage = "https://tsk.ngp.computer/" 10 + readme = "readme" 11 + authors = ["Noah Pederson <noah@packetlost.dev>"] 12 + 13 + [[bin]] 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 + }
+6
src/errors.rs
··· 20 20 Parse(String), 21 21 #[error("Error parsing bytes as utf-8: {0}")] 22 22 FromUtf8(#[from] FromUtf8Error), 23 + #[error("No tasks on stack")] 24 + NoTasks, 25 + #[error("No task selected.")] 26 + NotSelected, 23 27 #[allow(dead_code)] 24 28 #[error("An unexpected error occurred: {0}")] 25 29 Oops(Box<dyn std::error::Error>), 30 + #[error("System time/clock error: {0}")] 31 + SystemTime(#[from] std::time::SystemTimeError), 26 32 } 27 33 28 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() {
+346 -89
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}; 7 - use std::io; 8 + use clap_complete::{Shell, generate}; 9 + use errors::Result; 10 + use std::io::{self, Write}; 8 11 use std::path::PathBuf; 12 + use std::process::exit; 13 + use std::str::FromStr as _; 9 14 use std::{env::current_dir, io::Read}; 10 - use workspace::{Id, Workspace}; 15 + use task::ParsedLink; 16 + use workspace::{Id, Task, TaskIdentifier, Workspace}; 11 17 12 18 //use smol; 13 19 //use iocraft::prelude::*; 14 - use clap::{value_parser, Args, CommandFactory, Parser, Subcommand}; 20 + use clap::{Args, CommandFactory, Parser, Subcommand}; 15 21 use edit::edit as open_editor; 16 22 17 23 fn default_dir() -> PathBuf { 18 24 current_dir().unwrap() 19 25 } 20 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 + 21 31 #[derive(Parser)] 22 32 // TODO: add long_about 23 33 #[command(version, about)] 24 34 struct Cli { 35 + /// Override the tsk root directory. 25 36 #[arg(short = 'C', env = "TSK_ROOT", value_name = "DIR")] 26 37 dir: Option<PathBuf>, 27 38 // TODO: other global options ··· 50 61 #[command(flatten)] 51 62 title: Title, 52 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. 53 83 List { 54 84 /// Whether to list all tasks in the task stack. If specified, -c / count is ignored. 55 85 #[arg(short = 'a', default_value_t = false)] ··· 76 106 77 107 /// Use fuzzy finding with `fzf` to search for a task 78 108 Find { 79 - /// Include the contents of tasks in the search criteria. 80 - #[arg(short = 'b', default_value_t = false)] 81 - search_body: bool, 82 - /// Include archived tasks in the search criteria. Combine with `-b` to include archived 83 - /// bodies in the search criteria. 84 - #[arg(short = 'a', default_value_t = false)] 85 - 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 + }, 129 + 130 + /// Follow a link that is parsed from a task body. It may be an internal or external link (ie. 131 + /// a url or a wiki-style link using double square brackets). When using the `tsk show` 132 + /// command, links that are successfully parsed get a numeric superscript that may be used to 133 + /// address the link. That number should be supplied to the -l/link_index where it will be 134 + /// subsequently followed opened or shown. 135 + Follow { 136 + /// The task whose body will be searched for links. 137 + #[command(flatten)] 138 + task_id: TaskId, 139 + /// The index of the link to open. Must be supplied. 140 + #[arg(short = 'l', default_value_t = 1)] 141 + link_index: usize, 142 + /// When opening an internal link, whether to show or edit the addressed task. 143 + #[arg(short = 'e', default_value_t = false)] 144 + edit: bool, 86 145 }, 87 146 88 147 /// Drops the task on the top of the stack and archives it. 89 - Drop, 148 + Drop { 149 + /// The [TSK-]ID of the task to drop. 150 + #[command(flatten)] 151 + task_id: TaskId, 152 + }, 153 + 154 + /// Moves the 3rd item on the stack to the front of the stack, shifting everything else down by 155 + /// one. If there are less than 3 tasks on the stack, has no effect. 156 + Rot, 157 + /// Moves the task on the top of the stack back behind the 2nd element, shifting the next two 158 + /// task up. 159 + Tor, 160 + 161 + /// Prioritizes an arbitrary task to the top of the stack. 162 + Prioritize { 163 + /// The [TSK-]ID to prioritize. If it exists, it is moved to the top of the stack. 164 + #[command(flatten)] 165 + task_id: TaskId, 166 + }, 167 + 168 + /// Deprioritizes a task to the bottom of the stack. 169 + Deprioritize { 170 + /// The [TSK-]ID to deprioritize. If it exists, it is moved to the bottom of the stack. 171 + #[command(flatten)] 172 + task_id: TaskId, 173 + }, 90 174 } 91 175 92 176 #[derive(Args)] ··· 104 188 #[derive(Args)] 105 189 #[group(required = false, multiple = false)] 106 190 struct TaskId { 191 + /// The ID of the task to select as a plain integer. 107 192 #[arg(short = 't', value_name = "ID")] 108 193 id: Option<u32>, 109 194 110 - #[arg(short = 'T', value_name = "TSK-ID", value_parser = value_parser!(String))] 195 + /// The ID of the task to select with the 'tsk-' prefix. 196 + #[arg(short = 'T', value_name = "TSK-ID", value_parser = parse_id)] 111 197 tsk_id: Option<Id>, 198 + 199 + /// Selects a task relative to the top of the stack. 200 + /// If no option is specified, the task selected will be the top of the stack. 201 + #[arg(short = 'r', value_name = "RELATIVE", default_value_t = 0)] 202 + relative_id: u32, 203 + 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. 214 + #[arg(short = 'f', value_name = "FIND", default_value_t = false)] 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 + */ 232 + } 233 + 234 + impl From<TaskId> for TaskIdentifier { 235 + fn from(value: TaskId) -> Self { 236 + if let Some(id) = value.id.map(Id::from).or(value.tsk_id) { 237 + TaskIdentifier::Id(id) 238 + } else if value.find.find { 239 + TaskIdentifier::Find { 240 + exclude_body: value.find.args.exclude_body, 241 + archived: false, 242 + } 243 + } else { 244 + TaskIdentifier::Relative(value.relative_id) 245 + } 246 + } 112 247 } 113 248 114 249 fn main() { 115 250 let cli = Cli::parse(); 116 - match cli.command { 117 - Commands::Init => command_init(cli.dir.unwrap_or(default_dir())), 118 - Commands::Push { edit, body, title } => { 119 - command_push(cli.dir.unwrap_or(default_dir()), edit, body, title) 120 - } 121 - Commands::List { all, count } => command_list(cli.dir.unwrap_or(default_dir()), all, count), 122 - Commands::Swap => command_swap(cli.dir.unwrap_or(default_dir())), 123 - Commands::Edit { task_id } => command_edit(cli.dir.unwrap_or(default_dir()), task_id), 251 + let dir = cli.dir.unwrap_or(default_dir()); 252 + let var_name = match cli.command { 253 + Commands::Init => command_init(dir), 254 + Commands::Push { edit, body, title } => command_push(dir, edit, body, title), 255 + Commands::Append { edit, body, title } => command_append(dir, edit, body, title), 256 + Commands::List { all, count } => command_list(dir, all, count), 257 + Commands::Swap => command_swap(dir), 258 + Commands::Show { 259 + task_id, 260 + raw, 261 + show_attrs, 262 + } => command_show(dir, task_id, show_attrs, raw), 263 + Commands::Follow { 264 + task_id, 265 + link_index, 266 + edit, 267 + } => command_follow(dir, task_id, link_index, edit), 268 + Commands::Edit { task_id } => command_edit(dir, task_id), 124 269 Commands::Completion { shell } => command_completion(shell), 125 - Commands::Drop => command_drop(cli.dir.unwrap_or(default_dir())), 126 - Commands::Find { 127 - search_body, 128 - search_archived, 129 - } => command_search(cli.dir.unwrap_or(default_dir())), 270 + Commands::Drop { task_id } => command_drop(dir, task_id), 271 + Commands::Find { args, short_id } => command_find(dir, short_id, args), 272 + Commands::Rot => Workspace::from_path(dir).unwrap().rot(), 273 + Commands::Tor => Workspace::from_path(dir).unwrap().tor(), 274 + Commands::Prioritize { task_id } => command_prioritize(dir, task_id), 275 + Commands::Deprioritize { task_id } => command_deprioritize(dir, task_id), 276 + }; 277 + let result = var_name; 278 + match result { 279 + Ok(_) => exit(0), 280 + Err(e) => { 281 + eprintln!("{e}"); 282 + exit(2); 283 + } 284 + } 285 + } 286 + 287 + fn taskid_from_tsk_id(tsk_id: Id) -> TaskId { 288 + TaskId { 289 + tsk_id: Some(tsk_id), 290 + id: None, 291 + relative_id: 0, 292 + find: Find { 293 + find: false, 294 + args: FindArgs { exclude_body: true }, 295 + }, 130 296 } 131 297 } 132 298 133 - fn command_init(dir: PathBuf) { 134 - Workspace::init(dir).expect("Init failed") 299 + fn command_init(dir: PathBuf) -> Result<()> { 300 + Workspace::init(dir) 135 301 } 136 302 137 - fn command_push(dir: PathBuf, edit: bool, body: Option<String>, title: Title) { 138 - let workspace = Workspace::from_path(dir).expect("Unable to find .tsk dir"); 303 + fn create_task( 304 + workspace: &mut Workspace, 305 + edit: bool, 306 + body: Option<String>, 307 + title: Title, 308 + ) -> Result<Task> { 139 309 let mut title = if let Some(title) = title.title { 140 310 title 141 311 } else if let Some(title) = title.title_simple { 142 - let joined = title.join(" "); 143 - joined 312 + title.join(" ") 144 313 } else { 145 314 "".to_string() 146 315 }; 147 - 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 + }; 148 331 if body == "-" { 149 332 // add newline so you can type directly in the shell 150 - eprintln!(""); 333 + //eprintln!(""); 151 334 body.clear(); 152 - std::io::stdin() 153 - .read_to_string(&mut body) 154 - .expect("Failed to read stdin"); 335 + std::io::stdin().read_to_string(&mut body)?; 155 336 } 156 337 if edit { 157 - let new_content = open_editor(format!("{title}\n\n{body}")).expect("Failed to edit file"); 338 + let new_content = open_editor(format!("{title}\n\n{body}"))?; 158 339 if let Some(content) = new_content.split_once("\n") { 159 340 title = content.0.to_string(); 160 341 body = content.1.to_string(); 161 342 } 162 343 } 163 - let task = workspace 164 - .new_task(title, body) 165 - .expect("Failed to create task"); 166 - workspace 167 - .push_task(task) 168 - .expect("Failed to push task to stack"); 344 + // Ensure title never contains newlines (invariant for index file format) 345 + title = title.replace(['\n', '\r'], " "); 346 + let task = workspace.new_task(title, body)?; 347 + workspace.handle_metadata(&task, None)?; 348 + Ok(task) 349 + } 350 + 351 + fn command_push(dir: PathBuf, edit: bool, body: Option<String>, title: Title) -> Result<()> { 352 + let mut workspace = Workspace::from_path(dir)?; 353 + let task = create_task(&mut workspace, edit, body, title)?; 354 + workspace.push_task(task) 355 + } 356 + 357 + fn command_append(dir: PathBuf, edit: bool, body: Option<String>, title: Title) -> Result<()> { 358 + let mut workspace = Workspace::from_path(dir)?; 359 + let task = create_task(&mut workspace, edit, body, title)?; 360 + workspace.append_task(task) 169 361 } 170 362 171 - fn command_list(dir: PathBuf, all: bool, count: usize) { 172 - let workspace = Workspace::from_path(dir).expect("Unable to find .tsk dir"); 173 - let stack = if all { 174 - workspace.read_stack().expect("Failed to read index") 175 - } else { 176 - workspace.read_stack().expect("Failed to read index") 177 - }; 363 + fn command_list(dir: PathBuf, all: bool, count: usize) -> Result<()> { 364 + let workspace = Workspace::from_path(dir)?; 365 + let stack = workspace.read_stack()?; 366 + 178 367 if stack.empty() { 179 368 println!("*No tasks*"); 180 - } else { 181 - if !all { 182 - for stack_item in stack.into_iter().take(count) { 183 - println!("{stack_item}"); 184 - } 369 + exit(0); 370 + } 371 + 372 + for (_, stack_item) in stack 373 + .into_iter() 374 + .enumerate() 375 + .take_while(|(idx, _)| all || idx < &count) 376 + { 377 + if let Some(parsed) = task::parse(&stack_item.title) { 378 + println!("{}\t{}", stack_item.id, parsed.content.trim()); 185 379 } else { 186 - for stack_item in stack.into_iter() { 187 - println!("{stack_item}"); 188 - } 380 + println!("{stack_item}"); 189 381 } 190 382 } 383 + Ok(()) 191 384 } 192 385 193 - fn command_swap(dir: PathBuf) { 194 - let workspace = Workspace::from_path(dir).expect("Unable to find .tsk dir"); 195 - workspace.swap_top().expect("swap to work"); 386 + fn command_swap(dir: PathBuf) -> Result<()> { 387 + let workspace = Workspace::from_path(dir)?; 388 + workspace.swap_top()?; 389 + Ok(()) 196 390 } 197 391 198 - fn command_edit(dir: PathBuf, id: TaskId) { 199 - let workspace = Workspace::from_path(dir).expect("Unable to find .tsk dir"); 200 - let tsk_id: Option<Id> = id.id.map(Id::from).or(id.tsk_id); 201 - let mut task = if let Some(id) = tsk_id { 202 - workspace.task(id.into()).expect("To read task from disk") 203 - } else { 204 - let mut stack = workspace.read_stack().expect("to read stack"); 205 - let stack_item = stack.pop().expect("No tasks on stack."); 206 - workspace.task(stack_item.id).expect("couldn't read task") 207 - }; 208 - let new_content = open_editor(format!("{}\n\n{}", task.title.trim(), task.body.trim())) 209 - .expect("Failed to edit file"); 392 + fn command_edit(dir: PathBuf, id: TaskId) -> Result<()> { 393 + let workspace = Workspace::from_path(dir)?; 394 + let id: TaskIdentifier = id.into(); 395 + let mut task = workspace.task(id)?; 396 + let pre_links = task::parse(&task.to_string()).map(|pt| pt.intenal_links()); 397 + let new_content = open_editor(format!("{}\n\n{}", task.title.trim(), task.body.trim()))?; 210 398 if let Some((title, body)) = new_content.split_once("\n") { 211 - task.title = title.to_string(); 399 + // Ensure title never contains newlines (invariant for index file format) 400 + task.title = title.replace(['\n', '\r'], " "); 212 401 task.body = body.to_string(); 213 - task.save().expect("Failed to save task"); 402 + workspace.handle_metadata(&task, pre_links)?; 403 + task.save()?; 214 404 } 405 + Ok(()) 215 406 } 216 407 217 - fn command_completion(shell: Shell) { 218 - generate(shell, &mut Cli::command(), "tsk", &mut io::stdout()) 408 + fn command_completion(shell: Shell) -> Result<()> { 409 + generate(shell, &mut Cli::command(), "tsk", &mut io::stdout()); 410 + Ok(()) 219 411 } 220 412 221 - fn command_drop(dir: PathBuf) { 222 - if let Some(id) = Workspace::from_path(dir) 223 - .expect("Unable to find .tsk dir") 224 - .drop() 225 - .expect("Unable to drop task.") { 226 - println!("Dropped {id}") 413 + fn command_drop(dir: PathBuf, task_id: TaskId) -> Result<()> { 414 + if let Some(id) = Workspace::from_path(dir)?.drop(task_id.into())? { 415 + eprint!("Dropped "); 416 + println!("{id}"); 417 + } else { 418 + eprintln!("No task to drop."); 419 + exit(1); 227 420 } 421 + Ok(()) 228 422 } 229 423 230 - fn command_search(dir: PathBuf) { 231 - let id = Workspace::from_path(dir).unwrap().search().unwrap(); 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)?; 232 426 if let Some(id) = id { 233 - eprint!("Dropping "); 234 - println!("{id}"); 427 + if short_id { 428 + // print as integer 429 + println!("{}", id.0); 430 + } else { 431 + println!("{id}"); 432 + } 235 433 } else { 236 - eprintln!("No task to drop.") 434 + eprintln!("No task selected."); 435 + exit(1); 436 + } 437 + Ok(()) 438 + } 439 + 440 + fn command_prioritize(dir: PathBuf, task_id: TaskId) -> Result<()> { 441 + Workspace::from_path(dir)?.prioritize(task_id.into()) 442 + } 443 + 444 + fn command_deprioritize(dir: PathBuf, task_id: TaskId) -> Result<()> { 445 + Workspace::from_path(dir)?.deprioritize(task_id.into()) 446 + } 447 + 448 + fn command_show(dir: PathBuf, task_id: TaskId, show_attrs: bool, raw: bool) -> Result<()> { 449 + let task = Workspace::from_path(dir)?.task(task_id.into())?; 450 + // YAML front-matter style. YAML is gross, but it's what everyone uses! 451 + if show_attrs && !task.attributes.is_empty() { 452 + println!("---"); 453 + for (attr, value) in task.attributes.iter() { 454 + println!("{attr}: \"{value}\""); 455 + } 456 + println!("---"); 457 + } 458 + match task::parse(&task.to_string()) { 459 + Some(styled_task) if !raw => { 460 + writeln!(io::stdout(), "{}", styled_task.content)?; 461 + } 462 + _ => { 463 + println!("{task}"); 464 + } 465 + } 466 + Ok(()) 467 + } 468 + 469 + fn command_follow(dir: PathBuf, task_id: TaskId, link_index: usize, edit: bool) -> Result<()> { 470 + let task = Workspace::from_path(dir.clone())?.task(task_id.into())?; 471 + if let Some(parsed_task) = task::parse(&task.to_string()) { 472 + if link_index == 0 || link_index > parsed_task.links.len() { 473 + eprintln!("Link index out of bounds."); 474 + exit(1); 475 + } 476 + let link = &parsed_task.links[link_index - 1]; 477 + match link { 478 + ParsedLink::External(url) => { 479 + open::that_detached(url.as_str())?; 480 + Ok(()) 481 + } 482 + ParsedLink::Internal(id) => { 483 + let taskid = taskid_from_tsk_id(*id); 484 + if edit { 485 + command_edit(dir, taskid) 486 + } else { 487 + command_show(dir, taskid, false, false) 488 + } 489 + } 490 + } 491 + } else { 492 + eprintln!("Unable to parse any links from body."); 493 + exit(1); 237 494 } 238 495 }
+35 -15
src/stack.rs
··· 5 5 use crate::errors::{Error, Result}; 6 6 use crate::util; 7 7 use std::collections::VecDeque; 8 + use std::collections::vec_deque::Iter; 8 9 use std::fmt::Display; 10 + use std::fs::File; 9 11 use std::io::{self, BufRead, BufReader, Seek, Write}; 12 + use std::path::Path; 10 13 use std::str::FromStr; 11 14 use std::time::{Duration, SystemTime, UNIX_EPOCH}; 12 - use std::{fs::File, path::PathBuf}; 13 15 14 16 use nix::fcntl::{Flock, FlockArg}; 15 17 ··· 66 68 let mut parts = s.trim().split("\t"); 67 69 let id: Id = parts 68 70 .next() 69 - .ok_or(Error::Parse(format!( 70 - "Incomplete index line. Missing tsk ID" 71 - )))? 71 + .ok_or(Error::Parse( 72 + "Incomplete index line. Missing tsk ID".to_owned(), 73 + ))? 72 74 .parse()?; 73 75 let title: String = parts 74 76 .next() 75 - .ok_or(Error::Parse(format!( 76 - "Incomplete index line. Missing title." 77 - )))? 77 + .ok_or(Error::Parse( 78 + "Incomplete index line. Missing title.".to_owned(), 79 + ))? 78 80 .trim() 79 81 .to_string(); 80 82 // parse the timestamp as an integer ··· 95 97 96 98 impl StackItem { 97 99 /// Parses a [`StackItem`] from a string. The expected format is a tab-delimited line with the 98 - /// files: task id title 99 - 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> { 100 102 let mut stack_item: StackItem = line.parse()?; 101 103 102 104 let task = util::flopen( 103 105 workspace_path 104 106 .join(TASKSFOLDER) 105 - .join(stack_item.id.to_filename()), 107 + .join(stack_item.id.filename()), 106 108 FlockArg::LockExclusive, 107 109 )?; 108 110 let task_modify_time = task.metadata()?.modified()?; ··· 124 126 } 125 127 126 128 impl TaskStack { 127 - pub fn from_tskdir(workspace_path: &PathBuf) -> Result<Self> { 129 + pub fn from_tskdir(workspace_path: &Path) -> Result<Self> { 128 130 let file = util::flopen(workspace_path.join(INDEXFILE), FlockArg::LockExclusive)?; 129 131 let index = BufReader::new(&*file).lines(); 130 132 let mut all = VecDeque::new(); ··· 141 143 self.file.seek(std::io::SeekFrom::Start(0))?; 142 144 self.file.set_len(0)?; 143 145 for item in self.all.iter() { 144 - 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())?; 145 149 } 146 150 Ok(()) 147 151 } 148 152 149 153 pub fn push(&mut self, item: StackItem) { 150 154 self.all.push_front(item); 155 + } 156 + 157 + pub fn push_back(&mut self, item: StackItem) { 158 + self.all.push_back(item); 151 159 } 152 160 153 161 pub fn pop(&mut self) -> Option<StackItem> { ··· 157 165 pub fn swap(&mut self) { 158 166 let tip = self.all.pop_front(); 159 167 let second = self.all.pop_front(); 160 - if tip.is_some() && second.is_some() { 161 - self.all.push_front(tip.unwrap()); 162 - 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); 163 171 } 164 172 } 165 173 166 174 pub fn empty(&self) -> bool { 167 175 self.all.is_empty() 176 + } 177 + 178 + pub fn remove(&mut self, index: usize) -> Option<StackItem> { 179 + self.all.remove(index) 180 + } 181 + 182 + pub fn iter(&self) -> Iter<'_, StackItem> { 183 + self.all.iter() 184 + } 185 + 186 + pub fn get(&self, index: usize) -> Option<&StackItem> { 187 + self.all.get(index) 168 188 } 169 189 } 170 190
+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
+359 -29
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)] 18 - pub struct Id(u32); 27 + #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] 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 } 48 61 62 + pub enum TaskIdentifier { 63 + Id(Id), 64 + Relative(u32), 65 + Find { exclude_body: bool, archived: bool }, 66 + } 67 + 68 + impl From<Id> for TaskIdentifier { 69 + fn from(value: Id) -> Self { 70 + TaskIdentifier::Id(value) 71 + } 72 + } 73 + 49 74 pub struct Workspace { 50 75 /// The path to the workspace root, excluding the .tsk directory. This should *contain* the 51 76 /// .tsk directory. ··· 54 79 55 80 impl Workspace { 56 81 pub fn init(path: PathBuf) -> Result<()> { 82 + // TODO: detect if in a git repo and add .tsk/ to `.git/info/exclude` 57 83 let tsk_dir = path.join(".tsk"); 58 84 if tsk_dir.exists() { 59 85 return Err(Error::AlreadyInitialized); 60 86 } 61 87 std::fs::create_dir(&tsk_dir)?; 62 88 // Create the tasks directory 63 - std::fs::create_dir(&tsk_dir.join("tasks"))?; 89 + std::fs::create_dir(tsk_dir.join("tasks"))?; 64 90 // Create the archive directory 65 - std::fs::create_dir(&tsk_dir.join("archive"))?; 91 + std::fs::create_dir(tsk_dir.join("archive"))?; 66 92 let mut next = OpenOptions::new() 67 93 .read(true) 68 94 .write(true) 69 95 .create(true) 96 + .truncate(true) 70 97 .open(tsk_dir.join("next"))?; 98 + // initialize the next file with ID 1 71 99 next.write_all(b"1\n")?; 72 100 Ok(()) 73 101 } ··· 77 105 Ok(Self { path: tsk_dir }) 78 106 } 79 107 108 + fn resolve(&self, identifier: TaskIdentifier) -> Result<Id> { 109 + match identifier { 110 + TaskIdentifier::Id(id) => Ok(id), 111 + TaskIdentifier::Relative(r) => { 112 + let stack = self.read_stack()?; 113 + let stack_item = stack.get(r as usize).ok_or(Error::NoTasks)?; 114 + Ok(stack_item.id) 115 + } 116 + TaskIdentifier::Find { 117 + exclude_body, 118 + archived, 119 + } => self 120 + .search(None, !exclude_body, archived)? 121 + .ok_or(Error::NotSelected), 122 + } 123 + } 124 + 125 + /// Increments the `next` counter and returns the previous value. 80 126 pub fn next_id(&self) -> Result<Id> { 81 127 let mut file = util::flopen(self.path.join("next"), FlockArg::LockExclusive)?; 82 128 let mut buf = String::new(); ··· 94 140 // WARN: we could improperly increment the id if the task is not written to disk/errors. 95 141 // But who cares 96 142 let id = self.next_id()?; 97 - 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); 98 146 let mut file = util::flopen(task_path.clone(), FlockArg::LockExclusive)?; 99 147 file.write_all(format!("{title}\n\n{body}").as_bytes())?; 100 - // create a hardlink to the archive dir 101 - fs::hard_link( 102 - task_path, 103 - 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), 104 152 )?; 105 153 Ok(Task { 106 154 id, 107 155 title, 108 156 body, 109 157 file, 158 + attributes: Default::default(), 110 159 }) 111 160 } 112 161 113 - pub fn task(&self, id: Id) -> Result<Task> { 162 + pub fn task(&self, identifier: TaskIdentifier) -> Result<Task> { 163 + let id = self.resolve(identifier)?; 164 + 114 165 let file = util::flopen( 115 166 self.path.join("tasks").join(format!("tsk-{}.tsk", id.0)), 116 167 FlockArg::LockExclusive, ··· 121 172 reader.read_line(&mut title)?; 122 173 reader.read_to_string(&mut body)?; 123 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 + } 124 183 Ok(Task { 125 184 id, 126 - title, 127 - body, 128 185 file, 186 + title: title.trim().to_string(), 187 + body: body.trim().to_string(), 188 + attributes: Attrs::from_written(read_attributes), 129 189 }) 130 190 } 131 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 + 132 258 pub fn read_stack(&self) -> Result<TaskStack> { 133 259 TaskStack::from_tskdir(&self.path) 134 260 } ··· 140 266 Ok(()) 141 267 } 142 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 + 143 276 pub fn swap_top(&self) -> Result<()> { 144 277 let mut stack = TaskStack::from_tskdir(&self.path)?; 145 278 stack.swap(); ··· 147 280 Ok(()) 148 281 } 149 282 150 - pub fn drop(&self) -> Result<Option<Id>> { 283 + pub fn rot(&self) -> Result<()> { 284 + let mut stack = TaskStack::from_tskdir(&self.path)?; 285 + let top = stack.pop(); 286 + let second = stack.pop(); 287 + let third = stack.pop(); 288 + 289 + if top.is_none() || second.is_none() || third.is_none() { 290 + return Ok(()); 291 + } 292 + 293 + // unwrap is ok here because we checked above 294 + stack.push(second.unwrap()); 295 + stack.push(top.unwrap()); 296 + stack.push(third.unwrap()); 297 + stack.save()?; 298 + Ok(()) 299 + } 300 + 301 + /// The inverse of tor. Pushes the top item behind the second item, shifting #2 and #3 to #1 302 + /// and #2 respectively. 303 + pub fn tor(&self) -> Result<()> { 304 + let mut stack = TaskStack::from_tskdir(&self.path)?; 305 + let top = stack.pop(); 306 + let second = stack.pop(); 307 + let third = stack.pop(); 308 + 309 + if top.is_none() || second.is_none() || third.is_none() { 310 + return Ok(()); 311 + } 312 + 313 + stack.push(top.unwrap()); 314 + stack.push(third.unwrap()); 315 + stack.push(second.unwrap()); 316 + stack.save()?; 317 + Ok(()) 318 + } 319 + 320 + pub fn drop(&self, identifier: TaskIdentifier) -> Result<Option<Id>> { 321 + let id = self.resolve(identifier)?; 151 322 let mut stack = self.read_stack()?; 152 - if let Some(stack_item) = stack.pop() { 153 - let task_path = self 154 - .path 155 - .join("tasks") 156 - .join(format!("{}.tsk", stack_item.id)); 157 - 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); 158 327 stack.save()?; 159 - Ok(Some(stack_item.id)) 328 + prioritized_task.map(|t| t.id) 329 + } else { 330 + None 331 + }; 332 + remove_file(self.path.join("tasks").join(format!("{id}.tsk")))?; 333 + Ok(task) 334 + } 335 + 336 + pub fn search( 337 + &self, 338 + stack: Option<TaskStack>, 339 + search_body: bool, 340 + _include_archived: bool, 341 + ) -> Result<Option<Id>> { 342 + let stack = if let Some(stack) = stack { 343 + stack 344 + } else { 345 + self.read_stack()? 346 + }; 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 + )?) 160 366 } else { 161 - Ok(None) 367 + // just search the stack 368 + Ok(fzf::select::<_, Id, _>( 369 + stack, 370 + ["--delimiter=\t", "--accept-nth=1"], 371 + )?) 162 372 } 163 373 } 164 374 165 - pub fn search(&self) -> Result<Option<Id>> { 166 - let stack = self.read_stack()?; 167 - Ok(fzf::select(stack)?.map(|si| si.id)) 375 + pub fn prioritize(&self, identifier: TaskIdentifier) -> Result<()> { 376 + let id = self.resolve(identifier)?; 377 + let mut stack = self.read_stack()?; 378 + let index = &stack.iter().map(|i| i.id).position(|i| i == id); 379 + if let Some(index) = index { 380 + let prioritized_task = stack.remove(*index); 381 + // unwrap here is safe because we just searched for the index and know it exists 382 + stack.push(prioritized_task.unwrap()); 383 + stack.save()?; 384 + } 385 + Ok(()) 386 + } 387 + 388 + pub fn deprioritize(&self, identifier: TaskIdentifier) -> Result<()> { 389 + let id = self.resolve(identifier)?; 390 + let mut stack = self.read_stack()?; 391 + let index = &stack.iter().map(|i| i.id).position(|i| i == id); 392 + if let Some(index) = index { 393 + let deprioritized_task = stack.remove(*index); 394 + // unwrap here is safe because we just searched for the index and know it exists 395 + stack.push_back(deprioritized_task.unwrap()); 396 + stack.save()?; 397 + } 398 + Ok(()) 168 399 } 169 400 } 170 401 ··· 173 404 pub title: String, 174 405 pub body: String, 175 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 + } 176 414 } 177 415 178 416 impl Task { ··· 184 422 self.file.write_all(b"\n\n")?; 185 423 self.file.write_all(self.body.trim().as_bytes())?; 186 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()); 187 517 } 188 518 }