├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── benches ├── .keep ├── matcher.rs ├── matrix.rs └── scorer.rs ├── ci ├── before_deploy.sh ├── install.sh └── script.sh └── src ├── .keep ├── ansi ├── clear.rs ├── color.rs ├── cursor.rs ├── macros.rs ├── mod.rs └── style.rs ├── consts.rs ├── interface.rs ├── lib.rs ├── main.rs ├── matcher.rs ├── matrix.rs ├── scorer.rs ├── stdin.rs └── terminal ├── event.rs ├── input.rs └── mod.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Based on the "trust" template v0.1.2 2 | # https://github.com/japaric/trust/tree/v0.1.2 3 | 4 | dist: trusty 5 | language: rust 6 | services: docker 7 | sudo: required 8 | 9 | env: 10 | global: 11 | - CRATE_NAME=rff 12 | 13 | matrix: 14 | include: 15 | # ARM 16 | - env: TARGET=aarch64-unknown-linux-gnu 17 | - env: TARGET=arm-unknown-linux-gnueabi 18 | - env: TARGET=armv7-unknown-linux-gnueabihf 19 | 20 | # 686 21 | - env: TARGET=i686-unknown-linux-gnu 22 | - env: TARGET=i686-unknown-linux-musl 23 | 24 | # PowerPC 25 | - env: TARGET=powerpc-unknown-linux-gnu 26 | - env: TARGET=powerpc64-unknown-linux-gnu 27 | - env: TARGET=powerpc64le-unknown-linux-gnu 28 | 29 | # Good ol' x86_64 30 | - env: TARGET=x86_64-unknown-linux-gnu 31 | - env: TARGET=x86_64-unknown-linux-musl 32 | 33 | # macOS 34 | - env: TARGET=i686-apple-darwin 35 | os: osx 36 | - env: TARGET=x86_64-apple-darwin 37 | os: osx 38 | 39 | # Nightly 40 | - os: linux 41 | rust: nightly 42 | env: TARGET=i686-unknown-linux-musl 43 | - os: linux 44 | rust: nightly 45 | env: TARGET=x86_64-unknown-linux-musl 46 | - os: linux 47 | rust: nightly 48 | env: TARGET=x86_64-unknown-linux-gnu 49 | - os: osx 50 | rust: nightly 51 | env: TARGET=x86_64-apple-darwin 52 | 53 | # Beta 54 | - os: linux 55 | rust: beta 56 | env: TARGET=i686-unknown-linux-musl 57 | - os: linux 58 | rust: beta 59 | env: TARGET=x86_64-unknown-linux-musl 60 | - os: linux 61 | rust: beta 62 | env: TARGET=x86_64-unknown-linux-gnu 63 | - os: osx 64 | rust: beta 65 | env: TARGET=x86_64-apple-darwin 66 | 67 | before_install: 68 | - set -e 69 | - rustup self update 70 | 71 | install: 72 | - sh ci/install.sh 73 | - source ~/.cargo/env || true 74 | 75 | script: 76 | - bash ci/script.sh 77 | 78 | after_script: set +e 79 | 80 | before_deploy: 81 | - sh ci/before_deploy.sh 82 | 83 | deploy: 84 | api_key: 85 | secure: "uGQ8uifYOSOZqJtV4ydQ9tH+NMVWnCzRvX2Ngicc6jl/QzpGQfLyeaZP2QWssBQXsGmw5iPusvEPOVKmnkGffxfltsGd1zPaDFA83FcPAfC5zBDC5fjaCI8oQL9UqGxI3SGXnFKIQAjMW7HRDfhl/9jlk3KnTKYHjHwExJCzA6WwmMtrELbDiavGcfMkGkC69Jz3SDCbh/yILp/vpu6HJ/ol+7EckQXS44DBPoGyNZ2nGcDn1dXw98rHXOO7Y/G3codKK7+IO+4QzF9cOFIDiDvzyJIqMwGZo4tnQGUQs5WepJK8fQQTuJ1MCcL3SD0d7d/KykOaZoopC8f3oW1KQ+lEqZP6cQxGXStCgrqRRllf7eHahN70TBIbcQSDTwj40TidRE8ozQPNZBbOZAywsOUmrGk1ss3N8CKjP+5UELb09EyvGrlJIolS1K47MTnP4v6fOqPgJyw7DYh7HdI6fN7e2kHN8pDfIB1jAFb5YVRVsS6hdApnIEX/naxp2WLpFWohw01tp7uOJWXmJbsHE1lydvqyUKiWmCO5pDLDWLrZ8Q0kQMaJsW+oF4XnNXgOrPBNPXc7UxHbHdF9NuSdp6EswS5VcbcXbAG3NbH2zR472L+0BV5f4kQ9W6k33I5hvHT2IPKDwtakVbrCcRpUc0naUnd+if9mc/7nJE2jMwE=" 86 | file_glob: true 87 | file: $CRATE_NAME-$TRAVIS_TAG-$TARGET.* 88 | on: 89 | condition: $TRAVIS_RUST_VERSION = stable 90 | tags: true 91 | provider: releases 92 | skip_cleanup: true 93 | 94 | cache: cargo 95 | before_cache: 96 | # Travis can't cache files that are not readable by "others" 97 | - chmod -R a+r $HOME/.cargo 98 | 99 | branches: 100 | only: 101 | # release tags 102 | - /^v\d+\.\d+\.\d+.*$/ 103 | - master 104 | 105 | notifications: 106 | email: 107 | on_success: never 108 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "ansi_term" 5 | version = "0.11.0" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 8 | dependencies = [ 9 | "winapi", 10 | ] 11 | 12 | [[package]] 13 | name = "atty" 14 | version = "0.2.14" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 17 | dependencies = [ 18 | "hermit-abi", 19 | "libc", 20 | "winapi", 21 | ] 22 | 23 | [[package]] 24 | name = "autocfg" 25 | version = "1.0.1" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 28 | 29 | [[package]] 30 | name = "bitflags" 31 | version = "1.2.1" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 34 | 35 | [[package]] 36 | name = "cfg-if" 37 | version = "1.0.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 40 | 41 | [[package]] 42 | name = "clap" 43 | version = "2.33.3" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 46 | dependencies = [ 47 | "ansi_term", 48 | "atty", 49 | "bitflags", 50 | "strsim", 51 | "textwrap", 52 | "unicode-width", 53 | "vec_map", 54 | ] 55 | 56 | [[package]] 57 | name = "const_fn" 58 | version = "0.4.5" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6" 61 | 62 | [[package]] 63 | name = "crossbeam-channel" 64 | version = "0.5.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" 67 | dependencies = [ 68 | "cfg-if", 69 | "crossbeam-utils", 70 | ] 71 | 72 | [[package]] 73 | name = "crossbeam-deque" 74 | version = "0.8.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" 77 | dependencies = [ 78 | "cfg-if", 79 | "crossbeam-epoch", 80 | "crossbeam-utils", 81 | ] 82 | 83 | [[package]] 84 | name = "crossbeam-epoch" 85 | version = "0.9.1" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "a1aaa739f95311c2c7887a76863f500026092fb1dce0161dab577e559ef3569d" 88 | dependencies = [ 89 | "cfg-if", 90 | "const_fn", 91 | "crossbeam-utils", 92 | "lazy_static", 93 | "memoffset", 94 | "scopeguard", 95 | ] 96 | 97 | [[package]] 98 | name = "crossbeam-utils" 99 | version = "0.8.1" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d" 102 | dependencies = [ 103 | "autocfg", 104 | "cfg-if", 105 | "lazy_static", 106 | ] 107 | 108 | [[package]] 109 | name = "either" 110 | version = "1.6.1" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 113 | 114 | [[package]] 115 | name = "hermit-abi" 116 | version = "0.1.17" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" 119 | dependencies = [ 120 | "libc", 121 | ] 122 | 123 | [[package]] 124 | name = "lazy_static" 125 | version = "1.4.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 128 | 129 | [[package]] 130 | name = "libc" 131 | version = "0.2.82" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929" 134 | 135 | [[package]] 136 | name = "memoffset" 137 | version = "0.6.1" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "157b4208e3059a8f9e78d559edc658e13df41410cb3ae03979c83130067fdd87" 140 | dependencies = [ 141 | "autocfg", 142 | ] 143 | 144 | [[package]] 145 | name = "num_cpus" 146 | version = "1.13.0" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" 149 | dependencies = [ 150 | "hermit-abi", 151 | "libc", 152 | ] 153 | 154 | [[package]] 155 | name = "rayon" 156 | version = "1.5.0" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "8b0d8e0819fadc20c74ea8373106ead0600e3a67ef1fe8da56e39b9ae7275674" 159 | dependencies = [ 160 | "autocfg", 161 | "crossbeam-deque", 162 | "either", 163 | "rayon-core", 164 | ] 165 | 166 | [[package]] 167 | name = "rayon-core" 168 | version = "1.9.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a" 171 | dependencies = [ 172 | "crossbeam-channel", 173 | "crossbeam-deque", 174 | "crossbeam-utils", 175 | "lazy_static", 176 | "num_cpus", 177 | ] 178 | 179 | [[package]] 180 | name = "rff" 181 | version = "0.3.0" 182 | dependencies = [ 183 | "clap", 184 | "libc", 185 | "rayon", 186 | ] 187 | 188 | [[package]] 189 | name = "scopeguard" 190 | version = "1.1.0" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 193 | 194 | [[package]] 195 | name = "strsim" 196 | version = "0.8.0" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 199 | 200 | [[package]] 201 | name = "textwrap" 202 | version = "0.11.0" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 205 | dependencies = [ 206 | "unicode-width", 207 | ] 208 | 209 | [[package]] 210 | name = "unicode-width" 211 | version = "0.1.8" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 214 | 215 | [[package]] 216 | name = "vec_map" 217 | version = "0.8.2" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 220 | 221 | [[package]] 222 | name = "winapi" 223 | version = "0.3.9" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 226 | dependencies = [ 227 | "winapi-i686-pc-windows-gnu", 228 | "winapi-x86_64-pc-windows-gnu", 229 | ] 230 | 231 | [[package]] 232 | name = "winapi-i686-pc-windows-gnu" 233 | version = "0.4.0" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 236 | 237 | [[package]] 238 | name = "winapi-x86_64-pc-windows-gnu" 239 | version = "0.4.0" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 242 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rff" 3 | version = "0.3.0" 4 | authors = ["Andrew Stewart "] 5 | homepage = "https://github.com/stewart/rff" 6 | description = "rff is a fast, simple fuzzy text selector for the terminal." 7 | license = "MIT" 8 | categories = ["command-line-utilities"] 9 | exclude = ["benches/*", "ci/*"] 10 | 11 | [dependencies] 12 | clap = "2.33.0" 13 | libc = "0.2" 14 | rayon = "1.0.0" 15 | 16 | [[bin]] 17 | name = "rff" 18 | doc = false 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Andrew Stewart 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rff 2 | 3 | `rff` is a fast, simple fuzzy selector for the terminal with an advanced scoring algorithm and full UTF-8 support. 4 | 5 | ### Installation 6 | 7 | [**Pre-compiled binaries**](https://github.com/stewart/rff/releases) are available for common architectures, starting with version 0.3.0. 8 | 9 | If you have a Rust toolchain installed, you can also build `rff` from source via Cargo: 10 | 11 | $ cargo install rff 12 | 13 | ### Usage 14 | 15 | `rff` is a drop-in replacement for other fuzzy selection tools such as [`fzy`][fzy] or [`selecta`][selecta]. 16 | 17 | [selecta]: https://github.com/garybernhardt/selecta 18 | [fzy]: https://github.com/jhawthorn/fzy 19 | 20 | Its interface is straightforward: 21 | 22 | - pass it a set of choices on `STDIN` 23 | - it will present a fuzzy selection interface to the user, and block until they make a selection or quit with `^C` 24 | - it will print the user's selection on `STDOUT` 25 | 26 | As an example, you can say: 27 | 28 | $ vim $(find . -type f | rff) 29 | 30 | Which prompts the user to select a file in or below the current directory, and then opens the selected file in `vim`. 31 | 32 | `rff` supports these keys: 33 | 34 | - `^N` to select the next match 35 | - `^P` to select the previous match 36 | - `^U` to clear the search query 37 | - `^C`, `^D`, and `Esc` to exit without selecting a match 38 | 39 | ### Scoring 40 | 41 | `rff` is currently based on [`fzy`][fzy]'s scoring algoritm. For details on how this is better than most fuzzy finders, see `fzy`'s [`ALGORITHM.md`][fzy-algorithm]. 42 | 43 | [fzy-algorithm]: https://github.com/jhawthorn/fzy/blob/master/ALGORITHM.md 44 | -------------------------------------------------------------------------------- /benches/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stewart/rff/d46cbf98f5901f22ecf7c9b72ecf32c37141e5e9/benches/.keep -------------------------------------------------------------------------------- /benches/matcher.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | extern crate rff; 3 | extern crate test; 4 | 5 | use test::Bencher; 6 | 7 | use rff::matcher::matches; 8 | 9 | #[bench] 10 | fn bench_matches(b: &mut Bencher) { 11 | b.iter(|| matches("amor", "app/models/order.rb")) 12 | } 13 | 14 | #[bench] 15 | fn bench_matches_utf8(b: &mut Bencher) { 16 | b.iter(|| matches("ß", "WEIẞ")) 17 | } 18 | 19 | #[bench] 20 | fn bench_matches_mixed(b: &mut Bencher) { 21 | b.iter(|| matches("abc", "abØ")) 22 | } 23 | 24 | #[bench] 25 | fn bench_matches_more_specific(b: &mut Bencher) { 26 | b.iter(|| matches("app/models", "app/models/order.rb")) 27 | } 28 | 29 | #[bench] 30 | fn bench_matches_mixed_case(b: &mut Bencher) { 31 | b.iter(|| matches("AMOr", "App/Models/Order.rb")) 32 | } 33 | 34 | #[bench] 35 | fn bench_matches_multiple(b: &mut Bencher) { 36 | b.iter(|| { 37 | matches("amor", "app/models/order.rb"); 38 | matches("amor", "spec/models/order_spec.rb"); 39 | matches("amor", "other_garbage.rb"); 40 | matches("amor", "Gemfile"); 41 | matches("amor", "node_modules/test/a/thing.js"); 42 | matches("amor", "vendor/bundle/ruby/gem.rb") 43 | }) 44 | } 45 | 46 | #[bench] 47 | fn bench_matches_eq(b: &mut Bencher) { 48 | b.iter(|| { 49 | matches("Gemfile", "Gemfile"); 50 | matches("gemfile", "Gemfile") 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /benches/matrix.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | extern crate rff; 3 | extern crate test; 4 | 5 | use test::Bencher; 6 | 7 | use rff::matrix::Matrix; 8 | 9 | #[bench] 10 | fn bench_matrix_new(b: &mut Bencher) { 11 | b.iter(|| Matrix::new(10, 256)) 12 | } 13 | 14 | #[bench] 15 | fn bench_matrix_new_large_haystack(b: &mut Bencher) { 16 | b.iter(|| Matrix::new(10, 10_240)) 17 | } 18 | -------------------------------------------------------------------------------- /benches/scorer.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | extern crate rff; 3 | extern crate test; 4 | 5 | use test::Bencher; 6 | 7 | use rff::scorer::{score, score_with_positions, compute_bonus}; 8 | 9 | #[bench] 10 | fn bench_score(b: &mut Bencher) { 11 | b.iter(|| score("amor", "app/models/order.rb")) 12 | } 13 | 14 | #[bench] 15 | fn bench_score_empty_needle(b: &mut Bencher) { 16 | b.iter(|| score("", "app/models/order.rb")) 17 | } 18 | 19 | #[bench] 20 | fn bench_score_matching(b: &mut Bencher) { 21 | b.iter(|| score("app/models/order.rb", "app/models/order.rb")) 22 | } 23 | 24 | #[bench] 25 | fn bench_score_large_haystack(b: &mut Bencher) { 26 | let large_string = "X".repeat(1024); 27 | b.iter(|| score("amor", &large_string)) 28 | } 29 | 30 | #[bench] 31 | fn bench_score_huge_haystack(b: &mut Bencher) { 32 | let huge_string = "X".repeat(1025); 33 | b.iter(|| score("amor", &huge_string)) 34 | } 35 | 36 | #[bench] 37 | fn bench_score_multiple(b: &mut Bencher) { 38 | b.iter(|| { 39 | score("amor", "app/models/order.rb"); 40 | score("amor", "spec/models/order_spec.rb"); 41 | score("amor", "other_garbage.rb"); 42 | score("amor", "Gemfile"); 43 | score("amor", "node_modules/test/a/thing.js"); 44 | score("amor", "vendor/bundle/ruby/gem.rb") 45 | }) 46 | } 47 | 48 | #[bench] 49 | fn bench_score_with_positions(b: &mut Bencher) { 50 | b.iter(|| score_with_positions("amor", "app/models/order.rb")) 51 | } 52 | 53 | #[bench] 54 | fn bench_score_multiple_with_positions(b: &mut Bencher) { 55 | b.iter(|| { 56 | score_with_positions("amor", "app/models/order.rb"); 57 | score_with_positions("amor", "spec/models/order_spec.rb"); 58 | score_with_positions("amor", "other_garbage.rb"); 59 | score_with_positions("amor", "Gemfile"); 60 | score_with_positions("amor", "node_modules/test/a/thing.js"); 61 | score_with_positions("amor", "vendor/bundle/ruby/gem.rb") 62 | }) 63 | } 64 | 65 | #[bench] 66 | fn bench_compute_bonus(b: &mut Bencher) { 67 | b.iter(|| compute_bonus("app/models/this/is/a/strangely/nested/path.rb")) 68 | } 69 | 70 | #[bench] 71 | fn bench_compute_bonuses(b: &mut Bencher) { 72 | b.iter(|| { 73 | compute_bonus("app/models/order.rb"); 74 | compute_bonus("spec/models/order_spec.rb"); 75 | compute_bonus("other_garbage.rb"); 76 | compute_bonus("Gemfile"); 77 | compute_bonus("node_modules/test/a/thing.js"); 78 | compute_bonus("vendor/bundle/ruby/gem.rb") 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /ci/before_deploy.sh: -------------------------------------------------------------------------------- 1 | # This script takes care of building your crate and packaging it for release 2 | 3 | set -ex 4 | 5 | main() { 6 | local src=$(pwd) \ 7 | stage= 8 | 9 | case $TRAVIS_OS_NAME in 10 | linux) 11 | stage=$(mktemp -d) 12 | ;; 13 | osx) 14 | stage=$(mktemp -d -t tmp) 15 | ;; 16 | esac 17 | 18 | test -f Cargo.lock || cargo generate-lockfile 19 | 20 | cross build --target $TARGET --release 21 | 22 | cp target/$TARGET/release/rff $stage/ 23 | 24 | cd $stage 25 | tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz * 26 | cd $src 27 | 28 | rm -rf $stage 29 | } 30 | 31 | main 32 | -------------------------------------------------------------------------------- /ci/install.sh: -------------------------------------------------------------------------------- 1 | set -ex 2 | 3 | main() { 4 | local target= 5 | if [ $TRAVIS_OS_NAME = linux ]; then 6 | target=x86_64-unknown-linux-musl 7 | sort=sort 8 | else 9 | target=x86_64-apple-darwin 10 | sort=gsort # for `sort --sort-version`, from brew's coreutils. 11 | fi 12 | 13 | # Builds for iOS are done on OSX, but require the specific target to be 14 | # installed. 15 | case $TARGET in 16 | aarch64-apple-ios) 17 | rustup target install aarch64-apple-ios 18 | ;; 19 | armv7-apple-ios) 20 | rustup target install armv7-apple-ios 21 | ;; 22 | armv7s-apple-ios) 23 | rustup target install armv7s-apple-ios 24 | ;; 25 | i386-apple-ios) 26 | rustup target install i386-apple-ios 27 | ;; 28 | x86_64-apple-ios) 29 | rustup target install x86_64-apple-ios 30 | ;; 31 | esac 32 | 33 | # This fetches latest stable release 34 | local tag=$(git ls-remote --tags --refs --exit-code https://github.com/japaric/cross \ 35 | | cut -d/ -f3 \ 36 | | grep -E '^v[0.1.0-9.]+$' \ 37 | | $sort --version-sort \ 38 | | tail -n1) 39 | curl -LSfs https://japaric.github.io/trust/install.sh | \ 40 | sh -s -- \ 41 | --force \ 42 | --git japaric/cross \ 43 | --tag $tag \ 44 | --target $target 45 | } 46 | 47 | main 48 | -------------------------------------------------------------------------------- /ci/script.sh: -------------------------------------------------------------------------------- 1 | # This script takes care of testing your crate 2 | 3 | set -ex 4 | 5 | main() { 6 | cross build --target $TARGET 7 | cross build --target $TARGET --release 8 | 9 | if [ ! -z $DISABLE_TESTS ]; then 10 | return 11 | fi 12 | 13 | cross test --target $TARGET 14 | cross test --target $TARGET --release 15 | } 16 | 17 | # we don't run the "test phase" when doing deploys 18 | if [ -z $TRAVIS_TAG ]; then 19 | main 20 | fi 21 | -------------------------------------------------------------------------------- /src/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stewart/rff/d46cbf98f5901f22ecf7c9b72ecf32c37141e5e9/src/.keep -------------------------------------------------------------------------------- /src/ansi/clear.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result}; 2 | 3 | // Clears from the cursor to the end of the line 4 | generate_csi_struct!(AfterCursor, "K"); 5 | 6 | // Clears from the cursor to the beginning of the line 7 | generate_csi_struct!(BeforeCursor, "1K"); 8 | 9 | // Clears the current line 10 | generate_csi_struct!(Line, "2K"); 11 | 12 | // Clears from the cursor until the end of the screen 13 | generate_csi_struct!(Screen, "J"); 14 | 15 | #[cfg(test)] 16 | mod tests { 17 | use super::*; 18 | 19 | #[test] 20 | fn after_cursor() { 21 | assert_eq!(format!("{}", AfterCursor), "\x1b[K"); 22 | } 23 | 24 | #[test] 25 | fn before_cursor() { 26 | assert_eq!(format!("{}", BeforeCursor), "\x1b[1K"); 27 | } 28 | 29 | #[test] 30 | fn line() { 31 | assert_eq!(format!("{}", Line), "\x1b[2K"); 32 | } 33 | 34 | #[test] 35 | fn screen() { 36 | assert_eq!(format!("{}", Screen), "\x1b[J"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ansi/color.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result}; 2 | 3 | pub trait Color { 4 | fn write_fg(&self, f: &mut Formatter) -> Result; 5 | fn write_bg(&self, f: &mut Formatter) -> Result; 6 | } 7 | 8 | #[allow(dead_code)] 9 | #[derive(Copy, Clone)] 10 | pub enum Colors { 11 | Black, 12 | Red, 13 | Green, 14 | Yellow, 15 | Blue, 16 | Magenta, 17 | Cyan, 18 | White, 19 | LightBlack, 20 | LightRed, 21 | LightGreen, 22 | LightYellow, 23 | LightBlue, 24 | LightMagenta, 25 | LightCyan, 26 | LightWhite 27 | } 28 | 29 | impl Color for Colors { 30 | #[inline] 31 | fn write_fg(&self, f: &mut Formatter) -> Result { 32 | write!(f, csi!("38;5;{}m"), *self as u32) 33 | } 34 | 35 | #[inline] 36 | fn write_bg(&self, f: &mut Formatter) -> Result { 37 | write!(f, csi!("48;5;{}m"), *self as u32) 38 | } 39 | } 40 | 41 | impl<'a> Color for &'a Colors { 42 | #[inline] 43 | fn write_fg(&self, f: &mut Formatter) -> Result { 44 | (*self).write_fg(f) 45 | } 46 | 47 | #[inline] 48 | fn write_bg(&self, f: &mut Formatter) -> Result { 49 | (*self).write_bg(f) 50 | } 51 | } 52 | 53 | #[derive(Copy, Clone)] 54 | pub struct Reset; 55 | 56 | impl Color for Reset { 57 | #[inline] 58 | fn write_fg(&self, f: &mut Formatter) -> Result { 59 | write!(f, csi!("39m")) 60 | } 61 | 62 | #[inline] 63 | fn write_bg(&self, f: &mut Formatter) -> Result { 64 | write!(f, csi!("49m")) 65 | } 66 | } 67 | 68 | #[derive(Copy, Clone)] 69 | pub struct Fg(pub C); 70 | 71 | impl Display for Fg { 72 | fn fmt(&self, f: &mut Formatter) -> Result { 73 | self.0.write_fg(f) 74 | } 75 | } 76 | 77 | #[derive(Copy, Clone)] 78 | pub struct Bg(pub C); 79 | 80 | impl Display for Bg { 81 | fn fmt(&self, f: &mut Formatter) -> Result { 82 | self.0.write_bg(f) 83 | } 84 | } 85 | 86 | #[cfg(test)] 87 | mod tests { 88 | use super::*; 89 | 90 | macro_rules! test_color { 91 | ($test: ident, $name: ident, $value: expr) => { 92 | #[test] 93 | fn $test() { 94 | let fg = format!("{}", Fg(Colors::$name)); 95 | let bg = format!("{}", Bg(Colors::$name)); 96 | 97 | assert_eq!(fg, concat!("\x1b[38;5;", $value, "m")); 98 | assert_eq!(bg, concat!("\x1b[48;5;", $value, "m")); 99 | } 100 | } 101 | } 102 | 103 | test_color!(black, Black, 0); 104 | test_color!(red, Red, 1); 105 | test_color!(green, Green, 2); 106 | test_color!(yellow, Yellow, 3); 107 | test_color!(blue, Blue, 4); 108 | test_color!(magenta, Magenta, 5); 109 | test_color!(cyan, Cyan, 6); 110 | test_color!(white, White, 7); 111 | test_color!(light_black, LightBlack, 8); 112 | test_color!(light_red, LightRed, 9); 113 | test_color!(light_green, LightGreen, 10); 114 | test_color!(light_yellow, LightYellow, 11); 115 | test_color!(light_blue, LightBlue, 12); 116 | test_color!(light_magenta, LightMagenta, 13); 117 | test_color!(light_cyan, LightCyan, 14); 118 | test_color!(light_white, LightWhite, 15); 119 | 120 | #[test] 121 | fn test_reset() { 122 | let fg = format!("{}", Fg(Reset)); 123 | let bg = format!("{}", Bg(Reset)); 124 | 125 | assert_eq!(fg, "\x1b[39m"); 126 | assert_eq!(bg, "\x1b[49m") 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/ansi/cursor.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result}; 2 | 3 | /// Move the cursor to (row, col) position [(1, 1)-based] 4 | #[derive(Copy, Clone)] 5 | pub struct GoTo(pub u16, pub u16); 6 | 7 | impl Display for GoTo { 8 | fn fmt(&self, f: &mut Formatter) -> Result { 9 | debug_assert!(self.0 > 0 && self.1 > 0, "ANSI coordinates are 1-based"); 10 | write!(f, csi!("{};{}H"), self.0, self.1) 11 | } 12 | } 13 | 14 | // Move the cursor up N rows 15 | generate_csi_struct!(Up, "A", u16); 16 | 17 | // Move the cursor down N rows 18 | generate_csi_struct!(Down, "B", u16); 19 | 20 | // Move the cursor left N columns 21 | generate_csi_struct!(Left, "D", u16); 22 | 23 | // Move the cursor right N columns 24 | generate_csi_struct!(Right, "C", u16); 25 | 26 | // Move the cursor up N lines, and to the beginning of the line 27 | generate_csi_struct!(UpLine, "F", u16); 28 | 29 | // Move the cursor down N lines, and to the beginning of the line 30 | generate_csi_struct!(DownLine, "E", u16); 31 | 32 | // Set cursor column 33 | #[derive(Copy, Clone)] 34 | pub struct Column(pub u16); 35 | 36 | impl Display for Column { 37 | fn fmt(&self, f: &mut Formatter) -> Result { 38 | debug_assert!(self.0 > 0, "ANSI coordinates are 1-based"); 39 | write!(f, csi!("{}G"), self.0) 40 | } 41 | } 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | use super::*; 46 | 47 | #[test] 48 | fn goto() { 49 | let s = format!("{}", GoTo(1, 2)); 50 | assert_eq!(s, "\x1b[1;2H"); 51 | } 52 | 53 | #[test] 54 | fn up() { 55 | let s = format!("{}", Up(2)); 56 | assert_eq!(s, "\x1b[2A"); 57 | } 58 | 59 | #[test] 60 | fn down() { 61 | let s = format!("{}", Down(2)); 62 | assert_eq!(s, "\x1b[2B"); 63 | } 64 | 65 | #[test] 66 | fn left() { 67 | let s = format!("{}", Left(2)); 68 | assert_eq!(s, "\x1b[2D"); 69 | } 70 | 71 | #[test] 72 | fn right() { 73 | let s = format!("{}", Right(2)); 74 | assert_eq!(s, "\x1b[2C"); 75 | } 76 | 77 | #[test] 78 | fn up_line() { 79 | let s = format!("{}", UpLine(2)); 80 | assert_eq!(s, "\x1b[2F"); 81 | } 82 | 83 | #[test] 84 | fn down_line() { 85 | let s = format!("{}", DownLine(2)); 86 | assert_eq!(s, "\x1b[2E"); 87 | } 88 | 89 | #[test] 90 | fn column() { 91 | let s = format!("{}", Column(1)); 92 | assert_eq!(s, "\x1b[1G"); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/ansi/macros.rs: -------------------------------------------------------------------------------- 1 | /// Shortcut to generate an ESC-prefixed CSI sequence 2 | macro_rules! csi { 3 | ($( $l:expr ),*) => { 4 | concat!("\x1b[", $( $l ),*) 5 | }; 6 | } 7 | 8 | /// Generates an empty struct that prints as a CSI sequence 9 | macro_rules! generate_csi_struct { 10 | ($name:ident, $value:expr) => { 11 | pub struct $name; 12 | 13 | impl Display for $name { 14 | fn fmt(&self, f: &mut Formatter) -> Result { 15 | write!(f, csi!($value)) 16 | } 17 | } 18 | }; 19 | 20 | ($name:ident, $value:expr, u16) => { 21 | pub struct $name(pub u16); 22 | 23 | impl Display for $name { 24 | fn fmt(&self, f: &mut Formatter) -> Result { 25 | write!(f, csi!("{}", $value), self.0) 26 | } 27 | } 28 | }; 29 | } 30 | 31 | #[cfg(test)] 32 | mod tests { 33 | #[test] 34 | fn csi() { 35 | assert_eq!(csi!("123"), "\x1b[123"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ansi/mod.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | mod macros; 3 | 4 | pub mod cursor; 5 | pub mod clear; 6 | pub mod color; 7 | pub mod style; 8 | -------------------------------------------------------------------------------- /src/ansi/style.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result}; 2 | 3 | generate_csi_struct!(Reset, "m"); 4 | 5 | generate_csi_struct!(Bold, "1m"); 6 | generate_csi_struct!(Italic, "3m"); 7 | generate_csi_struct!(Underline, "4m"); 8 | generate_csi_struct!(Invert, "7m"); 9 | 10 | generate_csi_struct!(NoBold, "21m"); 11 | generate_csi_struct!(NoItalic, "23m"); 12 | generate_csi_struct!(NoUnderline, "24m"); 13 | generate_csi_struct!(NoInvert, "27m"); 14 | 15 | #[cfg(test)] 16 | mod tests { 17 | use super::*; 18 | 19 | macro_rules! test_style { 20 | ($test: ident, $name: ident, $value: expr) => { 21 | #[test] 22 | fn $test() { 23 | let result = format!("{}", $name); 24 | let expected = concat!("\x1b[", $value); 25 | assert_eq!(result, expected); 26 | } 27 | } 28 | } 29 | 30 | test_style!(reset, Reset, "m"); 31 | test_style!(bold, Bold, "1m"); 32 | test_style!(italic, Italic, "3m"); 33 | test_style!(underline, Underline, "4m"); 34 | test_style!(invert, Invert, "7m"); 35 | test_style!(no_bold, NoBold, "21m"); 36 | test_style!(no_italic, NoItalic, "23m"); 37 | test_style!(no_underline, NoUnderline, "24m"); 38 | test_style!(no_invert, NoInvert, "27m"); 39 | } 40 | -------------------------------------------------------------------------------- /src/consts.rs: -------------------------------------------------------------------------------- 1 | //! Useful constants for score calculations. 2 | 3 | use std::f64::{INFINITY, NEG_INFINITY}; 4 | 5 | pub const SCORE_MAX: f64 = INFINITY; 6 | pub const SCORE_MIN: f64 = NEG_INFINITY; 7 | pub const SCORE_GAP_LEADING: f64 = -0.005; 8 | pub const SCORE_GAP_TRAILING: f64 = -0.005; 9 | pub const SCORE_GAP_INNER: f64 = -0.01; 10 | pub const SCORE_MATCH_CONSECUTIVE: f64 = 1.0; 11 | pub const SCORE_MATCH_SLASH: f64 = 0.9; 12 | pub const SCORE_MATCH_WORD: f64 = 0.8; 13 | pub const SCORE_MATCH_CAPITAL: f64 = 0.7; 14 | pub const SCORE_MATCH_DOT: f64 = 0.6; 15 | -------------------------------------------------------------------------------- /src/interface.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write, BufWriter}; 2 | 3 | use super::{MatchWithPositions, match_and_score_with_positions}; 4 | use ansi::{clear, color, cursor, style}; 5 | use terminal::{self, Terminal, Key, Event}; 6 | 7 | use rayon::prelude::*; 8 | 9 | #[derive(Debug)] 10 | pub enum Error { 11 | Exit, 12 | Write(io::Error), 13 | Reset(terminal::Error) 14 | } 15 | 16 | impl From for Error { 17 | fn from(err: io::Error) -> Error { 18 | Error::Write(err) 19 | } 20 | } 21 | 22 | impl From for Error { 23 | fn from(err: terminal::Error) -> Error { 24 | Error::Reset(err) 25 | } 26 | } 27 | 28 | pub struct Interface<'a> { 29 | lines: &'a [String], 30 | matches: Vec>, 31 | 32 | search: String, 33 | selected: usize, 34 | 35 | choices_width: usize, 36 | width: usize, 37 | 38 | terminal: Terminal, 39 | } 40 | 41 | impl<'a> Interface<'a> { 42 | // Creates a new Interface with the provided lines 43 | pub fn new(lines: &'a [String]) -> Interface<'a> { 44 | let mut terminal = Terminal::from("/dev/tty").unwrap(); 45 | let choices_width = format!("{}", lines.len()).len(); 46 | 47 | terminal.set_raw_mode().unwrap(); 48 | 49 | Interface { 50 | lines: lines, 51 | matches: vec![], 52 | search: String::new(), 53 | selected: 0, 54 | choices_width: choices_width, 55 | width: terminal.max_width, 56 | terminal: terminal, 57 | } 58 | } 59 | 60 | // Runs the Interface, returning either the final selection, or an error 61 | pub fn run(&mut self) -> Result<&str, Error> { 62 | self.filter_matches(); 63 | self.render()?; 64 | 65 | for event in self.terminal.events()? { 66 | if let Event::Key(key) = event? { 67 | match key { 68 | Key::Ctrl('c') | Key::Ctrl('d') | Key::Escape => { 69 | self.reset()?; 70 | return Err(Error::Exit); 71 | } 72 | 73 | Key::Char('\n') => { 74 | break; 75 | }, 76 | 77 | Key::Ctrl('n') => { 78 | self.selected += 1; 79 | self.render()?; 80 | }, 81 | 82 | Key::Ctrl('p') => { 83 | self.selected = self.selected.saturating_sub(1); 84 | self.render()?; 85 | }, 86 | 87 | Key::Char(ch) => { 88 | self.search.push(ch); 89 | self.filter_existing(); 90 | self.render()?; 91 | }, 92 | 93 | Key::Backspace | Key::Ctrl('h') => { 94 | self.search.pop(); 95 | self.filter_matches(); 96 | self.render()?; 97 | } 98 | 99 | Key::Ctrl('u') => { 100 | self.search.clear(); 101 | self.filter_matches(); 102 | self.render()?; 103 | } 104 | 105 | _ => {} 106 | } 107 | }; 108 | } 109 | 110 | self.reset()?; 111 | Ok(self.result()) 112 | } 113 | 114 | // Matches and scores `lines` by `search`, sorting the result 115 | fn filter_matches(&mut self) { 116 | let ref search = self.search; 117 | 118 | self.matches = self.lines. 119 | par_iter(). 120 | filter_map(|line| match_and_score_with_positions(search, line)). 121 | collect(); 122 | 123 | self.matches.par_sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap().reverse()); 124 | } 125 | 126 | // Matches and scores the existing `matches` by `search`, sorting the result 127 | fn filter_existing(&mut self) { 128 | let ref search = self.search; 129 | 130 | self.matches = self.matches. 131 | par_iter(). 132 | filter_map(|&(line, _, _)| match_and_score_with_positions(search, line)). 133 | collect(); 134 | 135 | self.matches.par_sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap().reverse()); 136 | } 137 | 138 | // Renders the current state of the Interface to it's `terminal` 139 | fn render(&mut self) -> io::Result<()> { 140 | self.clamp_selected(); 141 | 142 | let prompt = self.prompt(); 143 | let matches = self.matches.iter().take(10); 144 | let n = matches.len() as u16; 145 | 146 | let mut term = BufWriter::new(&mut self.terminal); 147 | 148 | write!(term, "{}{}{}", cursor::Column(1), clear::Screen, prompt)?; 149 | 150 | for (i, choice) in matches.enumerate() { 151 | let selected = i == self.selected; 152 | let chars = choice.0.chars().take(self.width); 153 | 154 | write!(term, "\r\n")?; 155 | 156 | if selected { 157 | write!(term, "{}", style::Invert)?; 158 | } 159 | 160 | let ref positions = choice.2; 161 | 162 | for (i, ch) in chars.enumerate() { 163 | if positions.contains(&i) { 164 | let color = color::Fg(color::Colors::Magenta); 165 | let reset = color::Fg(color::Reset); 166 | write!(term, "{}{}{}", color, ch, reset)?; 167 | } else { 168 | write!(term, "{}", ch)?; 169 | } 170 | } 171 | 172 | if selected { 173 | write!(term, "{}", style::NoInvert)?; 174 | } 175 | } 176 | 177 | if n > 0 { 178 | let col = (prompt.len() + 1) as u16; 179 | write!(term, "{}{}", cursor::Up(n), cursor::Column(col))?; 180 | } 181 | 182 | Ok(()) 183 | } 184 | 185 | // Generates the input prompt 186 | fn prompt(&self) -> String { 187 | let count = self.matches.len(); 188 | format!("{:width$} > {}", count, self.search, width = self.choices_width) 189 | } 190 | 191 | // Clamps `selected`, such that it doesn't overflow the matches length 192 | fn clamp_selected(&mut self) { 193 | let mut max = self.matches.len(); 194 | if max > 10 { max = 10; } 195 | 196 | if self.selected >= max { 197 | self.selected = if max > 0 { max - 1 } else { 0 }; 198 | } 199 | } 200 | 201 | // Resets the `terminal` 202 | fn reset(&mut self) -> Result<(), Error> { 203 | write!(self.terminal, "{}{}", cursor::Column(1), clear::Screen)?; 204 | self.terminal.reset()?; 205 | Ok(()) 206 | } 207 | 208 | fn result(&mut self) -> &str { 209 | self.matches.iter(). 210 | nth(self.selected). 211 | map(|choice| choice.0). 212 | unwrap_or(&self.search) 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate libc; 2 | extern crate rayon; 3 | 4 | mod consts; 5 | mod terminal; 6 | 7 | pub mod ansi; 8 | pub mod stdin; 9 | pub mod matcher; 10 | pub mod matrix; 11 | pub mod scorer; 12 | pub mod interface; 13 | 14 | pub type Match<'a> = (&'a str, f64); 15 | pub type MatchWithPositions<'a> = (&'a str, f64, Vec); 16 | 17 | pub fn match_and_score<'a>(needle: &str, haystack: &'a str) -> Option> { 18 | if matcher::matches(needle, haystack) { 19 | Some((haystack, scorer::score(needle, haystack))) 20 | } else { 21 | None 22 | } 23 | } 24 | 25 | pub fn match_and_score_with_positions<'a>(needle: &str, haystack: &'a str) -> Option> { 26 | if matcher::matches(needle, haystack) { 27 | let (score, positions) = scorer::score_with_positions(needle, haystack); 28 | Some((haystack, score, positions)) 29 | } else { 30 | None 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate rff; 2 | extern crate clap; 3 | extern crate rayon; 4 | 5 | use std::io::{self, Write, BufWriter}; 6 | use rff::{stdin, match_and_score}; 7 | use rff::interface::{Interface, Error}; 8 | use clap::{App, Arg}; 9 | use rayon::prelude::*; 10 | 11 | fn main() { 12 | let status_code = run(); 13 | std::process::exit(status_code); 14 | } 15 | 16 | fn run() -> i32 { 17 | let matches = App::new("rff"). 18 | version(env!("CARGO_PKG_VERSION")). 19 | author("Andrew S. "). 20 | about("A fuzzy finder."). 21 | arg( 22 | Arg::with_name("query"). 23 | short("s"). 24 | long("search"). 25 | value_name("QUERY"). 26 | help("Term to search for") 27 | ). 28 | arg( 29 | Arg::with_name("benchmark"). 30 | long("benchmark"). 31 | help("Run rff in benchmark mode") 32 | ). 33 | get_matches(); 34 | 35 | let has_query = matches.is_present("query"); 36 | let has_benchmark = matches.is_present("benchmark"); 37 | 38 | if has_benchmark && !has_query { 39 | println!("Must specifiy -s/--search with --benchmark"); 40 | return 1 41 | } 42 | 43 | if has_query { 44 | let query = matches.value_of("query").unwrap(); 45 | 46 | if has_benchmark { 47 | benchmark(query); 48 | } else { 49 | search(query); 50 | } 51 | 52 | return 0 53 | } else { 54 | return interactive(); 55 | } 56 | } 57 | 58 | fn benchmark(needle: &str) { 59 | let lines = stdin::slurp(); 60 | 61 | // in benchmark mode, we run the match/score/sort loop 100 times 62 | for _ in 0..100 { 63 | lines 64 | .par_iter() 65 | .filter_map(|line| match_and_score(needle, line)) 66 | .collect::>() 67 | .par_sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap().reverse()); 68 | } 69 | } 70 | 71 | fn search(needle: &str) { 72 | let lines = stdin::slurp(); 73 | let mut lines: Vec<_> = lines 74 | .par_iter() 75 | .filter_map(|line| match_and_score(needle, line)) 76 | .collect(); 77 | 78 | lines.par_sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap().reverse()); 79 | 80 | let stdout = io::stdout(); 81 | let mut stdout = BufWriter::new(stdout.lock()); 82 | 83 | for line in &lines { 84 | writeln!(stdout, "{}", line.0).unwrap(); 85 | } 86 | } 87 | 88 | fn interactive() -> i32 { 89 | let lines = stdin::slurp(); 90 | 91 | match Interface::new(&lines).run() { 92 | Ok(result) => println!("{}", result), 93 | Err(Error::Exit) => { return 1 }, 94 | Err(error) => { 95 | eprintln!("{:?}", error); 96 | return 1 97 | }, 98 | } 99 | 100 | return 0 101 | } 102 | -------------------------------------------------------------------------------- /src/matcher.rs: -------------------------------------------------------------------------------- 1 | /// Searches for needle's chars in the haystack 2 | /// 3 | /// # Examples 4 | /// 5 | /// ``` 6 | /// assert!(rff::matcher::matches("amo", "app/models/order")); 7 | /// ``` 8 | #[inline] 9 | pub fn matches(needle: &str, haystack: &str) -> bool { 10 | if needle == "" { return true; } 11 | 12 | let mut hchars = haystack.chars(); 13 | 14 | needle.chars().all(|n| { 15 | hchars.any(|h| eq(n, h)) 16 | }) 17 | } 18 | 19 | /// Compare two `char` for case-insensitive equality. 20 | #[inline(always)] 21 | pub fn eq(a: char, b: char) -> bool { 22 | match a { 23 | _ if a == b => true, 24 | _ if a.is_ascii() || b.is_ascii() => a.eq_ignore_ascii_case(&b), 25 | _ => a.to_lowercase().eq(b.to_lowercase()) 26 | } 27 | } 28 | 29 | #[cfg(test)] 30 | mod tests { 31 | use super::*; 32 | 33 | #[test] 34 | fn test_eq() { 35 | assert!(eq('a', 'A')); 36 | assert!(eq('山', '山')); 37 | assert!(!eq('a', 'b')); 38 | } 39 | 40 | #[test] 41 | fn test_matches() { 42 | assert!(matches("", "a")); 43 | assert!(matches("a", "a")); 44 | assert!(matches("a", "abc")); 45 | assert!(matches("abc", "abc")); 46 | assert!(matches("ABC", "abc")); 47 | assert!(matches("abc", "a1b2c3")); 48 | assert!(matches("abc", "a1b2c3")); 49 | assert!(matches("test", "t/e/s/t")); 50 | assert!(matches("test", "t💣e💣s💣t")); 51 | assert!(matches("💣💣💣", "t💣e💣s💣t")); 52 | 53 | assert!(!matches("abc", "ab")); 54 | assert!(!matches("abc", "cab")); 55 | assert!(!matches("abc", "")); 56 | 57 | assert!(matches("", "")); 58 | assert!(matches("", "ab")); 59 | 60 | // UTF-8 case testing 61 | assert!(matches("a", "A")); 62 | assert!(matches("A", "a")); 63 | assert!(matches("山", "山")); 64 | assert!(matches("café", "CAFÉ")); 65 | assert!(matches("weiß", "WEIẞ")); 66 | assert!(matches("хди́ь", "ХОДИ́ТЬ")); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/matrix.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Index, IndexMut}; 2 | 3 | /// A type shorthand for the lookup key we'll be using, a width/height tuple. 4 | type Idx = (usize, usize); 5 | 6 | /// A 2-dimensional matrix of f64 values. 7 | #[derive(Clone, Debug)] 8 | pub struct Matrix { 9 | width: usize, 10 | contents: Vec, 11 | } 12 | 13 | impl Matrix { 14 | /// Creates a new Matrix with the provided width and height. 15 | pub fn new(width: usize, height: usize) -> Matrix { 16 | Matrix { 17 | width, 18 | contents: vec![0.0; width * height], 19 | } 20 | } 21 | } 22 | 23 | impl Index for Matrix { 24 | type Output = f64; 25 | 26 | fn index(&self, (width, height): Idx) -> &Self::Output { 27 | &self.contents[height * self.width + width] 28 | } 29 | } 30 | 31 | impl IndexMut for Matrix { 32 | fn index_mut(&mut self, (width, height): Idx) -> &mut Self::Output { 33 | &mut self.contents[height * self.width + width] 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod test { 39 | use super::*; 40 | 41 | #[test] 42 | fn test_matrix() { 43 | let mut matrix = Matrix::new(1024, 768); 44 | 45 | // Index 46 | assert_eq!(matrix[(1023, 767)], 0.0); 47 | 48 | // IndexMut 49 | matrix[(12, 24)] = 123.456; 50 | assert_eq!(matrix[(12, 24)], 123.456); 51 | assert_eq!(matrix[(24, 12)], 0.0); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/scorer.rs: -------------------------------------------------------------------------------- 1 | // A port of fzy's scoring algorithm. 2 | // fzy (c) 2014 John Hawthorn 3 | // Licensed under the MIT license 4 | // https://github.com/jhawthorn/fzy 5 | 6 | use consts::*; 7 | use matcher::eq; 8 | use matrix::Matrix; 9 | 10 | pub fn score(needle: &str, haystack: &str) -> f64 { 11 | let needle_length = needle.chars().count(); 12 | 13 | // empty needle 14 | if needle_length == 0 { 15 | return SCORE_MIN; 16 | } 17 | 18 | let haystack_length = haystack.chars().count(); 19 | 20 | // perfect match 21 | if needle_length == haystack_length { 22 | return SCORE_MAX; 23 | } 24 | 25 | // unreasonably large haystack 26 | if haystack_length > 1024 { 27 | return SCORE_MIN; 28 | } 29 | 30 | let (_, m) = calculate_score(needle, needle_length, haystack, haystack_length); 31 | 32 | m[(needle_length - 1, haystack_length - 1)] 33 | } 34 | 35 | pub fn score_with_positions(needle: &str, haystack: &str) -> (f64, Vec) { 36 | let needle_length = needle.chars().count(); 37 | 38 | // empty needle 39 | if needle_length == 0 { 40 | return (SCORE_MIN, vec![]); 41 | } 42 | 43 | let haystack_length = haystack.chars().count(); 44 | 45 | // perfect match 46 | if needle_length == haystack_length { 47 | return (SCORE_MAX, (0..needle_length).collect()); 48 | } 49 | 50 | // unreasonably large haystack 51 | if haystack_length > 1024 { 52 | return (SCORE_MIN, vec![]); 53 | } 54 | 55 | let (d, m) = calculate_score(needle, needle_length, haystack, haystack_length); 56 | let mut positions = vec![0 as usize; needle_length]; 57 | 58 | { 59 | let mut match_required = false; 60 | let mut j = haystack_length - 1; 61 | 62 | for i in (0..needle_length).rev() { 63 | while j > (0 as usize) { 64 | let last = if i > 0 && j > 0 { d[(i - 1, j - 1)] } else { 0.0 }; 65 | 66 | let d = d[(i, j)]; 67 | let m = m[(i, j)]; 68 | 69 | if d != SCORE_MIN && (match_required || d == m) { 70 | if i > 0 && j > 0 && m == last + SCORE_MATCH_CONSECUTIVE { 71 | match_required = true; 72 | } 73 | 74 | positions[i] = j; 75 | 76 | break; 77 | } 78 | 79 | j -= 1 80 | } 81 | } 82 | } 83 | 84 | (m[(needle_length - 1, haystack_length - 1)], positions) 85 | } 86 | 87 | fn calculate_score(needle: &str, needle_length: usize, haystack: &str, haystack_length: usize) -> (Matrix, Matrix) { 88 | let bonus = compute_bonus(haystack); 89 | 90 | let mut m = Matrix::new(needle_length, haystack_length); 91 | let mut d = Matrix::new(needle_length, haystack_length); 92 | 93 | for (i, n) in needle.chars().enumerate() { 94 | let mut prev_score = SCORE_MIN; 95 | let gap_score = if i == needle_length - 1 { SCORE_GAP_TRAILING } else { SCORE_GAP_INNER }; 96 | 97 | for (j, h) in haystack.chars().enumerate() { 98 | if eq(n, h) { 99 | let bonus_score = bonus[j]; 100 | 101 | let score = match i { 102 | 0 => ((j as f64) * SCORE_GAP_LEADING) + bonus_score, 103 | _ if j > 0 => { 104 | let m = m[(i - 1, j - 1)]; 105 | let d = d[(i - 1, j - 1)]; 106 | 107 | let m = m + bonus_score; 108 | let d = d + SCORE_MATCH_CONSECUTIVE; 109 | 110 | (m).max(d) 111 | }, 112 | _ => SCORE_MIN 113 | }; 114 | 115 | prev_score = score.max(prev_score + gap_score); 116 | 117 | d[(i, j)] = score; 118 | m[(i, j)] = prev_score; 119 | } else { 120 | prev_score += gap_score; 121 | 122 | d[(i, j)] = SCORE_MIN; 123 | m[(i, j)] = prev_score; 124 | } 125 | } 126 | } 127 | 128 | (d, m) 129 | } 130 | 131 | pub fn compute_bonus(haystack: &str) -> Vec { 132 | let mut last_char = '/'; 133 | 134 | let (_, len) = haystack.chars().size_hint(); 135 | let len = len.unwrap_or_else(|| haystack.chars().count()); 136 | 137 | haystack.chars().fold(Vec::with_capacity(len), |mut vec, ch| { 138 | vec.push(bonus_for_char(last_char, ch)); 139 | last_char = ch; 140 | vec 141 | }) 142 | } 143 | 144 | fn bonus_for_char(prev: char, current: char) -> f64 { 145 | match current { 146 | 'a' ..= 'z' | '0' ..= '9' => bonus_for_prev(prev), 147 | 'A' ..= 'Z' => { 148 | match prev { 149 | 'a' ..= 'z' => SCORE_MATCH_CAPITAL, 150 | _ => bonus_for_prev(prev) 151 | } 152 | } 153 | _ => 0.0 154 | } 155 | } 156 | 157 | fn bonus_for_prev(ch: char) -> f64 { 158 | match ch { 159 | '/' => SCORE_MATCH_SLASH, 160 | '-' | '_' | ' ' => SCORE_MATCH_WORD, 161 | '.' => SCORE_MATCH_DOT, 162 | _ => 0.0 163 | } 164 | } 165 | 166 | 167 | #[cfg(test)] 168 | mod tests { 169 | use super::*; 170 | 171 | #[test] 172 | fn test_score_basic() { 173 | assert_eq!(score("", "asdf"), SCORE_MIN); 174 | assert_eq!(score("asdf", "asdf"), SCORE_MAX); 175 | 176 | let huge_string = "X".repeat(1025); 177 | assert_eq!(score("asdf", &huge_string), SCORE_MIN); 178 | } 179 | 180 | #[test] 181 | fn relative_scores() { 182 | // App/Models/Order is better than App/MOdels/zRder 183 | assert!(score("amor", "app/models/order") > score("amor", "app/models/zrder")); 184 | 185 | // App/MOdels/foo is better than App/M/fOo 186 | assert!(score("amo", "app/m/foo") < score("amo", "app/models/foo")); 187 | 188 | // GEMFIle.Lock < GEMFILe 189 | assert!(score("gemfil", "Gemfile.lock") < score("gemfil", "Gemfile")); 190 | 191 | // GEMFIle.Lock < GEMFILe 192 | assert!(score("gemfil", "Gemfile.lock") < score("gemfil", "Gemfile")); 193 | 194 | // Prefer shorter scorees 195 | assert!(score("abce", "abcdef") > score("abce", "abc de")); 196 | 197 | // Prefer shorter candidates 198 | assert!(score("test", "tests") > score("test", "testing")); 199 | 200 | // Scores first letter highly 201 | assert!(score("test", "testing") > score("test", "/testing")); 202 | 203 | // Prefer shorter scorees 204 | assert!(score("abc", " a b c ") > score("abc", " a b c ")); 205 | assert!(score("abc", " a b c ") > score("abc", " a b c ")); 206 | } 207 | 208 | #[test] 209 | fn score_utf8() { 210 | assert_eq!(score("ß", "öäßéè"), -0.02); 211 | } 212 | 213 | #[test] 214 | fn test_compute_bonus() { 215 | assert_eq!(compute_bonus("a/b/c/d"), vec![0.9, 0.0, 0.9, 0.0, 0.9, 0.0, 0.9]); 216 | assert_eq!(compute_bonus("aTestString"), vec![0.9, 0.7, 0.0, 0.0, 0.0, 0.7, 0.0, 0.0, 0.0, 0.0, 0.0]); 217 | } 218 | 219 | #[test] 220 | fn test_for_char() { 221 | assert_eq!(bonus_for_char('*', '*'), 0.0); 222 | assert_eq!(bonus_for_char('a', 'a'), 0.0); 223 | 224 | assert_eq!(bonus_for_char('/', 'a'), SCORE_MATCH_SLASH); 225 | assert_eq!(bonus_for_char('/', 'A'), SCORE_MATCH_SLASH); 226 | assert_eq!(bonus_for_char('/', '0'), SCORE_MATCH_SLASH); 227 | 228 | assert_eq!(bonus_for_char('-', 'a'), SCORE_MATCH_WORD); 229 | assert_eq!(bonus_for_char('-', 'A'), SCORE_MATCH_WORD); 230 | assert_eq!(bonus_for_char('-', '0'), SCORE_MATCH_WORD); 231 | 232 | assert_eq!(bonus_for_char('_', 'a'), SCORE_MATCH_WORD); 233 | assert_eq!(bonus_for_char('_', 'A'), SCORE_MATCH_WORD); 234 | assert_eq!(bonus_for_char('_', '0'), SCORE_MATCH_WORD); 235 | 236 | assert_eq!(bonus_for_char(' ', 'a'), SCORE_MATCH_WORD); 237 | assert_eq!(bonus_for_char(' ', 'A'), SCORE_MATCH_WORD); 238 | assert_eq!(bonus_for_char(' ', '0'), SCORE_MATCH_WORD); 239 | 240 | assert_eq!(bonus_for_char('.', 'a'), SCORE_MATCH_DOT); 241 | assert_eq!(bonus_for_char('.', 'A'), SCORE_MATCH_DOT); 242 | assert_eq!(bonus_for_char('.', '0'), SCORE_MATCH_DOT); 243 | 244 | assert_eq!(bonus_for_char('a', 'A'), SCORE_MATCH_CAPITAL); 245 | } 246 | 247 | #[test] 248 | fn positions() { 249 | macro_rules! test_positions { 250 | ($needle:expr, $haystack:expr, $result:expr) => { 251 | let (_, positions) = score_with_positions($needle, $haystack); 252 | assert_eq!(positions, $result); 253 | } 254 | } 255 | 256 | test_positions!("amo", "app/models/foo", vec![0, 4, 5]); 257 | test_positions!("amor", "app/models/order", vec![0, 4, 11, 12]); 258 | test_positions!("as", "tags", vec![1, 3]); 259 | test_positions!("abc", "a/a/b/c/c", vec![2, 4, 6]); 260 | test_positions!("foo", "foo", vec![0, 1, 2]); 261 | test_positions!("drivers", "/path/to/drivers/file.txt", vec![9, 10, 11, 12, 13, 14, 15]); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/stdin.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, BufRead}; 2 | 3 | /// A useful alias for the backing storage we parse STDIN into. 4 | pub type InputLines = Vec; 5 | 6 | /// Pulls lines of input from STDIN into an `InputLines`. 7 | pub fn slurp() -> InputLines { 8 | let stdin = io::stdin(); 9 | let stdin = stdin.lock(); 10 | read_lines(stdin) 11 | } 12 | 13 | fn read_lines(buf: T) -> InputLines { 14 | buf.lines().map(Result::unwrap).collect() 15 | } 16 | 17 | #[cfg(test)] 18 | mod tests { 19 | use super::*; 20 | 21 | #[test] 22 | fn test_read_lines() { 23 | let input = b"a\nb\nc"; 24 | let slice = &input[..]; 25 | 26 | let expected = [ 27 | String::from("a"), 28 | String::from("b"), 29 | String::from("c") 30 | ]; 31 | 32 | assert_eq!(read_lines(slice), expected); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/terminal/event.rs: -------------------------------------------------------------------------------- 1 | // A subset of termion's event parsing. 2 | // Termion (c) 2016 Ticki 3 | // Licensed under the MIT license 4 | 5 | use std::io::{Result, Error, ErrorKind}; 6 | use std::str; 7 | 8 | #[derive(Debug)] 9 | pub enum Event { 10 | Key(Key), 11 | Unknown(Vec) 12 | } 13 | 14 | #[derive(Debug)] 15 | pub enum Key { 16 | Escape, 17 | Backspace, 18 | Tab, 19 | 20 | Left, Right, Up, Down, 21 | 22 | Home, End, 23 | 24 | PageUp, PageDown, 25 | 26 | Delete, Insert, 27 | 28 | Char(char), 29 | Alt(char), 30 | Ctrl(char), 31 | 32 | F(u8), 33 | 34 | Null 35 | } 36 | 37 | /// Parse an Event from `item` and possibly subsequent bytes through `iter`. 38 | pub fn parse_event(item: u8, iter: &mut I) -> Result 39 | where I: Iterator> 40 | { 41 | let error = Error::new(ErrorKind::Other, "Could not parse an event"); 42 | match item { 43 | b'\x1B' => { 44 | // This is an escape character, leading a control sequence. 45 | Ok(match iter.next() { 46 | Some(Ok(b'O')) => { 47 | match iter.next() { 48 | // F1-F4 49 | Some(Ok(val @ b'P'..=b'S')) => Event::Key(Key::F(1 + val - b'P')), 50 | _ => return Err(error), 51 | } 52 | } 53 | Some(Ok(b'[')) => { 54 | // This is a CSI sequence. 55 | parse_csi(iter).ok_or(error)? 56 | } 57 | Some(Ok(c)) => { 58 | let ch = parse_utf8_char(c, iter)?; 59 | Event::Key(Key::Alt(ch)) 60 | } 61 | Some(Err(_)) | None => return Err(error), 62 | }) 63 | } 64 | b'\n' | b'\r' => Ok(Event::Key(Key::Char('\n'))), 65 | b'\t' => Ok(Event::Key(Key::Char('\t'))), 66 | b'\x7F' => Ok(Event::Key(Key::Backspace)), 67 | c @ b'\x01'..=b'\x1A' => Ok(Event::Key(Key::Ctrl((c as u8 - 0x1 + b'a') as char))), 68 | c @ b'\x1C'..=b'\x1F' => Ok(Event::Key(Key::Ctrl((c as u8 - 0x1C + b'4') as char))), 69 | b'\0' => Ok(Event::Key(Key::Null)), 70 | c => { 71 | Ok({ 72 | let ch = parse_utf8_char(c, iter)?; 73 | Event::Key(Key::Char(ch)) 74 | }) 75 | } 76 | } 77 | } 78 | 79 | /// Parses a CSI sequence, just after reading ^[ 80 | /// 81 | /// Returns None if an unrecognized sequence is found. 82 | fn parse_csi(iter: &mut I) -> Option 83 | where I: Iterator> 84 | { 85 | Some(match iter.next() { 86 | Some(Ok(b'D')) => Event::Key(Key::Left), 87 | Some(Ok(b'C')) => Event::Key(Key::Right), 88 | Some(Ok(b'A')) => Event::Key(Key::Up), 89 | Some(Ok(b'B')) => Event::Key(Key::Down), 90 | Some(Ok(b'H')) => Event::Key(Key::Home), 91 | Some(Ok(b'F')) => Event::Key(Key::End), 92 | Some(Ok(c @ b'0'..=b'9')) => { 93 | // Numbered escape code. 94 | let mut buf = Vec::new(); 95 | buf.push(c); 96 | let mut c = iter.next().unwrap().unwrap(); 97 | // The final byte of a CSI sequence can be in the range 64-126, so 98 | // let's keep reading anything else. 99 | while c < 64 || c > 126 { 100 | buf.push(c); 101 | c = iter.next().unwrap().unwrap(); 102 | } 103 | 104 | match c { 105 | // Special key code. 106 | b'~' => { 107 | let str_buf = String::from_utf8(buf).unwrap(); 108 | 109 | // This CSI sequence can be a list of semicolon-separated 110 | // numbers. 111 | let nums: Vec = str_buf 112 | .split(';') 113 | .map(|n| n.parse().unwrap()) 114 | .collect(); 115 | 116 | if nums.is_empty() { 117 | return None; 118 | } 119 | 120 | // TODO: handle multiple values for key modififiers (ex: values 121 | // [3, 2] means Shift+Delete) 122 | if nums.len() > 1 { 123 | return None; 124 | } 125 | 126 | match nums[0] { 127 | 1 | 7 => Event::Key(Key::Home), 128 | 2 => Event::Key(Key::Insert), 129 | 3 => Event::Key(Key::Delete), 130 | 4 | 8 => Event::Key(Key::End), 131 | 5 => Event::Key(Key::PageUp), 132 | 6 => Event::Key(Key::PageDown), 133 | v @ 11..=15 => Event::Key(Key::F(v - 10)), 134 | v @ 17..=21 => Event::Key(Key::F(v - 11)), 135 | v @ 23..=24 => Event::Key(Key::F(v - 12)), 136 | _ => return None, 137 | } 138 | } 139 | _ => return None, 140 | } 141 | } 142 | _ => return None, 143 | }) 144 | 145 | } 146 | 147 | /// Parse `c` as either a single byte ASCII char or a variable size UTF-8 char. 148 | fn parse_utf8_char(c: u8, iter: &mut I) -> Result 149 | where I: Iterator> 150 | { 151 | let error = Err(Error::new(ErrorKind::Other, "Input character is not valid UTF-8")); 152 | if c.is_ascii() { 153 | Ok(c as char) 154 | } else { 155 | let bytes = &mut Vec::new(); 156 | bytes.push(c); 157 | 158 | loop { 159 | bytes.push(iter.next().unwrap().unwrap()); 160 | if let Ok(st) = str::from_utf8(bytes) { 161 | return Ok(st.chars().next().unwrap()); 162 | } 163 | if bytes.len() >= 4 { 164 | return error; 165 | } 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/terminal/input.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Result}; 2 | 3 | use super::event::{self, Event, Key}; 4 | 5 | pub struct Events { 6 | source: R, 7 | leftover: Option 8 | } 9 | 10 | impl Events { 11 | pub fn new(source: R) -> Events { 12 | Events { 13 | source: source, 14 | leftover: None 15 | } 16 | } 17 | } 18 | 19 | impl Iterator for Events { 20 | type Item = Result; 21 | 22 | fn next(&mut self) -> Option> { 23 | let ref mut source = self.source; 24 | 25 | if let Some(c) = self.leftover { 26 | // we have a leftover byte, use it 27 | self.leftover = None; 28 | return Some(parse_event(c, &mut source.bytes())); 29 | } 30 | 31 | let mut buf = [0u8; 2]; 32 | 33 | let result = match source.read(&mut buf) { 34 | Ok(0) => return None, 35 | Ok(1) => { 36 | match buf[0] { 37 | b'\x1b' => Ok(Event::Key(Key::Escape)), 38 | b'\t' => Ok(Event::Key(Key::Tab)), 39 | item => parse_event(item, &mut source.bytes()) 40 | } 41 | }, 42 | Ok(2) => { 43 | let option_iter = &mut Some(buf[1]).into_iter(); 44 | 45 | let result = { 46 | let mut iter = option_iter. 47 | map(Ok). 48 | chain(source.bytes()); 49 | 50 | parse_event(buf[0], &mut iter) 51 | }; 52 | 53 | // If the option_iter wasn't consumed, keep the byte for later. 54 | self.leftover = option_iter.next(); 55 | result 56 | }, 57 | Ok(_) => unreachable!(), 58 | Err(e) => Err(e) 59 | }; 60 | 61 | Some(result) 62 | } 63 | } 64 | 65 | fn parse_event(item: u8, iter: &mut I) -> Result 66 | where I: Iterator> 67 | { 68 | let mut buf = vec![item]; 69 | let result = { 70 | let mut iter = iter.inspect(|byte| { 71 | if let Ok(byte) = *byte { 72 | buf.push(byte); 73 | } 74 | }); 75 | event::parse_event(item, &mut iter) 76 | }; 77 | result.or_else(|_| Ok(Event::Unknown(buf))) 78 | } 79 | -------------------------------------------------------------------------------- /src/terminal/mod.rs: -------------------------------------------------------------------------------- 1 | mod event; 2 | mod input; 3 | use std::mem; 4 | use std::fs::{File, OpenOptions}; 5 | use std::io::{self, Write, Read}; 6 | use std::os::unix::io::AsRawFd; 7 | use libc::{TCSANOW, TIOCGWINSZ, winsize}; 8 | use libc::{termios, tcgetattr, tcsetattr}; 9 | use libc::{ioctl, cfmakeraw}; 10 | 11 | pub use self::input::*; 12 | pub use self::event::*; 13 | 14 | #[derive(Debug)] 15 | pub enum Error { 16 | TcGetAttr, 17 | TcSetAttr 18 | } 19 | 20 | pub struct Terminal { 21 | file: File, 22 | prev_termios: Option, 23 | pub max_width: usize, 24 | pub max_height: usize 25 | } 26 | 27 | impl Terminal { 28 | /// Creates a new Terminal from the provided filename 29 | pub fn from(filename: &str) -> io::Result { 30 | let file = OpenOptions::new().write(true).read(true).open(filename)?; 31 | let fd = file.as_raw_fd(); 32 | 33 | let mut terminal = Terminal { 34 | file: file, 35 | prev_termios: None, 36 | max_width: 80, 37 | max_height: 25 38 | }; 39 | 40 | unsafe { 41 | let mut ws: winsize = mem::zeroed(); 42 | if ioctl(fd, TIOCGWINSZ, &mut ws) != -1 { 43 | terminal.max_width = ws.ws_col as usize; 44 | terminal.max_height = ws.ws_row as usize; 45 | } 46 | } 47 | 48 | Ok(terminal) 49 | } 50 | 51 | pub fn set_raw_mode(&mut self) -> Result<(), Error> { 52 | let fd = self.file.as_raw_fd(); 53 | 54 | unsafe { 55 | let mut ios: termios = mem::zeroed(); 56 | 57 | // get the existing termios 58 | if tcgetattr(fd, &mut ios) != 0 { 59 | return Err(Error::TcGetAttr); 60 | } 61 | 62 | self.prev_termios = Some(ios); 63 | 64 | // enable raw mode 65 | cfmakeraw(&mut ios); 66 | 67 | // apply the raw mode termios 68 | if tcsetattr(fd, TCSANOW, &ios) != 0 { 69 | return Err(Error::TcSetAttr); 70 | } 71 | } 72 | 73 | Ok(()) 74 | } 75 | 76 | pub fn reset(&mut self) -> Result<(), Error> { 77 | if let Some(opts) = self.prev_termios { 78 | let fd = self.file.as_raw_fd(); 79 | 80 | // disable raw mode, by setting the original termios 81 | unsafe { 82 | if tcsetattr(fd, TCSANOW, &opts) != 0 { 83 | return Err(Error::TcSetAttr); 84 | } 85 | } 86 | } 87 | 88 | Ok(()) 89 | } 90 | 91 | pub fn events(&self) -> io::Result> { 92 | self.file.try_clone().map(Events::new) 93 | } 94 | } 95 | 96 | impl Read for Terminal { 97 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 98 | self.file.read(buf) 99 | } 100 | } 101 | 102 | impl Write for Terminal { 103 | fn write(&mut self, buf: &[u8]) -> io::Result { 104 | self.file.write(buf) 105 | } 106 | 107 | fn flush(&mut self) -> io::Result<()> { 108 | self.file.flush() 109 | } 110 | } 111 | 112 | impl Drop for Terminal { 113 | fn drop(&mut self) { 114 | self.reset().unwrap(); 115 | } 116 | } 117 | 118 | #[cfg(test)] 119 | mod tests { 120 | use super::*; 121 | use std::io::Write; 122 | 123 | #[test] 124 | fn terminal() { 125 | let mut term = Terminal::from("/dev/tty").expect("Unable to open /dev/tty"); 126 | 127 | // if all is good, this should _not_ break the terminal, because the 128 | // Drop trait impl will clean up 129 | term.set_raw_mode().expect("Unable to enable raw mode"); 130 | 131 | write!(term, "").unwrap(); 132 | term.flush().unwrap(); 133 | } 134 | } 135 | --------------------------------------------------------------------------------