├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── assets └── usage.gif ├── cdwe.toml ├── shells ├── cdwe_bash.txt ├── cdwe_fish.txt └── cdwe_zsh.txt └── src ├── cache.rs ├── cmd ├── cmd.rs ├── init.rs ├── mod.rs ├── run.rs └── shell.rs ├── config.rs ├── main.rs └── utils.rs /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - '*' 5 | workflow_dispatch: 6 | 7 | name: Publish 8 | 9 | jobs: 10 | publish: 11 | name: Publish 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout sources 15 | uses: actions/checkout@v2 16 | 17 | - name: Install stable toolchain 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: stable 22 | override: true 23 | 24 | - run: cargo publish --token ${CRATES_API_KEY} 25 | env: 26 | CRATES_API_KEY: ${{ secrets.CRATES_API_KEY }} 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | name: Test 7 | 8 | jobs: 9 | test: 10 | name: Test 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout sources 14 | uses: actions/checkout@v2 15 | 16 | - name: Install stable toolchain 17 | uses: actions-rs/toolchain@v1 18 | with: 19 | profile: minimal 20 | toolchain: stable 21 | override: true 22 | 23 | - run: cargo test 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdbtarget 15 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.22.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.3.2" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "is-terminal", 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle" 46 | version = "1.0.1" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.1" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.0.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 64 | dependencies = [ 65 | "windows-sys 0.48.0", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "1.0.1" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" 73 | dependencies = [ 74 | "anstyle", 75 | "windows-sys 0.48.0", 76 | ] 77 | 78 | [[package]] 79 | name = "anyhow" 80 | version = "1.0.72" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" 83 | 84 | [[package]] 85 | name = "autocfg" 86 | version = "1.3.0" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 89 | 90 | [[package]] 91 | name = "backtrace" 92 | version = "0.3.72" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" 95 | dependencies = [ 96 | "addr2line", 97 | "cc", 98 | "cfg-if", 99 | "libc", 100 | "miniz_oxide", 101 | "object", 102 | "rustc-demangle", 103 | ] 104 | 105 | [[package]] 106 | name = "bitflags" 107 | version = "1.3.2" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 110 | 111 | [[package]] 112 | name = "bitflags" 113 | version = "2.3.3" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" 116 | 117 | [[package]] 118 | name = "block-buffer" 119 | version = "0.10.4" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 122 | dependencies = [ 123 | "generic-array", 124 | ] 125 | 126 | [[package]] 127 | name = "bytes" 128 | version = "1.6.0" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" 131 | 132 | [[package]] 133 | name = "cc" 134 | version = "1.0.98" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" 137 | 138 | [[package]] 139 | name = "cdwe" 140 | version = "0.3.0" 141 | dependencies = [ 142 | "anyhow", 143 | "clap", 144 | "regex", 145 | "serde", 146 | "serde_json", 147 | "sha2", 148 | "tokio", 149 | "toml", 150 | ] 151 | 152 | [[package]] 153 | name = "cfg-if" 154 | version = "1.0.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 157 | 158 | [[package]] 159 | name = "clap" 160 | version = "4.3.12" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "3eab9e8ceb9afdade1ab3f0fd8dbce5b1b2f468ad653baf10e771781b2b67b73" 163 | dependencies = [ 164 | "clap_builder", 165 | "clap_derive", 166 | "once_cell", 167 | ] 168 | 169 | [[package]] 170 | name = "clap_builder" 171 | version = "4.3.12" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "9f2763db829349bf00cfc06251268865ed4363b93a943174f638daf3ecdba2cd" 174 | dependencies = [ 175 | "anstream", 176 | "anstyle", 177 | "clap_lex", 178 | "strsim", 179 | ] 180 | 181 | [[package]] 182 | name = "clap_derive" 183 | version = "4.3.12" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" 186 | dependencies = [ 187 | "heck", 188 | "proc-macro2", 189 | "quote", 190 | "syn", 191 | ] 192 | 193 | [[package]] 194 | name = "clap_lex" 195 | version = "0.5.0" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" 198 | 199 | [[package]] 200 | name = "colorchoice" 201 | version = "1.0.0" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 204 | 205 | [[package]] 206 | name = "cpufeatures" 207 | version = "0.2.12" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" 210 | dependencies = [ 211 | "libc", 212 | ] 213 | 214 | [[package]] 215 | name = "crypto-common" 216 | version = "0.1.6" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 219 | dependencies = [ 220 | "generic-array", 221 | "typenum", 222 | ] 223 | 224 | [[package]] 225 | name = "digest" 226 | version = "0.10.7" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 229 | dependencies = [ 230 | "block-buffer", 231 | "crypto-common", 232 | ] 233 | 234 | [[package]] 235 | name = "equivalent" 236 | version = "1.0.1" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 239 | 240 | [[package]] 241 | name = "errno" 242 | version = "0.3.1" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" 245 | dependencies = [ 246 | "errno-dragonfly", 247 | "libc", 248 | "windows-sys 0.48.0", 249 | ] 250 | 251 | [[package]] 252 | name = "errno-dragonfly" 253 | version = "0.1.2" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 256 | dependencies = [ 257 | "cc", 258 | "libc", 259 | ] 260 | 261 | [[package]] 262 | name = "generic-array" 263 | version = "0.14.7" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 266 | dependencies = [ 267 | "typenum", 268 | "version_check", 269 | ] 270 | 271 | [[package]] 272 | name = "gimli" 273 | version = "0.29.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" 276 | 277 | [[package]] 278 | name = "hashbrown" 279 | version = "0.14.0" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" 282 | 283 | [[package]] 284 | name = "heck" 285 | version = "0.4.1" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 288 | 289 | [[package]] 290 | name = "hermit-abi" 291 | version = "0.3.2" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" 294 | 295 | [[package]] 296 | name = "indexmap" 297 | version = "2.0.0" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" 300 | dependencies = [ 301 | "equivalent", 302 | "hashbrown", 303 | ] 304 | 305 | [[package]] 306 | name = "is-terminal" 307 | version = "0.4.9" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" 310 | dependencies = [ 311 | "hermit-abi", 312 | "rustix", 313 | "windows-sys 0.48.0", 314 | ] 315 | 316 | [[package]] 317 | name = "itoa" 318 | version = "1.0.11" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 321 | 322 | [[package]] 323 | name = "libc" 324 | version = "0.2.155" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 327 | 328 | [[package]] 329 | name = "linux-raw-sys" 330 | version = "0.4.3" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" 333 | 334 | [[package]] 335 | name = "lock_api" 336 | version = "0.4.12" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 339 | dependencies = [ 340 | "autocfg", 341 | "scopeguard", 342 | ] 343 | 344 | [[package]] 345 | name = "memchr" 346 | version = "2.7.2" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 349 | 350 | [[package]] 351 | name = "miniz_oxide" 352 | version = "0.7.3" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" 355 | dependencies = [ 356 | "adler", 357 | ] 358 | 359 | [[package]] 360 | name = "mio" 361 | version = "0.8.11" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 364 | dependencies = [ 365 | "libc", 366 | "wasi", 367 | "windows-sys 0.48.0", 368 | ] 369 | 370 | [[package]] 371 | name = "num_cpus" 372 | version = "1.16.0" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 375 | dependencies = [ 376 | "hermit-abi", 377 | "libc", 378 | ] 379 | 380 | [[package]] 381 | name = "object" 382 | version = "0.35.0" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" 385 | dependencies = [ 386 | "memchr", 387 | ] 388 | 389 | [[package]] 390 | name = "once_cell" 391 | version = "1.18.0" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 394 | 395 | [[package]] 396 | name = "parking_lot" 397 | version = "0.12.1" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 400 | dependencies = [ 401 | "lock_api", 402 | "parking_lot_core", 403 | ] 404 | 405 | [[package]] 406 | name = "parking_lot_core" 407 | version = "0.9.9" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 410 | dependencies = [ 411 | "cfg-if", 412 | "libc", 413 | "redox_syscall", 414 | "smallvec", 415 | "windows-targets 0.48.1", 416 | ] 417 | 418 | [[package]] 419 | name = "pin-project-lite" 420 | version = "0.2.14" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 423 | 424 | [[package]] 425 | name = "proc-macro2" 426 | version = "1.0.84" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" 429 | dependencies = [ 430 | "unicode-ident", 431 | ] 432 | 433 | [[package]] 434 | name = "quote" 435 | version = "1.0.36" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 438 | dependencies = [ 439 | "proc-macro2", 440 | ] 441 | 442 | [[package]] 443 | name = "redox_syscall" 444 | version = "0.4.1" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 447 | dependencies = [ 448 | "bitflags 1.3.2", 449 | ] 450 | 451 | [[package]] 452 | name = "regex" 453 | version = "1.10.4" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" 456 | dependencies = [ 457 | "aho-corasick", 458 | "memchr", 459 | "regex-automata", 460 | "regex-syntax", 461 | ] 462 | 463 | [[package]] 464 | name = "regex-automata" 465 | version = "0.4.6" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 468 | dependencies = [ 469 | "aho-corasick", 470 | "memchr", 471 | "regex-syntax", 472 | ] 473 | 474 | [[package]] 475 | name = "regex-syntax" 476 | version = "0.8.3" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" 479 | 480 | [[package]] 481 | name = "rustc-demangle" 482 | version = "0.1.24" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 485 | 486 | [[package]] 487 | name = "rustix" 488 | version = "0.38.4" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" 491 | dependencies = [ 492 | "bitflags 2.3.3", 493 | "errno", 494 | "libc", 495 | "linux-raw-sys", 496 | "windows-sys 0.48.0", 497 | ] 498 | 499 | [[package]] 500 | name = "ryu" 501 | version = "1.0.18" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 504 | 505 | [[package]] 506 | name = "scopeguard" 507 | version = "1.2.0" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 510 | 511 | [[package]] 512 | name = "serde" 513 | version = "1.0.203" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" 516 | dependencies = [ 517 | "serde_derive", 518 | ] 519 | 520 | [[package]] 521 | name = "serde_derive" 522 | version = "1.0.203" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" 525 | dependencies = [ 526 | "proc-macro2", 527 | "quote", 528 | "syn", 529 | ] 530 | 531 | [[package]] 532 | name = "serde_json" 533 | version = "1.0.117" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" 536 | dependencies = [ 537 | "itoa", 538 | "ryu", 539 | "serde", 540 | ] 541 | 542 | [[package]] 543 | name = "serde_spanned" 544 | version = "0.6.3" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" 547 | dependencies = [ 548 | "serde", 549 | ] 550 | 551 | [[package]] 552 | name = "sha2" 553 | version = "0.10.8" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 556 | dependencies = [ 557 | "cfg-if", 558 | "cpufeatures", 559 | "digest", 560 | ] 561 | 562 | [[package]] 563 | name = "signal-hook-registry" 564 | version = "1.4.1" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 567 | dependencies = [ 568 | "libc", 569 | ] 570 | 571 | [[package]] 572 | name = "smallvec" 573 | version = "1.13.2" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 576 | 577 | [[package]] 578 | name = "socket2" 579 | version = "0.5.6" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" 582 | dependencies = [ 583 | "libc", 584 | "windows-sys 0.52.0", 585 | ] 586 | 587 | [[package]] 588 | name = "strsim" 589 | version = "0.10.0" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 592 | 593 | [[package]] 594 | name = "syn" 595 | version = "2.0.66" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" 598 | dependencies = [ 599 | "proc-macro2", 600 | "quote", 601 | "unicode-ident", 602 | ] 603 | 604 | [[package]] 605 | name = "tokio" 606 | version = "1.38.0" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" 609 | dependencies = [ 610 | "backtrace", 611 | "bytes", 612 | "libc", 613 | "mio", 614 | "num_cpus", 615 | "parking_lot", 616 | "pin-project-lite", 617 | "signal-hook-registry", 618 | "socket2", 619 | "tokio-macros", 620 | "windows-sys 0.48.0", 621 | ] 622 | 623 | [[package]] 624 | name = "tokio-macros" 625 | version = "2.3.0" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" 628 | dependencies = [ 629 | "proc-macro2", 630 | "quote", 631 | "syn", 632 | ] 633 | 634 | [[package]] 635 | name = "toml" 636 | version = "0.7.6" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" 639 | dependencies = [ 640 | "serde", 641 | "serde_spanned", 642 | "toml_datetime", 643 | "toml_edit", 644 | ] 645 | 646 | [[package]] 647 | name = "toml_datetime" 648 | version = "0.6.3" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" 651 | dependencies = [ 652 | "serde", 653 | ] 654 | 655 | [[package]] 656 | name = "toml_edit" 657 | version = "0.19.14" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" 660 | dependencies = [ 661 | "indexmap", 662 | "serde", 663 | "serde_spanned", 664 | "toml_datetime", 665 | "winnow", 666 | ] 667 | 668 | [[package]] 669 | name = "typenum" 670 | version = "1.17.0" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 673 | 674 | [[package]] 675 | name = "unicode-ident" 676 | version = "1.0.11" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" 679 | 680 | [[package]] 681 | name = "utf8parse" 682 | version = "0.2.1" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 685 | 686 | [[package]] 687 | name = "version_check" 688 | version = "0.9.4" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 691 | 692 | [[package]] 693 | name = "wasi" 694 | version = "0.11.0+wasi-snapshot-preview1" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 697 | 698 | [[package]] 699 | name = "windows-sys" 700 | version = "0.48.0" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 703 | dependencies = [ 704 | "windows-targets 0.48.1", 705 | ] 706 | 707 | [[package]] 708 | name = "windows-sys" 709 | version = "0.52.0" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 712 | dependencies = [ 713 | "windows-targets 0.52.5", 714 | ] 715 | 716 | [[package]] 717 | name = "windows-targets" 718 | version = "0.48.1" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" 721 | dependencies = [ 722 | "windows_aarch64_gnullvm 0.48.0", 723 | "windows_aarch64_msvc 0.48.0", 724 | "windows_i686_gnu 0.48.0", 725 | "windows_i686_msvc 0.48.0", 726 | "windows_x86_64_gnu 0.48.0", 727 | "windows_x86_64_gnullvm 0.48.0", 728 | "windows_x86_64_msvc 0.48.0", 729 | ] 730 | 731 | [[package]] 732 | name = "windows-targets" 733 | version = "0.52.5" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 736 | dependencies = [ 737 | "windows_aarch64_gnullvm 0.52.5", 738 | "windows_aarch64_msvc 0.52.5", 739 | "windows_i686_gnu 0.52.5", 740 | "windows_i686_gnullvm", 741 | "windows_i686_msvc 0.52.5", 742 | "windows_x86_64_gnu 0.52.5", 743 | "windows_x86_64_gnullvm 0.52.5", 744 | "windows_x86_64_msvc 0.52.5", 745 | ] 746 | 747 | [[package]] 748 | name = "windows_aarch64_gnullvm" 749 | version = "0.48.0" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 752 | 753 | [[package]] 754 | name = "windows_aarch64_gnullvm" 755 | version = "0.52.5" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 758 | 759 | [[package]] 760 | name = "windows_aarch64_msvc" 761 | version = "0.48.0" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 764 | 765 | [[package]] 766 | name = "windows_aarch64_msvc" 767 | version = "0.52.5" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 770 | 771 | [[package]] 772 | name = "windows_i686_gnu" 773 | version = "0.48.0" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 776 | 777 | [[package]] 778 | name = "windows_i686_gnu" 779 | version = "0.52.5" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 782 | 783 | [[package]] 784 | name = "windows_i686_gnullvm" 785 | version = "0.52.5" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 788 | 789 | [[package]] 790 | name = "windows_i686_msvc" 791 | version = "0.48.0" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 794 | 795 | [[package]] 796 | name = "windows_i686_msvc" 797 | version = "0.52.5" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 800 | 801 | [[package]] 802 | name = "windows_x86_64_gnu" 803 | version = "0.48.0" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 806 | 807 | [[package]] 808 | name = "windows_x86_64_gnu" 809 | version = "0.52.5" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 812 | 813 | [[package]] 814 | name = "windows_x86_64_gnullvm" 815 | version = "0.48.0" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 818 | 819 | [[package]] 820 | name = "windows_x86_64_gnullvm" 821 | version = "0.52.5" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 824 | 825 | [[package]] 826 | name = "windows_x86_64_msvc" 827 | version = "0.48.0" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 830 | 831 | [[package]] 832 | name = "windows_x86_64_msvc" 833 | version = "0.52.5" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 836 | 837 | [[package]] 838 | name = "winnow" 839 | version = "0.5.0" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7" 842 | dependencies = [ 843 | "memchr", 844 | ] 845 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cdwe" 3 | description = "cd with env vars" 4 | version = "0.3.0" 5 | edition = "2021" 6 | authors = ["synoet"] 7 | include = ["src/**/*.rs", "shells/*"] 8 | categories = ["command-line-utilities"] 9 | homepage = "https://github.com/synoet/cdwe" 10 | repository = "https://github.com/synoet/cdwe" 11 | license = "MIT" 12 | readme = "README.md" 13 | keywords = [ 14 | "cli", 15 | "command", 16 | "command-line", 17 | "shell", 18 | "tool", 19 | ] 20 | 21 | [dependencies] 22 | anyhow = "1.0.72" 23 | clap = { version="4.3.12", features=["derive"] } 24 | regex = "1.10.4" 25 | serde = {version = "1.0.171", features = ["derive"]} 26 | serde_json = "1.0.117" 27 | sha2 = "0.10.8" 28 | tokio = { version = "1.38.0", features = ["tokio-macros", "rt", "full"] } 29 | toml = "0.7.6" 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # ⚡️cdwe (cd with env) 4 | A simple configurable cd wrapper that provides powerful utilities for customizing your envionment per directory. \ 5 | *(For **ZSH** / **BASH** / **FISH** Shells)* 6 | 7 | 8 | [Installation](#installation) • 9 | [Usage](#usage) • 10 | [Configuration](#configuration) • 11 | [Uninstalling](#uninstalling) 12 |
13 | 14 | ![usage](./assets/usage.gif) 15 | 16 | ## Features 17 | 18 | - **Per Directory Environment Variables** 19 | - **Auto Load .env files in Directories** 20 | - **Auto Execute Commands in Directories** 21 | - **Per Directory Aliases** 22 | - **Works with any CD like Command** 23 | 24 | ## Installation 25 | 26 | 1. **Install binary** 27 | ```bash 28 | cargo install cdwe 29 | ``` 30 | 31 | 2. **Init your shell** 32 | ```bash 33 | cdwe init zsh # zsh shells 34 | cdwe init bash # bash shells 35 | cdwe init fish # fish shells 36 | ``` 37 | 38 | 3. **Reload your shell and start using!** 39 | ```bash 40 | # check that env var gets set 41 | cdwe /Users/synoet/dev/projecta 42 | echo $IS_DEBUG 43 | 44 | # check that env var gets unset 45 | cdwe .. 46 | echo $IS_DEBUG 47 | ``` 48 | 49 | ## Usage 50 | 51 | ### Defining Per Directory Env Variables 52 | --- 53 | 54 | You can explicitly define environment variables in two ways: 55 | ```toml 56 | [[directory]] 57 | path = "/Users/synoet/dev/project" 58 | vars = {"IS_DEBUG" = "true", "IS_PROD" = "false"} 59 | 60 | # or 61 | 62 | [[directory]] 63 | path = "/Users/synoet/dev/project" 64 | vars = [ 65 | {name="IS_DEBUG", value ="true"}, 66 | {name="IS_PROD", value="false"} 67 | ] 68 | ``` 69 | `path`: the path to your directory you are configuring 70 | 71 | `vars`: a map of env vars to set 72 | 73 | *By default env vars will also be loaded in subdirectories, in this example `/Users/synoet/dev/project/src` would also have `IS_DEBUG` and `IS_PROD` set* 74 | 75 | *OR* 76 | 77 | 78 | ```toml 79 | [[env_variable]] 80 | name = "IS_DEBUG" 81 | value = "true" 82 | dirs = [ 83 | "/Users/synoet/dev/project1", 84 | "/Users/synoet/dev/project2" 85 | ] 86 | ``` 87 | Here you can define one env var for multiple directories. 88 | 89 | `name`: Is the key of the env variable 90 | 91 | `value`: is the value of the env variable 92 | 93 | `dirs`: Is a list of directories to load this env var for 94 | 95 | ### Loading From .env files 96 | --- 97 | The directory object also takes a `load_from` field 98 | ```toml 99 | [[directory]] 100 | path = "/Users/synoet/dev/project" 101 | vars = {"IS_DEBUG" = "true", "IS_PROD" = "false"} 102 | load_from = [".env"] 103 | ``` 104 | `load_from`: List of .env file names to auto load in, these should be relative to the dir defined in path. 105 | 106 | In this example we would try to load in a env file at `/Users/synoet/dev/project/.env` 107 | 108 | *Unlike per directory env vars, env files are only loaded in the exact matching directory not in subdirectories* 109 | 110 | **OR** 111 | 112 | Similarly we can define a single env file for multiple directories 113 | 114 | ```toml 115 | [[env_file]] 116 | load_from = ".env" 117 | dirs = [ 118 | "/Users/synoet/dev/macro/macro-site/astro", 119 | "/Users/synoet/dev/macro/app-monorepo/packages/app" 120 | ] 121 | ``` 122 | 123 | ### Defining Aliases Per Directory 124 | --- 125 | Here we can define aliases that will be set and unset as functions only in specific directories 126 | 127 | ```toml 128 | [[directory]] 129 | path = "/Users/synoet/dev/project" 130 | vars = {"IS_DEBUG" = "true", "IS_PROD" = "false"} 131 | load_from = [".env"] 132 | aliases = [ 133 | { name = "build", commands = ["yarn cache clean", "yarn build", "yarn package"] } 134 | ] 135 | ``` 136 | 137 | Here we define a `build` alias which will live only in `/User/synoet/dev/project/*` and all subdirectories. 138 | 139 | `aliases`: a list of aliases to define for the directory 140 | 141 | **OR** 142 | ```toml 143 | [[alias]] 144 | name = "build" 145 | commands = ["yarn cache clean", "yarn build", "yarn package"] 146 | dirs = [ 147 | "/Users/synoet/dev/projecta", 148 | "/Users/synoet/dev/projectb" 149 | ] 150 | ``` 151 | Here you are defining the same alias for multiple directories. 152 | 153 | ### Defining Auto Commands 154 | --- 155 | Here we can define commands that will automatically run anytime we cd into a specific directory 156 | ```toml 157 | [[directory]] 158 | path = "/Users/synoet/dev/project" 159 | vars = {"IS_DEBUG" = "true", "IS_PROD" = "false"} 160 | load_from = [".env"] 161 | aliases = [ 162 | { name = "build", commands = ["yarn cache clean", "yarn build", "yarn package"] } 163 | ] 164 | run = ["git fetch -p", "ls"] 165 | ``` 166 | In this case every time we enter `/Users/synoet/dev/project` cdwe will automatically run `git fetch -p` and `ls` 167 | 168 | *Auto Commands also require an exact match and don't propogate to subdirectories* 169 | 170 | **OR** 171 | 172 | ```toml 173 | [[command]] 174 | run = "git fetch -p" 175 | dirs = [ 176 | "/Users/synoet/dev/cdwe", 177 | "/Users/synoet/dev/macro/macro-api" 178 | ] 179 | ``` 180 | 181 | ## Configuration 182 | ### Global Configuration Options 183 | ```toml 184 | [config] 185 | # Shell (Created during cdwe init ) 186 | shell = "zsh" 187 | # Custom CD Command (defaults to cd) 188 | cd_command = "z" 189 | # Show alias hints on cd 190 | alias_hints = true 191 | # Show env hints on cd 192 | env_hints = true 193 | # shoe run hints on cd 194 | run_hints = true 195 | ``` 196 | 197 | ### Example Configuration 198 | ```toml 199 | [config] 200 | cd_command = "z" 201 | alias_hints = true 202 | env_hints = true 203 | command_hints = true 204 | run_hints = true 205 | shell = "zsh" 206 | 207 | # Defined a directory 208 | # Will have env var "TEST" set in this directory 209 | # Will auto run "git fetch -p" whenever you cd into this dir 210 | # Exposes the following aliases in that directory and sub dirs 211 | [[directory]] 212 | path = "/Users/synoet/dev/cdwe" 213 | vars = { "TEST" = "testing" } 214 | runs = ["git fetch -p"] 215 | aliases = [ 216 | { name = "build", commands = ["cargo build --release"]}, 217 | { name = "run", commands = ["cargo run"]}, 218 | { name = "ci", commands = ["cargo fmt", "cargo test"]} 219 | ] 220 | 221 | # sets the "ENV_VAR" env var in the following directories 222 | [[env_variable]] 223 | name = "ENV_VAR" 224 | value = "THIS IS A TEST" 225 | dirs = [ 226 | "/Users/synoet/dev/cdwe", 227 | "/Users/synoet/dev/ballast" 228 | ] 229 | 230 | # auto loads from .env file in following directories 231 | [[env_file]] 232 | load_from = ".env" 233 | dirs = [ 234 | "/Users/synoet/dev/cdwe", 235 | "/Users/synoet/dev/project-api" 236 | ] 237 | 238 | # will auto run the command "git fetch -p" in the following directories 239 | [[command]] 240 | run = "git fetch -p" 241 | dirs = [ 242 | "/Users/synoet/dev/cdwe", 243 | "/Users/synoet/dev/project-api" 244 | ] 245 | ``` 246 | 247 | ### Using CDWE Environment Variables in the paths 248 | 249 | If you want to use something like **$HOME** or any other environment variable 250 | set in a path, you can do so by wrapping the environment variable inside of 251 | `{{}}`. Using **$HOME** as an example, you would do {{HOME}}. This can make 252 | using one cdwe.toml across many machines nice if you have different users but a 253 | similar directory structure for each user. 254 | 255 | ## Uninstalling 256 | 1. Run cdwe-remove to clean up all shell artifacts 257 | ```bash 258 | cdwe-remove #removes the `source ` from your .zshrc/.bashrc/.fish 259 | 260 | zsh #reload your shell, use bash or fish if you use those. 261 | ``` 262 | 263 | 2. Uninstall binary 264 | ```bash 265 | cargo uninstall cdwe 266 | ``` 267 | 268 | -------------------------------------------------------------------------------- /assets/usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synoet/cdwe/27c50992e3bc8d4297dbe743c3636abaa55095b8/assets/usage.gif -------------------------------------------------------------------------------- /cdwe.toml: -------------------------------------------------------------------------------- 1 | variables = [ 2 | {name = "name", value = "world"}, 3 | ] 4 | 5 | aliases = [ 6 | {name = "hello", commands = ["echo Hello, {{name}}!"]}, 7 | ] 8 | 9 | commands = ["git fetch -p"] 10 | 11 | 12 | -------------------------------------------------------------------------------- /shells/cdwe_bash.txt: -------------------------------------------------------------------------------- 1 | function cdwe() { 2 | local old_dir="$PWD" 3 | 4 | {{{cd_command}}} "$@" || return 5 | 6 | local new_dir="$PWD" 7 | 8 | local result 9 | result="$({{{exec_path}}} run --old_dir="$old_dir" --new_dir="$new_dir")" 10 | eval "${result}" 11 | } 12 | 13 | cdwe_on_load() { 14 | current_dir="$PWD" 15 | result="$({{{exec_path}}} run --old_dir="$current_dir" --new_dir="$current_dir")" 16 | eval "${result}" 17 | } 18 | cdwe_on_load 19 | 20 | function cdwe-reload () { 21 | {{{exec_path}}} reload bash 22 | bash 23 | } 24 | 25 | function cdwe-remove() { 26 | {{{exec_path}}} remove bash 27 | bash 28 | } 29 | -------------------------------------------------------------------------------- /shells/cdwe_fish.txt: -------------------------------------------------------------------------------- 1 | function cdwe 2 | set old_dir $PWD 3 | 4 | {{{cd_command}}} $argv; or return 5 | 6 | set new_dir $PWD 7 | 8 | set result ({{{exec_path}}} run --old_dir="$old_dir" --new_dir="$new_dir") 9 | eval $result 10 | end 11 | 12 | function cdwe_on_load 13 | set current_dir (pwd) 14 | set result ({{{exec_path}}} run --old_dir="$current_dir" --new_dir="$current_dir") 15 | eval $result 16 | end 17 | 18 | cdwe_on_load 19 | 20 | function cdwe-remove 21 | ({{{exec_path}}} remove fish) 22 | fish 23 | end 24 | 25 | function cdwe-reload 26 | ({{{exec_path}}} reload fish) 27 | fish 28 | end 29 | 30 | 31 | -------------------------------------------------------------------------------- /shells/cdwe_zsh.txt: -------------------------------------------------------------------------------- 1 | function cdwe() { 2 | local old_dir="$PWD" 3 | 4 | {{{cd_command}}} "$@" || return 5 | 6 | local new_dir="$PWD" 7 | 8 | local result 9 | result="$({{{exec_path}}} run --old_dir="$old_dir" --new_dir="$new_dir")" 10 | eval "${result}" 11 | } 12 | 13 | _cdwe_on_load() { 14 | current_dir="$PWD" 15 | result="$({{{exec_path}}} run --old_dir="$current_dir" --new_dir="$current_dir")" 16 | eval "${result}" 17 | } 18 | 19 | _cdwe_on_load 20 | 21 | function cdwe-reload () { 22 | {{{exec_path}}} reload zsh 23 | zsh 24 | } 25 | 26 | function cdwe-remove() { 27 | {{{exec_path}}} remove zsh 28 | zsh 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/cache.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{Config, EnvAlias, EnvVariable, EnvVariableStruct}; 2 | use anyhow::{Context, Result}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::collections::HashMap; 5 | 6 | pub type DirCacheMap = HashMap; 7 | 8 | #[derive(Serialize, Deserialize)] 9 | pub struct DirCache { 10 | pub variables: Vec, 11 | pub run: Vec, 12 | pub aliases: Vec, 13 | pub load_from: Vec, 14 | } 15 | 16 | /// Cache is optimized for speed of lookup 17 | /// Config is optimized for readability and usability for the user 18 | /// Cache is stored in a json file ussually ~/.cdwe_cache.json 19 | #[derive(Serialize, Deserialize)] 20 | pub struct Cache { 21 | pub shell: String, 22 | pub hash: String, 23 | values: DirCacheMap, 24 | } 25 | 26 | /// Inserts any cdwe path environment variables into itself and returns 27 | /// updated path 28 | fn insert_env_var_into_path(re: ®ex::Regex, path: &str) -> String { 29 | re.replace_all(path, |caps: ®ex::Captures| { 30 | if let Some(value) = std::env::var(&caps[1]).ok() { 31 | value // If the env var exists, replace with its value 32 | } else { 33 | caps[0].to_string() // If not, keep the original text 34 | } 35 | }) 36 | .to_string() 37 | } 38 | 39 | impl Cache { 40 | pub fn new(shell: String, hash: String, values: DirCacheMap) -> Self { 41 | Cache { 42 | shell, 43 | hash, 44 | values, 45 | } 46 | } 47 | 48 | pub fn from_config(config: &Config, config_hash: &str) -> Self { 49 | let mut values: DirCacheMap = HashMap::new(); 50 | 51 | // Captures the content within {{}} 52 | let re = regex::Regex::new(r"\{\{(.*?)\}\}").unwrap(); 53 | 54 | for directory in &config.directories { 55 | let variables: Vec = match &directory.vars { 56 | Some(EnvVariableStruct::HashMap(hash_map)) => hash_map 57 | .iter() 58 | .map(|(name, value)| EnvVariable { 59 | name: name.clone(), 60 | value: value.clone(), 61 | }) 62 | .collect(), 63 | Some(EnvVariableStruct::EnvVariableVec(dir_env_variable)) => { 64 | dir_env_variable.to_vec() 65 | } 66 | None => vec![], 67 | }; 68 | 69 | let mut aliases: Vec = vec![]; 70 | 71 | if let Some(dir_aliases) = &directory.aliases { 72 | aliases.extend(dir_aliases.clone()); 73 | } 74 | 75 | let load_from = directory.load_from.clone().unwrap_or(vec![]); 76 | 77 | let run = directory.run.clone().unwrap_or(vec![]); 78 | 79 | let dir_cache: DirCache = DirCache { 80 | variables, 81 | run, 82 | load_from: load_from.clone(), 83 | aliases, 84 | }; 85 | 86 | let result = insert_env_var_into_path(&re, directory.path.as_str()); 87 | values.insert(result, dir_cache); 88 | } 89 | 90 | let shell = match &config.config { 91 | Some(global_config) => global_config.shell.clone().unwrap_or("bash".to_string()), 92 | None => "bash".to_string(), 93 | }; 94 | 95 | Cache::new(shell, config_hash.to_string(), values) 96 | } 97 | 98 | pub fn get(&self, path: &str) -> Option<&DirCache> { 99 | self.values.get(path) 100 | } 101 | } 102 | 103 | /// If a cache doesn't exist create one 104 | /// If a cache exists but the config has changed we create a new cache 105 | /// Returns the cache and a boolean indicating if the cache was created 106 | pub fn get_or_create_cache( 107 | cache_content: Option<&str>, 108 | config_content: &str, 109 | config_hash: &str, 110 | ) -> Result<(Cache, bool)> { 111 | if let Some(cache_content) = cache_content { 112 | let previous_cache: Cache = serde_json::from_str(cache_content)?; 113 | 114 | if previous_cache.hash == config_hash { 115 | return Ok((previous_cache, false)); 116 | } 117 | } 118 | 119 | let config = Config::from_str(config_content).context("failed to parse config")?; 120 | 121 | Ok((Cache::from_config(&config, config_hash), true)) 122 | } 123 | 124 | pub fn write_cache(cache: &Cache, home: &str) -> Result<()> { 125 | let cache_content = serde_json::to_string(cache)?; 126 | let home = home.to_string(); 127 | tokio::spawn(async move { 128 | std::fs::write( 129 | home.to_string() + "/.cdwe_cache.json", 130 | cache_content.as_bytes(), 131 | ) 132 | }); 133 | 134 | Ok(()) 135 | } 136 | 137 | #[cfg(test)] 138 | mod tests { 139 | use super::insert_env_var_into_path; 140 | 141 | #[test] 142 | fn test_insert_env_var_into_path() { 143 | let re = regex::Regex::new(r"\{\{(.*?)\}\}").unwrap(); 144 | std::env::set_var("TEST_HOME", "/home/user"); 145 | std::env::set_var("TEST_NAME", "testing"); 146 | assert_eq!( 147 | insert_env_var_into_path(&re, "{{TEST_HOME}}/testing"), 148 | "/home/user/testing" 149 | ); 150 | assert_eq!( 151 | insert_env_var_into_path(&re, "{{TEST_HOME}}/{{TEST_NAME}}"), 152 | "/home/user/testing" 153 | ); 154 | assert_eq!( 155 | insert_env_var_into_path(&re, "{{DOES_NOT_EXIST}}/{{TEST_NAME}}"), 156 | "{{DOES_NOT_EXIST}}/testing" 157 | ); 158 | assert_eq!( 159 | insert_env_var_into_path(&re, "/home/user/testing"), 160 | "/home/user/testing" 161 | ); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/cmd/cmd.rs: -------------------------------------------------------------------------------- 1 | use crate::cmd::shell::Shell; 2 | use clap::{command, Parser, Subcommand}; 3 | 4 | #[derive(Debug, Parser)] 5 | pub struct Cli { 6 | #[command(subcommand)] 7 | pub command: Commands, 8 | } 9 | 10 | #[derive(Debug, Subcommand)] 11 | pub enum Commands { 12 | #[command(arg_required_else_help = true)] 13 | Run { 14 | #[arg(long = "old_dir", required = true)] 15 | old_dir: String, 16 | #[arg(long = "new_dir", required = true)] 17 | new_dir: String, 18 | }, 19 | #[command(arg_required_else_help = true)] 20 | Init { 21 | #[arg(value_name = "SHELL", required = true)] 22 | shell: Option, 23 | }, 24 | Reload { 25 | #[arg(value_name = "SHELL", required = true)] 26 | shell: Option, 27 | }, 28 | Remove { 29 | #[arg(value_name = "SHELL", required = true)] 30 | shell: Option, 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /src/cmd/init.rs: -------------------------------------------------------------------------------- 1 | use super::super::config::Config; 2 | use super::Shell; 3 | use anyhow::{Context, Result}; 4 | use std::path::Path; 5 | 6 | pub fn init_shell(config: Option, shell: Shell) -> Result<()> { 7 | let home_var = std::env::var("HOME").context("no $HOME set")?; 8 | let home = Path::new(&home_var); 9 | let toml_path = std::path::Path::join(home, "cdwe.toml") 10 | .to_str() 11 | .context("failed to get toml path")? 12 | .to_string(); 13 | 14 | let config_path = shell.get_config_path()?; 15 | let mut shell_script = shell.get_shell_script(); 16 | let shell_script_target = shell.get_shell_script_target()?; 17 | let exe_path = std::env::current_exe().context("failed to get cdwe executable path")?; 18 | shell_script = shell_script.replace( 19 | "{{{exec_path}}}", 20 | exe_path 21 | .to_str() 22 | .context("failed to convert path to string")?, 23 | ); 24 | 25 | let cd_command = match config.and_then(|c| c.config) { 26 | Some(global_config) => global_config 27 | .cd_command 28 | .unwrap_or_else(|| shell.get_default_command().to_string()), 29 | None => shell.get_default_command().to_string(), 30 | }; 31 | 32 | shell_script = shell_script.replace("{{{cd_command}}}", &cd_command); 33 | 34 | std::fs::write(&shell_script_target, shell_script)?; 35 | 36 | let toml_content: String = std::fs::read_to_string(&toml_path).unwrap_or("".to_string()); 37 | 38 | if toml_content.is_empty() { 39 | let default_config = Config::default_for_shell(shell); 40 | std::fs::write(&toml_path, toml::to_string(&default_config)?) 41 | .context("failed to write default config")?; 42 | } 43 | 44 | let source_string = format!( 45 | "if [ -f '{}' ]; then . '{}'; fi", 46 | &shell_script_target, &shell_script_target 47 | ); 48 | 49 | let mut config = std::fs::read_to_string(&config_path) 50 | .with_context(|| format!("failed to read config path {}", config_path))?; 51 | if !config.contains(&source_string) { 52 | config.push_str(&format!("\n{}", source_string)); 53 | std::fs::write(&config_path, config) 54 | .with_context(|| format!("failed to write to config path {}", config_path))?; 55 | } 56 | Ok(()) 57 | } 58 | 59 | pub fn remove_shell(shell: Shell) -> Result<()> { 60 | let shell_script_target = shell.get_shell_script_target()?; 61 | let config_path = shell.get_config_path()?; 62 | let source_string = format!( 63 | "if [ -f '{}' ]; then . '{}'; fi", 64 | &shell_script_target, &shell_script_target 65 | ); 66 | let mut config = std::fs::read_to_string(&config_path) 67 | .with_context(|| format!("failed to read config path {}", config_path))?; 68 | 69 | if config.contains(&source_string) { 70 | config = config.replace(&source_string, ""); 71 | std::fs::write(&config_path, config) 72 | .with_context(|| format!("Failed to write to {}", &config_path))?; 73 | } 74 | 75 | std::fs::remove_file(&shell_script_target) 76 | .with_context(|| format!("failed to remove config file {}", &shell_script_target))?; 77 | 78 | Ok(()) 79 | } 80 | -------------------------------------------------------------------------------- /src/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | mod cmd; 2 | mod init; 3 | mod run; 4 | mod shell; 5 | 6 | pub use cmd::{Cli, Commands}; 7 | pub use init::{init_shell, remove_shell}; 8 | pub use run::{run, run_local}; 9 | pub use shell::Shell; 10 | -------------------------------------------------------------------------------- /src/cmd/run.rs: -------------------------------------------------------------------------------- 1 | use super::Shell; 2 | use crate::cache::{Cache, DirCache}; 3 | use crate::config::{EnvAlias, EnvVariable, LocalConfig}; 4 | use crate::utils::trim_quotes; 5 | use anyhow::{anyhow, Result}; 6 | use std::path::Path; 7 | 8 | /// Parses the content of an .env file with the following structure 9 | /// ``` 10 | /// SOME_VAR=test 11 | /// ANOTHER_VAR="test" 12 | /// ```` 13 | /// 14 | /// Supports values with or without quotes 15 | fn parse_env_file(content: &str, file_name: &str) -> Result> { 16 | let lines = content 17 | .lines() 18 | .filter(|line| !line.contains('#') && !line.trim().is_empty()); 19 | 20 | let mut vars = vec![]; 21 | 22 | for (index, line) in lines.enumerate() { 23 | let split = line 24 | .split_once('=') 25 | .ok_or_else(|| anyhow!("Invalid line in file: {}:{}: {}", file_name, index, line))?; 26 | 27 | let key = trim_quotes(split.0); 28 | let value = trim_quotes(split.1); 29 | 30 | vars.push(EnvVariable { 31 | name: key.to_string(), 32 | value: value.to_string(), 33 | }); 34 | } 35 | 36 | Ok(vars) 37 | } 38 | 39 | fn get_vars_from_env_file(base_path: &str, file_path: &str) -> Option> { 40 | let env_path = Path::new(&base_path).join(file_path); 41 | if let Ok(content) = std::fs::read_to_string(&env_path) { 42 | match parse_env_file(&content, &env_path.to_string_lossy()) { 43 | Ok(vars) => Some(vars), 44 | Err(_) => None, 45 | } 46 | } else { 47 | None 48 | } 49 | } 50 | 51 | /// Given a cache unsets the environment variables for the old directory 52 | /// variables are taken from the dir and from any .env files specified in the config 53 | pub fn unset_variables( 54 | variables: &Vec, 55 | load_from: Option<&Vec>, 56 | path: Option<&str>, 57 | ) { 58 | for var in variables.iter() { 59 | println!("unset {}", var.name); 60 | } 61 | 62 | // Unload variables from .env files specified in config 63 | // for the old directory 64 | 65 | if let (Some(path), Some(load_from)) = (path, load_from) { 66 | for file in load_from { 67 | let vars = get_vars_from_env_file(path, file); 68 | if let Some(vars) = vars { 69 | for var in vars { 70 | println!("unset {}", var.name); 71 | } 72 | } 73 | } 74 | } 75 | } 76 | 77 | /// Given a cache sets the environment variables for the new directory 78 | /// variables are taken from the dir and from any .enf files specified in the config 79 | pub fn set_variables( 80 | variables: &Vec, 81 | load_from: Option<&Vec>, 82 | path: Option<&str>, 83 | ) { 84 | for var in variables { 85 | println!("export {}=\"{}\"", var.name, var.value); 86 | } 87 | 88 | if let (Some(path), Some(load_from)) = (path, load_from) { 89 | // Load variables from .env files specified in config 90 | for file in load_from { 91 | let vars = get_vars_from_env_file(path, &file); 92 | if let Some(vars) = vars { 93 | for var in vars { 94 | println!("export {}=\"{}\"", var.name, var.value); 95 | } 96 | } 97 | } 98 | } 99 | } 100 | 101 | pub fn set_aliases(aliases: &Vec, shell: &str) -> Result<()> { 102 | let (start_str, end_str) = Shell::from_string(shell)?.get_alias_command(); 103 | for alias in aliases.iter() { 104 | let mut alias_string = start_str.clone().replace("{{{alias_name}}}", &alias.name); 105 | for cmd in &alias.commands { 106 | alias_string.push_str(&format!("{}\n", cmd)); 107 | } 108 | 109 | println!("{}\n{}\n", &alias_string, &end_str); 110 | } 111 | 112 | Ok(()) 113 | } 114 | 115 | pub fn unset_aliases(aliases: &Vec) { 116 | for alias in aliases.iter() { 117 | println!("unset -f {} &> /dev/null", alias.name); 118 | } 119 | } 120 | 121 | pub fn run_commands(commands: &Vec) { 122 | for command in commands.iter() { 123 | println!("{}", command); 124 | } 125 | } 126 | 127 | pub fn run(cache: &Cache, old_path: String, new_path: String) -> Result<()> { 128 | let old_dir: Option<&DirCache> = cache.get(&old_path); 129 | let new_dir: Option<&DirCache> = cache.get(&new_path); 130 | 131 | if old_dir.is_none() && new_dir.is_none() { 132 | return Ok(()); 133 | } 134 | 135 | // Unset old environment variables 136 | if let Some(old_dir) = old_dir { 137 | unset_variables( 138 | &old_dir.variables, 139 | Some(&old_dir.load_from), 140 | Some(&old_path), 141 | ); 142 | unset_aliases(&old_dir.aliases); 143 | } 144 | 145 | if let Some(new_dir) = new_dir { 146 | set_variables( 147 | &new_dir.variables, 148 | Some(&new_dir.load_from), 149 | Some(&new_path), 150 | ); 151 | set_aliases(&new_dir.aliases, &cache.shell)?; 152 | run_commands(&new_dir.run); 153 | } 154 | 155 | Ok(()) 156 | } 157 | 158 | pub fn run_local( 159 | old_local_config: Option<&LocalConfig>, 160 | new_local_config: Option<&LocalConfig>, 161 | shell: &str, 162 | ) -> Result<()> { 163 | if old_local_config.is_none() && new_local_config.is_none() { 164 | return Ok(()); 165 | } 166 | 167 | if let Some(old_local_config) = old_local_config { 168 | if let Some(vars) = &old_local_config.variables { 169 | unset_variables(vars, None, None); 170 | } 171 | 172 | if let Some(aliases) = &old_local_config.aliases { 173 | unset_aliases(aliases); 174 | } 175 | } 176 | 177 | if let Some(new_local_config) = new_local_config { 178 | if let Some(vars) = &new_local_config.variables { 179 | set_variables(vars, None, None); 180 | } 181 | 182 | if let Some(aliases) = &new_local_config.aliases { 183 | // This might be the only error worth reporting to the user 184 | // since it means that they have misconfigured cdwe 185 | if let Err(err) = set_aliases(aliases, shell) { 186 | eprintln!("ERROR: misconfigured shell { }", err); 187 | } 188 | } 189 | 190 | if let Some(commands) = &new_local_config.commands { 191 | run_commands(commands); 192 | } 193 | } 194 | 195 | return Ok(()); 196 | } 197 | 198 | #[cfg(test)] 199 | mod tests { 200 | #[test] 201 | fn test_parse_env_file() { 202 | use super::parse_env_file; 203 | use crate::config::EnvVariable; 204 | 205 | let test_content = "\ 206 | # THIS IS A TEST COMMMENT\n\ 207 | TEST_VAR=true\n\ 208 | ANOTHER_VAR=123\n\ 209 | QUOTED_VAR=\"test\"\n\ 210 | # ANOTHER TEST COMMENT\n\ 211 | SINGLE_QUOTED_VAR='test'\n\ 212 | ANOTHER_VAR=hello world this is a test\n\ 213 | "; 214 | 215 | let expected: Vec = vec![ 216 | EnvVariable { 217 | name: "TEST_VAR".to_string(), 218 | value: "true".to_string(), 219 | }, 220 | EnvVariable { 221 | name: "ANOTHER_VAR".to_string(), 222 | value: "123".to_string(), 223 | }, 224 | EnvVariable { 225 | name: "QUOTED_VAR".to_string(), 226 | value: "test".to_string(), 227 | }, 228 | EnvVariable { 229 | name: "SINGLE_QUOTED_VAR".to_string(), 230 | value: "test".to_string(), 231 | }, 232 | EnvVariable { 233 | name: "ANOTHER_VAR".to_string(), 234 | value: "hello world this is a test".to_string(), 235 | }, 236 | ]; 237 | 238 | assert_eq!(parse_env_file(test_content, "/.env").unwrap(), expected); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/cmd/shell.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use clap::ValueEnum; 3 | use std::path::Path; 4 | 5 | #[derive(Debug, ValueEnum, Clone)] 6 | pub enum Shell { 7 | Bash, 8 | Fish, 9 | Zsh, 10 | } 11 | 12 | impl ToString for Shell { 13 | fn to_string(&self) -> String { 14 | match self { 15 | Shell::Bash => "bash".to_string(), 16 | Shell::Fish => "fish".to_string(), 17 | Shell::Zsh => "zsh".to_string(), 18 | } 19 | } 20 | } 21 | 22 | impl Shell { 23 | pub fn get_config_path(&self) -> Result { 24 | let home_var = std::env::var("HOME").context("no $HOME set")?; 25 | let home = Path::new(&home_var); 26 | match self { 27 | Shell::Bash => Ok(std::path::Path::join(home, ".bashrc") 28 | .to_str() 29 | .context("failed to get bash config path")? 30 | .to_string()), 31 | Shell::Fish => Ok(std::path::Path::join(home, "/config/fish/config.fish") 32 | .to_str() 33 | .context("failed to get fish config path")? 34 | .to_string()), 35 | Shell::Zsh => Ok(std::path::Path::join(home, ".zshrc") 36 | .to_str() 37 | .context("failed to get zsh config path")? 38 | .to_string()), 39 | } 40 | } 41 | 42 | pub fn from_string(s: &str) -> Result { 43 | match s { 44 | "bash" => Ok(Shell::Bash), 45 | "fish" => Ok(Shell::Fish), 46 | "zsh" => Ok(Shell::Zsh), 47 | _ => Err(anyhow::anyhow!("invalid shell")), 48 | } 49 | } 50 | 51 | pub fn get_shell_script(&self) -> String { 52 | match self { 53 | Shell::Bash => include_str!("../../shells/cdwe_bash.txt").to_string(), 54 | Shell::Fish => include_str!("../../shells/cdwe_fish.txt").to_string(), 55 | Shell::Zsh => include_str!("../../shells/cdwe_zsh.txt").to_string(), 56 | } 57 | } 58 | 59 | pub fn get_shell_script_target(&self) -> Result { 60 | let home_var = std::env::var("HOME").context("no $HOME set")?; 61 | let home = Path::new(&home_var); 62 | match self { 63 | Shell::Bash => Ok(std::path::Path::join(home, ".cdwe.bash") 64 | .to_str() 65 | .context("failed to get bash target")? 66 | .to_string()), 67 | Shell::Fish => Ok(std::path::Path::join(home, ".cdwe.fish") 68 | .to_str() 69 | .context("failed to get fish target")? 70 | .to_string()), 71 | Shell::Zsh => Ok(std::path::Path::join(home, ".cdwe.zsh") 72 | .to_str() 73 | .context("failed to get zsh target")? 74 | .to_string()), 75 | } 76 | } 77 | 78 | pub fn get_default_command(&self) -> String { 79 | match self { 80 | Shell::Bash => "builtin cd".to_string(), 81 | Shell::Fish => "cd".to_string(), 82 | Shell::Zsh => "builtin cd".to_string(), 83 | } 84 | } 85 | 86 | pub fn get_alias_command(&self) -> (String, String) { 87 | match self { 88 | Shell::Zsh => ("{{{alias_name}}}() {\n".to_string(), "}\n".to_string()), 89 | Shell::Bash => ("{{{alias_name}}}() {\n".to_string(), "}\n".to_string()), 90 | Shell::Fish => ( 91 | "function {{{alias_name}}} -d \"{}\"\n".to_string(), 92 | "end\n".to_string(), 93 | ), 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::cmd::Shell; 2 | use anyhow::{Context, Result}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::collections::HashMap; 5 | 6 | pub type EnvVariableVec = Vec; 7 | #[derive(Deserialize, Serialize, Debug, Clone)] 8 | #[serde(untagged)] 9 | pub enum EnvVariableStruct { 10 | HashMap(HashMap), 11 | EnvVariableVec(EnvVariableVec), 12 | } 13 | 14 | impl From for EnvVariableVec { 15 | fn from(env_variable: EnvVariableStruct) -> Self { 16 | match env_variable { 17 | EnvVariableStruct::EnvVariableVec(dir_env_variable) => dir_env_variable, 18 | EnvVariableStruct::HashMap(hash_map) => hash_map 19 | .into_iter() 20 | .map(|(name, value)| EnvVariable { name, value }) 21 | .collect(), 22 | } 23 | } 24 | } 25 | 26 | #[derive(Deserialize, Serialize, Debug, Clone, Default)] 27 | pub struct LocalConfig { 28 | pub variables: Option>, 29 | pub aliases: Option>, 30 | pub commands: Option>, 31 | } 32 | 33 | impl LocalConfig { 34 | pub fn from_str(content: &str) -> Result { 35 | let config: LocalConfig = 36 | toml::from_str(content).with_context(|| "Could not parse local config file")?; 37 | 38 | Ok(config) 39 | } 40 | } 41 | 42 | #[derive(Deserialize, Serialize, Debug, Clone, Default)] 43 | pub struct Config { 44 | pub config: Option, 45 | #[serde(rename = "directory")] 46 | pub directories: Vec, 47 | #[serde(rename = "env_variable")] 48 | pub variables: Option>, 49 | #[serde(rename = "command")] 50 | pub commands: Option>, 51 | #[serde(rename = "env_file")] 52 | pub files: Option>, 53 | #[serde(rename = "alias")] 54 | pub aliases: Option>, 55 | } 56 | 57 | impl Config { 58 | pub fn default_for_shell(shell: Shell) -> Self { 59 | Config { 60 | config: Some(GlobalConfig { 61 | shell: Some(shell.to_string()), 62 | ..Default::default() 63 | }), 64 | directories: vec![EnvDirectory { 65 | path: "~".to_string(), 66 | vars: None, 67 | load_from: None, 68 | run: None, 69 | aliases: None, 70 | }], 71 | variables: None, 72 | commands: None, 73 | files: None, 74 | aliases: None, 75 | } 76 | } 77 | } 78 | 79 | #[derive(Deserialize, Serialize, Debug, Clone)] 80 | pub struct GlobalConfig { 81 | pub shell: Option, 82 | pub cd_command: Option, 83 | pub env_hints: Option, 84 | pub run_hints: Option, 85 | pub alias_hints: Option, 86 | } 87 | 88 | impl Default for GlobalConfig { 89 | fn default() -> Self { 90 | GlobalConfig { 91 | shell: None, 92 | cd_command: None, 93 | env_hints: Some(true), 94 | run_hints: Some(true), 95 | alias_hints: Some(true), 96 | } 97 | } 98 | } 99 | 100 | #[derive(Deserialize, Serialize, Debug, Clone)] 101 | pub struct EnvDirectory { 102 | pub path: String, 103 | pub vars: Option, 104 | pub load_from: Option>, 105 | pub run: Option>, 106 | pub aliases: Option>, 107 | } 108 | 109 | #[derive(Deserialize, Serialize, Debug, Clone)] 110 | pub struct EnvAlias { 111 | pub name: String, 112 | pub commands: Vec, 113 | } 114 | 115 | #[derive(Deserialize, Serialize, Debug, Clone)] 116 | pub struct DirectoryEnvAlias { 117 | pub name: String, 118 | pub commands: Vec, 119 | pub paths: Vec, 120 | } 121 | 122 | #[derive(Deserialize, Serialize, Debug, Clone)] 123 | pub struct DirEnvVariable { 124 | pub name: String, 125 | pub value: String, 126 | pub dirs: Vec, 127 | } 128 | 129 | #[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] 130 | pub struct EnvVariable { 131 | pub name: String, 132 | pub value: String, 133 | } 134 | 135 | #[derive(Deserialize, Serialize, Debug, Clone)] 136 | pub struct EnvCommand { 137 | pub run: String, 138 | pub dirs: Vec, 139 | } 140 | 141 | #[derive(Deserialize, Serialize, Debug, Clone)] 142 | pub struct EnvFile { 143 | pub load_from: String, 144 | pub dirs: Vec, 145 | } 146 | 147 | impl Config { 148 | pub fn from_config_file(path: &str) -> Result { 149 | let contents = std::fs::read_to_string(path) 150 | .with_context(|| format!("Could not read config file at {}", path))?; 151 | 152 | let config: Config = toml::from_str(&contents) 153 | .with_context(|| format!("Could not parse config file at {}", path))?; 154 | 155 | Ok(config) 156 | } 157 | 158 | pub fn from_str(content: &str) -> Result { 159 | let config: Config = 160 | toml::from_str(content).with_context(|| "Could not parse config file")?; 161 | 162 | Ok(config) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod cache; 2 | mod cmd; 3 | mod config; 4 | mod utils; 5 | use anyhow::{Context, Result}; 6 | use clap::Parser; 7 | use cmd::{init_shell, remove_shell, run, run_local, Cli}; 8 | use config::{Config, LocalConfig}; 9 | 10 | #[tokio::main] 11 | async fn main() -> Result<()> { 12 | let matches = Cli::parse(); 13 | let home = std::env::var("HOME").context("no $HOME set")?; 14 | let config_path = format!("{}/{}", &home, "cdwe.toml"); 15 | let cache_path = format!("{}/{}", &home, ".cdwe_cache.json"); 16 | 17 | match matches.command { 18 | cmd::Commands::Init { shell } => init_shell(None, shell.unwrap())?, 19 | cmd::Commands::Run { old_dir, new_dir } => { 20 | let local_config_path = format!("{}/{}", new_dir, "cdwe.toml"); 21 | let old_local_config_path = format!("{}/{}", old_dir, "cdwe.toml"); 22 | 23 | let contents = std::fs::read_to_string(&config_path) 24 | .with_context(|| format!("Could not read config file at {}", &config_path))?; 25 | let config_hash = utils::get_content_hash(&contents); 26 | let cache_contents: Option = std::fs::read_to_string(cache_path).ok(); 27 | let (cache, did_create_cache) = 28 | cache::get_or_create_cache(cache_contents.as_deref(), &contents, &config_hash)?; 29 | let shell = cache.shell.clone(); 30 | 31 | run(&cache, old_dir, new_dir)?; 32 | 33 | if did_create_cache { 34 | cache::write_cache(&cache, &home)?; 35 | } 36 | 37 | let old_local_config = match std::fs::read_to_string(&old_local_config_path) { 38 | Ok(contents) => Some(LocalConfig::from_str(&contents)?), 39 | Err(_) => None, 40 | }; 41 | 42 | let new_local_config = match std::fs::read_to_string(&local_config_path) { 43 | Ok(contents) => Some(LocalConfig::from_str(&contents)?), 44 | Err(_) => None, 45 | }; 46 | 47 | if old_local_config.is_some() || new_local_config.is_some() { 48 | run_local(old_local_config.as_ref(), new_local_config.as_ref(), &shell)?; 49 | } 50 | } 51 | cmd::Commands::Reload { shell } => { 52 | let config: Config = Config::from_config_file(&config_path)?; 53 | init_shell(Some(config), shell.unwrap())?; 54 | } 55 | cmd::Commands::Remove { shell } => remove_shell(shell.context("no shell passed")?)?, 56 | } 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use sha2::{Digest, Sha256}; 2 | 3 | pub fn get_content_hash(content: &str) -> String { 4 | let mut hasher = Sha256::new(); 5 | hasher.update(content.as_bytes()); 6 | 7 | format!("{:?}", hasher.finalize()) 8 | } 9 | 10 | pub fn trim_quotes(s: &str) -> String { 11 | if s.len() < 2 { 12 | return s.to_string(); 13 | } 14 | let mut chars = s.chars(); 15 | match (chars.next(), chars.next_back()) { 16 | (Some('"'), Some('"')) => chars.collect(), 17 | (Some('\''), Some('\'')) => chars.collect(), 18 | _ => s.to_string(), 19 | } 20 | } 21 | #[cfg(test)] 22 | mod tests { 23 | #[test] 24 | fn test_trim_quotes() { 25 | use super::trim_quotes; 26 | assert_eq!(trim_quotes("\"test\""), "test"); 27 | assert_eq!(trim_quotes("'test'"), "test"); 28 | assert_eq!(trim_quotes("test"), "test"); 29 | assert_eq!(trim_quotes("\"test"), "\"test"); 30 | assert_eq!(trim_quotes("test'"), "test'"); 31 | } 32 | } 33 | --------------------------------------------------------------------------------