├── .github └── workflows │ ├── build-binaries.yml │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── assets └── banner.svg ├── init ├── kn.bash ├── kn.fish └── kn.zsh ├── rustfmt.toml └── src ├── abbr.rs ├── args.rs ├── error.rs ├── init.rs ├── main.rs ├── query.rs └── utils.rs /.github/workflows/build-binaries.yml: -------------------------------------------------------------------------------- 1 | # Repurposed from `https://github.com/alexpdp7/cmdocker/blob/master/.github/workflows/quickstart.yml#L73`. 2 | 3 | name: Build binaries 4 | 5 | on: 6 | push: 7 | branches: [release] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build-binary-linux-gnu: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions-rs/toolchain@v1 18 | with: 19 | profile: minimal 20 | toolchain: nightly 21 | override: true 22 | target: x86_64-unknown-linux-gnu 23 | - run: mkdir bin 24 | - uses: actions-rs/cargo@v1 25 | with: 26 | command: build 27 | args: --release --target x86_64-unknown-linux-gnu -Z unstable-options --out-dir bin 28 | - name: Upload binary 29 | uses: actions/upload-artifact@v1 30 | with: 31 | name: kn-x86_64-unknown-linux-gnu 32 | path: bin/_kn 33 | 34 | build-binary-linux-musl: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v2 38 | - uses: actions-rs/toolchain@v1 39 | with: 40 | profile: minimal 41 | toolchain: nightly 42 | override: true 43 | target: x86_64-unknown-linux-musl 44 | - run: mkdir bin 45 | - uses: actions-rs/cargo@v1 46 | with: 47 | command: build 48 | args: --release --target x86_64-unknown-linux-musl -Z unstable-options --out-dir bin 49 | - name: Upload binary 50 | uses: actions/upload-artifact@v1 51 | with: 52 | name: kn-x86_64-unknown-linux-musl 53 | path: bin/_kn 54 | 55 | build-binary-macos: 56 | runs-on: macos-latest 57 | 58 | steps: 59 | - uses: actions/checkout@v2 60 | - uses: actions-rs/toolchain@v1 61 | with: 62 | profile: minimal 63 | toolchain: nightly 64 | override: true 65 | target: x86_64-apple-darwin 66 | - run: mkdir bin 67 | - uses: actions-rs/cargo@v1 68 | with: 69 | command: build 70 | args: --release --target x86_64-apple-darwin -Z unstable-options --out-dir bin 71 | - name: Upload binary 72 | uses: actions/upload-artifact@v1 73 | with: 74 | name: kn-x86_64-apple-darwin 75 | path: bin/_kn 76 | 77 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions-rs/toolchain@v1 18 | with: 19 | profile: minimal 20 | toolchain: stable 21 | override: true 22 | - uses: actions-rs/cargo@v1 23 | with: 24 | command: test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | notepad.md 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | The format is inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 4 | 5 | ## `0.3.3` - 2022-12-29 6 | 7 | - Update `powierża-coefficient` to `1.0.2`. 8 | 9 | ## `0.3.2` - 2022-07-17 (yanked) 10 | 11 | - Fix `--exclude-old-pwd` in `fish`. 12 | - Update `powierża-coefficient` to `1.0.1`. 13 | 14 | ## `0.3.1` - 2021-10-16 15 | 16 | - Fix shell scripts so that they don't exit the shell when no args were provided. 17 | 18 | ## `0.3.0` - 2021-10-14 (yanked) 19 | 20 | - Check if arg is a valid path before attempting abbreviation expansion. 21 | - Simplify `Congruence`. 22 | - Compare abbreviations and strings using [Powierża coefficient](https://github.com/micouy/powierza-coefficient) instead of Levenshtein distance. 23 | - Add `--exclude-old-pwd` flag to `_kn init`. 24 | - Remove interactive mode. 25 | - Add extensive docs. 26 | 27 | ## `0.2.2` - 2021-05-21 28 | 29 | ### Add 30 | 31 | - Handle components with more than 2 dots (`...` etc.) in the prefix in normal mode. 32 | 33 | ### Change 34 | 35 | - Remove state synchronization between search and UI. 36 | - Replace `clap` with `pico-args`. 37 | 38 | ### Remove 39 | 40 | - Remove `regex` and `ansi_term` from deps. 41 | 42 | ## `0.2.1` - 2021-05-16 43 | 44 | ### Fix 45 | 46 | - Fix shell scripts so that they remove the tmpfile. 47 | 48 | ## `0.2.0` - 2021-05-10 (yanked) 49 | 50 | ### Add 51 | 52 | - Add changelog. 53 | - Add interactive mode. 54 | - Add navigation with Tab and Shift + Tab or Ctrl + hjkl . 55 | - Add demos in [`README.md`](README.md). 56 | 57 | ### Change 58 | 59 | - Change shell scripts so that calling `kn` without args will enter interactive mode instead of changing current dir to `~`. 60 | - Move search to its own module. 61 | 62 | ## `0.1.0` - 2021-04-12 63 | 64 | ### Add 65 | 66 | - Add normal mode. 67 | - Handle abbreviations. 68 | - Handle prefix (`/`, `~`, `.`, etc.). 69 | - Handle wildcards (`-`). 70 | - Add shell scripts for `bash`, `fish` and `zsh`. 71 | - Add [`LICENSE.txt`](LICENSE.txt). 72 | - Add GitHub workflows. 73 | -------------------------------------------------------------------------------- /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 = "alphanumeric-sort" 7 | version = "1.4.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "20e59b2ccb4c1ffbbf45af6f493e16ac65a66981c85664f1587816c0b08cd698" 10 | 11 | [[package]] 12 | name = "ansi_term" 13 | version = "0.12.1" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 16 | dependencies = [ 17 | "winapi", 18 | ] 19 | 20 | [[package]] 21 | name = "bitflags" 22 | version = "1.3.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 25 | 26 | [[package]] 27 | name = "cfg-if" 28 | version = "1.0.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 31 | 32 | [[package]] 33 | name = "ctor" 34 | version = "0.1.21" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa" 37 | dependencies = [ 38 | "quote", 39 | "syn", 40 | ] 41 | 42 | [[package]] 43 | name = "diff" 44 | version = "0.1.12" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" 47 | 48 | [[package]] 49 | name = "dirs" 50 | version = "4.0.0" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" 53 | dependencies = [ 54 | "dirs-sys", 55 | ] 56 | 57 | [[package]] 58 | name = "dirs-sys" 59 | version = "0.3.6" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" 62 | dependencies = [ 63 | "libc", 64 | "redox_users", 65 | "winapi", 66 | ] 67 | 68 | [[package]] 69 | name = "getrandom" 70 | version = "0.2.3" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" 73 | dependencies = [ 74 | "cfg-if", 75 | "libc", 76 | "wasi", 77 | ] 78 | 79 | [[package]] 80 | name = "kn" 81 | version = "0.3.3" 82 | dependencies = [ 83 | "alphanumeric-sort", 84 | "dirs", 85 | "pico-args", 86 | "powierza-coefficient", 87 | "pretty_assertions", 88 | "serde", 89 | "serde_derive", 90 | "thiserror", 91 | "toml", 92 | ] 93 | 94 | [[package]] 95 | name = "libc" 96 | version = "0.2.112" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" 99 | 100 | [[package]] 101 | name = "output_vt100" 102 | version = "0.1.2" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" 105 | dependencies = [ 106 | "winapi", 107 | ] 108 | 109 | [[package]] 110 | name = "pico-args" 111 | version = "0.4.2" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" 114 | 115 | [[package]] 116 | name = "powierza-coefficient" 117 | version = "1.0.2" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "04123079750026568dff0e68efe1ca676f6686023f3bf7686b87dab661c0375b" 120 | 121 | [[package]] 122 | name = "pretty_assertions" 123 | version = "0.7.2" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "1cab0e7c02cf376875e9335e0ba1da535775beb5450d21e1dffca068818ed98b" 126 | dependencies = [ 127 | "ansi_term", 128 | "ctor", 129 | "diff", 130 | "output_vt100", 131 | ] 132 | 133 | [[package]] 134 | name = "proc-macro2" 135 | version = "1.0.30" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70" 138 | dependencies = [ 139 | "unicode-xid", 140 | ] 141 | 142 | [[package]] 143 | name = "quote" 144 | version = "1.0.10" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" 147 | dependencies = [ 148 | "proc-macro2", 149 | ] 150 | 151 | [[package]] 152 | name = "redox_syscall" 153 | version = "0.2.10" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" 156 | dependencies = [ 157 | "bitflags", 158 | ] 159 | 160 | [[package]] 161 | name = "redox_users" 162 | version = "0.4.0" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" 165 | dependencies = [ 166 | "getrandom", 167 | "redox_syscall", 168 | ] 169 | 170 | [[package]] 171 | name = "serde" 172 | version = "1.0.131" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "b4ad69dfbd3e45369132cc64e6748c2d65cdfb001a2b1c232d128b4ad60561c1" 175 | 176 | [[package]] 177 | name = "serde_derive" 178 | version = "1.0.131" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "b710a83c4e0dff6a3d511946b95274ad9ca9e5d3ae497b63fda866ac955358d2" 181 | dependencies = [ 182 | "proc-macro2", 183 | "quote", 184 | "syn", 185 | ] 186 | 187 | [[package]] 188 | name = "syn" 189 | version = "1.0.80" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" 192 | dependencies = [ 193 | "proc-macro2", 194 | "quote", 195 | "unicode-xid", 196 | ] 197 | 198 | [[package]] 199 | name = "thiserror" 200 | version = "1.0.30" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" 203 | dependencies = [ 204 | "thiserror-impl", 205 | ] 206 | 207 | [[package]] 208 | name = "thiserror-impl" 209 | version = "1.0.30" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" 212 | dependencies = [ 213 | "proc-macro2", 214 | "quote", 215 | "syn", 216 | ] 217 | 218 | [[package]] 219 | name = "toml" 220 | version = "0.5.8" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" 223 | dependencies = [ 224 | "serde", 225 | ] 226 | 227 | [[package]] 228 | name = "unicode-xid" 229 | version = "0.2.2" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 232 | 233 | [[package]] 234 | name = "wasi" 235 | version = "0.10.2+wasi-snapshot-preview1" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 238 | 239 | [[package]] 240 | name = "winapi" 241 | version = "0.3.9" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 244 | dependencies = [ 245 | "winapi-i686-pc-windows-gnu", 246 | "winapi-x86_64-pc-windows-gnu", 247 | ] 248 | 249 | [[package]] 250 | name = "winapi-i686-pc-windows-gnu" 251 | version = "0.4.0" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 254 | 255 | [[package]] 256 | name = "winapi-x86_64-pc-windows-gnu" 257 | version = "0.4.0" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 260 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kn" 3 | version = "0.3.3" 4 | edition = "2018" 5 | autobins = false 6 | include = [ 7 | "**/*.rs", 8 | "init", 9 | "Cargo.toml", 10 | "Cargo.lock", 11 | "README.md", 12 | "LICENSE.txt", 13 | "CHANGELOG.md", 14 | "rustfmt.toml", 15 | ] 16 | 17 | authors = ["micouy"] 18 | description = "nvgt/fldrs/qckly" 19 | repository = "https://github.com/micouy/kn" 20 | readme = "README.md" 21 | license = "MIT" 22 | 23 | categories = ["command-line-utilities", "filesystem"] 24 | keywords = ["cli", "utility", "filesystem"] 25 | 26 | [[bin]] 27 | name = "_kn" 28 | path = "src/main.rs" 29 | 30 | [dependencies] 31 | alphanumeric-sort = "1.4" 32 | thiserror = "1.0" 33 | pico-args = { version = "0.4", features = [] } 34 | powierza-coefficient = "1.0.2" 35 | serde_derive = "1.0" 36 | serde = "1.0" 37 | toml = "0.5" 38 | dirs = "4.0" 39 | 40 | [dev-dependencies] 41 | pretty_assertions = "0.7" 42 | 43 | [profile.release] 44 | lto = true 45 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mikołaj Powierża 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `kn` — nvgt/fldrs/qckly 2 | 3 | ![Github Actions badge](https://github.com/micouy/kn/actions/workflows/tests.yml/badge.svg) 4 | [![crates.io badge](https://img.shields.io/crates/v/kn.svg)](https://crates.io/crates/kn) 5 | 6 |

7 | 8 |

9 | 10 | ```fish 11 | cargo install kn 12 | ``` 13 | 14 | **Then follow the [configuration instructions](#configuring-your-shell).** 15 | 16 | # Features 17 | 18 | - [Abbreviations](#abbreviations) 19 | - [Wildcards](#wildcards) 20 | - [Multiple dots](#multiple-dots) 21 | - [`--exclude-old-pwd`](#--exclude-old-pwd) 22 | 23 | ## Abbreviations 24 | 25 | You can use `kn` just like you'd use `cd`. The difference is that you can also navigate with abbreviations instead of full dir names. **For example, instead of `foo/bar` you can type `fo/ba`.** 26 | 27 | ``` 28 | . 29 | ├── foo 30 | │ └── bar 31 | ├── bar 32 | ├── photosxxxxxxxxxxx2021 33 | └── photosxxxxxxxxxxx2020 34 | ``` 35 | 36 | ```fish 37 | kn foo/bar # Use `kn` just like `cd`... 38 | kn fo/ba # ...or navigate with abbreviations! No asterisks required. 39 | kn pho2021 # Type only the significant parts of the dir name. You can skip the middle part. 40 | ``` 41 | 42 | ## Wildcards 43 | 44 | You can also use wildcards `-` to avoid typing a dir name altogether i.e. `kn -/ba` to go to `foo/bar`. Note that `kn f-/b-` will not match `foo/bar`. In this case `-` functions as a literal character. 45 | 46 | ```fish 47 | kn -/bar # Wildcards can be used to skip a dir name altogether (changes dir to ./foo/bar/). 48 | ``` 49 | 50 | ## Multiple dots 51 | 52 | `kn` splits the arg into two parts, a prefix and a sequence of abbreviations. The prefix may contain components like `c:/`, `/`, `~/`, `.`, `..` and it is treated as a literal path. It may also contain components with more than two dots, which are interpreted like this: 53 | 54 | ```fish 55 | kn .. # Go to parent dir (as usual). 56 | kn ... # Go to grandparent dir (same as ../..). 57 | kn .... # Go to grand-grandparent dir (same as ../../..). 58 | 59 | kn ........ # Type as many dots as you want! 60 | kn .../.... # This works as well. 61 | 62 | kn .../..../abbr # You can put abbreviations after the prefix. 63 | ``` 64 | 65 | **If any of the mentioned components occurs in the path after an abbreviation, it is treated as an abbreviation.** 66 | 67 | ```fish 68 | kn ./../foo/bar/../baz 69 | # ^---^ prefix 70 | # ^------------^ abbreviations 71 | ``` 72 | 73 | `.` and the first `..` mean _current dir_ and _parent dir_, while the second `..` is treated as an abbreviation, that is, it will match a dir name containing two dots. 74 | 75 | ## `--exclude-old-pwd` 76 | 77 | This flag excludes your previous location from the search. You don't have to type it when using `kn`, just set it in your shell script (notice the underscore in `_kn`): 78 | 79 | ```fish 80 | _kn init --shell fish --exclude-old-pwd 81 | ``` 82 | 83 | It's useful when two paths match your abbreviation and you enter the wrong one: 84 | 85 | ```fish 86 | my-files/ 87 | $ kn d 88 | 89 | my-files/dir-1/ 90 | $ kn - 91 | 92 | my-files/ 93 | $ kn d # just press arrow up twice 94 | 95 | my-files/dir-2/ 96 | $ # success! 97 | ``` 98 | 99 | In order for `kn` to exclude the previous location there must be at least one other match and the provided arg must **not** be a literal path (that is, it must be an abbreviation). 100 | 101 | # Installation 102 | 103 | Make sure to [configure your shell](#configuring-your-shell) after the installation. 104 | 105 | ## From `crates.io` 106 | 107 | ```fish 108 | cargo install kn 109 | ``` 110 | 111 | ## From source 112 | 113 | 1. `git clone https://github.com/micouy/kn.git` 114 | 2. `cd kn` 115 | 3. Put the binary in a folder that is in `PATH`: 116 | 117 | `cargo build -Z unstable-options --out-dir DIR_IN_PATH --release` 118 | 119 | Or just build it and copy the binary to that dir: 120 | 121 | `cargo build --release` 122 | 123 | `cp target/release/_kn DIR_IN_PATH` 124 | 125 | ## From the release page 126 | 127 | Download a binary of the [latest release](https://github.com/micouy/kn/releases/latest) for your OS and move it to a directory which is in your `$PATH`. You may need to change the binary's permissions by running `chmod +x _kn`. 128 | 129 | If there are any problems with the pre-compiled binaries, file an issue. 130 | 131 | ## Configuring your shell 132 | 133 | Then add this line to the config of your shell (notice the underscore in `_kn`): 134 | 135 | - **fish** (usually `~/.config/fish/config.fish`): 136 | 137 | `_kn init --shell fish | source` 138 | 139 | - **bash** (usually `~/.bashrc`): 140 | 141 | `eval "$(_kn init --shell bash)"` 142 | 143 | - **zsh** (usually `~/.zshrc`): 144 | 145 | `eval "$(_kn init --shell zsh)"` 146 | 147 | You may also want to enable [the `--exclude-old-pwd` flag](#--exclude-old-pwd). To be able to use `kn`, reload your config or launch a new shell instance. 148 | 149 | # Help wanted 150 | 151 | In this project I have entered a lot of areas I have little knowledge about. Contributions and criticism are very welcome. Here are some things you can do: 152 | 153 | - Check the correctness of scripts in [init/](init/). 154 | - Add scripts and installation instructions for shells other than `fish`, `bash` and `zsh`. 155 | - Review Github Actions workflows in [.github/workflows/](.github/workflows/). 156 | 157 | # The algorithm 158 | 159 | `kn` doesn't track frecency or any other statistics. It searches the disk for paths matching the abbreviation. If it finds multiple matching paths, it orders them in such a way: 160 | 161 | 1. Compare each component against the corresponding component of the abbreviation. The components of the path may or may not match the abbreviation. If a component matches the abbreviation, there are three possible results: 162 | 163 | - `Complete` if the corresponding components are equal. 164 | - `Prefix` if the abbreviation's component is a prefix of the path's component. 165 | - `Subsequence(coefficient)` if the abbreviation's component is a subsequence of the path's component. The `coefficient` is the [_Powierża coefficient_](https://github.com/micouy/powierza-coefficient) of these strings. 166 | 167 | Retain only these paths in which all of the components match. 168 | 169 | 2. Order the paths in reverse lexicographical order (compare the results from right to left). `Complete` then `Prefix` then `Subsequence`. Order paths with `Subsequence` result in ascending order of their `coefficient`'s. 170 | 3. Order paths with the same results with [`alphanumeric_sort::compare_os_str`](https://docs.rs/alphanumeric-sort/1.4.3/alphanumeric_sort/fn.compare_os_str.html). 171 | -------------------------------------------------------------------------------- /init/kn.bash: -------------------------------------------------------------------------------- 1 | # Code repurposed from `zoxide/templates/bash.txt`. 2 | 3 | 4 | function kn() {{ 5 | if [[ "$#" -eq 0 ]]; then 6 | # no args provided 7 | 8 | \builtin cd 9 | elif [[ "$#" -eq 1 ]] && [[ "$1" = '-' ]]; then 10 | # only dash provided, go to previous location if it exists 11 | 12 | if [[ -n "${{OLDPWD}}" ]]; then 13 | \builtin cd "${{OLDPWD}}" 14 | fi 15 | else 16 | # otherwise, query _kn 17 | 18 | \builtin local __kn_result 19 | __kn_result="$({query_command})" && \builtin cd "${{__kn_result}}" 20 | fi 21 | }} 22 | -------------------------------------------------------------------------------- /init/kn.fish: -------------------------------------------------------------------------------- 1 | # Code repurposed from `zoxide/templates/fish.txt`. 2 | 3 | 4 | function kn 5 | set argc (count $argv) 6 | 7 | if test $argc -eq 0 8 | # no args provided 9 | 10 | cd 11 | else if begin; test $argc -eq 1; and test "$argv[1]" = '-'; end 12 | # only dash provided, go to previous location if it exists 13 | 14 | #if test (count $dirprev) -ne 0 15 | cd $dirprev[-1] 16 | # end 17 | else 18 | # otherwise, query _kn 19 | 20 | set -l __kn_result (command {query_command}) 21 | 22 | and if test -d "$__kn_result" 23 | cd "$__kn_result" 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /init/kn.zsh: -------------------------------------------------------------------------------- 1 | # Code repurposed from `zoxide/templates/zsh.txt`. 2 | 3 | 4 | function kn() {{ 5 | if [[ "$#" -eq 0 ]]; then 6 | # no args provided 7 | 8 | \builtin cd 9 | elif [[ "$#" -eq 1 ]] && [[ "$1" = '-' ]]; then 10 | # only dash provided, go to previous location 11 | 12 | if [ -n "${{OLDPWD}}" ]; then 13 | \builtin cd "${{OLDPWD}}" 14 | fi 15 | else 16 | # otherwise, query _kn 17 | 18 | \builtin local __kn_result 19 | __kn_result="$({query_command})" \ 20 | && \builtin cd "${{__kn_result}}" 21 | fi 22 | }} 23 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_layout = "HorizontalVertical" 2 | max_width = 80 3 | comment_width = 80 4 | normalize_comments = false 5 | match_arm_blocks = false 6 | wrap_comments = true 7 | blank_lines_upper_bound = 1 8 | use_field_init_shorthand = true 9 | reorder_impl_items = true 10 | -------------------------------------------------------------------------------- /src/abbr.rs: -------------------------------------------------------------------------------- 1 | //! Abbreviations. 2 | 3 | use std::cmp::{Ord, Ordering}; 4 | 5 | use powierza_coefficient::powierża_coefficient; 6 | 7 | /// A component of the user's query. 8 | /// 9 | /// It is used in comparing and ordering of found paths. Read more in 10 | /// [`Congruence`'s docs](Congruence). 11 | #[derive(Debug, Clone)] 12 | pub enum Abbr { 13 | /// Wildcard matches every component with congruence 14 | /// [`Complete`](Congruence::Complete). 15 | Wildcard, 16 | 17 | /// Literal abbreviation. 18 | Literal(String), 19 | } 20 | 21 | impl Abbr { 22 | /// Constructs [`Abbr::Wildcard`](Abbr::Wildcard) if the 23 | /// string slice is '-', otherwise constructs 24 | /// wrapped [`Abbr::Literal`](Abbr::Literal) with the abbreviation 25 | /// mapped to its ASCII lowercase equivalent. 26 | pub fn new_sanitized(abbr: &str) -> Self { 27 | if abbr == "-" { 28 | Self::Wildcard 29 | } else { 30 | Self::Literal(abbr.to_ascii_lowercase()) 31 | } 32 | } 33 | 34 | /// Compares a component against the abbreviation. 35 | pub fn compare(&self, component: &str) -> Option { 36 | // What about characters with accents? [https://eev.ee/blog/2015/09/12/dark-corners-of-unicode/] 37 | let component = component.to_ascii_lowercase(); 38 | 39 | match self { 40 | Self::Wildcard => Some(Congruence::Complete), 41 | Self::Literal(literal) => 42 | if literal.is_empty() || component.is_empty() { 43 | None 44 | } else if *literal == component { 45 | Some(Congruence::Complete) 46 | } else if component.starts_with(literal) { 47 | Some(Congruence::Prefix) 48 | } else { 49 | powierża_coefficient(literal, &component) 50 | .map(Congruence::Subsequence) 51 | }, 52 | } 53 | } 54 | } 55 | 56 | /// The strength of the match between an abbreviation and a component. 57 | /// 58 | /// [`Congruence`](Congruence) is used to order path components in the following 59 | /// way: 60 | /// 61 | /// 1. Components are first ordered based on how well they match the 62 | /// abbreviation — first [`Complete`](Congruence::Complete), then 63 | /// [`Prefix`](Congruence::Prefix), then 64 | /// [`Subsequence`](Congruence::Subsequence). 65 | /// 2. Components with congruence [`Subsequence`](Congruence::Subsequence) are 66 | /// ordered by their [Powierża coefficient](https://github.com/micouy/powierza-coefficient). 67 | /// 3. If the order of two components cannot be determined based on the above, [`alphanumeric_sort`](https://docs.rs/alphanumeric-sort) is used. 68 | /// 69 | /// Below are the results of matching components against abbreviation `abc`: 70 | /// 71 | /// | Component | Match strength | 72 | /// |-------------|------------------------------------------| 73 | /// | `abc` | [`Complete`](Congruence::Complete) | 74 | /// | `abc___` | [`Prefix`](Congruence::Prefix) | 75 | /// | `_a_b_c_` | [`Subsequence`](Congruence::Subsequence) | 76 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 77 | pub enum Congruence { 78 | /// Either the abbreviation and the component are the same or the 79 | /// abbreviation is a wildcard. 80 | Complete, 81 | 82 | /// The abbreviation is a prefix of the component. 83 | Prefix, 84 | 85 | /// The abbreviation's characters form a subsequence of the component's 86 | /// characters. The field contains the Powierża coefficient of the pair of 87 | /// strings. 88 | Subsequence(u32), 89 | } 90 | 91 | use Congruence::*; 92 | 93 | impl PartialOrd for Congruence { 94 | fn partial_cmp(&self, other: &Self) -> Option { 95 | Some(Ord::cmp(self, other)) 96 | } 97 | } 98 | 99 | impl Ord for Congruence { 100 | fn cmp(&self, other: &Self) -> Ordering { 101 | use Ordering::*; 102 | 103 | match (self, other) { 104 | (Complete, Complete) => Equal, 105 | (Complete, Prefix) => Less, 106 | (Complete, Subsequence(_)) => Less, 107 | 108 | (Prefix, Complete) => Greater, 109 | (Prefix, Prefix) => Equal, 110 | (Prefix, Subsequence(_)) => Less, 111 | 112 | (Subsequence(_), Complete) => Greater, 113 | (Subsequence(_), Prefix) => Greater, 114 | (Subsequence(dist_a), Subsequence(dist_b)) => dist_a.cmp(dist_b), 115 | } 116 | } 117 | } 118 | 119 | #[cfg(test)] 120 | mod test { 121 | use super::*; 122 | 123 | #[test] 124 | fn test_congruence_ordering() { 125 | assert!(Complete < Prefix); 126 | assert!(Complete < Subsequence(1)); 127 | assert!(Prefix < Subsequence(1)); 128 | assert!(Subsequence(1) < Subsequence(1000)); 129 | } 130 | 131 | #[test] 132 | fn test_compare_abbr() { 133 | let abbr = Abbr::new_sanitized("abcjkl"); 134 | 135 | assert_variant!(abbr.compare("abcjkl"), Some(Complete)); 136 | assert_variant!(abbr.compare("abcjkl_"), Some(Prefix)); 137 | assert_variant!(abbr.compare("_abcjkl"), Some(Subsequence(0))); 138 | assert_variant!(abbr.compare("abc_jkl"), Some(Subsequence(1))); 139 | assert_variant!(abbr.compare("abc____jkl"), Some(Subsequence(1))); 140 | 141 | assert_variant!(abbr.compare("xyz"), None); 142 | assert_variant!(abbr.compare(""), None); 143 | } 144 | 145 | #[test] 146 | fn test_compare_abbr_different_cases() { 147 | let abbr = Abbr::new_sanitized("AbCjKl"); 148 | 149 | assert_variant!(abbr.compare("aBcJkL"), Some(Complete)); 150 | assert_variant!(abbr.compare("AbcJkl_"), Some(Prefix)); 151 | assert_variant!(abbr.compare("_aBcjKl"), Some(Subsequence(0))); 152 | assert_variant!(abbr.compare("abC_jkL"), Some(Subsequence(1))); 153 | } 154 | 155 | #[test] 156 | fn test_empty_abbr_empty_component() { 157 | let empty = ""; 158 | 159 | let abbr = Abbr::new_sanitized(empty); 160 | assert_variant!(abbr.compare("non empty component"), None); 161 | 162 | let abbr = Abbr::new_sanitized("non empty abbr"); 163 | assert_variant!(abbr.compare(empty), None); 164 | } 165 | 166 | #[test] 167 | fn test_order_paths() { 168 | fn sort<'a>(paths: &'a Vec<&'a str>, abbr: &str) -> Vec<&'a str> { 169 | let abbr = Abbr::new_sanitized(abbr); 170 | let mut paths = paths.clone(); 171 | paths.sort_by_key(|path| abbr.compare(path).unwrap()); 172 | 173 | paths 174 | } 175 | 176 | let paths = vec!["playground", "plotka"]; 177 | assert_eq!(paths, sort(&paths, "pla")); 178 | 179 | let paths = vec!["veccentric", "vehiccles"]; 180 | assert_eq!(paths, sort(&paths, "vecc")); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | //! Arg parsing. 2 | 3 | use std::path::PathBuf; 4 | 5 | use crate::error::Error; 6 | 7 | /// Subcommand with its args. 8 | #[derive(Debug)] 9 | pub enum Subcommand { 10 | /// The [`init`](crate::init::init) subcommand. 11 | Init { 12 | /// User's shell. 13 | shell: Shell, 14 | 15 | /// The value of the `--exclude-old-pwd` flag. 16 | exclude_old_pwd: bool, 17 | }, 18 | /// The [`query`](crate::query::query) subcommand. 19 | Query { 20 | /// The abbr. 21 | abbr: String, 22 | 23 | /// Path excluded from search. 24 | excluded: Option, 25 | }, 26 | } 27 | 28 | /// The value of the `--shell` arg. 29 | #[derive(Debug)] 30 | pub enum Shell { 31 | #[allow(missing_docs)] 32 | Fish, 33 | 34 | #[allow(missing_docs)] 35 | Zsh, 36 | 37 | #[allow(missing_docs)] 38 | Bash, 39 | } 40 | 41 | const SUBCOMMAND_ARG: &str = "subcommand"; 42 | const SHELL_ARG: &str = "--shell"; 43 | const ABBR_ARG: &str = "--abbr"; 44 | const EXCLUDE_OLD_PWD_ARG: &str = "--exclude-old-pwd"; 45 | const EXCLUDE_ARG: &str = "--exclude"; 46 | const FISH_ARG: &str = "fish"; 47 | const BASH_ARG: &str = "bash"; 48 | const ZSH_ARG: &str = "zsh"; 49 | const INIT_SUBCOMMAND: &str = "init"; 50 | const QUERY_SUBCOMMAND: &str = "query"; 51 | 52 | /// Parses CLI args. 53 | pub fn parse_args() -> Result { 54 | let mut pargs = pico_args::Arguments::from_env(); 55 | 56 | let subcommand = pargs 57 | .subcommand()? 58 | .ok_or(pico_args::Error::MissingArgument)?; 59 | 60 | match subcommand.as_str() { 61 | INIT_SUBCOMMAND => { 62 | let shell: String = pargs.value_from_str(SHELL_ARG)?; 63 | 64 | let shell = match shell.as_str() { 65 | FISH_ARG => Shell::Fish, 66 | ZSH_ARG => Shell::Zsh, 67 | BASH_ARG => Shell::Bash, 68 | _ => return Err(Error::InvalidArgValue(SHELL_ARG.to_string())), 69 | }; 70 | 71 | let exclude_old_pwd = pargs.contains(EXCLUDE_OLD_PWD_ARG); 72 | 73 | Ok(Subcommand::Init { 74 | shell, 75 | exclude_old_pwd, 76 | }) 77 | } 78 | QUERY_SUBCOMMAND => { 79 | let abbr = pargs.value_from_str(ABBR_ARG)?; 80 | let excluded = pargs.opt_value_from_os_str::<_, _, Error>( 81 | EXCLUDE_ARG, 82 | |os_str| Ok(PathBuf::from(os_str)), 83 | )?; 84 | 85 | Ok(Subcommand::Query { abbr, excluded }) 86 | } 87 | _ => Err(Error::InvalidArgValue(SUBCOMMAND_ARG.to_string())), 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error. 2 | 3 | use thiserror::Error; 4 | 5 | /// Error. 6 | #[derive(Error, Debug)] 7 | pub enum Error { 8 | /// Wrapper around [`std::io::Error`](std::io::Error). 9 | #[error("IO error `{0}`.")] 10 | IO(#[from] std::io::Error), 11 | 12 | /// Non-Unicode input received. 13 | #[error("Non-Unicode input received.")] 14 | NonUnicodeInput, 15 | 16 | /// Path not found. 17 | #[error("Path not found.")] 18 | PathNotFound, 19 | 20 | /// An invalid arg value. 21 | #[error("Value of arg `{0}` is invalid.")] 22 | InvalidArgValue(String), 23 | 24 | /// Wrapper around [`pico_args::Error`](pico_args::Error). 25 | #[error("Args error: `{0}`.")] 26 | Args(#[from] pico_args::Error), 27 | 28 | /// Unexpected abbr component. 29 | #[error("Unexpected abbr component `{0}`.")] 30 | UnexpectedAbbrComponent(String), 31 | } 32 | -------------------------------------------------------------------------------- /src/init.rs: -------------------------------------------------------------------------------- 1 | //! The `init` subcommand. 2 | 3 | use crate::args::Shell; 4 | 5 | /// The `init` subcommand. 6 | /// 7 | /// Prints a shell script for initializing `kn`. The script 8 | /// can be configured. The `init` subcommand takes an arg `--shell`, 9 | /// specifying the used shell, and a flag `--exclude-old-pwd` which 10 | /// enables excluding the previous location from the search (only if there 11 | /// are other matching dirs). 12 | pub fn init(shell: Shell, exclude_old_pwd: bool) -> String { 13 | match shell { 14 | Shell::Fish => { 15 | let query_command = if exclude_old_pwd { 16 | "_kn query --exclude \"$dirprev[-1]\" --abbr \"$argv\"" 17 | } else { 18 | "_kn query --abbr \"$argv\"" 19 | }; 20 | 21 | format!( 22 | include_str!("../init/kn.fish"), 23 | query_command = query_command 24 | ) 25 | } 26 | Shell::Zsh => { 27 | let query_command = if exclude_old_pwd { 28 | "_kn query --exclude \"${OLDPWD}\" --abbr \"$@\"" 29 | } else { 30 | "_kn query --abbr \"$@\"" 31 | }; 32 | 33 | format!( 34 | include_str!("../init/kn.zsh"), 35 | query_command = query_command 36 | ) 37 | } 38 | Shell::Bash => { 39 | let query_command = if exclude_old_pwd { 40 | "_kn query --exclude \"${OLDPWD}\" --abbr \"$@\"" 41 | } else { 42 | "_kn query --abbr \"$@\"" 43 | }; 44 | 45 | format!( 46 | include_str!("../init/kn.bash"), 47 | query_command = query_command 48 | ) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | 3 | //! Alternative to `cd`. Navigate by typing abbreviations of paths. 4 | 5 | use std::process::exit; 6 | 7 | #[macro_use] 8 | pub mod utils; 9 | pub mod abbr; 10 | pub mod args; 11 | pub mod error; 12 | 13 | pub mod init; 14 | pub mod query; 15 | 16 | use crate::{args::Subcommand, error::Error}; 17 | 18 | /// A wrapper around the main function. 19 | fn main() { 20 | match _main() { 21 | Err(err) => { 22 | eprintln!("{}", err); 23 | 24 | exit(1); 25 | } 26 | Ok(()) => { 27 | exit(0); 28 | } 29 | } 30 | } 31 | 32 | /// The main function. 33 | fn _main() -> Result<(), Error> { 34 | let subcommand = args::parse_args()?; 35 | 36 | match subcommand { 37 | Subcommand::Init { 38 | shell, 39 | exclude_old_pwd, 40 | } => { 41 | let script = init::init(shell, exclude_old_pwd); 42 | print!("{}", script); 43 | 44 | Ok(()) 45 | } 46 | Subcommand::Query { abbr, excluded } => { 47 | match query::query(&abbr, excluded) { 48 | Err(error) => Err(error), 49 | Ok(path) => { 50 | println!("{}", path.display()); 51 | 52 | Ok(()) 53 | } 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/query.rs: -------------------------------------------------------------------------------- 1 | //! The `query` subcommand. 2 | 3 | use crate::{ 4 | abbr::{Abbr, Congruence}, 5 | error::Error, 6 | }; 7 | 8 | use std::{ 9 | convert::AsRef, 10 | ffi::{OsStr, OsString}, 11 | fs::DirEntry, 12 | mem, 13 | path::{Component, Path, PathBuf}, 14 | }; 15 | 16 | use alphanumeric_sort::compare_os_str; 17 | 18 | /// A path matching an abbreviation. 19 | /// 20 | /// Stores [`Congruence`](Congruence)'s of its ancestors, with that of the 21 | /// closest ancestors first (so that it can be compared 22 | /// [lexicographically](std::cmp::Ord#lexicographical-comparison). 23 | struct Finding { 24 | file_name: OsString, 25 | path: PathBuf, 26 | congruence: Vec, 27 | } 28 | 29 | /// Returns an interator over directory's children matching the abbreviation. 30 | fn get_matching_children<'a, P>( 31 | path: &'a P, 32 | abbr: &'a Abbr, 33 | parent_congruence: &'a [Congruence], 34 | ) -> impl Iterator + 'a 35 | where 36 | P: AsRef, 37 | { 38 | let filter_map_entry = move |entry: DirEntry| { 39 | let file_type = entry.file_type().ok()?; 40 | 41 | if file_type.is_dir() || file_type.is_symlink() { 42 | let file_name: String = entry.file_name().into_string().ok()?; 43 | 44 | if let Some(congruence) = abbr.compare(&file_name) { 45 | let mut entry_congruence = parent_congruence.to_vec(); 46 | entry_congruence.insert(0, congruence); 47 | 48 | return Some(Finding { 49 | file_name: entry.file_name(), 50 | congruence: entry_congruence, 51 | path: entry.path(), 52 | }); 53 | } 54 | } 55 | 56 | None 57 | }; 58 | 59 | path.as_ref() 60 | .read_dir() 61 | .ok() 62 | .map(|reader| { 63 | reader 64 | .filter_map(|entry| entry.ok()) 65 | .filter_map(filter_map_entry) 66 | }) 67 | .into_iter() 68 | .flatten() 69 | } 70 | 71 | /// The `query` subcommand. 72 | /// 73 | /// It takes two args — `--abbr` and `--exclude` (optionally). The value of 74 | /// `--abbr` gets split into a prefix containing components like `c:/`, `/`, 75 | /// `~/`, and dots, and [`Abbr`](Abbr)'s. If there is more than one dir matching 76 | /// the query, the value of `--exclude` is excluded from the search. 77 | pub fn query

(arg: &P, excluded: Option) -> Result 78 | where 79 | P: AsRef, 80 | { 81 | // If the arg is a real path and not an abbreviation, return it. It 82 | // prevents potential unexpected behavior due to abbreviation expansion. 83 | // For example, `kn` doesn't allow for any component other than `Normal` in 84 | // the abbreviation but the arg itself may be a valid path. `kn` should only 85 | // behave differently from `cd` in situations where `cd` would fail. 86 | if arg.as_ref().is_dir() { 87 | return Ok(arg.as_ref().into()); 88 | } 89 | 90 | let (prefix, abbrs) = parse_arg(&arg)?; 91 | let start_dir = match prefix { 92 | Some(start_dir) => start_dir, 93 | None => std::env::current_dir()?, 94 | }; 95 | 96 | match abbrs.as_slice() { 97 | [] => Ok(start_dir), 98 | [first_abbr, abbrs @ ..] => { 99 | let mut current_level = 100 | get_matching_children(&start_dir, first_abbr, &[]) 101 | .collect::>(); 102 | let mut next_level = vec![]; 103 | 104 | for abbr in abbrs { 105 | let children = current_level 106 | .iter() 107 | .map(|parent| { 108 | get_matching_children( 109 | &parent.path, 110 | abbr, 111 | &parent.congruence, 112 | ) 113 | }) 114 | .flatten(); 115 | 116 | next_level.clear(); 117 | next_level.extend(children); 118 | 119 | mem::swap(&mut next_level, &mut current_level); 120 | } 121 | 122 | let cmp_findings = |finding_a: &Finding, finding_b: &Finding| { 123 | finding_a.congruence.cmp(&finding_b.congruence).then( 124 | compare_os_str(&finding_a.file_name, &finding_b.file_name), 125 | ) 126 | }; 127 | 128 | let found_path = match excluded { 129 | Some(excluded) if current_level.len() > 1 => current_level 130 | .into_iter() 131 | .filter(|finding| finding.path != excluded) 132 | .min_by(cmp_findings) 133 | .map(|Finding { path, .. }| path), 134 | _ => current_level 135 | .into_iter() 136 | .min_by(cmp_findings) 137 | .map(|Finding { path, .. }| path), 138 | }; 139 | 140 | found_path.ok_or(Error::PathNotFound) 141 | } 142 | } 143 | } 144 | 145 | /// Checks if the component contains only dots and returns the equivalent number 146 | /// of [`ParentDir`](Component::ParentDir) components if it does. 147 | /// 148 | /// It is the number of dots, less one. For example, `...` is converted to 149 | /// `../..`, `....` to `../../..` etc. 150 | fn parse_dots(component: &str) -> Option { 151 | component 152 | .chars() 153 | .try_fold( 154 | 0, 155 | |n_dots, c| if c == '.' { Some(n_dots + 1) } else { None }, 156 | ) 157 | .and_then(|n_dots| if n_dots > 1 { Some(n_dots - 1) } else { None }) 158 | } 159 | 160 | /// Extracts leading components of the path that are not parts of the 161 | /// abbreviation. 162 | /// 163 | /// The prefix is the path where the search starts. If there is no prefix (when 164 | /// the path consists only of normal components), the search starts in the 165 | /// current directory, just as you'd expect. The function collects each 166 | /// [`Prefix`](Component::Prefix), [`RootDir`](Component::RootDir), 167 | /// [`CurDir`](Component::CurDir), and [`ParentDir`](Component::ParentDir) 168 | /// components and stops at the first [`Normal`](Component::Normal) component 169 | /// **unless** it only contains dots. In this case, it converts it to as many 170 | /// [`ParentDir`](Component::ParentDir)'s as there are dots in this component, 171 | /// less one. For example, `...` is converted to `../..`, `....` to `../../..` 172 | /// etc. 173 | fn extract_prefix<'a, P>( 174 | arg: &'a P, 175 | ) -> Result<(Option, impl Iterator> + 'a), Error> 176 | where 177 | P: AsRef + ?Sized + 'a, 178 | { 179 | use Component::*; 180 | 181 | let mut components = arg.as_ref().components().peekable(); 182 | let mut prefix: Option = None; 183 | let mut push_to_prefix = |component: Component| match &mut prefix { 184 | None => prefix = Some(PathBuf::from(&component)), 185 | Some(prefix) => prefix.push(component), 186 | }; 187 | let parse_dots_os = |component_os: &OsStr| { 188 | component_os 189 | .to_os_string() 190 | .into_string() 191 | .map_err(|_| Error::NonUnicodeInput) 192 | .map(|component| parse_dots(&component)) 193 | }; 194 | 195 | while let Some(component) = components.peek() { 196 | match component { 197 | Prefix(_) | RootDir | CurDir | ParentDir => 198 | push_to_prefix(*component), 199 | Normal(component_os) => { 200 | if let Some(n_dots) = parse_dots_os(component_os)? { 201 | (0..n_dots).for_each(|_| push_to_prefix(ParentDir)); 202 | } else { 203 | break; 204 | } 205 | } 206 | } 207 | 208 | let _consumed = components.next(); 209 | } 210 | 211 | Ok((prefix, components)) 212 | } 213 | 214 | /// Converts each component into [`Abbr`](Abbr) without checking 215 | /// the component's type. 216 | /// 217 | /// This may change in the future. 218 | fn parse_abbrs<'a, I>(components: I) -> Result, Error> 219 | where 220 | I: Iterator> + 'a, 221 | { 222 | use Component::*; 223 | 224 | let abbrs = components 225 | .into_iter() 226 | .map(|component| match component { 227 | Prefix(_) | RootDir | CurDir | ParentDir => { 228 | let component_string = component 229 | .as_os_str() 230 | .to_os_string() 231 | .to_string_lossy() 232 | .to_string(); 233 | 234 | Err(Error::UnexpectedAbbrComponent(component_string)) 235 | } 236 | Normal(component_os) => component_os 237 | .to_os_string() 238 | .into_string() 239 | .map_err(|_| Error::NonUnicodeInput) 240 | .map(|string| Abbr::new_sanitized(&string)), 241 | }) 242 | .collect::, _>>()?; 243 | 244 | Ok(abbrs) 245 | } 246 | 247 | /// Parses the provided argument into a prefix and [`Abbr`](Abbr)'s. 248 | fn parse_arg

(arg: &P) -> Result<(Option, Vec), Error> 249 | where 250 | P: AsRef, 251 | { 252 | let (prefix, suffix) = extract_prefix(arg)?; 253 | let abbrs = parse_abbrs(suffix)?; 254 | 255 | Ok((prefix, abbrs)) 256 | } 257 | 258 | #[cfg(test)] 259 | mod test { 260 | use super::*; 261 | 262 | use crate::utils::as_path; 263 | 264 | #[test] 265 | fn test_parse_dots() { 266 | assert_variant!(parse_dots(""), None); 267 | assert_variant!(parse_dots("."), None); 268 | assert_variant!(parse_dots(".."), Some(1)); 269 | assert_variant!(parse_dots("..."), Some(2)); 270 | assert_variant!(parse_dots("...."), Some(3)); 271 | assert_variant!(parse_dots("xyz"), None); 272 | assert_variant!(parse_dots("...dot"), None); 273 | } 274 | 275 | #[test] 276 | fn test_extract_prefix() { 277 | { 278 | let (prefix, suffix) = extract_prefix("suf/fix").unwrap(); 279 | let suffix = suffix.collect::(); 280 | 281 | assert_eq!(prefix, None); 282 | assert_eq!(as_path(&suffix), as_path("suf/fix")); 283 | } 284 | 285 | { 286 | let (prefix, suffix) = extract_prefix("./.././suf/fix").unwrap(); 287 | let suffix = suffix.collect::(); 288 | 289 | assert_eq!(prefix.unwrap(), as_path("./..")); 290 | assert_eq!(as_path(&suffix), as_path("suf/fix")); 291 | } 292 | 293 | { 294 | let (prefix, suffix) = extract_prefix(".../.../suf/fix").unwrap(); 295 | let suffix = suffix.collect::(); 296 | 297 | assert_eq!(prefix.unwrap(), as_path("../../../..")); 298 | assert_eq!(as_path(&suffix), as_path("suf/fix")); 299 | } 300 | } 301 | 302 | #[test] 303 | fn test_parse_arg_invalid_unicode() { 304 | #[cfg(unix)] 305 | { 306 | use std::ffi::OsStr; 307 | use std::os::unix::ffi::OsStrExt; 308 | 309 | let source = [0x66, 0x6f, 0x80, 0x6f]; 310 | let non_unicode_input = 311 | OsStr::from_bytes(&source[..]).to_os_string(); 312 | let result = parse_arg(&non_unicode_input); 313 | 314 | assert!(result.is_err()); 315 | } 316 | 317 | #[cfg(windows)] 318 | { 319 | use std::os::windows::prelude::*; 320 | 321 | let source = [0x0066, 0x006f, 0xd800, 0x006f]; 322 | let os_string = OsString::from_wide(&source[..]); 323 | let result = parse_arg(&non_unicode_input); 324 | 325 | assert!(result.is_err()); 326 | } 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | //! Utils. 2 | 3 | #[cfg(any(test, doc))] 4 | use std::{convert::AsRef, path::Path}; 5 | 6 | /// Asserts that the expression matches the variant. Optionally returns a value. 7 | /// 8 | /// Inspired by [`std::matches`](https://doc.rust-lang.org/stable/std/macro.matches.html). 9 | /// 10 | /// # Examples 11 | /// 12 | /// ``` 13 | /// # fn main() -> Option<()> { 14 | /// use kn::Congruence::*; 15 | /// 16 | /// let abbr = Abbr::new_sanitized("abcjkl"); 17 | /// let coeff_1 = assert_variant!(abbr.compare("abc_jkl"), Some(Subsequence(coeff)) => coeff); 18 | /// let coeff_2 = assert_variant!(abbr.compare("ab_cj_kl"), Some(Subsequence(coeff)) => coeff); 19 | /// assert!(coeff_1 < coeff_2); 20 | /// # Ok(()) 21 | /// # } 22 | /// ``` 23 | #[cfg(any(test, doc))] 24 | #[macro_export] 25 | macro_rules! assert_variant { 26 | ($expression_in:expr , $( $pattern:pat )|+ $( if $guard: expr )? $( => $expression_out:expr )? ) => { 27 | match $expression_in { 28 | $( $pattern )|+ $( if $guard )? => { $( $expression_out )? }, 29 | variant => panic!("{:?}", variant), 30 | } 31 | }; 32 | 33 | 34 | ($expression_in:expr , $( $pattern:pat )|+ $( if $guard: expr )? $( => $expression_out:expr)? , $panic:expr) => { 35 | match $expression_in { 36 | $( $pattern )|+ $( if $guard )? => { $( $expression_out )? }, 37 | _ => panic!($panic), 38 | } 39 | }; 40 | } 41 | 42 | /// Shorthand for `AsRef::as_ref(&x)`. 43 | #[cfg(any(test, doc))] 44 | pub fn as_path

(path: &P) -> &Path 45 | where 46 | P: AsRef + ?Sized, 47 | { 48 | path.as_ref() 49 | } 50 | --------------------------------------------------------------------------------