A file-based task manager

Compare changes

Choose any two refs to compare.

+810 -451
+1
.envrc
··· 1 + use flake
+2
.gitignore
··· 1 1 /target 2 + index.html 3 + flake.lock
+1 -1
.tsk/archive/tsk-14.tsk
··· 7 7 8 8 here's [a link](https://ngp.computer). 9 9 10 - and an internal link: [[tsk-11]] 10 + and an internal link: [[tsk-11]]. This should add a backlink 11 11 12 12 and some _underlined text_ 13 13
+3 -1
.tsk/archive/tsk-21.tsk
··· 2 2 3 3 Will want to prompt to add `.tsk` to the `.git/info/exclude` file (or 4 4 .gitignore/globally) and *probably* set up 5 - [metastore](https://github.com/przemoc/metastore). 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 +
+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
+3 -1
.tsk/archive/tsk-6.tsk
··· 1 1 automatically add backlinks 2 2 3 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 4 + corresponding task exists, update the targetted task with a backlink reference 5 + 6 + Using [[tsk-11]] as my test.
+12 -14
.tsk/index
··· 1 - tsk-21 Add command to setup git stuff 2 - tsk-20 fix issue where links use absolute paths 3 - tsk-19 add "raw" output option for show 4 - tsk-8 IMAP4-based sync 5 - tsk-17 Add reopen command 6 - tsk-16 Add ability to search archived tasks with find command 7 - tsk-15 Add link identification to tasks 8 - tsk-14 parse internal links from body 9 - tsk-6 automatically add backlinks 10 - tsk-9 fix timestamp storage and parsing 11 - tsk-10 foreign workspaces 12 - tsk-7 allow for creating tasks that don't go to top of stack 13 - tsk-13 user-defined labels 14 - tsk-18 Add reindex command 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 -1
.tsk/next
··· 1 - 22 1 + 32
+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
+276 -243
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.18" 7 + version = "0.6.20" 8 8 source = "registry+https://github.com/rust-lang/crates.io-index" 9 - checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 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.10" 22 + version = "1.0.11" 23 23 source = "registry+https://github.com/rust-lang/crates.io-index" 24 - checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 24 + checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 25 25 26 26 [[package]] 27 27 name = "anstyle-parse" 28 - version = "0.2.6" 28 + version = "0.2.7" 29 29 source = "registry+https://github.com/rust-lang/crates.io-index" 30 - checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 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.2" 37 + version = "1.1.4" 38 38 source = "registry+https://github.com/rust-lang/crates.io-index" 39 - checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 39 + checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 40 40 dependencies = [ 41 - "windows-sys 0.59.0", 41 + "windows-sys 0.60.2", 42 42 ] 43 43 44 44 [[package]] 45 45 name = "anstyle-wincon" 46 - version = "3.0.6" 46 + version = "3.0.10" 47 47 source = "registry+https://github.com/rust-lang/crates.io-index" 48 - checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 48 + checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 49 49 dependencies = [ 50 50 "anstyle", 51 - "windows-sys 0.59.0", 51 + "once_cell_polyfill", 52 + "windows-sys 0.60.2", 52 53 ] 53 54 54 55 [[package]] 55 56 name = "bitflags" 56 - version = "2.6.0" 57 + version = "2.9.4" 57 58 source = "registry+https://github.com/rust-lang/crates.io-index" 58 - checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 59 + checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" 59 60 60 61 [[package]] 61 62 name = "cfg-if" 62 - version = "1.0.0" 63 + version = "1.0.3" 63 64 source = "registry+https://github.com/rust-lang/crates.io-index" 64 - checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 65 + checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 65 66 66 67 [[package]] 67 68 name = "cfg_aliases" ··· 71 72 72 73 [[package]] 73 74 name = "clap" 74 - version = "4.5.21" 75 + version = "4.5.47" 75 76 source = "registry+https://github.com/rust-lang/crates.io-index" 76 - checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" 77 + checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" 77 78 dependencies = [ 78 79 "clap_builder", 79 80 "clap_derive", ··· 81 82 82 83 [[package]] 83 84 name = "clap_builder" 84 - version = "4.5.21" 85 + version = "4.5.47" 85 86 source = "registry+https://github.com/rust-lang/crates.io-index" 86 - checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" 87 + checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" 87 88 dependencies = [ 88 89 "anstream", 89 90 "anstyle", ··· 93 94 94 95 [[package]] 95 96 name = "clap_complete" 96 - version = "4.5.38" 97 + version = "4.5.57" 97 98 source = "registry+https://github.com/rust-lang/crates.io-index" 98 - checksum = "d9647a559c112175f17cf724dc72d3645680a883c58481332779192b0d8e7a01" 99 + checksum = "4d9501bd3f5f09f7bbee01da9a511073ed30a80cd7a509f1214bb74eadea71ad" 99 100 dependencies = [ 100 101 "clap", 101 102 ] 102 103 103 104 [[package]] 104 105 name = "clap_derive" 105 - version = "4.5.18" 106 + version = "4.5.47" 106 107 source = "registry+https://github.com/rust-lang/crates.io-index" 107 - checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 108 + checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" 108 109 dependencies = [ 109 110 "heck", 110 111 "proc-macro2", ··· 114 115 115 116 [[package]] 116 117 name = "clap_lex" 117 - version = "0.7.3" 118 + version = "0.7.5" 118 119 source = "registry+https://github.com/rust-lang/crates.io-index" 119 - checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" 120 + checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 120 121 121 122 [[package]] 122 123 name = "clap_mangen" 123 - version = "0.2.24" 124 + version = "0.2.29" 124 125 source = "registry+https://github.com/rust-lang/crates.io-index" 125 - checksum = "fbae9cbfdc5d4fa8711c09bd7b83f644cb48281ac35bf97af3e47b0675864bdf" 126 + checksum = "27b4c3c54b30f0d9adcb47f25f61fcce35c4dd8916638c6b82fbd5f4fb4179e2" 126 127 dependencies = [ 127 128 "clap", 128 129 "roff", ··· 130 131 131 132 [[package]] 132 133 name = "colorchoice" 133 - version = "1.0.3" 134 + version = "1.0.4" 134 135 source = "registry+https://github.com/rust-lang/crates.io-index" 135 - checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 136 + checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 136 137 137 138 [[package]] 138 139 name = "colored" 139 - version = "2.1.0" 140 + version = "3.0.0" 140 141 source = "registry+https://github.com/rust-lang/crates.io-index" 141 - checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" 142 + checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" 142 143 dependencies = [ 143 - "lazy_static", 144 - "windows-sys 0.48.0", 144 + "windows-sys 0.59.0", 145 145 ] 146 146 147 147 [[package]] ··· 167 167 168 168 [[package]] 169 169 name = "either" 170 - version = "1.13.0" 170 + version = "1.15.0" 171 171 source = "registry+https://github.com/rust-lang/crates.io-index" 172 - checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 172 + checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 173 173 174 174 [[package]] 175 175 name = "errno" 176 - version = "0.3.9" 176 + version = "0.3.13" 177 177 source = "registry+https://github.com/rust-lang/crates.io-index" 178 - checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 178 + checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 179 179 dependencies = [ 180 180 "libc", 181 - "windows-sys 0.52.0", 181 + "windows-sys 0.60.2", 182 182 ] 183 183 184 184 [[package]] 185 185 name = "fastrand" 186 - version = "2.2.0" 186 + version = "2.3.0" 187 187 source = "registry+https://github.com/rust-lang/crates.io-index" 188 - checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" 188 + checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 189 189 190 190 [[package]] 191 191 name = "form_urlencoded" 192 - version = "1.2.1" 192 + version = "1.2.2" 193 193 source = "registry+https://github.com/rust-lang/crates.io-index" 194 - checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 194 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 195 195 dependencies = [ 196 196 "percent-encoding", 197 197 ] 198 198 199 199 [[package]] 200 + name = "getrandom" 201 + version = "0.3.3" 202 + source = "registry+https://github.com/rust-lang/crates.io-index" 203 + checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 204 + dependencies = [ 205 + "cfg-if", 206 + "libc", 207 + "r-efi", 208 + "wasi", 209 + ] 210 + 211 + [[package]] 200 212 name = "heck" 201 213 version = "0.5.0" 202 214 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 204 216 205 217 [[package]] 206 218 name = "home" 207 - version = "0.5.9" 219 + version = "0.5.11" 208 220 source = "registry+https://github.com/rust-lang/crates.io-index" 209 - checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 221 + checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 210 222 dependencies = [ 211 - "windows-sys 0.52.0", 223 + "windows-sys 0.59.0", 212 224 ] 213 225 214 226 [[package]] 215 227 name = "icu_collections" 216 - version = "1.5.0" 228 + version = "2.0.0" 217 229 source = "registry+https://github.com/rust-lang/crates.io-index" 218 - checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 230 + checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 219 231 dependencies = [ 220 232 "displaydoc", 233 + "potential_utf", 221 234 "yoke", 222 235 "zerofrom", 223 236 "zerovec", 224 237 ] 225 238 226 239 [[package]] 227 - name = "icu_locid" 228 - version = "1.5.0" 240 + name = "icu_locale_core" 241 + version = "2.0.0" 229 242 source = "registry+https://github.com/rust-lang/crates.io-index" 230 - checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 243 + checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 231 244 dependencies = [ 232 245 "displaydoc", 233 246 "litemap", ··· 237 250 ] 238 251 239 252 [[package]] 240 - name = "icu_locid_transform" 241 - version = "1.5.0" 242 - source = "registry+https://github.com/rust-lang/crates.io-index" 243 - checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 244 - dependencies = [ 245 - "displaydoc", 246 - "icu_locid", 247 - "icu_locid_transform_data", 248 - "icu_provider", 249 - "tinystr", 250 - "zerovec", 251 - ] 252 - 253 - [[package]] 254 - name = "icu_locid_transform_data" 255 - version = "1.5.0" 256 - source = "registry+https://github.com/rust-lang/crates.io-index" 257 - checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 258 - 259 - [[package]] 260 253 name = "icu_normalizer" 261 - version = "1.5.0" 254 + version = "2.0.0" 262 255 source = "registry+https://github.com/rust-lang/crates.io-index" 263 - checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 256 + checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 264 257 dependencies = [ 265 258 "displaydoc", 266 259 "icu_collections", ··· 268 261 "icu_properties", 269 262 "icu_provider", 270 263 "smallvec", 271 - "utf16_iter", 272 - "utf8_iter", 273 - "write16", 274 264 "zerovec", 275 265 ] 276 266 277 267 [[package]] 278 268 name = "icu_normalizer_data" 279 - version = "1.5.0" 269 + version = "2.0.0" 280 270 source = "registry+https://github.com/rust-lang/crates.io-index" 281 - checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 271 + checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 282 272 283 273 [[package]] 284 274 name = "icu_properties" 285 - version = "1.5.1" 275 + version = "2.0.1" 286 276 source = "registry+https://github.com/rust-lang/crates.io-index" 287 - checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 277 + checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 288 278 dependencies = [ 289 279 "displaydoc", 290 280 "icu_collections", 291 - "icu_locid_transform", 281 + "icu_locale_core", 292 282 "icu_properties_data", 293 283 "icu_provider", 294 - "tinystr", 284 + "potential_utf", 285 + "zerotrie", 295 286 "zerovec", 296 287 ] 297 288 298 289 [[package]] 299 290 name = "icu_properties_data" 300 - version = "1.5.0" 291 + version = "2.0.1" 301 292 source = "registry+https://github.com/rust-lang/crates.io-index" 302 - checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 293 + checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 303 294 304 295 [[package]] 305 296 name = "icu_provider" 306 - version = "1.5.0" 297 + version = "2.0.0" 307 298 source = "registry+https://github.com/rust-lang/crates.io-index" 308 - checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 299 + checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 309 300 dependencies = [ 310 301 "displaydoc", 311 - "icu_locid", 312 - "icu_provider_macros", 302 + "icu_locale_core", 313 303 "stable_deref_trait", 314 304 "tinystr", 315 305 "writeable", 316 306 "yoke", 317 307 "zerofrom", 308 + "zerotrie", 318 309 "zerovec", 319 310 ] 320 311 321 312 [[package]] 322 - name = "icu_provider_macros" 323 - version = "1.5.0" 324 - source = "registry+https://github.com/rust-lang/crates.io-index" 325 - checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 326 - dependencies = [ 327 - "proc-macro2", 328 - "quote", 329 - "syn", 330 - ] 331 - 332 - [[package]] 333 313 name = "idna" 334 - version = "1.0.3" 314 + version = "1.1.0" 335 315 source = "registry+https://github.com/rust-lang/crates.io-index" 336 - checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 316 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 337 317 dependencies = [ 338 318 "idna_adapter", 339 319 "smallvec", ··· 342 322 343 323 [[package]] 344 324 name = "idna_adapter" 345 - version = "1.2.0" 325 + version = "1.2.1" 346 326 source = "registry+https://github.com/rust-lang/crates.io-index" 347 - checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 327 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 348 328 dependencies = [ 349 329 "icu_normalizer", 350 330 "icu_properties", ··· 376 356 checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 377 357 378 358 [[package]] 379 - name = "lazy_static" 380 - version = "1.5.0" 359 + name = "itertools" 360 + version = "0.14.0" 381 361 source = "registry+https://github.com/rust-lang/crates.io-index" 382 - checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 362 + checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 363 + dependencies = [ 364 + "either", 365 + ] 383 366 384 367 [[package]] 385 368 name = "libc" 386 - version = "0.2.164" 369 + version = "0.2.175" 370 + source = "registry+https://github.com/rust-lang/crates.io-index" 371 + checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" 372 + 373 + [[package]] 374 + name = "linux-raw-sys" 375 + version = "0.4.15" 387 376 source = "registry+https://github.com/rust-lang/crates.io-index" 388 - checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" 377 + checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 389 378 390 379 [[package]] 391 380 name = "linux-raw-sys" 392 - version = "0.4.14" 381 + version = "0.9.4" 393 382 source = "registry+https://github.com/rust-lang/crates.io-index" 394 - checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 383 + checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 395 384 396 385 [[package]] 397 386 name = "litemap" 398 - version = "0.7.4" 387 + version = "0.8.0" 399 388 source = "registry+https://github.com/rust-lang/crates.io-index" 400 - checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" 389 + checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 401 390 402 391 [[package]] 403 392 name = "nix" 404 - version = "0.29.0" 393 + version = "0.30.1" 405 394 source = "registry+https://github.com/rust-lang/crates.io-index" 406 - checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 395 + checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 407 396 dependencies = [ 408 397 "bitflags", 409 398 "cfg-if", ··· 413 402 414 403 [[package]] 415 404 name = "once_cell" 416 - version = "1.20.2" 405 + version = "1.21.3" 417 406 source = "registry+https://github.com/rust-lang/crates.io-index" 418 - checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 407 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 408 + 409 + [[package]] 410 + name = "once_cell_polyfill" 411 + version = "1.70.1" 412 + source = "registry+https://github.com/rust-lang/crates.io-index" 413 + checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 419 414 420 415 [[package]] 421 416 name = "open" 422 - version = "5.3.1" 417 + version = "5.3.2" 423 418 source = "registry+https://github.com/rust-lang/crates.io-index" 424 - checksum = "3ecd52f0b8d15c40ce4820aa251ed5de032e5d91fab27f7db2f40d42a8bdf69c" 419 + checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" 425 420 dependencies = [ 426 421 "is-wsl", 427 422 "libc", ··· 430 425 431 426 [[package]] 432 427 name = "pathdiff" 433 - version = "0.2.2" 428 + version = "0.2.3" 434 429 source = "registry+https://github.com/rust-lang/crates.io-index" 435 - checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361" 430 + checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" 436 431 437 432 [[package]] 438 433 name = "percent-encoding" 439 - version = "2.3.1" 434 + version = "2.3.2" 440 435 source = "registry+https://github.com/rust-lang/crates.io-index" 441 - checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 436 + checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 437 + 438 + [[package]] 439 + name = "potential_utf" 440 + version = "0.1.3" 441 + source = "registry+https://github.com/rust-lang/crates.io-index" 442 + checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" 443 + dependencies = [ 444 + "zerovec", 445 + ] 442 446 443 447 [[package]] 444 448 name = "proc-macro2" 445 - version = "1.0.92" 449 + version = "1.0.101" 446 450 source = "registry+https://github.com/rust-lang/crates.io-index" 447 - checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 451 + checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 448 452 dependencies = [ 449 453 "unicode-ident", 450 454 ] 451 455 452 456 [[package]] 453 457 name = "quote" 454 - version = "1.0.37" 458 + version = "1.0.40" 455 459 source = "registry+https://github.com/rust-lang/crates.io-index" 456 - checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 460 + checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 457 461 dependencies = [ 458 462 "proc-macro2", 459 463 ] 460 464 461 465 [[package]] 466 + name = "r-efi" 467 + version = "5.3.0" 468 + source = "registry+https://github.com/rust-lang/crates.io-index" 469 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 470 + 471 + [[package]] 462 472 name = "roff" 463 473 version = "0.2.2" 464 474 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 466 476 467 477 [[package]] 468 478 name = "rustix" 469 - version = "0.38.41" 479 + version = "0.38.44" 480 + source = "registry+https://github.com/rust-lang/crates.io-index" 481 + checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 482 + dependencies = [ 483 + "bitflags", 484 + "errno", 485 + "libc", 486 + "linux-raw-sys 0.4.15", 487 + "windows-sys 0.59.0", 488 + ] 489 + 490 + [[package]] 491 + name = "rustix" 492 + version = "1.0.8" 470 493 source = "registry+https://github.com/rust-lang/crates.io-index" 471 - checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" 494 + checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" 472 495 dependencies = [ 473 496 "bitflags", 474 497 "errno", 475 498 "libc", 476 - "linux-raw-sys", 477 - "windows-sys 0.52.0", 499 + "linux-raw-sys 0.9.4", 500 + "windows-sys 0.60.2", 478 501 ] 479 502 480 503 [[package]] 481 504 name = "serde" 482 - version = "1.0.215" 505 + version = "1.0.219" 483 506 source = "registry+https://github.com/rust-lang/crates.io-index" 484 - checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" 507 + checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 485 508 dependencies = [ 486 509 "serde_derive", 487 510 ] 488 511 489 512 [[package]] 490 513 name = "serde_derive" 491 - version = "1.0.215" 514 + version = "1.0.219" 492 515 source = "registry+https://github.com/rust-lang/crates.io-index" 493 - checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" 516 + checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 494 517 dependencies = [ 495 518 "proc-macro2", 496 519 "quote", ··· 498 521 ] 499 522 500 523 [[package]] 501 - name = "smallstr" 502 - version = "0.3.0" 503 - source = "registry+https://github.com/rust-lang/crates.io-index" 504 - checksum = "63b1aefdf380735ff8ded0b15f31aab05daf1f70216c01c02a12926badd1df9d" 505 - dependencies = [ 506 - "smallvec", 507 - ] 508 - 509 - [[package]] 510 524 name = "smallvec" 511 - version = "1.13.2" 525 + version = "1.15.1" 512 526 source = "registry+https://github.com/rust-lang/crates.io-index" 513 - checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 527 + checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 514 528 515 529 [[package]] 516 530 name = "stable_deref_trait" ··· 526 540 527 541 [[package]] 528 542 name = "syn" 529 - version = "2.0.89" 543 + version = "2.0.106" 530 544 source = "registry+https://github.com/rust-lang/crates.io-index" 531 - checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" 545 + checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 532 546 dependencies = [ 533 547 "proc-macro2", 534 548 "quote", ··· 537 551 538 552 [[package]] 539 553 name = "synstructure" 540 - version = "0.13.1" 554 + version = "0.13.2" 541 555 source = "registry+https://github.com/rust-lang/crates.io-index" 542 - checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 556 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 543 557 dependencies = [ 544 558 "proc-macro2", 545 559 "quote", ··· 548 562 549 563 [[package]] 550 564 name = "tempfile" 551 - version = "3.14.0" 565 + version = "3.21.0" 552 566 source = "registry+https://github.com/rust-lang/crates.io-index" 553 - checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" 567 + checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" 554 568 dependencies = [ 555 - "cfg-if", 556 569 "fastrand", 570 + "getrandom", 557 571 "once_cell", 558 - "rustix", 559 - "windows-sys 0.59.0", 572 + "rustix 1.0.8", 573 + "windows-sys 0.60.2", 560 574 ] 561 575 562 576 [[package]] 563 577 name = "thiserror" 564 - version = "1.0.69" 578 + version = "2.0.16" 565 579 source = "registry+https://github.com/rust-lang/crates.io-index" 566 - checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 580 + checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" 567 581 dependencies = [ 568 582 "thiserror-impl", 569 583 ] 570 584 571 585 [[package]] 572 586 name = "thiserror-impl" 573 - version = "1.0.69" 587 + version = "2.0.16" 574 588 source = "registry+https://github.com/rust-lang/crates.io-index" 575 - checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 589 + checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" 576 590 dependencies = [ 577 591 "proc-macro2", 578 592 "quote", ··· 581 595 582 596 [[package]] 583 597 name = "tinystr" 584 - version = "0.7.6" 598 + version = "0.8.1" 585 599 source = "registry+https://github.com/rust-lang/crates.io-index" 586 - checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 600 + checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 587 601 dependencies = [ 588 602 "displaydoc", 589 603 "zerovec", 590 604 ] 591 605 592 606 [[package]] 593 - name = "tsk" 594 - version = "0.2.0" 607 + name = "tsk-cli" 608 + version = "0.4.0" 595 609 dependencies = [ 596 610 "clap", 597 611 "clap_complete", 598 612 "clap_mangen", 599 613 "colored", 600 614 "edit", 615 + "itertools", 601 616 "nix", 602 617 "open", 603 - "smallstr", 604 618 "thiserror", 605 619 "url", 606 620 "xattr", ··· 608 622 609 623 [[package]] 610 624 name = "unicode-ident" 611 - version = "1.0.14" 625 + version = "1.0.18" 612 626 source = "registry+https://github.com/rust-lang/crates.io-index" 613 - checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 627 + checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 614 628 615 629 [[package]] 616 630 name = "url" 617 - version = "2.5.4" 631 + version = "2.5.7" 618 632 source = "registry+https://github.com/rust-lang/crates.io-index" 619 - checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 633 + checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 620 634 dependencies = [ 621 635 "form_urlencoded", 622 636 "idna", 623 637 "percent-encoding", 638 + "serde", 624 639 ] 625 - 626 - [[package]] 627 - name = "utf16_iter" 628 - version = "1.0.5" 629 - source = "registry+https://github.com/rust-lang/crates.io-index" 630 - checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 631 640 632 641 [[package]] 633 642 name = "utf8_iter" ··· 642 651 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 643 652 644 653 [[package]] 654 + name = "wasi" 655 + version = "0.14.4+wasi-0.2.4" 656 + source = "registry+https://github.com/rust-lang/crates.io-index" 657 + checksum = "88a5f4a424faf49c3c2c344f166f0662341d470ea185e939657aaff130f0ec4a" 658 + dependencies = [ 659 + "wit-bindgen", 660 + ] 661 + 662 + [[package]] 645 663 name = "which" 646 664 version = "4.4.2" 647 665 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 650 668 "either", 651 669 "home", 652 670 "once_cell", 653 - "rustix", 654 - ] 655 - 656 - [[package]] 657 - name = "windows-sys" 658 - version = "0.48.0" 659 - source = "registry+https://github.com/rust-lang/crates.io-index" 660 - checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 661 - dependencies = [ 662 - "windows-targets 0.48.5", 671 + "rustix 0.38.44", 663 672 ] 664 673 665 674 [[package]] 666 - name = "windows-sys" 667 - version = "0.52.0" 675 + name = "windows-link" 676 + version = "0.1.3" 668 677 source = "registry+https://github.com/rust-lang/crates.io-index" 669 - checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 670 - dependencies = [ 671 - "windows-targets 0.52.6", 672 - ] 678 + checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 673 679 674 680 [[package]] 675 681 name = "windows-sys" ··· 681 687 ] 682 688 683 689 [[package]] 684 - name = "windows-targets" 685 - version = "0.48.5" 690 + name = "windows-sys" 691 + version = "0.60.2" 686 692 source = "registry+https://github.com/rust-lang/crates.io-index" 687 - checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 693 + checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 688 694 dependencies = [ 689 - "windows_aarch64_gnullvm 0.48.5", 690 - "windows_aarch64_msvc 0.48.5", 691 - "windows_i686_gnu 0.48.5", 692 - "windows_i686_msvc 0.48.5", 693 - "windows_x86_64_gnu 0.48.5", 694 - "windows_x86_64_gnullvm 0.48.5", 695 - "windows_x86_64_msvc 0.48.5", 695 + "windows-targets 0.53.3", 696 696 ] 697 697 698 698 [[package]] ··· 704 704 "windows_aarch64_gnullvm 0.52.6", 705 705 "windows_aarch64_msvc 0.52.6", 706 706 "windows_i686_gnu 0.52.6", 707 - "windows_i686_gnullvm", 707 + "windows_i686_gnullvm 0.52.6", 708 708 "windows_i686_msvc 0.52.6", 709 709 "windows_x86_64_gnu 0.52.6", 710 710 "windows_x86_64_gnullvm 0.52.6", ··· 712 712 ] 713 713 714 714 [[package]] 715 - name = "windows_aarch64_gnullvm" 716 - version = "0.48.5" 715 + name = "windows-targets" 716 + version = "0.53.3" 717 717 source = "registry+https://github.com/rust-lang/crates.io-index" 718 - checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 718 + checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" 719 + dependencies = [ 720 + "windows-link", 721 + "windows_aarch64_gnullvm 0.53.0", 722 + "windows_aarch64_msvc 0.53.0", 723 + "windows_i686_gnu 0.53.0", 724 + "windows_i686_gnullvm 0.53.0", 725 + "windows_i686_msvc 0.53.0", 726 + "windows_x86_64_gnu 0.53.0", 727 + "windows_x86_64_gnullvm 0.53.0", 728 + "windows_x86_64_msvc 0.53.0", 729 + ] 719 730 720 731 [[package]] 721 732 name = "windows_aarch64_gnullvm" ··· 724 735 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 725 736 726 737 [[package]] 727 - name = "windows_aarch64_msvc" 728 - version = "0.48.5" 738 + name = "windows_aarch64_gnullvm" 739 + version = "0.53.0" 729 740 source = "registry+https://github.com/rust-lang/crates.io-index" 730 - checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 741 + checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 731 742 732 743 [[package]] 733 744 name = "windows_aarch64_msvc" ··· 736 747 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 737 748 738 749 [[package]] 739 - name = "windows_i686_gnu" 740 - version = "0.48.5" 750 + name = "windows_aarch64_msvc" 751 + version = "0.53.0" 741 752 source = "registry+https://github.com/rust-lang/crates.io-index" 742 - checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 753 + checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 743 754 744 755 [[package]] 745 756 name = "windows_i686_gnu" ··· 748 759 checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 749 760 750 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]] 751 768 name = "windows_i686_gnullvm" 752 769 version = "0.52.6" 753 770 source = "registry+https://github.com/rust-lang/crates.io-index" 754 771 checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 755 772 756 773 [[package]] 757 - name = "windows_i686_msvc" 758 - version = "0.48.5" 774 + name = "windows_i686_gnullvm" 775 + version = "0.53.0" 759 776 source = "registry+https://github.com/rust-lang/crates.io-index" 760 - checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 777 + checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 761 778 762 779 [[package]] 763 780 name = "windows_i686_msvc" ··· 766 783 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 767 784 768 785 [[package]] 769 - name = "windows_x86_64_gnu" 770 - version = "0.48.5" 786 + name = "windows_i686_msvc" 787 + version = "0.53.0" 771 788 source = "registry+https://github.com/rust-lang/crates.io-index" 772 - checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 789 + checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 773 790 774 791 [[package]] 775 792 name = "windows_x86_64_gnu" ··· 778 795 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 779 796 780 797 [[package]] 781 - name = "windows_x86_64_gnullvm" 782 - version = "0.48.5" 798 + name = "windows_x86_64_gnu" 799 + version = "0.53.0" 783 800 source = "registry+https://github.com/rust-lang/crates.io-index" 784 - checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 801 + checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 785 802 786 803 [[package]] 787 804 name = "windows_x86_64_gnullvm" ··· 790 807 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 791 808 792 809 [[package]] 793 - name = "windows_x86_64_msvc" 794 - version = "0.48.5" 810 + name = "windows_x86_64_gnullvm" 811 + version = "0.53.0" 795 812 source = "registry+https://github.com/rust-lang/crates.io-index" 796 - checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 813 + checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 797 814 798 815 [[package]] 799 816 name = "windows_x86_64_msvc" ··· 802 819 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 803 820 804 821 [[package]] 805 - name = "write16" 806 - version = "1.0.0" 822 + name = "windows_x86_64_msvc" 823 + version = "0.53.0" 807 824 source = "registry+https://github.com/rust-lang/crates.io-index" 808 - checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 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" 809 832 810 833 [[package]] 811 834 name = "writeable" 812 - version = "0.5.5" 835 + version = "0.6.1" 813 836 source = "registry+https://github.com/rust-lang/crates.io-index" 814 - checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 837 + checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 815 838 816 839 [[package]] 817 840 name = "xattr" 818 - version = "1.3.1" 841 + version = "1.5.1" 819 842 source = "registry+https://github.com/rust-lang/crates.io-index" 820 - checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" 843 + checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" 821 844 dependencies = [ 822 845 "libc", 823 - "linux-raw-sys", 824 - "rustix", 846 + "rustix 1.0.8", 825 847 ] 826 848 827 849 [[package]] 828 850 name = "yoke" 829 - version = "0.7.5" 851 + version = "0.8.0" 830 852 source = "registry+https://github.com/rust-lang/crates.io-index" 831 - checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 853 + checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 832 854 dependencies = [ 833 855 "serde", 834 856 "stable_deref_trait", ··· 838 860 839 861 [[package]] 840 862 name = "yoke-derive" 841 - version = "0.7.5" 863 + version = "0.8.0" 842 864 source = "registry+https://github.com/rust-lang/crates.io-index" 843 - checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 865 + checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 844 866 dependencies = [ 845 867 "proc-macro2", 846 868 "quote", ··· 850 872 851 873 [[package]] 852 874 name = "zerofrom" 853 - version = "0.1.5" 875 + version = "0.1.6" 854 876 source = "registry+https://github.com/rust-lang/crates.io-index" 855 - checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" 877 + checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 856 878 dependencies = [ 857 879 "zerofrom-derive", 858 880 ] 859 881 860 882 [[package]] 861 883 name = "zerofrom-derive" 862 - version = "0.1.5" 884 + version = "0.1.6" 863 885 source = "registry+https://github.com/rust-lang/crates.io-index" 864 - checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" 886 + checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 865 887 dependencies = [ 866 888 "proc-macro2", 867 889 "quote", ··· 870 892 ] 871 893 872 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]] 873 906 name = "zerovec" 874 - version = "0.10.4" 907 + version = "0.11.4" 875 908 source = "registry+https://github.com/rust-lang/crates.io-index" 876 - checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 909 + checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" 877 910 dependencies = [ 878 911 "yoke", 879 912 "zerofrom", ··· 882 915 883 916 [[package]] 884 917 name = "zerovec-derive" 885 - version = "0.10.3" 918 + version = "0.11.1" 886 919 source = "registry+https://github.com/rust-lang/crates.io-index" 887 - checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 920 + checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 888 921 dependencies = [ 889 922 "proc-macro2", 890 923 "quote",
+16 -6
Cargo.toml
··· 1 1 [package] 2 - name = "tsk" 3 - version = "0.2.1" 4 - edition = "2021" 2 + name = "tsk-cli" 3 + version = "0.4.0" 4 + edition = "2024" 5 5 publish = true 6 + license-file = "LICENSE" 7 + description = "A command-line first, file-system based task manager" 8 + repository = "https://codeberg.org/ngp/tsk" 9 + homepage = "https://tsk.ngp.computer/" 10 + readme = "readme" 11 + authors = ["Noah Pederson <noah@packetlost.dev>"] 12 + 13 + [[bin]] 14 + name = "tsk" 15 + path = "src/main.rs" 6 16 7 17 [dependencies] 8 18 clap = { version = "4", features = ["derive", "env"] } 9 19 clap_complete = "4" 10 20 edit = "0" 11 21 nix = { version = "0", features = ["fs"] } 12 - thiserror = "1" 22 + thiserror = "2" 13 23 url = "2" 14 24 xattr = "1" 15 - colored = "2" 16 - smallstr = "0" 25 + colored = "3" 17 26 open = "5" 27 + itertools = "0" 18 28 19 29 [build-dependencies] 20 30 clap_mangen = "0"
+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 +
+16 -4
readme
··· 36 36 tsk expects to run on POSIX-like systems. Microsoft Windows and other 37 37 non-UNIX-ey operating systems will never be directly supported. 38 38 39 + 40 + Installation 41 + ------------ 42 + 43 + ```sh 44 + cargo install --locked tsk-cli 45 + ``` 46 + 47 + 39 48 Building 40 49 -------- 41 50 ··· 174 183 175 184 A quick overview of the format: 176 185 177 - !Bolded! text is surrounded by exclamation marks (!) 178 - *Italicized* text is surrouneded by single asterists (*) 179 - _Underlined_ text is surrounded by underscores (_) 180 - ~Strikenthrough~ text is surrounded by tildes (~) 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 (`) 181 192 182 193 Links like in Markdown, along with the wiki-style links documented above. 194 + Raw links can also be written as \<https://example.com\>. 183 195 184 196 Misc 185 197 ----
+10 -8
src/attrs.rs
··· 1 + use std::collections::BTreeMap; 2 + use std::collections::btree_map::Entry; 1 3 use std::collections::btree_map::{IntoIter as BTreeIntoIter, Iter as BTreeMapIter}; 2 - use std::collections::BTreeMap; 3 4 use std::iter::Chain; 4 5 5 6 type Map = BTreeMap<String, String>; ··· 20 21 type IntoIter = Chain<BTreeIntoIter<String, String>, BTreeIntoIter<String, String>>; 21 22 22 23 fn into_iter(self) -> Self::IntoIter { 23 - self.written.into_iter().chain(self.updated.into_iter()) 24 + self.written.into_iter().chain(self.updated) 24 25 } 25 26 } 26 27 ··· 38 39 } 39 40 40 41 pub(crate) fn insert(&mut self, key: String, value: String) -> Option<String> { 41 - if self.updated.contains_key(&key) { 42 - self.updated.insert(key, value) 43 - } else { 44 - let maybe_old_value = self.written.get(&key); 45 - self.updated.insert(key, value); 46 - maybe_old_value.cloned() 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 + } 47 49 } 48 50 } 49 51
+2
src/errors.rs
··· 27 27 #[allow(dead_code)] 28 28 #[error("An unexpected error occurred: {0}")] 29 29 Oops(Box<dyn std::error::Error>), 30 + #[error("System time/clock error: {0}")] 31 + SystemTime(#[from] std::time::SystemTimeError), 30 32 } 31 33 32 34 impl From<Infallible> for Error {
+14 -6
src/fzf.rs
··· 1 1 use crate::errors::{Error, Result}; 2 + use std::ffi::OsStr; 2 3 use std::fmt::Display; 3 4 use std::io::Write; 4 5 use std::process::{Command, Stdio}; ··· 6 7 7 8 /// Sends each item as a line to stdin to the `fzf` command and returns the selected item's string 8 9 /// representation as output 9 - pub fn select<I>(input: impl IntoIterator<Item = I>) -> Result<Option<I>> 10 + pub fn select<I, O, S>( 11 + input: impl IntoIterator<Item = I>, 12 + extra: impl IntoIterator<Item = S>, 13 + ) -> Result<Option<O>> 10 14 where 11 - I: Display + FromStr, 12 - Error: From<<I as FromStr>::Err>, 15 + O: FromStr, 16 + I: Display, 17 + Error: From<<O as FromStr>::Err>, 18 + S: AsRef<OsStr>, 13 19 { 14 - let mut child = Command::new("fzf") 15 - .args(["-d", "\t"]) 20 + let mut command = Command::new("fzf"); 21 + let mut child = command 22 + .args(extra) 23 + .arg("--read0") 16 24 .stderr(Stdio::inherit()) 17 25 .stdin(Stdio::piped()) 18 26 .stdout(Stdio::piped()) ··· 20 28 // unwrap: this can never fail 21 29 let child_in = child.stdin.as_mut().unwrap(); 22 30 for item in input.into_iter() { 23 - write!(child_in, "{item}\n")?; 31 + write!(child_in, "{item}\0")?; 24 32 } 25 33 let output = child.wait_with_output()?; 26 34 if output.stdout.is_empty() {
+114 -51
src/main.rs
··· 5 5 mod task; 6 6 mod util; 7 7 mod workspace; 8 - use clap_complete::{generate, Shell}; 8 + use clap_complete::{Shell, generate}; 9 9 use errors::Result; 10 10 use std::io::{self, Write}; 11 11 use std::path::PathBuf; 12 12 use std::process::exit; 13 + use std::str::FromStr as _; 13 14 use std::{env::current_dir, io::Read}; 14 15 use task::ParsedLink; 15 - use workspace::{Id, TaskIdentifier, Workspace}; 16 + use workspace::{Id, Task, TaskIdentifier, Workspace}; 16 17 17 18 //use smol; 18 19 //use iocraft::prelude::*; 19 - use clap::{value_parser, Args, CommandFactory, Parser, Subcommand}; 20 + use clap::{Args, CommandFactory, Parser, Subcommand}; 20 21 use edit::edit as open_editor; 21 22 22 23 fn default_dir() -> PathBuf { 23 24 current_dir().unwrap() 25 + } 26 + 27 + fn parse_id(s: &str) -> std::result::Result<Id, &'static str> { 28 + Id::from_str(s).map_err(|_| "Unable to parse tsk- ID") 24 29 } 25 30 26 31 #[derive(Parser)] ··· 56 61 #[command(flatten)] 57 62 title: Title, 58 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 + }, 59 82 /// Print the task stack. This will include just TSK-IDs and the title. 60 83 List { 61 84 /// Whether to list all tasks in the task stack. If specified, -c / count is ignored. ··· 85 108 Find { 86 109 #[command(flatten)] 87 110 args: FindArgs, 88 - /// Whether to print the full TSK-ID (instead of just an integer) 89 - #[arg(short = 'F', default_value_t = true)] 90 - full_id: bool, 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, 91 114 }, 92 115 93 116 /// Prints the contents of a task, parsing the body as rich text and formatting it using ANSI ··· 96 119 /// Shows raw file attributes for the file 97 120 #[arg(short = 'x', default_value_t = false)] 98 121 show_attrs: bool, 122 + 123 + #[arg(short = 'R', default_value_t = false)] 124 + raw: bool, 99 125 /// The [TSK-]ID of the task to display 100 126 #[command(flatten)] 101 127 task_id: TaskId, ··· 111 137 #[command(flatten)] 112 138 task_id: TaskId, 113 139 /// The index of the link to open. Must be supplied. 114 - #[arg(short = 'l')] 140 + #[arg(short = 'l', default_value_t = 1)] 115 141 link_index: usize, 116 142 /// When opening an internal link, whether to show or edit the addressed task. 117 143 #[arg(short = 'e', default_value_t = false)] ··· 167 193 id: Option<u32>, 168 194 169 195 /// The ID of the task to select with the 'tsk-' prefix. 170 - #[arg(short = 'T', value_name = "TSK-ID", value_parser = value_parser!(String))] 196 + #[arg(short = 'T', value_name = "TSK-ID", value_parser = parse_id)] 171 197 tsk_id: Option<Id>, 172 198 173 199 /// Selects a task relative to the top of the stack. ··· 194 220 #[derive(Args)] 195 221 #[group(required = false, multiple = false)] 196 222 struct FindArgs { 197 - /// Include the contents of tasks in the search criteria. 223 + /// Exclude the contents of tasks in the search criteria. 198 224 #[arg(short = 'b', default_value_t = false)] 199 - search_body: bool, 225 + exclude_body: bool, 200 226 /* TODO: implement this 201 227 /// Include archived tasks in the search criteria. Combine with `-b` to include archived 202 228 /// bodies in the search criteria. ··· 209 235 fn from(value: TaskId) -> Self { 210 236 if let Some(id) = value.id.map(Id::from).or(value.tsk_id) { 211 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 + } 212 243 } else { 213 - if value.find.find { 214 - TaskIdentifier::Find { 215 - search_body: value.find.args.search_body, 216 - archived: false, 217 - } 218 - } else { 219 - TaskIdentifier::Relative(value.relative_id) 220 - } 244 + TaskIdentifier::Relative(value.relative_id) 221 245 } 222 246 } 223 247 } ··· 228 252 let var_name = match cli.command { 229 253 Commands::Init => command_init(dir), 230 254 Commands::Push { edit, body, title } => command_push(dir, edit, body, title), 255 + Commands::Append { edit, body, title } => command_append(dir, edit, body, title), 231 256 Commands::List { all, count } => command_list(dir, all, count), 232 257 Commands::Swap => command_swap(dir), 233 258 Commands::Show { 234 259 task_id, 260 + raw, 235 261 show_attrs, 236 - } => command_show(dir, task_id, show_attrs), 262 + } => command_show(dir, task_id, show_attrs, raw), 237 263 Commands::Follow { 238 264 task_id, 239 265 link_index, ··· 242 268 Commands::Edit { task_id } => command_edit(dir, task_id), 243 269 Commands::Completion { shell } => command_completion(shell), 244 270 Commands::Drop { task_id } => command_drop(dir, task_id), 245 - Commands::Find { args, full_id } => command_find(dir, full_id, args), 271 + Commands::Find { args, short_id } => command_find(dir, short_id, args), 246 272 Commands::Rot => Workspace::from_path(dir).unwrap().rot(), 247 273 Commands::Tor => Workspace::from_path(dir).unwrap().tor(), 248 274 Commands::Prioritize { task_id } => command_prioritize(dir, task_id), ··· 265 291 relative_id: 0, 266 292 find: Find { 267 293 find: false, 268 - args: FindArgs { search_body: false }, 294 + args: FindArgs { exclude_body: true }, 269 295 }, 270 296 } 271 297 } ··· 274 300 Workspace::init(dir) 275 301 } 276 302 277 - fn command_push(dir: PathBuf, edit: bool, body: Option<String>, title: Title) -> Result<()> { 278 - let workspace = Workspace::from_path(dir)?; 303 + fn create_task( 304 + workspace: &mut Workspace, 305 + edit: bool, 306 + body: Option<String>, 307 + title: Title, 308 + ) -> Result<Task> { 279 309 let mut title = if let Some(title) = title.title { 280 310 title 281 311 } else if let Some(title) = title.title_simple { 282 - let joined = title.join(" "); 283 - joined 312 + title.join(" ") 284 313 } else { 285 314 "".to_string() 286 315 }; 287 - 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 + }; 288 331 if body == "-" { 289 332 // add newline so you can type directly in the shell 290 333 //eprintln!(""); ··· 298 341 body = content.1.to_string(); 299 342 } 300 343 } 344 + // Ensure title never contains newlines (invariant for index file format) 345 + title = title.replace(['\n', '\r'], " "); 301 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)?; 302 354 workspace.push_task(task) 303 355 } 304 356 357 + fn command_append(dir: PathBuf, edit: bool, body: Option<String>, title: Title) -> Result<()> { 358 + let mut workspace = Workspace::from_path(dir)?; 359 + let task = create_task(&mut workspace, edit, body, title)?; 360 + workspace.append_task(task) 361 + } 362 + 305 363 fn command_list(dir: PathBuf, all: bool, count: usize) -> Result<()> { 306 364 let workspace = Workspace::from_path(dir)?; 307 - let stack = if all { 308 - workspace.read_stack()? 309 - } else { 310 - workspace.read_stack()? 311 - }; 365 + let stack = workspace.read_stack()?; 366 + 312 367 if stack.empty() { 313 368 println!("*No tasks*"); 314 369 exit(0); 315 - } else { 316 - if !all { 317 - for stack_item in stack.into_iter().take(count) { 318 - println!("{stack_item}"); 319 - } 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()); 320 379 } else { 321 - for stack_item in stack.into_iter() { 322 - println!("{stack_item}"); 323 - } 380 + println!("{stack_item}"); 324 381 } 325 382 } 326 383 Ok(()) ··· 336 393 let workspace = Workspace::from_path(dir)?; 337 394 let id: TaskIdentifier = id.into(); 338 395 let mut task = workspace.task(id)?; 396 + let pre_links = task::parse(&task.to_string()).map(|pt| pt.intenal_links()); 339 397 let new_content = open_editor(format!("{}\n\n{}", task.title.trim(), task.body.trim()))?; 340 398 if let Some((title, body)) = new_content.split_once("\n") { 341 - task.title = title.to_string(); 399 + // Ensure title never contains newlines (invariant for index file format) 400 + task.title = title.replace(['\n', '\r'], " "); 342 401 task.body = body.to_string(); 402 + workspace.handle_metadata(&task, pre_links)?; 343 403 task.save()?; 344 404 } 345 405 Ok(()) ··· 361 421 Ok(()) 362 422 } 363 423 364 - fn command_find(dir: PathBuf, full_id: bool, find_args: FindArgs) -> Result<()> { 365 - let id = Workspace::from_path(dir)?.search(None, find_args.search_body, false)?; 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)?; 366 426 if let Some(id) = id { 367 - if full_id { 368 - println!("{id}"); 369 - } else { 427 + if short_id { 370 428 // print as integer 371 429 println!("{}", id.0); 430 + } else { 431 + println!("{id}"); 372 432 } 373 433 } else { 374 434 eprintln!("No task selected."); ··· 385 445 Workspace::from_path(dir)?.deprioritize(task_id.into()) 386 446 } 387 447 388 - fn command_show(dir: PathBuf, task_id: TaskId, show_attrs: bool) -> Result<()> { 448 + fn command_show(dir: PathBuf, task_id: TaskId, show_attrs: bool, raw: bool) -> Result<()> { 389 449 let task = Workspace::from_path(dir)?.task(task_id.into())?; 390 450 // YAML front-matter style. YAML is gross, but it's what everyone uses! 391 451 if show_attrs && !task.attributes.is_empty() { ··· 395 455 } 396 456 println!("---"); 397 457 } 398 - if let Some(styled_task) = task::parse(&task.to_string()) { 399 - writeln!(io::stdout(), "{}", styled_task.content)?; 400 - } else { 401 - println!("{task}"); 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 + } 402 465 } 403 466 Ok(()) 404 467 } ··· 421 484 if edit { 422 485 command_edit(dir, taskid) 423 486 } else { 424 - command_show(dir, taskid, false) 487 + command_show(dir, taskid, false, false) 425 488 } 426 489 } 427 490 }
+20 -17
src/stack.rs
··· 4 4 5 5 use crate::errors::{Error, Result}; 6 6 use crate::util; 7 - use std::collections::vec_deque::Iter; 8 7 use std::collections::VecDeque; 8 + use std::collections::vec_deque::Iter; 9 9 use std::fmt::Display; 10 + use std::fs::File; 10 11 use std::io::{self, BufRead, BufReader, Seek, Write}; 12 + use std::path::Path; 11 13 use std::str::FromStr; 12 14 use std::time::{Duration, SystemTime, UNIX_EPOCH}; 13 - use std::{fs::File, path::PathBuf}; 14 15 15 16 use nix::fcntl::{Flock, FlockArg}; 16 17 ··· 67 68 let mut parts = s.trim().split("\t"); 68 69 let id: Id = parts 69 70 .next() 70 - .ok_or(Error::Parse(format!( 71 - "Incomplete index line. Missing tsk ID" 72 - )))? 71 + .ok_or(Error::Parse( 72 + "Incomplete index line. Missing tsk ID".to_owned(), 73 + ))? 73 74 .parse()?; 74 75 let title: String = parts 75 76 .next() 76 - .ok_or(Error::Parse(format!( 77 - "Incomplete index line. Missing title." 78 - )))? 77 + .ok_or(Error::Parse( 78 + "Incomplete index line. Missing title.".to_owned(), 79 + ))? 79 80 .trim() 80 81 .to_string(); 81 82 // parse the timestamp as an integer ··· 96 97 97 98 impl StackItem { 98 99 /// Parses a [`StackItem`] from a string. The expected format is a tab-delimited line with the 99 - /// files: task id title 100 - fn from_line(workspace_path: &PathBuf, line: String) -> Result<Self> { 100 + /// files: task id title 101 + fn from_line(workspace_path: &Path, line: String) -> Result<Self> { 101 102 let mut stack_item: StackItem = line.parse()?; 102 103 103 104 let task = util::flopen( 104 105 workspace_path 105 106 .join(TASKSFOLDER) 106 - .join(stack_item.id.to_filename()), 107 + .join(stack_item.id.filename()), 107 108 FlockArg::LockExclusive, 108 109 )?; 109 110 let task_modify_time = task.metadata()?.modified()?; ··· 125 126 } 126 127 127 128 impl TaskStack { 128 - pub fn from_tskdir(workspace_path: &PathBuf) -> Result<Self> { 129 + pub fn from_tskdir(workspace_path: &Path) -> Result<Self> { 129 130 let file = util::flopen(workspace_path.join(INDEXFILE), FlockArg::LockExclusive)?; 130 131 let index = BufReader::new(&*file).lines(); 131 132 let mut all = VecDeque::new(); ··· 142 143 self.file.seek(std::io::SeekFrom::Start(0))?; 143 144 self.file.set_len(0)?; 144 145 for item in self.all.iter() { 145 - self.file.write_all(format!("{item}\n").as_bytes())?; 146 + let time = item.modify_time.duration_since(UNIX_EPOCH)?.as_secs(); 147 + self.file 148 + .write_all(format!("{item}\t{}\n", time).as_bytes())?; 146 149 } 147 150 Ok(()) 148 151 } ··· 162 165 pub fn swap(&mut self) { 163 166 let tip = self.all.pop_front(); 164 167 let second = self.all.pop_front(); 165 - if tip.is_some() && second.is_some() { 166 - self.all.push_front(tip.unwrap()); 167 - 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); 168 171 } 169 172 } 170 173 ··· 176 179 self.all.remove(index) 177 180 } 178 181 179 - pub fn iter(&self) -> Iter<StackItem> { 182 + pub fn iter(&self) -> Iter<'_, StackItem> { 180 183 self.all.iter() 181 184 } 182 185
+89 -56
src/task.rs
··· 1 1 #![allow(dead_code)] 2 2 3 - use std::str::FromStr; 3 + use std::{collections::HashSet, str::FromStr}; 4 4 use url::Url; 5 5 6 6 use crate::workspace::Id; 7 7 use colored::Colorize; 8 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 + 9 14 #[derive(Debug, Eq, PartialEq, Clone, Copy)] 10 15 enum ParserState { 11 16 // Started by ` =`, terminated by `= 12 - Highlight(usize), 17 + Highlight(usize, usize), 13 18 // Started by ` [`, terminated by `](` 14 - Linktext(usize), 19 + Linktext(usize, usize), 15 20 // Started by `](`, terminated by `) `, must immedately follow a Linktext 16 - Link(usize), 17 - RawLink(usize), 21 + Link(usize, usize), 22 + RawLink(usize, usize), 18 23 // Started by ` [[`, terminated by `]] ` 19 - InternalLink(usize), 24 + InternalLink(usize, usize), 20 25 // Started by ` *`, terminated by `* ` 21 - Italics(usize), 26 + Italics(usize, usize), 22 27 // Started by ` !`, termianted by `!` 23 - Bold(usize), 28 + Bold(usize, usize), 24 29 // Started by ` _`, terminated by `_ ` 25 - Underline(usize), 30 + Underline(usize, usize), 26 31 // Started by ` -`, terminated by `- ` 27 - Strikethrough(usize), 32 + Strikethrough(usize, usize), 28 33 29 34 // TODO: implement these. 30 35 // Started by `_ `, terminated by `_` ··· 37 42 // `\n` and followed by a `\n` 38 43 BlockEnd(usize), 39 44 // Started by ` ``, terminated by `` ` or `\n` 40 - InlineBlock(usize), 45 + InlineBlock(usize, usize), 41 46 // Started by `^\w+>`, terminated by `\n` 42 47 Blockquote(usize), 43 48 } ··· 53 58 pub(crate) links: Vec<ParsedLink>, 54 59 } 55 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 + 56 73 pub(crate) fn parse(s: &str) -> Option<ParsedTask> { 57 74 let mut state: Vec<ParserState> = Vec::new(); 58 75 let mut out = String::with_capacity(s.len()); ··· 64 81 let state_last = state.last().cloned(); 65 82 match stream.next() { 66 83 // there will always be an op code in the stack 67 - Some((_, c)) => { 84 + Some((char_pos, c)) => { 68 85 out.push(c); 69 86 let end = out.len() - 1; 70 87 match (last, c, state_last) { 71 88 ('[', '[', _) => { 72 - state.push(InternalLink(end)); 89 + state.push(InternalLink(end, char_pos)); 73 90 } 74 - (']', ']', Some(InternalLink(il))) => { 91 + (']', ']', Some(InternalLink(il, s_pos))) => { 75 92 state.pop(); 76 - let contents = out.get(il + 1..out.len() - 2)?; 77 - if let Ok(id) = Id::from_str(&contents) { 93 + let contents = s.get(s_pos + 1..char_pos - 1)?; 94 + if let Ok(id) = Id::from_str(contents) { 78 95 let linktext = format!( 79 96 "{}{}", 80 97 contents.purple(), ··· 86 103 panic!("Internal link is not a valid id: {contents}"); 87 104 } 88 105 } 89 - (' ' | '\r' | '\n', '[', _) => { 90 - state.push(Linktext(end)); 106 + (last, '[', _) if is_boundary(last) => { 107 + state.push(Linktext(end, char_pos)); 91 108 } 92 - (']', '(', Some(Linktext(_))) => { 93 - state.push(Link(end)); 109 + (']', '(', Some(Linktext(_, _))) => { 110 + state.push(Link(end, char_pos)); 94 111 } 95 - (')', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Link(_))) => { 96 - let linkpos = if let Link(lp) = state.pop().unwrap() { 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() { 97 116 lp 98 117 } else { 99 118 // remove the linktext state, it is always present. 100 119 state.pop(); 101 120 continue; 102 121 }; 103 - let linktextpos = if let Linktext(lt) = state.pop().unwrap() { 122 + let linktextpos = if let Linktext(lt, _) = state.pop().unwrap() { 104 123 lt 105 124 } else { 106 125 continue; ··· 116 135 out.replace_range(linktextpos..end, &linktext); 117 136 } 118 137 } 119 - ('>', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(RawLink(hl))) => { 138 + ('>', c, Some(RawLink(hl, s_pos))) 139 + if is_boundary(c) && s_pos != char_pos - 1 => 140 + { 120 141 state.pop(); 121 - let link = out.get(hl + 1..out.len() - 2)?; 142 + let link = s.get(s_pos + 1..char_pos - 1)?; 122 143 if let Ok(url) = Url::parse(link) { 123 144 let linktext = 124 145 format!("{}{}", link.blue(), super_num(links.len() + 1).purple()); ··· 126 147 out.replace_range(hl..end, &linktext); 127 148 } 128 149 } 129 - (' ' | '\r' | '\n', '<', _) => { 130 - state.push(RawLink(end)); 150 + (last, '<', _) if is_boundary(last) => { 151 + state.push(RawLink(end, char_pos)); 131 152 } 132 - ('=', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Highlight(hl))) => { 153 + ('=', c, Some(Highlight(hl, s_pos))) 154 + if is_boundary(c) && s_pos != char_pos - 1 => 155 + { 133 156 state.pop(); 134 157 out.replace_range( 135 158 hl..end, 136 - &out.get(hl + 1..out.len() - 2)?.reversed().to_string(), 159 + &s.get(s_pos + 1..char_pos - 1)?.reversed().to_string(), 137 160 ); 138 161 } 139 - (' ' | '\r' | '\n', '=', _) => { 140 - state.push(Highlight(end)); 162 + (last, '=', _) if is_boundary(last) => { 163 + state.push(Highlight(end, char_pos)); 141 164 } 142 - (' ' | '\r' | '\n', '*', _) => { 143 - state.push(Italics(end)); 165 + (last, '*', _) if is_boundary(last) => { 166 + state.push(Italics(end, char_pos)); 144 167 } 145 - ('*', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Italics(il))) => { 168 + ('*', c, Some(Italics(il, s_pos))) 169 + if is_boundary(c) && s_pos != char_pos - 1 => 170 + { 146 171 state.pop(); 147 172 out.replace_range( 148 173 il..end, 149 - &out.get(il + 1..out.len() - 2)?.italic().to_string(), 174 + &s.get(s_pos + 1..char_pos - 1)?.italic().to_string(), 150 175 ); 151 176 } 152 - (' ' | '\r' | '\n', '!', _) => { 153 - state.push(Bold(end)); 177 + (last, '!', _) if is_boundary(last) => { 178 + state.push(Bold(end, char_pos)); 154 179 } 155 - ('!', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Bold(il))) => { 180 + ('!', c, Some(Bold(il, s_pos))) if is_boundary(c) && s_pos != char_pos - 1 => { 156 181 state.pop(); 157 - out.replace_range(il..end, &out.get(il + 1..end - 1)?.bold().to_string()); 182 + out.replace_range( 183 + il..end, 184 + &s.get(s_pos + 1..char_pos - 1)?.bold().to_string(), 185 + ); 158 186 } 159 - (' ' | '\r' | '\n', '_', _) => { 160 - state.push(Underline(end)); 187 + (last, '_', _) if is_boundary(last) => { 188 + state.push(Underline(end, char_pos)); 161 189 } 162 - ('_', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Underline(il))) => { 190 + ('_', c, Some(Underline(il, s_pos))) 191 + if is_boundary(c) && s_pos != char_pos - 1 => 192 + { 163 193 state.pop(); 164 194 out.replace_range( 165 195 il..end, 166 - &out.get(il + 1..end - 1)?.underline().to_string(), 196 + &s.get(s_pos + 1..char_pos - 1)?.underline().to_string(), 167 197 ); 168 198 } 169 - (' ' | '\r' | '\n', '~', _) => { 170 - state.push(Strikethrough(end)); 199 + (last, '~', _) if is_boundary(last) => { 200 + state.push(Strikethrough(end, char_pos)); 171 201 } 172 - ('~', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Strikethrough(il))) => { 202 + ('~', c, Some(Strikethrough(il, s_pos))) 203 + if is_boundary(c) && s_pos != char_pos - 1 => 204 + { 173 205 state.pop(); 174 206 out.replace_range( 175 207 il..end, 176 - &out.get(il + 1..end - 1)?.strikethrough().to_string(), 208 + &s.get(s_pos + 1..char_pos - 1)?.strikethrough().to_string(), 177 209 ); 178 210 } 179 - ('`', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(InlineBlock(hl))) => { 180 - state.pop(); 211 + ('`', c, Some(InlineBlock(hl, s_pos))) 212 + if is_boundary(c) && s_pos != char_pos - 1 => 213 + { 181 214 out.replace_range( 182 215 hl..end, 183 - &out.get(hl + 1..out.len() - 2)?.green().to_string(), 216 + &s.get(s_pos + 1..char_pos - 1)?.green().to_string(), 184 217 ); 185 218 } 186 - (' ' | '\r' | '\n', '`', _) => { 187 - state.push(InlineBlock(end)); 219 + (last, '`', _) if is_boundary(last) => { 220 + state.push(InlineBlock(end, char_pos)); 188 221 } 189 222 _ => (), 190 223 } ··· 261 294 fn test_link_no_terminal_link() { 262 295 let input = "hello [world](https://ngp.computer\n"; 263 296 let output = parse(input).expect("parse to work"); 264 - assert!(output.links.len() == 0); 297 + assert!(output.links.is_empty()); 265 298 assert_eq!(input, output.content); 266 299 } 267 300 #[test] 268 301 fn test_link_bad_no_start_link() { 269 302 let input = "hello [world]https://ngp.computer)\n"; 270 303 let output = parse(input).expect("parse to work"); 271 - assert!(output.links.len() == 0); 304 + assert!(output.links.is_empty()); 272 305 assert_eq!(input, output.content); 273 306 } 274 307 #[test] 275 308 fn test_link_bad_no_link() { 276 309 let input = "hello [world]\n"; 277 310 let output = parse(input).expect("parse to work"); 278 - assert!(output.links.len() == 0); 311 + assert!(output.links.is_empty()); 279 312 assert_eq!(input, output.content); 280 313 } 281 314 ··· 294 327 fn test_internal_link_bad() { 295 328 let input = "hello [[tsk-123"; 296 329 let output = parse(input).expect("parse to work"); 297 - assert!(output.links.len() == 0); 330 + assert!(output.links.is_empty()); 298 331 assert_eq!(input, output.content); 299 332 } 300 333
+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
+130 -41
src/workspace.rs
··· 5 5 use crate::attrs::Attrs; 6 6 use crate::errors::{Error, Result}; 7 7 use crate::stack::{StackItem, TaskStack}; 8 + use crate::task::parse as parse_task; 8 9 use crate::{fzf, util}; 9 - use std::collections::{vec_deque, BTreeMap}; 10 + use std::collections::{BTreeMap, HashSet, vec_deque}; 10 11 use std::ffi::OsString; 11 12 use std::fmt::Display; 12 - use std::fs::File; 13 + use std::fs::{File, remove_file}; 13 14 use std::io::{BufRead as _, BufReader, Read, Seek, SeekFrom}; 14 15 use std::ops::Deref; 15 16 use std::os::unix::fs::symlink; 16 17 use std::path::PathBuf; 18 + use std::process::{Command, Stdio}; 17 19 use std::str::FromStr; 18 20 use std::{fs::OpenOptions, io::Write}; 19 21 20 22 const INDEXFILE: &str = "index"; 21 23 const TITLECACHEFILE: &str = "cache"; 22 24 const XATTRPREFIX: &str = "user.tsk."; 25 + const BACKREFXATTR: &str = "user.tsk.references"; 23 26 /// A unique identifier for a task. When referenced in text, it is prefixed with `tsk-`. 24 - #[derive(Clone, Copy, Debug, Eq, PartialEq)] 27 + #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] 25 28 pub struct Id(pub u32); 26 29 27 30 impl FromStr for Id { 28 31 type Err = Error; 29 32 30 33 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { 31 - let s = s 32 - .strip_prefix("tsk-") 34 + let upper = s.to_uppercase(); 35 + let s = upper 36 + .trim() 37 + .strip_prefix("TSK-") 33 38 .ok_or(Self::Err::Parse(format!("expected tsk- prefix. Got {s}")))?; 34 39 Ok(Self(s.parse()?)) 35 40 } ··· 48 53 } 49 54 50 55 impl Id { 51 - pub fn to_filename(&self) -> String { 56 + /// Returns the filename for a task with this id. 57 + pub fn filename(&self) -> String { 52 58 format!("tsk-{}.tsk", self.0) 53 59 } 54 60 } ··· 56 62 pub enum TaskIdentifier { 57 63 Id(Id), 58 64 Relative(u32), 59 - Find { search_body: bool, archived: bool }, 65 + Find { exclude_body: bool, archived: bool }, 60 66 } 61 67 62 68 impl From<Id> for TaskIdentifier { ··· 80 86 } 81 87 std::fs::create_dir(&tsk_dir)?; 82 88 // Create the tasks directory 83 - std::fs::create_dir(&tsk_dir.join("tasks"))?; 89 + std::fs::create_dir(tsk_dir.join("tasks"))?; 84 90 // Create the archive directory 85 - std::fs::create_dir(&tsk_dir.join("archive"))?; 91 + std::fs::create_dir(tsk_dir.join("archive"))?; 86 92 let mut next = OpenOptions::new() 87 93 .read(true) 88 94 .write(true) 89 95 .create(true) 96 + .truncate(true) 90 97 .open(tsk_dir.join("next"))?; 98 + // initialize the next file with ID 1 91 99 next.write_all(b"1\n")?; 92 100 Ok(()) 93 101 } ··· 106 114 Ok(stack_item.id) 107 115 } 108 116 TaskIdentifier::Find { 109 - search_body, 117 + exclude_body, 110 118 archived, 111 119 } => self 112 - .search(None, search_body, archived)? 120 + .search(None, !exclude_body, archived)? 113 121 .ok_or(Error::NotSelected), 114 122 } 115 123 } ··· 181 189 }) 182 190 } 183 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 + 184 241 /// Reads an xattr from a file, stripping the prefix for 185 242 fn read_xattr<D: Deref<Target = File>>(file: &D, key: OsString) -> Option<(String, String)> { 186 243 // this *shouldn't* allocate, but it does O(n) scan the str for UTF-8 correctness ··· 189 246 Some((parsedkey.to_string(), String::from_utf8(valuebytes).ok()?)) 190 247 } 191 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 + 192 258 pub fn read_stack(&self) -> Result<TaskStack> { 193 259 TaskStack::from_tskdir(&self.path) 194 260 } ··· 196 262 pub fn push_task(&self, task: Task) -> Result<()> { 197 263 let mut stack = TaskStack::from_tskdir(&self.path)?; 198 264 stack.push(task.try_into()?); 265 + stack.save()?; 266 + Ok(()) 267 + } 268 + 269 + pub fn append_task(&self, task: Task) -> Result<()> { 270 + let mut stack = TaskStack::from_tskdir(&self.path)?; 271 + stack.push_back(task.try_into()?); 199 272 stack.save()?; 200 273 Ok(()) 201 274 } ··· 248 321 let id = self.resolve(identifier)?; 249 322 let mut stack = self.read_stack()?; 250 323 let index = &stack.iter().map(|i| i.id).position(|i| i == id); 324 + // TODO: remove the softlink in .tsk/tasks 251 325 let task = if let Some(index) = index { 252 326 let prioritized_task = stack.remove(*index); 253 327 stack.save()?; ··· 255 329 } else { 256 330 None 257 331 }; 332 + remove_file(self.path.join("tasks").join(format!("{id}.tsk")))?; 258 333 Ok(task) 259 334 } 260 335 ··· 275 350 workspace: self, 276 351 }; 277 352 // search the entirety of a task 278 - Ok(fzf::select(loader)?.map(|bt| bt.id)) 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 + )?) 279 366 } else { 280 367 // just search the stack 281 - Ok(fzf::select(stack)?.map(|si| si.id)) 368 + Ok(fzf::select::<_, Id, _>( 369 + stack, 370 + ["--delimiter=\t", "--accept-nth=1"], 371 + )?) 282 372 } 283 373 } 284 374 ··· 334 424 Ok(()) 335 425 } 336 426 427 + /// Returns a [`SearchTas`] which is plain task data with no file or attrs 337 428 fn bare(self) -> SearchTask { 338 429 SearchTask { 339 430 id: self.id, ··· 350 441 pub body: String, 351 442 } 352 443 353 - impl FromStr for SearchTask { 354 - type Err = Error; 355 - 356 - fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { 357 - let (tsk_id, task_content) = s.split_once('\t').ok_or(Error::Parse(format!( 358 - "Missing TSK-ID or content or task parse." 359 - )))?; 360 - let (title, body) = task_content 361 - .split_once('\t') 362 - .ok_or(Error::Parse(format!("Missing body for task parse.")))?; 363 - Ok(Self { 364 - id: tsk_id.parse()?, 365 - title: title.to_string(), 366 - body: body.to_string(), 367 - }) 368 - } 369 - } 370 - 371 444 impl Display for SearchTask { 372 445 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 373 - write!( 374 - f, 375 - "{}\t{}\t{}", 376 - self.id, 377 - self.title.trim(), 378 - self.body.replace('\n', " ").replace('\r', "") 379 - ) 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(()) 380 451 } 381 452 } 382 453 ··· 385 456 workspace: &'a Workspace, 386 457 } 387 458 388 - impl<'a> Iterator for LazyTaskLoader<'a> { 459 + impl Iterator for LazyTaskLoader<'_> { 389 460 type Item = SearchTask; 390 461 391 462 fn next(&mut self) -> Option<Self::Item> { ··· 398 469 } 399 470 } 400 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 + 401 490 #[cfg(test)] 402 491 mod test { 403 492 use super::*; ··· 407 496 let task = SearchTask { 408 497 id: Id(123), 409 498 title: "Hello, world".to_string(), 410 - body: "The body of the task.\nAnother line\r\nis here.".to_string(), 499 + body: "The body of the task.\nAnother line is here.".to_string(), 411 500 }; 412 501 assert_eq!( 413 - "tsk-123\tHello, world\tThe body of the task. Another line is here.", 502 + "tsk-123\tHello, world\n\nThe body of the task.\nAnother line is here.", 414 503 task.to_string() 415 504 ); 416 505 }