dialoguer-0.11.0/.cargo/config.toml000064400000000000000000000003061046102023000151700ustar 00000000000000[alias] format = "fmt" format-check = "fmt --check" lint = "clippy --all-targets --all-features -- -D warnings" test-cover = "llvm-cov --all-features --lcov --output-path lcov.info" dialoguer-0.11.0/.cargo_vcs_info.json0000644000000001360000000000100130660ustar { "git": { "sha1": "d952d17d1c09b6dcbfdb94850818fdce8c02bfeb" }, "path_in_vcs": "" }dialoguer-0.11.0/.github/workflows/ci.yml000064400000000000000000000031741046102023000163760ustar 00000000000000name: CI on: push: branches: [master] pull_request: branches: [master] jobs: test: name: Tests strategy: fail-fast: false matrix: include: - os: macos-latest target: x86_64-apple-darwin rust: 1.63.0 - os: ubuntu-latest target: x86_64-unknown-linux-gnu rust: 1.63.0 - os: ubuntu-latest target: i686-unknown-linux-gnu rust: 1.63.0 - os: windows-latest target: i686-pc-windows-msvc rust: 1.63.0 - os: windows-latest target: x86_64-pc-windows-msvc rust: 1.63.0 - os: ubuntu-latest target: x86_64-unknown-linux-gnu rust: stable runs-on: ${{ matrix.os }} steps: - name: Install rust uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} targets: ${{ matrix.target }} - name: Checkout uses: actions/checkout@v3 - name: Install linker if: matrix.target == 'i686-unknown-linux-gnu' run: | sudo apt-get update sudo apt-get install gcc-multilib - name: Test run: cargo test --all-features --target ${{ matrix.target }} lint: name: Linting (fmt + clippy) runs-on: ubuntu-latest steps: - name: Install rust uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy - name: Checkout uses: actions/checkout@v3 - name: Lint check run: cargo lint - name: Format check run: cargo format-check dialoguer-0.11.0/.gitignore000064400000000000000000000000221046102023000136400ustar 00000000000000target Cargo.lock dialoguer-0.11.0/.vscode/settings.json000064400000000000000000000000541046102023000157510ustar 00000000000000{ "rust-analyzer.cargo.features": "all" } dialoguer-0.11.0/CHANGELOG.md000064400000000000000000000102001046102023000134600ustar 00000000000000# Changelog ## 0.11.0 ### Enhancements * Added `dialouger::Result` and `dialouger::Error` * Added a `BasicHistory` implementation for `History` * Added vim mode for `FuzzySelect` * All prompts implement `Clone` * Add handling of `Delete` key for `FuzzySelect` ### Bug fixes * Resolve some issues on Windows where pressing shift keys sometimes aborted dialogs * Resolve `MultiSelect` checked and unchecked variants looking the same on Windows * `Input` values that are invalid are now also stored in `History` * Resolve some issues with cursor positioning in `Input` when using `utf-8` characters * Correct page is shown when default selected option is not on the first page for `Select` * Fix panic in `FuzzySelect` when using non-ASCII characters ### Breaking * Updated MSRV to `1.63.0` due to multiple dependencies on different platforms: `rustix`, `tempfile`,`linux-raw-sys` * Removed deprecated `Confirm::with_text` * Removed deprecated `ColorfulTheme::inline_selections` * Prompt builder functions now take `mut self` instead of `&mut self` * Prompt builder functions now return `Self` instead of `&mut Self` * Prompt interaction functions now take `self` instead of `&self` * Prompt interaction functions and other operations now return `dialouger::Result` instead of `std::io::Result` * Rename `Validator` to `InputValidator` * The trait method `Theme::format_fuzzy_select_prompt()` now takes a byte position instead of a cursor position in order to support UTF-8. ## 0.10.4 ### Enhancements * Added validator for password input ## 0.10.3 ### Enhancements * Fix various issues with fuzzy select * Enable customization of number of rows for fuzzy select * Added post completion text for input * Various cursor movement improvements * Correctly ignore unknown keys. * Reset prompt height in `TermThemeRenderer::clear`. ## 0.10.2 ### Enhancements * Fix fuzzy select active item colors. * Fix fuzzy search clear on cancel. * Clear everything on cancel via escape key. ## 0.10.1 ### Enhancements * Allow matches highlighting for `FuzzySelect` ## 0.10.0 ### Enhancements * Loosen some trait bounds * Improve keyboard interactions (#141, #162) * Added `max_length` to `MultiSelect`, `Select` and `Sort` * Allow completion support for `Input::interact_text*` behind `completion` feature ### Breaking * All prompts `*::new` will now don't report selected values unless `report(true)` is called on them. ## 0.9.0 ### Enhancements * Apply input validation to the default value too in `Input` * Added `FuzzySelect` behind `fuzzy-select` feature * Allow history processing for `Input::interact_text*` behind `history` feature * Added `interact_*_opt` methods for `MultiSelect` and `Sort`. ### Breaking * Updated MSRV to `1.51.0` * `Editor` is gated behind `editor` feature * `Password`, `Theme::format_password_prompt` and `Theme::format_password_prompt_selection` are gated behind `password` feature * Remove `Select::paged()`, `Sort::paged()` and `MultiSelect::paged()` in favor of automatic paging based on terminal size ## 0.8.0 ### Enhancements * `Input::validate_with` can take a `FnMut` (allowing multiple references) ### Breaking * `Input::interact*` methods take `&mut self` instead of `&self` ## 0.7.0 ### Enhancements * Added `wait_for_newline` to `Confirm` * More secure password prompt * More documentation * Added `interact_text` method for `Input` prompt * Added `inline_selections` to `ColorfulTheme` ### Breaking * Removed `theme::CustomPromptCharacterTheme` * `Input` validators now take the input type `T` as arg * `Confirm` has no `default` value by default now ## 0.6.2 ### Enhancements * Updating some docs ## 0.6.1 ### Bug fixes * `theme::ColorfulTheme` default styles are for stderr ## 0.6.0 ### Breaking * Removed `theme::SelectionStyle` enum * Allowed more customization for `theme::Theme` trait by changing methods * Allowed more customization for `theme::ColorfulTheme` by changing members * Renamed prompt `Confirmation` to `Confirm` * Renamed prompt `PasswordInput` to `Password` * Renamed prompt `OrderList` to `Sort` * Renamed prompt `Checkboxes` to `MultiSelect` ### Enhancements * Improved colored theme * Improved cursor visibility manipulation dialoguer-0.11.0/Cargo.lock0000644000000262220000000000100110450ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "cc" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "console" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" dependencies = [ "encode_unicode", "lazy_static", "libc", "unicode-width", "windows-sys 0.42.0", ] [[package]] name = "dialoguer" version = "0.11.0" dependencies = [ "console", "fuzzy-matcher", "shell-words", "tempfile", "thiserror", "zeroize", ] [[package]] name = "encode_unicode" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "errno" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" dependencies = [ "errno-dragonfly", "libc", "windows-sys 0.45.0", ] [[package]] name = "errno-dragonfly" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" dependencies = [ "cc", "libc", ] [[package]] name = "fastrand" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] [[package]] name = "fuzzy-matcher" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" dependencies = [ "thread_local", ] [[package]] name = "hermit-abi" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" [[package]] name = "instant" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", ] [[package]] name = "io-lifetimes" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" dependencies = [ "hermit-abi", "libc", "windows-sys 0.48.0", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.141" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" [[package]] name = "linux-raw-sys" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" [[package]] name = "once_cell" version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "proc-macro2" version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ "bitflags", ] [[package]] name = "rustix" version = "0.37.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aef160324be24d31a62147fae491c14d2204a3865c7ca8c3b0d7f7bcb3ea635" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", "windows-sys 0.48.0", ] [[package]] name = "shell-words" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" [[package]] name = "syn" version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcf316d5356ed6847742d036f8a39c3b8435cac10bd528a4bd461928a6ab34d5" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" dependencies = [ "cfg-if", "fastrand", "redox_syscall", "rustix", "windows-sys 0.45.0", ] [[package]] name = "thiserror" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thread_local" version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ "cfg-if", "once_cell", ] [[package]] name = "unicode-ident" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" [[package]] name = "unicode-width" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "windows-sys" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-sys" version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ "windows-targets 0.42.2", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.0", ] [[package]] name = "windows-targets" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-targets" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" dependencies = [ "windows_aarch64_gnullvm 0.48.0", "windows_aarch64_msvc 0.48.0", "windows_i686_gnu 0.48.0", "windows_i686_msvc 0.48.0", "windows_x86_64_gnu 0.48.0", "windows_x86_64_gnullvm 0.48.0", "windows_x86_64_msvc 0.48.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "zeroize" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" dialoguer-0.11.0/Cargo.toml0000644000000034760000000000100110760ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.63.0" name = "dialoguer" version = "0.11.0" authors = [ "Armin Ronacher ", "Pavan Kumar Sunkara ", ] description = "A command line prompting library." homepage = "https://github.com/console-rs/dialoguer" documentation = "https://docs.rs/dialoguer" readme = "README.md" keywords = [ "cli", "menu", "prompt", ] categories = ["command-line-interface"] license = "MIT" repository = "https://github.com/console-rs/dialoguer" [package.metadata.docs.rs] all-features = true [[example]] name = "password" required-features = ["password"] [[example]] name = "editor" required-features = ["editor"] [[example]] name = "fuzzy_select" required-features = ["fuzzy-select"] [[example]] name = "history" required-features = ["history"] [[example]] name = "completion" required-features = ["completion"] [dependencies.console] version = "0.15.0" [dependencies.fuzzy-matcher] version = "0.3.7" optional = true [dependencies.shell-words] version = "1.1.0" [dependencies.tempfile] version = "3" optional = true [dependencies.thiserror] version = "1.0.40" [dependencies.zeroize] version = "1.1.1" optional = true [features] completion = [] default = [ "editor", "password", ] editor = ["tempfile"] fuzzy-select = ["fuzzy-matcher"] history = [] password = ["zeroize"] dialoguer-0.11.0/Cargo.toml.orig000064400000000000000000000024621046102023000145510ustar 00000000000000[package] name = "dialoguer" description = "A command line prompting library." version = "0.11.0" edition = "2021" rust-version = "1.63.0" authors = [ "Armin Ronacher ", "Pavan Kumar Sunkara " ] keywords = ["cli", "menu", "prompt"] categories = ["command-line-interface"] license = "MIT" homepage = "https://github.com/console-rs/dialoguer" repository = "https://github.com/console-rs/dialoguer" documentation = "https://docs.rs/dialoguer" readme = "README.md" [features] default = ["editor", "password"] editor = ["tempfile"] fuzzy-select = ["fuzzy-matcher"] history = [] password = ["zeroize"] completion = [] [dependencies] console = "0.15.0" tempfile = { version = "3", optional = true } zeroize = { version = "1.1.1", optional = true } fuzzy-matcher = { version = "0.3.7", optional = true } shell-words = "1.1.0" thiserror = "1.0.40" [[example]] name = "password" required-features = ["password"] [[example]] name = "editor" required-features = ["editor"] [[example]] name = "fuzzy_select" required-features = ["fuzzy-select"] [[example]] name = "history" required-features = ["history"] [[example]] name = "completion" required-features = ["completion"] [workspace.metadata.workspaces] no_individual_tags = true [package.metadata.docs.rs] all-features = true dialoguer-0.11.0/LICENSE000064400000000000000000000021301046102023000126570ustar 00000000000000The MIT License (MIT) Copyright (c) 2017 Armin Ronacher Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. dialoguer-0.11.0/README.md000064400000000000000000000015271046102023000131420ustar 00000000000000# dialoguer [![Build Status](https://github.com/console-rs/dialoguer/workflows/CI/badge.svg)](https://github.com/console-rs/dialoguer/actions?query=branch%3Amaster) [![Latest version](https://img.shields.io/crates/v/dialoguer.svg)](https://crates.io/crates/dialoguer) [![Documentation](https://docs.rs/dialoguer/badge.svg)](https://docs.rs/dialoguer) A rust library for command line prompts and similar things. Best paired with other libraries in the family: * [console](https://github.com/console-rs/console) * [indicatif](https://github.com/console-rs/indicatif) ## License and Links * [Documentation](https://docs.rs/dialoguer/) * [Issue Tracker](https://github.com/console-rs/dialoguer/issues) * [Examples](https://github.com/console-rs/dialoguer/tree/master/examples) * License: [MIT](https://github.com/console-rs/dialoguer/blob/main/LICENSE) dialoguer-0.11.0/examples/buffered.rs000064400000000000000000000021131046102023000156210ustar 00000000000000use console::Term; use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select, Sort}; fn main() { let items = &[ "Ice Cream", "Vanilla Cupcake", "Chocolate Muffin", "A Pile of sweet, sweet mustard", ]; let term = Term::buffered_stderr(); let theme = ColorfulTheme::default(); println!("All the following controls are run in a buffered terminal"); Confirm::with_theme(&theme) .with_prompt("Do you want to continue?") .interact_on(&term) .unwrap(); let _: String = Input::with_theme(&theme) .with_prompt("Your name") .interact_on(&term) .unwrap(); Select::with_theme(&theme) .with_prompt("Pick an item") .items(items) .interact_on(&term) .unwrap(); MultiSelect::with_theme(&theme) .with_prompt("Pick some items") .items(items) .interact_on(&term) .unwrap(); Sort::with_theme(&theme) .with_prompt("Order these items") .items(items) .interact_on(&term) .unwrap(); } dialoguer-0.11.0/examples/completion.rs000064400000000000000000000021171046102023000162140ustar 00000000000000use dialoguer::{theme::ColorfulTheme, Completion, Input}; fn main() { println!("Use the Right arrow or Tab to complete your command"); let completion = MyCompletion::default(); Input::::with_theme(&ColorfulTheme::default()) .with_prompt("dialoguer") .completion_with(&completion) .interact_text() .unwrap(); } struct MyCompletion { options: Vec, } impl Default for MyCompletion { fn default() -> Self { MyCompletion { options: vec![ "orange".to_string(), "apple".to_string(), "banana".to_string(), ], } } } impl Completion for MyCompletion { /// Simple completion implementation based on substring fn get(&self, input: &str) -> Option { let matches = self .options .iter() .filter(|option| option.starts_with(input)) .collect::>(); if matches.len() == 1 { Some(matches[0].to_string()) } else { None } } } dialoguer-0.11.0/examples/confirm.rs000064400000000000000000000040331046102023000154770ustar 00000000000000use dialoguer::{theme::ColorfulTheme, Confirm}; fn main() { if Confirm::with_theme(&ColorfulTheme::default()) .with_prompt("Do you want to continue?") .interact() .unwrap() { println!("Looks like you want to continue"); } else { println!("nevermind then :("); } if Confirm::with_theme(&ColorfulTheme::default()) .with_prompt("Do you really want to continue?") .default(true) .interact() .unwrap() { println!("Looks like you want to continue"); } else { println!("nevermind then :("); } if Confirm::with_theme(&ColorfulTheme::default()) .with_prompt("Do you really really want to continue?") .default(true) .show_default(false) .wait_for_newline(true) .interact() .unwrap() { println!("Looks like you want to continue"); } else { println!("nevermind then :("); } if Confirm::with_theme(&ColorfulTheme::default()) .with_prompt("Do you really really really want to continue?") .wait_for_newline(true) .interact() .unwrap() { println!("Looks like you want to continue"); } else { println!("nevermind then :("); } match Confirm::with_theme(&ColorfulTheme::default()) .with_prompt("Do you really really really really want to continue?") .interact_opt() .unwrap() { Some(true) => println!("Looks like you want to continue"), Some(false) => println!("nevermind then :("), None => println!("Ok, we can start over later"), } match Confirm::with_theme(&ColorfulTheme::default()) .with_prompt("Do you really really really really really want to continue?") .default(true) .wait_for_newline(true) .interact_opt() .unwrap() { Some(true) => println!("Looks like you want to continue"), Some(false) => println!("nevermind then :("), None => println!("Ok, we can start over later"), } } dialoguer-0.11.0/examples/editor.rs000064400000000000000000000003421046102023000153270ustar 00000000000000use dialoguer::Editor; fn main() { if let Some(rv) = Editor::new().edit("Enter a commit message").unwrap() { println!("Your message:"); println!("{}", rv); } else { println!("Abort!"); } } dialoguer-0.11.0/examples/fuzzy_select.rs000064400000000000000000000017171046102023000165760ustar 00000000000000use dialoguer::{theme::ColorfulTheme, FuzzySelect}; fn main() { let selections = &[ "Ice Cream", "Vanilla Cupcake", "Chocolate Muffin", "A Pile of sweet, sweet mustard", "Carrots", "Peas", "Pistacio", "Mustard", "Cream", "Banana", "Chocolate", "Flakes", "Corn", "Cake", "Tarte", "Cheddar", "Vanilla", "Hazelnut", "Flour", "Sugar", "Salt", "Potato", "French Fries", "Pizza", "Mousse au chocolat", "Brown sugar", "Blueberry", "Burger", ]; let selection = FuzzySelect::with_theme(&ColorfulTheme::default()) .with_prompt("Pick your flavor") .default(0) .items(&selections[..]) .interact() .unwrap(); println!("Enjoy your {}!", selections[selection]); } dialoguer-0.11.0/examples/history.rs000064400000000000000000000013431046102023000155440ustar 00000000000000use dialoguer::{theme::ColorfulTheme, BasicHistory, Input}; use std::process; fn main() { println!("Use 'exit' to quit the prompt"); println!("In this example, history is limited to 8 entries and contains no duplicates"); println!("Use the Up/Down arrows to scroll through history"); println!(); let mut history = BasicHistory::new().max_entries(8).no_duplicates(true); loop { if let Ok(cmd) = Input::::with_theme(&ColorfulTheme::default()) .with_prompt("dialoguer") .history_with(&mut history) .interact_text() { if cmd == "exit" { process::exit(0); } println!("Entered {}", cmd); } } } dialoguer-0.11.0/examples/history_custom.rs000064400000000000000000000023371046102023000171420ustar 00000000000000use dialoguer::{theme::ColorfulTheme, History, Input}; use std::{collections::VecDeque, process}; fn main() { println!("Use 'exit' to quit the prompt"); println!("In this example, history is limited to 4 entries"); println!("Use the Up/Down arrows to scroll through history"); println!(); let mut history = MyHistory::default(); loop { if let Ok(cmd) = Input::::with_theme(&ColorfulTheme::default()) .with_prompt("dialoguer") .history_with(&mut history) .interact_text() { if cmd == "exit" { process::exit(0); } println!("Entered {}", cmd); } } } struct MyHistory { max: usize, history: VecDeque, } impl Default for MyHistory { fn default() -> Self { MyHistory { max: 4, history: VecDeque::new(), } } } impl History for MyHistory { fn read(&self, pos: usize) -> Option { self.history.get(pos).cloned() } fn write(&mut self, val: &T) { if self.history.len() == self.max { self.history.pop_back(); } self.history.push_front(val.to_string()); } } dialoguer-0.11.0/examples/input.rs000064400000000000000000000024611046102023000152040ustar 00000000000000use dialoguer::{theme::ColorfulTheme, Input}; fn main() { let input: String = Input::with_theme(&ColorfulTheme::default()) .with_prompt("Your name") .interact_text() .unwrap(); println!("Hello {}!", input); let mail: String = Input::with_theme(&ColorfulTheme::default()) .with_prompt("Your email") .validate_with({ let mut force = None; move |input: &String| -> Result<(), &str> { if input.contains('@') || force.as_ref().map_or(false, |old| old == input) { Ok(()) } else { force = Some(input.clone()); Err("This is not a mail address; type the same value again to force use") } } }) .interact_text() .unwrap(); println!("Email: {}", mail); let mail: String = Input::with_theme(&ColorfulTheme::default()) .with_prompt("Your planet") .default("Earth".to_string()) .interact_text() .unwrap(); println!("Planet: {}", mail); let mail: String = Input::with_theme(&ColorfulTheme::default()) .with_prompt("Your galaxy") .with_initial_text("Milky Way".to_string()) .interact_text() .unwrap(); println!("Galaxy: {}", mail); } dialoguer-0.11.0/examples/multi_select.rs000064400000000000000000000023251046102023000165350ustar 00000000000000use dialoguer::{theme::ColorfulTheme, MultiSelect}; fn main() { let multiselected = &[ "Ice Cream", "Vanilla Cupcake", "Chocolate Muffin", "A Pile of sweet, sweet mustard", ]; let defaults = &[false, false, true, false]; let selections = MultiSelect::with_theme(&ColorfulTheme::default()) .with_prompt("Pick your food") .items(&multiselected[..]) .defaults(&defaults[..]) .interact() .unwrap(); if selections.is_empty() { println!("You did not select anything :("); } else { println!("You selected these things:"); for selection in selections { println!(" {}", multiselected[selection]); } } let selections = MultiSelect::with_theme(&ColorfulTheme::default()) .with_prompt("Pick your food") .items(&multiselected[..]) .defaults(&defaults[..]) .max_length(2) .interact() .unwrap(); if selections.is_empty() { println!("You did not select anything :("); } else { println!("You selected these things:"); for selection in selections { println!(" {}", multiselected[selection]); } } } dialoguer-0.11.0/examples/paging.rs000064400000000000000000000016321046102023000153110ustar 00000000000000use dialoguer::{theme::ColorfulTheme, Select}; fn main() { let selections = &[ "Ice Cream", "Vanilla Cupcake", "Chocolate Muffin", "A Pile of sweet, sweet mustard", "Carrots", "Peas", "Pistacio", "Mustard", "Cream", "Banana", "Chocolate", "Flakes", "Corn", "Cake", "Tarte", "Cheddar", "Vanilla", "Hazelnut", "Flour", "Sugar", "Salt", "Potato", "French Fries", "Pizza", "Mousse au chocolat", "Brown sugar", "Blueberry", "Burger", ]; let selection = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Pick your flavor") .default(0) .items(&selections[..]) .interact() .unwrap(); println!("Enjoy your {}!", selections[selection]); } dialoguer-0.11.0/examples/password.rs000064400000000000000000000011501046102023000157010ustar 00000000000000use dialoguer::{theme::ColorfulTheme, Password}; fn main() { let password = Password::with_theme(&ColorfulTheme::default()) .with_prompt("Password") .with_confirmation("Repeat password", "Error: the passwords don't match.") .validate_with(|input: &String| -> Result<(), &str> { if input.chars().count() > 3 { Ok(()) } else { Err("Password must be longer than 3") } }) .interact() .unwrap(); println!( "Your password is {} characters long", password.chars().count() ); } dialoguer-0.11.0/examples/select.rs000064400000000000000000000022271046102023000153240ustar 00000000000000use dialoguer::{theme::ColorfulTheme, Select}; fn main() { let selections = &[ "Ice Cream", "Vanilla Cupcake", "Chocolate Muffin", "A Pile of sweet, sweet mustard", ]; let selection = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Pick your flavor") .default(0) .items(&selections[..]) .interact() .unwrap(); println!("Enjoy your {}!", selections[selection]); let selection = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Optionally pick your flavor") .default(0) .items(&selections[..]) .interact_opt() .unwrap(); if let Some(selection) = selection { println!("Enjoy your {}!", selections[selection]); } else { println!("You didn't select anything!"); } let selection = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Optionally pick your flavor, hint it might be on the second page") .default(0) .max_length(2) .items(&selections[..]) .interact() .unwrap(); println!("Enjoy your {}!", selections[selection]); } dialoguer-0.11.0/examples/sort.rs000064400000000000000000000016511046102023000150340ustar 00000000000000use dialoguer::{theme::ColorfulTheme, Sort}; fn main() { let list = &[ "Ice Cream", "Vanilla Cupcake", "Chocolate Muffin", "A Pile of sweet, sweet mustard", ]; let sorted = Sort::with_theme(&ColorfulTheme::default()) .with_prompt("Order your foods by preference") .items(&list[..]) .interact() .unwrap(); println!("Your favorite item:"); println!(" {}", list[sorted[0]]); println!("Your least favorite item:"); println!(" {}", list[sorted[sorted.len() - 1]]); let sorted = Sort::with_theme(&ColorfulTheme::default()) .with_prompt("Order your foods by preference") .items(&list[..]) .max_length(2) .interact() .unwrap(); println!("Your favorite item:"); println!(" {}", list[sorted[0]]); println!("Your least favorite item:"); println!(" {}", list[sorted[sorted.len() - 1]]); } dialoguer-0.11.0/examples/wizard.rs000064400000000000000000000037171046102023000153520ustar 00000000000000use std::error::Error; use std::net::IpAddr; use console::Style; use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select}; #[derive(Debug)] #[allow(dead_code)] struct Config { interface: IpAddr, hostname: String, use_acme: bool, private_key: Option, cert: Option, } fn init_config() -> Result, Box> { let theme = ColorfulTheme { values_style: Style::new().yellow().dim(), ..ColorfulTheme::default() }; println!("Welcome to the setup wizard"); if !Confirm::with_theme(&theme) .with_prompt("Do you want to continue?") .interact()? { return Ok(None); } let interface = Input::with_theme(&theme) .with_prompt("Interface") .default("127.0.0.1".parse().unwrap()) .interact()?; let hostname = Input::with_theme(&theme) .with_prompt("Hostname") .interact()?; let tls = Select::with_theme(&theme) .with_prompt("Configure TLS") .default(0) .item("automatic with ACME") .item("manual") .item("no") .interact()?; let (private_key, cert, use_acme) = match tls { 0 => (Some("acme.pkey".into()), Some("acme.cert".into()), true), 1 => ( Some( Input::with_theme(&theme) .with_prompt(" Path to private key") .interact()?, ), Some( Input::with_theme(&theme) .with_prompt(" Path to certificate") .interact()?, ), false, ), _ => (None, None, false), }; Ok(Some(Config { hostname, interface, private_key, cert, use_acme, })) } fn main() { match init_config() { Ok(None) => println!("Aborted."), Ok(Some(config)) => println!("{:#?}", config), Err(err) => println!("error: {}", err), } } dialoguer-0.11.0/src/completion.rs000064400000000000000000000001561046102023000151660ustar 00000000000000/// Trait for completion handling. pub trait Completion { fn get(&self, input: &str) -> Option; } dialoguer-0.11.0/src/edit.rs000064400000000000000000000062041046102023000137420ustar 00000000000000use std::{ env, ffi::{OsStr, OsString}, fs, io::{Read, Write}, process, }; use crate::Result; /// Launches the default editor to edit a string. /// /// ## Example /// /// ```rust,no_run /// use dialoguer::Editor; /// /// if let Some(rv) = Editor::new().edit("Enter a commit message").unwrap() { /// println!("Your message:"); /// println!("{}", rv); /// } else { /// println!("Abort!"); /// } /// ``` pub struct Editor { editor: OsString, extension: String, require_save: bool, trim_newlines: bool, } fn get_default_editor() -> OsString { if let Some(prog) = env::var_os("VISUAL") { return prog; } if let Some(prog) = env::var_os("EDITOR") { return prog; } if cfg!(windows) { "notepad.exe".into() } else { "vi".into() } } impl Default for Editor { fn default() -> Self { Self::new() } } impl Editor { /// Creates a new editor. pub fn new() -> Self { Self { editor: get_default_editor(), extension: ".txt".into(), require_save: true, trim_newlines: true, } } /// Sets a specific editor executable. pub fn executable>(&mut self, val: S) -> &mut Self { self.editor = val.as_ref().into(); self } /// Sets a specific extension pub fn extension(&mut self, val: &str) -> &mut Self { self.extension = val.into(); self } /// Enables or disables the save requirement. pub fn require_save(&mut self, val: bool) -> &mut Self { self.require_save = val; self } /// Enables or disables trailing newline stripping. /// /// This is on by default. pub fn trim_newlines(&mut self, val: bool) -> &mut Self { self.trim_newlines = val; self } /// Launches the editor to edit a string. /// /// Returns `None` if the file was not saved or otherwise the /// entered text. pub fn edit(&self, s: &str) -> Result> { let mut f = tempfile::Builder::new() .prefix("edit-") .suffix(&self.extension) .rand_bytes(12) .tempfile()?; f.write_all(s.as_bytes())?; f.flush()?; let ts = fs::metadata(f.path())?.modified()?; let s: String = self.editor.clone().into_string().unwrap(); let (cmd, args) = match shell_words::split(&s) { Ok(mut parts) => { let cmd = parts.remove(0); (cmd, parts) } Err(_) => (s, vec![]), }; let rv = process::Command::new(cmd) .args(args) .arg(f.path()) .spawn()? .wait()?; if rv.success() && self.require_save && ts >= fs::metadata(f.path())?.modified()? { return Ok(None); } let mut new_f = fs::File::open(f.path())?; let mut rv = String::new(); new_f.read_to_string(&mut rv)?; if self.trim_newlines { let len = rv.trim_end_matches(&['\n', '\r'][..]).len(); rv.truncate(len); } Ok(Some(rv)) } } dialoguer-0.11.0/src/error.rs000064400000000000000000000006041046102023000141440ustar 00000000000000use std::{io::Error as IoError, result::Result as StdResult}; use thiserror::Error; /// Possible errors returned by prompts. #[derive(Error, Debug)] pub enum Error { /// Error while executing IO operations. #[error("IO error: {0}")] IO(#[from] IoError), } /// Result type where errors are of type [Error](crate::error::Error) pub type Result = StdResult; dialoguer-0.11.0/src/history.rs000064400000000000000000000045071046102023000145220ustar 00000000000000use std::collections::VecDeque; /// Trait for history handling. pub trait History { /// This is called with the current position that should /// be read from history. The `pos` represents the number /// of times the `Up`/`Down` arrow key has been pressed. /// This would normally be used as an index to some sort /// of vector. If the `pos` does not have an entry, [`None`](Option::None) /// should be returned. fn read(&self, pos: usize) -> Option; /// This is called with the next value you should store /// in history at the first location. Normally history /// is implemented as a FIFO queue. fn write(&mut self, val: &T); } pub struct BasicHistory { max_entries: Option, deque: VecDeque, no_duplicates: bool, } impl BasicHistory { /// Creates a new basic history value which has no limit on the number of /// entries and allows for duplicates. /// /// # Example /// /// A history with at most 8 entries and no duplicates: /// /// ```rs /// let mut history = BasicHistory::new().max_entries(8).no_duplicates(true); /// ``` pub fn new() -> Self { Self { max_entries: None, deque: VecDeque::new(), no_duplicates: false, } } /// Limit the number of entries stored in the history. pub fn max_entries(self, max_entries: usize) -> Self { Self { max_entries: Some(max_entries), ..self } } /// Prevent duplicates in the history. This means that any previous entries /// that are equal to a new entry are removed before the new entry is added. pub fn no_duplicates(self, no_duplicates: bool) -> Self { Self { no_duplicates, ..self } } } impl History for BasicHistory { fn read(&self, pos: usize) -> Option { self.deque.get(pos).cloned() } fn write(&mut self, val: &T) { let val = val.to_string(); if self.no_duplicates { self.deque.retain(|v| v != &val); } self.deque.push_front(val); if let Some(max_entries) = self.max_entries { self.deque.truncate(max_entries); } } } impl Default for BasicHistory { fn default() -> Self { Self::new() } } dialoguer-0.11.0/src/lib.rs000064400000000000000000000035101046102023000135600ustar 00000000000000//! dialoguer is a library for Rust that helps you build useful small //! interactive user inputs for the command line. It provides utilities //! to render various simple dialogs like confirmation prompts, text //! inputs and more. //! //! Best paired with other libraries in the family: //! //! * [indicatif](https://docs.rs/indicatif) //! * [console](https://docs.rs/console) //! //! # Crate Contents //! //! * Confirmation prompts //! * Input prompts (regular and password) //! * Input validation //! * Selections prompts (single and multi) //! * Fuzzy select prompt //! * Other kind of prompts //! * Editor launching //! //! # Crate Features //! //! The following crate features are available: //! * `editor`: enables bindings to launch editor to edit strings //! * `fuzzy-select`: enables fuzzy select prompt //! * `history`: enables input prompts to be able to track history of inputs //! * `password`: enables password input prompt //! * `completion`: enables ability to implement custom tab-completion for input prompts //! //! By default `editor` and `password` are enabled. #![deny(clippy::all)] pub use console; #[cfg(feature = "completion")] pub use completion::Completion; #[cfg(feature = "editor")] pub use edit::Editor; pub use error::{Error, Result}; #[cfg(feature = "history")] pub use history::{BasicHistory, History}; use paging::Paging; pub use validate::{InputValidator, PasswordValidator}; #[cfg(feature = "fuzzy-select")] pub use prompts::fuzzy_select::FuzzySelect; #[cfg(feature = "password")] pub use prompts::password::Password; pub use prompts::{ confirm::Confirm, input::Input, multi_select::MultiSelect, select::Select, sort::Sort, }; #[cfg(feature = "completion")] mod completion; #[cfg(feature = "editor")] mod edit; mod error; #[cfg(feature = "history")] mod history; mod paging; mod prompts; pub mod theme; mod validate; dialoguer-0.11.0/src/paging.rs000064400000000000000000000073411046102023000142650ustar 00000000000000use console::Term; use crate::Result; /// Creates a paging module /// /// The paging module serves as tracking structure to allow paged views /// and automatically (de-)activates paging depending on the current terminal size. pub struct Paging<'a> { pub pages: usize, pub current_page: usize, pub capacity: usize, pub active: bool, pub max_capacity: Option, term: &'a Term, current_term_size: (u16, u16), items_len: usize, activity_transition: bool, } impl<'a> Paging<'a> { pub fn new(term: &'a Term, items_len: usize, max_capacity: Option) -> Paging<'a> { let term_size = term.size(); // Subtract -2 because we need space to render the prompt, if paging is active let capacity = max_capacity .unwrap_or(std::usize::MAX) .clamp(3, term_size.0 as usize) - 2; let pages = (items_len as f64 / capacity as f64).ceil() as usize; Paging { pages, current_page: 0, capacity, active: pages > 1, term, current_term_size: term_size, items_len, max_capacity, // Set transition initially to true to trigger prompt rendering for inactive paging on start activity_transition: true, } } pub fn update_page(&mut self, cursor_pos: usize) { if cursor_pos != !0 && (cursor_pos < self.current_page * self.capacity || cursor_pos >= (self.current_page + 1) * self.capacity) { self.current_page = cursor_pos / self.capacity; } } /// Updates all internal based on the current terminal size and cursor position pub fn update(&mut self, cursor_pos: usize) -> Result { let new_term_size = self.term.size(); if self.current_term_size != new_term_size { self.current_term_size = new_term_size; self.capacity = self .max_capacity .unwrap_or(std::usize::MAX) .clamp(3, self.current_term_size.0 as usize) - 2; self.pages = (self.items_len as f64 / self.capacity as f64).ceil() as usize; } if self.active == (self.pages > 1) { self.activity_transition = false; } else { self.active = self.pages > 1; self.activity_transition = true; // Clear everything to prevent "ghost" lines in terminal when a resize happened self.term.clear_last_lines(self.capacity)?; } self.update_page(cursor_pos); Ok(()) } /// Renders a prompt when the following conditions are met: /// * Paging is active /// * Transition of the paging activity happened (active -> inactive / inactive -> active) pub fn render_prompt(&mut self, mut render_prompt: F) -> Result where F: FnMut(Option<(usize, usize)>) -> Result, { if self.active { let paging_info = Some((self.current_page + 1, self.pages)); render_prompt(paging_info)?; } else if self.activity_transition { render_prompt(None)?; } self.term.flush()?; Ok(()) } /// Navigates to the next page pub fn next_page(&mut self) -> usize { if self.current_page == self.pages - 1 { self.current_page = 0; } else { self.current_page += 1; } self.current_page * self.capacity } /// Navigates to the previous page pub fn previous_page(&mut self) -> usize { if self.current_page == 0 { self.current_page = self.pages - 1; } else { self.current_page -= 1; } self.current_page * self.capacity } } dialoguer-0.11.0/src/prompts/confirm.rs000064400000000000000000000175571046102023000161730ustar 00000000000000use std::io; use console::{Key, Term}; use crate::{ theme::{render::TermThemeRenderer, SimpleTheme, Theme}, Result, }; /// Renders a confirm prompt. /// /// ## Example /// /// ```rust,no_run /// use dialoguer::Confirm; /// /// fn main() { /// let confirmation = Confirm::new() /// .with_prompt("Do you want to continue?") /// .interact() /// .unwrap(); /// /// if confirmation { /// println!("Looks like you want to continue"); /// } else { /// println!("nevermind then :("); /// } /// } /// ``` #[derive(Clone)] pub struct Confirm<'a> { prompt: String, report: bool, default: Option, show_default: bool, wait_for_newline: bool, theme: &'a dyn Theme, } impl Default for Confirm<'static> { fn default() -> Self { Self::new() } } impl Confirm<'static> { /// Creates a confirm prompt with default theme. pub fn new() -> Self { Self::with_theme(&SimpleTheme) } } impl Confirm<'_> { /// Sets the confirm prompt. pub fn with_prompt>(mut self, prompt: S) -> Self { self.prompt = prompt.into(); self } /// Indicates whether or not to report the chosen selection after interaction. /// /// The default is to report the chosen selection. pub fn report(mut self, val: bool) -> Self { self.report = val; self } /// Sets when to react to user input. /// /// When `false` (default), we check on each user keystroke immediately as /// it is typed. Valid inputs can be one of 'y', 'n', or a newline to accept /// the default. /// /// When `true`, the user must type their choice and hit the Enter key before /// proceeding. Valid inputs can be "yes", "no", "y", "n", or an empty string /// to accept the default. pub fn wait_for_newline(mut self, wait: bool) -> Self { self.wait_for_newline = wait; self } /// Sets a default. /// /// Out of the box the prompt does not have a default and will continue /// to display until the user inputs something and hits enter. If a default is set the user /// can instead accept the default with enter. pub fn default(mut self, val: bool) -> Self { self.default = Some(val); self } /// Disables or enables the default value display. /// /// The default is to append the default value to the prompt to tell the user. pub fn show_default(mut self, val: bool) -> Self { self.show_default = val; self } /// Enables user interaction and returns the result. /// /// The dialog is rendered on stderr. /// /// Result contains `bool` if user answered "yes" or "no" or `default` (configured in [`default`](Self::default) if pushes enter. /// This unlike [`interact_opt`](Self::interact_opt) does not allow to quit with 'Esc' or 'q'. #[inline] pub fn interact(self) -> Result { self.interact_on(&Term::stderr()) } /// Enables user interaction and returns the result. /// /// The dialog is rendered on stderr. /// /// Result contains `Some(bool)` if user answered "yes" or "no" or `Some(default)` (configured in [`default`](Self::default)) if pushes enter, /// or `None` if user cancelled with 'Esc' or 'q'. /// /// ## Example /// /// ```rust,no_run /// use dialoguer::Confirm; /// /// fn main() { /// let confirmation = Confirm::new() /// .interact_opt() /// .unwrap(); /// /// match confirmation { /// Some(answer) => println!("User answered {}", if answer { "yes" } else { "no " }), /// None => println!("User did not answer") /// } /// } /// ``` #[inline] pub fn interact_opt(self) -> Result> { self.interact_on_opt(&Term::stderr()) } /// Like [`interact`](Self::interact) but allows a specific terminal to be set. #[inline] pub fn interact_on(self, term: &Term) -> Result { Ok(self ._interact_on(term, false)? .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case"))?) } /// Like [`interact_opt`](Self::interact_opt) but allows a specific terminal to be set. #[inline] pub fn interact_on_opt(self, term: &Term) -> Result> { self._interact_on(term, true) } fn _interact_on(self, term: &Term, allow_quit: bool) -> Result> { if !term.is_term() { return Err(io::Error::new(io::ErrorKind::NotConnected, "not a terminal").into()); } let mut render = TermThemeRenderer::new(term, self.theme); let default_if_show = if self.show_default { self.default } else { None }; render.confirm_prompt(&self.prompt, default_if_show)?; term.hide_cursor()?; term.flush()?; let rv; if self.wait_for_newline { // Waits for user input and for the user to hit the Enter key // before validation. let mut value = default_if_show; loop { let input = term.read_key()?; match input { Key::Char('y') | Key::Char('Y') => { value = Some(true); } Key::Char('n') | Key::Char('N') => { value = Some(false); } Key::Enter => { if !allow_quit { value = value.or(self.default); } if value.is_some() || allow_quit { rv = value; break; } continue; } Key::Escape | Key::Char('q') if allow_quit => { value = None; } _ => { continue; } }; term.clear_line()?; render.confirm_prompt(&self.prompt, value)?; } } else { // Default behavior: matches continuously on every keystroke, // and does not wait for user to hit the Enter key. loop { let input = term.read_key()?; let value = match input { Key::Char('y') | Key::Char('Y') => Some(true), Key::Char('n') | Key::Char('N') => Some(false), Key::Enter if self.default.is_some() => Some(self.default.unwrap()), Key::Escape | Key::Char('q') if allow_quit => None, _ => { continue; } }; rv = value; break; } } term.clear_line()?; if self.report { render.confirm_prompt_selection(&self.prompt, rv)?; } term.show_cursor()?; term.flush()?; Ok(rv) } } impl<'a> Confirm<'a> { /// Creates a confirm prompt with a specific theme. /// /// ## Example /// /// ```rust,no_run /// use dialoguer::{theme::ColorfulTheme, Confirm}; /// /// fn main() { /// let confirmation = Confirm::with_theme(&ColorfulTheme::default()) /// .interact() /// .unwrap(); /// } /// ``` pub fn with_theme(theme: &'a dyn Theme) -> Self { Self { prompt: "".into(), report: true, default: None, show_default: true, wait_for_newline: false, theme, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_clone() { let confirm = Confirm::new().with_prompt("Do you want to continue?"); let _ = confirm.clone(); } } dialoguer-0.11.0/src/prompts/fuzzy_select.rs000064400000000000000000000315471046102023000172570ustar 00000000000000use std::{io, ops::Rem}; use console::{Key, Term}; use fuzzy_matcher::FuzzyMatcher; use crate::{ theme::{render::TermThemeRenderer, SimpleTheme, Theme}, Result, }; /// Renders a select prompt with fuzzy search. /// /// User can use fuzzy search to limit selectable items. /// Interaction returns index of an item selected in the order they appear in `item` invocation or `items` slice. /// /// ## Example /// /// ```rust,no_run /// use dialoguer::FuzzySelect; /// /// fn main() { /// let items = vec!["foo", "bar", "baz"]; /// /// let selection = FuzzySelect::new() /// .with_prompt("What do you choose?") /// .items(&items) /// .interact() /// .unwrap(); /// /// println!("You chose: {}", items[selection]); /// } /// ``` #[derive(Clone)] pub struct FuzzySelect<'a> { default: Option, items: Vec, prompt: String, report: bool, clear: bool, highlight_matches: bool, enable_vim_mode: bool, max_length: Option, theme: &'a dyn Theme, /// Search string that a fuzzy search with start with. /// Defaults to an empty string. initial_text: String, } impl Default for FuzzySelect<'static> { fn default() -> Self { Self::new() } } impl FuzzySelect<'static> { /// Creates a fuzzy select prompt with default theme. pub fn new() -> Self { Self::with_theme(&SimpleTheme) } } impl FuzzySelect<'_> { /// Sets the clear behavior of the menu. /// /// The default is to clear the menu. pub fn clear(mut self, val: bool) -> Self { self.clear = val; self } /// Sets a default for the menu pub fn default(mut self, val: usize) -> Self { self.default = Some(val); self } /// Add a single item to the fuzzy selector. pub fn item(mut self, item: T) -> Self { self.items.push(item.to_string()); self } /// Adds multiple items to the fuzzy selector. pub fn items(mut self, items: &[T]) -> Self { for item in items { self.items.push(item.to_string()); } self } /// Sets the search text that a fuzzy search starts with. pub fn with_initial_text>(mut self, initial_text: S) -> Self { self.initial_text = initial_text.into(); self } /// Prefaces the menu with a prompt. /// /// When a prompt is set the system also prints out a confirmation after /// the fuzzy selection. pub fn with_prompt>(mut self, prompt: S) -> Self { self.prompt = prompt.into(); self } /// Indicates whether to report the selected value after interaction. /// /// The default is to report the selection. pub fn report(mut self, val: bool) -> Self { self.report = val; self } /// Indicates whether to highlight matched indices /// /// The default is to highlight the indices pub fn highlight_matches(mut self, val: bool) -> Self { self.highlight_matches = val; self } /// Indicated whether to allow the use of vim mode /// /// Vim mode can be entered by pressing Escape. /// This then allows the user to navigate using hjkl. /// /// The default is to disable vim mode. pub fn vim_mode(mut self, val: bool) -> Self { self.enable_vim_mode = val; self } /// Sets the maximum number of visible options. /// /// The default is the height of the terminal minus 2. pub fn max_length(mut self, rows: usize) -> Self { self.max_length = Some(rows); self } /// Enables user interaction and returns the result. /// /// The user can select the items using 'Enter' and the index of selected item will be returned. /// The dialog is rendered on stderr. /// Result contains `index` of selected item if user hit 'Enter'. /// This unlike [`interact_opt`](Self::interact_opt) does not allow to quit with 'Esc' or 'q'. #[inline] pub fn interact(self) -> Result { self.interact_on(&Term::stderr()) } /// Enables user interaction and returns the result. /// /// The user can select the items using 'Enter' and the index of selected item will be returned. /// The dialog is rendered on stderr. /// Result contains `Some(index)` if user hit 'Enter' or `None` if user cancelled with 'Esc' or 'q'. /// /// ## Example /// /// ```rust,no_run /// use dialoguer::FuzzySelect; /// /// fn main() { /// let items = vec!["foo", "bar", "baz"]; /// /// let selection = FuzzySelect::new() /// .items(&items) /// .interact_opt() /// .unwrap(); /// /// match selection { /// Some(index) => println!("You chose: {}", items[index]), /// None => println!("You did not choose anything.") /// } /// } /// ``` #[inline] pub fn interact_opt(self) -> Result> { self.interact_on_opt(&Term::stderr()) } /// Like [`interact`](Self::interact) but allows a specific terminal to be set. #[inline] pub fn interact_on(self, term: &Term) -> Result { Ok(self ._interact_on(term, false)? .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case"))?) } /// Like [`interact_opt`](Self::interact_opt) but allows a specific terminal to be set. #[inline] pub fn interact_on_opt(self, term: &Term) -> Result> { self._interact_on(term, true) } fn _interact_on(self, term: &Term, allow_quit: bool) -> Result> { // Place cursor at the end of the search term let mut cursor = self.initial_text.chars().count(); let mut search_term = self.initial_text.to_owned(); let mut render = TermThemeRenderer::new(term, self.theme); let mut sel = self.default; let mut size_vec = Vec::new(); for items in self.items.iter().as_slice() { let size = &items.len(); size_vec.push(*size); } // Fuzzy matcher let matcher = fuzzy_matcher::skim::SkimMatcherV2::default(); // Subtract -2 because we need space to render the prompt. let visible_term_rows = (term.size().0 as usize).max(3) - 2; let visible_term_rows = self .max_length .unwrap_or(visible_term_rows) .min(visible_term_rows); // Variable used to determine if we need to scroll through the list. let mut starting_row = 0; term.hide_cursor()?; let mut vim_mode = false; loop { let mut byte_indices = search_term .char_indices() .map(|(index, _)| index) .collect::>(); byte_indices.push(search_term.len()); render.clear()?; render.fuzzy_select_prompt(self.prompt.as_str(), &search_term, byte_indices[cursor])?; // Maps all items to a tuple of item and its match score. let mut filtered_list = self .items .iter() .map(|item| (item, matcher.fuzzy_match(item, &search_term))) .filter_map(|(item, score)| score.map(|s| (item, s))) .collect::>(); // Renders all matching items, from best match to worst. filtered_list.sort_unstable_by(|(_, s1), (_, s2)| s2.cmp(s1)); for (idx, (item, _)) in filtered_list .iter() .enumerate() .skip(starting_row) .take(visible_term_rows) { render.fuzzy_select_prompt_item( item, Some(idx) == sel, self.highlight_matches, &matcher, &search_term, )?; } term.flush()?; match (term.read_key()?, sel, vim_mode) { (Key::Escape, _, false) if self.enable_vim_mode => { vim_mode = true; } (Key::Escape, _, false) | (Key::Char('q'), _, true) if allow_quit => { if self.clear { render.clear()?; term.flush()?; } term.show_cursor()?; return Ok(None); } (Key::Char('i' | 'a'), _, true) => { vim_mode = false; } (Key::ArrowUp | Key::BackTab, _, _) | (Key::Char('k'), _, true) if !filtered_list.is_empty() => { if sel == Some(0) { starting_row = filtered_list.len().max(visible_term_rows) - visible_term_rows; } else if sel == Some(starting_row) { starting_row -= 1; } sel = match sel { None => Some(filtered_list.len() - 1), Some(sel) => Some( ((sel as i64 - 1 + filtered_list.len() as i64) % (filtered_list.len() as i64)) as usize, ), }; term.flush()?; } (Key::ArrowDown | Key::Tab, _, _) | (Key::Char('j'), _, true) if !filtered_list.is_empty() => { sel = match sel { None => Some(0), Some(sel) => { Some((sel as u64 + 1).rem(filtered_list.len() as u64) as usize) } }; if sel == Some(visible_term_rows + starting_row) { starting_row += 1; } else if sel == Some(0) { starting_row = 0; } term.flush()?; } (Key::ArrowLeft, _, _) | (Key::Char('h'), _, true) if cursor > 0 => { cursor -= 1; term.flush()?; } (Key::ArrowRight, _, _) | (Key::Char('l'), _, true) if cursor < byte_indices.len() - 1 => { cursor += 1; term.flush()?; } (Key::Enter, Some(sel), _) if !filtered_list.is_empty() => { if self.clear { render.clear()?; } if self.report { render .input_prompt_selection(self.prompt.as_str(), filtered_list[sel].0)?; } let sel_string = filtered_list[sel].0; let sel_string_pos_in_items = self.items.iter().position(|item| item.eq(sel_string)); term.show_cursor()?; return Ok(sel_string_pos_in_items); } (Key::Backspace, _, _) if cursor > 0 => { cursor -= 1; search_term.remove(byte_indices[cursor]); term.flush()?; } (Key::Del, _, _) if cursor < byte_indices.len() - 1 => { search_term.remove(byte_indices[cursor]); term.flush()?; } (Key::Char(chr), _, _) if !chr.is_ascii_control() => { search_term.insert(byte_indices[cursor], chr); cursor += 1; term.flush()?; sel = Some(0); starting_row = 0; } _ => {} } render.clear_preserve_prompt(&size_vec)?; } } } impl<'a> FuzzySelect<'a> { /// Creates a fuzzy select prompt with a specific theme. /// /// ## Example /// /// ```rust,no_run /// use dialoguer::{theme::ColorfulTheme, FuzzySelect}; /// /// fn main() { /// let selection = FuzzySelect::with_theme(&ColorfulTheme::default()) /// .items(&["foo", "bar", "baz"]) /// .interact() /// .unwrap(); /// } /// ``` pub fn with_theme(theme: &'a dyn Theme) -> Self { Self { default: None, items: vec![], prompt: "".into(), report: true, clear: true, highlight_matches: true, enable_vim_mode: false, max_length: None, theme, initial_text: "".into(), } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_clone() { let fuzzy_select = FuzzySelect::new().with_prompt("Do you want to continue?"); let _ = fuzzy_select.clone(); } } dialoguer-0.11.0/src/prompts/input.rs000064400000000000000000000652251046102023000156700ustar 00000000000000use std::{ cmp::Ordering, io, iter, str::FromStr, sync::{Arc, Mutex}, }; use console::{Key, Term}; #[cfg(feature = "completion")] use crate::completion::Completion; #[cfg(feature = "history")] use crate::history::History; use crate::{ theme::{render::TermThemeRenderer, SimpleTheme, Theme}, validate::InputValidator, Result, }; type InputValidatorCallback<'a, T> = Arc Option + 'a>>; /// Renders an input prompt. /// /// ## Example /// /// ```rust,no_run /// use dialoguer::Input; /// /// fn main() { /// let name: String = Input::new() /// .with_prompt("Your name?") /// .interact_text() /// .unwrap(); /// /// println!("Your name is: {}", name); /// } /// ``` /// /// It can also be used with turbofish notation: /// /// ```rust,no_run /// use dialoguer::Input; /// /// fn main() { /// let name = Input::::new() /// .with_prompt("Your name?") /// .interact_text() /// .unwrap(); /// /// println!("Your name is: {}", name); /// } /// ``` #[derive(Clone)] pub struct Input<'a, T> { prompt: String, post_completion_text: Option, report: bool, default: Option, show_default: bool, initial_text: Option, theme: &'a dyn Theme, permit_empty: bool, validator: Option>, #[cfg(feature = "history")] history: Option>>>, #[cfg(feature = "completion")] completion: Option<&'a dyn Completion>, } impl Default for Input<'static, T> { fn default() -> Self { Self::new() } } impl Input<'_, T> { /// Creates an input prompt with default theme. pub fn new() -> Self { Self::with_theme(&SimpleTheme) } /// Sets the input prompt. pub fn with_prompt>(mut self, prompt: S) -> Self { self.prompt = prompt.into(); self } /// Changes the prompt text to the post completion text after input is complete pub fn with_post_completion_text>(mut self, post_completion_text: S) -> Self { self.post_completion_text = Some(post_completion_text.into()); self } /// Indicates whether to report the input value after interaction. /// /// The default is to report the input value. pub fn report(mut self, val: bool) -> Self { self.report = val; self } /// Sets initial text that user can accept or erase. pub fn with_initial_text>(mut self, val: S) -> Self { self.initial_text = Some(val.into()); self } /// Sets a default. /// /// Out of the box the prompt does not have a default and will continue /// to display until the user inputs something and hits enter. If a default is set the user /// can instead accept the default with enter. pub fn default(mut self, value: T) -> Self { self.default = Some(value); self } /// Enables or disables an empty input /// /// By default, if there is no default value set for the input, the user must input a non-empty string. pub fn allow_empty(mut self, val: bool) -> Self { self.permit_empty = val; self } /// Disables or enables the default value display. /// /// The default behaviour is to append [`default`](#method.default) to the prompt to tell the /// user what is the default value. /// /// This method does not affect existence of default value, only its display in the prompt! pub fn show_default(mut self, val: bool) -> Self { self.show_default = val; self } } impl<'a, T> Input<'a, T> { /// Creates an input prompt with a specific theme. /// /// ## Example /// /// ```rust,no_run /// use dialoguer::{theme::ColorfulTheme, Input}; /// /// fn main() { /// let name: String = Input::with_theme(&ColorfulTheme::default()) /// .interact() /// .unwrap(); /// } /// ``` pub fn with_theme(theme: &'a dyn Theme) -> Self { Self { prompt: "".into(), post_completion_text: None, report: true, default: None, show_default: true, initial_text: None, theme, permit_empty: false, validator: None, #[cfg(feature = "history")] history: None, #[cfg(feature = "completion")] completion: None, } } /// Enable history processing /// /// ## Example /// /// ```rust,no_run /// use std::{collections::VecDeque, fmt::Display}; /// use dialoguer::{History, Input}; /// /// struct MyHistory { /// history: VecDeque, /// } /// /// impl Default for MyHistory { /// fn default() -> Self { /// MyHistory { /// history: VecDeque::new(), /// } /// } /// } /// /// impl History for MyHistory { /// fn read(&self, pos: usize) -> Option { /// self.history.get(pos).cloned() /// } /// /// fn write(&mut self, val: &T) /// where /// { /// self.history.push_front(val.to_string()); /// } /// } /// /// fn main() { /// let mut history = MyHistory::default(); /// /// let input = Input::::new() /// .history_with(&mut history) /// .interact_text() /// .unwrap(); /// } /// ``` #[cfg(feature = "history")] pub fn history_with(mut self, history: &'a mut H) -> Self where H: History, { self.history = Some(Arc::new(Mutex::new(history))); self } /// Enable completion #[cfg(feature = "completion")] pub fn completion_with(mut self, completion: &'a C) -> Self where C: Completion, { self.completion = Some(completion); self } } impl<'a, T> Input<'a, T> where T: 'a, { /// Registers a validator. /// /// # Example /// /// ```rust,no_run /// use dialoguer::Input; /// /// fn main() { /// let mail: String = Input::new() /// .with_prompt("Enter email") /// .validate_with(|input: &String| -> Result<(), &str> { /// if input.contains('@') { /// Ok(()) /// } else { /// Err("This is not a mail address") /// } /// }) /// .interact() /// .unwrap(); /// } /// ``` pub fn validate_with(mut self, mut validator: V) -> Self where V: InputValidator + 'a, V::Err: ToString, { let mut old_validator_func = self.validator.take(); self.validator = Some(Arc::new(Mutex::new(move |value: &T| -> Option { if let Some(old) = old_validator_func.as_mut() { if let Some(err) = old.lock().unwrap()(value) { return Some(err); } } match validator.validate(value) { Ok(()) => None, Err(err) => Some(err.to_string()), } }))); self } } impl Input<'_, T> where T: Clone + ToString + FromStr, ::Err: ToString, { /// Enables the user to enter a printable ascii sequence and returns the result. /// /// Its difference from [`interact`](Self::interact) is that it only allows ascii characters for string, /// while [`interact`](Self::interact) allows virtually any character to be used e.g arrow keys. /// /// The dialog is rendered on stderr. pub fn interact_text(self) -> Result { self.interact_text_on(&Term::stderr()) } /// Like [`interact_text`](Self::interact_text) but allows a specific terminal to be set. pub fn interact_text_on(mut self, term: &Term) -> Result { if !term.is_term() { return Err(io::Error::new(io::ErrorKind::NotConnected, "not a terminal").into()); } let mut render = TermThemeRenderer::new(term, self.theme); loop { let default_string = self.default.as_ref().map(ToString::to_string); let prompt_len = render.input_prompt( &self.prompt, if self.show_default { default_string.as_deref() } else { None }, )?; let mut chars: Vec = Vec::new(); let mut position = 0; #[cfg(feature = "history")] let mut hist_pos = 0; if let Some(initial) = self.initial_text.as_ref() { term.write_str(initial)?; chars = initial.chars().collect(); position = chars.len(); } term.flush()?; loop { match term.read_key()? { Key::Backspace if position > 0 => { position -= 1; chars.remove(position); let line_size = term.size().1 as usize; // Case we want to delete last char of a line so the cursor is at the beginning of the next line if (position + prompt_len) % (line_size - 1) == 0 { term.clear_line()?; term.move_cursor_up(1)?; term.move_cursor_right(line_size + 1)?; } else { term.clear_chars(1)?; } let tail: String = chars[position..].iter().collect(); if !tail.is_empty() { term.write_str(&tail)?; let total = position + prompt_len + tail.chars().count(); let total_line = total / line_size; let line_cursor = (position + prompt_len) / line_size; term.move_cursor_up(total_line - line_cursor)?; term.move_cursor_left(line_size)?; term.move_cursor_right((position + prompt_len) % line_size)?; } term.flush()?; } Key::Char(chr) if !chr.is_ascii_control() => { chars.insert(position, chr); position += 1; let tail: String = iter::once(&chr).chain(chars[position..].iter()).collect(); term.write_str(&tail)?; term.move_cursor_left(tail.chars().count() - 1)?; term.flush()?; } Key::ArrowLeft if position > 0 => { if (position + prompt_len) % term.size().1 as usize == 0 { term.move_cursor_up(1)?; term.move_cursor_right(term.size().1 as usize)?; } else { term.move_cursor_left(1)?; } position -= 1; term.flush()?; } Key::ArrowRight if position < chars.len() => { if (position + prompt_len) % (term.size().1 as usize - 1) == 0 { term.move_cursor_down(1)?; term.move_cursor_left(term.size().1 as usize)?; } else { term.move_cursor_right(1)?; } position += 1; term.flush()?; } Key::UnknownEscSeq(seq) if seq == vec!['b'] => { let line_size = term.size().1 as usize; let nb_space = chars[..position] .iter() .rev() .take_while(|c| c.is_whitespace()) .count(); let find_last_space = chars[..position - nb_space] .iter() .rposition(|c| c.is_whitespace()); // If we find a space we set the cursor to the next char else we set it to the beginning of the input if let Some(mut last_space) = find_last_space { if last_space < position { last_space += 1; let new_line = (prompt_len + last_space) / line_size; let old_line = (prompt_len + position) / line_size; let diff_line = old_line - new_line; if diff_line != 0 { term.move_cursor_up(old_line - new_line)?; } let new_pos_x = (prompt_len + last_space) % line_size; let old_pos_x = (prompt_len + position) % line_size; let diff_pos_x = new_pos_x as i64 - old_pos_x as i64; //println!("new_pos_x = {}, old_pos_x = {}, diff = {}", new_pos_x, old_pos_x, diff_pos_x); if diff_pos_x < 0 { term.move_cursor_left(-diff_pos_x as usize)?; } else { term.move_cursor_right((diff_pos_x) as usize)?; } position = last_space; } } else { term.move_cursor_left(position)?; position = 0; } term.flush()?; } Key::UnknownEscSeq(seq) if seq == vec!['f'] => { let line_size = term.size().1 as usize; let find_next_space = chars[position..].iter().position(|c| c.is_whitespace()); // If we find a space we set the cursor to the next char else we set it to the beginning of the input if let Some(mut next_space) = find_next_space { let nb_space = chars[position + next_space..] .iter() .take_while(|c| c.is_whitespace()) .count(); next_space += nb_space; let new_line = (prompt_len + position + next_space) / line_size; let old_line = (prompt_len + position) / line_size; term.move_cursor_down(new_line - old_line)?; let new_pos_x = (prompt_len + position + next_space) % line_size; let old_pos_x = (prompt_len + position) % line_size; let diff_pos_x = new_pos_x as i64 - old_pos_x as i64; if diff_pos_x < 0 { term.move_cursor_left(-diff_pos_x as usize)?; } else { term.move_cursor_right((diff_pos_x) as usize)?; } position += next_space; } else { let new_line = (prompt_len + chars.len()) / line_size; let old_line = (prompt_len + position) / line_size; term.move_cursor_down(new_line - old_line)?; let new_pos_x = (prompt_len + chars.len()) % line_size; let old_pos_x = (prompt_len + position) % line_size; let diff_pos_x = new_pos_x as i64 - old_pos_x as i64; match diff_pos_x.cmp(&0) { Ordering::Less => { term.move_cursor_left((-diff_pos_x - 1) as usize)?; } Ordering::Equal => {} Ordering::Greater => { term.move_cursor_right((diff_pos_x) as usize)?; } } position = chars.len(); } term.flush()?; } #[cfg(feature = "completion")] Key::ArrowRight | Key::Tab => { if let Some(completion) = &self.completion { let input: String = chars.clone().into_iter().collect(); if let Some(x) = completion.get(&input) { term.clear_chars(chars.len())?; chars.clear(); position = 0; for ch in x.chars() { chars.insert(position, ch); position += 1; } term.write_str(&x)?; term.flush()?; } } } #[cfg(feature = "history")] Key::ArrowUp => { let line_size = term.size().1 as usize; if let Some(history) = &self.history { if let Some(previous) = history.lock().unwrap().read(hist_pos) { hist_pos += 1; let mut chars_len = chars.len(); while ((prompt_len + chars_len) / line_size) > 0 { term.clear_chars(chars_len)?; if (prompt_len + chars_len) % line_size == 0 { chars_len -= std::cmp::min(chars_len, line_size); } else { chars_len -= std::cmp::min( chars_len, (prompt_len + chars_len + 1) % line_size, ); } if chars_len > 0 { term.move_cursor_up(1)?; term.move_cursor_right(line_size)?; } } term.clear_chars(chars_len)?; chars.clear(); position = 0; for ch in previous.chars() { chars.insert(position, ch); position += 1; } term.write_str(&previous)?; term.flush()?; } } } #[cfg(feature = "history")] Key::ArrowDown => { let line_size = term.size().1 as usize; if let Some(history) = &self.history { let mut chars_len = chars.len(); while ((prompt_len + chars_len) / line_size) > 0 { term.clear_chars(chars_len)?; if (prompt_len + chars_len) % line_size == 0 { chars_len -= std::cmp::min(chars_len, line_size); } else { chars_len -= std::cmp::min( chars_len, (prompt_len + chars_len + 1) % line_size, ); } if chars_len > 0 { term.move_cursor_up(1)?; term.move_cursor_right(line_size)?; } } term.clear_chars(chars_len)?; chars.clear(); position = 0; // Move the history position back one in case we have up arrowed into it // and the position is sitting on the next to read if let Some(pos) = hist_pos.checked_sub(1) { hist_pos = pos; // Move it back again to get the previous history entry if let Some(pos) = pos.checked_sub(1) { if let Some(previous) = history.lock().unwrap().read(pos) { for ch in previous.chars() { chars.insert(position, ch); position += 1; } term.write_str(&previous)?; } } } term.flush()?; } } Key::Enter => break, _ => (), } } let input = chars.iter().collect::(); term.clear_line()?; render.clear()?; if chars.is_empty() { if let Some(ref default) = self.default { if let Some(ref mut validator) = self.validator { if let Some(err) = validator.lock().unwrap()(default) { render.error(&err)?; continue; } } if self.report { render.input_prompt_selection(&self.prompt, &default.to_string())?; } term.flush()?; return Ok(default.clone()); } else if !self.permit_empty { continue; } } match input.parse::() { Ok(value) => { #[cfg(feature = "history")] if let Some(history) = &mut self.history { history.lock().unwrap().write(&value); } if let Some(ref mut validator) = self.validator { if let Some(err) = validator.lock().unwrap()(&value) { render.error(&err)?; continue; } } if self.report { if let Some(post_completion_text) = &self.post_completion_text { render.input_prompt_selection(post_completion_text, &input)?; } else { render.input_prompt_selection(&self.prompt, &input)?; } } term.flush()?; return Ok(value); } Err(err) => { render.error(&err.to_string())?; continue; } } } } /// Enables user interaction and returns the result. /// /// Allows any characters as input, including e.g arrow keys. /// Some of the keys might have undesired behavior. /// For more limited version, see [`interact_text`](Self::interact_text). /// /// If the user confirms the result is `true`, `false` otherwise. /// The dialog is rendered on stderr. pub fn interact(self) -> Result { self.interact_on(&Term::stderr()) } /// Like [`interact`](Self::interact) but allows a specific terminal to be set. pub fn interact_on(mut self, term: &Term) -> Result { if !term.is_term() { return Err(io::Error::new(io::ErrorKind::NotConnected, "not a terminal").into()); } let mut render = TermThemeRenderer::new(term, self.theme); loop { let default_string = self.default.as_ref().map(ToString::to_string); render.input_prompt( &self.prompt, if self.show_default { default_string.as_deref() } else { None }, )?; term.flush()?; let input = if let Some(initial_text) = self.initial_text.as_ref() { term.read_line_initial_text(initial_text)? } else { term.read_line()? }; render.add_line(); term.clear_line()?; render.clear()?; if input.is_empty() { if let Some(ref default) = self.default { if let Some(ref mut validator) = self.validator { if let Some(err) = validator.lock().unwrap()(default) { render.error(&err)?; continue; } } if self.report { render.input_prompt_selection(&self.prompt, &default.to_string())?; } term.flush()?; return Ok(default.clone()); } else if !self.permit_empty { continue; } } match input.parse::() { Ok(value) => { if let Some(ref mut validator) = self.validator { if let Some(err) = validator.lock().unwrap()(&value) { render.error(&err)?; continue; } } if self.report { render.input_prompt_selection(&self.prompt, &input)?; } term.flush()?; return Ok(value); } Err(err) => { render.error(&err.to_string())?; continue; } } } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_clone() { let input = Input::::new().with_prompt("Your name"); let _ = input.clone(); } } dialoguer-0.11.0/src/prompts/mod.rs000064400000000000000000000003461046102023000153010ustar 00000000000000#![allow(clippy::needless_doctest_main)] pub mod confirm; pub mod input; pub mod multi_select; pub mod select; pub mod sort; #[cfg(feature = "fuzzy-select")] pub mod fuzzy_select; #[cfg(feature = "password")] pub mod password; dialoguer-0.11.0/src/prompts/multi_select.rs000064400000000000000000000266741046102023000172270ustar 00000000000000use std::{io, iter::repeat, ops::Rem}; use console::{Key, Term}; use crate::{ theme::{render::TermThemeRenderer, SimpleTheme, Theme}, Paging, Result, }; /// Renders a multi select prompt. /// /// ## Example /// /// ```rust,no_run /// use dialoguer::MultiSelect; /// /// fn main() { /// let items = vec!["foo", "bar", "baz"]; /// /// let selection = MultiSelect::new() /// .with_prompt("What do you choose?") /// .items(&items) /// .interact() /// .unwrap(); /// /// println!("You chose:"); /// /// for i in selection { /// println!("{}", items[i]); /// } /// } /// ``` #[derive(Clone)] pub struct MultiSelect<'a> { defaults: Vec, items: Vec, prompt: Option, report: bool, clear: bool, max_length: Option, theme: &'a dyn Theme, } impl Default for MultiSelect<'static> { fn default() -> Self { Self::new() } } impl MultiSelect<'static> { /// Creates a multi select prompt with default theme. pub fn new() -> Self { Self::with_theme(&SimpleTheme) } } impl MultiSelect<'_> { /// Sets the clear behavior of the menu. /// /// The default is to clear the menu. pub fn clear(mut self, val: bool) -> Self { self.clear = val; self } /// Sets a defaults for the menu. pub fn defaults(mut self, val: &[bool]) -> Self { self.defaults = val .to_vec() .iter() .copied() .chain(repeat(false)) .take(self.items.len()) .collect(); self } /// Sets an optional max length for a page /// /// Max length is disabled by None pub fn max_length(mut self, val: usize) -> Self { // Paging subtracts two from the capacity, paging does this to // make an offset for the page indicator. So to make sure that // we can show the intended amount of items we need to add two // to our value. self.max_length = Some(val + 2); self } /// Add a single item to the selector. #[inline] pub fn item(self, item: T) -> Self { self.item_checked(item, false) } /// Add a single item to the selector with a default checked state. pub fn item_checked(mut self, item: T, checked: bool) -> Self { self.items.push(item.to_string()); self.defaults.push(checked); self } /// Adds multiple items to the selector. pub fn items(mut self, items: &[T]) -> Self { for item in items { self.items.push(item.to_string()); self.defaults.push(false); } self } /// Adds multiple items to the selector with checked state pub fn items_checked(mut self, items: &[(T, bool)]) -> Self { for &(ref item, checked) in items { self.items.push(item.to_string()); self.defaults.push(checked); } self } /// Prefaces the menu with a prompt. /// /// By default, when a prompt is set the system also prints out a confirmation after /// the selection. You can opt-out of this with [`report`](Self::report). pub fn with_prompt>(mut self, prompt: S) -> Self { self.prompt = Some(prompt.into()); self } /// Indicates whether to report the selected values after interaction. /// /// The default is to report the selections. pub fn report(mut self, val: bool) -> Self { self.report = val; self } /// Enables user interaction and returns the result. /// /// The user can select the items with the 'Space' bar and on 'Enter' the indices of selected items will be returned. /// The dialog is rendered on stderr. /// Result contains `Vec` if user hit 'Enter'. /// This unlike [`interact_opt`](Self::interact_opt) does not allow to quit with 'Esc' or 'q'. #[inline] pub fn interact(self) -> Result> { self.interact_on(&Term::stderr()) } /// Enables user interaction and returns the result. /// /// The user can select the items with the 'Space' bar and on 'Enter' the indices of selected items will be returned. /// The dialog is rendered on stderr. /// Result contains `Some(Vec)` if user hit 'Enter' or `None` if user cancelled with 'Esc' or 'q'. /// /// ## Example /// /// ```rust,no_run /// use dialoguer::MultiSelect; /// /// fn main() { /// let items = vec!["foo", "bar", "baz"]; /// /// let ordered = MultiSelect::new() /// .items(&items) /// .interact_opt() /// .unwrap(); /// /// match ordered { /// Some(positions) => { /// println!("You chose:"); /// /// for i in positions { /// println!("{}", items[i]); /// } /// }, /// None => println!("You did not choose anything.") /// } /// } /// ``` #[inline] pub fn interact_opt(self) -> Result>> { self.interact_on_opt(&Term::stderr()) } /// Like [`interact`](Self::interact) but allows a specific terminal to be set. #[inline] pub fn interact_on(self, term: &Term) -> Result> { Ok(self ._interact_on(term, false)? .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case"))?) } /// Like [`interact_opt`](Self::interact_opt) but allows a specific terminal to be set. #[inline] pub fn interact_on_opt(self, term: &Term) -> Result>> { self._interact_on(term, true) } fn _interact_on(self, term: &Term, allow_quit: bool) -> Result>> { if !term.is_term() { return Err(io::Error::new(io::ErrorKind::NotConnected, "not a terminal").into()); } if self.items.is_empty() { return Err(io::Error::new( io::ErrorKind::Other, "Empty list of items given to `MultiSelect`", ))?; } let mut paging = Paging::new(term, self.items.len(), self.max_length); let mut render = TermThemeRenderer::new(term, self.theme); let mut sel = 0; let mut size_vec = Vec::new(); for items in self .items .iter() .flat_map(|i| i.split('\n')) .collect::>() { let size = &items.len(); size_vec.push(*size); } let mut checked: Vec = self.defaults.clone(); term.hide_cursor()?; loop { if let Some(ref prompt) = self.prompt { paging .render_prompt(|paging_info| render.multi_select_prompt(prompt, paging_info))?; } for (idx, item) in self .items .iter() .enumerate() .skip(paging.current_page * paging.capacity) .take(paging.capacity) { render.multi_select_prompt_item(item, checked[idx], sel == idx)?; } term.flush()?; match term.read_key()? { Key::ArrowDown | Key::Tab | Key::Char('j') => { if sel == !0 { sel = 0; } else { sel = (sel as u64 + 1).rem(self.items.len() as u64) as usize; } } Key::ArrowUp | Key::BackTab | Key::Char('k') => { if sel == !0 { sel = self.items.len() - 1; } else { sel = ((sel as i64 - 1 + self.items.len() as i64) % (self.items.len() as i64)) as usize; } } Key::ArrowLeft | Key::Char('h') => { if paging.active { sel = paging.previous_page(); } } Key::ArrowRight | Key::Char('l') => { if paging.active { sel = paging.next_page(); } } Key::Char(' ') => { checked[sel] = !checked[sel]; } Key::Char('a') => { if checked.iter().all(|&item_checked| item_checked) { checked.fill(false); } else { checked.fill(true); } } Key::Escape | Key::Char('q') => { if allow_quit { if self.clear { render.clear()?; } else { term.clear_last_lines(paging.capacity)?; } term.show_cursor()?; term.flush()?; return Ok(None); } } Key::Enter => { if self.clear { render.clear()?; } if let Some(ref prompt) = self.prompt { if self.report { let selections: Vec<_> = checked .iter() .enumerate() .filter_map(|(idx, &checked)| { if checked { Some(self.items[idx].as_str()) } else { None } }) .collect(); render.multi_select_prompt_selection(prompt, &selections[..])?; } } term.show_cursor()?; term.flush()?; return Ok(Some( checked .into_iter() .enumerate() .filter_map(|(idx, checked)| if checked { Some(idx) } else { None }) .collect(), )); } _ => {} } paging.update(sel)?; if paging.active { render.clear()?; } else { render.clear_preserve_prompt(&size_vec)?; } } } } impl<'a> MultiSelect<'a> { /// Creates a multi select prompt with a specific theme. /// /// ## Example /// /// ```rust,no_run /// use dialoguer::{theme::ColorfulTheme, MultiSelect}; /// /// fn main() { /// let selection = MultiSelect::with_theme(&ColorfulTheme::default()) /// .items(&["foo", "bar", "baz"]) /// .interact() /// .unwrap(); /// } /// ``` pub fn with_theme(theme: &'a dyn Theme) -> Self { Self { items: vec![], defaults: vec![], clear: true, prompt: None, report: true, max_length: None, theme, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_clone() { let multi_select = MultiSelect::new().with_prompt("Select your favorite(s)"); let _ = multi_select.clone(); } } dialoguer-0.11.0/src/prompts/password.rs000064400000000000000000000140731046102023000163660ustar 00000000000000use std::{io, sync::Arc}; use console::Term; use zeroize::Zeroizing; use crate::{ theme::{render::TermThemeRenderer, SimpleTheme, Theme}, validate::PasswordValidator, Result, }; type PasswordValidatorCallback<'a> = Arc Option + 'a>; /// Renders a password input prompt. /// /// ## Example /// /// ```rust,no_run /// use dialoguer::Password; /// /// fn main() { /// let password = Password::new() /// .with_prompt("New Password") /// .with_confirmation("Confirm password", "Passwords mismatching") /// .interact() /// .unwrap(); /// /// println!("Your password length is: {}", password.len()); /// } /// ``` #[derive(Clone)] pub struct Password<'a> { prompt: String, report: bool, theme: &'a dyn Theme, allow_empty_password: bool, confirmation_prompt: Option<(String, String)>, validator: Option>, } impl Default for Password<'static> { fn default() -> Password<'static> { Self::new() } } impl Password<'static> { /// Creates a password input prompt with default theme. pub fn new() -> Password<'static> { Self::with_theme(&SimpleTheme) } } impl Password<'_> { /// Sets the password input prompt. pub fn with_prompt>(mut self, prompt: S) -> Self { self.prompt = prompt.into(); self } /// Indicates whether to report confirmation after interaction. /// /// The default is to report. pub fn report(mut self, val: bool) -> Self { self.report = val; self } /// Enables confirmation prompting. pub fn with_confirmation(mut self, prompt: A, mismatch_err: B) -> Self where A: Into, B: Into, { self.confirmation_prompt = Some((prompt.into(), mismatch_err.into())); self } /// Allows/Disables empty password. /// /// By default this setting is set to false (i.e. password is not empty). pub fn allow_empty_password(mut self, allow_empty_password: bool) -> Self { self.allow_empty_password = allow_empty_password; self } /// Enables user interaction and returns the result. /// /// If the user confirms the result is `Ok()`, `Err()` otherwise. /// The dialog is rendered on stderr. pub fn interact(self) -> Result { self.interact_on(&Term::stderr()) } /// Like [`interact`](Self::interact) but allows a specific terminal to be set. pub fn interact_on(self, term: &Term) -> Result { if !term.is_term() { return Err(io::Error::new(io::ErrorKind::NotConnected, "not a terminal").into()); } let mut render = TermThemeRenderer::new(term, self.theme); render.set_prompts_reset_height(false); loop { let password = Zeroizing::new(self.prompt_password(&mut render, &self.prompt)?); if let Some(ref validator) = self.validator { if let Some(err) = validator(&password) { render.error(&err)?; continue; } } if let Some((ref prompt, ref err)) = self.confirmation_prompt { let pw2 = Zeroizing::new(self.prompt_password(&mut render, prompt)?); if *password != *pw2 { render.error(err)?; continue; } } render.clear()?; if self.report { render.password_prompt_selection(&self.prompt)?; } term.flush()?; return Ok((*password).clone()); } } fn prompt_password(&self, render: &mut TermThemeRenderer, prompt: &str) -> Result { loop { render.password_prompt(prompt)?; render.term().flush()?; let input = render.term().read_secure_line()?; render.add_line(); if !input.is_empty() || self.allow_empty_password { return Ok(input); } } } } impl<'a> Password<'a> { /// Registers a validator. /// /// # Example /// /// ```rust,no_run /// use dialoguer::Password; /// /// fn main() { /// let password: String = Password::new() /// .with_prompt("Enter password") /// .validate_with(|input: &String| -> Result<(), &str> { /// if input.chars().count() > 8 { /// Ok(()) /// } else { /// Err("Password must be longer than 8") /// } /// }) /// .interact() /// .unwrap(); /// } /// ``` pub fn validate_with(mut self, validator: V) -> Self where V: PasswordValidator + 'a, V::Err: ToString, { let old_validator_func = self.validator.take(); self.validator = Some(Arc::new(move |value: &String| -> Option { if let Some(old) = &old_validator_func { if let Some(err) = old(value) { return Some(err); } } match validator.validate(value) { Ok(()) => None, Err(err) => Some(err.to_string()), } })); self } /// Creates a password input prompt with a specific theme. /// /// ## Example /// /// ```rust,no_run /// use dialoguer::{theme::ColorfulTheme, Password}; /// /// fn main() { /// let password = Password::with_theme(&ColorfulTheme::default()) /// .interact() /// .unwrap(); /// } /// ``` pub fn with_theme(theme: &'a dyn Theme) -> Self { Self { prompt: "".into(), report: true, theme, allow_empty_password: false, confirmation_prompt: None, validator: None, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_clone() { let password = Password::new().with_prompt("Enter password"); let _ = password.clone(); } } dialoguer-0.11.0/src/prompts/select.rs000064400000000000000000000254501046102023000160040ustar 00000000000000use std::{io, ops::Rem}; use console::{Key, Term}; use crate::{ theme::{render::TermThemeRenderer, SimpleTheme, Theme}, Paging, Result, }; /// Renders a select prompt. /// /// User can select from one or more options. /// Interaction returns index of an item selected in the order they appear in `item` invocation or `items` slice. /// /// ## Example /// /// ```rust,no_run /// use dialoguer::Select; /// /// fn main() { /// let items = vec!["foo", "bar", "baz"]; /// /// let selection = Select::new() /// .with_prompt("What do you choose?") /// .items(&items) /// .interact() /// .unwrap(); /// /// println!("You chose: {}", items[selection]); /// } /// ``` #[derive(Clone)] pub struct Select<'a> { default: usize, items: Vec, prompt: Option, report: bool, clear: bool, theme: &'a dyn Theme, max_length: Option, } impl Default for Select<'static> { fn default() -> Self { Self::new() } } impl Select<'static> { /// Creates a select prompt with default theme. pub fn new() -> Self { Self::with_theme(&SimpleTheme) } } impl Select<'_> { /// Indicates whether select menu should be erased from the screen after interaction. /// /// The default is to clear the menu. pub fn clear(mut self, val: bool) -> Self { self.clear = val; self } /// Sets initial selected element when select menu is rendered /// /// Element is indicated by the index at which it appears in [`item`](Self::item) method invocation or [`items`](Self::items) slice. pub fn default(mut self, val: usize) -> Self { self.default = val; self } /// Sets an optional max length for a page. /// /// Max length is disabled by None pub fn max_length(mut self, val: usize) -> Self { // Paging subtracts two from the capacity, paging does this to // make an offset for the page indicator. So to make sure that // we can show the intended amount of items we need to add two // to our value. self.max_length = Some(val + 2); self } /// Add a single item to the selector. /// /// ## Example /// /// ```rust,no_run /// use dialoguer::Select; /// /// fn main() { /// let selection = Select::new() /// .item("Item 1") /// .item("Item 2") /// .interact() /// .unwrap(); /// } /// ``` pub fn item(mut self, item: T) -> Self { self.items.push(item.to_string()); self } /// Adds multiple items to the selector. pub fn items(mut self, items: &[T]) -> Self { for item in items { self.items.push(item.to_string()); } self } /// Sets the select prompt. /// /// By default, when a prompt is set the system also prints out a confirmation after /// the selection. You can opt-out of this with [`report`](Self::report). pub fn with_prompt>(mut self, prompt: S) -> Self { self.prompt = Some(prompt.into()); self.report = true; self } /// Indicates whether to report the selected value after interaction. /// /// The default is to report the selection. pub fn report(mut self, val: bool) -> Self { self.report = val; self } /// Enables user interaction and returns the result. /// /// The user can select the items with the 'Space' bar or 'Enter' and the index of selected item will be returned. /// The dialog is rendered on stderr. /// Result contains `index` if user selected one of items using 'Enter'. /// This unlike [`interact_opt`](Self::interact_opt) does not allow to quit with 'Esc' or 'q'. #[inline] pub fn interact(self) -> Result { self.interact_on(&Term::stderr()) } /// Enables user interaction and returns the result. /// /// The user can select the items with the 'Space' bar or 'Enter' and the index of selected item will be returned. /// The dialog is rendered on stderr. /// Result contains `Some(index)` if user selected one of items using 'Enter' or `None` if user cancelled with 'Esc' or 'q'. /// /// ## Example /// ///```rust,no_run /// use dialoguer::Select; /// /// fn main() { /// let items = vec!["foo", "bar", "baz"]; /// /// let selection = Select::new() /// .with_prompt("What do you choose?") /// .items(&items) /// .interact_opt() /// .unwrap(); /// /// match selection { /// Some(index) => println!("You chose: {}", items[index]), /// None => println!("You did not choose anything.") /// } /// } ///``` #[inline] pub fn interact_opt(self) -> Result> { self.interact_on_opt(&Term::stderr()) } /// Like [`interact`](Self::interact) but allows a specific terminal to be set. #[inline] pub fn interact_on(self, term: &Term) -> Result { Ok(self ._interact_on(term, false)? .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case"))?) } /// Like [`interact_opt`](Self::interact_opt) but allows a specific terminal to be set. #[inline] pub fn interact_on_opt(self, term: &Term) -> Result> { self._interact_on(term, true) } /// Like `interact` but allows a specific terminal to be set. fn _interact_on(self, term: &Term, allow_quit: bool) -> Result> { if !term.is_term() { return Err(io::Error::new(io::ErrorKind::NotConnected, "not a terminal").into()); } if self.items.is_empty() { return Err(io::Error::new( io::ErrorKind::Other, "Empty list of items given to `Select`", ))?; } let mut paging = Paging::new(term, self.items.len(), self.max_length); let mut render = TermThemeRenderer::new(term, self.theme); let mut sel = self.default; let mut size_vec = Vec::new(); for items in self .items .iter() .flat_map(|i| i.split('\n')) .collect::>() { let size = &items.len(); size_vec.push(*size); } term.hide_cursor()?; paging.update_page(sel); loop { if let Some(ref prompt) = self.prompt { paging.render_prompt(|paging_info| render.select_prompt(prompt, paging_info))?; } for (idx, item) in self .items .iter() .enumerate() .skip(paging.current_page * paging.capacity) .take(paging.capacity) { render.select_prompt_item(item, sel == idx)?; } term.flush()?; match term.read_key()? { Key::ArrowDown | Key::Tab | Key::Char('j') => { if sel == !0 { sel = 0; } else { sel = (sel as u64 + 1).rem(self.items.len() as u64) as usize; } } Key::Escape | Key::Char('q') => { if allow_quit { if self.clear { render.clear()?; } else { term.clear_last_lines(paging.capacity)?; } term.show_cursor()?; term.flush()?; return Ok(None); } } Key::ArrowUp | Key::BackTab | Key::Char('k') => { if sel == !0 { sel = self.items.len() - 1; } else { sel = ((sel as i64 - 1 + self.items.len() as i64) % (self.items.len() as i64)) as usize; } } Key::ArrowLeft | Key::Char('h') => { if paging.active { sel = paging.previous_page(); } } Key::ArrowRight | Key::Char('l') => { if paging.active { sel = paging.next_page(); } } Key::Enter | Key::Char(' ') if sel != !0 => { if self.clear { render.clear()?; } if let Some(ref prompt) = self.prompt { if self.report { render.select_prompt_selection(prompt, &self.items[sel])?; } } term.show_cursor()?; term.flush()?; return Ok(Some(sel)); } _ => {} } paging.update(sel)?; if paging.active { render.clear()?; } else { render.clear_preserve_prompt(&size_vec)?; } } } } impl<'a> Select<'a> { /// Creates a select prompt with a specific theme. /// /// ## Example /// /// ```rust,no_run /// use dialoguer::{theme::ColorfulTheme, Select}; /// /// fn main() { /// let selection = Select::with_theme(&ColorfulTheme::default()) /// .items(&["foo", "bar", "baz"]) /// .interact() /// .unwrap(); /// } /// ``` pub fn with_theme(theme: &'a dyn Theme) -> Self { Self { default: !0, items: vec![], prompt: None, report: false, clear: true, max_length: None, theme, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_clone() { let select = Select::new().with_prompt("Do you want to continue?"); let _ = select.clone(); } #[test] fn test_str() { let selections = &[ "Ice Cream", "Vanilla Cupcake", "Chocolate Muffin", "A Pile of sweet, sweet mustard", ]; assert_eq!( Select::new().default(0).items(&selections[..]).items, selections ); } #[test] fn test_string() { let selections = vec!["a".to_string(), "b".to_string()]; assert_eq!( Select::new().default(0).items(&selections[..]).items, selections ); } #[test] fn test_ref_str() { let a = "a"; let b = "b"; let selections = &[a, b]; assert_eq!( Select::new().default(0).items(&selections[..]).items, selections ); } } dialoguer-0.11.0/src/prompts/sort.rs000064400000000000000000000267221046102023000155170ustar 00000000000000use std::{io, ops::Rem}; use console::{Key, Term}; use crate::{ theme::{render::TermThemeRenderer, SimpleTheme, Theme}, Paging, Result, }; /// Renders a sort prompt. /// /// Returns list of indices in original items list sorted according to user input. /// /// ## Example /// /// ```rust,no_run /// use dialoguer::Sort; /// /// fn main() { /// let items = vec!["foo", "bar", "baz"]; /// /// let ordered = Sort::new() /// .with_prompt("Which order do you prefer?") /// .items(&items) /// .interact() /// .unwrap(); /// /// println!("You prefer:"); /// /// for i in ordered { /// println!("{}", items[i]); /// } /// } /// ``` #[derive(Clone)] pub struct Sort<'a> { items: Vec, prompt: Option, report: bool, clear: bool, max_length: Option, theme: &'a dyn Theme, } impl Default for Sort<'static> { fn default() -> Self { Self::new() } } impl Sort<'static> { /// Creates a sort prompt with default theme. pub fn new() -> Self { Self::with_theme(&SimpleTheme) } } impl Sort<'_> { /// Sets the clear behavior of the menu. /// /// The default is to clear the menu after user interaction. pub fn clear(mut self, val: bool) -> Self { self.clear = val; self } /// Sets an optional max length for a page /// /// Max length is disabled by None pub fn max_length(mut self, val: usize) -> Self { // Paging subtracts two from the capacity, paging does this to // make an offset for the page indicator. So to make sure that // we can show the intended amount of items we need to add two // to our value. self.max_length = Some(val + 2); self } /// Add a single item to the selector. pub fn item(mut self, item: T) -> Self { self.items.push(item.to_string()); self } /// Adds multiple items to the selector. pub fn items(mut self, items: &[T]) -> Self { for item in items { self.items.push(item.to_string()); } self } /// Prefaces the menu with a prompt. /// /// By default, when a prompt is set the system also prints out a confirmation after /// the selection. You can opt-out of this with [`report`](#method.report). pub fn with_prompt>(mut self, prompt: S) -> Self { self.prompt = Some(prompt.into()); self } /// Indicates whether to report the selected order after interaction. /// /// The default is to report the selected order. pub fn report(mut self, val: bool) -> Self { self.report = val; self } /// Enables user interaction and returns the result. /// /// The user can order the items with the 'Space' bar and the arrows. On 'Enter' ordered list of the incides of items will be returned. /// The dialog is rendered on stderr. /// Result contains `Vec` if user hit 'Enter'. /// This unlike [`interact_opt`](Self::interact_opt) does not allow to quit with 'Esc' or 'q'. #[inline] pub fn interact(self) -> Result> { self.interact_on(&Term::stderr()) } /// Enables user interaction and returns the result. /// /// The user can order the items with the 'Space' bar and the arrows. On 'Enter' ordered list of the incides of items will be returned. /// The dialog is rendered on stderr. /// Result contains `Some(Vec)` if user hit 'Enter' or `None` if user cancelled with 'Esc' or 'q'. /// /// ## Example /// /// ```rust,no_run /// use dialoguer::Sort; /// /// fn main() { /// let items = vec!["foo", "bar", "baz"]; /// /// let ordered = Sort::new() /// .items(&items) /// .interact_opt() /// .unwrap(); /// /// match ordered { /// Some(positions) => { /// println!("You prefer:"); /// /// for i in positions { /// println!("{}", items[i]); /// } /// }, /// None => println!("You did not prefer anything.") /// } /// } /// ``` #[inline] pub fn interact_opt(self) -> Result>> { self.interact_on_opt(&Term::stderr()) } /// Like [`interact`](Self::interact) but allows a specific terminal to be set. #[inline] pub fn interact_on(self, term: &Term) -> Result> { Ok(self ._interact_on(term, false)? .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case"))?) } /// Like [`interact_opt`](Self::interact_opt) but allows a specific terminal to be set. #[inline] pub fn interact_on_opt(self, term: &Term) -> Result>> { self._interact_on(term, true) } fn _interact_on(self, term: &Term, allow_quit: bool) -> Result>> { if !term.is_term() { return Err(io::Error::new(io::ErrorKind::NotConnected, "not a terminal").into()); } if self.items.is_empty() { return Err(io::Error::new( io::ErrorKind::Other, "Empty list of items given to `Sort`", ))?; } let mut paging = Paging::new(term, self.items.len(), self.max_length); let mut render = TermThemeRenderer::new(term, self.theme); let mut sel = 0; let mut size_vec = Vec::new(); for items in self.items.iter().as_slice() { let size = &items.len(); size_vec.push(*size); } let mut order: Vec<_> = (0..self.items.len()).collect(); let mut checked: bool = false; term.hide_cursor()?; loop { if let Some(ref prompt) = self.prompt { paging.render_prompt(|paging_info| render.sort_prompt(prompt, paging_info))?; } for (idx, item) in order .iter() .enumerate() .skip(paging.current_page * paging.capacity) .take(paging.capacity) { render.sort_prompt_item(&self.items[*item], checked, sel == idx)?; } term.flush()?; match term.read_key()? { Key::ArrowDown | Key::Tab | Key::Char('j') => { let old_sel = sel; if sel == !0 { sel = 0; } else { sel = (sel as u64 + 1).rem(self.items.len() as u64) as usize; } if checked && old_sel != sel { order.swap(old_sel, sel); } } Key::ArrowUp | Key::BackTab | Key::Char('k') => { let old_sel = sel; if sel == !0 { sel = self.items.len() - 1; } else { sel = ((sel as i64 - 1 + self.items.len() as i64) % (self.items.len() as i64)) as usize; } if checked && old_sel != sel { order.swap(old_sel, sel); } } Key::ArrowLeft | Key::Char('h') => { if paging.active { let old_sel = sel; let old_page = paging.current_page; sel = paging.previous_page(); if checked { let indexes: Vec<_> = if old_page == 0 { let indexes1: Vec<_> = (0..=old_sel).rev().collect(); let indexes2: Vec<_> = (sel..self.items.len()).rev().collect(); [indexes1, indexes2].concat() } else { (sel..=old_sel).rev().collect() }; for index in 0..(indexes.len() - 1) { order.swap(indexes[index], indexes[index + 1]); } } } } Key::ArrowRight | Key::Char('l') => { if paging.active { let old_sel = sel; let old_page = paging.current_page; sel = paging.next_page(); if checked { let indexes: Vec<_> = if old_page == paging.pages - 1 { let indexes1: Vec<_> = (old_sel..self.items.len()).collect(); let indexes2: Vec<_> = vec![0]; [indexes1, indexes2].concat() } else { (old_sel..=sel).collect() }; for index in 0..(indexes.len() - 1) { order.swap(indexes[index], indexes[index + 1]); } } } } Key::Char(' ') => { checked = !checked; } Key::Escape | Key::Char('q') => { if allow_quit { if self.clear { render.clear()?; } else { term.clear_last_lines(paging.capacity)?; } term.show_cursor()?; term.flush()?; return Ok(None); } } Key::Enter => { if self.clear { render.clear()?; } if let Some(ref prompt) = self.prompt { if self.report { let list: Vec<_> = order .iter() .enumerate() .map(|(_, item)| self.items[*item].as_str()) .collect(); render.sort_prompt_selection(prompt, &list[..])?; } } term.show_cursor()?; term.flush()?; return Ok(Some(order)); } _ => {} } paging.update(sel)?; if paging.active { render.clear()?; } else { render.clear_preserve_prompt(&size_vec)?; } } } } impl<'a> Sort<'a> { /// Creates a sort prompt with a specific theme. /// /// ## Example /// /// ```rust,no_run /// use dialoguer::{theme::ColorfulTheme, Sort}; /// /// fn main() { /// let ordered = Sort::with_theme(&ColorfulTheme::default()) /// .items(&["foo", "bar", "baz"]) /// .interact() /// .unwrap(); /// } /// ``` pub fn with_theme(theme: &'a dyn Theme) -> Self { Self { items: vec![], clear: true, prompt: None, report: true, max_length: None, theme, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_clone() { let sort = Sort::new().with_prompt("Which order do you prefer?"); let _ = sort.clone(); } } dialoguer-0.11.0/src/theme/colorful.rs000064400000000000000000000316501046102023000157470ustar 00000000000000use std::fmt; use console::{style, Style, StyledObject}; #[cfg(feature = "fuzzy-select")] use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use crate::theme::Theme; /// A colorful theme pub struct ColorfulTheme { /// The style for default values pub defaults_style: Style, /// The style for prompt pub prompt_style: Style, /// Prompt prefix value and style pub prompt_prefix: StyledObject, /// Prompt suffix value and style pub prompt_suffix: StyledObject, /// Prompt on success prefix value and style pub success_prefix: StyledObject, /// Prompt on success suffix value and style pub success_suffix: StyledObject, /// Error prefix value and style pub error_prefix: StyledObject, /// The style for error message pub error_style: Style, /// The style for hints pub hint_style: Style, /// The style for values on prompt success pub values_style: Style, /// The style for active items pub active_item_style: Style, /// The style for inactive items pub inactive_item_style: Style, /// Active item in select prefix value and style pub active_item_prefix: StyledObject, /// Inctive item in select prefix value and style pub inactive_item_prefix: StyledObject, /// Checked item in multi select prefix value and style pub checked_item_prefix: StyledObject, /// Unchecked item in multi select prefix value and style pub unchecked_item_prefix: StyledObject, /// Picked item in sort prefix value and style pub picked_item_prefix: StyledObject, /// Unpicked item in sort prefix value and style pub unpicked_item_prefix: StyledObject, /// Formats the cursor for a fuzzy select prompt #[cfg(feature = "fuzzy-select")] pub fuzzy_cursor_style: Style, // Formats the highlighting if matched characters #[cfg(feature = "fuzzy-select")] pub fuzzy_match_highlight_style: Style, } impl Default for ColorfulTheme { fn default() -> ColorfulTheme { ColorfulTheme { defaults_style: Style::new().for_stderr().cyan(), prompt_style: Style::new().for_stderr().bold(), prompt_prefix: style("?".to_string()).for_stderr().yellow(), prompt_suffix: style("›".to_string()).for_stderr().black().bright(), success_prefix: style("✔".to_string()).for_stderr().green(), success_suffix: style("·".to_string()).for_stderr().black().bright(), error_prefix: style("✘".to_string()).for_stderr().red(), error_style: Style::new().for_stderr().red(), hint_style: Style::new().for_stderr().black().bright(), values_style: Style::new().for_stderr().green(), active_item_style: Style::new().for_stderr().cyan(), inactive_item_style: Style::new().for_stderr(), active_item_prefix: style("❯".to_string()).for_stderr().green(), inactive_item_prefix: style(" ".to_string()).for_stderr(), checked_item_prefix: style("✔".to_string()).for_stderr().green(), unchecked_item_prefix: style("⬚".to_string()).for_stderr().magenta(), picked_item_prefix: style("❯".to_string()).for_stderr().green(), unpicked_item_prefix: style(" ".to_string()).for_stderr(), #[cfg(feature = "fuzzy-select")] fuzzy_cursor_style: Style::new().for_stderr().black().on_white(), #[cfg(feature = "fuzzy-select")] fuzzy_match_highlight_style: Style::new().for_stderr().bold(), } } } impl Theme for ColorfulTheme { /// Formats a prompt. fn format_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result { if !prompt.is_empty() { write!( f, "{} {} ", &self.prompt_prefix, self.prompt_style.apply_to(prompt) )?; } write!(f, "{}", &self.prompt_suffix) } /// Formats an error fn format_error(&self, f: &mut dyn fmt::Write, err: &str) -> fmt::Result { write!( f, "{} {}", &self.error_prefix, self.error_style.apply_to(err) ) } /// Formats an input prompt. fn format_input_prompt( &self, f: &mut dyn fmt::Write, prompt: &str, default: Option<&str>, ) -> fmt::Result { if !prompt.is_empty() { write!( f, "{} {} ", &self.prompt_prefix, self.prompt_style.apply_to(prompt) )?; } match default { Some(default) => write!( f, "{} {} ", self.hint_style.apply_to(&format!("({})", default)), &self.prompt_suffix ), None => write!(f, "{} ", &self.prompt_suffix), } } /// Formats a confirm prompt. fn format_confirm_prompt( &self, f: &mut dyn fmt::Write, prompt: &str, default: Option, ) -> fmt::Result { if !prompt.is_empty() { write!( f, "{} {} ", &self.prompt_prefix, self.prompt_style.apply_to(prompt) )?; } match default { None => write!( f, "{} {}", self.hint_style.apply_to("(y/n)"), &self.prompt_suffix ), Some(true) => write!( f, "{} {} {}", self.hint_style.apply_to("(y/n)"), &self.prompt_suffix, self.defaults_style.apply_to("yes") ), Some(false) => write!( f, "{} {} {}", self.hint_style.apply_to("(y/n)"), &self.prompt_suffix, self.defaults_style.apply_to("no") ), } } /// Formats a confirm prompt after selection. fn format_confirm_prompt_selection( &self, f: &mut dyn fmt::Write, prompt: &str, selection: Option, ) -> fmt::Result { if !prompt.is_empty() { write!( f, "{} {} ", &self.success_prefix, self.prompt_style.apply_to(prompt) )?; } let selection = selection.map(|b| if b { "yes" } else { "no" }); match selection { Some(selection) => { write!( f, "{} {}", &self.success_suffix, self.values_style.apply_to(selection) ) } None => { write!(f, "{}", &self.success_suffix) } } } /// Formats an input prompt after selection. fn format_input_prompt_selection( &self, f: &mut dyn fmt::Write, prompt: &str, sel: &str, ) -> fmt::Result { if !prompt.is_empty() { write!( f, "{} {} ", &self.success_prefix, self.prompt_style.apply_to(prompt) )?; } write!( f, "{} {}", &self.success_suffix, self.values_style.apply_to(sel) ) } /// Formats a password prompt after selection. #[cfg(feature = "password")] fn format_password_prompt_selection( &self, f: &mut dyn fmt::Write, prompt: &str, ) -> fmt::Result { self.format_input_prompt_selection(f, prompt, "********") } /// Formats a multi select prompt after selection. fn format_multi_select_prompt_selection( &self, f: &mut dyn fmt::Write, prompt: &str, selections: &[&str], ) -> fmt::Result { if !prompt.is_empty() { write!( f, "{} {} ", &self.success_prefix, self.prompt_style.apply_to(prompt) )?; } write!(f, "{} ", &self.success_suffix)?; for (idx, sel) in selections.iter().enumerate() { write!( f, "{}{}", if idx == 0 { "" } else { ", " }, self.values_style.apply_to(sel) )?; } Ok(()) } /// Formats a select prompt item. fn format_select_prompt_item( &self, f: &mut dyn fmt::Write, text: &str, active: bool, ) -> fmt::Result { let details = if active { ( &self.active_item_prefix, self.active_item_style.apply_to(text), ) } else { ( &self.inactive_item_prefix, self.inactive_item_style.apply_to(text), ) }; write!(f, "{} {}", details.0, details.1) } /// Formats a multi select prompt item. fn format_multi_select_prompt_item( &self, f: &mut dyn fmt::Write, text: &str, checked: bool, active: bool, ) -> fmt::Result { let details = match (checked, active) { (true, true) => ( &self.checked_item_prefix, self.active_item_style.apply_to(text), ), (true, false) => ( &self.checked_item_prefix, self.inactive_item_style.apply_to(text), ), (false, true) => ( &self.unchecked_item_prefix, self.active_item_style.apply_to(text), ), (false, false) => ( &self.unchecked_item_prefix, self.inactive_item_style.apply_to(text), ), }; write!(f, "{} {}", details.0, details.1) } /// Formats a sort prompt item. fn format_sort_prompt_item( &self, f: &mut dyn fmt::Write, text: &str, picked: bool, active: bool, ) -> fmt::Result { let details = match (picked, active) { (true, true) => ( &self.picked_item_prefix, self.active_item_style.apply_to(text), ), (false, true) => ( &self.unpicked_item_prefix, self.active_item_style.apply_to(text), ), (_, false) => ( &self.unpicked_item_prefix, self.inactive_item_style.apply_to(text), ), }; write!(f, "{} {}", details.0, details.1) } /// Formats a fuzzy select prompt item. #[cfg(feature = "fuzzy-select")] fn format_fuzzy_select_prompt_item( &self, f: &mut dyn fmt::Write, text: &str, active: bool, highlight_matches: bool, matcher: &SkimMatcherV2, search_term: &str, ) -> fmt::Result { write!( f, "{} ", if active { &self.active_item_prefix } else { &self.inactive_item_prefix } )?; if highlight_matches { if let Some((_score, indices)) = matcher.fuzzy_indices(text, search_term) { for (idx, c) in text.chars().enumerate() { if indices.contains(&idx) { if active { write!( f, "{}", self.active_item_style .apply_to(self.fuzzy_match_highlight_style.apply_to(c)) )?; } else { write!(f, "{}", self.fuzzy_match_highlight_style.apply_to(c))?; } } else if active { write!(f, "{}", self.active_item_style.apply_to(c))?; } else { write!(f, "{}", c)?; } } return Ok(()); } } write!(f, "{}", text) } /// Formats a fuzzy-selectprompt after selection. #[cfg(feature = "fuzzy-select")] fn format_fuzzy_select_prompt( &self, f: &mut dyn fmt::Write, prompt: &str, search_term: &str, bytes_pos: usize, ) -> fmt::Result { if !prompt.is_empty() { write!( f, "{} {} ", self.prompt_prefix, self.prompt_style.apply_to(prompt) )?; } let (st_head, remaining) = search_term.split_at(bytes_pos); let mut chars = remaining.chars(); let chr = chars.next().unwrap_or(' '); let st_cursor = self.fuzzy_cursor_style.apply_to(chr); let st_tail = chars.as_str(); let prompt_suffix = &self.prompt_suffix; write!(f, "{prompt_suffix} {st_head}{st_cursor}{st_tail}",) } } dialoguer-0.11.0/src/theme/mod.rs000064400000000000000000000160271046102023000147020ustar 00000000000000//! Customizes the rendering of the elements. use std::fmt; #[cfg(feature = "fuzzy-select")] use console::style; #[cfg(feature = "fuzzy-select")] use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; mod colorful; pub(crate) mod render; mod simple; pub use colorful::ColorfulTheme; pub use simple::SimpleTheme; /// Implements a theme for dialoguer. pub trait Theme { /// Formats a prompt. #[inline] fn format_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result { write!(f, "{}:", prompt) } /// Formats out an error. #[inline] fn format_error(&self, f: &mut dyn fmt::Write, err: &str) -> fmt::Result { write!(f, "error: {}", err) } /// Formats a confirm prompt. fn format_confirm_prompt( &self, f: &mut dyn fmt::Write, prompt: &str, default: Option, ) -> fmt::Result { if !prompt.is_empty() { write!(f, "{} ", &prompt)?; } match default { None => write!(f, "[y/n] ")?, Some(true) => write!(f, "[Y/n] ")?, Some(false) => write!(f, "[y/N] ")?, } Ok(()) } /// Formats a confirm prompt after selection. fn format_confirm_prompt_selection( &self, f: &mut dyn fmt::Write, prompt: &str, selection: Option, ) -> fmt::Result { let selection = selection.map(|b| if b { "yes" } else { "no" }); match selection { Some(selection) if prompt.is_empty() => { write!(f, "{}", selection) } Some(selection) => { write!(f, "{} {}", &prompt, selection) } None if prompt.is_empty() => Ok(()), None => { write!(f, "{}", &prompt) } } } /// Formats an input prompt. fn format_input_prompt( &self, f: &mut dyn fmt::Write, prompt: &str, default: Option<&str>, ) -> fmt::Result { match default { Some(default) if prompt.is_empty() => write!(f, "[{}]: ", default), Some(default) => write!(f, "{} [{}]: ", prompt, default), None => write!(f, "{}: ", prompt), } } /// Formats an input prompt after selection. #[inline] fn format_input_prompt_selection( &self, f: &mut dyn fmt::Write, prompt: &str, sel: &str, ) -> fmt::Result { write!(f, "{}: {}", prompt, sel) } /// Formats a password prompt. #[inline] #[cfg(feature = "password")] fn format_password_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result { self.format_input_prompt(f, prompt, None) } /// Formats a password prompt after selection. #[inline] #[cfg(feature = "password")] fn format_password_prompt_selection( &self, f: &mut dyn fmt::Write, prompt: &str, ) -> fmt::Result { self.format_input_prompt_selection(f, prompt, "[hidden]") } /// Formats a select prompt. #[inline] fn format_select_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result { self.format_prompt(f, prompt) } /// Formats a select prompt after selection. #[inline] fn format_select_prompt_selection( &self, f: &mut dyn fmt::Write, prompt: &str, sel: &str, ) -> fmt::Result { self.format_input_prompt_selection(f, prompt, sel) } /// Formats a multi select prompt. #[inline] fn format_multi_select_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result { self.format_prompt(f, prompt) } /// Formats a sort prompt. #[inline] fn format_sort_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result { self.format_prompt(f, prompt) } /// Formats a multi_select prompt after selection. fn format_multi_select_prompt_selection( &self, f: &mut dyn fmt::Write, prompt: &str, selections: &[&str], ) -> fmt::Result { write!(f, "{}: ", prompt)?; for (idx, sel) in selections.iter().enumerate() { write!(f, "{}{}", if idx == 0 { "" } else { ", " }, sel)?; } Ok(()) } /// Formats a sort prompt after selection. #[inline] fn format_sort_prompt_selection( &self, f: &mut dyn fmt::Write, prompt: &str, selections: &[&str], ) -> fmt::Result { self.format_multi_select_prompt_selection(f, prompt, selections) } /// Formats a select prompt item. fn format_select_prompt_item( &self, f: &mut dyn fmt::Write, text: &str, active: bool, ) -> fmt::Result { write!(f, "{} {}", if active { ">" } else { " " }, text) } /// Formats a multi select prompt item. fn format_multi_select_prompt_item( &self, f: &mut dyn fmt::Write, text: &str, checked: bool, active: bool, ) -> fmt::Result { write!( f, "{} {}", match (checked, active) { (true, true) => "> [x]", (true, false) => " [x]", (false, true) => "> [ ]", (false, false) => " [ ]", }, text ) } /// Formats a sort prompt item. fn format_sort_prompt_item( &self, f: &mut dyn fmt::Write, text: &str, picked: bool, active: bool, ) -> fmt::Result { write!( f, "{} {}", match (picked, active) { (true, true) => "> [x]", (false, true) => "> [ ]", (_, false) => " [ ]", }, text ) } /// Formats a fuzzy select prompt item. #[cfg(feature = "fuzzy-select")] fn format_fuzzy_select_prompt_item( &self, f: &mut dyn fmt::Write, text: &str, active: bool, highlight_matches: bool, matcher: &SkimMatcherV2, search_term: &str, ) -> fmt::Result { write!(f, "{} ", if active { ">" } else { " " })?; if highlight_matches { if let Some((_score, indices)) = matcher.fuzzy_indices(text, search_term) { for (idx, c) in text.chars().enumerate() { if indices.contains(&idx) { write!(f, "{}", style(c).for_stderr().bold())?; } else { write!(f, "{}", c)?; } } return Ok(()); } } write!(f, "{}", text) } /// Formats a fuzzy select prompt. #[cfg(feature = "fuzzy-select")] fn format_fuzzy_select_prompt( &self, f: &mut dyn fmt::Write, prompt: &str, search_term: &str, bytes_pos: usize, ) -> fmt::Result { if !prompt.is_empty() { write!(f, "{prompt} ")?; } let (st_head, st_tail) = search_term.split_at(bytes_pos); write!(f, "{st_head}|{st_tail}") } } dialoguer-0.11.0/src/theme/render.rs000064400000000000000000000177031046102023000154040ustar 00000000000000use std::{fmt, io}; use console::{measure_text_width, Term}; #[cfg(feature = "fuzzy-select")] use fuzzy_matcher::skim::SkimMatcherV2; use crate::{theme::Theme, Result}; /// Helper struct to conveniently render a theme. pub(crate) struct TermThemeRenderer<'a> { term: &'a Term, theme: &'a dyn Theme, height: usize, prompt_height: usize, prompts_reset_height: bool, } impl<'a> TermThemeRenderer<'a> { pub fn new(term: &'a Term, theme: &'a dyn Theme) -> TermThemeRenderer<'a> { TermThemeRenderer { term, theme, height: 0, prompt_height: 0, prompts_reset_height: true, } } #[cfg(feature = "password")] pub fn set_prompts_reset_height(&mut self, val: bool) { self.prompts_reset_height = val; } #[cfg(feature = "password")] pub fn term(&self) -> &Term { self.term } pub fn add_line(&mut self) { self.height += 1; } fn write_formatted_str< F: FnOnce(&mut TermThemeRenderer, &mut dyn fmt::Write) -> fmt::Result, >( &mut self, f: F, ) -> Result { let mut buf = String::new(); f(self, &mut buf).map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; self.height += buf.chars().filter(|&x| x == '\n').count(); self.term.write_str(&buf)?; Ok(measure_text_width(&buf)) } fn write_formatted_line< F: FnOnce(&mut TermThemeRenderer, &mut dyn fmt::Write) -> fmt::Result, >( &mut self, f: F, ) -> Result { let mut buf = String::new(); f(self, &mut buf).map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; self.height += buf.chars().filter(|&x| x == '\n').count() + 1; Ok(self.term.write_line(&buf)?) } fn write_formatted_prompt< F: FnOnce(&mut TermThemeRenderer, &mut dyn fmt::Write) -> fmt::Result, >( &mut self, f: F, ) -> Result { self.write_formatted_line(f)?; if self.prompts_reset_height { self.prompt_height = self.height; self.height = 0; } Ok(()) } fn write_paging_info(buf: &mut dyn fmt::Write, paging_info: (usize, usize)) -> fmt::Result { write!(buf, " [Page {}/{}] ", paging_info.0, paging_info.1) } pub fn error(&mut self, err: &str) -> Result { self.write_formatted_line(|this, buf| this.theme.format_error(buf, err)) } pub fn confirm_prompt(&mut self, prompt: &str, default: Option) -> Result { self.write_formatted_str(|this, buf| this.theme.format_confirm_prompt(buf, prompt, default)) } pub fn confirm_prompt_selection(&mut self, prompt: &str, sel: Option) -> Result { self.write_formatted_prompt(|this, buf| { this.theme.format_confirm_prompt_selection(buf, prompt, sel) }) } #[cfg(feature = "fuzzy-select")] pub fn fuzzy_select_prompt( &mut self, prompt: &str, search_term: &str, cursor_pos: usize, ) -> Result { self.write_formatted_prompt(|this, buf| { this.theme .format_fuzzy_select_prompt(buf, prompt, search_term, cursor_pos) }) } pub fn input_prompt(&mut self, prompt: &str, default: Option<&str>) -> Result { self.write_formatted_str(|this, buf| this.theme.format_input_prompt(buf, prompt, default)) } pub fn input_prompt_selection(&mut self, prompt: &str, sel: &str) -> Result { self.write_formatted_prompt(|this, buf| { this.theme.format_input_prompt_selection(buf, prompt, sel) }) } #[cfg(feature = "password")] pub fn password_prompt(&mut self, prompt: &str) -> Result { self.write_formatted_str(|this, buf| { write!(buf, "\r")?; this.theme.format_password_prompt(buf, prompt) }) } #[cfg(feature = "password")] pub fn password_prompt_selection(&mut self, prompt: &str) -> Result { self.write_formatted_prompt(|this, buf| { this.theme.format_password_prompt_selection(buf, prompt) }) } pub fn select_prompt(&mut self, prompt: &str, paging_info: Option<(usize, usize)>) -> Result { self.write_formatted_prompt(|this, buf| { this.theme.format_select_prompt(buf, prompt)?; if let Some(paging_info) = paging_info { TermThemeRenderer::write_paging_info(buf, paging_info)?; } Ok(()) }) } pub fn select_prompt_selection(&mut self, prompt: &str, sel: &str) -> Result { self.write_formatted_prompt(|this, buf| { this.theme.format_select_prompt_selection(buf, prompt, sel) }) } pub fn select_prompt_item(&mut self, text: &str, active: bool) -> Result { self.write_formatted_line(|this, buf| { this.theme.format_select_prompt_item(buf, text, active) }) } #[cfg(feature = "fuzzy-select")] pub fn fuzzy_select_prompt_item( &mut self, text: &str, active: bool, highlight: bool, matcher: &SkimMatcherV2, search_term: &str, ) -> Result { self.write_formatted_line(|this, buf| { this.theme.format_fuzzy_select_prompt_item( buf, text, active, highlight, matcher, search_term, ) }) } pub fn multi_select_prompt( &mut self, prompt: &str, paging_info: Option<(usize, usize)>, ) -> Result { self.write_formatted_prompt(|this, buf| { this.theme.format_multi_select_prompt(buf, prompt)?; if let Some(paging_info) = paging_info { TermThemeRenderer::write_paging_info(buf, paging_info)?; } Ok(()) }) } pub fn multi_select_prompt_selection(&mut self, prompt: &str, sel: &[&str]) -> Result { self.write_formatted_prompt(|this, buf| { this.theme .format_multi_select_prompt_selection(buf, prompt, sel) }) } pub fn multi_select_prompt_item(&mut self, text: &str, checked: bool, active: bool) -> Result { self.write_formatted_line(|this, buf| { this.theme .format_multi_select_prompt_item(buf, text, checked, active) }) } pub fn sort_prompt(&mut self, prompt: &str, paging_info: Option<(usize, usize)>) -> Result { self.write_formatted_prompt(|this, buf| { this.theme.format_sort_prompt(buf, prompt)?; if let Some(paging_info) = paging_info { TermThemeRenderer::write_paging_info(buf, paging_info)?; } Ok(()) }) } pub fn sort_prompt_selection(&mut self, prompt: &str, sel: &[&str]) -> Result { self.write_formatted_prompt(|this, buf| { this.theme.format_sort_prompt_selection(buf, prompt, sel) }) } pub fn sort_prompt_item(&mut self, text: &str, picked: bool, active: bool) -> Result { self.write_formatted_line(|this, buf| { this.theme .format_sort_prompt_item(buf, text, picked, active) }) } pub fn clear(&mut self) -> Result { self.term .clear_last_lines(self.height + self.prompt_height)?; self.height = 0; self.prompt_height = 0; Ok(()) } pub fn clear_preserve_prompt(&mut self, size_vec: &[usize]) -> Result { let mut new_height = self.height; let prefix_width = 2; //Check each item size, increment on finding an overflow for size in size_vec { if *size > self.term.size().1 as usize { new_height += (((*size as f64 + prefix_width as f64) / self.term.size().1 as f64) .ceil()) as usize - 1; } } self.term.clear_last_lines(new_height)?; self.height = 0; Ok(()) } } dialoguer-0.11.0/src/theme/simple.rs000064400000000000000000000001501046102023000154020ustar 00000000000000use crate::theme::Theme; /// The default theme. pub struct SimpleTheme; impl Theme for SimpleTheme {} dialoguer-0.11.0/src/validate.rs000064400000000000000000000023141046102023000146040ustar 00000000000000//! Provides validation for text inputs /// Trait for input validators. /// /// A generic implementation for `Fn(&str) -> Result<(), E>` is provided /// to facilitate development. pub trait InputValidator { type Err; /// Invoked with the value to validate. /// /// If this produces `Ok(())` then the value is used and parsed, if /// an error is returned validation fails with that error. fn validate(&mut self, input: &T) -> Result<(), Self::Err>; } impl InputValidator for F where F: FnMut(&T) -> Result<(), E>, { type Err = E; fn validate(&mut self, input: &T) -> Result<(), Self::Err> { self(input) } } /// Trait for password validators. #[allow(clippy::ptr_arg)] pub trait PasswordValidator { type Err; /// Invoked with the value to validate. /// /// If this produces `Ok(())` then the value is used and parsed, if /// an error is returned validation fails with that error. fn validate(&self, input: &String) -> Result<(), Self::Err>; } impl PasswordValidator for F where F: Fn(&String) -> Result<(), E>, { type Err = E; fn validate(&self, input: &String) -> Result<(), Self::Err> { self(input) } }