├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── USAGE.md ├── examples ├── README.md ├── blocking.rs ├── custom_io.rs ├── find.rs ├── fzf.rs ├── fzf_err_handling.rs ├── options.rs ├── poems.json ├── restart.rs ├── restart_ext.rs └── serde.rs └── src ├── component.rs ├── error.rs ├── event.rs ├── event └── bind.rs ├── incremental.rs ├── incremental └── partial.rs ├── injector.rs ├── lazy.rs ├── lib.rs ├── match_list.rs ├── match_list ├── draw.rs ├── item.rs ├── layout.rs ├── layout │ ├── reset.rs │ ├── resize.rs │ ├── selection.rs │ └── update.rs ├── span.rs ├── span │ └── tests.rs ├── tests.rs └── unicode.rs ├── observer.rs ├── prompt.rs ├── prompt └── tests.rs ├── render.rs └── util.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [ "master" ] 7 | 8 | permissions: 9 | contents: read 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | RUSTFLAGS: -D warnings 14 | RUSTDOCFLAGS: -D warnings 15 | 16 | jobs: 17 | tests: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | persist-credentials: false 23 | - uses: dtolnay/rust-toolchain@stable 24 | - uses: Swatinem/rust-cache@v2 25 | - name: Build test binaries 26 | run: cargo test --no-run --all-features 27 | - name: Run tests 28 | run: cargo test --no-fail-fast --all-features 29 | 30 | checks: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v4 34 | with: 35 | persist-credentials: false 36 | - uses: dtolnay/rust-toolchain@stable 37 | with: 38 | components: clippy, rustfmt 39 | - uses: Swatinem/rust-cache@v2 40 | - name: Build docs 41 | run: cargo doc --no-deps --all-features 42 | - name: Run Clippy lints 43 | run: cargo clippy 44 | - name: Check formatting 45 | run: cargo fmt --check 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.8.1] - 2025-02-07 9 | 10 | ### Added 11 | - Added method `Injector::renderer` to get a reference to the `Render` implementation internal to the picker. 12 | 13 | ## [0.8.0] - 2025-01-14 14 | 15 | ### Changed 16 | - **Breaking** The `EventSource` trait method `recv_timeout` now takes a mutable self-reference. 17 | This is to allow an `EventSource` implementation to maintain internal state. 18 | 19 | ### Added 20 | - Keybindings are now permitted to be `FnMut` rather than just `Fn`. 21 | 22 | ## [0.7.0] - 2025-01-13 23 | 24 | ### Changed 25 | - **Breaking** `Picker::pick` now returns an `error::PickError` instead of an `io::Error`. 26 | The new error type is required to more faithfully represent the possible failure modes of a custom `EventSource` implementation. 27 | There is a `From for io::Error` implementation to minimize breakage of existing code. 28 | However, the corresponding `io::Error::other` message contents have now changed to respect the new error types. 29 | 30 | ### Added 31 | - Reset selection to beginning of match list `ctrl + 0`. 32 | - New `PickerOptions::frame_interval` option to customize the refresh rate of the picker. 33 | - Reversed rendering with `PickerOptions::reversed` 34 | - New `Picker::pick_with_io` and `Picker::pick_with_keybind` functions that allows much greater IO customization. 35 | - Provide your own `Writer`. 36 | - Customize keybindings using a `StdinReader`. 37 | - Drive the picker using a `mpsc` channel. 38 | - Propagate custom errors to the picker from other threads. 39 | - Implement your own `EventSource` for total customization. 40 | - Support for interactive restarting 41 | - Initialize a restart using `Event::Restart`. 42 | - Watch for new `Injector`s using the `Observer` returned by `Picker::injector_observer`. 43 | - New examples to demonstrate the `Event` system 44 | - `custom_io` for a basic example 45 | - `fzf_err_handling` to use channels for event propagation 46 | - `restart` to demonstrate interactive restarting (with extended example `restart_ext`) 47 | 48 | ### Fixed 49 | - Fixed screen layout when resizing to prefer higher score elements. 50 | - Uses panic hook to correctly clean up screen if the picker panics. 51 | 52 | ## [0.6.4] - 2024-12-16 53 | 54 | ### Added 55 | - The picker now quits on `ctrl + d` if the query is empty. 56 | - Add "Backspace Word" on `ctrl + w`. 57 | 58 | ### Fixed 59 | - Picker no longer quits when pressing 'Enter' with no matches 60 | 61 | ## [0.6.3] - 2024-12-11 62 | 63 | ### Fixed 64 | - STDERR is now buffered to improve terminal write performance. 65 | - Corrected docs to clarify that control characters should not be included in rendered text. 66 | 67 | ## [0.6.2] - 2024-12-11 68 | 69 | ### Added 70 | - Added configuration for prompt padding and scroll padding. 71 | - Added key-bindings to go forward and backward by word, and to clear before and after cursor. 72 | - Support deleting next character (i.e. `Delete` on windows, and `fn + delete` on MacOS). 73 | 74 | ### Deprecated 75 | - `PickerOptions::right_highlight_padding` has been deprecated; use `PickerOptions::highlight_padding` instead. 76 | 77 | ### Fixed 78 | - Fixed highlight padding to correctly fill for highlight matches very close to the end of the screen. 79 | - Proper handling of graphemes and multi-width characters in the prompt string (#4). 80 | - Removed some unnecessary features from dependencies. 81 | 82 | ## [0.6.1] - 2024-12-04 83 | 84 | ### Added 85 | - New implementation of `Render` for any type which is `for<'a> Fn(&'a T) -> Cow<'a, str>`. 86 | - Improved documentation. 87 | 88 | ## [0.6.0] - 2024-12-01 89 | 90 | ### Changed 91 | - **Breaking** `Picker` now requires new `Render` implementation to describe how a given type is displayed on the screen. 92 | - `Picker::new` signature has changed. 93 | - `PickerOptions::picker` signature has changed. 94 | - **Breaking** `PickerOptions::query` and `Picker::set_query` now accept any argument which is `Into` instead of `ToString`. 95 | - **Breaking** `Picker::pick` uses STDERR instead of STDOUT for interactive screen. 96 | A lock is acquired to STDERR to reduce the chance of rendering corruption and prevent Mutex contention. 97 | - If your application requires debug logging, it is probably best to log to a file instead. 98 | - **Breaking** `Picker::injector` now returns a `nucleo_picker::Injector` instead of a `nucleo::Injector`. The `nucleo_picker::Injector` no longer exposes the internal match object; instead, rendering is done by the new `Render` trait. 99 | - User CTRL-C during `Picker::pick` now returns `io::Error` with custom error message. 100 | 101 | ### Removed 102 | - Suggested support for multiple columns has now been removed (multiple columns were never supported internally). 103 | 104 | ### Fixed 105 | - Picker no longer blocks STDIN and STDOUT. (#15) 106 | - Pressing DELETE when the prompt is empty no longer causes screen redraw. 107 | - Use synchronized output to avoid screen tearing on large render calls. (#14) 108 | - Correctly handle `\!`, `\^`, and `\$`. 109 | - Query strings are now correctly normalized to replace newlines and tabs with single spaces, and to disallow ASCII control characters. 110 | 111 | ### Added 112 | - Match highlighting. (#9) 113 | - Robust Unicode and multi-line support 114 | - Correctly renders multi-line items 115 | - Unicode width computations to correctly handle double-width and zero-width graphemes. 116 | - Full match item scrollback 117 | - Convenient `Render` implementations in new `render` module. 118 | - New configuration options for `PickerOptions`. (#2) 119 | - New example: `fzf` clone 120 | - Convenience features for adding new items to a `Picker`: 121 | - `Picker` and `Injector` now implement `Extend` for convenient item adding. 122 | - With the optional `serde` feature, an `&Injector` now implements `DeserializeSeed` to allow adding items from the picker directly from a deserializer. 123 | 124 | ## [0.5.0] - 2024-11-07 125 | 126 | ### Added 127 | - Better exposure of nucleo internals: 128 | - Restart the internal matcher with `Picker::restart` 129 | - Update internal configuration without rebuilding the `Picker` using `Picker::update_config` 130 | - Modify the default query string using `Picker::update_query` 131 | - New `PickerOptions` for more flexible `Picker` configuration: 132 | - Specifying an initial query string with `PickerOptions::query` 133 | 134 | ### Deprecated 135 | - `Picker::new` has been deprecated; use `PickerOptions`. 136 | 137 | ### Changed 138 | - Modified interactive checks: now requires both stdin and stdout to be interactive. 139 | - Various keybinding changes. 140 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Alex Rutar "] 3 | categories = ["command-line-interface"] 4 | description = "A performant and Unicode-aware fuzzy picker tui library" 5 | edition = "2021" 6 | keywords = ["cli"] 7 | license = "MIT OR Apache-2.0" 8 | name = "nucleo-picker" 9 | repository = "https://github.com/autobib/nucleo-picker" 10 | version = "0.8.1" 11 | 12 | [package.metadata.docs.rs] 13 | all-features = true 14 | rustdoc-args = ["--cfg", "docsrs"] 15 | 16 | [dependencies] 17 | crossterm = { version = "0.28", features = ["use-dev-tty"] } 18 | memchr = "2.7" 19 | nucleo = "0.5" 20 | parking_lot = "0.12.3" 21 | unicode-segmentation = "1.10" 22 | unicode-width = { version = "0.2", default-features = false } 23 | serde = { version = "1.0", optional = true } 24 | 25 | [dev-dependencies] 26 | crossbeam = "0.8.4" 27 | rand = "0.8.5" 28 | ignore = "0.4" 29 | serde = { version = "1.0", features = ["derive"] } 30 | serde_json = "1.0" 31 | 32 | [lints.clippy] 33 | redundant_closure_for_method_calls = "warn" 34 | same_functions_in_if_condition = "warn" 35 | semicolon_if_nothing_returned = "warn" 36 | uninlined_format_args = "warn" 37 | 38 | [[example]] 39 | name = "custom_io" 40 | 41 | [[example]] 42 | name = "fzf_err_handling" 43 | 44 | [[example]] 45 | name = "find" 46 | 47 | [[example]] 48 | name = "restart" 49 | 50 | [[example]] 51 | name = "restart_ext" 52 | 53 | [[example]] 54 | name = "blocking" 55 | 56 | [[example]] 57 | name = "serde" 58 | required-features = ["serde"] 59 | 60 | [[example]] 61 | name = "fzf" 62 | 63 | [[example]] 64 | name = "options" 65 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Version 2.0, January 2004 2 | http://www.apache.org/licenses/ 3 | 4 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 5 | 6 | 1. Definitions. 7 | 8 | "License" shall mean the terms and conditions for use, reproduction, 9 | and distribution as defined by Sections 1 through 9 of this document. 10 | 11 | "Licensor" shall mean the copyright owner or entity authorized by 12 | the copyright owner that is granting the License. 13 | 14 | "Legal Entity" shall mean the union of the acting entity and all 15 | other entities that control, are controlled by, or are under common 16 | control with that entity. For the purposes of this definition, 17 | "control" means (i) the power, direct or indirect, to cause the 18 | direction or management of such entity, whether by contract or 19 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity 23 | exercising permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, 26 | including but not limited to software source code, documentation 27 | source, and configuration files. 28 | 29 | "Object" form shall mean any form resulting from mechanical 30 | transformation or translation of a Source form, including but 31 | not limited to compiled object code, generated documentation, 32 | and conversions to other media types. 33 | 34 | "Work" shall mean the work of authorship, whether in Source or 35 | Object form, made available under the License, as indicated by a 36 | copyright notice that is included in or attached to the work 37 | (an example is provided in the Appendix below). 38 | 39 | "Derivative Works" shall mean any work, whether in Source or Object 40 | form, that is based on (or derived from) the Work and for which the 41 | editorial revisions, annotations, elaborations, or other modifications 42 | represent, as a whole, an original work of authorship. For the purposes 43 | of this License, Derivative Works shall not include works that remain 44 | separable from, or merely link (or bind by name) to the interfaces of, 45 | the Work and Derivative Works thereof. 46 | 47 | "Contribution" shall mean any work of authorship, including 48 | the original version of the Work and any modifications or additions 49 | to that Work or Derivative Works thereof, that is intentionally 50 | submitted to Licensor for inclusion in the Work by the copyright owner 51 | or by an individual or Legal Entity authorized to submit on behalf of 52 | the copyright owner. For the purposes of this definition, "submitted" 53 | means any form of electronic, verbal, or written communication sent 54 | to the Licensor or its representatives, including but not limited to 55 | communication on electronic mailing lists, source code control systems, 56 | and issue tracking systems that are managed by, or on behalf of, the 57 | Licensor for the purpose of discussing and improving the Work, but 58 | excluding communication that is conspicuously marked or otherwise 59 | designated in writing by the copyright owner as "Not a Contribution." 60 | 61 | "Contributor" shall mean Licensor and any individual or Legal Entity 62 | on behalf of whom a Contribution has been received by Licensor and 63 | subsequently incorporated within the Work. 64 | 65 | 2. Grant of Copyright License. Subject to the terms and conditions of 66 | this License, each Contributor hereby grants to You a perpetual, 67 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 68 | copyright license to reproduce, prepare Derivative Works of, 69 | publicly display, publicly perform, sublicense, and distribute the 70 | Work and such Derivative Works in Source or Object form. 71 | 72 | 3. Grant of Patent License. Subject to the terms and conditions of 73 | this License, each Contributor hereby grants to You a perpetual, 74 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 75 | (except as stated in this section) patent license to make, have made, 76 | use, offer to sell, sell, import, and otherwise transfer the Work, 77 | where such license applies only to those patent claims licensable 78 | by such Contributor that are necessarily infringed by their 79 | Contribution(s) alone or by combination of their Contribution(s) 80 | with the Work to which such Contribution(s) was submitted. If You 81 | institute patent litigation against any entity (including a 82 | cross-claim or counterclaim in a lawsuit) alleging that the Work 83 | or a Contribution incorporated within the Work constitutes direct 84 | or contributory patent infringement, then any patent licenses 85 | granted to You under this License for that Work shall terminate 86 | as of the date such litigation is filed. 87 | 88 | 4. Redistribution. You may reproduce and distribute copies of the 89 | Work or Derivative Works thereof in any medium, with or without 90 | modifications, and in Source or Object form, provided that You 91 | meet the following conditions: 92 | 93 | (a) You must give any other recipients of the Work or 94 | Derivative Works a copy of this License; and 95 | 96 | (b) You must cause any modified files to carry prominent notices 97 | stating that You changed the files; and 98 | 99 | (c) You must retain, in the Source form of any Derivative Works 100 | that You distribute, all copyright, patent, trademark, and 101 | attribution notices from the Source form of the Work, 102 | excluding those notices that do not pertain to any part of 103 | the Derivative Works; and 104 | 105 | (d) If the Work includes a "NOTICE" text file as part of its 106 | distribution, then any Derivative Works that You distribute must 107 | include a readable copy of the attribution notices contained 108 | within such NOTICE file, excluding those notices that do not 109 | pertain to any part of the Derivative Works, in at least one 110 | of the following places: within a NOTICE text file distributed 111 | as part of the Derivative Works; within the Source form or 112 | documentation, if provided along with the Derivative Works; or, 113 | within a display generated by the Derivative Works, if and 114 | wherever such third-party notices normally appear. The contents 115 | of the NOTICE file are for informational purposes only and 116 | do not modify the License. You may add Your own attribution 117 | notices within Derivative Works that You distribute, alongside 118 | or as an addendum to the NOTICE text from the Work, provided 119 | that such additional attribution notices cannot be construed 120 | as modifying the License. 121 | 122 | You may add Your own copyright statement to Your modifications and 123 | may provide additional or different license terms and conditions 124 | for use, reproduction, or distribution of Your modifications, or 125 | for any such Derivative Works as a whole, provided Your use, 126 | reproduction, and distribution of the Work otherwise complies with 127 | the conditions stated in this License. 128 | 129 | 5. Submission of Contributions. Unless You explicitly state otherwise, 130 | any Contribution intentionally submitted for inclusion in the Work 131 | by You to the Licensor shall be under the terms and conditions of 132 | this License, without any additional terms or conditions. 133 | Notwithstanding the above, nothing herein shall supersede or modify 134 | the terms of any separate license agreement you may have executed 135 | with Licensor regarding such Contributions. 136 | 137 | 6. Trademarks. This License does not grant permission to use the trade 138 | names, trademarks, service marks, or product names of the Licensor, 139 | except as required for reasonable and customary use in describing the 140 | origin of the Work and reproducing the content of the NOTICE file. 141 | 142 | 7. Disclaimer of Warranty. Unless required by applicable law or 143 | agreed to in writing, Licensor provides the Work (and each 144 | Contributor provides its Contributions) on an "AS IS" BASIS, 145 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 146 | implied, including, without limitation, any warranties or conditions 147 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 148 | PARTICULAR PURPOSE. You are solely responsible for determining the 149 | appropriateness of using or redistributing the Work and assume any 150 | risks associated with Your exercise of permissions under this License. 151 | 152 | 8. Limitation of Liability. In no event and under no legal theory, 153 | whether in tort (including negligence), contract, or otherwise, 154 | unless required by applicable law (such as deliberate and grossly 155 | negligent acts) or agreed to in writing, shall any Contributor be 156 | liable to You for damages, including any direct, indirect, special, 157 | incidental, or consequential damages of any character arising as a 158 | result of this License or out of the use or inability to use the 159 | Work (including but not limited to damages for loss of goodwill, 160 | work stoppage, computer failure or malfunction, or any and all 161 | other commercial damages or losses), even if such Contributor 162 | has been advised of the possibility of such damages. 163 | 164 | 9. Accepting Warranty or Additional Liability. While redistributing 165 | the Work or Derivative Works thereof, You may choose to offer, 166 | and charge a fee for, acceptance of support, warranty, indemnity, 167 | or other liability obligations and/or rights consistent with this 168 | License. However, in accepting such obligations, You may act only 169 | on Your own behalf and on Your sole responsibility, not on behalf 170 | of any other Contributor, and only if You agree to indemnify, 171 | defend, and hold each Contributor harmless for any liability 172 | incurred by, or claims asserted against, such Contributor by reason 173 | of your accepting any such warranty or additional liability. 174 | 175 | END OF TERMS AND CONDITIONS 176 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 by Alex Rutar 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 | [![Current crates.io release](https://img.shields.io/crates/v/nucleo-picker)](https://crates.io/crates/nucleo-picker) 2 | [![Documentation](https://img.shields.io/badge/docs.rs-nucleo--picker-66c2a5?labelColor=555555&logoColor=white&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K)](https://docs.rs/nucleo-picker/) 3 | 4 | # nucleo-picker 5 | A native [Rust](https://www.rust-lang.org/) library which enables you to incorporate a highly performant and Unicode-aware fuzzy picker directly in your own terminal application. 6 | 7 | This library provides a TUI for the [`nucleo`](https://docs.rs/nucleo/latest/nucleo/) crate with an interface similar to the [fzf](https://github.com/junegunn/fzf) command-line tool. 8 | 9 | - For implementation examples, jump to the [fzf example](#example) or see the [`examples`](examples) directory. 10 | - For documentation of interactive usage of the picker, see the [`USAGE.md`](USAGE.md) file. 11 | - For a list of recent changes, see the [`CHANGELOG.md`](CHANGELOG.md) file. 12 | 13 | ## Elevator pitch 14 | Why use this library instead of a general-purpose fuzzy-finder such as `fzf` or a lower level library such as `nucleo`? 15 | 16 | 1. **Much tighter integration between your data source and your application.** 17 | Instead of reading from a SQLite database with `sqlite3` and then parsing raw text, read directly into in-memory data structures with [`rusqlite`](https://docs.rs/rusqlite/latest/rusqlite/) and render the in-memory objects in the picker. 18 | 2. **Skip the subprocess overhead and improve startup time.** 19 | Instead of starting up a subprocess to call `fzf`, have the picker integrated directly into your binary. 20 | 3. **Distinguish items from their matcher representation.** 21 | Instead of writing your data structure to a string, passing it to `fzf`, and then parsing the resulting match string back into your data structure, directly obtain the original data structure when matching is complete. 22 | 4. **Don't spend time debugging terminal rendering edge cases.** 23 | Out-of-the-box, `nucleo-picker` handles terminal rendering subtleties such as *multiline rendering*, *double-width Unicode*, *automatic overflow scrollthrough*, and *grapheme-aware query input* so you don't have to. 24 | 5. **Handle complex use cases using events.** 25 | `nucleo-picker` exposes a fully-featured [event system](https://docs.rs/nucleo-picker/latest/nucleo_picker/event/) which can be used to drive the picker. 26 | This lets you [*customize keybindings*](https://docs.rs/nucleo-picker/latest/nucleo_picker/event/struct.StdinReader.html), support [*interactive restarts*](https://docs.rs/nucleo-picker/0.7.0-alpha.3/nucleo_picker/event/enum.Event.html#restart), and much more by implementing the [`EventSource`](https://docs.rs/nucleo-picker/latest/nucleo_picker/event/trait.EventSource.html) trait. 27 | Simplified versions of such features are available in [fzf](https://github.com/junegunn/fzf) but essentially require manual configuration via an embedded DSL. 28 | 29 | ## Features 30 | - [Highly optimized matching](https://github.com/helix-editor/nucleo). 31 | - Robust rendering: 32 | - Full Unicode handling with [Unicode text segmentation](https://crates.io/crates/unicode-segmentation) and [Unicode width](https://crates.io/crates/unicode-width). 33 | - Match highlighting with automatic scroll-through. 34 | - Correctly render multi-line or overflowed items, with standard and reversed item order. 35 | - Responsive interface with batched keyboard input. 36 | - Ergonomic API: 37 | - Fully concurrent lock- and wait-free streaming of input items. 38 | - Generic [`Picker`](https://docs.rs/nucleo-picker/latest/nucleo_picker/struct.Picker.html) for any type `T` which is `Send + Sync + 'static`. 39 | - [Customizable rendering](https://docs.rs/nucleo-picker/latest/nucleo_picker/trait.Render.html) of crate-local and foreign types with the `Render` trait. 40 | - Fully configurable event system: 41 | - Easily customizable keybindings. 42 | - Run the picker concurrently with your application using a fully-featured [event system](https://docs.rs/nucleo-picker/latest/nucleo_picker/event/), with optional support for complex features such as [*interactive restarting*](https://docs.rs/nucleo-picker/0.7.0-alpha.3/nucleo_picker/event/enum.Event.html#restart). 43 | - Optional and flexible [error propagation generics](https://docs.rs/nucleo-picker/latest/nucleo_picker/event/enum.Event.html#application-defined-abort) so your application errors can interface cleanly with the picker. 44 | 45 | ## Example 46 | Implement a heavily simplified `fzf` clone in 25 lines of code. 47 | Try it out with: 48 | ``` 49 | cargo build --release --example fzf 50 | cat myfile.txt | ./target/release/examples/fzf 51 | ``` 52 | The code to create the binary: 53 | ```rust 54 | use std::{ 55 | io::{self, IsTerminal}, 56 | process::exit, 57 | thread::spawn, 58 | }; 59 | 60 | use nucleo_picker::{render::StrRenderer, Picker}; 61 | 62 | fn main() -> io::Result<()> { 63 | let mut picker = Picker::new(StrRenderer); 64 | 65 | let injector = picker.injector(); 66 | spawn(move || { 67 | let stdin = io::stdin(); 68 | if !stdin.is_terminal() { 69 | for line in stdin.lines() { 70 | // silently drop IO errors! 71 | if let Ok(s) = line { 72 | injector.push(s); 73 | } 74 | } 75 | } 76 | }); 77 | 78 | match picker.pick()? { 79 | Some(it) => println!("{it}"), 80 | None => exit(1), 81 | } 82 | Ok(()) 83 | } 84 | ``` 85 | 86 | 87 | ## Related crates 88 | 89 | This crate mainly exists as a result of the author's annoyance with pretty much every fuzzy picker TUI in the rust ecosystem. 90 | As far as I am aware, the fully-exposed [event system](https://docs.rs/nucleo-picker/latest/nucleo_picker/event/enum.Event.html) is unique to this crate. 91 | Beyond this, here is a brief comparison: 92 | 93 | - [skim](https://docs.rs/skim/latest/skim/)'s `Arc` is inconvenient for a [variety of reasons](https://rutar.org/writing/using-closure-traits-to-simplify-rust-api/). 94 | `skim` also has a large number of dependencies and is designed more as a binary than a library. 95 | - [fuzzypicker](https://docs.rs/fuzzypicker/latest/fuzzypicker/) is based on `skim` and inherits `skim`'s problems. 96 | - [nucleo-ui](https://docs.rs/nucleo-ui/latest/nucleo_ui/) only has a blocking API and only supports matching on `String`. It also seems to be un-maintained. 97 | - [fuzzy-select](https://docs.rs/fuzzy-select/latest/fuzzy_select/) only has a blocking API. 98 | - [dialoguer `FuzzySelect`](https://docs.rs/dialoguer/latest/dialoguer/struct.FuzzySelect.html) only has a blocking API and only supports matching on `String`. 99 | The terminal handling also has a few strange bugs. 100 | 101 | ## Disclaimer 102 | There are a currently a few known problems which have not been addressed (see the [issues page on GitHub](https://github.com/autobib/nucleo-picker/issues) for a list). Issues and contributions are welcome! 103 | -------------------------------------------------------------------------------- /USAGE.md: -------------------------------------------------------------------------------- 1 | # Picker interactive usage 2 | This file contains documentation for interactive use of the picker. 3 | Jump to: 4 | 5 | - [Query syntax](#query-syntax) 6 | - [Keyboard shortcuts](#keyboard-shortcuts) 7 | - [Scroll and paste](#scroll-and-paste) 8 | 9 | 10 | ## Query syntax 11 | The query is parsed as a sequence of whitespace-separated "atoms", such as `a1 a2 a3`. 12 | By default, each atom corresponds to a fuzzy match: that is, higher score is assigned for a closer match, but exact match is not required. 13 | There is also a special syntax for various types of exact matches. 14 | 15 | - `'foo` matches an exact substring, with negation `!foo` 16 | - `^foo` matches an exact prefix, with negation `!^foo` 17 | - `foo$` matches an exact suffix, with negation `!foo$` 18 | - `^foo$` matches the entire string exactly, with negation `!^foo$` 19 | 20 | Note that the negations must match exactly. 21 | The negation does not impact scoring: instead, any match for a negative atom is simply discarded, regardless of score. 22 | 23 | Whitespace (that is, anything with the [Unicode whitespace property](https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt)) and control symbols `'^$!` can also be interpreted literally by escaping with a backslash `\`. 24 | Otherwise, backslashes are interpreted literally; in particular, backslashes do not need to be escaped. 25 | For example: 26 | 27 | - `\ ` matches the literal space ` `. 28 | - `\\` and `\a` match, respectively, literal `\\` and `\a`. 29 | - The query `fo\$ ^bar` means that we match for strings which contain `fo$` (or similar), and which begin with the exact string `bar`. 30 | 31 | The query syntax is also documented in the [nucleo-matcher](https://docs.rs/nucleo-matcher/latest/nucleo_matcher/pattern/enum.AtomKind.html) crate. 32 | 33 | 34 | ## Keyboard shortcuts 35 | Generally speaking, we attempt to follow the bash-like or vim-like keyboard shortcut conventions. 36 | Most of these bindings are relatively standard, with some exceptions like `ctrl + o` and `ctrl + r`. 37 | 38 | Key bindings(s) | Action 39 | ------------------------|-------------------- 40 | ctrl + c | Abort 41 | esc, ctrl + g, ctrl + q | Quit (no selection) 42 | ctrl + d | Quit If Query Empty (no selection) 43 | ⏎, shift + ⏎ | Select and Quit 44 | ↑, ctrl + k, ctrl + p | Selection Up 45 | ↓, ctrl + j, ctrl + n | Selection Down 46 | ctrl + 0 | Reset Selection Scroll 47 | ←, ctrl + b | Cursor Left 48 | →, ctrl + f | Cursor Right 49 | ctrl + a, ⇱ | Cursor To Start 50 | ctrl + e | Cursor To End 51 | ctrl + u | Clear Before Cursor 52 | ctrl + o | Clear After Cursor 53 | ⌫, ctrl + h, shift + ⌫ | Backspace 54 | ctrl + w | Backspace Word 55 | ␡, fn + ␡ | Delete 56 | 57 | 58 | ## Scroll and paste 59 | By default, the picker does not directly capture scroll actions, but if your terminal forwards scroll as up / down arrow input, then scrolling will work as expected. 60 | 61 | Pasting is also not directly handled, but rather depends on whether or not your terminal handles [bracketed paste](https://en.wikipedia.org/wiki/Bracketed-paste). 62 | If your terminal does not handle bracketed paste, then the characters are entered as though they were typed in one at a time, which may result in strange behaviour. 63 | By default, input characters are normalized: newlines and tabs are replaced with spaces, and control characters are removed. 64 | This is mainly relevant when pasting text into the query. 65 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Picker examples 2 | This directory contains a variety of examples of how to use the [nucleo-picker](https://docs.rs/nucleo-picker/latest/nucleo_picker/) crate in practice. 3 | 4 | In order to try out the examples, run 5 | ``` 6 | cargo run --release --example 7 | ``` 8 | where `` is the part of the path without the `.rs` suffix. 9 | 10 | Some of the examples may require arguments or feature flags to run properly; see the individual files for more information. 11 | 12 | ## Directory 13 | 14 | File | Description 15 | -------------------------------------------|------------- 16 | [blocking.rs](blocking.rs) | A basic blocking example with a very small number of matches. 17 | [custom_io.rs](custom_io.rs) | Customize IO with keybindings and alternative writer. 18 | [find.rs](find.rs) | A basic [find](https://en.wikipedia.org/wiki/Find_(Unix)) implementation with fuzzy matching on resulting items. 19 | [fzf.rs](fzf.rs) | A simple [fzf](https://github.com/junegunn/fzf) clone which reads lines from STDIN and presents for matching. 20 | [fzf_err_handling.rs](fzf_err_handling.rs) | An improved version of the `fzf` example using channels to propagate read errors. 21 | [options.rs](options.rs) | Some customization examples of the picker. 22 | [restart.rs](restart.rs) | Demonstration of interactive restarting in response to user input. 23 | [restart_ext.rs](restart_ext.rs) | An extended version of the restart example. 24 | [serde.rs](serde.rs) | Use `serde` to deserialize picker items from input. 25 | -------------------------------------------------------------------------------- /examples/blocking.rs: -------------------------------------------------------------------------------- 1 | //! # Basic blocking picker 2 | //! 3 | //! This is almost a minimal example, but not really a good example of what to do in practice unless 4 | //! the number of items is very small since we block the main thread to populate the matcher. See 5 | //! [`find`](/examples/find.rs) for a (somewhat) more realistic use-case. 6 | use std::io; 7 | 8 | use nucleo_picker::{render::StrRenderer, Picker}; 9 | 10 | fn main() -> io::Result<()> { 11 | let mut picker = Picker::new(StrRenderer); 12 | 13 | let choices = vec![ 14 | "Rembrandt", 15 | "Velázquez", 16 | "Schiele", 17 | "Hockney", 18 | "Klimt", 19 | "Bruegel", 20 | "Magritte", 21 | "Carvaggio", 22 | ]; 23 | 24 | // populate the matcher 25 | let injector = picker.injector(); 26 | for opt in choices { 27 | // Use `RenderStr` renderer to generate the match contents, since the choices are already 28 | // string types. 29 | injector.push(opt); 30 | } 31 | 32 | // open interactive prompt 33 | match picker.pick()? { 34 | Some(opt) => println!("You selected: '{opt}'"), 35 | None => println!("Nothing selected!"), 36 | } 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /examples/custom_io.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; 4 | use nucleo_picker::{ 5 | event::{keybind_default, Event, StdinReader}, 6 | render::StrRenderer, 7 | Picker, 8 | }; 9 | 10 | /// Keybindings which use the default keybindings, but instead of aborting on `ctrl + c`, 11 | /// simply perform a normal quit action. 12 | fn keybind_no_interrupt(key_event: KeyEvent) -> Option { 13 | match key_event { 14 | KeyEvent { 15 | kind: KeyEventKind::Press, 16 | modifiers: KeyModifiers::CONTROL, 17 | code: KeyCode::Char('c'), 18 | .. 19 | } => Some(Event::Quit), 20 | e => keybind_default(e), 21 | } 22 | } 23 | 24 | fn main() -> io::Result<()> { 25 | let mut picker = Picker::new(StrRenderer); 26 | 27 | let choices = vec![ 28 | "Alvar Aalto", 29 | "Frank Lloyd Wright", 30 | "Zaha Hadid", 31 | "Le Corbusier", 32 | ]; 33 | 34 | // populate the matcher using the convenience 'extend' implementation 35 | picker.extend(choices); 36 | 37 | // launch the interactive picker with the customized keybindings, and draw the picker on 38 | // standard output 39 | match picker.pick_with_io( 40 | StdinReader::new(keybind_no_interrupt), 41 | &mut std::io::stdout(), 42 | )? { 43 | Some(opt) => println!("Your preferred architect is: '{opt}'"), 44 | None => println!("No architect selected!"), 45 | } 46 | 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /examples/find.rs: -------------------------------------------------------------------------------- 1 | //! # Non-blocking `find`-style picker 2 | //! 3 | //! Iterate over directories to populate the picker, but do not block so that 4 | //! matching can be done while the picker is populated. 5 | use std::{borrow::Cow, env::args, io, path::PathBuf, thread::spawn}; 6 | 7 | use ignore::{DirEntry, WalkBuilder, WalkState}; 8 | use nucleo_picker::{nucleo::Config, PickerOptions, Render}; 9 | 10 | pub struct DirEntryRender; 11 | 12 | impl Render for DirEntryRender { 13 | type Str<'a> = Cow<'a, str>; 14 | 15 | /// Render a `DirEntry` using its internal path buffer. 16 | fn render<'a>(&self, value: &'a DirEntry) -> Self::Str<'a> { 17 | value.path().to_string_lossy() 18 | } 19 | } 20 | 21 | fn main() -> io::Result<()> { 22 | let mut picker = PickerOptions::default() 23 | // See the nucleo configuration for more options: 24 | // https://docs.rs/nucleo/latest/nucleo/struct.Config.html 25 | .config(Config::DEFAULT.match_paths()) 26 | // Use our custom renderer for a `DirEntry` 27 | .picker(DirEntryRender); 28 | 29 | // "argument parsing" 30 | let root: PathBuf = match args().nth(1) { 31 | Some(path) => path.into(), 32 | None => ".".into(), 33 | }; 34 | 35 | // populate from a separate thread to avoid locking the picker interface 36 | let injector = picker.injector(); 37 | spawn(move || { 38 | WalkBuilder::new(root).build_parallel().run(|| { 39 | let injector = injector.clone(); 40 | Box::new(move |walk_res| { 41 | if let Ok(dir) = walk_res { 42 | injector.push(dir); 43 | } 44 | WalkState::Continue 45 | }) 46 | }); 47 | }); 48 | 49 | match picker.pick()? { 50 | // the matched `entry` is `&DirEntry` 51 | Some(entry) => println!("Path of selected file: '{}'", entry.path().display()), 52 | None => println!("No file selected!"), 53 | } 54 | 55 | Ok(()) 56 | } 57 | -------------------------------------------------------------------------------- /examples/fzf.rs: -------------------------------------------------------------------------------- 1 | //! # Simple `fzf` clone 2 | //! 3 | //! Read lines from `stdin` in a streaming fashion and populate the picker, imitating the basic 4 | //! functionality of [fzf](https://github.com/junegunn/fzf). 5 | use std::{ 6 | io::{self, IsTerminal}, 7 | process::exit, 8 | thread::spawn, 9 | }; 10 | 11 | use nucleo_picker::{render::StrRenderer, Picker}; 12 | 13 | fn main() -> io::Result<()> { 14 | let mut picker = Picker::new(StrRenderer); 15 | 16 | let injector = picker.injector(); 17 | spawn(move || { 18 | let stdin = io::stdin(); 19 | if !stdin.is_terminal() { 20 | for line in stdin.lines() { 21 | // silently drop IO errors! 22 | if let Ok(s) = line { 23 | injector.push(s); 24 | } 25 | } 26 | } 27 | }); 28 | 29 | match picker.pick()? { 30 | Some(it) => println!("{it}"), 31 | None => exit(1), 32 | } 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /examples/fzf_err_handling.rs: -------------------------------------------------------------------------------- 1 | //! # A version of the `fzf` clone with better error handling 2 | //! 3 | //! Read lines from `stdin` in a streaming fashion and populate the picker, imitating the basic 4 | //! functionality of [fzf](https://github.com/junegunn/fzf). 5 | //! 6 | //! Unlike the `fzf` example, this example forwards IO errors to the picker thread and tells it to 7 | //! disconnect. 8 | 9 | use std::{ 10 | io::{self, IsTerminal}, 11 | process::exit, 12 | sync::mpsc::channel, 13 | thread::spawn, 14 | }; 15 | 16 | use nucleo_picker::{ 17 | event::{Event, StdinEventSender}, 18 | render::StrRenderer, 19 | Picker, 20 | }; 21 | 22 | /// The custom error type for our application. We could just use an `io::Error` directly (and also 23 | /// benefit from free error propogation), but for demonstration purposes we also add more context 24 | /// to the IO error with a custom wrapper type. 25 | enum AppError { 26 | Key(io::Error), 27 | Stdin(io::Error), 28 | } 29 | 30 | fn main() -> io::Result<()> { 31 | let mut picker = Picker::new(StrRenderer); 32 | 33 | // initialize stderr and check that we can actually write to the screen 34 | let mut stderr = io::stderr().lock(); 35 | if !stderr.is_terminal() { 36 | eprintln!("Failed to start picker: STDERR not interactive!"); 37 | exit(1); 38 | } 39 | 40 | // create a new channel. The `sender` end is used to send `Event`s to the picker, and the 41 | // `receiver` end is passed directly to the picker so that it can receive the corresponding 42 | // events 43 | let (sender, receiver) = channel(); 44 | 45 | // spawn a stdin watcher to read keyboard events and send them to the channel 46 | let stdin_watcher = StdinEventSender::with_default_keybindings(sender.clone()); 47 | spawn(move || match stdin_watcher.watch() { 48 | Ok(()) => { 49 | // this path occurs when the picker quits and the receiver is dropped so there 50 | // is no more work to be done 51 | } 52 | Err(io_err) => { 53 | // we received an IO error while trying to read keyboard events, so we recover the 54 | // inner channel and send an `Abort` event to tell the picker to quit immediately 55 | // 56 | // if we do not send the `Abort` event, or any other event which causes the picker to 57 | // quit (such as a `Quit` event), the picker will hang until the thread reading from 58 | // standard input completes, which could be a very long time 59 | let inner = stdin_watcher.into_sender(); 60 | // if this fails, the picker already quit 61 | let _ = inner.send(Event::Abort(AppError::Key(io_err))); 62 | return; 63 | } 64 | }); 65 | 66 | // spawn a reader to read lines from standard input 67 | let injector = picker.injector(); 68 | spawn(move || { 69 | let stdin = io::stdin(); 70 | if !stdin.is_terminal() { 71 | for line in stdin.lines() { 72 | match line { 73 | // add the line to the match list 74 | Ok(s) => injector.push(s), 75 | Err(io_err) => { 76 | // if we encounter an IO error, we send the corresponding error 77 | // to the picker so that it can abort and propogate the error 78 | // 79 | // it would also be fine to return but not send an abort event 80 | // since the picker will remain interactive with the items it has 81 | // already received. 82 | let _ = sender.send(Event::Abort(AppError::Stdin(io_err))); 83 | return; 84 | } 85 | } 86 | } 87 | } 88 | }); 89 | 90 | match picker.pick_with_io(receiver, &mut stderr) { 91 | Ok(Some(item)) => { 92 | println!("{item}"); 93 | Ok(()) 94 | } 95 | Ok(None) => exit(1), 96 | Err(e) => { 97 | // the 'factor' convenience method splits the error into a 98 | // `Result>`; so we just need to handle our application error. 99 | match e.factor()? { 100 | AppError::Key(io_err) => eprintln!("IO error during keyboard input: {io_err}"), 101 | AppError::Stdin(io_err) => { 102 | eprintln!("IO error when reading from standard input: {io_err}") 103 | } 104 | } 105 | exit(1); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /examples/options.rs: -------------------------------------------------------------------------------- 1 | //! # Demonstration of configuration options 2 | //! 3 | //! This blocking example demonstrates some of the configuration options available to the picker. 4 | use std::io::Result; 5 | 6 | use nucleo_picker::{nucleo::Config, render::StrRenderer, PickerOptions}; 7 | 8 | fn main() -> Result<()> { 9 | let mut picker = PickerOptions::default() 10 | // set the configuration to match 'path-like' objects 11 | .config(Config::DEFAULT.match_paths()) 12 | // set the default prompt to `/var` 13 | .query("/var") 14 | .picker(StrRenderer); 15 | 16 | let choices = vec![ 17 | "/var/tmp", 18 | "/var", 19 | "/usr/local", 20 | "/usr", 21 | "/usr/local/share", 22 | "/dev", 23 | ]; 24 | 25 | // populate the matcher 26 | let injector = picker.injector(); 27 | for opt in choices { 28 | injector.push(opt); 29 | } 30 | 31 | // open interactive prompt 32 | match picker.pick()? { 33 | Some(opt) => println!("You selected: '{opt}'"), 34 | None => println!("Nothing selected!"), 35 | } 36 | 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /examples/poems.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "author": "Ted Kooser", 4 | "title": "Snow Fence", 5 | "lines": [ 6 | "The red fence", 7 | "takes the cold trail", 8 | "north; no meat", 9 | "on its ribs", 10 | "but neither has it", 11 | "much to carry." 12 | ] 13 | }, 14 | { 15 | "author": "Ezra Pound", 16 | "title": "In a Station of the Metro", 17 | "lines": [ 18 | "The apparition of these faces in the crowd:", 19 | "Petals on a wet, black bough." 20 | ] 21 | }, 22 | { 23 | "author": "Robert Frost", 24 | "title": "Nothing Gold Can Stay", 25 | "lines": [ 26 | "Nature's first green is gold,", 27 | "Her hardest hue to hold.", 28 | "Her early leaf's a flower;", 29 | "But only so an hour." 30 | ] 31 | }, 32 | { 33 | "author": "Matsuo Bashō", 34 | "title": "The Old Pond", 35 | "lines": [ 36 | "An old silent pond", 37 | "A frog jumps into the pond—", 38 | "Splash! Silence again." 39 | ] 40 | }, 41 | { 42 | "author": "Alfred, Lord Tennyson", 43 | "title": "The Eagle", 44 | "lines": [ 45 | "He clasps the crag with crooked hands;", 46 | "Close to the sun in lonely lands,", 47 | "Ring'd with the azure world, he stands.", 48 | "", 49 | "The wrinkled sea beneath him crawls;", 50 | "He watches from his mountain walls,", 51 | "And like a thunderbolt he falls." 52 | ] 53 | }, 54 | { 55 | "author": "Amir Khusrau", 56 | "title": "He Visits My Town Once a Year", 57 | "lines": [ 58 | "He visits my town once a year.", 59 | "He fills my mouth with kisses and nectar.", 60 | "I spend all my money on him.", 61 | "Who, girl, your man?", 62 | "No, a mango" 63 | ] 64 | }, 65 | { 66 | "author": "Shel Silverstein", 67 | "title": "Tell Me", 68 | "lines": [ 69 | "Tell me I'm clever,", 70 | "Tell me I'm kind,", 71 | "Tell me I'm talented,", 72 | "Tell me I'm cute,", 73 | "Tell me I'm sensitive,", 74 | "Graceful and wise,", 75 | "Tell me I'm perfect-", 76 | "But tell me the truth. " 77 | ] 78 | }, 79 | { 80 | "author": "Uejima Onitsura", 81 | "title": "Untitled", 82 | "lines": [ 83 | "Look, a nightingale", 84 | "They have lighted on plum-trees", 85 | "From antiquity." 86 | ] 87 | } 88 | ] 89 | -------------------------------------------------------------------------------- /examples/restart.rs: -------------------------------------------------------------------------------- 1 | //! # Picker with interactive restarts 2 | //! 3 | //! Generates a list of 100 random `u32`s to be selected. The user can either select 4 | //! one of the options, or press `ctrl + r` to receive a new list of random integers. 5 | 6 | use std::{ 7 | convert::Infallible, 8 | io::{self, IsTerminal}, 9 | process::exit, 10 | thread::spawn, 11 | }; 12 | 13 | use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; 14 | use nucleo_picker::{ 15 | event::{keybind_default, Event, StdinReader}, 16 | render::DisplayRenderer, 17 | Picker, 18 | }; 19 | use rand::{distributions::Standard, thread_rng, Rng}; 20 | 21 | fn main() -> io::Result<()> { 22 | let mut picker: Picker = Picker::new(DisplayRenderer); 23 | 24 | // initialize stderr and check that we can actually write to the screen 25 | let mut stderr = io::stderr().lock(); 26 | if !stderr.is_terminal() { 27 | eprintln!("Failed to start picker: STDERR not interactive!"); 28 | exit(1); 29 | } 30 | 31 | // get a restart observer, which will be sent the new injectors when the picker processes a 32 | // `Restart` event; we initialize with an injector so that items are sent immediately 33 | let observer = picker.injector_observer(true); 34 | 35 | // Create a thread to regenerate the number list in response to 'restart' events. 36 | // 37 | // Generating 100 random `u32`s is extremely fast so we do not need to worry about keeping 38 | // up with restart events. For slower and more resource-intensive item generation, in 39 | // practice one would regularly check the channel using `receiver.try_recv` for new 40 | // restart events to check if the current computation should be halted and restarted on a 41 | // new injector. See the `restart_ext` example for this implementation. 42 | spawn(move || { 43 | let mut rng = thread_rng(); 44 | 45 | // block when waiting for new events, since we have nothing else to do. If the match does 46 | // not succeed, it means the channel dropped so we can shut down this thread. 47 | while let Ok(mut injector) = observer.recv() { 48 | // the restart event here is an injector for the picker; send the new items to the 49 | // injector every time we witness the event 50 | injector.extend((&mut rng).sample_iter(Standard).take(100)); 51 | } 52 | }); 53 | 54 | // Initialize an event source to watch for keyboard events. 55 | // 56 | // It is also possible to process restart events in the same thread used to process keyboard 57 | // events. However, if generating new items were to take a long time, we do not want to lag 58 | // user input and block watching for new keyboard events. In this specific example, it would 59 | // be fine. 60 | let event_source = StdinReader::new(move |key_event| match key_event { 61 | KeyEvent { 62 | kind: KeyEventKind::Press, 63 | modifiers: KeyModifiers::CONTROL, 64 | code: KeyCode::Char('r'), 65 | .. 66 | } => { 67 | // we create the restart event on `ctrl + n`. since this invalidates existing 68 | // injectors, a new injector is immediately sent to the observer which we are watching 69 | // in the other thread 70 | Some(Event::::Restart) 71 | } 72 | e => keybind_default(e), 73 | }); 74 | 75 | match picker.pick_with_io(event_source, &mut stderr)? { 76 | Some(num) => { 77 | println!("Your favourite number is: {num}"); 78 | Ok(()) 79 | } 80 | None => { 81 | println!("You didn't like any of the numbers!"); 82 | exit(1); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /examples/restart_ext.rs: -------------------------------------------------------------------------------- 1 | //! # Picker with interactive restarts and slow event generation 2 | //! 3 | //! Generates a list of 1000 random `u32`s to be selected but with intentionally introduced delay 4 | //! to imitate 'work' being done in the background. 5 | //! 6 | //! The user can either select one of the options, or press `ctrl + r` to receive a new list of 7 | //! random integers. Try pressing `ctrl + r` very rapidly; new numbers will be generated even before 8 | //! the previous list has finished rendering. 9 | //! 10 | //! This is a more complex version of the `restart` example; it is better to start there first. The 11 | //! only documentation here is for the changes relative to the `restart` example. 12 | 13 | use std::{ 14 | convert::Infallible, 15 | io::{self, IsTerminal}, 16 | process::exit, 17 | sync::mpsc::TryRecvError, 18 | thread::{sleep, spawn}, 19 | time::Duration, 20 | }; 21 | 22 | use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; 23 | use nucleo_picker::{ 24 | event::{keybind_default, Event, StdinReader}, 25 | render::DisplayRenderer, 26 | Picker, 27 | }; 28 | use rand::random; 29 | 30 | /// Generate a random `u32` but with a lot of extra delay to imitate an expensive computation. 31 | fn slow_random() -> u32 { 32 | sleep(Duration::from_millis(5)); 33 | random() 34 | } 35 | 36 | fn main() -> io::Result<()> { 37 | let mut picker: Picker = Picker::new(DisplayRenderer); 38 | 39 | let mut stderr = io::stderr().lock(); 40 | if !stderr.is_terminal() { 41 | eprintln!("Failed to start picker: STDERR not interactive!"); 42 | exit(1); 43 | } 44 | 45 | // Do not initialize with an injector since we will send it ourself 46 | let observer = picker.injector_observer(false); 47 | 48 | let injector = picker.injector(); 49 | spawn(move || { 50 | const NUM_ITEMS: usize = 1000; 51 | 52 | // because of the delay, generating 1000 `u32`s will take about 5 seconds, which is way 53 | // too long to be done in a single frame. Therefore after generating each random number 54 | // we check for a restart event before continuing. 55 | 56 | // In this example, coming up with the check frequency is quite easy since we know the 57 | // delay is 5ms, which is approximately the frame interval. In practice, with 58 | // computation-heavy item generation, tuning the check frequency to happen approximately 59 | // twice per frame can be very challenging. Note that the `receiver.try_recv` call is quite 60 | // cheap so it is better to err towards overeager checks than infrequent checks. 61 | 62 | // the current active injector 63 | let mut current_injector = injector; 64 | let mut remaining_items = NUM_ITEMS; 65 | 66 | loop { 67 | match observer.try_recv() { 68 | Ok(new_injector) => { 69 | // we received a new injector so we should immediately start sending `u32`s to 70 | // it instead 71 | current_injector = new_injector; 72 | remaining_items = NUM_ITEMS; 73 | } 74 | Err(TryRecvError::Empty) => { 75 | if remaining_items > 0 { 76 | // we still have remaining data to be sent; continue to send it to the 77 | // picker 78 | remaining_items -= 1; 79 | current_injector.push(slow_random()); 80 | } else if let Ok(new_injector) = observer.recv() { 81 | // we have sent all of the necessary data; but we cannot simply skip this 82 | // branch or we will spin-loop and consume unnecessary CPU cycles. Instead, 83 | // we should block and wait for the next restart event since we have 84 | // nothing else to do. Once we receive the new injector, reset the state 85 | // and begin generating again. 86 | current_injector = new_injector; 87 | remaining_items = NUM_ITEMS; 88 | } else { 89 | // observer.recv() returned an error, means the channel disconnected so we 90 | // can shut down this thread 91 | return; 92 | } 93 | } 94 | Err(TryRecvError::Disconnected) => { 95 | // the channel disconnected so we can shut down this thread 96 | return; 97 | } 98 | } 99 | } 100 | }); 101 | 102 | let event_source = StdinReader::new(move |key_event| match key_event { 103 | KeyEvent { 104 | kind: KeyEventKind::Press, 105 | modifiers: KeyModifiers::CONTROL, 106 | code: KeyCode::Char('r'), 107 | .. 108 | } => Some(Event::::Restart), 109 | e => keybind_default(e), 110 | }); 111 | 112 | match picker.pick_with_io(event_source, &mut stderr)? { 113 | Some(num) => { 114 | println!("Your favourite number is: {num}"); 115 | Ok(()) 116 | } 117 | None => { 118 | println!("You didn't like any of the numbers!"); 119 | exit(1); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /examples/serde.rs: -------------------------------------------------------------------------------- 1 | //! # Serde support and multiline rendering 2 | //! 3 | //! This example demonstrates how to use serde support when rendering from an input sequence. The 4 | //! example also incorporates multi-line items to demonstrate large item rendering. 5 | //! 6 | //! This example requires the `serde` feature: run with 7 | //! ```bash 8 | //! cargo run --release --example serde --features serde 9 | //! ``` 10 | //! To try out the 'reversed' rendering, add the '--reversed' command line option: 11 | //! ```bash 12 | //! cargo run --release --example serde --features serde -- --reversed 13 | //! ``` 14 | use std::{env::args, io::Result, thread::spawn}; 15 | 16 | use nucleo_picker::{PickerOptions, Render}; 17 | use serde::{de::DeserializeSeed, Deserialize}; 18 | use serde_json::Deserializer; 19 | 20 | /// The picker item, which also implements [`Deserialize`]. 21 | #[derive(Deserialize)] 22 | struct Poem { 23 | author: String, 24 | title: String, 25 | lines: Vec, 26 | } 27 | 28 | struct PoemRenderer; 29 | 30 | impl Render for PoemRenderer { 31 | type Str<'a> = String; 32 | 33 | /// Render the text of the poem by joining the lines. 34 | fn render<'a>(&self, poem: &'a Poem) -> Self::Str<'a> { 35 | poem.lines.join("\n") 36 | } 37 | } 38 | 39 | fn main() -> Result<()> { 40 | // "argument parsing" 41 | let opts = PickerOptions::new(); 42 | let picker_opts = match args().nth(1) { 43 | Some(s) if s == "--reversed" => opts.reversed(true), 44 | _ => opts, 45 | }; 46 | 47 | let mut picker = picker_opts.picker(PoemRenderer); 48 | let injector = picker.injector(); 49 | 50 | spawn(move || { 51 | // just for the example; usually you would read this from a file at runtime or similar and 52 | // instead use `serde_json::from_reader`. 53 | let poems_json = include_str!("poems.json"); 54 | 55 | // use the deserialize implementation of a `Poem` to deserialize from the contents of 56 | // `poems.json`. the `DeserializeSeed` implementation of `&Injector` expects that the input 57 | // is a sequence of values which can be deserialized into the picker item, which in this 58 | // case is a `Poem`. 59 | injector 60 | .deserialize(&mut Deserializer::from_str(poems_json)) 61 | .unwrap(); 62 | }); 63 | 64 | // open interactive prompt 65 | match picker.pick()? { 66 | Some(poem) => println!("'{}' by {}", poem.title, poem.author), 67 | None => println!("Nothing selected!"), 68 | } 69 | 70 | Ok(()) 71 | } 72 | -------------------------------------------------------------------------------- /src/component.rs: -------------------------------------------------------------------------------- 1 | use std::ops::BitOrAssign; 2 | 3 | pub trait Status: BitOrAssign + Default { 4 | fn needs_redraw(&self) -> bool; 5 | } 6 | 7 | impl Status for bool { 8 | fn needs_redraw(&self) -> bool { 9 | *self 10 | } 11 | } 12 | 13 | pub trait Component { 14 | /// The event that this component can handle. 15 | type Event; 16 | 17 | /// The status of the component after handling an event, such as whether or not the component 18 | /// needs to be redrawn. Supports updating. 19 | type Status: Status; 20 | 21 | /// Update the component state in response to the given event, returning whether or not the 22 | /// component changed. 23 | fn handle(&mut self, event: Self::Event) -> Self::Status; 24 | 25 | /// Redraw the component in the screen. The cursor will be placed in the top-left corner of the 26 | /// provided region during redraw. 27 | fn draw( 28 | &mut self, 29 | width: u16, 30 | height: u16, 31 | writer: &mut W, 32 | ) -> std::io::Result<()>; 33 | } 34 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! # Errors during interactive picker usage 2 | //! This module contains the custom error type [`PickError`] returned by the 3 | //! [`Picker::pick`](crate::Picker::pick) method, and siblings `Picker::pick_*`. The error type is 4 | //! comprehensive and the individual picker method used may or may not result in particular error 5 | //! variants. 6 | //! 7 | //! See the [`PickError`] documentation for more detail. 8 | //! 9 | //! ## Example 10 | //! Convert a [`PickError::UserInterrupted`] silently into no choice, propagating any other error as an IO 11 | //! error. Use with `picker.pick().or_else(suppress_abort)`. 12 | //! ``` 13 | //! # use nucleo_picker::error::PickError; 14 | //! # use std::io; 15 | //! fn suppress_abort(err: PickError) -> Result { 16 | //! match err { 17 | //! PickError::UserInterrupted => Ok(D::default()), 18 | //! e => Err(e.into()), 19 | //! } 20 | //! } 21 | //! ``` 22 | 23 | use std::{convert::Infallible, error::Error as StdError, fmt, io}; 24 | 25 | /// An error which may occur while running the picker interactively. 26 | /// 27 | /// This is marked non-exhaustive since more variants may be added in the future. It is recommended 28 | /// to handle the errors that are relevant to your application and propagate any remaining errors 29 | /// as an [`io::Error`]. 30 | /// 31 | /// ## Type parameter for `Aborted` variant 32 | /// The [`PickError::Aborted`] variant can be used by the application to propagate errors to the 33 | /// picker; the application-defined error type is the type parameter `A`. By default, `A = !` 34 | /// which means this type of error will *never occur* and can be ignored during pattern matching. 35 | /// 36 | /// This library will never generate an abort error directly. In order to pass errors downstream to 37 | /// the picker, the application can define an abort error type using the 38 | /// [`EventSource::AbortErr`](crate::EventSource::AbortErr) associated type. This associated type 39 | /// is the same as the type parameter here when used in 40 | /// [`Picker::pick_with_io`](crate::Picker::pick_with_io). 41 | /// 42 | /// ## Relationship to `io::Error` 43 | /// This error type with the default type parameter is (in spirit) an [`io::Error`], but with 44 | /// more precise variants not present in the default [`io::Error`]. For convenience and 45 | /// (partial) backwards compatibility, there is a `From for io::Error` implementation; 46 | /// this propagates the underlying IO error and converts any other error message to an 47 | /// [`io::Error`] using [`io::Error::other`]. 48 | /// 49 | /// There is also a `From> for io::Error` to handle the common use-case that 50 | /// the only error type which may occur during standard operation of your application is an IO 51 | /// error; in this case, the conversion maps both the `Aborted(io::Error)` and `IO(io::Error)` 52 | /// versions directly to an `io::Error`. 53 | /// 54 | /// Any other abort error type `A` requires manual handling. The [`PickError::factor`] method 55 | /// can be used to unwind non-aborted variants into an `io::Error` and extract the 56 | /// error present in the `Aborted` variant. 57 | #[derive(Debug)] 58 | #[non_exhaustive] 59 | pub enum PickError { 60 | /// A read or write resulted in an IO error. 61 | IO(io::Error), 62 | /// A necessary channel disconnected while the picker was still running. 63 | Disconnected, 64 | /// The picker quit at the user's request. 65 | UserInterrupted, 66 | /// The picker could not be started since the writer is not interactive. 67 | NotInteractive, 68 | /// The picker was aborted because of an upstream error. 69 | Aborted(A), 70 | } 71 | 72 | impl PickError { 73 | /// Convert a `PickError` into either an `Ok(A)` or `Err(PickError)`, for 74 | /// convenience of error propogation. 75 | /// 76 | /// # Example 77 | /// Use `factor` to simplify processing of custom [`PickError`]s when you mainly care about 78 | /// your application error. 79 | /// ``` 80 | /// # use nucleo_picker::error::PickError; 81 | /// use std::{fmt::Display, io}; 82 | /// 83 | /// // Even though `PickError` need not satisfy `Into`, `PickError` 84 | /// // always does. 85 | /// fn print_or_propogate(pick_err: PickError) -> Result<(), io::Error> { 86 | /// let app_err = pick_err.factor()?; 87 | /// eprintln!("{app_err}"); 88 | /// Ok(()) 89 | /// } 90 | /// ``` 91 | pub fn factor(self) -> Result { 92 | match self { 93 | PickError::IO(error) => Err(PickError::IO(error)), 94 | PickError::Disconnected => Err(PickError::Disconnected), 95 | PickError::UserInterrupted => Err(PickError::UserInterrupted), 96 | PickError::NotInteractive => Err(PickError::NotInteractive), 97 | PickError::Aborted(a) => Ok(a), 98 | } 99 | } 100 | } 101 | 102 | impl fmt::Display for PickError { 103 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 104 | match self { 105 | PickError::IO(error) => error.fmt(f), 106 | PickError::Disconnected => { 107 | f.write_str("event source disconnected while picker was still active") 108 | } 109 | PickError::Aborted(err) => write!(f, "received abort: {err}"), 110 | PickError::UserInterrupted => f.write_str("keyboard interrupt"), 111 | PickError::NotInteractive => { 112 | f.write_str("picker could not start since the screen is not interactive") 113 | } 114 | } 115 | } 116 | } 117 | 118 | impl StdError for PickError {} 119 | 120 | impl From for PickError { 121 | fn from(err: io::Error) -> Self { 122 | Self::IO(err) 123 | } 124 | } 125 | 126 | // ideally we would like to replace these two conversions with a blanket implementation 127 | // `impl> From> for io::Error`; however, currently there is no 128 | // implementation of `From for T` for a variety of reasons; so we are stuck with doing this for 129 | // maximal compabitility since in the vast majority of cases, `A = !`. 130 | impl From for io::Error { 131 | fn from(err: PickError) -> Self { 132 | match err { 133 | PickError::IO(io_err) => io_err, 134 | _ => io::Error::other(err), 135 | } 136 | } 137 | } 138 | 139 | impl From> for io::Error { 140 | fn from(err: PickError) -> Self { 141 | match err.factor() { 142 | Ok(io_err) => io_err, 143 | Err(pick_err) => pick_err.into(), 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | //! # Extended event handling 2 | //! 3 | //! This module defines the core [`Event`] type handled by a [`Picker`](crate::Picker), which 4 | //! defines an interactive update to the picker state. 5 | //! 6 | //! By default, the interactive picker launched by [`Picker::pick`](crate::Picker::pick) watches 7 | //! for terminal events (such as key presses) and maps them to [`Event`]s. The process of reading 8 | //! events is encapsulated in the [`EventSource`] trait, which you can implement yourself and pass 9 | //! directly to the picker using the [`Picker::pick_with_io`](crate::Picker::pick_with_io). 10 | //! 11 | //! Jump to: 12 | //! - The [`EventSource`] trait. 13 | //! - The [`StdinReader`], for automatically reading events from standard input, with customizable 14 | //! keybindings. 15 | //! - The [`StdinEventSender`] to read events from standard input and send them through a 16 | //! [mpsc channel](std::sync::mpsc::channel). 17 | //! - The [default keybindings](keybind_default), which are also useful to provide fallbacks for 18 | //! keybind customization 19 | //! 20 | //! For somewhat comprehensive examples, see the [extended fzf 21 | //! example](https://github.com/autobib/nucleo-picker/blob/master/examples/fzf_err_handling.rs) or 22 | //! the [restart 23 | //! example](https://github.com/autobib/nucleo-picker/blob/master/examples/restart.rs). 24 | 25 | mod bind; 26 | 27 | use std::{ 28 | convert::Infallible, 29 | io, 30 | marker::PhantomData, 31 | sync::mpsc::{Receiver, RecvTimeoutError, Sender}, 32 | time::Duration, 33 | }; 34 | 35 | use crossterm::event::{poll, read, KeyEvent}; 36 | 37 | use self::bind::convert_crossterm_event; 38 | 39 | pub use self::bind::keybind_default; 40 | pub use crate::{match_list::MatchListEvent, observer::Observer, prompt::PromptEvent}; 41 | 42 | /// An event which controls the picker behaviour. 43 | /// 44 | /// The type parameter `A` is the application-defined error which can be used to propagate 45 | /// application errors to the main thread where the picker is running. 46 | /// 47 | /// Most events are explained directly in the enum variant documentation. A few special cases 48 | /// require a bit more detail: [redraw](#redraw), 49 | /// [application-defined abort](#application-defined-abort), and [restart](#restart) 50 | /// 51 | /// ## Redraw 52 | /// In most cases, it is not necessary to manually send an [`Event::Redraw`] since the default 53 | /// behaviour of the picker is to automatically redraw on each frame if the state of the screen 54 | /// would change when handling an event, or when the item list is updated internally. 55 | /// 56 | /// There is no `Resize` variant since the screen size is automatically checked immediately before 57 | /// drawing to the screen. If you are generating your own events, propagate a screen resize as a 58 | /// [`Event::Redraw`], which will force a redraw to respect the new screen size. 59 | /// 60 | /// ## Application-defined abort 61 | /// The abort event is a special event used to propagate errors from the application to the picker. 62 | /// When the picker receives an abort event, it immediately terminates and passes the abort event 63 | /// onwards inside the [`PickError::Aborted`](crate::error::PickError::Aborted) error variant. 64 | /// 65 | /// By default, the associated type parameter is `!`, which means that [`Event::Abort`] cannot be 66 | /// constructed in ordinary circumstances. In order to generate [`Event::Abort`], you must use the 67 | /// [`Picker::pick_with_io`](crate::Picker::pick_with_io) method and pass an appropriate 68 | /// [`EventSource`] which generates your desired errors. 69 | /// 70 | /// The provided [`EventSource`] implementations, namely [`StdinReader`] and 71 | /// [`mpsc::Receiver`](std::sync::mpsc::Receiver), are both generic over the same type parameter 72 | /// `A` so you can construct this variant with a custom error type if desired. 73 | /// 74 | /// ## Restart 75 | /// The [`Event::Restart`] is used to restart the picker while it is still running. After a 76 | /// restart, all previously created [`Injector`][i]s become invalidated and the match list is 77 | /// cleared on the next frame. Therefore to receive a valid [`Injector`][i], the caller must 78 | /// watch for new injectors using the [`Observer`] returned by 79 | /// [`Picker::injector_observer`](crate::Picker::injector_observer`). 80 | /// 81 | /// When the [`Event::Restart`] is processed by the picker, it will clear the item list and 82 | /// immediately update the observer with the new [`Injector`][i]. If the send fails because 83 | /// there is no receiver, the picker will fail with 84 | /// [`PickError::Disconnected`](crate::error::PickError::Disconnected).The picker will overwrite any 85 | /// previously pushed [`Injector`][i] when pushing the updated one to the channel. In particular, 86 | /// the [`Injector`][i] in the channel (if any) is always the most up-to-date. 87 | /// 88 | /// It is possible that no [`Injector`][i] will be sent if the picker exits or disconnects 89 | /// before the event is processed. 90 | /// 91 | /// For a detailed implementation example, see the [restart 92 | /// example](https://github.com/autobib/nucleo-picker/blob/master/examples/restart.rs). 93 | /// 94 | /// [i]: crate::Injector 95 | #[non_exhaustive] 96 | pub enum Event { 97 | /// Modify the prompt. 98 | Prompt(PromptEvent), 99 | /// Modify the list of matches. 100 | MatchList(MatchListEvent), 101 | /// Quit the picker (no selection). 102 | Quit, 103 | /// Quit the picker (no selection) if the prompt is empty. 104 | QuitPromptEmpty, 105 | /// Abort the picker (error) at user request. 106 | UserInterrupt, 107 | /// Abort the picker (error) for another reason. 108 | Abort(A), 109 | /// Redraw the screen. 110 | Redraw, 111 | /// Quit the picker and select the given item. 112 | Select, 113 | /// Restart the picker, invalidating all existing injectors. 114 | Restart, 115 | } 116 | 117 | /// The result of waiting for an update from an [`EventSource`] with a timeout. 118 | /// 119 | /// This is quite similar to the standard library 120 | /// [`mpsc::RecvTimeoutError`](std::sync::mpsc::RecvTimeoutError), but also permitting an 121 | /// [`io::Error`] which may result from reading from standard input. 122 | #[non_exhaustive] 123 | pub enum RecvError { 124 | /// No event was received because we timed out. 125 | Timeout, 126 | /// The source is disconnected and there are no more messages. 127 | Disconnected, 128 | /// An IO error occurred while trying to read an event. 129 | IO(io::Error), 130 | } 131 | 132 | impl From for RecvError { 133 | fn from(err: io::Error) -> Self { 134 | Self::IO(err) 135 | } 136 | } 137 | 138 | impl From for RecvError { 139 | fn from(value: RecvTimeoutError) -> Self { 140 | match value { 141 | RecvTimeoutError::Timeout => Self::Timeout, 142 | RecvTimeoutError::Disconnected => Self::Disconnected, 143 | } 144 | } 145 | } 146 | 147 | /// An abstraction over sources of [`Event`]s which drive a [`Picker`](crate::Picker). 148 | /// 149 | /// Usually, you do not need to implement this trait yourself and can instead use one of the 150 | /// provided implementations: 151 | /// 152 | /// - An implementation for [`StdinReader`], which reads key events interactively from standard 153 | /// input and supports custom key bindings. 154 | /// - An implementation for the [`Receiver`] end of a [`sync::mpsc`](std::sync::mpsc) channel. 155 | /// 156 | /// The [`Receiver`] implementation means, in most cases, you can simply run an event driver in a 157 | /// separate thread and pass the receiver to the [`Picker`](crate::Picker). This might also be 158 | /// useful when co-existing with other parts of the application which might themselves generate 159 | /// events which are relevant for a picker. Also see the [`StdinEventSender`] struct. 160 | /// 161 | /// ## Debouncing 162 | /// The picker automatically debounces incoming events, so you do not need to handle this yourself. 163 | /// However, since there are limitations to the commutativity of events, if the event stream is 164 | /// very overactive, the picker may still lag. 165 | /// 166 | /// ## Associated `AbortErr` type 167 | /// The associated `AbortErr` type defines the application-specific error type which may be 168 | /// propagated directly to the picker. This is the same type as present in 169 | /// [`PickError::Aborted`](crate::error::PickError) as well as [`Event::Abort`]. 170 | /// 171 | /// If you do not need to construct this variant at all, you should set `AbortErr = !` so that 172 | /// you do not need to match on the corresponding [`PickError`](crate::error::PickError) variant. 173 | /// 174 | /// The provided implementations for [`StdinReader`] and [`Receiver`] are both generic over a type 175 | /// parameter `A` which defaults to `A = !`. This type parameter is used as `AbortErr` in the 176 | /// provided [`EventSource`] implementation. 177 | /// 178 | /// ## Implementation example 179 | /// Here is an example implementation for a `crossbeam::channel::Receiver`. This is identical to 180 | /// the implementation for [`mpsc::Receiver`](std::sync::mpsc::Receiver). 181 | /// ``` 182 | /// use std::time::Duration; 183 | /// 184 | /// use crossbeam::channel::{Receiver, RecvTimeoutError}; 185 | /// use nucleo_picker::event::{Event, EventSource, RecvError}; 186 | /// 187 | /// struct EventReceiver { 188 | /// inner: Receiver> 189 | /// } 190 | /// 191 | /// impl EventSource for EventReceiver { 192 | /// type AbortErr = A; 193 | /// 194 | /// fn recv_timeout(&mut self, duration: Duration) -> Result, RecvError> { 195 | /// self.inner.recv_timeout(duration).map_err(|err| match err { 196 | /// RecvTimeoutError::Timeout => RecvError::Timeout, 197 | /// RecvTimeoutError::Disconnected => RecvError::Disconnected, 198 | /// }) 199 | /// } 200 | /// } 201 | /// ``` 202 | /// 203 | /// ## Usage example 204 | /// This is a partial usage example illustrating how to use a [`Receiver`] 205 | /// 206 | /// In order to complete this example, one should also call 207 | /// [`Picker::pick_with_io`](crate::Picker::pick_with_io) using the 208 | /// receiver end of the channel. 209 | /// 210 | /// For the full version of this example with these additional components, visit the [example on 211 | /// GitHub](https://github.com/autobib/nucleo-picker/blob/master/examples/fzf_err_handling.rs) 212 | /// ``` 213 | /// use std::{ 214 | /// io::{self, BufRead}, 215 | /// sync::mpsc::channel, 216 | /// thread::spawn, 217 | /// }; 218 | /// 219 | /// use nucleo_picker::{ 220 | /// event::{Event, StdinEventSender}, 221 | /// render::StrRenderer, 222 | /// Picker, 223 | /// }; 224 | /// 225 | /// 226 | /// // initialize a mpsc channel; we use see the 'sender' end to communicate with the picker 227 | /// let (sender, receiver) = channel(); 228 | /// 229 | /// let mut picker = Picker::new(StrRenderer); 230 | /// 231 | /// // spawn a stdin watcher to read keyboard events and send them to the channel 232 | /// let stdin_watcher = StdinEventSender::with_default_keybindings(sender.clone()); 233 | /// spawn(move || match stdin_watcher.watch() { 234 | /// Ok(()) => { 235 | /// // this path occurs when the picker quits and the receiver is dropped so there 236 | /// // is no more work to be done 237 | /// } 238 | /// Err(io_err) => { 239 | /// // we received an IO error while trying to read keyboard events, so we recover the 240 | /// // inner channel and send an `Abort` event to tell the picker to quit immediately 241 | /// // 242 | /// // if we do not send the `Abort` event, or any other event which causes the picker to 243 | /// // quit (such as a `Quit` event), the picker will hang until the thread reading from 244 | /// // standard input completes, which could be a very long time 245 | /// let inner = stdin_watcher.into_sender(); 246 | /// // if this fails, the picker already quit 247 | /// let _ = inner.send(Event::Abort(io_err)); 248 | /// return; 249 | /// } 250 | /// }); 251 | /// 252 | /// // read input from standard input 253 | /// let injector = picker.injector(); 254 | /// spawn(move || { 255 | /// // in practice, one should also check that `stdin` is not interactive using `IsTerminal`. 256 | /// let stdin = io::stdin(); 257 | /// for line in stdin.lines() { 258 | /// match line { 259 | /// Ok(s) => injector.push(s), 260 | /// Err(io_err) => { 261 | /// // if we encounter an IO error, we send the corresponding error 262 | /// // to the picker so that it can abort and propagate the error 263 | /// // 264 | /// // here, it is also safe to simply ignore the IO error since the picker will 265 | /// // remain interactive with the items it has already received. 266 | /// let _ = sender.send(Event::Abort(io_err)); 267 | /// return; 268 | /// } 269 | /// } 270 | /// } 271 | /// }); 272 | /// ``` 273 | pub trait EventSource { 274 | /// The application-defined abort error propagated to the picker. 275 | type AbortErr; 276 | 277 | /// Receive a new event, timing out after the provided duration. 278 | /// 279 | /// If the receiver times out, the implementation should return a [`RecvError::Timeout`]. 280 | /// If the receiver cannot receive any more events, the implementation should return a 281 | /// [`RecvError::Disconnected`]. Otherwise, return one of the other variants. 282 | fn recv_timeout(&mut self, duration: Duration) -> Result, RecvError>; 283 | } 284 | 285 | impl EventSource for Receiver> { 286 | type AbortErr = A; 287 | 288 | fn recv_timeout(&mut self, duration: Duration) -> Result, RecvError> { 289 | Receiver::recv_timeout(self, duration).map_err(From::from) 290 | } 291 | } 292 | 293 | /// An [`EventSource`] implementation which reads events from [`io::Stdin`] and maps key 294 | /// events to events using a keybind closure. 295 | /// 296 | /// The default implementation uses the [`keybind_default`] function for keybindings. 297 | /// 298 | /// ## Customizing keybindings 299 | /// 300 | /// The default keybindings are documented 301 | /// [here](https://github.com/autobib/nucleo-picker/blob/master/USAGE.md#keyboard-shortcuts). When 302 | /// modifying keybindings, if you are targeting Windows as a platform, you probably want to check 303 | /// for [`KeyEventKind::Press`](crossterm::event::KeyEventKind::Press) or you may get duplicated 304 | /// events. 305 | /// 306 | /// ## Example 307 | /// 308 | /// Use the [`keybind_default`] function to simplify your implementation of keybindings: 309 | /// ``` 310 | /// use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; 311 | /// use nucleo_picker::event::{keybind_default, Event, StdinReader}; 312 | /// 313 | /// /// Keybindings which use the default keybindings, but instead of interrupting on `ctrl + c`, 314 | /// /// instead performs a normal quit action. Generic over all possible `Event` type parameters 315 | /// /// for flexibility. 316 | /// fn keybind_no_interrupt(key_event: KeyEvent) -> Option> { 317 | /// match key_event { 318 | /// KeyEvent { 319 | /// kind: KeyEventKind::Press, 320 | /// modifiers: KeyModifiers::CONTROL, 321 | /// code: KeyCode::Char('c'), 322 | /// .. 323 | /// } => Some(Event::Quit), 324 | /// e => keybind_default(e), 325 | /// } 326 | /// } 327 | /// ``` 328 | pub struct StdinReader Option>> { 329 | keybind: F, 330 | _abort: PhantomData, 331 | } 332 | 333 | impl Default for StdinReader { 334 | fn default() -> Self { 335 | Self::new(keybind_default) 336 | } 337 | } 338 | 339 | impl Option>> StdinReader { 340 | /// Create a new [`StdinReader`] with keybindings provided by the given closure. 341 | pub fn new(keybind: F) -> Self { 342 | Self { 343 | keybind, 344 | _abort: PhantomData, 345 | } 346 | } 347 | } 348 | 349 | impl Option>> EventSource for StdinReader { 350 | type AbortErr = A; 351 | 352 | fn recv_timeout(&mut self, duration: Duration) -> Result, RecvError> { 353 | if poll(duration)? { 354 | if let Some(event) = convert_crossterm_event(read()?, &mut self.keybind) { 355 | return Ok(event); 356 | } 357 | }; 358 | Err(RecvError::Timeout) 359 | } 360 | } 361 | 362 | /// A wrapper for a [`Sender`] which reads events from standard input and sends them to the 363 | /// channel. 364 | /// 365 | /// The internal implementation is identical to the [`StdinReader`] struct, but instead of 366 | /// generating the events directly, sends them to the channel. 367 | pub struct StdinEventSender Option>> { 368 | sender: Sender>, 369 | keybind: F, 370 | } 371 | 372 | impl StdinEventSender { 373 | /// Initialize a new [`StdinEventSender`] with default keybindings in the provided channel. 374 | pub fn with_default_keybindings(sender: Sender>) -> Self { 375 | Self { 376 | sender, 377 | keybind: keybind_default, 378 | } 379 | } 380 | } 381 | 382 | impl Option>> StdinEventSender { 383 | /// Watch for events until either the receiver is dropped (in which case `Ok(())` is returned), 384 | /// or there is an IO error while reading from standard input. This method will block the 385 | /// current thread until the channel disconnects or a read fails. 386 | /// 387 | /// This method is only compatible with keybindings which do not mutate internal state. For a 388 | /// version which permits mutation, see [`watch_mut`](Self::watch_mut). 389 | pub fn watch(&self) -> io::Result<()> { 390 | loop { 391 | if let Some(event) = convert_crossterm_event(read()?, &self.keybind) { 392 | if self.sender.send(event).is_err() { 393 | return Ok(()); 394 | } 395 | } 396 | } 397 | } 398 | } 399 | 400 | impl Option>> StdinEventSender { 401 | /// Initialize a new [`StdinEventSender`] with the given keybindings in the provided channel. 402 | pub fn new(sender: Sender>, keybind: F) -> Self { 403 | Self { sender, keybind } 404 | } 405 | 406 | /// Convert into the inner [`Sender`] to send further events when finished. 407 | pub fn into_sender(self) -> Sender> { 408 | self.sender 409 | } 410 | 411 | /// Watch for events until either the receiver is dropped (in which case `Ok(())` is returned), 412 | /// or there is an IO error while reading from standard input. This method will block the 413 | /// current thread until the channel disconnects or a read fails. 414 | /// 415 | /// If the mutable self reference is inconvenient and your keybindings do not mutate internal 416 | /// state, use [`watch`](Self::watch). 417 | pub fn watch_mut(&mut self) -> io::Result<()> { 418 | loop { 419 | if let Some(event) = convert_crossterm_event(read()?, &mut self.keybind) { 420 | if self.sender.send(event).is_err() { 421 | return Ok(()); 422 | } 423 | } 424 | } 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /src/event/bind.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; 2 | 3 | use super::{Event, MatchListEvent, PromptEvent}; 4 | 5 | /// The default keybindings. 6 | /// 7 | /// These are the keybindings used in the [`Default`] implementation for 8 | /// [`StdinReader`](super::StdinReader). 9 | /// 10 | /// # Generic parameter 11 | /// This function is generic over the type parameter `A`, which is the associated type 12 | /// [`AbortErr`](super::EventSource::AbortErr) of an [`EventSource`](super::EventSource). 13 | /// However, the type parameter does not appear anywhere in the function arguments since an 14 | /// [`Event::Abort`] is never produced by the default keybindings. This type parameter is simply 15 | /// here for flexibility to generate events of a particular type when used in situations where `A` 16 | /// is not the default `!`. 17 | #[inline] 18 | pub fn keybind_default(key_event: KeyEvent) -> Option> { 19 | match key_event { 20 | KeyEvent { 21 | kind: KeyEventKind::Press, 22 | modifiers: KeyModifiers::NONE, 23 | code, 24 | .. 25 | } => match code { 26 | KeyCode::Esc => Some(Event::Quit), 27 | KeyCode::Up => Some(Event::MatchList(MatchListEvent::Up(1))), 28 | KeyCode::Down => Some(Event::MatchList(MatchListEvent::Down(1))), 29 | KeyCode::Left => Some(Event::Prompt(PromptEvent::Left(1))), 30 | KeyCode::Right => Some(Event::Prompt(PromptEvent::Right(1))), 31 | KeyCode::Home => Some(Event::Prompt(PromptEvent::ToStart)), 32 | KeyCode::End => Some(Event::Prompt(PromptEvent::ToEnd)), 33 | KeyCode::Char(ch) => Some(Event::Prompt(PromptEvent::Insert(ch))), 34 | KeyCode::Backspace => Some(Event::Prompt(PromptEvent::Backspace(1))), 35 | KeyCode::Enter => Some(Event::Select), 36 | KeyCode::Delete => Some(Event::Prompt(PromptEvent::Delete(1))), 37 | _ => None, 38 | }, 39 | KeyEvent { 40 | kind: KeyEventKind::Press, 41 | modifiers: KeyModifiers::CONTROL, 42 | code, 43 | .. 44 | } => match code { 45 | KeyCode::Char('c') => Some(Event::UserInterrupt), 46 | KeyCode::Char('d') => Some(Event::QuitPromptEmpty), 47 | KeyCode::Char('0') => Some(Event::MatchList(MatchListEvent::Reset)), 48 | KeyCode::Char('g' | 'q') => Some(Event::Quit), 49 | KeyCode::Char('k' | 'p') => Some(Event::MatchList(MatchListEvent::Up(1))), 50 | KeyCode::Char('j' | 'n') => Some(Event::MatchList(MatchListEvent::Down(1))), 51 | KeyCode::Char('b') => Some(Event::Prompt(PromptEvent::Left(1))), 52 | KeyCode::Char('f') => Some(Event::Prompt(PromptEvent::Right(1))), 53 | KeyCode::Char('a') => Some(Event::Prompt(PromptEvent::ToStart)), 54 | KeyCode::Char('e') => Some(Event::Prompt(PromptEvent::ToEnd)), 55 | KeyCode::Char('h') => Some(Event::Prompt(PromptEvent::Backspace(1))), 56 | KeyCode::Char('w') => Some(Event::Prompt(PromptEvent::BackspaceWord(1))), 57 | KeyCode::Char('u') => Some(Event::Prompt(PromptEvent::ClearBefore)), 58 | KeyCode::Char('o') => Some(Event::Prompt(PromptEvent::ClearAfter)), 59 | _ => None, 60 | }, 61 | KeyEvent { 62 | kind: KeyEventKind::Press, 63 | modifiers: KeyModifiers::ALT, 64 | code, 65 | .. 66 | } => match code { 67 | KeyCode::Char('f') => Some(Event::Prompt(PromptEvent::WordLeft(1))), 68 | KeyCode::Char('b') => Some(Event::Prompt(PromptEvent::WordRight(1))), 69 | _ => None, 70 | }, 71 | KeyEvent { 72 | kind: KeyEventKind::Press, 73 | modifiers: KeyModifiers::SHIFT, 74 | code, 75 | .. 76 | } => match code { 77 | KeyCode::Char(ch) => Some(Event::Prompt(PromptEvent::Insert(ch))), 78 | KeyCode::Backspace => Some(Event::Prompt(PromptEvent::Backspace(1))), 79 | KeyCode::Enter => Some(Event::Select), 80 | _ => None, 81 | }, 82 | _ => None, 83 | } 84 | } 85 | 86 | /// Convert a crossterm event into an [`Event`], mapping key events with the giving key bindings. 87 | pub fn convert_crossterm_event Option>>( 88 | ct_event: CrosstermEvent, 89 | mut keybind: F, 90 | ) -> Option> { 91 | match ct_event { 92 | CrosstermEvent::Key(key_event) => (keybind)(key_event), 93 | CrosstermEvent::Resize(_, _) => Some(Event::Redraw), 94 | CrosstermEvent::Paste(contents) => Some(Event::Prompt(PromptEvent::Paste(contents))), 95 | _ => None, 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/incremental.rs: -------------------------------------------------------------------------------- 1 | //! An incremental buffer extension implementation. 2 | 3 | mod partial; 4 | 5 | pub use partial::{IncrementalIterator, Partial}; 6 | 7 | pub trait OrderedCollection { 8 | /// Append an item to the collection. 9 | fn append(&mut self, item: usize); 10 | 11 | /// Get a mutable reference to the last element in the collection. 12 | /// 13 | /// ## Safety 14 | /// Must be valid if and only if there was a previous call to `append`. 15 | unsafe fn last_appended(&mut self) -> &mut usize; 16 | 17 | /// Get a slice corresponding to the current items. 18 | #[cfg(test)] 19 | fn slice(&self) -> &[usize]; 20 | } 21 | 22 | impl OrderedCollection for &'_ mut Vec { 23 | fn append(&mut self, item: usize) { 24 | self.push(item); 25 | } 26 | 27 | unsafe fn last_appended(&mut self) -> &mut usize { 28 | // SAFETY: `append` was previously called. 29 | unsafe { self.last_mut().unwrap_unchecked() } 30 | } 31 | 32 | #[cfg(test)] 33 | fn slice(&self) -> &[usize] { 34 | self 35 | } 36 | } 37 | 38 | impl OrderedCollection for Vec { 39 | fn append(&mut self, item: usize) { 40 | self.push(item); 41 | } 42 | 43 | unsafe fn last_appended(&mut self) -> &mut usize { 44 | // SAFETY: `append` was previously called. 45 | unsafe { self.last_mut().unwrap_unchecked() } 46 | } 47 | 48 | #[cfg(test)] 49 | fn slice(&self) -> &[usize] { 50 | self 51 | } 52 | } 53 | 54 | pub trait ExtendIncremental { 55 | /// Extend the internal collection, ensuring not to add more than `limit_size` 56 | /// to the buffer in total, and not step the underlying iterator more than `limit_steps` times. 57 | /// 58 | /// Returns the total of the elements added to the buffer. 59 | fn extend_bounded(&mut self, limit_size: u16, limit_steps: usize) -> u16; 60 | 61 | /// Extend the internal collection, ensuring not to add more than `limit_size` to the 62 | /// buffer in total. 63 | /// 64 | /// Returns the total of the elements added to the buffer. 65 | fn extend_unbounded(&mut self, limit_size: u16) -> u16; 66 | } 67 | 68 | /// Incremental collection of an [`Iterator`] of [`usize`] into a vector. 69 | /// 70 | /// See the [`extended_bounded`](Self::extend_bounded) method for more detail. 71 | pub struct Incremental> { 72 | /// The internal vector. 73 | vec: C, 74 | /// The internal iterator. 75 | sizes: IncrementalIterator, 76 | } 77 | 78 | impl> ExtendIncremental for Incremental { 79 | #[inline] 80 | fn extend_bounded(&mut self, limit_size: u16, limit_steps: usize) -> u16 { 81 | self.extend_impl(limit_size, limit_steps) 82 | } 83 | 84 | fn extend_unbounded(&mut self, limit_size: u16) -> u16 { 85 | self.extend_impl(limit_size, ()) 86 | } 87 | } 88 | 89 | impl> Incremental { 90 | /// Initialize an [`IncrementalExtension`] targeting the given vector with the 91 | /// provided iterator. 92 | /// 93 | /// New elements will be appended to the vector. 94 | pub fn new(vec: C, sizes: I) -> Self { 95 | Self { 96 | vec, 97 | sizes: IncrementalIterator::new(sizes), 98 | } 99 | } 100 | 101 | #[cfg(test)] 102 | pub fn view(&self) -> &[usize] { 103 | self.vec.slice() 104 | } 105 | 106 | #[inline] 107 | fn extend_impl(&mut self, limit_size: u16, limit_steps: D) -> u16 { 108 | // SAFETY: extend_impl_inverted returns a value less than `limit_size`. 109 | unsafe { limit_size.unchecked_sub(self.extend_impl_inverted(limit_size, limit_steps)) } 110 | } 111 | 112 | /// The actual implementation of the 'reversed' version, which returns the number of remaining 113 | /// elements to be added. 114 | #[inline] 115 | fn extend_impl_inverted( 116 | &mut self, 117 | mut remaining: u16, 118 | mut limit_steps: D, 119 | ) -> u16 { 120 | while remaining > 0 { 121 | if limit_steps.is_finished() && !self.sizes.is_incomplete() { 122 | return remaining; 123 | } 124 | 125 | match self.sizes.next_partial(remaining) { 126 | Some(Partial { new, size }) => { 127 | unsafe { 128 | // SAFETY: `next_partial` returns a `size` which is at most `limit_size`. 129 | remaining = remaining.unchecked_sub(size); 130 | if new { 131 | // SAFETY: we can only be in this branch if the guard call to 132 | // `self.sizes.next_is_not_new()` returned false, in which 133 | // if `limit_steps.is_finished()`, we would have returned earlier. 134 | limit_steps.decr(); 135 | self.vec.append(size as usize); 136 | } else { 137 | // SAFETY: there must have been a previous call to `self.vec.append` 138 | // since the first item returned by an `IncrementalIterator` is 139 | // guaranteed to be new. 140 | let buf_last = self.vec.last_appended(); 141 | // SAFETY: the underlying iterator yields `usize`, so the size of each 142 | // element in total cannot exceed a `usize`. 143 | *buf_last = buf_last.unchecked_add(size as usize); 144 | } 145 | } 146 | } 147 | None => { 148 | return remaining; 149 | } 150 | } 151 | } 152 | 153 | 0 154 | } 155 | } 156 | 157 | /// An internal trait for a counter which can be decreased until it is finished. 158 | /// 159 | /// The implementation for [`usize`] represents a 'bounded' counter, and the implementation for 160 | /// `()` represents an 'unbounded' counter. 161 | trait Decrement { 162 | /// Whether or not we have finished decrementing this value. 163 | fn is_finished(&self) -> bool; 164 | 165 | /// Decrement the value. 166 | /// 167 | /// # Safety 168 | /// Can only be called if `is_finished` returned false. 169 | unsafe fn decr(&mut self); 170 | } 171 | 172 | impl Decrement for () { 173 | #[inline] 174 | fn is_finished(&self) -> bool { 175 | false 176 | } 177 | 178 | #[inline] 179 | unsafe fn decr(&mut self) {} 180 | } 181 | 182 | impl Decrement for usize { 183 | #[inline] 184 | fn is_finished(&self) -> bool { 185 | *self == 0 186 | } 187 | 188 | #[inline] 189 | unsafe fn decr(&mut self) { 190 | // SAFETY: only called if `is_finished` returned false, in which case `self >= 1`. 191 | unsafe { *self = self.unchecked_sub(1) } 192 | } 193 | } 194 | 195 | #[cfg(test)] 196 | mod tests { 197 | use super::*; 198 | 199 | #[test] 200 | fn test_incremental() { 201 | let mut vec = Vec::new(); 202 | let mut incr = Incremental::new(&mut vec, [1, 6, 2, 3, 5, 3, 5].into_iter()); 203 | 204 | assert_eq!(incr.extend_bounded(5, 2), 5); 205 | assert_eq!(incr.view(), &[1, 4]); 206 | 207 | assert_eq!(incr.extend_bounded(2, 1), 2); 208 | assert_eq!(incr.view(), &[1, 6]); 209 | 210 | assert_eq!(incr.extend_bounded(1, 1), 1); 211 | assert_eq!(incr.view(), &[1, 6, 1]); 212 | 213 | assert_eq!(incr.extend_bounded(0, 1), 0); 214 | assert_eq!(incr.view(), &[1, 6, 1]); 215 | 216 | assert_eq!(incr.extend_bounded(10, 1), 4); 217 | assert_eq!(incr.view(), &[1, 6, 2, 3]); 218 | 219 | assert_eq!(incr.extend_bounded(2, 3), 2); 220 | assert_eq!(incr.view(), &[1, 6, 2, 3, 2]); 221 | 222 | assert_eq!(incr.extend_bounded(1, 2), 1); 223 | assert_eq!(incr.view(), &[1, 6, 2, 3, 3]); 224 | 225 | assert_eq!(incr.extend_bounded(1, 4), 1); 226 | assert_eq!(incr.view(), &[1, 6, 2, 3, 4]); 227 | 228 | assert_eq!(incr.extend_bounded(0, 0), 0); 229 | assert_eq!(incr.view(), &[1, 6, 2, 3, 4]); 230 | 231 | assert_eq!(incr.extend_bounded(100, 4), 9); 232 | assert_eq!(incr.view(), &[1, 6, 2, 3, 5, 3, 5]); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/incremental/partial.rs: -------------------------------------------------------------------------------- 1 | /// The result stepping an [`Iterator`] with a limit on the size of the element returned. 2 | #[derive(Debug, PartialEq)] 3 | pub struct Partial { 4 | /// The total amount returned. 5 | pub size: u16, 6 | /// Whether the amount returned corresponds to a new element. 7 | pub new: bool, 8 | } 9 | 10 | /// An iterator adaptor which supports a variant of `next` which receives a bound, and only 11 | /// returns the part of the value that fits within the bound. 12 | /// 13 | /// If the value does not fit within the bound, the excess is retained and returned on subsequent 14 | /// calls. 15 | pub struct IncrementalIterator> { 16 | iter: I, 17 | partial: usize, 18 | } 19 | 20 | impl> IncrementalIterator { 21 | /// Returns a new [`IncrementalIterator`], consuming the given iterator. 22 | #[inline] 23 | pub fn new>(iter: J) -> Self { 24 | Self { 25 | iter: iter.into_iter(), 26 | partial: 0, 27 | } 28 | } 29 | 30 | /// Returns whether or not the next call to [`next_partial`](Self::next_partial) will 31 | /// yield a [`Partial`] with `new = false`; that is, the previously returned size is 32 | /// incomplete. 33 | /// 34 | /// If this method returns false, [`next_partial`](Self::next_partial) could either return 35 | /// `None` or a [`Partial`] with `new = false` if called with `limit = 0`. If the internal 36 | /// iterator is not finished and `limit > 0`, the next call will return a [`Partial`] with 37 | /// `new = true`. 38 | #[inline] 39 | pub fn is_incomplete(&self) -> bool { 40 | self.partial > 0 41 | } 42 | 43 | /// Return the next [`Partial`] constrained by the provided limit. 44 | /// 45 | /// # API Guarantees 46 | /// 1. The returned [`Partial`] contains a `size` that is bounded above by `limit`. 47 | /// 2. The first returned value from a newly constructed [`IncrementalIterator`] is 48 | /// either `None`, or a [`Partial`] with `new == true`. 49 | #[inline] 50 | pub fn next_partial(&mut self, limit: u16) -> Option { 51 | if self.partial > 0 { 52 | Some(Partial { 53 | new: false, 54 | size: if self.partial > limit.into() { 55 | // SAFETY: partial > limit 56 | self.partial = unsafe { self.partial.unchecked_sub(limit as usize) }; 57 | // SAFETY: Guarantee 2: returns limit 58 | limit 59 | } else { 60 | let ret = self.partial as u16; 61 | self.partial = 0; 62 | // SAFETY: Guarantee 2: self.partial <= limit from branch 63 | ret 64 | }, 65 | }) 66 | } else { 67 | // SAFETY: Guarantee 1: a newly initialized IncrementalIterator has `partial == 0`, so 68 | // the first iteration must reach this branch. 69 | match self.iter.next() { 70 | Some(new) => Some(Partial { 71 | new: true, 72 | size: if new > limit.into() { 73 | // SAFETY: new > limit 74 | self.partial = unsafe { new.unchecked_sub(limit as usize) }; 75 | // SAFETY: Guarantee 2: returns limit 76 | limit 77 | } else { 78 | // SAFETY: Guarantee 2: new <= limit from branch 79 | new as u16 80 | }, 81 | }), 82 | None => None, 83 | } 84 | } 85 | } 86 | } 87 | 88 | #[cfg(test)] 89 | mod tests { 90 | use super::*; 91 | 92 | struct PartialTester> { 93 | partial: IncrementalIterator, 94 | } 95 | 96 | impl> PartialTester { 97 | fn assert(&mut self, limit: u16, size: u16, new: bool) { 98 | assert_eq!( 99 | self.partial.next_partial(limit), 100 | Some(Partial { size, new }) 101 | ); 102 | } 103 | } 104 | 105 | #[test] 106 | fn test_partial_iterator() { 107 | let mut ap = PartialTester { 108 | partial: IncrementalIterator::new([1, 7, 3, 2, 5]), 109 | }; 110 | 111 | ap.assert(2, 1, true); 112 | ap.assert(5, 5, true); 113 | assert!(ap.partial.is_incomplete()); 114 | ap.assert(1, 1, false); 115 | assert!(ap.partial.is_incomplete()); 116 | ap.assert(1, 1, false); 117 | ap.assert(3, 3, true); 118 | ap.assert(1, 1, true); 119 | assert!(ap.partial.is_incomplete()); 120 | ap.assert(8, 1, false); 121 | ap.assert(4, 4, true); 122 | ap.assert(0, 0, false); 123 | ap.assert(1, 1, false); 124 | assert!(ap.partial.next_partial(0).is_none()); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/injector.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use nucleo as nc; 4 | 5 | use super::Render; 6 | 7 | /// A handle which allows adding new items to a [`Picker`](super::Picker). 8 | /// 9 | /// This struct is cheaply clonable and can be sent across threads. By default, add new items to 10 | /// the [`Picker`](super::Picker) using the [`push`](Injector::push) method. For convenience, an 11 | /// injector also implements [`Extend`] if you want to add items from an iterator. 12 | /// 13 | /// ## `DeserializeSeed` implementation 14 | /// If your items are being read from an external source and deserialized within the 15 | /// [`serde`](::serde) framework, you may find it convenient to enable the `serde` optional feature. 16 | /// With this feature enabled, an injector implements 17 | /// [`DeserializeSeed`](::serde::de::DeserializeSeed) and expects a sequence of picker items. 18 | /// The [`DeserializeSeed`](::serde::de::DeserializeSeed) implementation sends the items to the 19 | /// picker immediately, without waiting for the entire file to be deserialized (or even loaded into 20 | /// memory). 21 | /// ``` 22 | /// use nucleo_picker::{render::StrRenderer, Picker, Render}; 23 | /// use serde::{de::DeserializeSeed, Deserialize}; 24 | /// use serde_json::Deserializer; 25 | /// 26 | /// let input = r#" 27 | /// [ 28 | /// "Alvar Aalto", 29 | /// "Frank Lloyd Wright", 30 | /// "Zaha Hadid", 31 | /// "Le Corbusier" 32 | /// ] 33 | /// "#; 34 | /// 35 | /// // the type annotation here also tells `serde_json` to deserialize `input` as a sequence of 36 | /// // `String`. 37 | /// let mut picker: Picker = Picker::new(StrRenderer); 38 | /// let injector = picker.injector(); 39 | /// 40 | /// // in practice, you would read from a file or a socket and use 41 | /// // `Deserializer::from_reader` instead, and run this in a separate thread 42 | /// injector 43 | /// .deserialize(&mut Deserializer::from_str(input)) 44 | /// .unwrap(); 45 | /// ``` 46 | pub struct Injector { 47 | inner: nc::Injector, 48 | render: Arc, 49 | } 50 | 51 | impl Clone for Injector { 52 | fn clone(&self) -> Self { 53 | Self { 54 | inner: self.inner.clone(), 55 | render: self.render.clone(), 56 | } 57 | } 58 | } 59 | 60 | impl> Injector { 61 | pub(crate) fn new(inner: nc::Injector, render: Arc) -> Self { 62 | Self { inner, render } 63 | } 64 | } 65 | 66 | impl> Injector { 67 | /// Add an item to the picker. 68 | pub fn push(&self, item: T) { 69 | self.inner.push(item, |s, columns| { 70 | columns[0] = self.render.render(s).as_ref().into(); 71 | }); 72 | } 73 | 74 | /// Returns a reference to the renderer internal to the picker. 75 | pub fn renderer(&self) -> &R { 76 | &self.render 77 | } 78 | } 79 | 80 | impl> Extend for Injector { 81 | fn extend>(&mut self, iter: I) { 82 | for it in iter { 83 | self.push(it); 84 | } 85 | } 86 | } 87 | 88 | #[cfg(feature = "serde")] 89 | mod serde { 90 | use serde::{ 91 | de::{DeserializeSeed, Deserializer, SeqAccess, Visitor}, 92 | Deserialize, 93 | }; 94 | 95 | use super::Injector; 96 | use crate::Render; 97 | 98 | impl<'de, T, R> Visitor<'de> for &Injector 99 | where 100 | T: Send + Sync + 'static + Deserialize<'de>, 101 | R: Render, 102 | { 103 | type Value = (); 104 | 105 | fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 106 | f.write_str("a sequence of picker items") 107 | } 108 | 109 | fn visit_seq(self, mut seq: S) -> Result<(), S::Error> 110 | where 111 | S: SeqAccess<'de>, 112 | { 113 | while let Some(item) = seq.next_element()? { 114 | self.push(item); 115 | } 116 | 117 | Ok(()) 118 | } 119 | } 120 | 121 | impl<'de, T, R> DeserializeSeed<'de> for &Injector 122 | where 123 | T: Send + Sync + 'static + Deserialize<'de>, 124 | R: Render, 125 | { 126 | type Value = (); 127 | 128 | /// Deserialize from a sequence of picker items. 129 | /// This implementation is enabled using the `serde` feature. 130 | fn deserialize(self, deserializer: D) -> Result 131 | where 132 | D: Deserializer<'de>, 133 | { 134 | deserializer.deserialize_seq(self) 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/lazy.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | component::Component, 3 | event::{MatchListEvent, PromptEvent}, 4 | match_list::MatchList, 5 | prompt::{Prompt, PromptStatus}, 6 | util::as_u32, 7 | Injector, Render, 8 | }; 9 | 10 | pub struct LazyMatchList<'a, T: Send + Sync + 'static, R: Render> { 11 | match_list: &'a mut MatchList, 12 | buffered_selection: u32, 13 | } 14 | 15 | impl<'a, T: Send + Sync + 'static, R: Render> LazyMatchList<'a, T, R> { 16 | pub fn new(match_list: &'a mut MatchList) -> Self { 17 | let buffered_selection = match_list.selection(); 18 | Self { 19 | match_list, 20 | buffered_selection, 21 | } 22 | } 23 | 24 | pub fn restart(&mut self) -> Injector { 25 | self.match_list.restart(); 26 | self.buffered_selection = 0; 27 | self.match_list.injector() 28 | } 29 | 30 | pub fn is_empty(&self) -> bool { 31 | self.match_list.is_empty() 32 | } 33 | 34 | pub fn selection(&self) -> u32 { 35 | self.buffered_selection 36 | } 37 | 38 | fn decr(&mut self, n: usize) { 39 | self.buffered_selection = self.buffered_selection.saturating_sub(as_u32(n)); 40 | } 41 | 42 | fn incr(&mut self, n: usize) { 43 | self.buffered_selection = self 44 | .buffered_selection 45 | .saturating_add(as_u32(n)) 46 | .min(self.match_list.max_selection()); 47 | } 48 | 49 | /// Handle an event. 50 | /// 51 | /// Note that this may not actually apply the event change to the underlying [`MatchList`]; you 52 | /// must call [`finish`](Self::finish) in order to guarantee that all events are fully 53 | /// processed. 54 | pub fn handle(&mut self, event: MatchListEvent) { 55 | match event { 56 | MatchListEvent::Up(n) => { 57 | if self.match_list.reversed() { 58 | self.decr(n); 59 | } else { 60 | self.incr(n); 61 | } 62 | } 63 | MatchListEvent::Down(n) => { 64 | if self.match_list.reversed() { 65 | self.incr(n); 66 | } else { 67 | self.decr(n); 68 | } 69 | } 70 | MatchListEvent::Reset => { 71 | self.buffered_selection = 0; 72 | } 73 | } 74 | } 75 | 76 | /// Complete processing and clear any buffered events. 77 | pub fn finish(self) -> bool { 78 | self.match_list.set_selection(self.buffered_selection) 79 | } 80 | } 81 | 82 | pub struct LazyPrompt<'a> { 83 | prompt: &'a mut Prompt, 84 | buffered_event: Option, 85 | status: PromptStatus, 86 | } 87 | 88 | impl<'a> LazyPrompt<'a> { 89 | pub fn is_empty(&self) -> bool { 90 | self.prompt.is_empty() 91 | } 92 | 93 | pub fn new(prompt: &'a mut Prompt) -> Self { 94 | Self { 95 | prompt, 96 | buffered_event: None, 97 | status: PromptStatus::default(), 98 | } 99 | } 100 | 101 | /// `self.buffered_event` must be Some() 102 | fn swap_and_process_buffer(&mut self, mut event: PromptEvent) { 103 | // put the 'new' event in the buffer, and move the 'buffered' event into new 104 | std::mem::swap( 105 | unsafe { self.buffered_event.as_mut().unwrap_unchecked() }, 106 | &mut event, 107 | ); 108 | // process the buffered event (now swapped) 109 | self.status |= self.prompt.handle(event); 110 | } 111 | 112 | pub fn finish(mut self) -> PromptStatus { 113 | if let Some(event) = self.buffered_event { 114 | self.status |= self.prompt.handle(event); 115 | } 116 | 117 | self.status 118 | } 119 | 120 | pub fn handle(&mut self, mut event: PromptEvent) { 121 | match self.buffered_event { 122 | None => { 123 | self.buffered_event = Some(event); 124 | } 125 | Some(ref mut buffered) => match event { 126 | PromptEvent::Left(ref mut n1) => { 127 | if let PromptEvent::Left(n2) = buffered { 128 | *n1 += *n2; 129 | } else { 130 | self.swap_and_process_buffer(event); 131 | } 132 | } 133 | PromptEvent::WordLeft(ref mut n1) => { 134 | if let PromptEvent::WordLeft(n2) = buffered { 135 | *n1 += *n2; 136 | } else { 137 | self.swap_and_process_buffer(event); 138 | } 139 | } 140 | PromptEvent::Right(ref mut n1) => { 141 | if let PromptEvent::Right(n2) = buffered { 142 | *n1 += *n2; 143 | } else { 144 | self.swap_and_process_buffer(event); 145 | } 146 | } 147 | PromptEvent::WordRight(ref mut n1) => { 148 | if let PromptEvent::WordRight(n2) = buffered { 149 | *n1 += *n2; 150 | } else { 151 | self.swap_and_process_buffer(event); 152 | } 153 | } 154 | PromptEvent::ToStart => { 155 | if buffered.is_cursor_movement() { 156 | *buffered = PromptEvent::ToStart; 157 | } else { 158 | self.swap_and_process_buffer(event); 159 | } 160 | } 161 | PromptEvent::ToEnd => { 162 | if buffered.is_cursor_movement() { 163 | *buffered = PromptEvent::ToEnd; 164 | } else { 165 | self.swap_and_process_buffer(event); 166 | } 167 | } 168 | PromptEvent::Backspace(ref mut n1) => { 169 | if let PromptEvent::Backspace(n2) = buffered { 170 | *n1 += *n2; 171 | } else { 172 | self.swap_and_process_buffer(event); 173 | } 174 | } 175 | PromptEvent::Delete(ref mut n1) => { 176 | if let PromptEvent::Delete(n2) = buffered { 177 | *n1 += *n2; 178 | } else { 179 | self.swap_and_process_buffer(event); 180 | } 181 | } 182 | PromptEvent::BackspaceWord(ref mut n1) => { 183 | if let PromptEvent::BackspaceWord(n2) = buffered { 184 | *n1 += *n2; 185 | } else { 186 | self.swap_and_process_buffer(event); 187 | } 188 | } 189 | PromptEvent::ClearBefore => { 190 | if matches!( 191 | buffered, 192 | PromptEvent::Backspace(_) 193 | | PromptEvent::ClearBefore 194 | | PromptEvent::BackspaceWord(_) 195 | ) { 196 | *buffered = PromptEvent::ClearBefore; 197 | } else { 198 | self.swap_and_process_buffer(event); 199 | } 200 | } 201 | PromptEvent::ClearAfter => { 202 | if matches!(buffered, PromptEvent::Delete(_) | PromptEvent::ClearAfter) { 203 | *buffered = PromptEvent::ClearAfter; 204 | } else { 205 | self.swap_and_process_buffer(event); 206 | } 207 | } 208 | PromptEvent::Insert(ch1) => match buffered { 209 | PromptEvent::Insert(ch2) => { 210 | let mut s = ch1.to_string(); 211 | s.push(*ch2); 212 | *buffered = PromptEvent::Paste(s); 213 | } 214 | PromptEvent::Paste(new) => { 215 | let mut s = ch1.to_string(); 216 | s.push_str(new); 217 | *buffered = PromptEvent::Paste(s); 218 | } 219 | _ => { 220 | self.swap_and_process_buffer(event); 221 | } 222 | }, 223 | PromptEvent::Paste(ref mut s) => match buffered { 224 | PromptEvent::Insert(ch2) => { 225 | s.push(*ch2); 226 | } 227 | PromptEvent::Paste(new) => { 228 | s.push_str(new); 229 | } 230 | _ => { 231 | self.swap_and_process_buffer(event); 232 | } 233 | }, 234 | PromptEvent::Reset(_) => { 235 | // a 'set' event overwrites any other event since it resets the buffer 236 | *buffered = event; 237 | } 238 | }, 239 | }; 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/match_list/draw.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | 3 | use nucleo as nc; 4 | 5 | use super::{ 6 | item::RenderedItem, 7 | span::{Head, KeepLines, Spanned, Tail}, 8 | unicode::{AsciiProcessor, UnicodeProcessor}, 9 | IndexBuffer, MatchList, MatchListConfig, MatchListEvent, 10 | }; 11 | use crate::{ 12 | component::Component, 13 | util::{as_u16, as_u32}, 14 | Render, 15 | }; 16 | 17 | use crossterm::{ 18 | cursor::MoveToNextLine, 19 | style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor}, 20 | terminal::{Clear, ClearType}, 21 | QueueableCommand, 22 | }; 23 | 24 | /// The inner `match draw` implementation. 25 | #[inline] 26 | #[allow(clippy::too_many_arguments)] 27 | fn draw_single_match< 28 | T: Send + Sync + 'static, 29 | R: Render, 30 | L: KeepLines, 31 | W: Write + ?Sized, 32 | const SELECTED: bool, 33 | >( 34 | writer: &mut W, 35 | buffer: &mut IndexBuffer, 36 | max_draw_length: u16, // the width not including the space for the selection marker 37 | config: &MatchListConfig, 38 | item: &nc::Item<'_, T>, 39 | snapshot: &nc::Snapshot, 40 | matcher: &mut nc::Matcher, 41 | height: u16, 42 | render: &R, 43 | ) -> io::Result<()> { 44 | // generate the indices 45 | if config.highlight { 46 | buffer.indices.clear(); 47 | snapshot.pattern().column_pattern(0).indices( 48 | item.matcher_columns[0].slice(..), 49 | matcher, 50 | &mut buffer.indices, 51 | ); 52 | buffer.indices.sort_unstable(); 53 | buffer.indices.dedup(); 54 | } 55 | 56 | match RenderedItem::new(item, render) { 57 | RenderedItem::Ascii(s) => Spanned::<'_, AsciiProcessor>::new( 58 | &buffer.indices, 59 | s, 60 | &mut buffer.spans, 61 | &mut buffer.lines, 62 | L::from_offset(height), 63 | ) 64 | .queue_print(writer, SELECTED, max_draw_length, config.highlight_padding), 65 | RenderedItem::Unicode(r) => Spanned::<'_, UnicodeProcessor>::new( 66 | &buffer.indices, 67 | r.as_ref(), 68 | &mut buffer.spans, 69 | &mut buffer.lines, 70 | L::from_offset(height), 71 | ) 72 | .queue_print(writer, SELECTED, max_draw_length, config.highlight_padding), 73 | } 74 | } 75 | 76 | #[allow(clippy::too_many_arguments)] 77 | fn draw_matches<'a, T: Send + Sync + 'static, R: Render, W: io::Write + ?Sized>( 78 | writer: &mut W, 79 | buffer: &mut IndexBuffer, 80 | config: &MatchListConfig, 81 | snapshot: &nc::Snapshot, 82 | matcher: &mut nc::Matcher, 83 | render: &R, 84 | match_list_width: u16, 85 | above: &[usize], 86 | below: &[usize], 87 | mut item_iter: impl Iterator>, 88 | ) -> io::Result<()> { 89 | // render above the selection 90 | for (item_height, item) in above.iter().rev().zip(item_iter.by_ref()) { 91 | draw_single_match::<_, _, Tail, _, false>( 92 | writer, 93 | buffer, 94 | match_list_width, 95 | config, 96 | &item, 97 | snapshot, 98 | matcher, 99 | as_u16(*item_height), 100 | render, 101 | )?; 102 | } 103 | 104 | // render the selection 105 | draw_single_match::<_, _, Head, _, true>( 106 | writer, 107 | buffer, 108 | match_list_width, 109 | config, 110 | &item_iter.next().unwrap(), 111 | snapshot, 112 | matcher, 113 | as_u16(below[0]), 114 | render, 115 | )?; 116 | 117 | // render below the selection 118 | for (item_height, item) in below[1..].iter().zip(item_iter.by_ref()) { 119 | draw_single_match::<_, _, Head, _, false>( 120 | writer, 121 | buffer, 122 | match_list_width, 123 | config, 124 | &item, 125 | snapshot, 126 | matcher, 127 | as_u16(*item_height), 128 | render, 129 | )?; 130 | } 131 | 132 | Ok(()) 133 | } 134 | 135 | fn draw_match_counts( 136 | writer: &mut W, 137 | matched: u32, 138 | total: u32, 139 | ) -> io::Result<()> { 140 | writer 141 | .queue(SetAttribute(Attribute::Italic))? 142 | .queue(SetForegroundColor(Color::Green))? 143 | .queue(Print(" "))? 144 | .queue(Print(matched))? 145 | .queue(Print("/"))? 146 | .queue(Print(total))? 147 | .queue(SetAttribute(Attribute::Reset))? 148 | .queue(ResetColor)? 149 | .queue(Clear(ClearType::UntilNewLine))?; 150 | 151 | Ok(()) 152 | } 153 | 154 | impl> Component for MatchList { 155 | type Event = MatchListEvent; 156 | 157 | type Status = bool; 158 | 159 | fn handle(&mut self, e: Self::Event) -> bool { 160 | match e { 161 | MatchListEvent::Up(n) => { 162 | if self.config.reversed { 163 | self.selection_decr(as_u32(n)) 164 | } else { 165 | self.selection_incr(as_u32(n)) 166 | } 167 | } 168 | MatchListEvent::Down(n) => { 169 | if self.config.reversed { 170 | self.selection_incr(as_u32(n)) 171 | } else { 172 | self.selection_decr(as_u32(n)) 173 | } 174 | } 175 | MatchListEvent::Reset => self.reset(), 176 | } 177 | } 178 | 179 | fn draw( 180 | &mut self, 181 | width: u16, 182 | height: u16, 183 | writer: &mut W, 184 | ) -> std::io::Result<()> { 185 | let match_list_height = height - 1; 186 | let match_list_width = width.saturating_sub(3); 187 | 188 | if match_list_height != self.size { 189 | self.resize(match_list_height); 190 | } 191 | 192 | let snapshot = self.nucleo.snapshot(); 193 | let matched_item_count = snapshot.matched_item_count(); 194 | 195 | let mut total_whitespace = self.whitespace(); 196 | 197 | // draw the matches 198 | if height == 1 { 199 | draw_match_counts(writer, matched_item_count, snapshot.item_count())?; 200 | } else if self.config.reversed { 201 | draw_match_counts(writer, matched_item_count, snapshot.item_count())?; 202 | writer.queue(MoveToNextLine(1))?; 203 | 204 | if matched_item_count != 0 { 205 | let item_iter = snapshot.matched_items(self.selection_range()); 206 | draw_matches( 207 | writer, 208 | &mut self.scratch, 209 | &self.config, 210 | snapshot, 211 | &mut self.matcher, 212 | self.render.as_ref(), 213 | match_list_width, 214 | &self.above, 215 | &self.below, 216 | item_iter, 217 | )?; 218 | } 219 | 220 | if total_whitespace > 0 { 221 | writer.queue(Clear(ClearType::FromCursorDown))?; 222 | } 223 | } else { 224 | // skip / clear whitespace if necessary 225 | while total_whitespace > 0 { 226 | total_whitespace -= 1; 227 | writer 228 | .queue(Clear(ClearType::UntilNewLine))? 229 | .queue(MoveToNextLine(1))?; 230 | } 231 | 232 | if matched_item_count != 0 { 233 | let item_iter = snapshot.matched_items(self.selection_range()).rev(); 234 | draw_matches( 235 | writer, 236 | &mut self.scratch, 237 | &self.config, 238 | snapshot, 239 | &mut self.matcher, 240 | self.render.as_ref(), 241 | match_list_width, 242 | &self.above, 243 | &self.below, 244 | item_iter, 245 | )?; 246 | } 247 | 248 | draw_match_counts(writer, matched_item_count, snapshot.item_count())?; 249 | } 250 | 251 | Ok(()) 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/match_list/item.rs: -------------------------------------------------------------------------------- 1 | use memchr::memchr_iter; 2 | use nucleo::{Item, Snapshot, Utf32Str}; 3 | 4 | use super::{ItemList, ItemSize}; 5 | use crate::Render; 6 | 7 | impl ItemSize for Item<'_, T> { 8 | fn size(&self) -> usize { 9 | let num_linebreaks = match self.matcher_columns[0].slice(..) { 10 | Utf32Str::Ascii(bytes) => memchr_iter(b'\n', bytes).count(), 11 | Utf32Str::Unicode(chars) => { 12 | // TODO: there is an upstream Unicode handling issue in that windows-style newlines are 13 | // mapped to `\r` instead of `\n`. Therefore we count both the number of occurrences of 14 | // `\r` and `\n`. This handles mixed `\r\n` as well as `\n`, but returns the incorrect 15 | // value in the presence of free-standing carriage returns. 16 | chars 17 | .iter() 18 | .filter(|ch| **ch == '\n' || **ch == '\r') 19 | .count() 20 | } 21 | }; 22 | 1 + num_linebreaks 23 | } 24 | } 25 | 26 | impl ItemList for Snapshot { 27 | type Item<'a> 28 | = Item<'a, T> 29 | where 30 | Self: 'a; 31 | 32 | fn total(&self) -> u32 { 33 | self.matched_item_count() 34 | } 35 | 36 | fn lower(&self, selection: u32) -> impl DoubleEndedIterator> { 37 | self.matched_items(..selection).rev() 38 | } 39 | 40 | fn lower_inclusive(&self, selection: u32) -> impl DoubleEndedIterator> { 41 | self.matched_items(..=selection).rev() 42 | } 43 | 44 | fn higher(&self, selection: u32) -> impl DoubleEndedIterator> { 45 | // we skip the first item rather than iterate on the range `selection + 1..` in case 46 | // `selection + 1` is an invalid index in which case `matched_items` would panic 47 | self.matched_items(selection..).skip(1) 48 | } 49 | 50 | fn higher_inclusive(&self, selection: u32) -> impl DoubleEndedIterator> { 51 | // we skip the first item rather than iterate on the range `selection + 1..` in case 52 | // `selection + 1` is an invalid index in which case `matched_items` would panic 53 | self.matched_items(selection..) 54 | } 55 | } 56 | 57 | /// A container type since a [`Render`] implementation might return a type which needs ownership. 58 | /// 59 | /// For the given item, check the corresponding variant. If the variant is ASCII, that means we can 60 | /// use much more efficient ASCII processing on rendering. 61 | pub enum RenderedItem<'a, S> { 62 | Ascii(&'a str), 63 | Unicode(S), 64 | } 65 | 66 | impl<'a, S> RenderedItem<'a, S> { 67 | /// Initialize a new `RenderedItem` from an [`Item`] and a [`Render`] implementation. 68 | pub fn new(item: &Item<'a, T>, renderer: &R) -> Self 69 | where 70 | R: Render = S>, 71 | { 72 | if let Utf32Str::Ascii(bytes) = item.matcher_columns[0].slice(..) { 73 | RenderedItem::Ascii(unsafe { std::str::from_utf8_unchecked(bytes) }) 74 | } else { 75 | RenderedItem::Unicode(renderer.render(item.data)) 76 | } 77 | } 78 | } 79 | 80 | impl> AsRef for RenderedItem<'_, S> { 81 | fn as_ref(&self) -> &str { 82 | match self { 83 | RenderedItem::Ascii(s) => s, 84 | RenderedItem::Unicode(u) => u.as_ref(), 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/match_list/layout.rs: -------------------------------------------------------------------------------- 1 | pub mod reset; 2 | pub mod resize; 3 | pub mod selection; 4 | pub mod update; 5 | 6 | use super::MatchListState; 7 | -------------------------------------------------------------------------------- /src/match_list/layout/reset.rs: -------------------------------------------------------------------------------- 1 | use crate::incremental::ExtendIncremental; 2 | 3 | #[inline] 4 | pub fn reset( 5 | mut total_remaining: u16, 6 | padding_top: u16, 7 | mut sizes_below_incl: impl ExtendIncremental, 8 | mut sizes_above: impl ExtendIncremental, 9 | ) { 10 | // cursor = 0, so this renders exactly one element; but we need to make sure we do not 11 | // accidentally fill `padding_top` as well 12 | total_remaining -= sizes_below_incl.extend_unbounded(total_remaining - padding_top); 13 | sizes_above.extend_unbounded(total_remaining); 14 | } 15 | 16 | #[inline] 17 | pub fn reset_rev(total_remaining: u16, mut sizes_below_incl: impl ExtendIncremental) { 18 | sizes_below_incl.extend_unbounded(total_remaining); 19 | } 20 | -------------------------------------------------------------------------------- /src/match_list/layout/resize.rs: -------------------------------------------------------------------------------- 1 | use super::MatchListState; 2 | use crate::incremental::ExtendIncremental; 3 | 4 | #[inline] 5 | pub fn larger( 6 | previous: MatchListState, 7 | mut total_remaining: u16, 8 | mut sizes_below_incl: impl ExtendIncremental, 9 | mut sizes_above: impl ExtendIncremental, 10 | ) { 11 | // fill the space below as far as possible 12 | total_remaining -= sizes_below_incl.extend_unbounded(total_remaining - previous.above); 13 | 14 | // and then anything remaining above: we use `total_remaining` rather than `previous.above` 15 | // since it is possible that we now hit the bottom of the screen in which case there is extra 16 | // space above 17 | sizes_above.extend_unbounded(total_remaining); 18 | } 19 | 20 | #[inline] 21 | pub fn smaller( 22 | previous: MatchListState, 23 | mut total_remaining: u16, 24 | padding_top: u16, 25 | mut sizes_below_incl: impl ExtendIncremental, 26 | mut sizes_above: impl ExtendIncremental, 27 | ) { 28 | // since the screen size changed, take the capacity from above, but do not exceed the top 29 | // padding 30 | let max_allowed_above = previous 31 | .above 32 | .saturating_sub(previous.size - total_remaining) 33 | .max(padding_top); 34 | 35 | // this is valid since the `previous.above` was already clamped 36 | let max_allowed_below = total_remaining - max_allowed_above; 37 | 38 | // first, render below: note that this is guaranteed to render as much of the selection as 39 | // possible since the selection size is unchanged, and we have first removed elements from 40 | // above as much as possible 41 | total_remaining -= sizes_below_incl.extend_unbounded(max_allowed_below); 42 | 43 | // then above 44 | sizes_above.extend_unbounded(total_remaining); 45 | } 46 | 47 | #[inline] 48 | pub fn larger_rev( 49 | previous: MatchListState, 50 | mut total_remaining: u16, 51 | padding_top: u16, 52 | mut sizes_below_incl: impl ExtendIncremental, 53 | mut sizes_above: impl ExtendIncremental, 54 | ) { 55 | let new_size = total_remaining; 56 | 57 | // since the selection may have not fit with the previous screen size, try again to render as 58 | // much of the selection as possible 59 | total_remaining -= sizes_below_incl.extend_bounded(total_remaining - padding_top, 1); 60 | 61 | // then render into the new space above 62 | total_remaining -= sizes_above.extend_unbounded(total_remaining.min(new_size - previous.below)); 63 | 64 | // and then any more space below 65 | sizes_below_incl.extend_unbounded(total_remaining); 66 | } 67 | 68 | #[inline] 69 | pub fn smaller_rev( 70 | previous: MatchListState, 71 | mut total_remaining: u16, 72 | padding_top: u16, 73 | padding_bottom: u16, 74 | mut sizes_below_incl: impl ExtendIncremental, 75 | mut sizes_above: impl ExtendIncremental, 76 | ) { 77 | // the amount that the screen decreased by 78 | let screen_delta = previous.size - total_remaining; 79 | 80 | // render as much of the selection as possible 81 | let selection_size = sizes_below_incl.extend_bounded(total_remaining - padding_top, 1); 82 | 83 | // since the screen size changed, take the capacity from below, but do not exceed the bottom 84 | // padding or the selection size; take the remaining capacity from above 85 | let max_allowed_below = previous 86 | .below 87 | .saturating_sub(screen_delta) 88 | .max(padding_bottom + 1) 89 | .max(selection_size); 90 | let max_allowed_above = total_remaining - max_allowed_below; 91 | 92 | // and then above 93 | total_remaining -= selection_size; 94 | total_remaining -= sizes_above.extend_unbounded(max_allowed_above); 95 | 96 | // and then any of the remaining space below 97 | sizes_below_incl.extend_unbounded(total_remaining); 98 | } 99 | -------------------------------------------------------------------------------- /src/match_list/layout/selection.rs: -------------------------------------------------------------------------------- 1 | use super::MatchListState; 2 | use crate::{incremental::ExtendIncremental, util::as_usize}; 3 | 4 | #[inline] 5 | pub fn incr( 6 | previous: MatchListState, 7 | cursor: u32, 8 | padding_top: u16, 9 | mut sizes_below_incl: impl ExtendIncremental, 10 | mut sizes_above: impl ExtendIncremental, 11 | ) { 12 | let mut total_remaining = previous.size; 13 | 14 | // render new elements strictly above the previous selection 15 | let new_size_above = sizes_below_incl.extend_bounded( 16 | total_remaining - padding_top, 17 | as_usize(cursor - previous.selection), 18 | ); 19 | total_remaining -= new_size_above; 20 | 21 | // subtract the newly rendered items from the space above; but do not exceed the top padding 22 | let max_allowed_above = previous 23 | .above 24 | .saturating_sub(new_size_above) 25 | .max(padding_top); 26 | 27 | // render the remaining elements: we are guaranteed to not hit the bottom of the screen since 28 | // the number of items rendered above in total can only increase 29 | sizes_above.extend_unbounded(max_allowed_above); 30 | sizes_below_incl.extend_unbounded(total_remaining - max_allowed_above); 31 | } 32 | 33 | #[inline] 34 | pub fn decr( 35 | previous: MatchListState, 36 | cursor: u32, 37 | padding_top: u16, 38 | padding_bottom: u16, 39 | mut sizes_below_incl: impl ExtendIncremental, 40 | mut sizes_above: impl ExtendIncremental, 41 | ) { 42 | let mut total_remaining = previous.size; 43 | 44 | // render as much of the selection as possible 45 | let selection_rendered = sizes_below_incl.extend_bounded(total_remaining - padding_top, 1); 46 | total_remaining -= selection_rendered; 47 | 48 | // also try to fill the bottom padding 49 | total_remaining -= 50 | sizes_below_incl.extend_unbounded((padding_bottom + 1).saturating_sub(selection_rendered)); 51 | 52 | // render above above until we hit the previous selection 53 | total_remaining -= 54 | sizes_above.extend_bounded(total_remaining, as_usize(previous.selection - cursor)); 55 | 56 | // truncate below to prevent the screen from scrolling unnecessarily 57 | let max_space_below = total_remaining - total_remaining.min(previous.above); 58 | 59 | // render any remaining space below 60 | total_remaining -= sizes_below_incl.extend_unbounded(max_space_below); 61 | 62 | // render above 63 | sizes_above.extend_unbounded(total_remaining); 64 | } 65 | 66 | #[inline] 67 | pub fn incr_rev( 68 | previous: MatchListState, 69 | cursor: u32, 70 | padding_top: u16, 71 | padding_bottom: u16, 72 | mut sizes_below_incl: impl ExtendIncremental, 73 | mut sizes_above: impl ExtendIncremental, 74 | ) { 75 | let mut total_remaining = previous.size; 76 | 77 | // render as much of the selection as possible 78 | let selection_rendered = sizes_below_incl.extend_bounded(total_remaining - padding_top, 1); 79 | total_remaining -= selection_rendered; 80 | 81 | // render above above until we hit the previous selection, without also filling the bottom 82 | // padding 83 | let rendered_above = sizes_above.extend_bounded( 84 | total_remaining.min(previous.size - padding_bottom - 1), 85 | as_usize(cursor - previous.selection), 86 | ); 87 | total_remaining -= rendered_above; 88 | 89 | // compute the maximum amount of space above by taking the previous size and subtracting the 90 | // amount of space the new items rendered below occupy, making sure to also reserve space 91 | // for the bottom padding 92 | let max_space_above = previous.size 93 | - (rendered_above + selection_rendered.max(padding_bottom + 1)).max(previous.below); 94 | 95 | // render above; note that `max_space_above <= total_remaining` since we only restrict the size 96 | // more 97 | total_remaining -= sizes_above.extend_unbounded(max_space_above); 98 | 99 | // render anything remaining 100 | sizes_below_incl.extend_unbounded(total_remaining); 101 | } 102 | 103 | #[inline] 104 | pub fn decr_rev( 105 | previous: MatchListState, 106 | cursor: u32, 107 | padding_top: u16, 108 | mut sizes_below_incl: impl ExtendIncremental, 109 | mut sizes_above: impl ExtendIncremental, 110 | ) { 111 | let mut total_remaining = previous.size; 112 | 113 | // render new elements strictly above the previous selection 114 | let new_size_above = sizes_below_incl.extend_bounded( 115 | total_remaining - padding_top, 116 | as_usize(previous.selection - cursor), 117 | ); 118 | total_remaining -= new_size_above; 119 | 120 | // subtract space from the previous space above, but do not go below the top padding 121 | let max_space_above = (previous.size - previous.below) 122 | .saturating_sub(new_size_above) 123 | .max(padding_top); 124 | 125 | total_remaining -= sizes_above.extend_unbounded(max_space_above); 126 | sizes_below_incl.extend_unbounded(total_remaining); 127 | } 128 | -------------------------------------------------------------------------------- /src/match_list/layout/update.rs: -------------------------------------------------------------------------------- 1 | use super::MatchListState; 2 | use crate::incremental::ExtendIncremental; 3 | 4 | #[inline] 5 | pub fn items( 6 | previous: MatchListState, 7 | padding_top: u16, 8 | mut sizes_below_incl: impl ExtendIncremental, 9 | mut sizes_above: impl ExtendIncremental, 10 | ) { 11 | // we want to preserve the value of `previous.above`; but this might fail if: 12 | // 1. we hit the start of the list when rendering below, or 13 | // 2. the size of the selection is too large. 14 | 15 | let mut total_remaining = previous.size; 16 | 17 | // render the selection 18 | total_remaining -= sizes_below_incl.extend_bounded(total_remaining - padding_top, 1); 19 | 20 | // render any space below the selection, attempting to reserve 'previous.above' space if 21 | // possible 22 | total_remaining -= 23 | sizes_below_incl.extend_unbounded(total_remaining.saturating_sub(previous.above)); 24 | 25 | // render anything remaining above the selection 26 | sizes_above.extend_unbounded(total_remaining); 27 | } 28 | 29 | #[inline] 30 | pub fn items_rev( 31 | previous: MatchListState, 32 | padding_top: u16, 33 | mut sizes_below_incl: impl ExtendIncremental, 34 | mut sizes_above: impl ExtendIncremental, 35 | ) { 36 | // we want to preserve the value of `previous.below`; but this might fail if: 37 | // 1. we hit the start of the list when rendering above, or 38 | // 2. the size of the selection is too large. 39 | 40 | let mut total_remaining = previous.size; 41 | 42 | // render the selection and any space above the selection, attempting to reserve 43 | // 'previous.below' space if possible 44 | let selection_size = sizes_below_incl.extend_bounded(total_remaining - padding_top, 1); 45 | total_remaining -= sizes_above 46 | .extend_unbounded(total_remaining.saturating_sub(previous.below.max(selection_size))); 47 | total_remaining -= selection_size; 48 | 49 | // render anything remaining below the selection 50 | sizes_below_incl.extend_unbounded(total_remaining); 51 | } 52 | -------------------------------------------------------------------------------- /src/match_list/span.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests; 3 | 4 | use std::{ 5 | io::{self, Write}, 6 | iter::once, 7 | marker::PhantomData, 8 | ops::Range, 9 | slice::Iter, 10 | }; 11 | 12 | use crossterm::{ 13 | cursor::{MoveToColumn, MoveToNextLine}, 14 | style::{ 15 | Attribute, Color, Print, PrintStyledContent, SetAttribute, SetBackgroundColor, Stylize, 16 | }, 17 | terminal::{Clear, ClearType}, 18 | QueueableCommand, 19 | }; 20 | 21 | use super::unicode::{consume, spans_from_indices, truncate, Processor, Span}; 22 | 23 | const ELLIPSIS: char = '…'; 24 | 25 | /// An iterator over lines, as span slices. 26 | pub struct SpannedLines<'a> { 27 | iter: Iter<'a, Range>, 28 | spans: &'a [Span], 29 | } 30 | 31 | impl<'a> Iterator for SpannedLines<'a> { 32 | type Item = &'a [Span]; 33 | 34 | #[inline] 35 | fn next(&mut self) -> Option { 36 | match self.iter.next() { 37 | Some(rg) => Some(&self.spans[rg.start..rg.end]), 38 | None => None, 39 | } 40 | } 41 | } 42 | 43 | pub trait KeepLines { 44 | fn from_offset(offset: u16) -> Self; 45 | 46 | fn subslice<'a>(&self, lines: &'a [Range]) -> &'a [Range]; 47 | } 48 | 49 | pub struct Tail(usize); 50 | 51 | impl KeepLines for Tail { 52 | fn subslice<'a>(&self, lines: &'a [Range]) -> &'a [Range] { 53 | &lines[lines.len() - self.0..] 54 | } 55 | 56 | fn from_offset(offset: u16) -> Self { 57 | Self(offset as usize) 58 | } 59 | } 60 | 61 | pub struct Head(usize); 62 | 63 | impl KeepLines for Head { 64 | fn subslice<'a>(&self, lines: &'a [Range]) -> &'a [Range] { 65 | &lines[..self.0] 66 | } 67 | 68 | fn from_offset(offset: u16) -> Self { 69 | Self(offset as usize) 70 | } 71 | } 72 | 73 | struct All; 74 | 75 | impl KeepLines for All { 76 | fn subslice<'a>(&self, lines: &'a [Range]) -> &'a [Range] { 77 | lines 78 | } 79 | 80 | fn from_offset(_: u16) -> Self { 81 | Self 82 | } 83 | } 84 | 85 | /// Represent additional data on top of a string slice. 86 | /// 87 | /// The `spans` are guaranteed to not contain newlines. In order to determine which spans belong to 88 | /// which line, `lines` consists of contiguous sub-slices of `spans`. 89 | #[derive(Debug)] 90 | pub struct Spanned<'a, P> { 91 | rendered: &'a str, 92 | spans: &'a [Span], 93 | lines: &'a [Range], 94 | _marker: PhantomData

, 95 | } 96 | 97 | impl<'a, P: Processor> Spanned<'a, P> { 98 | #[inline] 99 | pub fn new( 100 | indices: &[u32], 101 | rendered: &'a str, 102 | spans: &'a mut Vec, 103 | lines: &'a mut Vec>, 104 | keep_lines: L, 105 | ) -> Self { 106 | spans_from_indices::

(indices, rendered, spans, lines); 107 | Self { 108 | rendered, 109 | spans, 110 | lines: keep_lines.subslice(lines), 111 | _marker: PhantomData, 112 | } 113 | } 114 | 115 | /// Compute the maximum number of bytes over all lines. 116 | #[inline] 117 | fn max_line_bytes(&self) -> usize { 118 | let mut max_line_bytes = 0; 119 | for line in self.lines() { 120 | if !line.is_empty() { 121 | max_line_bytes = max_line_bytes 122 | .max(line.last().unwrap().range.end - line.first().unwrap().range.start); 123 | } 124 | } 125 | 126 | max_line_bytes 127 | } 128 | 129 | /// Returns the width (possibly 0) required to render all of the spans which require highlighting. 130 | #[inline] 131 | fn required_width(&self) -> usize { 132 | let mut required_width = 0; 133 | 134 | for line in self.lines() { 135 | // find the 'rightmost' highlighted span 136 | if let Some(span) = line.iter().rev().find(|span| span.is_match) { 137 | required_width = required_width.max( 138 | // spans[0] must exist since `find` returned something 139 | P::width(&self.rendered[line[0].range.start..span.range.end]), 140 | ); 141 | } 142 | } 143 | required_width 144 | } 145 | 146 | /// Returns the optiomal offset (in terminal columns) for printing the given line. 147 | /// The offset automatically reserves an extra space for a single indicator symbol (such as an 148 | /// ellipsis), if required. The ellipsis should be printed whenever the returned value is not 149 | /// `0`. 150 | #[inline] 151 | fn required_offset(&self, max_width: u16, highlight_padding: u16) -> usize { 152 | match (self.required_width() + highlight_padding as usize).checked_sub(max_width as usize) { 153 | None | Some(0) => 0, 154 | Some(mut offset) => { 155 | // ideally, we would like to offset by `offset`; but we prefer highlighting 156 | // matches which are earlier in the string. Therefore, reduce `offset` so that it 157 | // lies before the first highlighted character in each line. 158 | 159 | let mut is_sharp = false; // if the offset cannot be increased because of a 160 | // highlighted char early in the match 161 | 162 | for line in self.lines() { 163 | // find the 'leftmost' highlighted span. 164 | if let Some(span) = line.iter().find(|span| span.is_match) { 165 | let no_highlight_width = 166 | P::width(&self.rendered[line[0].range.start..span.range.start]); 167 | if no_highlight_width <= offset { 168 | offset = no_highlight_width; 169 | is_sharp = true; 170 | } 171 | } 172 | } 173 | 174 | // if the offset is not sharp, reserve an extra space for the ellipsis symbol 175 | if !is_sharp { 176 | offset += 1; 177 | }; 178 | 179 | // if the offset is exactly 1, set it to 0 since we can just print the first 180 | // character instead of the ellipsis 181 | if offset == 1 { 182 | 0 183 | } else { 184 | offset 185 | } 186 | } 187 | } 188 | } 189 | 190 | /// Print the header for each line, which is either two spaces or styled indicator. This also 191 | /// sets the highlighting features for the given line. 192 | #[inline] 193 | fn start_line(stderr: &mut W, selected: bool) -> io::Result<()> { 194 | if selected { 195 | // print the line as bold, and with a 'selection' marker 196 | stderr 197 | .queue(SetAttribute(Attribute::Bold))? 198 | .queue(SetBackgroundColor(Color::DarkGrey))? 199 | .queue(PrintStyledContent("▌ ".magenta()))?; 200 | } else { 201 | // print a blank instead 202 | stderr.queue(Print(" "))?; 203 | } 204 | Ok(()) 205 | } 206 | 207 | /// Queue a string slice for printing to stderr, either highlighted or printed. 208 | #[inline] 209 | fn print_span( 210 | stderr: &mut W, 211 | to_print: &str, 212 | highlight: bool, 213 | ) -> io::Result<()> { 214 | if highlight { 215 | stderr.queue(PrintStyledContent(to_print.cyan()))?; 216 | } else { 217 | stderr.queue(Print(to_print))?; 218 | } 219 | Ok(()) 220 | } 221 | 222 | /// Clean up after printing the line by resetting any display styling, clearing any trailing 223 | /// characters, and moving to the next line. 224 | #[inline] 225 | fn finish_line(stderr: &mut W) -> io::Result<()> { 226 | stderr 227 | .queue(SetAttribute(Attribute::Reset))? 228 | .queue(Clear(ClearType::UntilNewLine))? 229 | .queue(MoveToNextLine(1))?; 230 | Ok(()) 231 | } 232 | 233 | /// Print for display into a terminal with width `max_width`, and with styling to match if the 234 | /// item is selected or not. 235 | #[inline] 236 | pub fn queue_print( 237 | &self, 238 | stderr: &mut W, 239 | selected: bool, 240 | max_width: u16, 241 | highlight_padding: u16, 242 | ) -> io::Result<()> { 243 | if self.max_line_bytes() <= max_width.saturating_sub(highlight_padding) as usize { 244 | // Fast path: all of the lines are short, so we can just render them without any unicode width 245 | // checks. This should be the case for the majority of situations, unless the screen is 246 | // very narrow or the rendered items are very wide. 247 | // 248 | // This check is safe since the only unicode characters which require two columns consist of 249 | // at least two bytes, so the number of bytes is always an upper bound for the number of 250 | // columns. 251 | // 252 | // If the input is ASCII, this check is optimal. 253 | for line in self.lines() { 254 | Self::start_line(stderr, selected)?; 255 | for span in line { 256 | Self::print_span(stderr, self.index_in(span), span.is_match)?; 257 | } 258 | Self::finish_line(stderr)?; 259 | } 260 | } else { 261 | let offset = self.required_offset(max_width, highlight_padding); 262 | 263 | for line in self.lines() { 264 | Self::start_line(stderr, selected)?; 265 | self.queue_print_line(stderr, line, offset, max_width)?; 266 | Self::finish_line(stderr)?; 267 | } 268 | } 269 | Ok(()) 270 | } 271 | 272 | /// Print a single line (represented as a slice of [`Span`]) to the terminal screen, with the 273 | /// given `offset` and the width of the screen in columns, as `capacity`. 274 | #[inline] 275 | fn queue_print_line( 276 | &self, 277 | stderr: &mut W, 278 | line: &[Span], 279 | offset: usize, 280 | capacity: u16, 281 | ) -> io::Result<()> { 282 | let mut remaining_capacity = capacity; 283 | 284 | // do not print ellipsis if line is empty or the screen is extremely narrow 285 | if line.is_empty() || remaining_capacity == 0 { 286 | return Ok(()); 287 | }; 288 | 289 | if offset > 0 { 290 | // we just checked that `capacity != 0` 291 | remaining_capacity -= 1; 292 | stderr.queue(Print(ELLIPSIS))?; 293 | }; 294 | 295 | // consume as much of the first span as required to overtake the offset. since the width of 296 | // the offset is bounded above by the width of the first span, this is guaranteed to occur 297 | // within the first span 298 | let first_span = &line[0]; 299 | let (init, alignment) = consume::

(self.index_in(first_span), offset); 300 | let new_first_span = Span { 301 | range: first_span.range.start + init..first_span.range.end, 302 | is_match: first_span.is_match, 303 | }; 304 | 305 | // print the extra alignment characters 306 | match (remaining_capacity as usize).checked_sub(alignment) { 307 | Some(new) => { 308 | remaining_capacity = new as u16; 309 | for _ in 0..alignment { 310 | stderr.queue(Print(ELLIPSIS))?; 311 | } 312 | } 313 | None => return Ok(()), 314 | } 315 | 316 | // print as many spans as possible 317 | for span in once(&new_first_span).chain(line[1..].iter()) { 318 | let substr = self.index_in(span); 319 | match truncate::

(substr, remaining_capacity) { 320 | Ok(new) => { 321 | remaining_capacity = new; 322 | Self::print_span(stderr, substr, span.is_match)?; 323 | } 324 | Err((prefix, alignment)) => { 325 | Self::print_span(stderr, prefix, span.is_match)?; 326 | if alignment > 0 { 327 | // there is already extra space; fill it 328 | for _ in 0..alignment { 329 | stderr.queue(Print(ELLIPSIS))?; 330 | } 331 | } else { 332 | // overwrite the previous grapheme 333 | let undo_width = P::last_grapheme_width( 334 | &self.rendered[..span.range.start + prefix.len()], 335 | ); 336 | 337 | stderr.queue(MoveToColumn(2 + capacity - undo_width as u16))?; 338 | for _ in 0..undo_width { 339 | stderr.queue(Print(ELLIPSIS))?; 340 | } 341 | } 342 | return Ok(()); 343 | } 344 | } 345 | } 346 | 347 | Ok(()) 348 | } 349 | 350 | /// Compute the string slice corresponding to the given [`Span`]. 351 | /// 352 | /// # Panics 353 | /// This method must be called with a span with `range.start` and `range.end` corresponding to 354 | /// valid unicode indices in `rendered`. 355 | #[inline] 356 | fn index_in(&self, span: &Span) -> &str { 357 | &self.rendered[span.range.start..span.range.end] 358 | } 359 | 360 | #[inline] 361 | fn lines(&self) -> SpannedLines<'_> { 362 | SpannedLines { 363 | iter: self.lines.iter(), 364 | spans: self.spans, 365 | } 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /src/match_list/span/tests.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | super::unicode::{is_ascii_safe, is_unicode_safe, AsciiProcessor, UnicodeProcessor}, 3 | *, 4 | }; 5 | 6 | #[test] 7 | fn required_width() { 8 | fn assert_correct_width(indices: Vec, rendered: &str, expected_width: usize) { 9 | let mut spans = Vec::new(); 10 | let mut lines = Vec::new(); 11 | let spanned: Spanned<'_, UnicodeProcessor> = 12 | Spanned::new(&indices, rendered, &mut spans, &mut lines, All); 13 | 14 | if is_unicode_safe(rendered) { 15 | assert_eq!(spanned.required_width(), expected_width); 16 | } 17 | 18 | if is_ascii_safe(rendered) { 19 | let spanned: Spanned<'_, AsciiProcessor> = 20 | Spanned::new(&indices, rendered, &mut spans, &mut lines, All); 21 | assert_eq!(spanned.required_width(), expected_width); 22 | } 23 | } 24 | 25 | assert_correct_width(vec![], "a", 0); 26 | assert_correct_width(vec![0], "a", 1); 27 | assert_correct_width(vec![1], "ab", 2); 28 | assert_correct_width(vec![0], "Hb", 2); 29 | assert_correct_width(vec![1], "Hb", 3); 30 | 31 | assert_correct_width(vec![0, 4], "ab\ncd", 2); 32 | assert_correct_width(vec![0, 4], "ab\nHd", 3); 33 | assert_correct_width(vec![0, 5], "ab\n\nHH", 4); 34 | assert_correct_width(vec![1, 5], "HHb\n\nab", 4); 35 | } 36 | 37 | #[test] 38 | fn required_offset() { 39 | fn assert_correct_offset( 40 | indices: Vec, 41 | rendered: &str, 42 | max_width: u16, 43 | expected_offset: usize, 44 | ) { 45 | let mut spans = Vec::new(); 46 | let mut lines = Vec::new(); 47 | 48 | if is_unicode_safe(rendered) { 49 | let spanned: Spanned<'_, UnicodeProcessor> = 50 | Spanned::new(&indices, rendered, &mut spans, &mut lines, All); 51 | assert_eq!(spanned.required_offset(max_width, 0), expected_offset); 52 | } 53 | 54 | if is_ascii_safe(rendered) { 55 | let spanned: Spanned<'_, AsciiProcessor> = 56 | Spanned::new(&indices, rendered, &mut spans, &mut lines, All); 57 | assert_eq!(spanned.required_offset(max_width, 0), expected_offset); 58 | } 59 | } 60 | 61 | assert_correct_offset(vec![], "a", 1, 0); 62 | assert_correct_offset(vec![], "abc", 1, 0); 63 | assert_correct_offset(vec![2], "abc", 1, 2); 64 | assert_correct_offset(vec![2], "abc", 2, 2); 65 | assert_correct_offset(vec![2], "abc", 3, 0); 66 | assert_correct_offset(vec![2], "abc\nab", 2, 2); 67 | assert_correct_offset(vec![7], "abc\nabcd", 2, 3); 68 | 69 | assert_correct_offset(vec![7], "abc\nabcd", 2, 3); 70 | 71 | assert_correct_offset(vec![0, 7], "abc\nabcd", 2, 0); 72 | assert_correct_offset(vec![1, 7], "abc\nabcd", 2, 0); 73 | assert_correct_offset(vec![2, 7], "abc\nabcd", 2, 2); 74 | 75 | assert_correct_offset(vec![0, 6], "abc\naHd", 2, 0); 76 | assert_correct_offset(vec![1, 6], "abc\naHd", 2, 0); 77 | assert_correct_offset(vec![2, 6], "abc\naHd", 2, 2); 78 | assert_correct_offset(vec![2, 6], "abc\naHd", 3, 2); 79 | 80 | assert_correct_offset(vec![2, 4, 8], "abc\na\r\naHd", 1, 0); 81 | assert_correct_offset(vec![2, 4, 8], "abc\na\r\naHd", 2, 0); 82 | assert_correct_offset(vec![2, 8], "abc\na\r\naHd", 2, 2); 83 | assert_correct_offset(vec![2, 4, 8], "abc\na\r\naHd", 3, 0); 84 | assert_correct_offset(vec![2, 8], "abc\na\r\naHd", 3, 2); 85 | assert_correct_offset(vec![2, 8], "abc\na\r\naHd", 4, 0); 86 | } 87 | -------------------------------------------------------------------------------- /src/match_list/tests.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use nucleo::{Config, Nucleo, Utf32String}; 4 | 5 | use super::*; 6 | 7 | use crate::render::StrRenderer; 8 | 9 | use Action::*; 10 | 11 | enum Action<'a> { 12 | Incr(u32), 13 | Decr(u32), 14 | Reset, 15 | Update(&'a [&'static str]), 16 | Resize(u16), 17 | } 18 | 19 | fn reset(nc: &mut Nucleo<&'static str>, items: &[&'static str]) { 20 | nc.restart(true); 21 | let injector = nc.injector(); 22 | for item in items { 23 | injector.push(item, |item, cols| { 24 | cols[0] = Utf32String::from(*item); 25 | }); 26 | } 27 | 28 | while nc.tick(5).running {} 29 | } 30 | 31 | struct MatchListTester { 32 | match_list: MatchList<&'static str, StrRenderer>, 33 | } 34 | 35 | /// A view into a [`Matcher`] at a given point in time. 36 | #[derive(Debug, Clone, PartialEq)] 37 | struct LayoutView<'a> { 38 | /// The number of lines to render for each item beginning below the screen index and rendering 39 | /// downwards. 40 | pub below: &'a [usize], 41 | /// The number of lines to render for each item beginning above the screen index and rendering 42 | /// upwards. 43 | pub above: &'a [usize], 44 | } 45 | 46 | impl MatchListTester { 47 | fn init_inner(size: u16, max_padding: u16, reversed: bool) -> Self { 48 | let nc = Nucleo::new(Config::DEFAULT, Arc::new(|| {}), Some(1), 1); 49 | let mut mc = MatchListConfig::default(); 50 | mc.scroll_padding = max_padding; 51 | mc.reversed = reversed; 52 | 53 | let mut match_list = MatchList::new(mc, Config::DEFAULT, nc, StrRenderer.into()); 54 | match_list.resize(size); 55 | 56 | Self { match_list } 57 | } 58 | 59 | fn init(size: u16, max_padding: u16) -> Self { 60 | Self::init_inner(size, max_padding, false) 61 | } 62 | 63 | fn init_rev(size: u16, max_padding: u16) -> Self { 64 | Self::init_inner(size, max_padding, true) 65 | } 66 | 67 | fn update(&mut self, lc: Action) { 68 | match lc { 69 | Action::Incr(incr) => { 70 | self.match_list.selection_incr(incr); 71 | } 72 | Action::Decr(decr) => { 73 | self.match_list.selection_decr(decr); 74 | } 75 | Action::Reset => { 76 | self.match_list.reset(); 77 | } 78 | Action::Update(items) => { 79 | reset(&mut self.match_list.nucleo, items); 80 | self.match_list.update_items(); 81 | } 82 | Action::Resize(sz) => { 83 | self.match_list.resize(sz); 84 | } 85 | } 86 | } 87 | 88 | fn view(&self) -> LayoutView { 89 | LayoutView { 90 | above: &self.match_list.above, 91 | below: &self.match_list.below, 92 | } 93 | } 94 | 95 | #[allow(unused)] 96 | fn debug_items(&self) { 97 | for item in self.match_list.nucleo.snapshot().matched_items(..).rev() { 98 | println!("* * * * * *\n{}", item.data); 99 | } 100 | } 101 | 102 | #[allow(unused)] 103 | fn debug_items_rev(&self) { 104 | for item in self.match_list.nucleo.snapshot().matched_items(..) { 105 | println!("* * * * * *\n{}", item.data); 106 | } 107 | } 108 | } 109 | 110 | macro_rules! assert_layout { 111 | ($lt:ident, $op:expr_2021, $below:expr_2021, $above:expr_2021) => { 112 | $lt.update($op); 113 | assert_eq!( 114 | $lt.view(), 115 | LayoutView { 116 | below: $below, 117 | above: $above 118 | } 119 | ); 120 | }; 121 | } 122 | 123 | #[test] 124 | fn basic() { 125 | let mut lt = MatchListTester::init(6, 2); 126 | assert_layout!(lt, Update(&["12\n34", "ab"]), &[2], &[1]); 127 | assert_layout!(lt, Incr(1), &[1, 2], &[]); 128 | assert_layout!(lt, Reset, &[2], &[1]); 129 | assert_layout!(lt, Incr(1), &[1, 2], &[]); 130 | assert_layout!(lt, Incr(1), &[1, 2], &[]); 131 | 132 | let mut lt = MatchListTester::init_rev(6, 2); 133 | assert_layout!(lt, Update(&["12\n34", "ab"]), &[2, 1], &[]); 134 | assert_layout!(lt, Incr(1), &[1], &[2]); 135 | assert_layout!(lt, Reset, &[2, 1], &[]); 136 | assert_layout!(lt, Incr(1), &[1], &[2]); 137 | assert_layout!(lt, Incr(1), &[1], &[2]); 138 | } 139 | 140 | #[test] 141 | fn size_and_item_edge_cases() { 142 | let mut lt = MatchListTester::init(6, 2); 143 | assert_layout!(lt, Incr(1), &[], &[]); 144 | assert_layout!(lt, Decr(1), &[], &[]); 145 | assert_layout!(lt, Update(&[]), &[], &[]); 146 | assert_layout!(lt, Resize(0), &[], &[]); 147 | assert_layout!(lt, Resize(1), &[], &[]); 148 | assert_layout!(lt, Update(&["a"]), &[1], &[]); 149 | assert_layout!(lt, Resize(0), &[], &[]); 150 | 151 | let mut lt = MatchListTester::init_rev(6, 2); 152 | assert_layout!(lt, Incr(1), &[], &[]); 153 | assert_layout!(lt, Decr(1), &[], &[]); 154 | assert_layout!(lt, Update(&[]), &[], &[]); 155 | assert_layout!(lt, Resize(0), &[], &[]); 156 | assert_layout!(lt, Resize(1), &[], &[]); 157 | assert_layout!(lt, Update(&["a"]), &[1], &[]); 158 | assert_layout!(lt, Resize(0), &[], &[]); 159 | } 160 | 161 | #[test] 162 | fn small() { 163 | let mut lt = MatchListTester::init(5, 1); 164 | assert_layout!(lt, Update(&["12", "a\nb"]), &[1], &[2]); 165 | assert_layout!(lt, Incr(1), &[2, 1], &[]); 166 | assert_layout!(lt, Decr(1), &[1], &[2]); 167 | 168 | let mut lt = MatchListTester::init_rev(5, 1); 169 | assert_layout!(lt, Update(&["12", "a\nb"]), &[1, 2], &[]); 170 | assert_layout!(lt, Incr(1), &[2], &[1]); 171 | assert_layout!(lt, Decr(1), &[1, 2], &[]); 172 | } 173 | 174 | #[test] 175 | fn item_change() { 176 | let mut lt = MatchListTester::init(4, 1); 177 | assert_layout!( 178 | lt, 179 | Update(&["0\n1\n2\n3\n4\n5", "0\n1", "0\n1", "0\n1\n2\n3"]), 180 | &[3], 181 | &[1] 182 | ); 183 | assert_layout!(lt, Incr(1), &[2, 1], &[1]); 184 | assert_layout!(lt, Update(&["0\n0", "1"]), &[1, 2], &[]); 185 | assert_layout!(lt, Update(&["0\n0", "1\n1"]), &[2, 1], &[]); 186 | assert_layout!(lt, Update(&["0"]), &[1], &[]); 187 | assert_layout!(lt, Update(&["0\n0\n0\n0", "1", "2", "3"]), &[3], &[1]); 188 | assert_layout!(lt, Incr(1), &[1, 2], &[1]); 189 | assert_layout!(lt, Update(&["0", "1", "2", "3"]), &[1, 1], &[1, 1]); 190 | assert_layout!(lt, Update(&[]), &[], &[]); 191 | 192 | let mut lt = MatchListTester::init_rev(4, 1); 193 | assert_layout!( 194 | lt, 195 | Update(&["0\n1\n2\n3\n4\n5", "0\n1", "0\n1", "0\n1\n2\n3"]), 196 | &[4], 197 | &[] 198 | ); 199 | assert_layout!(lt, Incr(1), &[2], &[2]); 200 | assert_layout!(lt, Update(&["0\n0", "1"]), &[1], &[2]); 201 | assert_layout!(lt, Update(&["0\n0", "1\n1"]), &[2], &[2]); 202 | assert_layout!(lt, Update(&["0"]), &[1], &[]); 203 | assert_layout!(lt, Update(&["0\n0\n0\n0", "1", "2", "3"]), &[4], &[]); 204 | assert_layout!(lt, Incr(1), &[1, 1], &[2]); 205 | assert_layout!(lt, Update(&["0", "1", "2", "3"]), &[1, 1, 1], &[1]); 206 | assert_layout!(lt, Update(&[]), &[], &[]); 207 | } 208 | 209 | #[test] 210 | fn rev_incl_selection() { 211 | let mut lt = MatchListTester::init_rev(5, 1); 212 | assert_layout!( 213 | lt, 214 | Update(&["0", "1", "2", "3", "4", "5\n5\n5"]), 215 | &[1, 1, 1, 1, 1], 216 | &[] 217 | ); 218 | assert_layout!(lt, Incr(4), &[1, 1], &[1, 1, 1]); 219 | assert_layout!(lt, Incr(1), &[3], &[1, 1]); 220 | } 221 | 222 | #[test] 223 | fn resize_basic() { 224 | let mut lt = MatchListTester::init(5, 1); 225 | assert_layout!( 226 | lt, 227 | Update(&["0", "1", "2", "3", "4", "5"]), 228 | &[1], 229 | &[1, 1, 1, 1] 230 | ); 231 | assert_layout!(lt, Incr(5), &[1, 1, 1, 1], &[]); 232 | assert_layout!(lt, Resize(3), &[1, 1], &[]); 233 | assert_layout!(lt, Resize(5), &[1, 1, 1, 1], &[]); 234 | 235 | let mut lt = MatchListTester::init_rev(5, 1); 236 | assert_layout!( 237 | lt, 238 | Update(&["0", "1", "2", "3", "4", "5"]), 239 | &[1, 1, 1, 1, 1], 240 | &[] 241 | ); 242 | assert_layout!(lt, Incr(5), &[1], &[1, 1, 1]); 243 | assert_layout!(lt, Resize(3), &[1], &[1]); 244 | } 245 | 246 | #[test] 247 | fn resize_with_padding_change() { 248 | let mut lt = MatchListTester::init(10, 2); 249 | assert_layout!( 250 | lt, 251 | Update(&["0\n0\n0", "1", "2\n2", "3\n3\n3\n3"]), 252 | &[3], 253 | &[1, 2, 4] 254 | ); 255 | assert_layout!(lt, Resize(4), &[3], &[1]); 256 | assert_layout!(lt, Incr(2), &[2, 1], &[1]); 257 | assert_layout!(lt, Resize(8), &[2, 1, 3], &[2]); 258 | assert_layout!(lt, Resize(4), &[2, 1], &[1]); 259 | 260 | let mut lt = MatchListTester::init_rev(10, 2); 261 | assert_layout!( 262 | lt, 263 | Update(&["0\n0\n0", "1", "2\n2", "3\n3\n3\n3"]), 264 | &[3, 1, 2, 4], 265 | &[] 266 | ); 267 | assert_layout!(lt, Resize(4), &[3, 1], &[]); 268 | assert_layout!(lt, Incr(2), &[2], &[1, 1]); 269 | assert_layout!(lt, Resize(8), &[2, 2], &[1, 3]); 270 | assert_layout!(lt, Resize(4), &[2], &[1, 1]); 271 | } 272 | 273 | #[test] 274 | fn item_alignment() { 275 | let mut lt = MatchListTester::init(20, 3); 276 | assert_layout!(lt, Update(&["0\n1\n2\n", "0\n1", "0\n1"]), &[4], &[2, 2]); 277 | assert_layout!(lt, Incr(2), &[2, 2, 4], &[]); 278 | assert_layout!(lt, Decr(1), &[2, 4], &[2]); 279 | 280 | let mut lt = MatchListTester::init_rev(20, 3); 281 | assert_layout!(lt, Update(&["0\n1\n2\n", "0\n1", "0\n1"]), &[4, 2, 2], &[]); 282 | assert_layout!(lt, Incr(2), &[2], &[2, 4]); 283 | assert_layout!(lt, Decr(1), &[2, 2], &[4]); 284 | } 285 | 286 | #[test] 287 | fn scrolldown() { 288 | let mut lt = MatchListTester::init(8, 2); 289 | assert_layout!( 290 | lt, 291 | Update(&["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]), 292 | &[1], 293 | &[1, 1, 1, 1, 1, 1, 1] 294 | ); 295 | assert_layout!(lt, Incr(100), &[1, 1, 1, 1, 1, 1], &[]); 296 | assert_layout!(lt, Decr(1), &[1, 1, 1, 1, 1], &[1]); 297 | assert_layout!(lt, Decr(1), &[1, 1, 1, 1], &[1, 1]); 298 | assert_layout!(lt, Decr(1), &[1, 1, 1], &[1, 1, 1]); 299 | assert_layout!(lt, Decr(1), &[1, 1, 1], &[1, 1, 1, 1]); 300 | assert_layout!(lt, Decr(1), &[1, 1, 1], &[1, 1, 1, 1, 1]); 301 | assert_layout!(lt, Decr(3), &[1, 1, 1], &[1, 1, 1, 1, 1]); 302 | assert_layout!(lt, Decr(1), &[1, 1], &[1, 1, 1, 1, 1, 1]); 303 | assert_layout!(lt, Decr(1), &[1], &[1, 1, 1, 1, 1, 1, 1]); 304 | assert_layout!(lt, Decr(1), &[1], &[1, 1, 1, 1, 1, 1, 1]); 305 | 306 | let mut lt = MatchListTester::init_rev(8, 2); 307 | assert_layout!( 308 | lt, 309 | Update(&["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]), 310 | &[1, 1, 1, 1, 1, 1, 1, 1], 311 | &[] 312 | ); 313 | assert_layout!(lt, Incr(100), &[1], &[1, 1, 1, 1, 1]); 314 | assert_layout!(lt, Decr(1), &[1, 1], &[1, 1, 1, 1]); 315 | assert_layout!(lt, Decr(1), &[1, 1, 1], &[1, 1, 1]); 316 | assert_layout!(lt, Decr(1), &[1, 1, 1, 1], &[1, 1]); 317 | assert_layout!(lt, Decr(1), &[1, 1, 1, 1, 1], &[1, 1]); 318 | assert_layout!(lt, Decr(1), &[1, 1, 1, 1, 1, 1], &[1, 1]); 319 | assert_layout!(lt, Decr(3), &[1, 1, 1, 1, 1, 1], &[1, 1]); 320 | assert_layout!(lt, Decr(1), &[1, 1, 1, 1, 1, 1, 1], &[1]); 321 | assert_layout!(lt, Decr(1), &[1, 1, 1, 1, 1, 1, 1, 1], &[]); 322 | } 323 | 324 | #[test] 325 | fn scrollback() { 326 | let mut lt = MatchListTester::init(5, 1); 327 | assert_layout!( 328 | lt, 329 | Update(&["12\n34", "ab", "c", "d", "e", "f\ng"]), 330 | &[2], 331 | &[1, 1, 1] 332 | ); 333 | assert_layout!(lt, Incr(3), &[1, 1, 1, 1], &[1]); 334 | assert_layout!(lt, Incr(1), &[1, 1, 1, 1], &[1]); 335 | assert_layout!(lt, Incr(1), &[2, 1, 1], &[]); 336 | assert_layout!(lt, Decr(1), &[1, 1], &[2]); 337 | assert_layout!(lt, Incr(1), &[2, 1, 1], &[]); 338 | assert_layout!(lt, Decr(2), &[1, 1], &[1, 2]); 339 | assert_layout!(lt, Decr(1), &[1, 1], &[1, 1, 1]); 340 | assert_layout!(lt, Decr(1), &[1, 1], &[1, 1, 1]); 341 | assert_layout!(lt, Decr(1), &[2], &[1, 1, 1]); 342 | 343 | let mut lt = MatchListTester::init_rev(5, 1); 344 | assert_layout!( 345 | lt, 346 | Update(&["12\n34", "ab", "c", "d", "e", "f\ng"]), 347 | &[2, 1, 1, 1], 348 | &[] 349 | ); 350 | assert_layout!(lt, Incr(3), &[1, 1], &[1, 1, 1]); 351 | assert_layout!(lt, Incr(1), &[1, 1], &[1, 1, 1]); 352 | assert_layout!(lt, Incr(1), &[2], &[1, 1, 1]); 353 | assert_layout!(lt, Decr(1), &[1, 2], &[1, 1]); 354 | assert_layout!(lt, Incr(1), &[2], &[1, 1, 1]); 355 | assert_layout!(lt, Decr(2), &[1, 1, 2], &[1]); 356 | assert_layout!(lt, Decr(1), &[1, 1, 1, 1], &[1]); 357 | assert_layout!(lt, Decr(1), &[1, 1, 1, 1], &[1]); 358 | assert_layout!(lt, Decr(1), &[2, 1, 1, 1], &[]); 359 | } 360 | 361 | #[test] 362 | fn multiline_jitter() { 363 | let mut lt = MatchListTester::init(12, 3); 364 | assert_layout!( 365 | lt, 366 | Update(&["a", "b", "0\n1\n2\n3", "0\n1", "0\n1"]), 367 | &[1], 368 | &[1, 4, 2, 2] 369 | ); 370 | assert_layout!(lt, Incr(1), &[1, 1], &[4, 2, 2]); 371 | assert_layout!(lt, Incr(1), &[4, 1, 1], &[2, 2]); 372 | assert_layout!(lt, Incr(1), &[2, 4, 1, 1], &[2]); 373 | assert_layout!(lt, Incr(1), &[2, 2, 4, 1], &[]); 374 | assert_layout!(lt, Decr(1), &[2, 4, 1], &[2]); 375 | assert_layout!(lt, Decr(1), &[4, 1], &[2, 2]); 376 | assert_layout!(lt, Incr(1), &[2, 4, 1], &[2]); 377 | 378 | let mut lt = MatchListTester::init_rev(10, 3); 379 | assert_layout!( 380 | lt, 381 | Update(&["a", "b", "0\n1\n2\n3", "0\n1", "0\n1"]), 382 | &[1, 1, 4, 2, 2], 383 | &[] 384 | ); 385 | assert_layout!(lt, Incr(1), &[1, 4, 2, 2], &[1]); 386 | assert_layout!(lt, Incr(1), &[4, 2, 2], &[1, 1]); 387 | assert_layout!(lt, Incr(1), &[2, 2], &[4, 1, 1]); 388 | assert_layout!(lt, Incr(1), &[2], &[2, 4]); 389 | assert_layout!(lt, Decr(1), &[2, 2], &[4]); 390 | assert_layout!(lt, Decr(1), &[4, 2, 2], &[1, 1]); 391 | assert_layout!(lt, Incr(1), &[2, 2], &[4, 1, 1]); 392 | } 393 | 394 | #[test] 395 | fn scroll_mid() { 396 | let mut lt = MatchListTester::init(5, 1); 397 | assert_layout!( 398 | lt, 399 | Update(&["0", "1", "2", "3", "4", "5", "6", "7"]), 400 | &[1], 401 | &[1, 1, 1, 1] 402 | ); 403 | 404 | assert_layout!(lt, Incr(4), &[1, 1, 1, 1], &[1]); 405 | assert_layout!(lt, Decr(2), &[1, 1], &[1, 1, 1]); 406 | assert_layout!(lt, Incr(1), &[1, 1, 1], &[1, 1]); 407 | assert_layout!(lt, Decr(1), &[1, 1], &[1, 1, 1]); 408 | assert_layout!(lt, Resize(7), &[1, 1, 1], &[1, 1, 1, 1]); 409 | assert_layout!(lt, Decr(1), &[1, 1], &[1, 1, 1, 1, 1]); 410 | assert_layout!(lt, Incr(2), &[1, 1, 1, 1], &[1, 1, 1]); 411 | assert_layout!(lt, Decr(1), &[1, 1, 1], &[1, 1, 1, 1]); 412 | assert_layout!(lt, Incr(1), &[1, 1, 1, 1], &[1, 1, 1]); 413 | assert_layout!(lt, Decr(2), &[1, 1], &[1, 1, 1, 1, 1]); 414 | assert_layout!(lt, Incr(20), &[1, 1, 1, 1, 1, 1], &[]); 415 | 416 | let mut lt = MatchListTester::init_rev(5, 1); 417 | assert_layout!( 418 | lt, 419 | Update(&["0", "1", "2", "3", "4", "5", "6", "7"]), 420 | &[1, 1, 1, 1, 1], 421 | &[] 422 | ); 423 | assert_layout!(lt, Incr(4), &[1, 1], &[1, 1, 1]); 424 | assert_layout!(lt, Decr(2), &[1, 1, 1, 1], &[1]); 425 | assert_layout!(lt, Incr(1), &[1, 1, 1], &[1, 1]); 426 | assert_layout!(lt, Decr(1), &[1, 1, 1, 1], &[1]); 427 | assert_layout!(lt, Resize(7), &[1, 1, 1, 1, 1], &[1, 1]); 428 | assert_layout!(lt, Decr(1), &[1, 1, 1, 1, 1, 1], &[1]); 429 | } 430 | -------------------------------------------------------------------------------- /src/observer.rs: -------------------------------------------------------------------------------- 1 | //! # An observer channel 2 | use parking_lot::{Condvar, Mutex}; 3 | use std::sync::{ 4 | mpsc::{RecvError, SendError, TryRecvError}, 5 | Arc, 6 | }; 7 | 8 | type Channel = Mutex<(Option, bool)>; 9 | 10 | /// The 'notify' end of the single slot channel. 11 | pub(crate) struct Notifier { 12 | inner: Arc<(Channel, Condvar)>, 13 | } 14 | 15 | #[inline] 16 | fn channel_inner(msg: Option) -> (Notifier, Observer) { 17 | let inner = Arc::new((Mutex::new((msg, true)), Condvar::new())); 18 | 19 | let observer = Observer { 20 | inner: Arc::clone(&inner), 21 | }; 22 | 23 | let notifier = Notifier { inner }; 24 | 25 | (notifier, observer) 26 | } 27 | 28 | pub(crate) fn occupied_channel(msg: T) -> (Notifier, Observer) { 29 | channel_inner(Some(msg)) 30 | } 31 | 32 | pub(crate) fn channel() -> (Notifier, Observer) { 33 | channel_inner(None) 34 | } 35 | 36 | impl Notifier { 37 | /// Push a message to the channel. This overwrites any pre-existing message already in the 38 | /// channel. 39 | pub fn push(&self, msg: T) -> Result<(), SendError> { 40 | if Arc::strong_count(&self.inner) == 1 { 41 | // there are no senders so the channel is disconnected 42 | Err(SendError(msg)) 43 | } else { 44 | // overwrite the channel with the new message and notify an observer that a message 45 | // is avaliable 46 | let (lock, cvar) = &*self.inner; 47 | let mut channel = lock.lock(); 48 | channel.0 = Some(msg); 49 | cvar.notify_one(); 50 | Ok(()) 51 | } 52 | } 53 | } 54 | 55 | impl Drop for Notifier { 56 | fn drop(&mut self) { 57 | // when we drop the notifier, we need to inform all observers that are potentially waiting 58 | // for a message that the channel is closed 59 | let (lock, cvar) = &*self.inner; 60 | 61 | let mut channel = lock.lock(); 62 | channel.1 = false; 63 | cvar.notify_all(); 64 | } 65 | } 66 | 67 | /// An `Observer` watching for a single message `T`. 68 | /// 69 | /// This is similar to the 'receiver' end of a channel of length 1, but instead of blocking, the 70 | /// 'sender' always overwrites any element in the channel. In particular, any message obtain by 71 | /// [`recv`](Observer::recv) or [`try_recv`](Observer::try_recv) is guaranteed to be the most 72 | /// up-to-date at the moment when the message is received. 73 | /// 74 | /// The channel may be updated when not observed. Receiving a message moves it out of the observer. 75 | pub struct Observer { 76 | inner: Arc<(Channel, Condvar)>, 77 | } 78 | 79 | impl Clone for Observer { 80 | fn clone(&self) -> Self { 81 | Self { 82 | inner: Arc::clone(&self.inner), 83 | } 84 | } 85 | } 86 | 87 | impl Observer { 88 | /// Receive a message, blocking until a message is available or the channel 89 | /// disconnects. 90 | pub fn recv(&self) -> Result { 91 | let (lock, cvar) = &*self.inner; 92 | let mut channel = lock.lock(); 93 | match channel.0.take() { 94 | Some(msg) => Ok(msg), 95 | None => { 96 | if channel.1 { 97 | // the channel is active, so we wait for a notification 98 | cvar.wait(&mut channel); 99 | 100 | // we received a notification that there was a change 101 | match channel.0.take() { 102 | // the change was that a new message has been pushed, so we can return it 103 | Some(msg) => Ok(msg), 104 | // there is no message despite the notification, so the channel is 105 | // disconnected. this path is followed if the notifier is dropped while we 106 | // are waiting for a new message 107 | None => Err(RecvError), 108 | } 109 | } else { 110 | Err(RecvError) 111 | } 112 | } 113 | } 114 | } 115 | 116 | /// Optimistically receive a message if one is available without blocking the current thread. 117 | /// 118 | /// This operation will fail if there is no message or if there are are no remaining senders. 119 | pub fn try_recv(&self) -> Result { 120 | let (lock, _) = &*self.inner; 121 | let mut channel = lock.lock(); 122 | channel.0.take().ok_or(if channel.1 { 123 | TryRecvError::Empty 124 | } else { 125 | TryRecvError::Disconnected 126 | }) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/prompt.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests; 3 | 4 | use unicode_segmentation::UnicodeSegmentation; 5 | use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; 6 | 7 | use crate::{ 8 | component::{Component, Status}, 9 | util::as_u16, 10 | }; 11 | 12 | trait Cursor { 13 | fn right(self, s: &str, steps: usize) -> Self; 14 | fn right_word(self, s: &str, steps: usize) -> Self; 15 | fn left(self, s: &str, steps: usize) -> Self; 16 | fn left_word(self, s: &str, steps: usize) -> Self; 17 | } 18 | 19 | impl Cursor for usize { 20 | fn right(self, s: &str, steps: usize) -> Self { 21 | match s[self..].grapheme_indices(true).nth(steps) { 22 | Some((offset, _)) => self + offset, 23 | None => s.len(), 24 | } 25 | } 26 | 27 | fn right_word(self, s: &str, steps: usize) -> Self { 28 | match s[self..].unicode_word_indices().nth(steps) { 29 | Some((offset, _)) => self + offset, 30 | None => s.len(), 31 | } 32 | } 33 | 34 | fn left(self, s: &str, steps: usize) -> Self { 35 | match s[..self].grapheme_indices(true).rev().take(steps).last() { 36 | Some((offset, _)) => offset, 37 | None => 0, 38 | } 39 | } 40 | 41 | fn left_word(self, s: &str, steps: usize) -> Self { 42 | match s[..self].unicode_word_indices().rev().take(steps).last() { 43 | Some((offset, _)) => offset, 44 | None => 0, 45 | } 46 | } 47 | } 48 | 49 | /// Mutate a given string in-place, removing ASCII control characters and converting newlines, 50 | /// carriage returns, and TABs to ASCII space. 51 | pub fn normalize_prompt_string(s: &mut String) { 52 | *s = s 53 | .chars() 54 | .filter_map(normalize_char) 55 | .map(|(ch, _)| ch) 56 | .collect(); 57 | } 58 | 59 | /// Normalize a single char, returning the resulting char as well as the width. 60 | /// 61 | /// This automaticlly removes control characters since `ch.width()` returns `None` for control 62 | /// characters. 63 | #[inline] 64 | fn normalize_char(ch: char) -> Option<(char, usize)> { 65 | match ch { 66 | '\n' | '\t' => Some((' ', 1)), 67 | ch => ch.width().map(|w| (ch, w)), 68 | } 69 | } 70 | 71 | /// An event that modifies the prompt. 72 | #[derive(Debug, PartialEq, Eq)] 73 | #[non_exhaustive] 74 | pub enum PromptEvent { 75 | /// Move the cursor `usize` graphemes to the left. 76 | Left(usize), 77 | /// Move the cursor `usize` Unicode words to the left. 78 | WordLeft(usize), 79 | /// Move the cursor `usize` graphemes to the right. 80 | Right(usize), 81 | /// Move the cursor `usize` Unicode words to the right. 82 | WordRight(usize), 83 | /// Move the cursor to the start. 84 | ToStart, 85 | /// Move the cursor to the end. 86 | ToEnd, 87 | /// Delete `usize` graphemes immediately preceding the cursor. 88 | Backspace(usize), 89 | /// Delete `usize` graphemes immediately following the cursor. 90 | Delete(usize), 91 | /// Delete `usize` Unicode words immediately preceding the cursor. 92 | BackspaceWord(usize), 93 | /// Clear everything before the cursor. 94 | ClearBefore, 95 | /// Clear everything after the cursor. 96 | ClearAfter, 97 | /// Insert a character at the cursor. 98 | Insert(char), 99 | /// Paste a string at the cursor. 100 | Paste(String), 101 | /// Reset the prompt to a new string and move the cursor to the end. 102 | Reset(String), 103 | } 104 | 105 | impl PromptEvent { 106 | /// Whether or not the event is a cursor movement that does not edit the prompt string. 107 | pub fn is_cursor_movement(&self) -> bool { 108 | matches!( 109 | &self, 110 | PromptEvent::Left(_) 111 | | PromptEvent::WordLeft(_) 112 | | PromptEvent::Right(_) 113 | | PromptEvent::WordRight(_) 114 | | PromptEvent::ToStart 115 | | PromptEvent::ToEnd 116 | ) 117 | } 118 | } 119 | 120 | /// A movement to apply to an [`Prompt`]. 121 | #[derive(Debug, PartialEq, Eq)] 122 | enum CursorMovement { 123 | /// Move the cursor left. 124 | Left(usize), 125 | /// Move the cursor left an entire word. 126 | WordLeft(usize), 127 | /// Move the cursor right. 128 | Right(usize), 129 | /// Move the cursor right an entire word. 130 | WordRight(usize), 131 | /// Move the cursor to the start. 132 | ToStart, 133 | /// Move the cursor to the end. 134 | ToEnd, 135 | } 136 | 137 | #[derive(Debug, Clone)] 138 | pub struct PromptConfig { 139 | pub padding: u16, 140 | } 141 | 142 | impl Default for PromptConfig { 143 | fn default() -> Self { 144 | Self { padding: 2 } 145 | } 146 | } 147 | 148 | #[derive(Debug)] 149 | pub struct Prompt { 150 | contents: String, 151 | offset: usize, 152 | screen_offset: u16, 153 | width: u16, 154 | config: PromptConfig, 155 | } 156 | 157 | impl Prompt { 158 | /// Create a new editable string with initial screen width and maximum padding. 159 | pub fn new(config: PromptConfig) -> Self { 160 | Self { 161 | contents: String::new(), 162 | offset: 0, 163 | screen_offset: 0, 164 | width: u16::MAX, 165 | config, 166 | } 167 | } 168 | 169 | pub fn padding(&self) -> u16 { 170 | self.config.padding.min(self.width.saturating_sub(1) / 2) 171 | } 172 | 173 | /// Whether or not the prompt is empty. 174 | pub fn is_empty(&self) -> bool { 175 | self.contents.is_empty() 176 | } 177 | 178 | /// Return the prompt contents as well as an 'offset' which is required in the presence of an 179 | /// initial grapheme that is too large to fit at the beginning of the screen. 180 | pub fn view(&self) -> (&str, u16) { 181 | if self.width == 0 { 182 | return ("", 0); 183 | } 184 | 185 | let mut left_indices = self.contents[..self.offset].grapheme_indices(true).rev(); 186 | let mut total_left_width = 0; 187 | let (left_offset, extra) = loop { 188 | match left_indices.next() { 189 | Some((offset, grapheme)) => { 190 | total_left_width += grapheme.width(); 191 | if total_left_width >= self.screen_offset.into() { 192 | let extra = (total_left_width - self.screen_offset as usize) as u16; 193 | break ( 194 | offset 195 | + if total_left_width == usize::from(self.screen_offset) { 196 | 0 197 | } else { 198 | grapheme.len() 199 | }, 200 | extra, 201 | ); 202 | } 203 | } 204 | None => break (0, 0), 205 | } 206 | }; 207 | 208 | let mut right_indices = self.contents[self.offset..].grapheme_indices(true); 209 | let mut total_right_width = 0; 210 | let max_right_width = self.width - self.screen_offset; 211 | let right_offset = loop { 212 | match right_indices.next() { 213 | Some((offset, grapheme)) => { 214 | total_right_width += grapheme.width(); 215 | if total_right_width > max_right_width as usize { 216 | break self.offset + offset; 217 | } 218 | } 219 | None => break self.contents.len(), 220 | } 221 | }; 222 | 223 | (&self.contents[left_offset..right_offset], extra) 224 | } 225 | 226 | /// Resize the screen, adjusting the padding and the screen width. 227 | pub fn resize(&mut self, width: u16) { 228 | // TODO: this is not really correct, since it does not handle width 0 correctly. 229 | // but in practice, for the prompt this is quite rare; but should fix it at some point 230 | // 231 | // to witness in tests, set the width to 0 and then to some large value and the screen 232 | // offset will be incorrect 233 | // 234 | // this is also the reason that the prompt defaults to `u16::MAX`; and this should be fixed 235 | // as well when this is fixed. 236 | self.width = width; 237 | self.screen_offset = self.screen_offset.min(width - self.padding()); 238 | } 239 | 240 | /// Get the cursor offset within the screen. 241 | pub fn screen_offset(&self) -> u16 { 242 | self.screen_offset 243 | } 244 | 245 | /// Get the contents of the prompt. 246 | pub fn contents(&self) -> &str { 247 | &self.contents 248 | } 249 | 250 | /// Reset the prompt, moving the cursor to the end. 251 | pub fn set_query>(&mut self, prompt: Q) { 252 | self.contents = prompt.into(); 253 | normalize_prompt_string(&mut self.contents); 254 | self.offset = self.contents.len(); 255 | self.screen_offset = as_u16(self.contents.width()).min(self.width - self.padding()); 256 | } 257 | 258 | /// Increase the screen offset by the provided width, without exceeding the maximum offset. 259 | fn right_by(&mut self, width: usize) { 260 | self.screen_offset = self 261 | .screen_offset 262 | .saturating_add(as_u16(width)) 263 | .min(self.width - self.padding()); 264 | } 265 | 266 | /// Insert a character at the cursor position. 267 | fn insert_char(&mut self, ch: char, w: usize) { 268 | self.contents.insert(self.offset, ch); 269 | self.right_by(w); 270 | self.offset += ch.len_utf8(); 271 | } 272 | 273 | /// Insert a string at the cursor position. 274 | fn insert(&mut self, string: &str) { 275 | self.contents.insert_str(self.offset, string); 276 | self.right_by(string.width()); 277 | self.offset += string.len(); 278 | } 279 | 280 | #[inline] 281 | fn left_by(&mut self, width: usize) { 282 | // check if we would hit the beginning of the string 283 | let mut total_left_width = 0; 284 | let mut graphemes = self.contents[..self.offset].graphemes(true).rev(); 285 | let left_padding = loop { 286 | match graphemes.next() { 287 | Some(g) => { 288 | total_left_width += g.width(); 289 | let left_padding = self.padding(); 290 | if total_left_width >= left_padding as usize { 291 | break left_padding; 292 | } 293 | } 294 | None => { 295 | break total_left_width as u16; 296 | } 297 | } 298 | }; 299 | 300 | self.screen_offset = self 301 | .screen_offset 302 | .saturating_sub(as_u16(width)) 303 | .max(left_padding); 304 | } 305 | 306 | /// Move the cursor. 307 | #[inline] 308 | #[allow(clippy::needless_pass_by_value)] 309 | fn move_cursor(&mut self, cm: CursorMovement) -> bool { 310 | match cm { 311 | CursorMovement::Left(n) => { 312 | let new_offset = self.offset.left(&self.contents, n); 313 | if new_offset != self.offset { 314 | let step_width = self.contents[new_offset..self.offset].width(); 315 | self.offset = new_offset; 316 | self.left_by(step_width); 317 | true 318 | } else { 319 | false 320 | } 321 | } 322 | CursorMovement::WordLeft(n) => { 323 | let new_offset = self.offset.left_word(&self.contents, n); 324 | if new_offset != self.offset { 325 | let step_width = self.contents[new_offset..self.offset].width(); 326 | self.offset = new_offset; 327 | self.left_by(step_width); 328 | true 329 | } else { 330 | false 331 | } 332 | } 333 | CursorMovement::Right(n) => { 334 | let new_offset = self.offset.right(&self.contents, n); 335 | if new_offset != self.offset { 336 | let step_width = self.contents[self.offset..new_offset].width(); 337 | self.offset = new_offset; 338 | self.right_by(step_width); 339 | true 340 | } else { 341 | false 342 | } 343 | } 344 | CursorMovement::WordRight(n) => { 345 | let new_offset = self.offset.right_word(&self.contents, n); 346 | if new_offset != self.offset { 347 | let step_width = self.contents[self.offset..new_offset].width(); 348 | self.offset = new_offset; 349 | self.right_by(step_width); 350 | true 351 | } else { 352 | false 353 | } 354 | } 355 | CursorMovement::ToStart => { 356 | if self.offset == 0 { 357 | false 358 | } else { 359 | self.offset = 0; 360 | self.screen_offset = 0; 361 | true 362 | } 363 | } 364 | CursorMovement::ToEnd => { 365 | if self.offset == self.contents.len() { 366 | false 367 | } else { 368 | let max_offset = self.width - self.padding(); 369 | for gp in self.contents[self.offset..].graphemes(true) { 370 | self.screen_offset = self 371 | .screen_offset 372 | .saturating_add(gp.width().try_into().unwrap_or(u16::MAX)); 373 | if self.screen_offset >= max_offset { 374 | self.screen_offset = max_offset; 375 | break; 376 | } 377 | } 378 | self.offset = self.contents.len(); 379 | true 380 | } 381 | } 382 | } 383 | } 384 | } 385 | 386 | #[derive(Debug, Clone, Copy, Default)] 387 | pub struct PromptStatus { 388 | pub needs_redraw: bool, 389 | pub contents_changed: bool, 390 | } 391 | 392 | impl Status for PromptStatus { 393 | fn needs_redraw(&self) -> bool { 394 | self.needs_redraw 395 | } 396 | } 397 | 398 | impl std::ops::BitOrAssign for PromptStatus { 399 | fn bitor_assign(&mut self, rhs: Self) { 400 | self.needs_redraw |= rhs.needs_redraw; 401 | self.contents_changed |= rhs.contents_changed; 402 | } 403 | } 404 | 405 | impl Component for Prompt { 406 | type Event = PromptEvent; 407 | 408 | type Status = PromptStatus; 409 | 410 | fn handle(&mut self, e: Self::Event) -> Self::Status { 411 | let mut contents_changed = false; 412 | 413 | let needs_redraw = match e { 414 | PromptEvent::Reset(s) => { 415 | self.set_query(s); 416 | true 417 | } 418 | PromptEvent::Left(n) => self.move_cursor(CursorMovement::Left(n)), 419 | PromptEvent::WordLeft(n) => self.move_cursor(CursorMovement::WordLeft(n)), 420 | PromptEvent::Right(n) => self.move_cursor(CursorMovement::Right(n)), 421 | PromptEvent::WordRight(n) => self.move_cursor(CursorMovement::WordRight(n)), 422 | PromptEvent::ToStart => self.move_cursor(CursorMovement::ToStart), 423 | PromptEvent::ToEnd => self.move_cursor(CursorMovement::ToEnd), 424 | PromptEvent::Insert(ch) => { 425 | if let Some((ch, w)) = normalize_char(ch) { 426 | contents_changed = true; 427 | self.insert_char(ch, w); 428 | true 429 | } else { 430 | false 431 | } 432 | } 433 | PromptEvent::Paste(mut s) => { 434 | normalize_prompt_string(&mut s); 435 | if !s.is_empty() { 436 | contents_changed = true; 437 | self.insert(&s); 438 | true 439 | } else { 440 | false 441 | } 442 | } 443 | PromptEvent::Backspace(n) => { 444 | let delete_until = self.offset; 445 | if self.move_cursor(CursorMovement::Left(n)) { 446 | self.contents.replace_range(self.offset..delete_until, ""); 447 | contents_changed = true; 448 | true 449 | } else { 450 | false 451 | } 452 | } 453 | PromptEvent::BackspaceWord(n) => { 454 | let delete_until = self.offset; 455 | if self.move_cursor(CursorMovement::WordLeft(n)) { 456 | self.contents.replace_range(self.offset..delete_until, ""); 457 | contents_changed = true; 458 | true 459 | } else { 460 | false 461 | } 462 | } 463 | PromptEvent::ClearBefore => { 464 | if self.offset == 0 { 465 | false 466 | } else { 467 | self.contents.replace_range(..self.offset, ""); 468 | self.offset = 0; 469 | self.screen_offset = 0; 470 | contents_changed = true; 471 | true 472 | } 473 | } 474 | PromptEvent::Delete(n) => { 475 | let new_offset = self.offset.right(&self.contents, n); 476 | if new_offset != self.offset { 477 | self.contents.replace_range(self.offset..new_offset, ""); 478 | contents_changed = true; 479 | true 480 | } else { 481 | false 482 | } 483 | } 484 | PromptEvent::ClearAfter => { 485 | if self.offset == self.contents.len() { 486 | false 487 | } else { 488 | self.contents.truncate(self.offset); 489 | contents_changed = true; 490 | true 491 | } 492 | } 493 | }; 494 | 495 | Self::Status { 496 | needs_redraw, 497 | contents_changed, 498 | } 499 | } 500 | 501 | fn draw( 502 | &mut self, 503 | width: u16, 504 | _height: u16, 505 | writer: &mut W, 506 | ) -> std::io::Result<()> { 507 | use crossterm::{ 508 | cursor::MoveRight, 509 | style::Print, 510 | terminal::{Clear, ClearType}, 511 | QueueableCommand, 512 | }; 513 | 514 | writer.queue(Print("> "))?; 515 | 516 | if let Some(width) = width.checked_sub(2) { 517 | if width != self.width { 518 | self.resize(width); 519 | } 520 | 521 | let (contents, shift) = self.view(); 522 | 523 | if shift != 0 { 524 | writer.queue(MoveRight(shift))?; 525 | } 526 | 527 | writer 528 | .queue(Print(contents))? 529 | .queue(Clear(ClearType::UntilNewLine))?; 530 | } 531 | 532 | Ok(()) 533 | } 534 | } 535 | -------------------------------------------------------------------------------- /src/prompt/tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | fn init_prompt(width: u16, padding: u16) -> Prompt { 4 | let mut cfg = PromptConfig::default(); 5 | cfg.padding = padding; 6 | let mut prompt = Prompt::new(cfg); 7 | prompt.resize(width); 8 | prompt 9 | } 10 | 11 | #[test] 12 | fn layout() { 13 | let mut editable = init_prompt(6, 2); 14 | editable.handle(PromptEvent::Insert('a')); 15 | assert_eq!(editable.screen_offset, 1); 16 | editable.handle(PromptEvent::Insert('A')); 17 | assert_eq!(editable.screen_offset, 3); 18 | editable.handle(PromptEvent::Insert('B')); 19 | assert_eq!(editable.screen_offset, 4); 20 | 21 | let mut editable = init_prompt(6, 2); 22 | editable.handle(PromptEvent::Paste("AaA".to_owned())); 23 | assert_eq!(editable.screen_offset, 4); 24 | 25 | let mut editable = init_prompt(6, 2); 26 | editable.handle(PromptEvent::Paste("abc".to_owned())); 27 | assert_eq!(editable.screen_offset, 3); 28 | editable.handle(PromptEvent::Paste("ab".to_owned())); 29 | assert_eq!(editable.screen_offset, 4); 30 | editable.handle(PromptEvent::Left(1)); 31 | assert_eq!(editable.screen_offset, 3); 32 | editable.handle(PromptEvent::Left(1)); 33 | assert_eq!(editable.screen_offset, 2); 34 | editable.handle(PromptEvent::Left(1)); 35 | assert_eq!(editable.screen_offset, 2); 36 | editable.handle(PromptEvent::Left(1)); 37 | assert_eq!(editable.screen_offset, 1); 38 | editable.handle(PromptEvent::Left(1)); 39 | assert_eq!(editable.screen_offset, 0); 40 | 41 | let mut editable = init_prompt(7, 2); 42 | editable.handle(PromptEvent::Paste("AAAAA".to_owned())); 43 | editable.handle(PromptEvent::ToStart); 44 | assert_eq!(editable.screen_offset, 0); 45 | editable.handle(PromptEvent::Right(1)); 46 | assert_eq!(editable.screen_offset, 2); 47 | editable.handle(PromptEvent::Right(1)); 48 | assert_eq!(editable.screen_offset, 4); 49 | editable.handle(PromptEvent::Right(1)); 50 | assert_eq!(editable.screen_offset, 5); 51 | editable.handle(PromptEvent::Right(1)); 52 | assert_eq!(editable.screen_offset, 5); 53 | editable.handle(PromptEvent::Left(1)); 54 | assert_eq!(editable.screen_offset, 3); 55 | editable.handle(PromptEvent::Left(1)); 56 | assert_eq!(editable.screen_offset, 2); 57 | editable.handle(PromptEvent::Left(1)); 58 | assert_eq!(editable.screen_offset, 2); 59 | editable.handle(PromptEvent::Left(1)); 60 | assert_eq!(editable.screen_offset, 0); 61 | 62 | let mut editable = init_prompt(7, 2); 63 | editable.handle(PromptEvent::Paste("abc".to_owned())); 64 | editable.handle(PromptEvent::ToStart); 65 | editable.handle(PromptEvent::ToEnd); 66 | assert_eq!(editable.screen_offset, 3); 67 | editable.handle(PromptEvent::Paste("defghi".to_owned())); 68 | editable.handle(PromptEvent::ToStart); 69 | editable.handle(PromptEvent::ToEnd); 70 | assert_eq!(editable.screen_offset, 5); 71 | } 72 | 73 | #[test] 74 | fn view() { 75 | let mut editable = init_prompt(7, 2); 76 | editable.handle(PromptEvent::Paste("abc".to_owned())); 77 | assert_eq!(editable.view(), ("abc", 0)); 78 | 79 | let mut editable = init_prompt(6, 1); 80 | editable.handle(PromptEvent::Paste("AAAAAA".to_owned())); 81 | assert_eq!(editable.view(), ("AA", 1)); 82 | 83 | let mut editable = init_prompt(7, 2); 84 | editable.handle(PromptEvent::Paste("AAAA".to_owned())); 85 | assert_eq!(editable.view(), ("AA", 1)); 86 | editable.handle(PromptEvent::Left(1)); 87 | assert_eq!(editable.view(), ("AA", 1)); 88 | editable.handle(PromptEvent::Left(1)); 89 | assert_eq!(editable.view(), ("AAA", 0)); 90 | 91 | let mut editable = init_prompt(7, 2); 92 | editable.handle(PromptEvent::Paste("012345678".to_owned())); 93 | editable.handle(PromptEvent::ToStart); 94 | assert_eq!(editable.view(), ("0123456", 0)); 95 | 96 | let mut editable = init_prompt(7, 2); 97 | editable.handle(PromptEvent::Paste("012345A".to_owned())); 98 | editable.handle(PromptEvent::ToStart); 99 | assert_eq!(editable.view(), ("012345", 0)); 100 | 101 | let mut editable = init_prompt(4, 1); 102 | editable.handle(PromptEvent::Paste("01234567".to_owned())); 103 | assert_eq!(editable.view(), ("567", 0)); 104 | editable.handle(PromptEvent::Left(1)); 105 | assert_eq!(editable.view(), ("567", 0)); 106 | editable.handle(PromptEvent::Left(1)); 107 | assert_eq!(editable.view(), ("567", 0)); 108 | editable.handle(PromptEvent::Left(1)); 109 | assert_eq!(editable.view(), ("4567", 0)); 110 | editable.handle(PromptEvent::Left(1)); 111 | assert_eq!(editable.view(), ("3456", 0)); 112 | editable.handle(PromptEvent::Left(1)); 113 | assert_eq!(editable.view(), ("2345", 0)); 114 | editable.handle(PromptEvent::Right(1)); 115 | assert_eq!(editable.view(), ("2345", 0)); 116 | editable.handle(PromptEvent::Right(1)); 117 | assert_eq!(editable.view(), ("2345", 0)); 118 | editable.handle(PromptEvent::Right(1)); 119 | assert_eq!(editable.view(), ("3456", 0)); 120 | } 121 | 122 | #[test] 123 | fn test_word_movement() { 124 | let mut editable = init_prompt(100, 2); 125 | editable.handle(PromptEvent::Paste("one two".to_owned())); 126 | editable.handle(PromptEvent::WordLeft(1)); 127 | editable.handle(PromptEvent::WordLeft(1)); 128 | assert_eq!(editable.screen_offset, 0); 129 | editable.handle(PromptEvent::WordRight(1)); 130 | assert_eq!(editable.screen_offset, 4); 131 | editable.handle(PromptEvent::WordRight(1)); 132 | assert_eq!(editable.screen_offset, 7); 133 | editable.handle(PromptEvent::WordRight(1)); 134 | assert_eq!(editable.screen_offset, 7); 135 | } 136 | 137 | #[test] 138 | fn test_clear() { 139 | let mut editable = init_prompt(7, 2); 140 | editable.handle(PromptEvent::Paste("Abcde".to_owned())); 141 | editable.handle(PromptEvent::ToStart); 142 | editable.handle(PromptEvent::Right(1)); 143 | editable.handle(PromptEvent::Right(1)); 144 | editable.handle(PromptEvent::ClearAfter); 145 | assert_eq!(editable.contents, "Ab"); 146 | editable.handle(PromptEvent::Insert('c')); 147 | editable.handle(PromptEvent::Left(1)); 148 | editable.handle(PromptEvent::ClearBefore); 149 | assert_eq!(editable.contents, "c"); 150 | } 151 | 152 | #[test] 153 | fn test_delete() { 154 | let mut editable = init_prompt(7, 2); 155 | editable.handle(PromptEvent::Paste("Ab".to_owned())); 156 | editable.handle(PromptEvent::Backspace(1)); 157 | assert_eq!(editable.contents, "A"); 158 | assert_eq!(editable.screen_offset, 2); 159 | editable.handle(PromptEvent::Backspace(1)); 160 | assert_eq!(editable.contents, ""); 161 | assert_eq!(editable.screen_offset, 0); 162 | } 163 | 164 | #[test] 165 | fn test_normalize_prompt() { 166 | let mut s = "a\nb".to_owned(); 167 | normalize_prompt_string(&mut s); 168 | assert_eq!(s, "a b"); 169 | 170 | let mut s = "o\no".to_owned(); 171 | normalize_prompt_string(&mut s); 172 | assert_eq!(s, "o o"); 173 | 174 | let mut s = "a\n\u{07}o".to_owned(); 175 | normalize_prompt_string(&mut s); 176 | assert_eq!(s, "a o"); 177 | } 178 | 179 | #[test] 180 | fn test_editable() { 181 | let mut editable = init_prompt(3, 1); 182 | for e in [ 183 | PromptEvent::Insert('a'), 184 | PromptEvent::Left(1), 185 | PromptEvent::Insert('b'), 186 | PromptEvent::ToEnd, 187 | PromptEvent::Insert('c'), 188 | PromptEvent::ToStart, 189 | PromptEvent::Insert('d'), 190 | PromptEvent::Left(1), 191 | PromptEvent::Left(1), 192 | PromptEvent::Right(1), 193 | PromptEvent::Insert('e'), 194 | ] { 195 | editable.handle(e); 196 | } 197 | assert_eq!(editable.contents, "debac"); 198 | 199 | let mut editable = init_prompt(3, 1); 200 | for e in [ 201 | PromptEvent::Insert('a'), 202 | PromptEvent::Insert('b'), 203 | PromptEvent::Insert('c'), 204 | PromptEvent::Insert('d'), 205 | PromptEvent::Left(1), 206 | PromptEvent::Insert('1'), 207 | PromptEvent::Insert('2'), 208 | PromptEvent::Insert('3'), 209 | PromptEvent::ToStart, 210 | PromptEvent::Backspace(1), 211 | PromptEvent::Insert('4'), 212 | PromptEvent::ToEnd, 213 | PromptEvent::Backspace(1), 214 | PromptEvent::Left(1), 215 | PromptEvent::Delete(1), 216 | ] { 217 | editable.handle(e); 218 | } 219 | 220 | assert_eq!(editable.contents, "4abc12"); 221 | } 222 | 223 | #[test] 224 | fn test_editable_unicode() { 225 | let mut editable = init_prompt(3, 1); 226 | for e in [ 227 | PromptEvent::Paste("दे".to_owned()), 228 | PromptEvent::Left(1), 229 | PromptEvent::Insert('a'), 230 | PromptEvent::ToEnd, 231 | PromptEvent::Insert('A'), 232 | ] { 233 | editable.handle(e); 234 | } 235 | assert_eq!(editable.contents, "aदेA"); 236 | 237 | for e in [ 238 | PromptEvent::ToStart, 239 | PromptEvent::Right(1), 240 | PromptEvent::ToEnd, 241 | PromptEvent::Left(1), 242 | PromptEvent::Backspace(1), 243 | ] { 244 | editable.handle(e); 245 | } 246 | 247 | assert_eq!(editable.contents, "aA"); 248 | } 249 | -------------------------------------------------------------------------------- /src/render.rs: -------------------------------------------------------------------------------- 1 | //! # Renderers for use in a [`Picker`](super::Picker) 2 | //! 3 | //! This module contains built-in renderers to be used in a [`Picker`](super::Picker), and (with 4 | //! appropriate types) can be used as the arguments passed to the 5 | //! [`PickerOptions::picker`](super::PickerOptions::picker) and [`Picker::new`](super::Picker::new) 6 | //! methods. 7 | use std::{borrow::Cow, path::Path}; 8 | 9 | use super::Render; 10 | 11 | /// A renderer for any type which de-references as [`str`], such as a [`String`]. 12 | /// 13 | /// ## Example 14 | /// ``` 15 | /// # use nucleo_picker::{render::StrRenderer, Render}; 16 | /// let str_renderer = StrRenderer; 17 | /// 18 | /// let st = "Hello!".to_owned(); 19 | /// 20 | /// assert_eq!(str_renderer.render(&st), "Hello!"); 21 | /// ``` 22 | pub struct StrRenderer; 23 | 24 | impl> Render for StrRenderer { 25 | type Str<'a> 26 | = &'a str 27 | where 28 | T: 'a; 29 | 30 | fn render<'a>(&self, item: &'a T) -> Self::Str<'a> { 31 | item.as_ref() 32 | } 33 | } 34 | 35 | /// A renderer for any type which de-references as [`Path`], such as a 36 | /// [`PathBuf`](std::path::PathBuf). 37 | /// 38 | /// ## Example 39 | /// ``` 40 | /// # use nucleo_picker::{render::PathRenderer, Render}; 41 | /// use std::path::PathBuf; 42 | /// let path_renderer = PathRenderer; 43 | /// 44 | /// let mut path = PathBuf::new(); 45 | /// 46 | /// path.push("/"); 47 | /// path.push("dev"); 48 | /// path.push("null"); 49 | /// 50 | /// // Note: platform-dependent output 51 | /// assert_eq!(path_renderer.render(&path), "/dev/null"); 52 | /// ``` 53 | pub struct PathRenderer; 54 | 55 | impl> Render for PathRenderer { 56 | type Str<'a> 57 | = Cow<'a, str> 58 | where 59 | T: 'a; 60 | 61 | fn render<'a>(&self, item: &'a T) -> Self::Str<'a> { 62 | item.as_ref().to_string_lossy() 63 | } 64 | } 65 | 66 | /// A renderer which uses a type's [`Display`](std::fmt::Display) implementation. 67 | /// 68 | /// ## Example 69 | /// ``` 70 | /// # use nucleo_picker::{render::DisplayRenderer, Render}; 71 | /// let display_renderer = DisplayRenderer; 72 | /// 73 | /// assert_eq!(display_renderer.render(&1.624f32), "1.624"); 74 | /// ``` 75 | pub struct DisplayRenderer; 76 | 77 | impl Render for DisplayRenderer { 78 | type Str<'a> 79 | = String 80 | where 81 | T: 'a; 82 | 83 | fn render<'a>(&self, item: &'a T) -> Self::Str<'a> { 84 | item.to_string() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | /// Convert a type into a [`usize`], falling back to [`usize::MAX`] if it fails. This is mainly 2 | /// used for converting `u32 -> usize` and will compile down to a no-op on the vast majority of 3 | /// machines. 4 | #[inline] 5 | pub fn as_usize>(num: T) -> usize { 6 | num.try_into().unwrap_or(usize::MAX) 7 | } 8 | 9 | /// Convert a type into a [`u32`], falling back to [`u32::MAX`] if it fails. This is mainly 10 | /// used for converting `usize -> u32`. 11 | #[inline] 12 | pub fn as_u32>(num: T) -> u32 { 13 | num.try_into().unwrap_or(u32::MAX) 14 | } 15 | 16 | #[inline] 17 | pub fn as_u16>(num: T) -> u16 { 18 | num.try_into().unwrap_or(u16::MAX) 19 | } 20 | --------------------------------------------------------------------------------