diff options
Diffstat (limited to 'vendor/dialoguer')
35 files changed, 5173 insertions, 0 deletions
diff --git a/vendor/dialoguer/.cargo-checksum.json b/vendor/dialoguer/.cargo-checksum.json new file mode 100644 index 0000000..f3475ad --- /dev/null +++ b/vendor/dialoguer/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"CHANGELOG.md":"abf94b42ba3b3e8982c8f3453277488cdf254717750bdde6788659ef4847d6d1","Cargo.lock":"6491d36f2328f571f3334169897ecf4d1864da6485c0acc7d1880e83681acc1e","Cargo.toml":"543ccb04d5aea6cab6b2c425332effc08773c686ef9ca92ce156db64ffefabb8","LICENSE":"2022c11b24fc1e50fa06a5959e40198a3464f560729ad3d00139abb8a328362d","Makefile":"0247f9342ca2d816bec3173b41761ccba74c2a9fa0122cd7d7d2f46b4b13dc82","README.md":"2793836e3dd9e50b735103c216c2b4c6aa0fc2335816c367ea33956de24db132","examples/buffered.rs":"abaca0a19a6b3bfef72f37c9995341550a17794709515861f0f671c71cd95a56","examples/completion.rs":"01fe4ac9c26790e82ae1ae6263faa6dc143c44deadc8b83101900fc7276e4cb2","examples/confirm.rs":"cc8f6b68dfe07ea462de33abb6e9c2f96fb98903c6f47c48b4c47b2b1aa350cd","examples/editor.rs":"651e7ddef61cd3b2bd4655b619e76ab347dbfa5d1b90a7f95704e5ccd2d53d5e","examples/fuzzyselect.rs":"1a07a63cd2acbf54ee7dc305bb6689cf054186296a0a6bb3033d584e32fbe3c2","examples/history.rs":"dcb59f6d2db60bed5625545cebfda509e38ab61fa9db28c76009ac62a89b4bce","examples/input.rs":"81c27b4f6d05c589c946a03d72db552d16afe50efe9644a871e13dca4f111fb8","examples/multi_select.rs":"d41c1ee68e2b5fd6a93f4f58399d6094caee9883506f4f9f4ca1c7ed12e31068","examples/paging.rs":"66cd79fc4110b807bfd34121746d9ab6707b501a6cf4f8dfb9cfde774ad1fb2d","examples/password.rs":"d04586c76d8811e9b6a93608f7d19f663c6c284ebfe559794c6e7e3ef83b2d08","examples/select.rs":"49c633ce9cf30df11eca564029301f1240bce74e041e40aa8815929d69c5215e","examples/sort.rs":"39dd6c326ffb48b0afc69c0b72b7a256d212eb5c57bc6df6e0c7cb7e461bc753","examples/wizard.rs":"117038bc4a85204a667d45d0a76995e535b0f982befe3cd8bdbc9088401ee418","src/completion.rs":"f17a2af4341dd7098837c52da57f90cfc755b33db21599797ae1ff9d82f2b701","src/edit.rs":"6c4622998c7d0516525361d9cb988a50908045908af00e9398f51333704fdf3a","src/history.rs":"82e3d338a7cdf9172a16baba2617a28601e32df06fe109c954051f3a64dca374","src/lib.rs":"3467d3ed35906f93e1f6cfeceda6f4a79df9448880505c9d7fa1c572c189575f","src/paging.rs":"37e8a9851c80d415e828e43565818f6fd92532eea7d5e948eb6d49c98cc90316","src/prompts/confirm.rs":"5c88bd9a7a06682043bd196bad882e9f9cd59272da866afa5357b57e456752a6","src/prompts/fuzzy_select.rs":"862a5713792117744d88d1c91660ed768770d40d444ef30b07b18e81e6a7d8dc","src/prompts/input.rs":"aecb18548a63093703172ca223dff1dedd79ab86abb856e6e6fa60758dd7d413","src/prompts/mod.rs":"33c9ac2b7bdadcf5461e8a0fcb7db87d7635d7f77a00dc9049550d819f0ae6be","src/prompts/multi_select.rs":"53b21e8b90cbbbe1dddd798a611ebcfaefe3b845a7156411c80240e88f6c2afd","src/prompts/password.rs":"13c25b3e4dd8572fe4e6ab7910847b322da4eafcb36945b4903dc5df0f89606b","src/prompts/select.rs":"c29568116bf5b7dc281832f7fe31884737772aa8f5900a03e8fe44ac37b59e9a","src/prompts/sort.rs":"2a2fb7cb21720b59d7aa80d3fff12d1c87e8125bff99f2289a6a2f92696455a2","src/theme.rs":"3ec68222853ce2cf959900fb443b7f5123418d84db7cab64f24a7b6ddde9d5e2","src/validate.rs":"c61723bfbcceec71f7b2802203791ac9b81590c1cbe85ab1c17ad5cfc907ed97"},"package":"59c6f2989294b9a498d3ad5491a79c6deb604617378e1cdc4bfc1c1361fe2f87"}
\ No newline at end of file diff --git a/vendor/dialoguer/CHANGELOG.md b/vendor/dialoguer/CHANGELOG.md new file mode 100644 index 0000000..065f015 --- /dev/null +++ b/vendor/dialoguer/CHANGELOG.md @@ -0,0 +1,116 @@ +# Changelog + +## 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 + +### Bugfixes + +* `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 diff --git a/vendor/dialoguer/Cargo.lock b/vendor/dialoguer/Cargo.lock new file mode 100644 index 0000000..da31c0b --- /dev/null +++ b/vendor/dialoguer/Cargo.lock @@ -0,0 +1,351 @@ +# 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.10.4" +dependencies = [ + "console", + "fuzzy-matcher", + "shell-words", + "tempfile", + "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 = "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 = "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 = "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-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" diff --git a/vendor/dialoguer/Cargo.toml b/vendor/dialoguer/Cargo.toml new file mode 100644 index 0000000..9c50fe5 --- /dev/null +++ b/vendor/dialoguer/Cargo.toml @@ -0,0 +1,83 @@ +# 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 = "2018" +name = "dialoguer" +version = "0.10.4" +authors = [ + "Armin Ronacher <armin.ronacher@active-4.com>", + "Pavan Kumar Sunkara <pavan.sss1991@gmail.com>", +] +description = "A command line prompting library." +homepage = "https://github.com/mitsuhiko/dialoguer" +documentation = "https://docs.rs/dialoguer" +readme = "README.md" +keywords = [ + "cli", + "menu", + "prompt", +] +categories = ["command-line-interface"] +license = "MIT" +repository = "https://github.com/mitsuhiko/dialoguer" + +[package.metadata.docs.rs] +all-features = true + +[[example]] +name = "password" +required-features = ["password"] + +[[example]] +name = "editor" +required-features = ["editor"] + +[[example]] +name = "fuzzyselect" +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.zeroize] +version = "1.1.1" +optional = true + +[features] +completion = [] +default = [ + "editor", + "password", +] +editor = ["tempfile"] +fuzzy-select = ["fuzzy-matcher"] +history = [] +password = ["zeroize"] diff --git a/vendor/dialoguer/LICENSE b/vendor/dialoguer/LICENSE new file mode 100644 index 0000000..dc9a85c --- /dev/null +++ b/vendor/dialoguer/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2017 Armin Ronacher <armin.ronacher@active-4.com> + +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. + diff --git a/vendor/dialoguer/Makefile b/vendor/dialoguer/Makefile new file mode 100644 index 0000000..6d25c8a --- /dev/null +++ b/vendor/dialoguer/Makefile @@ -0,0 +1,30 @@ +all: test + +check: + @cargo check --all-features + +build: + @cargo build --all-features + +doc: + @cargo doc --all-features + +test: + @echo "CARGO TESTS" + @cargo test + @cargo test --all-features + @cargo test --no-default-features + +format: + @rustup component add rustfmt 2> /dev/null + @cargo fmt --all + +format-check: + @rustup component add rustfmt 2> /dev/null + @cargo fmt --all -- --check + +lint: + @rustup component add clippy 2> /dev/null + @cargo clippy --examples --tests + +.PHONY: all doc build check test format format-check lint diff --git a/vendor/dialoguer/README.md b/vendor/dialoguer/README.md new file mode 100644 index 0000000..2eb7ede --- /dev/null +++ b/vendor/dialoguer/README.md @@ -0,0 +1,19 @@ +# 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) diff --git a/vendor/dialoguer/examples/buffered.rs b/vendor/dialoguer/examples/buffered.rs new file mode 100644 index 0000000..96f62bd --- /dev/null +++ b/vendor/dialoguer/examples/buffered.rs @@ -0,0 +1,42 @@ +use 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(); +} diff --git a/vendor/dialoguer/examples/completion.rs b/vendor/dialoguer/examples/completion.rs new file mode 100644 index 0000000..76d790b --- /dev/null +++ b/vendor/dialoguer/examples/completion.rs @@ -0,0 +1,44 @@ +use dialoguer::{theme::ColorfulTheme, Completion, Input}; + +fn main() -> Result<(), std::io::Error> { + println!("Use the Right arrow or Tab to complete your command"); + let completion = MyCompletion::default(); + Input::<String>::with_theme(&ColorfulTheme::default()) + .with_prompt("dialoguer") + .completion_with(&completion) + .interact_text()?; + Ok(()) +} + +struct MyCompletion { + options: Vec<String>, +} + +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<String> { + let matches = self + .options + .iter() + .filter(|option| option.starts_with(input)) + .collect::<Vec<_>>(); + + if matches.len() == 1 { + Some(matches[0].to_string()) + } else { + None + } + } +} diff --git a/vendor/dialoguer/examples/confirm.rs b/vendor/dialoguer/examples/confirm.rs new file mode 100644 index 0000000..3b1d3ca --- /dev/null +++ b/vendor/dialoguer/examples/confirm.rs @@ -0,0 +1,70 @@ +use 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"), + } +} diff --git a/vendor/dialoguer/examples/editor.rs b/vendor/dialoguer/examples/editor.rs new file mode 100644 index 0000000..5cc21db --- /dev/null +++ b/vendor/dialoguer/examples/editor.rs @@ -0,0 +1,10 @@ +use dialoguer::Editor; + +fn main() { + if let Some(rv) = Editor::new().edit("Enter a commit message").unwrap() { + println!("Your message:"); + println!("{}", rv); + } else { + println!("Abort!"); + } +} diff --git a/vendor/dialoguer/examples/fuzzyselect.rs b/vendor/dialoguer/examples/fuzzyselect.rs new file mode 100644 index 0000000..144d8b6 --- /dev/null +++ b/vendor/dialoguer/examples/fuzzyselect.rs @@ -0,0 +1,43 @@ +use 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]); +} diff --git a/vendor/dialoguer/examples/history.rs b/vendor/dialoguer/examples/history.rs new file mode 100644 index 0000000..0d69b27 --- /dev/null +++ b/vendor/dialoguer/examples/history.rs @@ -0,0 +1,51 @@ +use 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::<String>::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<String>, +} + +impl Default for MyHistory { + fn default() -> Self { + MyHistory { + max: 4, + history: VecDeque::new(), + } + } +} + +impl<T: ToString> History<T> for MyHistory { + fn read(&self, pos: usize) -> Option<String> { + 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()); + } +} diff --git a/vendor/dialoguer/examples/input.rs b/vendor/dialoguer/examples/input.rs new file mode 100644 index 0000000..8691c2e --- /dev/null +++ b/vendor/dialoguer/examples/input.rs @@ -0,0 +1,44 @@ +use 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); +} diff --git a/vendor/dialoguer/examples/multi_select.rs b/vendor/dialoguer/examples/multi_select.rs new file mode 100644 index 0000000..97d3bae --- /dev/null +++ b/vendor/dialoguer/examples/multi_select.rs @@ -0,0 +1,42 @@ +use 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]); + } + } +} diff --git a/vendor/dialoguer/examples/paging.rs b/vendor/dialoguer/examples/paging.rs new file mode 100644 index 0000000..63f9726 --- /dev/null +++ b/vendor/dialoguer/examples/paging.rs @@ -0,0 +1,43 @@ +use 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]); +} diff --git a/vendor/dialoguer/examples/password.rs b/vendor/dialoguer/examples/password.rs new file mode 100644 index 0000000..d85df1b --- /dev/null +++ b/vendor/dialoguer/examples/password.rs @@ -0,0 +1,18 @@ +use 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.len() > 3 { + Ok(()) + } else { + Err("Password must be longer than 3") + } + }) + .interact() + .unwrap(); + + println!("Your password is {} characters long", password.len()); +} diff --git a/vendor/dialoguer/examples/select.rs b/vendor/dialoguer/examples/select.rs new file mode 100644 index 0000000..3382e8b --- /dev/null +++ b/vendor/dialoguer/examples/select.rs @@ -0,0 +1,42 @@ +use 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]); +} diff --git a/vendor/dialoguer/examples/sort.rs b/vendor/dialoguer/examples/sort.rs new file mode 100644 index 0000000..ae920aa --- /dev/null +++ b/vendor/dialoguer/examples/sort.rs @@ -0,0 +1,32 @@ +use 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]]); +} diff --git a/vendor/dialoguer/examples/wizard.rs b/vendor/dialoguer/examples/wizard.rs new file mode 100644 index 0000000..b914032 --- /dev/null +++ b/vendor/dialoguer/examples/wizard.rs @@ -0,0 +1,81 @@ +use 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<String>, + cert: Option<String>, +} + +fn init_config() -> Result<Option<Config>, Box<dyn Error>> { + 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), + } +} diff --git a/vendor/dialoguer/src/completion.rs b/vendor/dialoguer/src/completion.rs new file mode 100644 index 0000000..98c2a9c --- /dev/null +++ b/vendor/dialoguer/src/completion.rs @@ -0,0 +1,4 @@ +/// Trait for completion handling. +pub trait Completion { + fn get(&self, input: &str) -> Option<String>; +} diff --git a/vendor/dialoguer/src/edit.rs b/vendor/dialoguer/src/edit.rs new file mode 100644 index 0000000..f69844c --- /dev/null +++ b/vendor/dialoguer/src/edit.rs @@ -0,0 +1,131 @@ +use std::{ + env, + ffi::{OsStr, OsString}, + fs, io, + io::{Read, Write}, + process, +}; + +/// 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<S: AsRef<OsStr>>(&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) -> io::Result<Option<String>> { + 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)) + } +} diff --git a/vendor/dialoguer/src/history.rs b/vendor/dialoguer/src/history.rs new file mode 100644 index 0000000..d0818cc --- /dev/null +++ b/vendor/dialoguer/src/history.rs @@ -0,0 +1,15 @@ +/// Trait for history handling. +pub trait History<T> { + /// 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<String>; + + /// 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); +} diff --git a/vendor/dialoguer/src/lib.rs b/vendor/dialoguer/src/lib.rs new file mode 100644 index 0000000..cd8e307 --- /dev/null +++ b/vendor/dialoguer/src/lib.rs @@ -0,0 +1,62 @@ +//! 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)] + +#[cfg(feature = "completion")] +pub use completion::Completion; +pub use console; +#[cfg(feature = "editor")] +pub use edit::Editor; +#[cfg(feature = "history")] +pub use history::History; +use paging::Paging; +pub use prompts::{ + confirm::Confirm, input::Input, multi_select::MultiSelect, select::Select, sort::Sort, +}; +pub use validate::Validator; + +#[cfg(feature = "fuzzy-select")] +pub use prompts::fuzzy_select::FuzzySelect; + +#[cfg(feature = "password")] +pub use prompts::password::Password; + +#[cfg(feature = "completion")] +mod completion; +#[cfg(feature = "editor")] +mod edit; +#[cfg(feature = "history")] +mod history; +mod paging; +mod prompts; +pub mod theme; +mod validate; diff --git a/vendor/dialoguer/src/paging.rs b/vendor/dialoguer/src/paging.rs new file mode 100644 index 0000000..f85d9b3 --- /dev/null +++ b/vendor/dialoguer/src/paging.rs @@ -0,0 +1,118 @@ +use std::io; + +use console::Term; + +/// 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<usize>, + 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<usize>) -> 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, + } + } + + /// Updates all internal based on the current terminal size and cursor position + pub fn update(&mut self, cursor_pos: usize) -> io::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)?; + } + + 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; + } + + 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<F>(&mut self, mut render_prompt: F) -> io::Result<()> + where + F: FnMut(Option<(usize, usize)>) -> io::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 + } +} diff --git a/vendor/dialoguer/src/prompts/confirm.rs b/vendor/dialoguer/src/prompts/confirm.rs new file mode 100644 index 0000000..24bcc4c --- /dev/null +++ b/vendor/dialoguer/src/prompts/confirm.rs @@ -0,0 +1,287 @@ +use std::io; + +use crate::theme::{SimpleTheme, TermThemeRenderer, Theme}; + +use console::{Key, Term}; + +/// Renders a confirm prompt. +/// +/// ## Example usage +/// +/// ```rust,no_run +/// # fn test() -> Result<(), Box<dyn std::error::Error>> { +/// use dialoguer::Confirm; +/// +/// if Confirm::new().with_prompt("Do you want to continue?").interact()? { +/// println!("Looks like you want to continue"); +/// } else { +/// println!("nevermind then :("); +/// } +/// # Ok(()) } fn main() { test().unwrap(); } +/// ``` +pub struct Confirm<'a> { + prompt: String, + report: bool, + default: Option<bool>, + 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. + pub fn new() -> Self { + Self::with_theme(&SimpleTheme) + } +} + +impl Confirm<'_> { + /// Sets the confirm prompt. + pub fn with_prompt<S: Into<String>>(&mut self, prompt: S) -> &mut 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) -> &mut Self { + self.report = val; + self + } + + #[deprecated(note = "Use with_prompt() instead", since = "0.6.0")] + #[inline] + pub fn with_text(&mut self, text: &str) -> &mut Self { + self.with_prompt(text) + } + + /// 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) -> &mut 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) -> &mut 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) -> &mut 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) -> io::Result<bool> { + 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'. + #[inline] + pub fn interact_opt(&self) -> io::Result<Option<bool>> { + self.interact_on_opt(&Term::stderr()) + } + + /// Like [interact](#method.interact) but allows a specific terminal to be set. + /// + /// ## Examples + /// + /// ```rust,no_run + /// use dialoguer::Confirm; + /// use console::Term; + /// + /// # fn main() -> std::io::Result<()> { + /// let proceed = Confirm::new() + /// .with_prompt("Do you wish to continue?") + /// .interact_on(&Term::stderr())?; + /// # Ok(()) + /// # } + /// ``` + #[inline] + pub fn interact_on(&self, term: &Term) -> io::Result<bool> { + 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. + /// + /// ## Examples + /// ```rust,no_run + /// use dialoguer::Confirm; + /// use console::Term; + /// + /// fn main() -> std::io::Result<()> { + /// let confirmation = Confirm::new() + /// .interact_on_opt(&Term::stdout())?; + /// + /// match confirmation { + /// Some(answer) => println!("User answered {}", if answer { "yes" } else { "no " }), + /// None => println!("User did not answer") + /// } + /// + /// Ok(()) + /// } + /// ``` + #[inline] + pub fn interact_on_opt(&self, term: &Term) -> io::Result<Option<bool>> { + self._interact_on(term, true) + } + + fn _interact_on(&self, term: &Term, allow_quit: bool) -> io::Result<Option<bool>> { + 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; + } + Key::Unknown => { + return Err(io::Error::new( + io::ErrorKind::NotConnected, + "Not a terminal", + )) + } + _ => { + 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, + Key::Unknown => { + return Err(io::Error::new( + io::ErrorKind::NotConnected, + "Not a terminal", + )) + } + _ => { + 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. + /// + /// ## Examples + /// ```rust,no_run + /// use dialoguer::{ + /// Confirm, + /// theme::ColorfulTheme + /// }; + /// + /// # fn main() -> std::io::Result<()> { + /// let proceed = Confirm::with_theme(&ColorfulTheme::default()) + /// .with_prompt("Do you wish to continue?") + /// .interact()?; + /// # Ok(()) + /// # } + /// ``` + pub fn with_theme(theme: &'a dyn Theme) -> Self { + Self { + prompt: "".into(), + report: true, + default: None, + show_default: true, + wait_for_newline: false, + theme, + } + } +} diff --git a/vendor/dialoguer/src/prompts/fuzzy_select.rs b/vendor/dialoguer/src/prompts/fuzzy_select.rs new file mode 100644 index 0000000..9b7f992 --- /dev/null +++ b/vendor/dialoguer/src/prompts/fuzzy_select.rs @@ -0,0 +1,326 @@ +use crate::theme::{SimpleTheme, TermThemeRenderer, Theme}; +use console::{Key, Term}; +use fuzzy_matcher::FuzzyMatcher; +use std::{io, ops::Rem}; + +/// Renders a selection menu that user can fuzzy match to reduce set. +/// +/// 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. +/// +/// ## Examples +/// +/// ```rust,no_run +/// use dialoguer::{ +/// FuzzySelect, +/// theme::ColorfulTheme +/// }; +/// use console::Term; +/// +/// fn main() -> std::io::Result<()> { +/// let items = vec!["Item 1", "item 2"]; +/// let selection = FuzzySelect::with_theme(&ColorfulTheme::default()) +/// .items(&items) +/// .default(0) +/// .interact_on_opt(&Term::stderr())?; +/// +/// match selection { +/// Some(index) => println!("User selected item : {}", items[index]), +/// None => println!("User did not select anything") +/// } +/// +/// Ok(()) +/// } +/// ``` + +pub struct FuzzySelect<'a> { + default: Option<usize>, + items: Vec<String>, + prompt: String, + report: bool, + clear: bool, + highlight_matches: bool, + max_length: Option<usize>, + 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 the prompt with a specific text. + 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) -> &mut Self { + self.clear = val; + self + } + + /// Sets a default for the menu + pub fn default(&mut self, val: usize) -> &mut Self { + self.default = Some(val); + self + } + + /// Add a single item to the fuzzy selector. + pub fn item<T: ToString>(&mut self, item: T) -> &mut Self { + self.items.push(item.to_string()); + self + } + + /// Adds multiple items to the fuzzy selector. + pub fn items<T: ToString>(&mut self, items: &[T]) -> &mut 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<S: Into<String>>(&mut self, initial_text: S) -> &mut 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<S: Into<String>>(&mut self, prompt: S) -> &mut 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) -> &mut 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) -> &mut Self { + self.highlight_matches = 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) -> &mut 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](#method.interact_opt) does not allow to quit with 'Esc' or 'q'. + #[inline] + pub fn interact(&self) -> io::Result<usize> { + 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'. + #[inline] + pub fn interact_opt(&self) -> io::Result<Option<usize>> { + self.interact_on_opt(&Term::stderr()) + } + + /// Like `interact` but allows a specific terminal to be set. + #[inline] + pub fn interact_on(&self, term: &Term) -> io::Result<usize> { + self._interact_on(term, false)? + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case")) + } + + /// Like `interact` but allows a specific terminal to be set. + #[inline] + pub fn interact_on_opt(&self, term: &Term) -> io::Result<Option<usize>> { + self._interact_on(term, true) + } + + /// Like `interact` but allows a specific terminal to be set. + fn _interact_on(&self, term: &Term, allow_quit: bool) -> io::Result<Option<usize>> { + // Place cursor at the end of the search term + let mut position = self.initial_text.len(); + 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()?; + + loop { + render.clear()?; + render.fuzzy_select_prompt(self.prompt.as_str(), &search_term, position)?; + + // 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::<Vec<_>>(); + + // 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) { + (Key::Escape, _) if allow_quit => { + if self.clear { + render.clear()?; + term.flush()?; + } + term.show_cursor()?; + return Ok(None); + } + (Key::ArrowUp | Key::BackTab, _) 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, _) 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, _) if position > 0 => { + position -= 1; + term.flush()?; + } + (Key::ArrowRight, _) if position < search_term.len() => { + position += 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 position > 0 => { + position -= 1; + search_term.remove(position); + term.flush()?; + } + (Key::Char(chr), _) if !chr.is_ascii_control() => { + search_term.insert(position, chr); + position += 1; + term.flush()?; + sel = Some(0); + starting_row = 0; + } + + _ => {} + } + + render.clear_preserve_prompt(&size_vec)?; + } + } +} + +impl<'a> FuzzySelect<'a> { + /// Same as `new` but with a specific theme. + pub fn with_theme(theme: &'a dyn Theme) -> Self { + Self { + default: None, + items: vec![], + prompt: "".into(), + report: true, + clear: true, + highlight_matches: true, + max_length: None, + theme, + initial_text: "".into(), + } + } +} diff --git a/vendor/dialoguer/src/prompts/input.rs b/vendor/dialoguer/src/prompts/input.rs new file mode 100644 index 0000000..b7cd829 --- /dev/null +++ b/vendor/dialoguer/src/prompts/input.rs @@ -0,0 +1,691 @@ +use std::{cmp::Ordering, fmt::Debug, io, iter, str::FromStr}; + +#[cfg(feature = "completion")] +use crate::completion::Completion; +#[cfg(feature = "history")] +use crate::history::History; +use crate::{ + theme::{SimpleTheme, TermThemeRenderer, Theme}, + validate::Validator, +}; + +use console::{Key, Term}; + +type ValidatorCallback<'a, T> = Box<dyn FnMut(&T) -> Option<String> + 'a>; + +/// Renders an input prompt. +/// +/// ## Example usage +/// +/// ```rust,no_run +/// use dialoguer::Input; +/// +/// # fn test() -> Result<(), Box<dyn std::error::Error>> { +/// let input : String = Input::new() +/// .with_prompt("Tea or coffee?") +/// .with_initial_text("Yes") +/// .default("No".into()) +/// .interact_text()?; +/// # Ok(()) +/// # } +/// ``` +/// It can also be used with turbofish notation: +/// +/// ```rust,no_run +/// # fn test() -> Result<(), Box<dyn std::error::Error>> { +/// # use dialoguer::Input; +/// let input = Input::<String>::new() +/// .interact_text()?; +/// # Ok(()) +/// # } +/// ``` +pub struct Input<'a, T> { + prompt: String, + post_completion_text: Option<String>, + report: bool, + default: Option<T>, + show_default: bool, + initial_text: Option<String>, + theme: &'a dyn Theme, + permit_empty: bool, + validator: Option<ValidatorCallback<'a, T>>, + #[cfg(feature = "history")] + history: Option<&'a mut dyn History<T>>, + #[cfg(feature = "completion")] + completion: Option<&'a dyn Completion>, +} + +impl<T> Default for Input<'static, T> { + fn default() -> Self { + Self::new() + } +} + +impl<T> Input<'_, T> { + /// Creates an input prompt. + pub fn new() -> Self { + Self::with_theme(&SimpleTheme) + } + + /// Sets the input prompt. + pub fn with_prompt<S: Into<String>>(&mut self, prompt: S) -> &mut 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<S: Into<String>>( + &mut self, + post_completion_text: S, + ) -> &mut 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) -> &mut Self { + self.report = val; + self + } + + /// Sets initial text that user can accept or erase. + pub fn with_initial_text<S: Into<String>>(&mut self, val: S) -> &mut 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) -> &mut 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) -> &mut 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) -> &mut Self { + self.show_default = val; + self + } +} + +impl<'a, T> Input<'a, T> { + /// Creates an input prompt with a specific theme. + 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 + /// + /// ```no_run + /// # use dialoguer::{History, Input}; + /// # use std::{collections::VecDeque, fmt::Display}; + /// let mut history = MyHistory::default(); + /// loop { + /// if let Ok(input) = Input::<String>::new() + /// .with_prompt("hist") + /// .history_with(&mut history) + /// .interact_text() + /// { + /// // Do something with the input + /// } + /// } + /// # struct MyHistory { + /// # history: VecDeque<String>, + /// # } + /// # + /// # impl Default for MyHistory { + /// # fn default() -> Self { + /// # MyHistory { + /// # history: VecDeque::new(), + /// # } + /// # } + /// # } + /// # + /// # impl<T: ToString> History<T> for MyHistory { + /// # fn read(&self, pos: usize) -> Option<String> { + /// # self.history.get(pos).cloned() + /// # } + /// # + /// # fn write(&mut self, val: &T) + /// # where + /// # { + /// # self.history.push_front(val.to_string()); + /// # } + /// # } + /// ``` + #[cfg(feature = "history")] + pub fn history_with<H>(&mut self, history: &'a mut H) -> &mut Self + where + H: History<T>, + { + self.history = Some(history); + self + } + + /// Enable completion + #[cfg(feature = "completion")] + pub fn completion_with<C>(&mut self, completion: &'a C) -> &mut Self + where + C: Completion, + { + self.completion = Some(completion); + self + } +} + +impl<'a, T> Input<'a, T> +where + T: 'a, +{ + /// Registers a validator. + /// + /// # Example + /// + /// ```no_run + /// # use dialoguer::Input; + /// 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<V>(&mut self, mut validator: V) -> &mut Self + where + V: Validator<T> + 'a, + V::Err: ToString, + { + let mut old_validator_func = self.validator.take(); + + self.validator = Some(Box::new(move |value: &T| -> Option<String> { + if let Some(old) = old_validator_func.as_mut() { + if let Some(err) = old(value) { + return Some(err); + } + } + + match validator.validate(value) { + Ok(()) => None, + Err(err) => Some(err.to_string()), + } + })); + + self + } +} + +impl<T> Input<'_, T> +where + T: Clone + ToString + FromStr, + <T as FromStr>::Err: Debug + ToString, +{ + /// Enables the user to enter a printable ascii sequence and returns the result. + /// + /// Its difference from [`interact`](#method.interact) is that it only allows ascii characters for string, + /// while [`interact`](#method.interact) allows virtually any character to be used e.g arrow keys. + /// + /// The dialog is rendered on stderr. + pub fn interact_text(&mut self) -> io::Result<T> { + self.interact_text_on(&Term::stderr()) + } + + /// Like [`interact_text`](#method.interact_text) but allows a specific terminal to be set. + pub fn interact_text_on(&mut self, term: &Term) -> io::Result<T> { + 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 + }, + )?; + + // Read input by keystroke so that we can suppress ascii control characters + if !term.features().is_attended() { + return Ok("".to_owned().parse::<T>().unwrap()); + } + + let mut chars: Vec<char> = 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.len(); + 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.len() - 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.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.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::<String>(); + + 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(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::<T>() { + Ok(value) => { + if let Some(ref mut validator) = self.validator { + if let Some(err) = validator(&value) { + render.error(&err)?; + continue; + } + } + + #[cfg(feature = "history")] + if let Some(history) = &mut self.history { + history.write(&value); + } + + 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; + } + } + } + } +} + +impl<T> Input<'_, T> +where + T: Clone + ToString + FromStr, + <T as FromStr>::Err: ToString, +{ + /// 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`](#method.interact_text). + /// + /// If the user confirms the result is `true`, `false` otherwise. + /// The dialog is rendered on stderr. + pub fn interact(&mut self) -> io::Result<T> { + self.interact_on(&Term::stderr()) + } + + /// Like [`interact`](#method.interact) but allows a specific terminal to be set. + pub fn interact_on(&mut self, term: &Term) -> io::Result<T> { + 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(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::<T>() { + Ok(value) => { + if let Some(ref mut validator) = self.validator { + if let Some(err) = validator(&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; + } + } + } + } +} diff --git a/vendor/dialoguer/src/prompts/mod.rs b/vendor/dialoguer/src/prompts/mod.rs new file mode 100644 index 0000000..1c13185 --- /dev/null +++ b/vendor/dialoguer/src/prompts/mod.rs @@ -0,0 +1,13 @@ +#![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; diff --git a/vendor/dialoguer/src/prompts/multi_select.rs b/vendor/dialoguer/src/prompts/multi_select.rs new file mode 100644 index 0000000..eed55a1 --- /dev/null +++ b/vendor/dialoguer/src/prompts/multi_select.rs @@ -0,0 +1,356 @@ +use std::{io, iter::repeat, ops::Rem}; + +use crate::{ + theme::{SimpleTheme, TermThemeRenderer, Theme}, + Paging, +}; + +use console::{Key, Term}; + +/// Renders a multi select prompt. +/// +/// ## Example usage +/// ```rust,no_run +/// # fn test() -> Result<(), Box<dyn std::error::Error>> { +/// use dialoguer::MultiSelect; +/// +/// let items = vec!["Option 1", "Option 2"]; +/// let chosen : Vec<usize> = MultiSelect::new() +/// .items(&items) +/// .interact()?; +/// # Ok(()) +/// # } +/// ``` +pub struct MultiSelect<'a> { + defaults: Vec<bool>, + items: Vec<String>, + prompt: Option<String>, + report: bool, + clear: bool, + max_length: Option<usize>, + theme: &'a dyn Theme, +} + +impl Default for MultiSelect<'static> { + fn default() -> Self { + Self::new() + } +} + +impl MultiSelect<'static> { + /// Creates a multi select prompt. + 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) -> &mut Self { + self.clear = val; + self + } + + /// Sets a defaults for the menu. + pub fn defaults(&mut self, val: &[bool]) -> &mut 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) -> &mut 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<T: ToString>(&mut self, item: T) -> &mut Self { + self.item_checked(item, false) + } + + /// Add a single item to the selector with a default checked state. + pub fn item_checked<T: ToString>(&mut self, item: T, checked: bool) -> &mut Self { + self.items.push(item.to_string()); + self.defaults.push(checked); + self + } + + /// Adds multiple items to the selector. + pub fn items<T: ToString>(&mut self, items: &[T]) -> &mut 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<T: ToString>(&mut self, items: &[(T, bool)]) -> &mut 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`](#method.report). + pub fn with_prompt<S: Into<String>>(&mut self, prompt: S) -> &mut 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) -> &mut 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<index>` 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) -> io::Result<Vec<usize>> { + 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<index>)` if user hit 'Enter' or `None` if user cancelled with 'Esc' or 'q'. + #[inline] + pub fn interact_opt(&self) -> io::Result<Option<Vec<usize>>> { + self.interact_on_opt(&Term::stderr()) + } + + /// Like [interact](#method.interact) but allows a specific terminal to be set. + /// + /// ## Examples + ///```rust,no_run + /// use dialoguer::MultiSelect; + /// use console::Term; + /// + /// fn main() -> std::io::Result<()> { + /// let selections = MultiSelect::new() + /// .item("Option A") + /// .item("Option B") + /// .interact_on(&Term::stderr())?; + /// + /// println!("User selected options at indices {:?}", selections); + /// + /// Ok(()) + /// } + ///``` + #[inline] + pub fn interact_on(&self, term: &Term) -> io::Result<Vec<usize>> { + 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. + /// + /// ## Examples + /// ```rust,no_run + /// use dialoguer::MultiSelect; + /// use console::Term; + /// + /// fn main() -> std::io::Result<()> { + /// let selections = MultiSelect::new() + /// .item("Option A") + /// .item("Option B") + /// .interact_on_opt(&Term::stdout())?; + /// + /// match selections { + /// Some(positions) => println!("User selected options at indices {:?}", positions), + /// None => println!("User exited using Esc or q") + /// } + /// + /// Ok(()) + /// } + /// ``` + #[inline] + pub fn interact_on_opt(&self, term: &Term) -> io::Result<Option<Vec<usize>>> { + self._interact_on(term, true) + } + + fn _interact_on(&self, term: &Term, allow_quit: bool) -> io::Result<Option<Vec<usize>>> { + 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::<Vec<_>>() + { + let size = &items.len(); + size_vec.push(*size); + } + + let mut checked: Vec<bool> = 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. + pub fn with_theme(theme: &'a dyn Theme) -> Self { + Self { + items: vec![], + defaults: vec![], + clear: true, + prompt: None, + report: true, + max_length: None, + theme, + } + } +} diff --git a/vendor/dialoguer/src/prompts/password.rs b/vendor/dialoguer/src/prompts/password.rs new file mode 100644 index 0000000..7327605 --- /dev/null +++ b/vendor/dialoguer/src/prompts/password.rs @@ -0,0 +1,194 @@ +use std::io; + +use crate::{ + theme::{SimpleTheme, TermThemeRenderer, Theme}, + validate::PasswordValidator, +}; + +use console::Term; +use zeroize::Zeroizing; + +type PasswordValidatorCallback<'a> = Box<dyn Fn(&String) -> Option<String> + 'a>; + +/// Renders a password input prompt. +/// +/// ## Example usage +/// +/// ```rust,no_run +/// # fn test() -> Result<(), Box<std::error::Error>> { +/// use dialoguer::Password; +/// +/// let password = Password::new().with_prompt("New Password") +/// .with_confirmation("Confirm password", "Passwords mismatching") +/// .interact()?; +/// println!("Length of the password is: {}", password.len()); +/// # Ok(()) } fn main() { test().unwrap(); } +/// ``` +pub struct Password<'a> { + prompt: String, + report: bool, + theme: &'a dyn Theme, + allow_empty_password: bool, + confirmation_prompt: Option<(String, String)>, + validator: Option<PasswordValidatorCallback<'a>>, +} + +impl Default for Password<'static> { + fn default() -> Password<'static> { + Self::new() + } +} + +impl Password<'static> { + /// Creates a password input prompt. + pub fn new() -> Password<'static> { + Self::with_theme(&SimpleTheme) + } +} + +impl<'a> Password<'a> { + /// Sets the password input prompt. + pub fn with_prompt<S: Into<String>>(&mut self, prompt: S) -> &mut 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) -> &mut Self { + self.report = val; + self + } + + /// Enables confirmation prompting. + pub fn with_confirmation<A, B>(&mut self, prompt: A, mismatch_err: B) -> &mut Self + where + A: Into<String>, + B: Into<String>, + { + 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) -> &mut Self { + self.allow_empty_password = allow_empty_password; + self + } + + /// Registers a validator. + /// + /// # Example + /// + /// ```no_run + /// # use dialoguer::Password; + /// let password: String = Password::new() + /// .with_prompt("Enter password") + /// .validate_with(|input: &String| -> Result<(), &str> { + /// if input.len() > 8 { + /// Ok(()) + /// } else { + /// Err("Password must be longer than 8") + /// } + /// }) + /// .interact() + /// .unwrap(); + /// ``` + pub fn validate_with<V>(&mut self, validator: V) -> &mut Self + where + V: PasswordValidator + 'a, + V::Err: ToString, + { + let old_validator_func = self.validator.take(); + + self.validator = Some(Box::new(move |value: &String| -> Option<String> { + 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 + } + + /// Enables user interaction and returns the result. + /// + /// If the user confirms the result is `true`, `false` otherwise. + /// The dialog is rendered on stderr. + pub fn interact(&self) -> io::Result<String> { + self.interact_on(&Term::stderr()) + } + + /// Like `interact` but allows a specific terminal to be set. + pub fn interact_on(&self, term: &Term) -> io::Result<String> { + 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) -> io::Result<String> { + 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> { + /// Creates a password input prompt with a specific theme. + pub fn with_theme(theme: &'a dyn Theme) -> Self { + Self { + prompt: "".into(), + report: true, + theme, + allow_empty_password: false, + confirmation_prompt: None, + validator: None, + } + } +} diff --git a/vendor/dialoguer/src/prompts/select.rs b/vendor/dialoguer/src/prompts/select.rs new file mode 100644 index 0000000..d080abd --- /dev/null +++ b/vendor/dialoguer/src/prompts/select.rs @@ -0,0 +1,419 @@ +use std::{io, ops::Rem}; + +use crate::paging::Paging; +use crate::theme::{SimpleTheme, TermThemeRenderer, Theme}; + +use console::{Key, Term}; + +/// 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. +/// +/// ## Examples +/// +/// ```rust,no_run +/// use dialoguer::{console::Term, theme::ColorfulTheme, Select}; +/// +/// fn main() -> std::io::Result<()> { +/// let items = vec!["Item 1", "item 2"]; +/// let selection = Select::with_theme(&ColorfulTheme::default()) +/// .items(&items) +/// .default(0) +/// .interact_on_opt(&Term::stderr())?; +/// +/// match selection { +/// Some(index) => println!("User selected item : {}", items[index]), +/// None => println!("User did not select anything") +/// } +/// +/// Ok(()) +/// } +/// ``` +pub struct Select<'a> { + default: usize, + items: Vec<String>, + prompt: Option<String>, + report: bool, + clear: bool, + theme: &'a dyn Theme, + max_length: Option<usize>, +} + +impl Default for Select<'static> { + fn default() -> Self { + Self::new() + } +} + +impl Select<'static> { + /// Creates a select prompt builder 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) -> &mut 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` method invocation or `items` slice. + pub fn default(&mut self, val: usize) -> &mut 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) -> &mut 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. + /// + /// ## Examples + /// ```rust,no_run + /// use dialoguer::Select; + /// + /// fn main() -> std::io::Result<()> { + /// let selection: usize = Select::new() + /// .item("Item 1") + /// .item("Item 2") + /// .interact()?; + /// + /// Ok(()) + /// } + /// ``` + pub fn item<T: ToString>(&mut self, item: T) -> &mut Self { + self.items.push(item.to_string()); + self + } + + /// Adds multiple items to the selector. + /// + /// ## Examples + /// ```rust,no_run + /// use dialoguer::Select; + /// + /// fn main() -> std::io::Result<()> { + /// let items = vec!["Item 1", "Item 2"]; + /// let selection: usize = Select::new() + /// .items(&items) + /// .interact()?; + /// + /// println!("{}", items[selection]); + /// + /// Ok(()) + /// } + /// ``` + pub fn items<T: ToString>(&mut self, items: &[T]) -> &mut 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`](#method.report). + /// + /// ## Examples + /// ```rust,no_run + /// use dialoguer::Select; + /// + /// fn main() -> std::io::Result<()> { + /// let selection = Select::new() + /// .with_prompt("Which option do you prefer?") + /// .item("Option A") + /// .item("Option B") + /// .interact()?; + /// + /// Ok(()) + /// } + /// ``` + pub fn with_prompt<S: Into<String>>(&mut self, prompt: S) -> &mut 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) -> &mut 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) -> io::Result<usize> { + 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'. + #[inline] + pub fn interact_opt(&self) -> io::Result<Option<usize>> { + self.interact_on_opt(&Term::stderr()) + } + + /// Like [interact](#method.interact) but allows a specific terminal to be set. + /// + /// ## Examples + ///```rust,no_run + /// use dialoguer::{console::Term, Select}; + /// + /// fn main() -> std::io::Result<()> { + /// let selection = Select::new() + /// .item("Option A") + /// .item("Option B") + /// .interact_on(&Term::stderr())?; + /// + /// println!("User selected option at index {}", selection); + /// + /// Ok(()) + /// } + ///``` + #[inline] + pub fn interact_on(&self, term: &Term) -> io::Result<usize> { + 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. + /// + /// ## Examples + /// ```rust,no_run + /// use dialoguer::{console::Term, Select}; + /// + /// fn main() -> std::io::Result<()> { + /// let selection = Select::new() + /// .item("Option A") + /// .item("Option B") + /// .interact_on_opt(&Term::stdout())?; + /// + /// match selection { + /// Some(position) => println!("User selected option at index {}", position), + /// None => println!("User did not select anything or exited using Esc or q") + /// } + /// + /// Ok(()) + /// } + /// ``` + #[inline] + pub fn interact_on_opt(&self, term: &Term) -> io::Result<Option<usize>> { + self._interact_on(term, true) + } + + /// Like `interact` but allows a specific terminal to be set. + fn _interact_on(&self, term: &Term, allow_quit: bool) -> io::Result<Option<usize>> { + 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::<Vec<_>>() + { + let size = &items.len(); + size_vec.push(*size); + } + + term.hide_cursor()?; + + 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 builder with a specific theme. + /// + /// ## Examples + /// ```rust,no_run + /// use dialoguer::{ + /// Select, + /// theme::ColorfulTheme + /// }; + /// + /// fn main() -> std::io::Result<()> { + /// let selection = Select::with_theme(&ColorfulTheme::default()) + /// .item("Option A") + /// .item("Option B") + /// .interact()?; + /// + /// Ok(()) + /// } + /// ``` + 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_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 + ); + } +} diff --git a/vendor/dialoguer/src/prompts/sort.rs b/vendor/dialoguer/src/prompts/sort.rs new file mode 100644 index 0000000..03152bf --- /dev/null +++ b/vendor/dialoguer/src/prompts/sort.rs @@ -0,0 +1,348 @@ +use std::{io, ops::Rem}; + +use crate::{ + theme::{SimpleTheme, TermThemeRenderer, Theme}, + Paging, +}; + +use console::{Key, Term}; + +/// Renders a sort prompt. +/// +/// Returns list of indices in original items list sorted according to user input. +/// +/// ## Example usage +/// ```rust,no_run +/// use dialoguer::Sort; +/// +/// # fn test() -> Result<(), Box<dyn std::error::Error>> { +/// let items_to_order = vec!["Item 1", "Item 2", "Item 3"]; +/// let ordered = Sort::new() +/// .with_prompt("Order the items") +/// .items(&items_to_order) +/// .interact()?; +/// # Ok(()) +/// # } +/// ``` +pub struct Sort<'a> { + items: Vec<String>, + prompt: Option<String>, + report: bool, + clear: bool, + max_length: Option<usize>, + theme: &'a dyn Theme, +} + +impl Default for Sort<'static> { + fn default() -> Self { + Self::new() + } +} + +impl Sort<'static> { + /// Creates a sort prompt. + 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) -> &mut 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) -> &mut 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<T: ToString>(&mut self, item: T) -> &mut Self { + self.items.push(item.to_string()); + self + } + + /// Adds multiple items to the selector. + pub fn items<T: ToString>(&mut self, items: &[T]) -> &mut 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<S: Into<String>>(&mut self, prompt: S) -> &mut 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) -> &mut 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<index>` 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) -> io::Result<Vec<usize>> { + 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<index>)` if user hit 'Enter' or `None` if user cancelled with 'Esc' or 'q'. + #[inline] + pub fn interact_opt(&self) -> io::Result<Option<Vec<usize>>> { + self.interact_on_opt(&Term::stderr()) + } + + /// Like [interact](#method.interact) but allows a specific terminal to be set. + /// + /// ## Examples + ///```rust,no_run + /// use dialoguer::Sort; + /// use console::Term; + /// + /// fn main() -> std::io::Result<()> { + /// let selections = Sort::new() + /// .item("Option A") + /// .item("Option B") + /// .interact_on(&Term::stderr())?; + /// + /// println!("User sorted options as indices {:?}", selections); + /// + /// Ok(()) + /// } + ///``` + #[inline] + pub fn interact_on(&self, term: &Term) -> io::Result<Vec<usize>> { + 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. + /// + /// ## Examples + /// ```rust,no_run + /// use dialoguer::Sort; + /// use console::Term; + /// + /// fn main() -> std::io::Result<()> { + /// let selections = Sort::new() + /// .item("Option A") + /// .item("Option B") + /// .interact_on_opt(&Term::stdout())?; + /// + /// match selections { + /// Some(positions) => println!("User sorted options as indices {:?}", positions), + /// None => println!("User exited using Esc or q") + /// } + /// + /// Ok(()) + /// } + /// ``` + #[inline] + pub fn interact_on_opt(&self, term: &Term) -> io::Result<Option<Vec<usize>>> { + self._interact_on(term, true) + } + + fn _interact_on(&self, term: &Term, allow_quit: bool) -> io::Result<Option<Vec<usize>>> { + 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. + pub fn with_theme(theme: &'a dyn Theme) -> Self { + Self { + items: vec![], + clear: true, + prompt: None, + report: true, + max_length: None, + theme, + } + } +} diff --git a/vendor/dialoguer/src/theme.rs b/vendor/dialoguer/src/theme.rs new file mode 100644 index 0000000..1fbde92 --- /dev/null +++ b/vendor/dialoguer/src/theme.rs @@ -0,0 +1,976 @@ +//! Customizes the rendering of the elements. +use std::{fmt, io}; + +use console::{measure_text_width, style, Style, StyledObject, Term}; +#[cfg(feature = "fuzzy-select")] +use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; + +/// 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<bool>, + ) -> 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<bool>, + ) -> 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().into_iter().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, + cursor_pos: usize, + ) -> fmt::Result { + if !prompt.is_empty() { + write!(f, "{} ", prompt,)?; + } + + if cursor_pos < search_term.len() { + let st_head = search_term[0..cursor_pos].to_string(); + let st_tail = search_term[cursor_pos..search_term.len()].to_string(); + let st_cursor = "|".to_string(); + write!(f, "{}{}{}", st_head, st_cursor, st_tail) + } else { + let cursor = "|".to_string(); + write!(f, "{}{}", search_term.to_string(), cursor) + } + } +} + +/// The default theme. +pub struct SimpleTheme; + +impl Theme for SimpleTheme {} + +/// 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<String>, + /// Prompt suffix value and style + pub prompt_suffix: StyledObject<String>, + /// Prompt on success prefix value and style + pub success_prefix: StyledObject<String>, + /// Prompt on success suffix value and style + pub success_suffix: StyledObject<String>, + /// Error prefix value and style + pub error_prefix: StyledObject<String>, + /// 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<String>, + /// Inctive item in select prefix value and style + pub inactive_item_prefix: StyledObject<String>, + /// Checked item in multi select prefix value and style + pub checked_item_prefix: StyledObject<String>, + /// Unchecked item in multi select prefix value and style + pub unchecked_item_prefix: StyledObject<String>, + /// Picked item in sort prefix value and style + pub picked_item_prefix: StyledObject<String>, + /// Unpicked item in sort prefix value and style + pub unpicked_item_prefix: StyledObject<String>, + /// 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, + /// Show the selections from certain prompts inline + pub inline_selections: bool, +} + +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().black(), + 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(), + inline_selections: true, + } + } +} + +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<bool>, + ) -> 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<bool>, + ) -> 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)?; + + if self.inline_selections { + 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().into_iter().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, + cursor_pos: usize, + ) -> fmt::Result { + if !prompt.is_empty() { + write!( + f, + "{} {} ", + &self.prompt_prefix, + self.prompt_style.apply_to(prompt) + )?; + } + + if cursor_pos < search_term.len() { + let st_head = search_term[0..cursor_pos].to_string(); + let st_tail = search_term[cursor_pos + 1..search_term.len()].to_string(); + let st_cursor = self + .fuzzy_cursor_style + .apply_to(search_term.to_string().chars().nth(cursor_pos).unwrap()); + write!( + f, + "{} {}{}{}", + &self.prompt_suffix, st_head, st_cursor, st_tail + ) + } else { + let cursor = self.fuzzy_cursor_style.apply_to(" "); + write!( + f, + "{} {}{}", + &self.prompt_suffix, + search_term.to_string(), + cursor + ) + } + } +} + +/// Helper struct to conveniently render a theme of a term. +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, + ) -> io::Result<usize> { + 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, + ) -> io::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; + self.term.write_line(&buf) + } + + fn write_formatted_prompt< + F: FnOnce(&mut TermThemeRenderer, &mut dyn fmt::Write) -> fmt::Result, + >( + &mut self, + f: F, + ) -> io::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) -> io::Result<()> { + self.write_formatted_line(|this, buf| this.theme.format_error(buf, err)) + } + + pub fn confirm_prompt(&mut self, prompt: &str, default: Option<bool>) -> io::Result<usize> { + 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<bool>) -> io::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, + ) -> io::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>) -> io::Result<usize> { + 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) -> io::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) -> io::Result<usize> { + 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) -> io::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)>, + ) -> io::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) -> io::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) -> io::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, + ) -> io::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)>, + ) -> io::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]) -> io::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, + ) -> io::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)>, + ) -> io::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]) -> io::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) -> io::Result<()> { + self.write_formatted_line(|this, buf| { + this.theme + .format_sort_prompt_item(buf, text, picked, active) + }) + } + + pub fn clear(&mut self) -> io::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]) -> io::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(()) + } +} diff --git a/vendor/dialoguer/src/validate.rs b/vendor/dialoguer/src/validate.rs new file mode 100644 index 0000000..addc9b4 --- /dev/null +++ b/vendor/dialoguer/src/validate.rs @@ -0,0 +1,49 @@ +//! Provides validation for text inputs + +/// Trait for input validators. +/// +/// A generic implementation for `Fn(&str) -> Result<(), E>` is provided +/// to facilitate development. +pub trait Validator<T> { + 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<T, F, E> Validator<T> 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<F, E> PasswordValidator for F +where + F: Fn(&String) -> Result<(), E>, +{ + type Err = E; + + fn validate(&self, input: &String) -> Result<(), Self::Err> { + self(input) + } +} |