├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md └── src ├── cmd.rs ├── index.rs ├── main.rs └── shells ├── init.bash ├── init.fish └── init.zsh /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is_terminal_polyfill", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.10" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.6" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.1.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 40 | dependencies = [ 41 | "windows-sys", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "3.0.7" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 49 | dependencies = [ 50 | "anstyle", 51 | "once_cell", 52 | "windows-sys", 53 | ] 54 | 55 | [[package]] 56 | name = "anyhow" 57 | version = "1.0.98" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 60 | 61 | [[package]] 62 | name = "bitflags" 63 | version = "2.8.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" 66 | 67 | [[package]] 68 | name = "cfg-if" 69 | version = "1.0.0" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 72 | 73 | [[package]] 74 | name = "clap" 75 | version = "4.5.36" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04" 78 | dependencies = [ 79 | "clap_builder", 80 | "clap_derive", 81 | ] 82 | 83 | [[package]] 84 | name = "clap_builder" 85 | version = "4.5.36" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5" 88 | dependencies = [ 89 | "anstream", 90 | "anstyle", 91 | "clap_lex", 92 | "strsim", 93 | ] 94 | 95 | [[package]] 96 | name = "clap_complete" 97 | version = "4.5.47" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "c06f5378ea264ad4f82bbc826628b5aad714a75abf6ece087e923010eb937fb6" 100 | dependencies = [ 101 | "clap", 102 | ] 103 | 104 | [[package]] 105 | name = "clap_derive" 106 | version = "4.5.32" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 109 | dependencies = [ 110 | "heck", 111 | "proc-macro2", 112 | "quote", 113 | "syn", 114 | ] 115 | 116 | [[package]] 117 | name = "clap_lex" 118 | version = "0.7.4" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 121 | 122 | [[package]] 123 | name = "colorchoice" 124 | version = "1.0.3" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 127 | 128 | [[package]] 129 | name = "dirs" 130 | version = "6.0.0" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" 133 | dependencies = [ 134 | "dirs-sys", 135 | ] 136 | 137 | [[package]] 138 | name = "dirs-sys" 139 | version = "0.5.0" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" 142 | dependencies = [ 143 | "libc", 144 | "option-ext", 145 | "redox_users", 146 | "windows-sys", 147 | ] 148 | 149 | [[package]] 150 | name = "dunce" 151 | version = "1.0.5" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" 154 | 155 | [[package]] 156 | name = "getrandom" 157 | version = "0.2.15" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 160 | dependencies = [ 161 | "cfg-if", 162 | "libc", 163 | "wasi", 164 | ] 165 | 166 | [[package]] 167 | name = "heck" 168 | version = "0.5.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 171 | 172 | [[package]] 173 | name = "is_terminal_polyfill" 174 | version = "1.70.1" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 177 | 178 | [[package]] 179 | name = "jumpy" 180 | version = "0.4.10" 181 | dependencies = [ 182 | "anyhow", 183 | "clap", 184 | "clap_complete", 185 | "dirs", 186 | "dunce", 187 | ] 188 | 189 | [[package]] 190 | name = "libc" 191 | version = "0.2.169" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 194 | 195 | [[package]] 196 | name = "libredox" 197 | version = "0.1.3" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 200 | dependencies = [ 201 | "bitflags", 202 | "libc", 203 | ] 204 | 205 | [[package]] 206 | name = "once_cell" 207 | version = "1.21.3" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 210 | 211 | [[package]] 212 | name = "option-ext" 213 | version = "0.2.0" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 216 | 217 | [[package]] 218 | name = "proc-macro2" 219 | version = "1.0.94" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 222 | dependencies = [ 223 | "unicode-ident", 224 | ] 225 | 226 | [[package]] 227 | name = "quote" 228 | version = "1.0.40" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 231 | dependencies = [ 232 | "proc-macro2", 233 | ] 234 | 235 | [[package]] 236 | name = "redox_users" 237 | version = "0.5.0" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" 240 | dependencies = [ 241 | "getrandom", 242 | "libredox", 243 | "thiserror", 244 | ] 245 | 246 | [[package]] 247 | name = "strsim" 248 | version = "0.11.1" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 251 | 252 | [[package]] 253 | name = "syn" 254 | version = "2.0.100" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 257 | dependencies = [ 258 | "proc-macro2", 259 | "quote", 260 | "unicode-ident", 261 | ] 262 | 263 | [[package]] 264 | name = "thiserror" 265 | version = "2.0.11" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" 268 | dependencies = [ 269 | "thiserror-impl", 270 | ] 271 | 272 | [[package]] 273 | name = "thiserror-impl" 274 | version = "2.0.11" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" 277 | dependencies = [ 278 | "proc-macro2", 279 | "quote", 280 | "syn", 281 | ] 282 | 283 | [[package]] 284 | name = "unicode-ident" 285 | version = "1.0.18" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 288 | 289 | [[package]] 290 | name = "utf8parse" 291 | version = "0.2.2" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 294 | 295 | [[package]] 296 | name = "wasi" 297 | version = "0.11.0+wasi-snapshot-preview1" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 300 | 301 | [[package]] 302 | name = "windows-sys" 303 | version = "0.59.0" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 306 | dependencies = [ 307 | "windows-targets", 308 | ] 309 | 310 | [[package]] 311 | name = "windows-targets" 312 | version = "0.52.6" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 315 | dependencies = [ 316 | "windows_aarch64_gnullvm", 317 | "windows_aarch64_msvc", 318 | "windows_i686_gnu", 319 | "windows_i686_gnullvm", 320 | "windows_i686_msvc", 321 | "windows_x86_64_gnu", 322 | "windows_x86_64_gnullvm", 323 | "windows_x86_64_msvc", 324 | ] 325 | 326 | [[package]] 327 | name = "windows_aarch64_gnullvm" 328 | version = "0.52.6" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 331 | 332 | [[package]] 333 | name = "windows_aarch64_msvc" 334 | version = "0.52.6" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 337 | 338 | [[package]] 339 | name = "windows_i686_gnu" 340 | version = "0.52.6" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 343 | 344 | [[package]] 345 | name = "windows_i686_gnullvm" 346 | version = "0.52.6" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 349 | 350 | [[package]] 351 | name = "windows_i686_msvc" 352 | version = "0.52.6" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 355 | 356 | [[package]] 357 | name = "windows_x86_64_gnu" 358 | version = "0.52.6" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 361 | 362 | [[package]] 363 | name = "windows_x86_64_gnullvm" 364 | version = "0.52.6" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 367 | 368 | [[package]] 369 | name = "windows_x86_64_msvc" 370 | version = "0.52.6" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 373 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "jumpy" 3 | version = "0.4.10" 4 | edition = "2021" 5 | authors = ["Clément Nerma "] 6 | license = "Apache-2.0" 7 | description = "A full-featured replacement jump utilities like Zoxide or `z`" 8 | readme = "README.md" 9 | repository = "https://github.com/ClementNerma/Jumpy" 10 | 11 | [dependencies] 12 | anyhow = "1.0.98" 13 | clap = { version = "4.5.36", features = ["derive"] } 14 | clap_complete = "4.5.47" 15 | dirs = "6.0.0" 16 | dunce = "1.0.5" 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Apache License 2 | 3 | _Version 2.0, January 2004_ 4 | _[http://www.apache.org/licenses/](http://www.apache.org/licenses/)_ 5 | 6 | ## TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | ### 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | ### 2. Grant of Copyright License. 68 | 69 | Subject to the terms and conditions of 70 | this License, each Contributor hereby grants to You a perpetual, 71 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 72 | copyright license to reproduce, prepare Derivative Works of, 73 | publicly display, publicly perform, sublicense, and distribute the 74 | Work and such Derivative Works in Source or Object form. 75 | 76 | ### 3. Grant of Patent License. 77 | 78 | Subject to the terms and conditions of 79 | this License, each Contributor hereby grants to You a perpetual, 80 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 81 | (except as stated in this section) patent license to make, have made, 82 | use, offer to sell, sell, import, and otherwise transfer the Work, 83 | where such license applies only to those patent claims licensable 84 | by such Contributor that are necessarily infringed by their 85 | Contribution(s) alone or by combination of their Contribution(s) 86 | with the Work to which such Contribution(s) was submitted. If You 87 | institute patent litigation against any entity (including a 88 | cross-claim or counterclaim in a lawsuit) alleging that the Work 89 | or a Contribution incorporated within the Work constitutes direct 90 | or contributory patent infringement, then any patent licenses 91 | granted to You under this License for that Work shall terminate 92 | as of the date such litigation is filed. 93 | 94 | ### 4. Redistribution. 95 | 96 | You may reproduce and distribute copies of the 97 | Work or Derivative Works thereof in any medium, with or without 98 | modifications, and in Source or Object form, provided that You 99 | meet the following conditions: 100 | 101 | (a) You must give any other recipients of the Work or 102 | Derivative Works a copy of this License; and 103 | 104 | (b) You must cause any modified files to carry prominent notices 105 | stating that You changed the files; and 106 | 107 | (c) You must retain, in the Source form of any Derivative Works 108 | that You distribute, all copyright, patent, trademark, and 109 | attribution notices from the Source form of the Work, 110 | excluding those notices that do not pertain to any part of 111 | the Derivative Works; and 112 | 113 | (d) If the Work includes a "NOTICE" text file as part of its 114 | distribution, then any Derivative Works that You distribute must 115 | include a readable copy of the attribution notices contained 116 | within such NOTICE file, excluding those notices that do not 117 | pertain to any part of the Derivative Works, in at least one 118 | of the following places: within a NOTICE text file distributed 119 | as part of the Derivative Works; within the Source form or 120 | documentation, if provided along with the Derivative Works; or, 121 | within a display generated by the Derivative Works, if and 122 | wherever such third-party notices normally appear. The contents 123 | of the NOTICE file are for informational purposes only and 124 | do not modify the License. You may add Your own attribution 125 | notices within Derivative Works that You distribute, alongside 126 | or as an addendum to the NOTICE text from the Work, provided 127 | that such additional attribution notices cannot be construed 128 | as modifying the License. 129 | 130 | You may add Your own copyright statement to Your modifications and 131 | may provide additional or different license terms and conditions 132 | for use, reproduction, or distribution of Your modifications, or 133 | for any such Derivative Works as a whole, provided Your use, 134 | reproduction, and distribution of the Work otherwise complies with 135 | the conditions stated in this License. 136 | 137 | ### 5. Submission of Contributions. 138 | 139 | Unless You explicitly state otherwise, 140 | any Contribution intentionally submitted for inclusion in the Work 141 | by You to the Licensor shall be under the terms and conditions of 142 | this License, without any additional terms or conditions. 143 | Notwithstanding the above, nothing herein shall supersede or modify 144 | the terms of any separate license agreement you may have executed 145 | with Licensor regarding such Contributions. 146 | 147 | ### 6. Trademarks. 148 | 149 | This License does not grant permission to use the trade 150 | names, trademarks, service marks, or product names of the Licensor, 151 | except as required for reasonable and customary use in describing the 152 | origin of the Work and reproducing the content of the NOTICE file. 153 | 154 | ### 7. Disclaimer of Warranty. 155 | 156 | Unless required by applicable law or 157 | agreed to in writing, Licensor provides the Work (and each 158 | Contributor provides its Contributions) on an "AS IS" BASIS, 159 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 160 | implied, including, without limitation, any warranties or conditions 161 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 162 | PARTICULAR PURPOSE. You are solely responsible for determining the 163 | appropriateness of using or redistributing the Work and assume any 164 | risks associated with Your exercise of permissions under this License. 165 | 166 | ### 8. Limitation of Liability. 167 | 168 | In no event and under no legal theory, 169 | whether in tort (including negligence), contract, or otherwise, 170 | unless required by applicable law (such as deliberate and grossly 171 | negligent acts) or agreed to in writing, shall any Contributor be 172 | liable to You for damages, including any direct, indirect, special, 173 | incidental, or consequential damages of any character arising as a 174 | result of this License or out of the use or inability to use the 175 | Work (including but not limited to damages for loss of goodwill, 176 | work stoppage, computer failure or malfunction, or any and all 177 | other commercial damages or losses), even if such Contributor 178 | has been advised of the possibility of such damages. 179 | 180 | ### 9. Accepting Warranty or Additional Liability. 181 | 182 | While redistributing 183 | the Work or Derivative Works thereof, You may choose to offer, 184 | and charge a fee for, acceptance of support, warranty, indemnity, 185 | or other liability obligations and/or rights consistent with this 186 | License. However, in accepting such obligations, You may act only 187 | on Your own behalf and on Your sole responsibility, not on behalf 188 | of any other Contributor, and only if You agree to indemnify, 189 | defend, and hold each Contributor harmless for any liability 190 | incurred by, or claims asserted against, such Contributor by reason 191 | of your accepting any such warranty or additional liability. 192 | 193 | ## END OF TERMS AND CONDITIONS 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jumpy 2 | 3 | Jumpy is a tool that allows to quickly jump to one of the directory you've visited in the past. 4 | 5 | It is heavily inspired by [Zoxide](https://github.com/ajeetdsouza/zoxide/) but is more lightweight and a lot faster. 6 | 7 | In its current version it is mostly intended for my personal use, if I find to work well enough I'll improve the documentation and add new features. 8 | 9 | Updates can be found in the [releases](https://github.com/ClementNerma/Jumpy/releases). 10 | 11 | ## Performance 12 | 13 | On a Ryzen 7900 (running on a single core), it takes about 4 seconds to decode a 500 MB index file with 10 million registered directories, and 2 seconds to traverse it entirely to find the very last entry. 14 | 15 | On a small and more realistic example, with 1 thousand directories, it takes about 250 µs to decode the 50 KB index file and 250 µs to traverse it to find the last entry. 16 | 17 | ## Setup 18 | 19 | ```shell 20 | # ZSH 21 | eval "$(jumpy completions zsh)" 22 | 23 | # Fish 24 | jumpy completions fish | source 25 | ``` 26 | 27 | This will allow Jumpy to register each change of directory to add them to its database. 28 | 29 | To perform a query and jump to it, just use `z `. 30 | 31 | ## Usage 32 | 33 | ```shell 34 | # [With shell integration] Jumpy to the first directory matching the query 35 | z 36 | 37 | # Get the most relevant directory from a query 38 | jumpy query 39 | 40 | # Add a new directory to the database, or increment its score 41 | jumpy add 42 | 43 | # List all registered directories, sorted by score 44 | jumpy list 45 | 46 | # Clear the database 47 | jumpy clear 48 | ``` 49 | -------------------------------------------------------------------------------- /src/cmd.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::{Parser, Subcommand, ValueEnum, ValueHint}; 4 | 5 | #[derive(Parser)] 6 | #[clap(author, version, about, long_about = None)] 7 | pub struct Command { 8 | #[clap(short, long, value_hint = ValueHint::FilePath)] 9 | pub index_file: Option, 10 | 11 | #[clap(subcommand)] 12 | pub action: Action, 13 | } 14 | 15 | #[derive(Subcommand)] 16 | pub enum Action { 17 | #[clap( 18 | about = "Add a new directory if not yet registered", 19 | long_about = "Does nothing if the directory is already registered" 20 | )] 21 | Add { 22 | #[clap(value_hint = ValueHint::DirPath)] 23 | path: String, 24 | }, 25 | 26 | #[clap( 27 | about = "Increment a registered directory's score or add it to the database", 28 | long_about = "Adds the directory to the database if it is not registered yet" 29 | )] 30 | Inc { 31 | #[clap(value_hint = ValueHint::DirPath)] 32 | path: String, 33 | 34 | #[clap(long, help = "Give the maximum score to this directory")] 35 | top: bool, 36 | }, 37 | 38 | #[clap(about = "Find the most relevant directory for the provided query")] 39 | Query { 40 | #[clap(value_hint = ValueHint::Other)] 41 | query: String, 42 | 43 | #[clap(short, long, value_hint = ValueHint::DirPath)] 44 | after: Option, 45 | 46 | #[clap(short, long)] 47 | checked: bool, 48 | }, 49 | 50 | #[clap(about = "List all registered directories")] 51 | List { 52 | #[clap(short, long, help = "Display scores and sort directories by them")] 53 | scores: bool, 54 | }, 55 | 56 | #[clap(about = "Delete a registered directory from the database")] 57 | Del { 58 | #[clap(value_hint = ValueHint::DirPath)] 59 | path: String, 60 | }, 61 | 62 | #[clap(about = "Cleanup the database to remove deleted directories")] 63 | Cleanup, 64 | 65 | #[clap(about = "Clear the database")] 66 | Clear, 67 | 68 | #[clap(about = "Output the entire database (plain text)")] 69 | Export, 70 | 71 | #[clap(about = "Get the path of the index file")] 72 | Path { 73 | #[clap( 74 | short, 75 | long, 76 | help = "If the path contains invalid UTF-8 characters, don't fail and print it lossily instead" 77 | )] 78 | lossily: bool, 79 | }, 80 | 81 | #[clap(about = "Generate completions for a given shell")] 82 | Completions { 83 | #[clap(help = "Shell to generate completions for", value_hint = ValueHint::Other)] 84 | for_shell: CompletionShellName, 85 | }, 86 | } 87 | 88 | #[derive(Clone, Copy, ValueEnum)] 89 | pub enum CompletionShellName { 90 | Bash, 91 | Zsh, 92 | Fish, 93 | Elvish, 94 | PowerShell, 95 | } 96 | -------------------------------------------------------------------------------- /src/index.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{hash_map::Entry, HashMap}, 3 | path::Path, 4 | }; 5 | 6 | use anyhow::{bail, Context, Result}; 7 | 8 | pub struct Index { 9 | scored_entries: HashMap, 10 | } 11 | 12 | impl Index { 13 | pub fn new() -> Self { 14 | Self { 15 | scored_entries: HashMap::new(), 16 | } 17 | } 18 | 19 | pub fn canonicalize(path: impl AsRef) -> Result { 20 | let path = path.as_ref(); 21 | 22 | // NOTE: we use 'dunce' to avoid (when possible) UNC paths which may 23 | // result in unexpected behaviours 24 | let path = dunce::canonicalize(path) 25 | .with_context(|| format!("Failed to canonicalize path: {path}"))? 26 | .to_str() 27 | .with_context(|| format!("Path contains invalid UTF-8 characters: {path}"))? 28 | .to_string(); 29 | 30 | Ok(path) 31 | } 32 | 33 | pub fn add_or_inc( 34 | &mut self, 35 | path: String, 36 | update_score: impl FnOnce(u64) -> u64, 37 | default_value: u64, 38 | ) -> Result<()> { 39 | if path.is_empty() { 40 | bail!("Please provide a valid path."); 41 | } 42 | 43 | if !Path::new(&path).exists() { 44 | bail!("Provided directory does not exist."); 45 | } 46 | 47 | let path = Self::canonicalize(path)?; 48 | 49 | // Silently ignore root path 50 | if path == "/" { 51 | return Ok(()); 52 | } 53 | 54 | match self.scored_entries.entry(path) { 55 | Entry::Occupied(mut entry) => { 56 | *entry.get_mut() = update_score(*entry.get()); 57 | } 58 | 59 | Entry::Vacant(entry) => { 60 | entry.insert(default_value); 61 | } 62 | } 63 | 64 | Ok(()) 65 | } 66 | 67 | pub fn add(&mut self, path: String) -> Result<()> { 68 | self.add_or_inc(path, |score| score, 1) 69 | } 70 | 71 | pub fn inc(&mut self, path: String, set_top: bool) -> Result<()> { 72 | self.add_or_inc( 73 | path, 74 | |score| { 75 | if set_top { 76 | u64::MAX 77 | } else { 78 | score.saturating_add(1) 79 | } 80 | }, 81 | if set_top { u64::MAX / 2 } else { 1 }, 82 | ) 83 | } 84 | 85 | pub fn query_all(&self, query: &str, after: Option<&str>) -> Vec { 86 | let query = query.to_lowercase(); 87 | 88 | let mut results = self 89 | .scored_entries 90 | .iter() 91 | .map(IndexEntry::from) 92 | .filter(|entry| matches_query(Path::new(entry.path), &query)) 93 | .collect::>(); 94 | 95 | // Sort by score (relevancy) 96 | results.sort_by_key(|entry| entry.score); 97 | results.reverse(); 98 | 99 | // Ignore 'after' parameter if the query doesn't match 100 | // Avoids the following problem: 101 | // 102 | // Scored entries have `/a/1`, `/b` and `/a/2`, in that order 103 | // We are in directory `/b` for whatever reason 104 | // We query `a`, we'll end up in `/a/2` instead of `/a/1` 105 | let after = after.filter(|after| matches_query(Path::new(after), &query)); 106 | 107 | if let Some(after) = after { 108 | if let Some(index) = results.iter().position(|entry| entry.path == after) { 109 | // First we remove the results that are prior to the provided path... 110 | let evicted = results.drain(0..=index).collect::>(); 111 | // ...then we add them at the back 112 | // This makes it cyclic: when the last item is reached, it goes back to the beginning of the list 113 | results.extend(evicted); 114 | } 115 | } 116 | 117 | results 118 | } 119 | 120 | /// Perform a query without checking if the result directories still exist 121 | pub fn query_unchecked(&self, query: &str, after: Option<&str>) -> Option<&str> { 122 | self.query_all(query, after) 123 | .first() 124 | .map(|result| result.path) 125 | } 126 | 127 | /// Perform a query but remove directory entries which don't exist anymore 128 | pub fn query_checked(&mut self, query: &str, after: Option<&str>) -> Option { 129 | let mut to_remove = vec![]; 130 | 131 | let path = self 132 | .query_all(query, after) 133 | .into_iter() 134 | .find(|result| { 135 | if Path::new(result.path).exists() { 136 | true 137 | } else { 138 | to_remove.push(result.path.to_string()); 139 | false 140 | } 141 | }) 142 | .map(|result| result.path.to_string())?; 143 | 144 | for path in to_remove { 145 | self.remove_canonicalized(&path).unwrap(); 146 | } 147 | 148 | Some(path) 149 | } 150 | 151 | pub fn iter(&self) -> impl Iterator { 152 | self.scored_entries.iter().map(IndexEntry::from) 153 | } 154 | 155 | pub fn remove_canonicalized(&mut self, path: &str) -> Result<()> { 156 | match self.scored_entries.remove(path) { 157 | Some(_) => Ok(()), 158 | None => bail!("Provided directory is not registered"), 159 | } 160 | } 161 | 162 | pub fn cleanup(&mut self) { 163 | let to_remove = self 164 | .scored_entries 165 | .keys() 166 | .filter(|path| !Path::new(path).is_dir()) 167 | .cloned() 168 | .collect::>(); 169 | 170 | for path in to_remove { 171 | self.remove_canonicalized(&path).unwrap(); 172 | } 173 | } 174 | 175 | pub fn clear(&mut self) { 176 | self.scored_entries = HashMap::new(); 177 | } 178 | 179 | pub fn encode(&self) -> String { 180 | self.scored_entries 181 | .iter() 182 | .map(IndexEntry::from) 183 | .map(|entry| format!("{} {}", entry.score, entry.path)) 184 | .collect::>() 185 | .join("\n") 186 | } 187 | 188 | pub fn decode(input: &str) -> Result { 189 | let mut n = 0; 190 | let mut scored_entries = HashMap::new(); 191 | 192 | for line in input.lines() { 193 | n += 1; 194 | 195 | let split = line 196 | .find(' ') 197 | .with_context(|| format!("Missing whitespace delimited at line {n}"))?; 198 | 199 | let score = &line[0..split] 200 | .parse::() 201 | .with_context(|| format!("Failed to parse score at line {n}"))?; 202 | 203 | let path = &line[split + 1..]; 204 | 205 | if scored_entries.contains_key(path) { 206 | bail!("Duplicate directory entry at line {n}: {path}"); 207 | } 208 | 209 | scored_entries.insert(path.to_string(), *score); 210 | } 211 | 212 | Ok(Self { scored_entries }) 213 | } 214 | } 215 | 216 | #[derive(Debug, PartialEq, Eq)] 217 | pub struct IndexEntry<'a> { 218 | pub path: &'a str, 219 | pub score: u64, 220 | } 221 | 222 | impl<'a> From<(&'a String, &'a u64)> for IndexEntry<'a> { 223 | fn from(iter_entry: (&'a String, &'a u64)) -> Self { 224 | Self { 225 | path: iter_entry.0.as_str(), 226 | score: *iter_entry.1, 227 | } 228 | } 229 | } 230 | 231 | fn matches_query(path: &Path, query: &str) -> bool { 232 | Path::new(path) 233 | .file_name() 234 | .map(|filename| filename.to_str().unwrap()) 235 | .filter(|filename| filename.to_lowercase().contains(query)) 236 | .is_some() 237 | } 238 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | #![forbid(unused_must_use)] 3 | #![forbid(unused_allocation)] 4 | 5 | mod cmd; 6 | mod index; 7 | 8 | use std::{fs, io::stdout, process::ExitCode}; 9 | 10 | use anyhow::{bail, Context, Result}; 11 | use clap::{CommandFactory, Parser}; 12 | 13 | use crate::{ 14 | cmd::*, 15 | index::{Index, IndexEntry}, 16 | }; 17 | 18 | static INDEX_FILENAME: &str = "jumpy.db"; 19 | 20 | fn main() -> ExitCode { 21 | match inner_main() { 22 | Ok(()) => ExitCode::SUCCESS, 23 | Err(err) => { 24 | eprintln!("{err:?}"); 25 | ExitCode::FAILURE 26 | } 27 | } 28 | } 29 | 30 | fn inner_main() -> Result<()> { 31 | let cmd = Command::parse(); 32 | 33 | let index_file = cmd.index_file.unwrap_or_else(|| { 34 | dirs::config_dir() 35 | .expect("Failed to get configuration directory!") 36 | .join(INDEX_FILENAME) 37 | }); 38 | 39 | let (mut index, source) = if index_file.exists() { 40 | let content = fs::read_to_string(&index_file).context("Failed to read index file")?; 41 | 42 | ( 43 | Index::decode(&content).context("Failed to decode index")?, 44 | content, 45 | ) 46 | } else { 47 | (Index::new(), String::new()) 48 | }; 49 | 50 | match cmd.action { 51 | Action::Add { path } => { 52 | index.add(path).context("Failed to add directory")?; 53 | } 54 | 55 | Action::Inc { path, top } => { 56 | index 57 | .inc(path, top) 58 | .context("Failed to increment directory visit counts")?; 59 | } 60 | 61 | Action::Query { 62 | query, 63 | after, 64 | checked, 65 | } => { 66 | if query.is_empty() { 67 | bail!("Please provide a query to search from."); 68 | } 69 | 70 | let result = if checked { 71 | index.query_checked(&query, after.as_deref()) 72 | } else { 73 | index 74 | .query_unchecked(&query, after.as_deref()) 75 | .map(str::to_string) 76 | }; 77 | 78 | match result { 79 | Some(result) => println!("{result}"), 80 | None => bail!("No result found"), 81 | } 82 | } 83 | 84 | Action::List { scores } => { 85 | let mut entries = index.iter().collect::>(); 86 | 87 | if scores { 88 | entries.sort_by_key(|entry| entry.score); 89 | entries.reverse(); 90 | } else { 91 | entries.sort_by_key(|entry| entry.path); 92 | } 93 | 94 | let longest_score = entries 95 | .iter() 96 | .map(|entry| entry.score) 97 | .max() 98 | .map(|score| score.to_string().len()) 99 | .unwrap_or(0); 100 | 101 | for IndexEntry { path, score } in entries { 102 | if scores { 103 | println!("{score:>longest_score$} {path}"); 104 | } else { 105 | println!("{path}"); 106 | } 107 | } 108 | } 109 | 110 | Action::Del { path } => { 111 | let path = Index::canonicalize(path)?; 112 | index.remove_canonicalized(&path)?; 113 | } 114 | 115 | Action::Clear => { 116 | index.clear(); 117 | } 118 | 119 | Action::Cleanup => index.cleanup(), 120 | 121 | Action::Export => println!("{}", index.encode()), 122 | 123 | Action::Path { lossily } => match index_file.to_str() { 124 | Some(lossless) => println!("{}", lossless), 125 | None => { 126 | if lossily { 127 | println!("{}", index_file.to_string_lossy()) 128 | } else { 129 | bail!("Path to index file contains invalid UTF-8 characters. Use --lossily to print it nonetheless."); 130 | } 131 | } 132 | }, 133 | 134 | Action::Completions { for_shell } => { 135 | use clap_complete::*; 136 | 137 | let shell = match for_shell { 138 | CompletionShellName::Bash => Shell::Bash, 139 | CompletionShellName::Zsh => Shell::Zsh, 140 | CompletionShellName::Fish => Shell::Fish, 141 | CompletionShellName::Elvish => Shell::Elvish, 142 | CompletionShellName::PowerShell => Shell::PowerShell, 143 | }; 144 | 145 | let cmd = &mut Command::command(); 146 | 147 | aot::generate(shell, cmd, cmd.get_name().to_string(), &mut stdout()); 148 | 149 | println!( 150 | "{}", 151 | match for_shell { 152 | CompletionShellName::Bash => include_str!("shells/init.bash"), 153 | CompletionShellName::Zsh => include_str!("shells/init.zsh"), 154 | CompletionShellName::Fish => include_str!("shells/init.fish"), 155 | CompletionShellName::Elvish => { 156 | // TODO 157 | "" 158 | } 159 | CompletionShellName::PowerShell => { 160 | // TODO 161 | "" 162 | } 163 | } 164 | ); 165 | } 166 | } 167 | 168 | let updated = index.encode(); 169 | 170 | if updated != source { 171 | fs::write(&index_file, updated).with_context(|| { 172 | format!( 173 | "Failed to write index file at path: {}", 174 | index_file.display() 175 | ) 176 | }) 177 | } else { 178 | Ok(()) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/shells/init.bash: -------------------------------------------------------------------------------- 1 | # NOTE: This is a hack as Bash doesn't support current directory watchers 2 | function cd { 3 | builtin cd "$@" || return 4 | jumpy inc "$PWD" 5 | } 6 | 7 | function z { 8 | if [[ -z $1 ]]; then 9 | echo "ERROR: Please provide a query." 10 | return 1 11 | fi 12 | 13 | local result=$(jumpy query "$1" --checked --after "$PWD") 14 | 15 | if [[ -n $result ]]; then 16 | builtin cd "$result" 17 | fi 18 | } 19 | -------------------------------------------------------------------------------- /src/shells/init.fish: -------------------------------------------------------------------------------- 1 | 2 | function __jumpy_reg --on-variable PWD --description 'Register directory changes with Jumpy' 3 | if set -q __JUMPY_DONT_REGISTER 4 | return 5 | end 6 | 7 | jumpy inc "$PWD" 8 | end 9 | 10 | function z -a query 11 | set result $(jumpy query "$query" --checked --after "$PWD") 12 | 13 | if test -n "$result" 14 | set __JUMPY_DONT_REGISTER 15 | cd "$result" 16 | set -e __JUMPY_DONT_REGISTER 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /src/shells/init.zsh: -------------------------------------------------------------------------------- 1 | export __JUMPY_DONT_REGISTER=0 2 | 3 | function jumpy_handler() { 4 | if (( $__JUMPY_DONT_REGISTER )); then 5 | return 6 | fi 7 | 8 | emulate -L zsh 9 | jumpy inc "$PWD" 10 | } 11 | 12 | function z() { 13 | [[ -z $1 ]] && {{ echo "ERROR: Please provide a query."; return 1 }} 14 | 15 | local result=$(jumpy query "$1" --checked --after "$PWD") 16 | 17 | if [[ -n $result ]]; then 18 | export __JUMPY_DONT_REGISTER=1 19 | cd "$result" 20 | export __JUMPY_DONT_REGISTER=0 21 | fi 22 | } 23 | 24 | chpwd_functions=(${chpwd_functions[@]} "jumpy_handler") 25 | --------------------------------------------------------------------------------