pax_global_header00006660000000000000000000000064136312504320014511gustar00rootroot0000000000000052 comment=7fec5d178971274577c61e59f92b9b3cafc5d5b1 tty-prompt-0.21.0/000077500000000000000000000000001363125043200137305ustar00rootroot00000000000000tty-prompt-0.21.0/.github/000077500000000000000000000000001363125043200152705ustar00rootroot00000000000000tty-prompt-0.21.0/.github/FUNDING.yml000066400000000000000000000000241363125043200171010ustar00rootroot00000000000000github: piotrmurach tty-prompt-0.21.0/.github/ISSUE_TEMPLATE.md000066400000000000000000000010761363125043200200010ustar00rootroot00000000000000### Are you in the right place? * For issues or feature requests file a GitHub issue in this repository * For general questions or discussion post in [Gitter](https://gitter.im/piotrmurach/tty) ### Describe the problem A brief description of the issue/feature. ### Steps to reproduce the problem ``` Your code here to reproduce the issue ``` ### Actual behaviour What happened? This could be a description, log output, error raised etc... ### Expected behaviour What did you expect to happen? ### Describe your environment * OS version: * Ruby version: * TTY version: tty-prompt-0.21.0/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000006771363125043200211030ustar00rootroot00000000000000### Describe the change What does this Pull Request do? ### Why are we doing this? Any related context as to why is this is a desirable change. ### Benefits How will the library improve? ### Drawbacks Possible drawbacks applying this change. ### Requirements Put an X between brackets on each line if you have done the item: [] Tests written & passing locally? [] Code style checked? [] Rebased with `master` branch? [] Documentation updated? tty-prompt-0.21.0/.gitignore000066400000000000000000000002231363125043200157150ustar00rootroot00000000000000/.bundle/ /.ruby-version /.ruby-gemset /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ *.bundle *.so *.o *.a mkmf.log tty-prompt-0.21.0/.rspec000066400000000000000000000000511363125043200150410ustar00rootroot00000000000000--color --require spec_helper --warnings tty-prompt-0.21.0/.rubocop.yml000066400000000000000000000012661363125043200162070ustar00rootroot00000000000000Lint/AssignmentInCondition: Enabled: false Metrics/AbcSize: Max: 30 Metrics/BlockLength: CountComments: true Max: 25 ExcludedMethods: [] Exclude: - "spec/**/*" Metrics/ClassLength: Max: 1500 Metrics/CyclomaticComplexity: Enabled: false Metrics/LineLength: Max: 80 Metrics/MethodLength: Max: 20 Naming/BinaryOperatorParameterName: Enabled: false Style/AsciiComments: Enabled: false Style/LambdaCall: SupportedStyles: - call - braces Style/StringLiterals: EnforcedStyle: single_quotes Style/TrivialAccessors: Enabled: false # { ... } for multi-line blocks is okay Style/BlockDelimiters: Enabled: false Style/CommentedKeyword: Enabled: false tty-prompt-0.21.0/.travis.yml000066400000000000000000000006551363125043200160470ustar00rootroot00000000000000--- language: ruby before_install: "gem install bundler -v '< 2.0'" bundler_args: --without tools script: bundle exec rake ci rvm: - 2.0.0 - 2.1.10 - 2.2.10 - 2.3.8 - 2.4.9 - 2.5.7 - 2.6.5 - 2.7.0 - ruby-head - jruby-9.2.6.0 - jruby-head matrix: allow_failures: - rvm: ruby-head - rvm: jruby-head - rvm: jruby-9.2.6.0 fast_finish: true branches: only: master notifications: email: false tty-prompt-0.21.0/CHANGELOG.md000066400000000000000000000341601363125043200155450ustar00rootroot00000000000000# Change log ## [v0.21.0] - 2019-03-08 ### Added * Add :min option to #multi_select prompt by Katelyn Schiesser(@slowbro) ### Changed * Change gemspec to remove test artifacts ### Fixed * Fix :help_color option for multi_selct prompt by @robbystk ## [v0.20.0] - 2019-11-24 ### Changed * Change to update tty-reader dependency * Change gemspec to include metadata ### Fixed * Fix Choice#from to differentiate between nil and false by Katelyn Schiesser(@slowbro) * Fix yes? and no? prompts to stop raising on invalid/blank input by Katelyn Schiesser(@slowbro) * Fix Ruby 2.7 keyword arguments warnings * Fix question validation to work with nil input ## [v0.19.0] - 2019-05-27 ### Added * Add Prompt#debug to allow displaying values in terminal's top right corner * Add :max to limit number of choices in #multi_select prompt * Add :value to pre populate #ask prompt line content * Add :auto_hint to expand default hint in #expand prompt by Ewoudt Kellerman(@hellola) * Add Timer to track and timeout code execution ### Changed * Change Paginator to expose #start_index & #end_index * Change Paginator to figure out #start_index based on per page size and adjust boundaries to match active selection * Change #ask prompt to allow no question * Change #enum_select to automatically assigned non-disabled default option * Change #enum_select to set default choice when navigating by page * Change #select & #multi_select to allow navigation by page with left/right keys * Change #keypress to use Timer * Change Choice#from to allow any object coercible to string * Change to remove test artifacts from the gem bundle * Change to remove timers dependency * Change to update tty-reader dependency ## [v0.18.1] - 2018-12-29 ### Changed * Change #multi_select & #select to auto select first non-disabled active choice ### Fixed * Fix #select, #multi_select & #enum_select to allow for symbols as choice names ## [v0.18.0] - 2018-11-24 ### Changed * Change to update tty-reader dependency * Remove encoding magic comments ### Fixed * Fix #keypress to stop using the :nonblock option * Fix input reading to correctly capture the Esc key(#84) * Fix line editing when cursor is on second to last character(#94) ## [v0.17.2] - 2018-11-01 ### Fixed * Fix #yes? & #no? prompt suffix option to all non-standard characters by Rui(@rpbaltazar) ## [v0.17.1] - 2018-10-03 ### Change * Change #select, #multi_select to allow alphanumeric, punctuation and space characters in filters ### Fixed * Fix #select by making filter an array to avoid frozen string issues by Chris Hoffman(@yarmiganosca) ## [v0.17.0] - 2018-08-05 ### Changed * Change to update tty-reader & tty-cursor dependencies * Change to directly require files in gemspec ## [v0.16.1] - 2018-04-29 ### Fixed * Fix key events subscription to only listen for the current prompt events ## [v0.16.0] - 2018-03-11 ### Added * Add :disabled key to Choice * Add ability to disable choices in #select, #multi_selct & #enum_select prompts * Add #frozen_string_literal to all files ### Changed * Change Choice#from to allow parsing different data structures * Change all classes to prevent strings mutations * Change Timeout to cleanly terminate keypress input without raising errors ### Fixed * Fix #select, #enum_select & #multi_select navigation to work correctly with items longer than terminal screen width * Fix timeout on Ruby 2.5 and stop raising Timeout::Error ## [v0.15.0] - 2018-02-08 ### Added * Add ability to filter list items in #select, #multi_select & #enum_selct prompts by Saverio Miroddi(@saveriomiroddi) * Add support for array of values for an answer collector key by Danny Hadley(@dadleyy) ### Changed * Relax dependency on timers by Andy Brody(@brodygov) ## [v0.14.0] - 2018-01-01 ### Added * Add :cycle option to #select, #multi_select & #enum_select prompts to allow toggling between infinite and bounded list by Jonas Müller(@muellerj) ### Changed * Change #multi_selct, #select & #enum_select to stop cycling options by default by Jona Müller(@muellerj) * Change gemspec to require ruby >= 2.0.0 * Change #slider prompt to display slider next to query and help underneath * Change to use tty-reader v0.2.0 with new line editing features for processing long inputs ### Fixed * Fix Paginator & EnumPaginator to allow only positive integer values by Andy Brody(@ab) * Fix EnumSelect to report on default option out of range and raise correctly * Fix #ask :file & :path converters to correctly locate the files * Fix #ask, #multiline to correctly handle long strings that wrap around screen * Fix #slider prompt to correctly scale sliding ## [v0.13.2] - 2017-08-30 ### Changed * Change to extract TTY::Prompt::Reader to its own dependency ## [v0.13.1] - 2017-08-16 ### Added * Add ability to manually cancel the time scheduler ### Changed * Change #keypress to use new scheduler cancelling * Change Reader to inline interrupt to allow for early exit ### Fix * Fix keypress reading on Windows to distinguish between blocking & non-blocking IO ## [v0.13.0] - 2017-08-11 ### Changed * Change Timeout to use clock time instead of sleep to measure interval * Upgrade tty-cursor to fix save & restore ### Fixed * Fix keypress with timeout option to cleanly stop timeout thread * Fix Reader on Windows to stop blocking when waiting for key press ## [v0.12.0] - 2017-03-19 ### Added * Add Multiline question type * Add Keypress question type * Add Reader::History for storing buffered lines * Add Reader::Line for line abstraction ### Changed * Remove :read option from Question * Chnage Reader#read_line to handle raw mode for processing special characters such as Ctrl+x, navigate through history buffer using up/down arrows, allow editing current line by moving left/right with arrow keys and inserting content * Change Reader#read_multiline to gather multi line input correctly, skip empty lines and terminate when Ctrl+d and Ctrl+z are pressed * Change Reader::Mode to check if tty is available by Matt Martyn (@MMartyn) * Change #keypress prompt to correctly refresh line and accept :keys & :timeout options ### Fixed * Fix issue with #select, #multi_selct, #enum_select when choices are provided as hash object together with prompt options. * Fix issue with default parameter for yes?/no? prompt by Carlos Fonseca (@carlosefonseca) * Fix List#help to allow setting help text through DSL ## [v0.11.0] - 2017-02-26 ### Added * Add Console for reading input characters on Unix systems * Add WinConsole for reading input characters on Windows systems * Add WindowsApi to allow for calls to external Windows api * Add echo support to multilist by Keith Keith T. Garner(@ktgeek) ### Changed * Change Reader to use Console for input reading * Change Codes to use codepoints instead of strings * Change Reader#read_line to match #gets behaviour * Change Symbols to provide Unicode support on windows * Change Slider to display Unicode when possible * Change ConverterRegistry to be immutable * Change Reader to expose #trigger in place of #publish for events firing ### Fixed * Fix `modify` throwing exception, when user enters empty input by Igor Rzegocki(@ajgon) * Fix #clear_line behaviour by using tty-cursor 0.4.0 to work in all terminals * Fix paging issue for lists shorter than :per_page value repeating title * Fix #mask prompt to correctly match input on Windows * Fix @mask to use default error messages * Fix #select & #multi_select prompts to allow changing options with arrow keys on Windows * Fix #echo to work correctly in zsh shell by štef(@d4be4st) * Fix Slider#keyright event accepting max value outside of range * Fix 2.4.0 conversion errors by using necromancer 0.4.0 * Fix #enum_select preventing selection of first item ## [v0.10.1] - 2017-02-06 ### Fixed * Fix File namespacing ## [v0.10.0] - 2017-01-01 ### Added * Add :enable_color option for toggling colors support ### Changed * Update pastel dependency version ## [v0.9.0] - 2016-12-20 ### Added * Add ability to paginate choices list for #select, #multi_select & #enum_select with :per_page, :page_info and :default options * Add ability to switch through options in #select & #multi_select using the tab key ### Fixed * Fix readers to accept multibyte characters reported by Jaehyun Shin(@keepcosmos) ## [v0.8.0] - 2016-11-29 ### Added * Add ability to publish custom key events for VIM keybindings customisations etc... ### Fixed * Fix Reader#read_char to use Ruby internal buffers instead of direct system call by @kke(Kimmo Lehto) * Fix issue with #ask required & validate checks to take into account required when validating values * Fix bug with #read_keypress to handle function keys and meta navigation keys * Fix issue with default messages not displaying for `range`, `required` and `validate` ## [v0.7.1] - 2016-08-07 ### Fixed * Fix Reader::Mode to include standard io library ## [v0.7.0] - 2016-07-17 ### Added * Add :interrupt_handler option to customise keyboard interrupt behaviour ### Changed * Remove tty-platform dependency ### Fixed * Fix Reader#read_keypress issue when handling interrupt signal by Ondrej Moravcik(@ondra-m) * Fix raw & echo modes to use standard library support by Kim Burgestrand(@Burgestrand) ## [v0.6.0] - 2016-05-21 ### Changed * Upgrade tty-cursor dependency ### Fixed * Fix issue with reader trapping signals by @kylekyle * Fix expand to use new prev_line implementation ## [v0.5.0] - 2016-03-28 ### Added * Add ConfirmQuestion for #yes? & #no? calls * Add ability to collect more than one answer through #collect call * Add Choices#find_by for selecting choice based on attribute * Add Prompt#expand for expanding key options * Add :active_color, :help_color, :prefix options for customizing prompts display ### Changed * Change Choice#from to allow for coersion of complex objects with keys * Change Choices#pluck to search through object attributes * Change #select :enum option help text to display actual numbers range ### Fixed * Fix #no? to correctly ask negative question by @ondra-m * Fix #ask :default option to handle nil or empty string * Fix #multi_select :default option and color changing ## [v0.4.0] - 2016-02-08 ### Added * Add :enum option for #select & #multi_select to allow for numerical selection by @rtoshiro * Add new key event types to KeyEvent * Add #slider for picking values from range of numbers * Add #enum_select for selecting option from enumerated list * Add ability to configure error messages for #ask call * Add new ConversionError type ### Changed * Move #blank? to Utils * Update pastel dependency ## [v0.3.0] - 2015-12-28 ### Added * Add prefix option to prompt to customize #ask, #select, #multi_select * Add default printing to #ask * Add #yes?/#no? boolean queries * Add Evaluator and Result for validation checking to Question * Add ability for #ask to display error messages on failed validation * Add ability to specify in-built names for validation e.i. :email * Add KeyEvent for keyboard events publishing to Reader * Add #read_multiline to Reader * Add :convert option for ask configuration * Add ability to specify custom proc converters * Add #ask_keypress to gather character input * Add #ask_multiline to gather multiline input * Add MaskedQuestion & #mask method for masking input stream characters ### Changed * Change Reader#read_keypress to be robust and read correctly byte sequences * Change Reader#getc to #read_line and extend arguments with echo option * Extract cursor movement to dependency tty-cursor * Change List & MultiList to subscribe to keyboard events * Change to move mode inside reader namespace * Remove Response & Error objects * Remove :char option from #ask * Change :read option to specify mode of reading out of :line, :multiline, :keypress * Rename #confirm to #ok ## [v0.2.0] - 2015-11-23 ### Added * Add ability to select choice form list #select * Add ability to select multiple options #multi_select * Add :read option to #ask for reading specific type input ### Changed * Change #ask api to be similar to #select and #multi_select behaviour * Change #ask :argument option to be :required * Remove :valid option from #ask as #select is a better solution ## [v0.1.0] - 2015-11-01 * Initial implementation and release [v0.20.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.19.0...v0.20.0 [v0.19.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.18.1...v0.19.0 [v0.18.1]: https://github.com/piotrmurach/tty-prompt/compare/v0.18.0...v0.18.1 [v0.18.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.17.2...v0.18.0 [v0.17.2]: https://github.com/piotrmurach/tty-prompt/compare/v0.17.1...v0.17.2 [v0.17.1]: https://github.com/piotrmurach/tty-prompt/compare/v0.17.0...v0.17.1 [v0.17.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.16.1...v0.17.0 [v0.16.1]: https://github.com/piotrmurach/tty-prompt/compare/v0.16.0...v0.16.1 [v0.16.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.15.0...v0.16.0 [v0.15.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.14.0...v0.15.0 [v0.14.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.13.2...v0.14.0 [v0.13.2]: https://github.com/piotrmurach/tty-prompt/compare/v0.13.1...v0.13.2 [v0.13.1]: https://github.com/piotrmurach/tty-prompt/compare/v0.13.0...v0.13.1 [v0.13.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.12.0...v0.13.0 [v0.12.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.11.0...v0.12.0 [v0.11.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.10.1...v0.11.0 [v0.10.1]: https://github.com/piotrmurach/tty-prompt/compare/v0.10.0...v0.10.1 [v0.10.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.9.0...v0.10.0 [v0.9.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.8.0...v0.9.0 [v0.8.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.7.1...v0.8.0 [v0.7.1]: https://github.com/piotrmurach/tty-prompt/compare/v0.7.0...v0.7.1 [v0.7.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.6.0...v0.7.0 [v0.6.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.5.0...v0.6.0 [v0.5.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.4.0...v0.5.0 [v0.4.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.3.0...v0.4.0 [v0.3.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.2.0...v0.3.0 [v0.2.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.1.0...v0.2.0 [v0.1.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.1.0 tty-prompt-0.21.0/CODE_OF_CONDUCT.md000066400000000000000000000062401363125043200165310ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at piotr@piotrmurach.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] [homepage]: https://contributor-covenant.org [version]: https://contributor-covenant.org/version/1/4/ tty-prompt-0.21.0/Gemfile000066400000000000000000000004111363125043200152170ustar00rootroot00000000000000source 'https://rubygems.org' gemspec #gem 'tty-reader', path: '../tty-reader' #gem 'tty-reader', git: 'https://github.com/piotrmurach/tty-reader' group :test do gem 'benchmark-ips', '~> 2.7.2' gem 'simplecov', '~> 0.16.1' gem 'coveralls', '~> 0.8.22' end tty-prompt-0.21.0/LICENSE.txt000066400000000000000000000020551363125043200155550ustar00rootroot00000000000000Copyright (c) 2015 Piotr Murach MIT License 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. tty-prompt-0.21.0/README.md000066400000000000000000001226311363125043200152140ustar00rootroot00000000000000
tty logo
# TTY::Prompt [![Gitter](https://badges.gitter.im/Join%20Chat.svg)][gitter] [![Gem Version](https://badge.fury.io/rb/tty-prompt.svg)][gem] [![Build Status](https://secure.travis-ci.org/piotrmurach/tty-prompt.svg?branch=master)][travis] [![Build status](https://ci.appveyor.com/api/projects/status/4cguoiah5dprbq7n?svg=true)][appveyor] [![Code Climate](https://codeclimate.com/github/piotrmurach/tty-prompt/badges/gpa.svg)][codeclimate] [![Coverage Status](https://coveralls.io/repos/github/piotrmurach/tty-prompt/badge.svg)][coverage] [![Inline docs](http://inch-ci.org/github/piotrmurach/tty-prompt.svg?branch=master)][inchpages] [gitter]: https://gitter.im/piotrmurach/tty [gem]: http://badge.fury.io/rb/tty-prompt [travis]: http://travis-ci.org/piotrmurach/tty-prompt [appveyor]: https://ci.appveyor.com/project/piotrmurach/tty-prompt [codeclimate]: https://codeclimate.com/github/piotrmurach/tty-prompt [coverage]: https://coveralls.io/github/piotrmurach/tty-prompt [inchpages]: http://inch-ci.org/github/piotrmurach/tty-prompt > A beautiful and powerful interactive command line prompt. **TTY::Prompt** provides independent prompt component for [TTY](https://github.com/piotrmurach/tty) toolkit. ## Features * Number of prompt types for gathering user input * A robust API for validating complex inputs * User friendly error feedback * Intuitive DSL for creating complex menus * Ability to page long menus * Support for Linux, OS X, FreeBSD and Windows systems ## Windows support `tty-prompt` works across all Unix and Windows systems in the "best possible" way. On Windows, it uses Win32 API in place of terminal device to provide matching functionality. Since Unix terminals provide richer set of features than Windows PowerShell consoles, expect to have a better experience on Unix-like platform. Some features like `select` or `multi_select` menus may not work on Windows when run from Git Bash. See GitHub suggested [fixes](https://github.com/git-for-windows/git/wiki/FAQ#some-native-console-programs-dont-work-when-run-from-git-bash-how-to-fix-it). For Windows, consider installing [ConEmu](https://conemu.github.io/), [cmder](http://cmder.net/) or [PowerCmd](http://www.powercmd.com/). ## Installation Add this line to your application's Gemfile: ```ruby gem 'tty-prompt' ``` And then execute: $ bundle Or install it yourself as: $ gem install tty-prompt ## Contents * [1. Usage](#1-usage) * [2. Interface](#2-interface) * [2.1 ask](#21-ask) * [2.1.1 convert](#211-convert) * [2.1.2 default](#212-default) * [2.1.3 value](#213-value) * [2.1.4 echo](#214-echo) * [2.1.5 error messages](#215-error-messages) * [2.1.6 in](#216-in) * [2.1.7 modify](#217-modify) * [2.1.8 required](#218-required) * [2.1.9 validate](#219-validate) * [2.2 keypress](#22-keypress) * [2.2.1 :timeout](#221-timeout) * [2.3 multiline](#23-multiline) * [2.4 mask](#24-mask) * [2.5 yes?/no?](#25-yesno) * [2.6 menu](#26-menu) * [2.6.1 choices](#261-choices) * [2.6.1.1 :disabled](#2611-disabled) * [2.6.2 select](#262-select) * [2.6.2.1 :per_page](#2621-per_page) * [2.6.2.2 :disabled](#2622-disabled) * [2.6.2.3 :filter](#2623-filter) * [2.6.3 multi_select](#263-multi_select) * [2.6.3.1 :disabled](#2631-disabled) * [2.6.3.2 :echo](#2632-echo) * [2.6.3.3 :filter](#2633-filter) * [2.6.3.4 :min](#2634-min) * [2.6.3.5 :max](#2635-max) * [2.6.4 enum_select](#264-enum_select) * [2.6.4.1 :per_page](#2641-per_page) * [2.6.4.1 :disabled](#2641-disabled) * [2.7 expand](#27-expand) * [2.7.1 auto_hint](#271-auto_hint) * [2.8 collect](#28-collect) * [2.9 suggest](#29-suggest) * [2.10 slider](#210-slider) * [2.11 say](#211-say) * [2.11.1 ok](#2111-ok) * [2.11.2 warn](#2112-warn) * [2.11.3 error](#2113-error) * [2.12 keyboard events](#212-keyboard-events) * [3. settings](#3-settings) * [3.1 :symbols](#31-symbols) * [3.2 :active_color](#32-active_color) * [3.3 :enable_color](#33-enable-color) * [3.4 :help_color](#34-help_color) * [3.5 :interrupt](#35-interrupt) * [3.6 :prefix](#36-prefix) * [3.7 :track_history](#37-track_history) ## 1. Usage In order to start asking questions on the command line, create prompt: ```ruby require "tty-prompt" prompt = TTY::Prompt.new ``` And then call `ask` with the question for simple input: ```ruby prompt.ask('What is your name?', default: ENV['USER']) # => What is your name? (piotr) ``` To confirm input use `yes?`: ```ruby prompt.yes?('Do you like Ruby?') # => Do you like Ruby? (Y/n) ``` If you want to input password or secret information use `mask`: ```ruby prompt.mask("What is your secret?") # => What is your secret? •••• ``` Asking question with list of options couldn't be easier using `select` like so: ```ruby prompt.select("Choose your destiny?", %w(Scorpion Kano Jax)) # => # Choose your destiny? (Use ↑/↓ arrow keys, press Enter to select) # ‣ Scorpion # Kano # Jax ``` Also, asking multiple choice questions is a breeze with `multi_select`: ```ruby choices = %w(vodka beer wine whisky bourbon) prompt.multi_select("Select drinks?", choices) # => # # Select drinks? (Use ↑/↓ arrow keys, press Space to select and Enter to finish)" # ‣ ⬡ vodka # ⬡ beer # ⬡ wine # ⬡ whisky # ⬡ bourbon ``` To ask for a selection from enumerated list you can use `enum_select`: ```ruby choices = %w(emacs nano vim) prompt.enum_select("Select an editor?", choices) # => # # Select an editor? # 1) emacs # 2) nano # 3) vim # Choose 1-3 [1]: ``` However, if you have a lot of options to choose from you may want to use `expand`: ```ruby choices = [ { key: 'y', name: 'overwrite this file', value: :yes }, { key: 'n', name: 'do not overwrite this file', value: :no }, { key: 'a', name: 'overwrite this file and all later files', value: :all }, { key: 'd', name: 'show diff', value: :diff }, { key: 'q', name: 'quit; do not overwrite this file ', value: :quit } ] prompt.expand('Overwrite Gemfile?', choices) # => # Overwrite Gemfile? (enter "h" for help) [y,n,a,d,q,h] ``` If you wish to collect more than one answer use `collect`: ```ruby result = prompt.collect do key(:name).ask('Name?') key(:age).ask('Age?', convert: :int) key(:address) do key(:street).ask('Street?', required: true) key(:city).ask('City?') key(:zip).ask('Zip?', validate: /\A\d{3}\Z/) end end # => # {:name => "Piotr", :age => 30, :address => {:street => "Street", :city => "City", :zip => "123"}} ``` ## 2. Interface ### 2.1 ask In order to ask a basic question do: ```ruby prompt.ask("What is your name?") ``` However, to prompt for more complex input you can use robust API by passing hash of properties or using a block like so: ```ruby prompt.ask("What is your name?") do |q| q.required true q.validate /\A\w+\Z/ q.modify :capitalize end ``` #### 2.1.1 convert The `convert` property is used to convert input to a required type. By default no conversion is performed. The following conversions are provided: ```ruby :bool # true or false for strings such as "Yes", "No" :date # date type :datetime # datetime type :file # File object :float # decimal or error if cannot convert :int # integer or error if cannot convert :path # Pathname object :range # range type :regexp # regex expression :string # string :symbol # symbol ``` For example, if you are interested in range type as answer do the following: ```ruby prompt.ask("Provide range of numbers?", convert: :range) # Provide range of numbers? 1-10 # => 1..10 ``` You can also provide a custom conversion like so: ```ruby prompt.ask('Ingredients? (comma sep list)') do |q| q.convert -> (input) { input.split(/,\s*/) } end # Ingredients? (comma sep list) milk, eggs, flour # => ['milk', 'eggs', 'flour'] ``` #### 2.1.2 default The `:default` option is used if the user presses return key: ```ruby prompt.ask('What is your name?', default: 'Anonymous') # => # What is your name? (Anonymous) ``` #### 2.1.3 value To pre-populate the input line for editing use `:value` option: ```ruby prompt.ask("What is your name?", value: "Piotr") # => # What is your name? Piotr ``` #### 2.1.4 echo To control whether the input is shown back in terminal or not use `:echo` option like so: ```ruby prompt.ask('password:', echo: false) ``` #### 2.1.5 error messages By default `tty-prompt` comes with predefined error messages for `required`, `in`, `validate` options. You can change these and configure to your liking either by passing message as second argument with the option: ```ruby prompt.ask('What is your email?') do |q| q.validate(/\A\w+@\w+\.\w+\Z/, 'Invalid email address') end ``` Or change the `messages` key entry out of `:required?`, `:valid?`, `:range?`: ```ruby prompt.ask('What is your email?') do |q| q.validate(/\A\w+@\w+\.\w+\Z/) q.messages[:valid?] = 'Invalid email address' end ``` To change default range validation error message do: ```ruby prompt.ask('How spicy on scale (1-5)? ') do |q| q.in '1-5' q.messages[:range?] = '%{value} out of expected range #{in}' end ``` #### 2.1.6 in In order to check that provided input falls inside a range of inputs use the `in` option. For example, if we wanted to ask a user for a single digit in given range we may do following: ```ruby ask("Provide number in range: 0-9?") { |q| q.in('0-9') } ``` #### 2.1.7 modify Set the `:modify` option if you want to handle whitespace or letter capitalization. ```ruby prompt.ask('Enter text:') do |q| q.modify :strip, :collapse end ``` Available letter casing settings are: ```ruby :up # change to upper case :down # change to small case :capitalize # capitalize each word ``` Available whitespace settings are: ```ruby :trim # remove whitespace from both ends of the input :strip # same as :trim :chomp # remove whitespace at the end of input :collapse # reduce all whitespace to single character :remove # remove all whitespace ``` #### 2.1.8 required To ensure that input is provided use `:required` option: ```ruby prompt.ask("What's your phone number?", required: true) # What's your phone number? # >> Value must be provided ``` #### 2.1.9 validate In order to validate that input matches a given pattern you can pass the `validate` option. Validate setting accepts `Regex`, `Proc` or `Symbol`. ```ruby prompt.ask('What is your username?') do |q| q.validate /^[^\.]+\.[^\.]+/ end ``` ```ruby prompt.ask('What is your username?') do |q| q.validate { |input| input =~ /^[^\.]+\.[^\.]+/ } end ``` The **TTY::Prompt** comes with built-in validations for `:email` and you can use them directly like so: ```ruby prompt.ask('What is your email?') { |q| q.validate :email } ``` ### 2.2. keypress In order to ask question that awaits a single character answer use `keypress` prompt like so: ```ruby prompt.keypress("Press key ?") # Press key? # => a ``` By default any key is accepted but you can limit keys by using `:keys` option. Any key event names such as `:space` or `:ctrl_k` are valid: ```ruby prompt.keypress("Press space or enter to continue", keys: [:space, :return]) ``` #### 2.2.1 timeout Timeout can be set using `:timeout` option to expire prompt and allow the script to continue automatically: ```ruby prompt.keypress("Press any key to continue, resumes automatically in 3 seconds ...", timeout: 3) ``` In addition the `keypress` recognises `:countdown` token when inserted inside the question. It will automatically countdown the time in seconds: ```ruby prompt.keypress("Press any key to continue, resumes automatically in :countdown ...", timeout: 3) ``` ### 2.3 multiline Asking for multiline input can be done with `multiline` method. The reading of input will terminate when `Ctrl+d` or `Ctrl+z` is pressed. Empty lines will not be included in the returned array. ```ruby prompt.multiline("Description?") # Description? (Press CTRL-D or CTRL-Z to finish) # I know not all that may be coming, # but be it what it will, # I'll go to it laughing. # => ["I know not all that may be coming,\n", "but be it what it will,\n", "I'll go to it laughing.\n"] ``` The `multiline` uses similar options to those supported by `ask` prompt. For example, to provide default description: ```ruby prompt.multiline("Description?", default: 'A super sweet prompt.') ``` Or using DSL: ```ruby prompt.multiline("Description?") do |q| q.default 'A super sweet prompt.' q.help 'Press thy ctrl+d to end' end ``` ### 2.4 mask If you require input of confidential information use `mask` method. By default each character that is printed is replaced by `•` symbol. All configuration options applicable to `ask` method can be used with `mask` as well. ```ruby prompt.mask('What is your secret?') # => What is your secret? •••• ``` The masking character can be changed by passing `:symbols` option with `:mask` key: ```ruby heart = prompt.decorate(prompt.symbols[:heart] + ' ', :magenta) prompt.mask('What is your secret?', symbols: {mask: heart}) # => What is your secret? ❤ ❤ ❤ ❤ ❤ ``` If you don't wish to show any output use `:echo` option like so: ```ruby prompt.mask('What is your secret?', echo: false) ``` You can also provide validation for your mask to enforce for instance strong passwords: ```ruby prompt.mask('What is your secret?', mask: heart) do |q| q.validate(/[a-z\ ]{5,15}/) end ``` ### 2.5 yes?/no? In order to display a query asking for boolean input from user use `yes?` like so: ```ruby prompt.yes?('Do you like Ruby?') # => # Do you like Ruby? (Y/n) ``` You can further customize question by passing `suffix`, `positive`, `negative` and `convert` options. The `suffix` changes text of available options, the `positive` specifies display string for successful answer and `negative` changes display string for negative answer. The final value is a boolean provided the `convert` option evaluates to boolean. It's enough to provide the `suffix` option for the prompt to accept matching answers with correct labels: ```ruby prompt.yes?("Are you a human?") do |q| q.suffix 'Yup/nope' end # => # Are you a human? (Yup/nope) ``` Alternatively, instead of `suffix` option provide the `positive` and `negative` labels: ```ruby prompt.yes?("Are you a human?") do |q| q.default false q.positive 'Yup' q.negative 'Nope' end # => # Are you a human? (yup/Nope) ``` Finally, providing all available options you can ask fully customized question: ```ruby prompt.yes?('Are you a human?') do |q| q.suffix 'Agree/Disagree' q.positive 'Agree' q.negative 'Disagree' q.convert -> (input) { !input.match(/^agree$/i).nil? } end # => # Are you a human? (Agree/Disagree) ``` There is also the opposite for asking confirmation of negative question: ```ruby prompt.no?('Do you hate Ruby?') # => # Do you hate Ruby? (y/N) ``` Similarly to `yes?` method, you can supply the same options to customize the question. ### 2.6 menu ### 2.6.1 choices There are many ways in which you can add menu choices. The simplest way is to create an array of values: ```ruby choices = %w(small medium large) ``` By default the choice name is also the value the prompt will return when selected. To provide custom values, you can provide a hash with keys as choice names and their respective values: ```ruby choices = {small: 1, medium: 2, large: 3} ``` Finally, you can define an array of choices where each choice is a hash value with `:name` & `:value` keys which can include other options for customising individual choices: ```ruby choices = [ {name: 'small', value: 1}, {name: 'medium', value: 2, disabled: '(out of stock)'}, {name: 'large', value: 3} ] ``` You can specify `:key` as an additional option which will be used as short name for selecting the choice via keyboard key press. Another way to create menu with choices is using the DSL and the `choice` method. For example, the previous array of choices with hash values can be translated as: ```ruby prompt.select('What size?') do |menu| menu.choice name: 'small', value: 1 menu.choice name: 'medium', value: 2, disabled: '(out of stock)' menu.choice name: 'large', value: 3 end ``` or in a more compact way: ```ruby prompt.select('What size?') do |menu| menu.choice 'small', 1 menu.choice 'medium', 2, disabled: '(out of stock)' menu.choice 'large', 3 end ``` #### 2.6.1.1 `:disabled` The `:disabled` key indicates to display a choice as currently unavailable to select. Disabled choices are displayed with a cross `✘` character next to them. If the choice is disabled, it cannot be selected. The value for the `:disabled` is used next to the choice to provide reason for excluding it from the selection menu. For example: ```ruby choices = [ {name: 'small', value: 1}, {name: 'medium', value: 2, disabled: '(out of stock)'} {name: 'large', value: 3} ] ``` ### 2.6.2 select For asking questions involving list of options use `select` method by passing the question and possible choices: ```ruby prompt.select("Choose your destiny?", %w(Scorpion Kano Jax)) # => # Choose your destiny? (Use ↑/↓ arrow keys, press Enter to select) # ‣ Scorpion # Kano # Jax ``` You can also provide options through DSL using the `choice` method for single entry and/or `choices` for more than one choice: ```ruby prompt.select("Choose your destiny?") do |menu| menu.choice 'Scorpion' menu.choice 'Kano' menu.choice 'Jax' end # => # Choose your destiny? (Use ↑/↓ arrow keys, press Enter to select) # ‣ Scorpion # Kano # Jax ``` By default the choice name is used as return value, but you can provide your custom values including a `Proc` object: ```ruby prompt.select("Choose your destiny?") do |menu| menu.choice 'Scorpion', 1 menu.choice 'Kano', 2 menu.choice 'Jax', -> { 'Nice choice captain!' } end # => # Choose your destiny? (Use ↑/↓ arrow keys, press Enter to select) # ‣ Scorpion # Kano # Jax ``` If you wish you can also provide a simple hash to denote choice name and its value like so: ```ruby choices = {'Scorpion' => 1, 'Kano' => 2, 'Jax' => 3} prompt.select("Choose your destiny?", choices) ``` To mark particular answer as selected use `default` with index of the option starting from `1`: ```ruby prompt.select("Choose your destiny?") do |menu| menu.default 3 menu.choice 'Scorpion', 1 menu.choice 'Kano', 2 menu.choice 'Jax', 3 end # => # Choose your destiny? (Use ↑/↓ arrow keys, press Enter to select) # Scorpion # Kano # ‣ Jax ``` You can navigate the choices using the arrow keys or define your own key mappings (see [keyboard events](#212-keyboard-events). When reaching the top/bottom of the list, the selection does not cycle around by default. If you wish to enable cycling, you can pass `cycle: true` to `select` and `multi_select`: ```ruby prompt.select("Choose your destiny?", %w(Scorpion Kano Jax), cycle: true) # => # Choose your destiny? (Use ↑/↓ arrow keys, press Enter to select) # ‣ Scorpion # Kano # Jax ``` For ordered choices set `enum` to any delimiter String. In that way, you can use arrows keys and numbers (0-9) to select the item. ```ruby prompt.select("Choose your destiny?") do |menu| menu.enum '.' menu.choice 'Scorpion', 1 menu.choice 'Kano', 2 menu.choice 'Jax', 3 end # => # Choose your destiny? (Use ↑/↓ arrow or number (0-9) keys, press Enter to select) # 1. Scorpion # 2. Kano # ‣ 3. Jax ``` You can configure help message and/or marker like so ```ruby choices = %w(Scorpion Kano Jax) prompt.select("Choose your destiny?", choices, help: "(Bash keyboard)", symbols: {marker: '>'}) # => # Choose your destiny? (Bash keyboard) # > Scorpion # Kano # Jax ``` #### 2.6.1.1 `:per_page` By default the menu is paginated if selection grows beyond `6` items. To change this setting use `:per_page` configuration. ```ruby letters = ('A'..'Z').to_a prompt.select("Choose your letter?", letters, per_page: 4) # => # Which letter? (Use ↑/↓ and ←/→ arrow keys, press Enter to select) # ‣ A # B # C # D ``` You can also customise page navigation text using `:help` option: ```ruby letters = ('A'..'Z').to_a prompt.select("Choose your letter?") do |menu| menu.per_page 4 menu.help '(Wiggle thy finger up/down and left/right to see more)' menu.choices letters end # => # Which letter? (Wiggle thy finger up/down and left/right to see more) # ‣ A # B # C # D ``` #### 2.6.2.2 `:disabled` To disable menu choice, use the `:disabled` key with a value that explains the reason for the choice being unavailable. For example, out of all warriors, the Goro is currently injured: ```ruby warriors = [ 'Scorpion', 'Kano', { name: 'Goro', disabled: '(injury)' }, 'Jax', 'Kitana', 'Raiden' ] ``` The disabled choice will be displayed with a cross `✘` character next to it and followed by an explanation: ```ruby prompt.select('Choose your destiny?', warriors) # => # Choose your destiny? (Use ↑/↓ arrow keys, press Enter to select) # ‣ Scorpion # Kano # ✘ Goro (injury) # Jax # Kitana # Raiden ``` #### 2.6.2.3 `:filter` To activate dynamic list searching on letter/number key presses use `:filter` option: ```ruby warriors = %w(Scorpion Kano Jax Kitana Raiden) prompt.select('Choose your destiny?', warriors, filter: true) # => # Choose your destiny? (Use ↑/↓ arrow keys, press Enter to select, and letter keys to filter) # ‣ Scorpion # Kano # Jax # Kitana # Raiden ``` After the user presses "k": ```ruby # => # Choose your destiny? (Filter: "k") # ‣ Kano # Kitana ``` After the user presses "ka": ```ruby # => # Choose your destiny? (Filter: "ka") # ‣ Kano ``` Filter characters can be deleted partially or entirely via, respectively, Backspace and Canc. If the user changes or deletes a filter, the choices previously selected remain selected. ### 2.6.3 multi_select For asking questions involving multiple selection list use `multi_select` method by passing the question and possible choices: ```ruby choices = %w(vodka beer wine whisky bourbon) prompt.multi_select("Select drinks?", choices) # => # # Select drinks? (Use ↑/↓ arrow keys, press Space to select and Enter to finish)" # ‣ ⬡ vodka # ⬡ beer # ⬡ wine # ⬡ whisky # ⬡ bourbon ``` As a return value, the `multi_select` will always return an array by default populated with the names of the choices. If you wish to return custom values for the available choices do: ```ruby choices = {vodka: 1, beer: 2, wine: 3, whisky: 4, bourbon: 5} prompt.multi_select("Select drinks?", choices) # Provided that vodka and beer have been selected, the function will return # => [1, 2] ``` Similar to `select` method, you can also provide options through DSL using the `choice` method for single entry and/or `choices` call for more than one choice: ```ruby prompt.multi_select("Select drinks?") do |menu| menu.choice :vodka, {score: 1} menu.choice :beer, 2 menu.choice :wine, 3 menu.choices whisky: 4, bourbon: 5 end ``` To mark choice(s) as selected use the `default` option with index(s) of the option(s) starting from `1`: ```ruby prompt.multi_select("Select drinks?") do |menu| menu.default 2, 5 menu.choice :vodka, {score: 10} menu.choice :beer, {score: 20} menu.choice :wine, {score: 30} menu.choice :whisky, {score: 40} menu.choice :bourbon, {score: 50} end # => # Select drinks? beer, bourbon # ⬡ vodka # ⬢ beer # ⬡ wine # ⬡ whisky # ‣ ⬢ bourbon ``` Like `select`, for ordered choices set `enum` to any delimiter String. In that way, you can use arrows keys and numbers (0-9) to select the item. ```ruby prompt.multi_select("Select drinks?") do |menu| menu.enum ')' menu.choice :vodka, {score: 10} menu.choice :beer, {score: 20} menu.choice :wine, {score: 30} menu.choice :whisky, {score: 40} menu.choice :bourbon, {score: 50} end # => # Select drinks? beer, bourbon # ⬡ 1) vodka # ⬢ 2) beer # ⬡ 3) wine # ⬡ 4) whisky # ‣ ⬢ 5) bourbon ``` And when you press enter you will see the following selected: ```ruby # Select drinks? beer, bourbon # => [{score: 20}, {score: 50}] ``` Also like, `select`, the method takes an option `cycle` (which defaults to `false`), which lets you configure whether the selection should cycle around when reaching the top/bottom of the list when navigating: ```ruby prompt.multi_select("Select drinks?", %w(vodka beer wine), cycle: true) ``` You can configure help message and/or marker like so ```ruby choices = {vodka: 1, beer: 2, wine: 3, whisky: 4, bourbon: 5} prompt.multi_select("Select drinks?", choices, help: 'Press beer can against keyboard') # => # Select drinks? (Press beer can against keyboard)" # ‣ ⬡ vodka # ⬡ beer # ⬡ wine # ⬡ whisky # ⬡ bourbon ``` By default the menu is paginated if selection grows beyond `6` items. To change this setting use `:per_page` configuration. ```ruby letters = ('A'..'Z').to_a prompt.multi_select("Choose your letter?", letters, per_page: 4) # => # Which letter? (Use ↑/↓ and ←/→ arrow keys, press Space to select and Enter to finish) # ‣ ⬡ A # ⬡ B # ⬡ C # ⬡ D ``` #### 2.6.3.1 `:disabled` To disable menu choice, use the `:disabled` key with a value that explains the reason for the choice being unavailable. For example, out of all drinks, the sake and beer are currently out of stock: ```ruby drinks = [ 'bourbon', {name: 'sake', disabled: '(out of stock)'}, 'vodka', {name: 'beer', disabled: '(out of stock)'}, 'wine', 'whisky' ] ``` The disabled choice will be displayed with a cross `✘` character next to it and followed by an explanation: ```ruby prompt.multi_select('Choose your favourite drink?', drinks) # => # Choose your favourite drink? (Use ↑/↓ arrow keys, press Space to select and Enter to finish) # ‣ ⬡ bourbon # ✘ sake (out of stock) # ⬡ vodka # ✘ beer (out of stock) # ⬡ wine # ⬡ whisky ``` #### 2.6.3.2 `:echo` To control whether the selected items are shown on the question header use the :echo option: ```ruby choices = %w(vodka beer wine whisky bourbon) prompt.multi_select("Select drinks?", choices, echo: false) # => # Select drinks? # ⬡ vodka # ⬢ 2) beer # ⬡ 3) wine # ⬡ 4) whisky # ‣ ⬢ 5) bourbon ``` #### 2.6.3.3 `:filter` To activate dynamic list filtering on letter/number typing, use the :filter option: ```ruby choices = %w(vodka beer wine whisky bourbon) prompt.multi_select("Select drinks?", choices, filter: true) # => # Select drinks? (Use ↑/↓ arrow keys, press Space to select and Enter to finish, and letter keys to filter) # ‣ ⬡ vodka # ⬡ beer # ⬡ wine # ⬡ whisky # ⬡ bourbon ``` After the user presses "w": ```ruby # Select drinks? (Filter: "w") # ‣ ⬡ wine # ⬡ whisky ``` Filter characters can be deleted partially or entirely via, respectively, Backspace and Canc. If the user changes or deletes a filter, the choices previously selected remain selected. The `filter` option is not compatible with `enum`. #### 2.6.3.4 `:min` To force the minimum number of choices an user must select, use the `:min` option: ```ruby choices = %w(vodka beer wine whisky bourbon) prompt.multi_select("Select drinks?", choices, min: 3) # => # Select drinks? (min. 3) vodka, beer # ⬢ vodka # ⬢ beer # ⬡ wine # ⬡ wiskey # ‣ ⬡ bourbon ``` #### 2.6.3.5 `:max` To limit the number of choices an user can select, use the `:max` option: ```ruby choices = %w(vodka beer wine whisky bourbon) prompt.multi_select("Select drinks?", choices, max: 3) # => # Select drinks? (max. 3) vodka, beer, whisky # ⬢ vodka # ⬢ beer # ⬡ wine # ⬢ whisky # ‣ ⬡ bourbon ``` ### 2.6.4 enum_select In order to ask for standard selection from indexed list you can use `enum_select` and pass question together with possible choices: ```ruby choices = %w(emacs nano vim) prompt.enum_select("Select an editor?") # => # # Select an editor? # 1) nano # 2) vim # 3) emacs # Choose 1-3 [1]: ``` Similar to `select` and `multi_select`, you can provide question options through DSL using `choice` method and/or `choices` like so: ```ruby choices = %w(nano vim emacs) prompt.enum_select("Select an editor?") do |menu| menu.choice :nano, '/bin/nano' menu.choice :vim, '/usr/bin/vim' menu.choice :emacs, '/usr/bin/emacs' end # => # # Select an editor? # 1) nano # 2) vim # 3) emacs # Choose 1-3 [1]: # # Select an editor? /bin/nano ``` You can change the indexed numbers by passing `enum` option and the default option by using `default` like so ```ruby choices = %w(nano vim emacs) prompt.enum_select("Select an editor?") do |menu| menu.default 2 menu.enum '.' menu.choice :nano, '/bin/nano' menu.choice :vim, '/usr/bin/vim' menu.choice :emacs, '/usr/bin/emacs' end # => # # Select an editor? # 1. nano # 2. vim # 3. emacs # Choose 1-3 [2]: # # Select an editor? /usr/bin/vim ``` #### 2.6.4.1 `:per_page` By default the menu is paginated if selection grows beyond `6` items. To change this setting use `:per_page` configuration. ```ruby letters = ('A'..'Z').to_a prompt.enum_select("Choose your letter?", letters, per_page: 4) # => # Which letter? # 1) A # 2) B # 3) C # 4) D # Choose 1-26 [1]: # (Press tab/right or left to reveal more choices) ``` #### 2.6.4.2 `:disabled` To make a choice unavailable use the `:disabled` option and, if you wish, as value provide a reason: ```ruby choices = [ {name: "Emacs", disabled: '(not installed)'}, "Atom", "GNU nano", {name: "Notepad++", disabled: '(not installed)'}, "Sublime", "Vim" ] ``` The disabled choice will be displayed with a cross ✘ character next to it and followed by an explanation: ```ruby prompt.enum_select('Select an editor', choices) # => # Select an editor # ✘ 1) Emacs (not installed) # 2) Atom # 3) GNU nano # ✘ 4) Notepad++ (not installed) # 5) Sublime # 6) Vim # Choose 1-6 [2]: ``` ### 2.7 expand The `expand` provides a compact way to ask a question with many options. As first argument `expand` takes the message to display and as a second an array of choices. Compared to the `select`, `multi_select` and `enum_select`, the choices need to be objects that include `:key`, `:name` and `:value` keys. The `:key` must be a single character. The help choice is added automatically as the last option under the key `h`. ```ruby choices = [ { key: 'y', name: 'overwrite this file', value: :yes }, { key: 'n', name: 'do not overwrite this file', value: :no }, { key: 'q', name: 'quit; do not overwrite this file ', value: :quit } ] ``` The choices can also be provided through DSL using the `choice` method. The `:value` can be a primitive value or `Proc` instance that gets executed and whose value is used as returned type. For example: ```ruby prompt.expand('Overwrite Gemfile?') do |q| q.choice key: 'y', name: 'Overwrite' do :ok end q.choice key: 'n', name: 'Skip', value: :no q.choice key: 'a', name: 'Overwrite all', value: :all q.choice key: 'd', name: 'Show diff', value: :diff q.choice key: 'q', name: 'Quit', value: :quit end ``` The first element in the array of choices or provided via `choice` DSL will be the default choice, you can change that by passing `default` option. ```ruby prompt.expand('Overwrite Gemfile?', choices) # => # Overwrite Gemfile? (enter "h" for help) [y,n,q,h] ``` Each time user types an option a hint will be displayed: ```ruby # Overwrite Gemfile? (enter "h" for help) [y,n,a,d,q,h] y # >> overwrite this file ``` If user types `h` and presses enter, an expanded view will be shown which further allows to refine the choice: ```ruby # Overwrite Gemfile? # y - overwrite this file # n - do not overwrite this file # q - quit; do not overwrite this file # h - print help # Choice [y]: ``` Run `examples/expand.rb` to see the prompt in action. #### 2.7.1 `:auto_hint` To show hint by default use `:auto_hint` option: ```ruby prompt.expand('Overwrite Gemfile?', choices, auto_hint: true) # => # Overwrite Gemfile? (enter "h" for help) [y,n,q,h] # >> overwrite this file ``` ### 2.8 collect In order to collect more than one answer use `collect` method. Using the `key` you can describe the answers key name. All the methods for asking user input such as `ask`, `mask`, `select` can be directly invoked on the key. The key composition is very flexible by allowing nested keys. If you want the value to be automatically converted to required type use [convert](#221-convert). For example to gather some contact information do: ```ruby prompt.collect do key(:name).ask('Name?') key(:age).ask('Age?', convert: :int) key(:address) do key(:street).ask('Street?', required: true) key(:city).ask('City?') key(:zip).ask('Zip?', validate: /\A\d{3}\Z/) end end # => # {:name => "Piotr", :age => 30, :address => {:street => "Street", :city => "City", :zip => "123"}} ``` In order to collect _mutliple values_ for a given key in a loop, chain `values` onto the `key` desired: ```ruby result = prompt.collect do key(:name).ask('Name?') key(:age).ask('Age?', convert: :int) while prompt.yes?("continue?") key(:addresses).values do key(:street).ask('Street?', required: true) key(:city).ask('City?') key(:zip).ask('Zip?', validate: /\A\d{3}\Z/) end end end # => # { # :name => "Piotr", # :age => 30, # :addresses => [ # {:street => "Street", :city => "City", :zip => "123"}, # {:street => "Street", :city => "City", :zip => "234"} # ] # } ``` ### 2.9 suggest To suggest possible matches for the user input use `suggest` method like so: ```ruby prompt.suggest('sta', ['stage', 'stash', 'commit', 'branch']) # => # Did you mean one of these? # stage # stash ``` To customize query text presented pass `:single_text` and `:plural_text` options to respectively change the message when one match is found or many. ```ruby possible = %w(status stage stash commit branch blame) prompt.suggest('b', possible, indent: 4, single_text: 'Perhaps you meant?') # => # Perhaps you meant? # blame ``` ### 2.10 slider If you have constrained range of numbers for user to choose from you may consider using `slider`. The slider provides easy visual way of picking a value marked by `●` symbol. You can set `:min`(defaults to 0), `:max` and `:step`(defaults to 1) options to configure slider range: ```ruby prompt.slider('Volume', max: 100, step: 5) # => # Volume ──────────●────────── 50 # (Use arrow keys, press Enter to select) ``` By default the slider is configured to pick middle of the range as a start value, you can change this by using the `:default` option: ```ruby prompt.slider('Volume', max: 100, step: 5, default: 75) # => # Volume ───────────────●────── 75 # (Use arrow keys, press Enter to select) ``` You can also change the default slider formatting using the `:format`. The value must contain the `:slider` token to show current value and any `sprintf` compatible flag for number display, in our case `%d`: ```ruby prompt.slider('Volume', max: 100, step: 5, default: 75, format: "|:slider| %d%") # => # Volume |───────────────●──────| 75% # (Use arrow keys, press Enter to select) ``` You can also specify slider range with decimal numbers. For example, to have a step of `0.5` and display each value with a single decimal place use `%f` as format: ```ruby prompt.slider("Volume", max: 10, step: 0.5, default: 5, format: "|:slider| %.1f") # => # Volume |───────────────●──────| 7.5 # (Use arrow keys, press Enter to select) ``` If you wish to change the slider handle and the slider range display use `:symbols` option: ```ruby prompt.slider("Volume", max: 100, step: 5, default: 75, symbols: {bullet: 'x', line: '_'}) # => # Volume _______________x______ 75% # (Use arrow keys, press Enter to select) ``` Slider can be configured through DSL as well: ```ruby prompt.slider('What size?') do |range| range.max 100 range.step 5 range.default 75 range.format "|:slider| %d%" end # => # Volume |───────────────●──────| 75% # (Use arrow keys, press Enter to select) ``` ### 2.11 say To simply print message out to standard output use `say` like so: ```ruby prompt.say(...) ``` The `say` method also accepts option `:color` which supports all the colors provided by [pastel](https://github.com/piotrmurach/pastel#3-supported-colors) **TTY::Prompt** provides more specific versions of `say` method to better express intention behind the message such as `ok`, `warn` and `error`. #### 2.11.1 ok Print message(s) in green do: ```ruby prompt.ok(...) ``` #### 2.12.2 warn Print message(s) in yellow do: ```ruby prompt.warn(...) ``` #### 2.11.3 error Print message(s) in red do: ```ruby prompt.error(...) ``` #### 2.12 keyboard events All the prompt types, when a key is pressed, fire key press events. You can subscribe to listen to this events by calling `on` with type of event name. ```ruby prompt.on(:keypress) { |event| ... } ``` The event object is yielded to a block whenever particular event fires. The event has `key` and `value` methods. Further, the `key` responds to following messages: * `name` - the name of the event such as :up, :down, letter or digit * `meta` - true if event is non-standard key associated * `shift` - true if shift has been pressed with the key * `ctrl` - true if ctrl has been pressed with the key For example, to add vim like key navigation to `select` prompt one would do the following: ```ruby prompt.on(:keypress) do |event| if event.value == 'j' prompt.trigger(:keydown) end if event.value == 'k' prompt.trigger(:keyup) end end ``` You can subscribe to more than one event: ```ruby prompt.on(:keypress) { |key| ... } .on(:keydown) { |key| ... } ``` The available events are: * `:keypress` * `:keydown` * `:keyup` * `:keyleft` * `:keyright` * `:keynum` * `:keytab` * `:keyenter` * `:keyreturn` * `:keyspace` * `:keyescape` * `:keydelete` * `:keybackspace` ## 3 settings ### 3.1. `:symbols` Many prompts use symbols to display information. You can overwrite the default symbols for all the prompts using the `:symbols` key and hash of symbol names as value: ```ruby prompt = TTY::Prompt.new(symbols: {marker: '>'}) ``` The following symbols can be overwritten: | Symbols | Unicode | ASCII | | ----------- |:-------:|:-----:| | tick | `✓` | `√` | | cross | `✘` | `x` | | marker | `‣` | `>` | | dot | `•` | `.` | | bullet | `●` | `O` | | line | `─` | `-` | | radio_on | `⬢` | `(*)` | | radio_off | `⬡` | `( )` | | arrow_up | `↑` | `↑` | | arrow_down | `↓` | `↓` | | arrow_left | `←` | `←` | | arrow_right| `→` | `→` | ### 3.2 `:active_color` All prompt types support `:active_color` option. In case of `select`, `multi_select`, `enum_select` or `expand` this color is used to highlight the currently selected choice. All the resulted inputs provided by user that are read in by the prompt as answer are highlighted with this color. This option can be applied either globally for all prompts or individually. ```ruby prompt = TTY::Prompt.new(active_color: :cyan) ``` or per individual input do: ```ruby prompt.select('What size?', %w(Large Medium Small), active_color: :cyan) ``` Please [see pastel](https://github.com/piotrmurach/pastel#3-supported-colors) for all supported colors. ### 3.3 `:enable_color` If you wish to disable coloring for a prompt simply pass `:enable_color` option ```ruby prompt = TTY::Prompt.new(enable_color: true) ``` ### 3.4 `:help_color` Prompts such as `select`, `multi_select`, `expand` support `:help_color` which is used to customize the help text. This option can be applied either globally for all prompts or individually. ```ruby prompt = TTY::Prompt.new(help_color: :cyan) ``` or per individual input do: ```ruby prompt.select('What size?', %w(Large Medium Small), help_color: :cyan) ``` ### 3.5 `:interrupt` By default `InputInterrupt` error will be raised when the user hits the interrupt key(Control-C). However, you can customise this behaviour by passing the `:interrupt` option. The available options are: * `:signal` - sends interrupt signal * `:exit` - exists with status code * `:noop` - skips handler * custom proc For example, to send interrupt signal do: ```ruby prompt = TTY::Prompt.new(interrupt: :signal) ``` ### 3.6 `:prefix` You can prefix each question asked using the `:prefix` option. This option can be applied either globally for all prompts or individual for each one: ```ruby prompt = TTY::Prompt.new(prefix: '[?] ') ``` ### 3.7 `:track_history` The prompts that accept line input such as `multiline` or `ask` provide history buffer that tracks all the lines entered during `TTY::Prompt.new` interactions. The history buffer provides previous or next lines when user presses up/down arrows respectively. However, if you wish to disable this behaviour use `:track_history` option like so: ```ruby prompt = TTY::Prompt.new(track_history: false) ``` ## Contributing 1. Fork it ( https://github.com/piotrmurach/tty-prompt/fork ) 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create a new Pull Request This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. ## Copyright Copyright (c) 2015 Piotr Murach. See LICENSE for further details. tty-prompt-0.21.0/Rakefile000066400000000000000000000002171363125043200153750ustar00rootroot00000000000000require "bundler/gem_tasks" FileList['tasks/**/*.rake'].each(&method(:import)) desc 'Run all specs' task ci: %w[ spec ] task default: :spec tty-prompt-0.21.0/appveyor.yml000066400000000000000000000012341363125043200163200ustar00rootroot00000000000000--- install: - SET PATH=C:\Ruby%ruby_version%\bin;%PATH% - gem install bundler -v '< 2.0' - bundle install before_test: - ruby -v - gem -v - bundle -v build: off test_script: - bundle exec rake ci environment: matrix: - ruby_version: "200" - ruby_version: "200-x64" - ruby_version: "21" - ruby_version: "21-x64" - ruby_version: "22" - ruby_version: "22-x64" - ruby_version: "23" - ruby_version: "23-x64" - ruby_version: "24" - ruby_version: "24-x64" - ruby_version: "25" - ruby_version: "25-x64" - ruby_version: "26" - ruby_version: "26-x64" - ruby_version: "27" - ruby_version: "27-x64" tty-prompt-0.21.0/benchmarks/000077500000000000000000000000001363125043200160455ustar00rootroot00000000000000tty-prompt-0.21.0/benchmarks/speed.rb000066400000000000000000000013141363125043200174710ustar00rootroot00000000000000# coding: utf-8 require 'benchmark/ips' require 'stringio' require_relative '../lib/tty-prompt' input = ::StringIO.new output = ::StringIO.new prompt = TTY::Prompt.new(input: input, output: output) Benchmark.ips do |r| r.report("Ruby #puts") do output.puts "What is your name?" end r.report("TTY::Prompt #ask") do prompt.ask("What is your name?") end end # Calculating ------------------------------------- # Ruby #puts 34601 i/100ms # TTY::Prompt #ask 12 i/100ms # ------------------------------------------------- # Ruby #puts 758640.5 (±14.9%) i/s - 3736908 in 5.028562s # TTY::Prompt #ask 63.1 (±7.9%) i/s - 324 in 5.176857s tty-prompt-0.21.0/examples/000077500000000000000000000000001363125043200155465ustar00rootroot00000000000000tty-prompt-0.21.0/examples/ask.rb000066400000000000000000000002261363125043200166510ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new prompt.ask('What is your name?', default: ENV['USER']) tty-prompt-0.21.0/examples/ask_blank.rb000066400000000000000000000002351363125043200200200ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new(prefix: ">") answer= prompt.ask puts "Answer: \"#{answer}\"" tty-prompt-0.21.0/examples/ask_multiline.rb000066400000000000000000000002301363125043200207260ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new prompt.ask("What\nis your\nname?", default: ENV['USER']) tty-prompt-0.21.0/examples/ask_valid.rb000066400000000000000000000004611363125043200200310ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new prompt.ask('Folder name?') do |q| q.required(true) q.validate ->(v) { return !Dir.exist?(v) } q.messages[:valid?] = 'Folder already exists?' q.messages[:required?] = 'Folder name must not be empty' end tty-prompt-0.21.0/examples/collect.rb000066400000000000000000000006321363125043200175210ustar00rootroot00000000000000# frozen_string_literal: true require 'json' require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new(prefix: '[?] ') result = prompt.collect do key(:name).ask('Name?') key(:age).ask('Age?', convert: :int) key(:address) do key(:street).ask('Street?', required: true) key(:city).ask('City?') key(:zip).ask('Zip?', validate: /\A\d{3}\Z/) end end puts JSON.pretty_generate(result) tty-prompt-0.21.0/examples/echo.rb000066400000000000000000000003241363125043200170100ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new answer = prompt.ask('Password?', echo: false) do |q| q.validate(/^[^\.]+\.[^\.]+/) end puts "Password: #{answer}" tty-prompt-0.21.0/examples/enum_select.rb000066400000000000000000000003271363125043200204000ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new choices = %i(/bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny) prompt.enum_select('Select an editor', choices, default: 2) tty-prompt-0.21.0/examples/enum_select_disabled.rb000066400000000000000000000004571363125043200222330ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new choices = [ {name: "Emacs", disabled: '(not installed)'}, "Atom", "GNU nano", {name: "Notepad++", disabled: '(not installed)'}, "Sublime", "Vim" ] prompt.enum_select('Select an editor', choices) tty-prompt-0.21.0/examples/enum_select_paged.rb000066400000000000000000000003151363125043200215350ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new alfabet = ('A'..'Z').to_a prompt.enum_select('Which letter?', alfabet, per_page: 4, cycle: true, default: 2) tty-prompt-0.21.0/examples/enum_select_wrapped.rb000066400000000000000000000017301363125043200221210ustar00rootroot00000000000000# frozen_string_literal: true require_relative '../lib/tty-prompt' prompt = TTY::Prompt.new quotes = [ "There are certain queer times and occasions in this strange mixed affair we call life when a man takes this whole universe for a vast practical joke, though the wit thereof he but dimly discerns, and more than suspects that the joke is at nobody's expense but his own.", "Talk not to me of blasphemy, man;\n I'd strike the sun if it insulted me.", "There is a wisdom that is woe; but there is a woe that is madness. And there is a Catskill eagle in some souls that can alike dive down into the blackest gorges, and soar out of them again and become invisible in the sunny spaces. And even if he for ever flies within the gorge, that gorge is in the mountains; so that even in his lowest swoop the mountain eagle is still higher than other birds upon the plain, even though they soar." ] answer = prompt.enum_select('Chose your quote?', quotes) puts "Answer: #{answer}" tty-prompt-0.21.0/examples/expand.rb000066400000000000000000000007721363125043200173600ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" choices = [{ key: 'y', name: 'overwrite this file', value: :yes }, { key: 'n', name: 'do not overwrite this file', value: :no }, { key: 'a', name: 'overwrite this file and all later files', value: :all }, { key: 'd', name: 'show diff', value: :diff }, { key: 'q', name: 'quit; do not overwrite this file ', value: :quit }] prompt = TTY::Prompt.new prompt.expand('Overwrite Gemfile?', choices, default: 3) tty-prompt-0.21.0/examples/expand_auto.rb000066400000000000000000000007771363125043200204150ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" choices = [{ key: 'y', name: 'overwrite this file', value: :yes }, { key: 'n', name: 'do not overwrite this file', value: :no }, { key: 'a', name: 'overwrite this file and all later files', value: :all }, { key: 'd', name: 'show diff', value: :diff }, { key: 'q', name: 'quit; do not overwrite this file ', value: :quit }] prompt = TTY::Prompt.new prompt.expand('Overwrite Gemfile?', choices, auto_hint: true) tty-prompt-0.21.0/examples/in.rb000066400000000000000000000003231363125043200164770ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new prompt.ask('How do you like it on scale 1 - 10?', in: '1-10') do |q| q.messages[:range?] = "Sorry wrong one!" end tty-prompt-0.21.0/examples/inputs.rb000066400000000000000000000004301363125043200174120ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new prompt.ask('What is your name?', default: ENV['USER']) prompt.yes?('Do you like Ruby?') prompt.mask("What is your secret?") prompt.select("Choose your destiny?", %w(Scorpion Kano Jax)) tty-prompt-0.21.0/examples/key_events.rb000066400000000000000000000004261363125043200202510ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt::new(interrupt: :exit) prompt.on(:keypress) do |event| puts "name: #{event.key.name}, value: #{event.value.dump}" end prompt.on(:keyescape) do |event| exit end prompt.read_keypress tty-prompt-0.21.0/examples/keypress.rb000066400000000000000000000002701363125043200177370ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt::new answer = prompt.keypress("Press any key to continue") puts "Answer: #{answer.inspect}" tty-prompt-0.21.0/examples/mask.rb000066400000000000000000000004531363125043200170300ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" require 'pastel' prompt = TTY::Prompt.new heart = prompt.decorate(prompt.symbols[:heart] + ' ', :magenta) res = prompt.mask('What is your secret?', mask: heart) do |q| q.validate(/[a-z\ ]{5,15}/) end puts "Secret: \"#{res}\"" tty-prompt-0.21.0/examples/multi_select.rb000066400000000000000000000003071363125043200205640ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new drinks = %w(vodka beer wine whisky bourbon) prompt.multi_select('Choose your favourite drink?', drinks) tty-prompt-0.21.0/examples/multi_select_disabled.rb000066400000000000000000000005171363125043200224160ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new drinks = [ 'bourbon', {name: 'sake', disabled: '(out of stock)'}, 'vodka', {name: 'beer', disabled: '(out of stock)'}, 'wine', 'whisky' ] answer = prompt.multi_select('Choose your favourite drink?', drinks) puts answer.inspect tty-prompt-0.21.0/examples/multi_select_disabled_paged.rb000066400000000000000000000005761363125043200235630ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new numbers = [ {name: '1', disabled: 'out'}, '2', {name: '3', disabled: 'out'}, '4', '5', {name: '6', disabled: 'out'}, '7', '8', '9', {name: '10', disabled: 'out'} ] answer = prompt.multi_select('Which letter?', numbers, per_page: 4, cycle: true) puts answer.inspect tty-prompt-0.21.0/examples/multi_select_paged.rb000066400000000000000000000002751363125043200217300ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new alfabet = ('A'..'Z').to_a prompt.multi_select('Which letter?', alfabet, per_page: 7, max: 3) tty-prompt-0.21.0/examples/multi_select_wrapped.rb000066400000000000000000000017451363125043200223150ustar00rootroot00000000000000# frozen_string_literal: true require_relative '../lib/tty-prompt' prompt = TTY::Prompt.new quotes = [ "There are certain queer times and occasions in this strange mixed affair we call life when a man takes this whole universe for a vast practical joke, though the wit thereof he but dimly discerns, and more than suspects that the joke is at nobody's expense but his own.", "Talk not to me of blasphemy, man; I'd strike the sun if it insulted me.", "There is a wisdom that is woe; but there is a woe that is madness. And there is a Catskill eagle in some souls that can alike dive down into the blackest gorges, and soar out of them again and become invisible in the sunny spaces. And even if he for ever flies within the gorge, that gorge is in the mountains; so that even in his lowest swoop the mountain eagle is still higher than other birds upon the plain, even though they soar." ] answer = prompt.multi_select('Choose your quote?', quotes, echo: false) puts "Answer: #{answer}" tty-prompt-0.21.0/examples/multiline.rb000066400000000000000000000002541363125043200200760ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt::new answer = prompt.multiline("Description:") puts "Answer: #{answer.inspect}" tty-prompt-0.21.0/examples/pause.rb000066400000000000000000000004201363125043200172040ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt::new answer = prompt.keypress("Press space or enter to continue, continuing automatically in :countdown ...", keys: [:space, :return], timeout: 3) puts "Answer: #{answer.inspect}" tty-prompt-0.21.0/examples/select.rb000066400000000000000000000006461363125043200173600ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new warriors = %i(Scorpion Kano Jax Kitana Raiden) prompt.on(:keypress) do |event| if event.value == 'j' prompt.trigger(:keydown) end if event.value == 'k' prompt.trigger(:keyup) end end prompt.on(:keyescape) do |event| exit(1) end answer = prompt.select('Choose your destiny?', warriors) puts answer.inspect tty-prompt-0.21.0/examples/select_disabled.rb000066400000000000000000000004521363125043200212020ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new warriors = [ 'Scorpion', 'Kano', { name: 'Goro', disabled: '(injury)' }, 'Jax', 'Kitana', 'Raiden' ] answer = prompt.select('Choose your destiny?', warriors, enum: ')') puts answer.inspect tty-prompt-0.21.0/examples/select_disabled_paged.rb000066400000000000000000000005701363125043200223430ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new numbers = [ {name: '1', disabled: 'out'}, '2', {name: '3', disabled: 'out'}, '4', '5', {name: '6', disabled: 'out'}, '7', '8', '9', {name: '10', disabled: 'out'} ] answer = prompt.select('Which letter?', numbers, per_page: 4, cycle: true) puts answer.inspect tty-prompt-0.21.0/examples/select_enum.rb000066400000000000000000000002731363125043200204000ustar00rootroot00000000000000# frozen_string_litreal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new warriors = %w(Scorpion Kano Jax) prompt.select('Choose your destiny?', warriors, enum: ')') tty-prompt-0.21.0/examples/select_filtered.rb000066400000000000000000000003531363125043200212310ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new warriors = %w(Scorpion Kano Jax Kitana Raiden) answer = prompt.select('Choose your destiny?', warriors, filter: true) puts answer.inspect tty-prompt-0.21.0/examples/select_paginated.rb000066400000000000000000000003461363125043200213710ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new alfabet = ('A'..'Z').to_a answer = prompt.select('Which letter?', alfabet, per_page: 7, cycle: true, default: 5) puts answer.inspect tty-prompt-0.21.0/examples/select_wrapped.rb000066400000000000000000000017231363125043200210770ustar00rootroot00000000000000# frozen_string_literal: true require_relative '../lib/tty-prompt' prompt = TTY::Prompt.new quotes = [ "There are certain queer times and occasions in this strange mixed affair we call life when a man takes this whole universe for a vast practical joke, though the wit thereof he but dimly discerns, and more than suspects that the joke is at nobody's expense but his own.", "Talk not to me of blasphemy, man;\n I'd strike the sun if it insulted me.", "There is a wisdom that is woe; but there is a woe that is madness. And there is a Catskill eagle in some souls that can alike dive down into the blackest gorges, and soar out of them again and become invisible in the sunny spaces. And even if he for ever flies within the gorge, that gorge is in the mountains; so that even in his lowest swoop the mountain eagle is still higher than other birds upon the plain, even though they soar." ] answer = prompt.select('Chose your quote?', quotes) puts "Answer: #{answer}" tty-prompt-0.21.0/examples/slider.rb000066400000000000000000000002601363125043200173530ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new prompt.slider("Volume", max: 100, step: 5, default: 75, format: "|:slider| %d%%") tty-prompt-0.21.0/examples/validation.rb000066400000000000000000000002571363125043200202310ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new prompt.ask('What is your username?') do |q| q.validate(/^[^\.]+\.[^\.]+/) end tty-prompt-0.21.0/examples/yes_no.rb000066400000000000000000000002001363125043200173570ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new prompt.yes?('Do you like Ruby?') tty-prompt-0.21.0/lib/000077500000000000000000000000001363125043200144765ustar00rootroot00000000000000tty-prompt-0.21.0/lib/tty-prompt.rb000066400000000000000000000001011363125043200171520ustar00rootroot00000000000000require_relative 'tty/prompt' require_relative 'tty/test_prompt' tty-prompt-0.21.0/lib/tty/000077500000000000000000000000001363125043200153165ustar00rootroot00000000000000tty-prompt-0.21.0/lib/tty/prompt.rb000066400000000000000000000357661363125043200172050ustar00rootroot00000000000000# frozen_string_literal: true require 'forwardable' require 'pastel' require 'tty-cursor' require 'tty-reader' require 'tty-screen' require_relative 'prompt/answers_collector' require_relative 'prompt/confirm_question' require_relative 'prompt/expander' require_relative 'prompt/enum_list' require_relative 'prompt/keypress' require_relative 'prompt/list' require_relative 'prompt/multi_list' require_relative 'prompt/multiline' require_relative 'prompt/mask_question' require_relative 'prompt/question' require_relative 'prompt/slider' require_relative 'prompt/statement' require_relative 'prompt/suggestion' require_relative 'prompt/symbols' require_relative 'prompt/utils' require_relative 'prompt/version' module TTY # A main entry for asking prompt questions. class Prompt extend Forwardable # Raised when wrong parameter is used to configure prompt ConfigurationError = Class.new(StandardError) # Raised when type conversion cannot be performed ConversionError = Class.new(StandardError) # Raised when the passed in validation argument is of wrong type ValidationCoercion = Class.new(TypeError) # Raised when the required argument is not supplied ArgumentRequired = Class.new(ArgumentError) # Raised when the argument validation fails ArgumentValidation = Class.new(ArgumentError) # Raised when the argument is not expected InvalidArgument = Class.new(ArgumentError) # @api private attr_reader :input # @api private attr_reader :output attr_reader :reader attr_reader :cursor # Prompt prefix # # @example # prompt = TTY::Prompt.new(prefix: [?]) # # @return [String] # # @api private attr_reader :prefix # Theme colors # # @api private attr_reader :active_color, :help_color, :error_color, :enabled_color # The collection of display symbols # # @example # prompt = TTY::Prompt.new(symbols: {marker: '>'}) # # @return [Hash] # # @api private attr_reader :symbols def_delegators :@pastel, :decorate, :strip def_delegators :@cursor, :clear_lines, :clear_line, :show, :hide def_delegators :@reader, :read_char, :read_keypress, # :read_line, :read_multiline, :on, :subscribe, :unsubscribe, :trigger, :count_screen_lines def_delegators :@output, :print, :puts, :flush def self.messages { range?: 'Value %{value} must be within the range %{in}', valid?: 'Your answer is invalid (must match %{valid})', required?: 'Value must be provided' } end # This fixes Forwardable module keyword arguments warning def read_line(message, **options) @reader.read_line(message, **options) end # Initialize a Prompt # # @param [Hash] options # @option options [IO] :input # the input stream # @option options [IO] :output # the output stream # @option options [Hash] :env # the environment variables # @option options [String] :prefix # the prompt prefix, by default empty # @option options [Boolean] :enable_color # enable color support, true by default # @option options [String] :active_color # the color used for selected option # @option options [String] :help_color # the color used for help text # @option options [String] :error_color # the color used for displaying error messages # @option options [Symbol] :interrupt # handling of Ctrl+C key out of :signal, :exit, :noop # @option options [Boolean] :track_history # disable line history tracking, true by default # @option options [Hash] :symbols # the symbols displayed in prompts such as :marker, :cross # # @api public def initialize(*args) options = Utils.extract_options!(args) @input = options.fetch(:input) { $stdin } @output = options.fetch(:output) { $stdout } @env = options.fetch(:env) { ENV } @prefix = options.fetch(:prefix) { '' } @enabled_color = options[:enable_color] @active_color = options.fetch(:active_color) { :green } @help_color = options.fetch(:help_color) { :bright_black } @error_color = options.fetch(:error_color) { :red } @interrupt = options.fetch(:interrupt) { :error } @track_history = options.fetch(:track_history) { true } @symbols = Symbols.symbols.merge(options.fetch(:symbols, {})) @cursor = TTY::Cursor @pastel = Pastel.new(@enabled_color.nil? ? {} : { enabled: @enabled_color }) @reader = TTY::Reader.new( input: @input, output: @output, interrupt: @interrupt, track_history: @track_history, env: @env ) end # Invoke a question type of prompt # # @example # prompt = TTY::Prompt.new # prompt.invoke_question(Question, "Your name? ") # # @return [String] # # @api public def invoke_question(object, message, **options, &block) options[:messages] = self.class.messages question = object.new(self, **options) question.(message, &block) end # Ask a question. # # @example # propmt = TTY::Prompt.new # prompt.ask("What is your name?") # # @param [String] message # the question to be asked # # @yieldparam [TTY::Prompt::Question] question # further configure the question # # @yield [question] # # @return [TTY::Prompt::Question] # # @api public def ask(message = '', **options, &block) invoke_question(Question, message, **options, &block) end # Ask a question with a keypress answer # # @see #ask # # @api public def keypress(message = '', **options, &block) invoke_question(Keypress, message, **options, &block) end # Ask a question with a multiline answer # # @example # prompt.multiline('Description?') # # @return [Array[String]] # # @api public def multiline(message = '', **options, &block) invoke_question(Multiline, message, **options, &block) end # Invoke a list type of prompt # # @example # prompt = TTY::Prompt.new # editors = %w(emacs nano vim) # prompt.invoke_select(EnumList, "Select editor: ", editors) # # @return [String] # # @api public def invoke_select(object, question, *args, &block) options = Utils.extract_options!(args) choices = if block [] elsif args.empty? possible = options.dup options = {} possible elsif args.size == 1 && args[0].is_a?(Hash) Utils.extract_options!(args) else args.flatten end list = object.new(self, **options) list.(question, choices, &block) end # Ask masked question # # @example # propmt = TTY::Prompt.new # prompt.mask("What is your secret?") # # @return [TTY::Prompt::MaskQuestion] # # @api public def mask(message = '', **options, &block) invoke_question(MaskQuestion, message, **options, &block) end # Ask a question with a list of options # # @example # prompt = TTY::Prompt.new # prompt.select("What size?", %w(large medium small)) # # @example # prompt = TTY::Prompt.new # prompt.select("What size?") do |menu| # menu.choice :large # menu.choices %w(:medium :small) # end # # @param [String] question # the question to ask # # @param [Array[Object]] choices # the choices to select from # # @api public def select(question, *args, &block) invoke_select(List, question, *args, &block) end # Ask a question with multiple attributes activated # # @example # prompt = TTY::Prompt.new # choices = %w(Scorpion Jax Kitana Baraka Jade) # prompt.multi_select("Choose your destiny?", choices) # # @param [String] question # the question to ask # # @param [Array[Object]] choices # the choices to select from # # @return [String] # # @api public def multi_select(question, *args, &block) invoke_select(MultiList, question, *args, &block) end # Ask a question with indexed list # # @example # prompt = TTY::Prompt.new # editors = %w(emacs nano vim) # prompt.enum_select(EnumList, "Select editor: ", editors) # # @param [String] question # the question to ask # # @param [Array[Object]] choices # the choices to select from # # @return [String] # # @api public def enum_select(question, *args, &block) invoke_select(EnumList, question, *args, &block) end # A shortcut method to ask the user positive question and return # true for 'yes' reply, false for 'no'. # # @example # prompt = TTY::Prompt.new # prompt.yes?('Are you human?') # # => Are you human? (Y/n) # # @return [Boolean] # # @api public def yes?(message, *args, &block) defaults = { default: true } options = Utils.extract_options!(args) options.merge!(defaults.reject { |k, _| options.key?(k) }) question = ConfirmQuestion.new(self, **options) question.call(message, &block) end # A shortcut method to ask the user negative question and return # true for 'no' reply. # # @example # prompt = TTY::Prompt.new # prompt.no?('Are you alien?') # => true # # => Are you human? (y/N) # # @return [Boolean] # # @api public def no?(message, *args, &block) defaults = { default: false } options = Utils.extract_options!(args) options.merge!(defaults.reject { |k, _| options.key?(k) }) question = ConfirmQuestion.new(self, **options) !question.call(message, &block) end # Expand available options # # @example # prompt = TTY::Prompt.new # choices = [{ # key: 'Y', # name: 'Overwrite', # value: :yes # }, { # key: 'n', # name: 'Skip', # value: :no # }] # prompt.expand('Overwirte Gemfile?', choices) # # @return [Object] # the user specified value # # @api public def expand(message, *args, &block) invoke_select(Expander, message, *args, &block) end # Ask a question with a range slider # # @example # prompt = TTY::Prompt.new # prompt.slider('What size?', min: 32, max: 54, step: 2) # # @param [String] question # the question to ask # # @return [String] # # @api public def slider(question, *args, &block) options = Utils.extract_options!(args) slider = Slider.new(self, **options) slider.call(question, &block) end # Print statement out. If the supplied message ends with a space or # tab character, a new line will not be appended. # # @example # say("Simple things.", color: :red) # # @param [String] message # # @return [String] # # @api public def say(message = '', options = {}) message = message.to_s return if message.empty? statement = Statement.new(self, options) statement.call(message) end # Print statement(s) out in red green. # # @example # prompt.ok "Are you sure?" # prompt.ok "All is fine!", "This is fine too." # # @param [Array] messages # # @return [Array] messages # # @api public def ok(*args) options = Utils.extract_options!(args) args.each { |message| say message, options.merge(color: :green) } end # Print statement(s) out in yellow color. # # @example # prompt.warn "This action can have dire consequences" # prompt.warn "Carefull young apprentice", "This is potentially dangerous" # # @param [Array] messages # # @return [Array] messages # # @api public def warn(*args) options = Utils.extract_options!(args) args.each { |message| say message, options.merge(color: :yellow) } end # Print statement(s) out in red color. # # @example # prompt.error "Shutting down all systems!" # prompt.error "Nothing is fine!", "All is broken!" # # @param [Array] messages # # @return [Array] messages # # @api public def error(*args) options = Utils.extract_options!(args) args.each { |message| say message, options.merge(color: :red) } end # Print debug information in terminal top right corner # # @example # prompt.debug "info1", "info2" # # @param [Array] messages # # @retrun [nil] # # @api public def debug(*messages) longest = messages.max_by(&:length).size width = TTY::Screen.width - longest print cursor.save messages.reverse_each.with_index do |msg, i| print cursor.column(width) + cursor.up + cursor.clear_line_after print msg end ensure print cursor.restore end # Takes the string provided by the user and compare it with other possible # matches to suggest an unambigous string # # @example # prompt.suggest('sta', ['status', 'stage', 'commit', 'branch']) # # => "status, stage" # # @param [String] message # # @param [Array] possibilities # # @param [Hash] options # @option options [String] :indent # The number of spaces for indentation # @option options [String] :single_text # The text for a single suggestion # @option options [String] :plural_text # The text for multiple suggestions # # @return [String] # # @api public def suggest(message, possibilities, **options) suggestion = Suggestion.new(**options) say(suggestion.suggest(message, possibilities)) end # Gathers more than one aswer # # @example # prompt.collect do # key(:name).ask('Name?') # end # # @return [Hash] # the collection of answers # # @api public def collect(**options, &block) collector = AnswersCollector.new(self, **options) collector.call(&block) end # Check if outputing to terminal # # @return [Boolean] # # @api public def tty? stdout.tty? end # Return standard in # # @api private def stdin $stdin end # Return standard out # # @api private def stdout $stdout end # Return standard error # # @api private def stderr $stderr end # Inspect class name and public attributes # @return [String] # # @api public def inspect attributes = { input: input, output: output, prefix: prefix, active_color: active_color, error_color: error_color, enabled_color: enabled_color, help_color: help_color } "#<#{self.class}: #{attributes.each { |name, val| "@#{name}=#{val}" }}" end end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/000077500000000000000000000000001363125043200166375ustar00rootroot00000000000000tty-prompt-0.21.0/lib/tty/prompt/answers_collector.rb000066400000000000000000000032101363125043200227100ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt class AnswersCollector # Initialize answer collector # # @api public def initialize(prompt, **options) @prompt = prompt @answers = options.fetch(:answers) { {} } end # Start gathering answers # # @return [Hash] # the collection of all answers # # @api public def call(&block) instance_eval(&block) @answers end # Create answer entry # # @example # key(:name).ask('Name?') # # @api public def key(name, &block) @name = name if block answer = create_collector.call(&block) add_answer(answer) end self end # Change to collect all values for a key # # @example # key(:colors).values.ask('Color?') # # @api public def values(&block) @answers[@name] = Array(@answers[@name]) if block answer = create_collector.call(&block) add_answer(answer) end self end # @api public def create_collector self.class.new(@prompt) end # @api public def add_answer(answer) if @answers[@name].is_a?(Array) @answers[@name] << answer else @answers[@name] = answer end end private # @api private def method_missing(method, *args, **options, &block) answer = @prompt.public_send(method, *args, **options, &block) add_answer(answer) end end # AnswersCollector end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/block_paginator.rb000066400000000000000000000033671363125043200223330ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'paginator' module TTY class Prompt class BlockPaginator < Paginator # Paginate list of choices based on current active choice. # Move entire pages. # # @api public def paginate(list, active, per_page = nil, &block) default_size = (list.size <= DEFAULT_PAGE_SIZE ? list.size : DEFAULT_PAGE_SIZE) @per_page = @per_page || per_page || default_size check_page_size! # Don't paginate short lists if list.size <= @per_page @start_index = 0 @end_index = list.size - 1 if block return list.each_with_index(&block) else return list.each_with_index.to_enum end end unless active.nil? # User may input index out of range @last_index = active end page = (@last_index / @per_page.to_f).ceil pages = (list.size / @per_page.to_f).ceil if page == 0 @start_index = 0 @end_index = @start_index + @per_page - 1 elsif page > 0 && page < pages @start_index = (page - 1) * @per_page @end_index = @start_index + @per_page - 1 elsif page == pages @start_index = (page - 1) * @per_page @end_index = list.size - 1 else @end_index = list.size - 1 @start_index = @end_index - @per_page + 1 end sliced_list = list[@start_index..@end_index] page_range = (@start_index..@end_index) return sliced_list.zip(page_range).to_enum unless block_given? sliced_list.each_with_index do |item, index| block[item, @start_index + index] end end end # EnumPaginator end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/choice.rb000066400000000000000000000055521363125043200204250ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt # An immutable representation of a single choice option from select menu # # @api public class Choice # Create choice from value # # @examples # Choice.from(:foo) # # => # # Choice.from([:foo, 1]) # # => # # Choice.from({name: :foo, value: 1, key: 'f'} # # => # # @param [Object] val # the value to be converted # # @raise [ArgumentError] # # @return [Choice] # # @api public def self.from(val) case val when Choice val when Array name, value, options = *val if name.is_a?(Hash) convert_hash(name) else new(name.to_s, (value.nil? ? name.to_s : value), **(options || {})) end when Hash convert_hash(val) else new(val, val) end end # Convert hash into choice # # @api public def self.convert_hash(val) if val.key?(:name) && val.key?(:value) new(val[:name].to_s, val[:value], **val) elsif val.key?(:name) new(val[:name].to_s, val[:name].to_s, **val) else new(val.keys.first.to_s, val.values.first) end end # The label name # # @api public attr_reader :name # The keyboard key to activate this choice # # @api public attr_reader :key # The text to display for disabled choice # # @api public attr_reader :disabled # Create a Choice instance # # @api public def initialize(name, value, **options) @name = name @value = value @key = options[:key] @disabled = options[:disabled].nil? ? false : options[:disabled] freeze end # Check if this choice is disabled # # @return [Boolean] # # @api public def disabled? !!@disabled end # Read value and evaluate # # @api public def value case @value when Proc @value.call else @value end end # Object equality comparison # # @return [Boolean] # # @api public def ==(other) return false unless other.is_a?(self.class) name == other.name && value == other.value && key == other.key end # Object string representation # # @return [String] # # @api public def to_s "#{name}" end end # Choice end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/choices.rb000066400000000000000000000044451363125043200206100ustar00rootroot00000000000000# frozen_string_literal: true require 'forwardable' require_relative 'choice' module TTY class Prompt # A class responsible for storing a collection of choices # # @api private class Choices include Enumerable extend Forwardable # The actual collection choices # # @return [Array[Choice]] # # @api public attr_reader :choices def_delegators :choices, :length, :size, :to_ary, :empty?, :values_at, :index # Convenience for creating choices # # @param [Array[Object]] choices # the choice objects # # @return [Choices] # the choices collection # # @api public def self.[](*choices) new(choices) end # Create Choices collection # # @param [Array[Choice]] choices # the choices to add to collection # # @api public def initialize(choices = []) @choices = choices.map do |choice| Choice.from(choice) end end # Iterate over all choices in the collection # # @yield [Choice] # # @api public def each(&block) return to_enum unless block_given? choices.each(&block) end # Add choice to collection # # @param [Object] choice # the choice to add # # @api public def <<(choice) choices << Choice.from(choice) end # Access choice by index # # @param [Integer] index # # @return [Choice] # # @api public def [](index) @choices[index] end # Pluck a choice by its name from collection # # @param [String] name # the label name for the choice # # @return [Choice] # # @api public def pluck(name) map { |choice| choice.public_send(name) } end # Find a matching choice # # @exmaple # choices.find_by(:name, 'small') # # @param [Symbol] attr # the attribute name # @param [Object] value # # @return [Choice] # # @api public def find_by(attr, value) find { |choice| choice.public_send(attr) == value } end end # Choices end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/confirm_question.rb000066400000000000000000000070661363125043200225610ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'question' require_relative 'utils' module TTY class Prompt class ConfirmQuestion < Question # Create confirmation question # # @param [Hash] options # @option options [String] :suffix # @option options [String] :positive # @option options [String] :negative # # @api public def initialize(prompt, **options) super @suffix = options.fetch(:suffix) { UndefinedSetting } @positive = options.fetch(:positive) { UndefinedSetting } @negative = options.fetch(:negative) { UndefinedSetting } end def positive? @positive != UndefinedSetting end def negative? @negative != UndefinedSetting end def suffix? @suffix != UndefinedSetting end # Set question suffix # # @api public def suffix(value = (not_set = true)) return @negative if not_set @suffix = value end # Set value for matching positive choice # # @api public def positive(value = (not_set = true)) return @positive if not_set @positive = value end # Set value for matching negative choice # # @api public def negative(value = (not_set = true)) return @negative if not_set @negative = value end def call(message, &block) return if Utils.blank?(message) @message = message block.call(self) if block setup_defaults render end # Render confirmation question # # @return [String] # # @api private def render_question header = "#{@prefix}#{message} " if !@done header += @prompt.decorate("(#{@suffix})", @help_color) + ' ' else answer = convert_result(@input) label = answer ? @positive : @negative header += @prompt.decorate(label, @active_color) end header << "\n" if @done header end protected # Decide how to handle input from user # # @api private def process_input(question) @input = read_input(question) if Utils.blank?(@input) @input = default ? positive : negative end @evaluator.call(@input) end # @api private def setup_defaults @convert = conversion return if suffix? && positive? if suffix? && (!positive? || !negative?) parts = @suffix.split('/') @positive = parts[0] @negative = parts[1] elsif !suffix? && positive? @suffix = create_suffix else create_default_labels end end # @api private def create_default_labels @suffix = default ? 'Y/n' : 'y/N' @positive = default ? 'Yes' : 'yes' @negative = default ? 'no' : 'No' @validation = /^(y(es)?|no?)$/i @messages[:valid?] = "Invalid input." end # @api private def create_suffix (default ? positive.capitalize : positive.downcase) + '/' + (default ? negative.downcase : negative.capitalize) end # Create custom conversion # # @api private def conversion proc { |input| positive_word = Regexp.escape(positive) positive_letter = Regexp.escape(positive[0]) pattern = Regexp.new("^#{positive_word}|#{positive_letter}$", true) !input.match(pattern).nil? } end end # ConfirmQuestion end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/converter_dsl.rb000066400000000000000000000007301363125043200220350ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'converter_registry' module TTY class Prompt module ConverterDSL def converter_registry @converter_registry ||= ConverterRegistry.new end def converter(name, &block) @converter_registry = converter_registry.register(name, &block) self end def convert(name, data) @converter_registry[name, data] end end # ConverterDSL end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/converter_registry.rb000066400000000000000000000026261363125043200231310ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt # Immutable collection of converters for type transformation # # @api private class ConverterRegistry # Create a registry of conversions # # @param [Hash] registry # # @api private def initialize(registry = {}) @_registry = registry.dup.freeze freeze end # Register converter # # @param [Symbol] name # the converter name # # @api public def register(name, contents = nil, &block) item = block_given? ? block : contents if key?(name) raise ArgumentError, "Converter for #{name.inspect} already registered" end self.class.new(@_registry.merge(name => item)) end # Check if converter is registered # # @return [Boolean] # # @api public def key?(key) @_registry.key?(key) end # Execute converter # # @api public def call(name, input) if name.respond_to?(:call) converter = name else converter = @_registry.fetch(name) do raise ArgumentError, "#{name.inspect} is not registered" end end converter[input] end alias [] call def inspect @_registry.inspect end end # ConverterRegistry end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/converters.rb000066400000000000000000000033401363125043200213560ustar00rootroot00000000000000# frozen_string_literal: true require 'pathname' require 'necromancer' require_relative 'converter_dsl' module TTY class Prompt module Converters extend ConverterDSL # Delegate Necromancer errors # # @api private def self.on_error if block_given? yield else raise ArgumentError, 'You need to provide a block argument.' end rescue Necromancer::ConversionTypeError => e raise ConversionError, e.message end converter(:bool) do |input| on_error { Necromancer.convert(input).to(:boolean, strict: true) } end converter(:string) do |input| String(input).chomp end converter(:symbol) do |input| input.to_sym end converter(:date) do |input| on_error { Necromancer.convert(input).to(:date, strict: true) } end converter(:datetime) do |input| on_error { Necromancer.convert(input).to(:datetime, strict: true) } end converter(:int) do |input| on_error { Necromancer.convert(input).to(:integer, strict: true) } end converter(:float) do |input| on_error { Necromancer.convert(input).to(:float, strict: true) } end converter(:range) do |input| on_error { Necromancer.convert(input).to(:range, strict: true) } end converter(:regexp) do |input| Regexp.new(input) end converter(:file) do |input| ::File.open(::File.join(Dir.pwd, input)) end converter(:path) do |input| Pathname.new(::File.join(Dir.pwd, input)) end converter(:char) do |input| String(input).chars.to_a[0] end end # Converters end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/distance.rb000066400000000000000000000031371363125043200207620ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt # A class responsible for string comparison class Distance # Calculate the optimal string alignment distance # # @api public def distance(first, second) distances = [] rows = first.to_s.length cols = second.to_s.length 0.upto(rows) do |index| distances << [index] + [0] * cols end distances[0] = 0.upto(cols).to_a 1.upto(rows) do |first_index| 1.upto(cols) do |second_index| first_char = first[first_index - 1] second_char = second[second_index - 1] cost = first_char == second_char ? 0 : 1 distances[first_index][second_index] = [ distances[first_index - 1][second_index], # deletion distances[first_index][second_index - 1], # insertion distances[first_index - 1][second_index - 1] # substitution ].min + cost if first_index > 1 && second_index > 1 first_previous_char = first[first_index - 2] second_previous_char = second[second_index - 2] if first_char == second_previous_char && second_char == first_previous_char distances[first_index][second_index] = [ distances[first_index][second_index], distances[first_index - 2][second_index - 2] + 1 # transposition ].min end end end end distances[rows][cols] end end # Distance end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/enum_list.rb000066400000000000000000000240761363125043200211740ustar00rootroot00000000000000# frozen_string_literal: true require 'English' require_relative 'choices' require_relative 'block_paginator' require_relative 'paginator' module TTY class Prompt # A class reponsible for rendering enumerated list menu. # Used by {Prompt} to display static choice menu. # # @api private class EnumList PAGE_HELP = '(Press tab/right or left to reveal more choices)' # Create instance of EnumList menu. # # @api public def initialize(prompt, **options) @prompt = prompt @prefix = options.fetch(:prefix) { @prompt.prefix } @enum = options.fetch(:enum) { ')' } @default = options.fetch(:default) { -1 } @active_color = options.fetch(:active_color) { @prompt.active_color } @help_color = options.fetch(:help_color) { @prompt.help_color } @error_color = options.fetch(:error_color) { @prompt.error_color } @cycle = options.fetch(:cycle) { false } @symbols = @prompt.symbols.merge(options.fetch(:symbols, {})) @input = nil @done = false @first_render = true @failure = false @active = @default @choices = Choices.new @per_page = options[:per_page] @page_help = options[:page_help] || PAGE_HELP @paginator = BlockPaginator.new @page_active = @default end # Change symbols used by this prompt # # @param [Hash] new_symbols # the new symbols to use # # @api public def symbols(new_symbols = (not_set = true)) return @symbols if not_set @symbols.merge!(new_symbols) end # Set default option selected # # @api public def default(default) @default = default end # Check if default value is set # # @return [Boolean] # # @api public def default? @default > 0 end # Set number of items per page # # @api public def per_page(value) @per_page = value end def page_size (@per_page || Paginator::DEFAULT_PAGE_SIZE) end # Check if list is paginated # # @return [Boolean] # # @api private def paginated? @choices.size > page_size end # @param [String] text # the help text to display per page # @api pbulic def page_help(text) @page_help = text end # Set selecting active index using number pad # # @api public def enum(value) @enum = value end # Add a single choice # # @api public def choice(*value, &block) if block @choices << (value << block) else @choices << value end end # Add multiple choices # # @param [Array[Object]] values # the values to add as choices # # @api public def choices(values = (not_set = true)) if not_set @choices else values.each { |val| @choices << val } end end # Call the list menu by passing question and choices # # @param [String] question # # @param # @api public def call(question, possibilities, &block) choices(possibilities) @question = question block[self] if block setup_defaults @prompt.subscribe(self) do render end end def keypress(event) if %i[backspace delete].include?(event.key.name) return if @input.empty? @input.chop! mark_choice_as_active elsif event.value =~ /^\d+$/ @input += event.value mark_choice_as_active end end def keyreturn(*) @failure = false num = @input.to_i choice_disabled = choices[num - 1] && choices[num - 1].disabled? choice_in_range = num > 0 && num <= @choices.size if choice_in_range && !choice_disabled || @input.empty? @done = true else @input = '' @failure = true end end alias keyenter keyreturn def keyright(*) if (@page_active + page_size) <= @choices.size @page_active += page_size elsif @cycle @page_active = 1 end end alias keytab keyright def keyleft(*) if (@page_active - page_size) >= 0 @page_active -= page_size elsif @cycle @page_active = @choices.size - 1 end end private # Find active choice or set to default # # @return [nil] # # @api private def mark_choice_as_active next_active = @choices[@input.to_i - 1] if next_active && next_active.disabled? # noop elsif (@input.to_i > 0) && next_active @active = @input.to_i else @active = @default end @page_active = @active end # Validate default indexes to be within range # # @api private def validate_defaults msg = if @default.nil? || @default.to_s.empty? "default index must be an integer in range (1 - #{choices.size})" elsif @default < 1 || @default > @choices.size "default index #{@default} out of range (1 - #{@choices.size})" elsif choices[@default - 1] && choices[@default - 1].disabled? "default index #{@default} matches disabled choice item" end raise(ConfigurationError, msg) if msg end # Setup default option and active selection # # @api private def setup_defaults if !default? @default = (0..choices.length).find {|i| !choices[i].disabled? } + 1 end validate_defaults mark_choice_as_active end # Render a selection list. # # By default the result is printed out. # # @return [Object] value # return the selected value # # @api private def render @input = '' until @done question = render_question @prompt.print(question) @prompt.print(render_error) if @failure if paginated? && !@done @prompt.print(render_page_help) end @prompt.read_keypress question_lines = question.split($INPUT_RECORD_SEPARATOR, -1) @prompt.print(refresh(question_lines_count(question_lines))) end @prompt.print(render_question) answer end # Count how many screen lines the question spans # # @return [Integer] # # @api private def question_lines_count(question_lines) question_lines.reduce(0) do |acc, line| acc + @prompt.count_screen_lines(line) end end # Find value for the choice selected # # @return [nil, Object] # # @api private def answer @choices[@active - 1].value end # Determine area of the screen to clear # # @param [Integer] lines # the lines to clear # # @return [String] # # @api private def refresh(lines) @prompt.clear_lines(lines) + @prompt.cursor.clear_screen_down end # Render question with the menu options # # @return [String] # # @api private def render_question header = ["#{@prefix}#{@question} #{render_header}\n"] unless @done header << render_menu header << render_footer end header.join end # Error message when incorrect index chosen # # @api private def error_message error = 'Please enter a valid number' "\n" + @prompt.decorate('>>', @error_color) + ' ' + error end # Render error message and return cursor to position of input # # @return [String] # # @api private def render_error error = error_message.dup if !paginated? error << @prompt.cursor.prev_line error << @prompt.cursor.forward(render_footer.size) end error end # Render chosen option # # @return [String] # # @api private def render_header return '' unless @done return '' unless @active selected_item = @choices[@active - 1].name.to_s @prompt.decorate(selected_item, @active_color) end # Render footer for the indexed menu # # @return [String] # # @api private def render_footer " Choose 1-#{@choices.size} [#{@default}]: #{@input}" end # Pagination help message # # @return [String] # # @api private def page_help_message return '' unless paginated? "\n" + @prompt.decorate(@page_help, @help_color) end # Render page help # # @return [String] # # @api private def render_page_help help = page_help_message.dup if @failure help << @prompt.cursor.prev_line end help << @prompt.cursor.prev_line help << @prompt.cursor.forward(render_footer.size) end # Render menu with indexed choices to select from # # @return [String] # # @api private def render_menu output = [] @paginator.paginate(@choices, @page_active, @per_page) do |choice, index| num = (index + 1).to_s + @enum + ' ' selected = num.to_s + choice.name.to_s output << if index + 1 == @active && !choice.disabled? (' ' * 2) + @prompt.decorate(selected, @active_color) elsif choice.disabled? @prompt.decorate(@symbols[:cross], :red) + ' ' + selected + ' ' + choice.disabled.to_s else (' ' * 2) + selected end output << "\n" end output.join end end # EnumList end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/evaluator.rb000066400000000000000000000012021363125043200211610ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'result' module TTY class Prompt # Evaluates provided parameters and stops if any of them fails # @api private class Evaluator attr_reader :results def initialize(question, &block) @question = question @results = [] instance_eval(&block) if block end def call(initial) seed = Result::Success.new(@question, initial) results.reduce(seed, &:with) end def check(proc = nil, &block) results << (proc || block) end alias_method :<<, :check end # Evaluator end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/expander.rb000066400000000000000000000166321363125043200210020ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'choices' module TTY class Prompt # A class responsible for rendering expanding options # Used by {Prompt} to display key options question. # # @api private class Expander HELP_CHOICE = { key: 'h', name: 'print help', value: :help }.freeze # Create instance of Expander # # @api public def initialize(prompt, options = {}) @prompt = prompt @prefix = options.fetch(:prefix) { @prompt.prefix } @default = options.fetch(:default) { 1 } @auto_hint = options.fetch(:auto_hint) { false } @active_color = options.fetch(:active_color) { @prompt.active_color } @help_color = options.fetch(:help_color) { @prompt.help_color } @choices = Choices.new @selected = nil @done = false @status = :collapsed @hint = nil @default_key = false end def expanded? @status == :expanded end def collapsed? @status == :collapsed end def expand @status = :expanded end # Respond to submit event # # @api public def keyenter(_) if @input.nil? || @input.empty? @input = @choices[@default - 1].key @default_key = true end selected = select_choice(@input) if selected && selected.key.to_s == 'h' expand @selected = nil @input = '' elsif selected @done = true @selected = selected @hint = nil else @input = '' end end alias keyreturn keyenter # Respond to key press event # # @api public def keypress(event) if [:backspace, :delete].include?(event.key.name) @input.chop! unless @input.empty? elsif event.value =~ /^[^\e\n\r]/ @input += event.value end @selected = select_choice(@input) if @selected && !@default_key && collapsed? @hint = @selected.name end end # Select choice by given key # # @return [Choice] # # @api private def select_choice(key) @choices.find_by(:key, key) end # Set default value. # # @api public def default(value = (not_set = true)) return @default if not_set @default = value end # Add a single choice # # @api public def choice(value, &block) if block @choices << value.update(value: block) else @choices << value end end # Add multiple choices # # @param [Array[Object]] values # the values to add as choices # # @api public def choices(values) values.each { |val| choice(val) } end # Execute this prompt # # @api public def call(message, possibilities, &block) choices(possibilities) @message = message block.call(self) if block setup_defaults choice(HELP_CHOICE) @prompt.subscribe(self) do render end end private # Create possible keys with current choice highlighted # # @return [String] # # @api private def possible_keys keys = @choices.pluck(:key) default_key = keys[@default - 1] if @selected index = keys.index(@selected.key) keys[index] = @prompt.decorate(keys[index], @active_color) elsif @input.to_s.empty? && default_key keys[@default - 1] = @prompt.decorate(default_key, @active_color) end keys.join(',') end # @api private def render @input = '' until @done question = render_question @prompt.print(question) read_input @prompt.print(refresh(question.lines.count)) end @prompt.print(render_question) answer end # @api private def answer @selected.value end # Render message with options # # @return [String] # # @api private def render_header header = ["#{@prefix}#{@message} "] if @done selected_item = @selected.name.to_s header << @prompt.decorate(selected_item, @active_color) elsif collapsed? header << %[(enter "h" for help) ] header << "[#{possible_keys}] " header << @input end header.join end # Show hint for selected option key # # return [String] # # @api private def render_hint "\n" + @prompt.decorate('>> ', @active_color) + @hint + @prompt.cursor.prev_line + @prompt.cursor.forward(@prompt.strip(render_header).size) end # Render question with menu # # @return [String] # # @api private def render_question load_auto_hint if @auto_hint header = render_header header << render_hint if @hint header << "\n" if @done if !@done && expanded? header << render_menu header << render_footer end header end def load_auto_hint if @hint.nil? && collapsed? if @selected @hint = @selected.name else if @input.empty? @hint = @choices[@default - 1].name else @hint = "invalid option" end end end end def render_footer " Choice [#{@choices[@default - 1].key}]: #{@input}" end def read_input @prompt.read_keypress end # Refresh the current input # # @param [Integer] lines # # @return [String] # # @api private def refresh(lines) if (@hint && (!@selected || @done)) || (@auto_hint && collapsed?) @hint = nil @prompt.clear_lines(lines, :down) + @prompt.cursor.prev_line elsif expanded? @prompt.clear_lines(lines) else @prompt.clear_line end end # Render help menu # # @api private def render_menu output = ["\n"] @choices.each do |choice| chosen = %(#{choice.key} - #{choice.name}) if @selected && @selected.key == choice.key chosen = @prompt.decorate(chosen, @active_color) end output << ' ' + chosen + "\n" end output.join end def setup_defaults validate_choices end def validate_choices errors = [] keys = [] @choices.each do |choice| if choice.key.nil? errors << "Choice #{choice.name} is missing a :key attribute" next end if choice.key.length != 1 errors << "Choice key `#{choice.key}` is more than one character long." end if choice.key.to_s == 'h' errors << "Choice key `#{choice.key}` is reserved for help menu." end if keys.include?(choice.key) errors << "Choice key `#{choice.key}` is a duplicate." end keys << choice.key if choice.key end errors.each { |err| raise ConfigurationError, err } end end # Expander end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/keypress.rb000066400000000000000000000045601363125043200210360ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'question' require_relative 'timer' module TTY class Prompt class Keypress < Question # Create keypress question # # @param [Prompt] prompt # @param [Hash] options # # @api public def initialize(prompt, **options) super @echo = options.fetch(:echo) { false } @keys = options.fetch(:keys) { UndefinedSetting } @timeout = options.fetch(:timeout) { UndefinedSetting } @interval = options.fetch(:interval) { (@timeout != UndefinedSetting && @timeout < 1) ? @timeout : 1 } @decimals = (@interval.to_s.split('.')[1] || []).size @countdown = @timeout time = timeout? ? Float(@timeout) : nil @timer = Timer.new(time, Float(@interval)) @prompt.subscribe(self) end def countdown(value = (not_set = true)) return @countdown if not_set @countdown = value end # Check if any specific keys are set def any_key? @keys == UndefinedSetting end # Check if timeout is set def timeout? @timeout != UndefinedSetting end def keypress(event) if any_key? @done = true elsif @keys.is_a?(Array) && @keys.include?(event.key.name) @done = true else @done = false end end def render_question header = super if timeout? header.gsub!(/:countdown/, format("%.#{@decimals}f", countdown)) end header end def interval_handler(time) return if @done question = render_question line_size = question.size total_lines = @prompt.count_screen_lines(line_size) @prompt.print(refresh(question.lines.count, total_lines)) countdown(time) @prompt.print(render_question) end def process_input(question) @prompt.print(render_question) @timer.on_tick do |time| interval_handler(time) end @timer.while_remaining do |remaining| break if @done @input = @prompt.read_keypress(nonblock: true) end countdown(0) unless @done @evaluator.(@input) end def refresh(lines, lines_to_clear) @prompt.clear_lines(lines) end end # Keypress end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/list.rb000066400000000000000000000341201363125043200201370ustar00rootroot00000000000000# frozen_string_literal: true require 'English' require_relative 'choices' require_relative 'paginator' require_relative 'block_paginator' module TTY class Prompt # A class responsible for rendering select list menu # Used by {Prompt} to display interactive menu. # # @api private class List HELP = '(Use %s arrow%s keys, press Enter to select%s)' # Allowed keys for filter, along with backspace and canc. FILTER_KEYS_MATCHER = /\A([[:alnum:]]|[[:punct:]])\Z/.freeze # Create instance of TTY::Prompt::List menu. # # @param Hash options # the configuration options # @option options [Symbol] :default # the default active choice, defaults to 1 # @option options [Symbol] :color # the color for the selected item, defualts to :green # @option options [Symbol] :marker # the marker for the selected item # @option options [String] :enum # the delimiter for the item index # # @api public def initialize(prompt, **options) check_options_consistency(options) @prompt = prompt @prefix = options.fetch(:prefix) { @prompt.prefix } @enum = options.fetch(:enum) { nil } @default = Array(options[:default]) @choices = Choices.new @active_color = options.fetch(:active_color) { @prompt.active_color } @help_color = options.fetch(:help_color) { @prompt.help_color } @cycle = options.fetch(:cycle) { false } @filterable = options.fetch(:filter) { false } @symbols = @prompt.symbols.merge(options.fetch(:symbols, {})) @filter = [] @filter_cache = {} @help = options[:help] @first_render = true @done = false @per_page = options[:per_page] @paginator = Paginator.new @block_paginator = BlockPaginator.new @by_page = false @paging_changed = false end # Change symbols used by this prompt # # @param [Hash] new_symbols # the new symbols to use # # @api public def symbols(new_symbols = (not_set = true)) return @symbols if not_set @symbols.merge!(new_symbols) end # Set default option selected # # @api public def default(*default_values) @default = default_values end # Select paginator based on the current navigation key # # @return [Paginator] # # @api private def paginator @by_page ? @block_paginator : @paginator end # Synchronize paginators start positions # # @api private def sync_paginators if @by_page if @paginator.start_index @block_paginator.reset! @block_paginator.start_index = @paginator.start_index end else if @block_paginator.start_index @paginator.reset! @paginator.start_index = @block_paginator.start_index end end end # Set number of items per page # # @api public def per_page(value) @per_page = value end def page_size (@per_page || Paginator::DEFAULT_PAGE_SIZE) end # Check if list is paginated # # @return [Boolean] # # @api private def paginated? choices.size > page_size end # Provide help information # # @param [String] value # the new help text # # @return [String] # # @api public def help(value = (not_set = true)) return @help if !@help.nil? && not_set @help = (@help.nil? && !not_set) ? value : default_help end # Information about arrow keys # # @return [String] # # @api private def arrows_help up_down = @symbols[:arrow_up] + "/" + @symbols[:arrow_down] left_right = @symbols[:arrow_left] + "/" + @symbols[:arrow_right] arrows = [up_down] arrows << " and " if paginated? arrows << left_right if paginated? arrows.join end # Default help text # # @api public def default_help # Note that enumeration and filter are mutually exclusive tokens = if enumerate? [" or number (1-#{choices.size})", ''] elsif filterable? ['', ', and letter keys to filter'] else ['', ''] end format(self.class::HELP, arrows_help, *tokens) end # Set selecting active index using number pad # # @api public def enum(value) @enum = value end # Add a single choice # # @api public def choice(*value, &block) @filter_cache = {} if block @choices << (value << block) else @choices << value end end # Add multiple choices, or return them. # # @param [Array[Object]] values # the values to add as choices; if not passed, the current # choices are displayed. # # @api public def choices(values = (not_set = true)) if not_set if !filterable? || @filter.empty? @choices else filter_value = @filter.join.downcase @filter_cache[filter_value] ||= @choices.select do |choice| !choice.disabled? && choice.name.downcase.include?(filter_value) end end else @filter_cache = {} values.each { |val| @choices << val } end end # Call the list menu by passing question and choices # # @param [String] question # # @param # @api public def call(question, possibilities, &block) choices(possibilities) @question = question block.call(self) if block setup_defaults @prompt.subscribe(self) do render end end # Check if list is enumerated # # @return [Boolean] def enumerate? !@enum.nil? end def keynum(event) return unless enumerate? value = event.value.to_i return unless (1..choices.count).cover?(value) return if choices[value - 1].disabled? @active = value end def keyenter(*) @done = true unless choices.empty? end alias keyreturn keyenter alias keyspace keyenter def search_choice_in(searchable) searchable.find { |i| !choices[i - 1].disabled? } end def keyup(*) searchable = (@active - 1).downto(1).to_a prev_active = search_choice_in(searchable) if prev_active @active = prev_active elsif @cycle searchable = choices.length.downto(1).to_a prev_active = search_choice_in(searchable) @active = prev_active if prev_active end @paging_changed = @by_page @by_page = false end def keydown(*) searchable = ((@active + 1)..choices.length) next_active = search_choice_in(searchable) if next_active @active = next_active elsif @cycle searchable = (1..choices.length) next_active = search_choice_in(searchable) @active = next_active if next_active end @paging_changed = @by_page @by_page = false end alias keytab keydown # Moves all choices page by page keeping the current selected item # at the same level on each page. # # When the choice on a page is outside of next page range then # adjust it to the last item, otherwise leave unchanged. def keyright(*) if (@active + page_size) <= @choices.size searchable = ((@active + page_size)..choices.length) @active = search_choice_in(searchable) elsif @active <= @choices.size # last page shorter current = @active % page_size remaining = @choices.size % page_size if current.zero? || (remaining > 0 && current > remaining) searchable = @choices.size.downto(0).to_a @active = search_choice_in(searchable) elsif @cycle searchable = ((current.zero? ? page_size : current)..choices.length) @active = search_choice_in(searchable) end end @paging_changed = !@by_page @by_page = true end alias keypage_down keyright def keyleft(*) if (@active - page_size) > 0 searchable = ((@active - page_size)..choices.length) @active = search_choice_in(searchable) elsif @cycle searchable = @choices.size.downto(1).to_a @active = search_choice_in(searchable) end @paging_changed = !@by_page @by_page = true end alias keypage_up keyleft def keypress(event) return unless filterable? if event.value =~ FILTER_KEYS_MATCHER @filter << event.value @active = 1 end end def keydelete(*) return unless filterable? @filter.clear @active = 1 end def keybackspace(*) return unless filterable? @filter.pop @active = 1 end private def check_options_consistency(options) if options.key?(:enum) && options.key?(:filter) raise ConfigurationError, "Enumeration can't be used with filter" end end # Setup default option and active selection # # @api private def setup_defaults validate_defaults if !@default.empty? @active = @default.first else @active = @choices.index { |choice| !choice.disabled? } + 1 end end # Validate default indexes to be within range # # @raise [ConfigurationError] # raised when the default index is either non-integer, # out of range or clashes with disabled choice item. # # @api private def validate_defaults @default.each do |d| msg = if d.nil? || d.to_s.empty? "default index must be an integer in range (1 - #{choices.size})" elsif d < 1 || d > choices.size "default index `#{d}` out of range (1 - #{choices.size})" elsif choices[d - 1] && choices[d - 1].disabled? "default index `#{d}` matches disabled choice item" end raise(ConfigurationError, msg) if msg end end # Render a selection list. # # By default the result is printed out. # # @return [Object] value # return the selected value # # @api private def render @prompt.print(@prompt.hide) until @done question = render_question @prompt.print(question) @prompt.read_keypress # Split manually; if the second line is blank (when there are no # matching lines), it won't be included by using String#lines. question_lines = question.split($INPUT_RECORD_SEPARATOR, -1) @prompt.print(refresh(question_lines_count(question_lines))) end @prompt.print(render_question) answer ensure @prompt.print(@prompt.show) end # Count how many screen lines the question spans # # @return [Integer] # # @api private def question_lines_count(question_lines) question_lines.reduce(0) do |acc, line| acc + @prompt.count_screen_lines(line) end end # Find value for the choice selected # # @return [nil, Object] # # @api private def answer choices[@active - 1].value end # Clear screen lines # # @param [String] # # @api private def refresh(lines) @prompt.clear_lines(lines) end # Render question with instructions and menu # # @return [String] # # @api private def render_question header = ["#{@prefix}#{@question} #{render_header}\n"] @first_render = false unless @done header << render_menu end header.join end # Is filtering enabled? # # @return [Boolean] # # @api private def filterable? @filterable end # Header part showing the current filter # # @return String # # @api private def filter_help "(Filter: #{@filter.join.inspect})" end # Render initial help and selected choice # # @return [String] # # @api private def render_header if @done selected_item = choices[@active - 1].name @prompt.decorate(selected_item.to_s, @active_color) elsif @first_render @prompt.decorate(help, @help_color) elsif filterable? && @filter.any? @prompt.decorate(filter_help, @help_color) end end # Render menu with choices to select from # # @return [String] # # @api private def render_menu output = [] sync_paginators if @paging_changed paginator.paginate(choices, @active, @per_page) do |choice, index| num = enumerate? ? (index + 1).to_s + @enum + ' ' : '' message = if index + 1 == @active && !choice.disabled? selected = "#{@symbols[:marker]} #{num}#{choice.name}" @prompt.decorate(selected.to_s, @active_color) elsif choice.disabled? @prompt.decorate(@symbols[:cross], :red) + " #{num}#{choice.name} #{choice.disabled}" else " #{num}#{choice.name}" end end_index = paginated? ? paginator.end_index : choices.size - 1 newline = (index == end_index) ? '' : "\n" output << (message + newline) end output.join end end # List end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/mask_question.rb000066400000000000000000000043321363125043200220500ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'question' module TTY class Prompt class MaskQuestion < Question # Create masked question # # @param [Hash] options # @option options [String] :mask # # @api public def initialize(prompt, **options) super @mask = options.fetch(:mask) { @prompt.symbols[:dot] } @done_masked = false @failure = false end # Set character for masking the STDIN input # # @param [String] char # # @return [self] # # @api public def mask(char = (not_set = true)) return @mask if not_set @mask = char end def keyreturn(event) @done_masked = true end def keyenter(event) @done_masked = true end def keypress(event) if [:backspace, :delete].include?(event.key.name) @input.chop! unless @input.empty? elsif event.value =~ /^[^\e\n\r]/ @input += event.value end end # Render question and input replaced with masked character # # @api private def render_question header = ["#{@prefix}#{message} "] if echo? masked = @mask.to_s * @input.to_s.length if @done_masked && !@failure masked = @prompt.decorate(masked, @active_color) elsif @done_masked && @failure masked = @prompt.decorate(masked, @error_color) end header << masked end header << "\n" if @done header.join end def render_error(errors) @failure = !errors.empty? super end # Read input from user masked by character # # @private def read_input(question) @done_masked = false @failure = false @input = '' @prompt.print(question) until @done_masked @prompt.read_keypress question = render_question total_lines = @prompt.count_screen_lines(question) @prompt.print(@prompt.clear_lines(total_lines)) @prompt.print(render_question) end @prompt.puts @input end end # MaskQuestion end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/multi_list.rb000066400000000000000000000110351363125043200213510ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'list' module TTY class Prompt # A class responsible for rendering multi select list menu. # Used by {Prompt} to display interactive choice menu. # # @api private class MultiList < List HELP = '(Use %s arrow%s keys, press Space to select and Enter to finish%s)' # Create instance of TTY::Prompt::MultiList menu. # # @param [Prompt] :prompt # @param [Hash] options # # @api public def initialize(prompt, **options) super @selected = [] @help = options[:help] @echo = options.fetch(:echo, true) @min = options[:min] @max = options[:max] end # Set a minimum number of choices # # @api public def min(value) @min = value end # Set a maximum number of choices # # @api public def max(value) @max = value end # Callback fired when enter/return key is pressed # # @api private def keyenter(*) if @min super if @selected.size >= @min else super end end alias keyreturn keyenter # Callback fired when space key is pressed # # @api private def keyspace(*) active_choice = choices[@active - 1] if @selected.include?(active_choice) @selected.delete(active_choice) else return if @max && @selected.size >= @max @selected << active_choice end end private # Setup default options and active selection # # @api private def setup_defaults validate_defaults # At this stage, @choices matches all the visible choices. @selected = @choices.values_at(*@default.map { |d| d - 1 }) if !@default.empty? @active = @default.last else @active = @choices.index { |choice| !choice.disabled? } + 1 end end # Generate selected items names # # @return [String] # # @api private def selected_names @selected.map(&:name).join(', ') end # Header part showing the minimum/maximum number of choices # # @return [String] # # @api private def minmax_help help = [] help << "min. #{@min}" if @min help << "max. #{@max}" if @max "(%s) " % [ help.join(' ') ] end # Render initial help text and then currently selected choices # # @api private def render_header instructions = @prompt.decorate(help, @help_color) minmax_suffix = @min || @max ? minmax_help : "" if @done && @echo @prompt.decorate(selected_names, @active_color) elsif @selected.size.nonzero? && @echo help_suffix = filterable? && @filter.any? ? " #{filter_help}" : "" minmax_suffix + selected_names + (@first_render ? " #{instructions}" : help_suffix) elsif @first_render minmax_suffix + instructions elsif filterable? && @filter.any? minmax_suffix + filter_help elsif @min || @max minmax_help end end # All values for the choices selected # # @return [Array[nil,Object]] # # @api private def answer @selected.map(&:value) end # Render menu with choices to select from # # @return [String] # # @api private def render_menu output = [] sync_paginators if @paging_changed paginator.paginate(choices, @active, @per_page) do |choice, index| num = enumerate? ? (index + 1).to_s + @enum + ' ' : '' indicator = (index + 1 == @active) ? @symbols[:marker] : ' ' indicator += ' ' message = if @selected.include?(choice) && !choice.disabled? selected = @prompt.decorate(@symbols[:radio_on], @active_color) "#{selected} #{num}#{choice.name}" elsif choice.disabled? @prompt.decorate(@symbols[:cross], :red) + " #{num}#{choice.name} #{choice.disabled}" else "#{@symbols[:radio_off]} #{num}#{choice.name}" end end_index = paginated? ? paginator.end_index : choices.size - 1 newline = (index == end_index) ? '' : "\n" output << indicator + message + newline end output.join end end # MultiList end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/multiline.rb000066400000000000000000000031751363125043200211740ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'question' require_relative 'symbols' module TTY class Prompt # A prompt responsible for multi line user input # # @api private class Multiline < Question HELP = '(Press CTRL-D or CTRL-Z to finish)'.freeze def initialize(prompt, **options) super @help = options[:help] || self.class::HELP @first_render = true @lines_count = 0 end # Provide help information # # @return [String] # # @api public def help(value = (not_set = true)) return @help if not_set @help = value end def read_input @prompt.read_multiline end def keyreturn(*) @lines_count += 1 end alias keyenter keyreturn def render_question header = ["#{@prefix}#{message} "] if !echo? header elsif @done header << @prompt.decorate("#{@input}", @active_color) elsif @first_render header << @prompt.decorate(help, @help_color) @first_render = false end header << "\n" header.join end def process_input(question) @prompt.print(question) @lines = read_input @input = "#{@lines.first.strip} ..." unless @lines.first.to_s.empty? if Utils.blank?(@input) @input = default? ? default : nil end @evaluator.(@lines) end def refresh(lines, lines_to_clear) size = @lines_count + lines_to_clear + 1 @prompt.clear_lines(size) end end # Multiline end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/paginator.rb000066400000000000000000000063321363125043200211540ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt class Paginator DEFAULT_PAGE_SIZE = 6 # The 0-based index of the first item on this page attr_accessor :start_index # The 0-based index of the last item on this page attr_reader :end_index # The 0-based index of the active item on this page attr_reader :current_index # The 0-based index of the previously active item on this page attr_reader :last_index # Create a Paginator # # @api private def initialize(**options) @last_index = Array(options[:default]).flatten.first || 0 @per_page = options[:per_page] @start_index = Array(options[:default]).flatten.first end # Reset current page indexes # # @api private def reset! @start_index = nil @end_index = nil end # Check if page size is valid # # @raise [InvalidArgument] # # @api private def check_page_size! raise InvalidArgument, 'per_page must be > 0' if @per_page < 1 end # Paginate collection given an active index # # @param [Array[Choice]] list # a collection of choice items # @param [Integer] active # current choice active index # @param [Integer] per_page # number of choice items per page # # @return [Enumerable] # the list between start and end index # # @api public def paginate(list, active, per_page = nil, &block) current_index = active - 1 default_size = (list.size <= DEFAULT_PAGE_SIZE ? list.size : DEFAULT_PAGE_SIZE) @per_page = @per_page || per_page || default_size check_page_size! @start_index ||= (current_index / @per_page) * @per_page @end_index ||= @start_index + @per_page - 1 # Don't paginate short lists if list.size <= @per_page @start_index = 0 @end_index = list.size - 1 if block return list.each_with_index(&block) else return list.each_with_index.to_enum end end step = (current_index - @last_index).abs if current_index > @last_index # going up if current_index >= @end_index && current_index < list.size - 1 last_page = list.size - @per_page @start_index = [@start_index + step, last_page].min end elsif current_index < @last_index # going down if current_index <= @start_index && current_index > 0 @start_index = [@start_index - step, 0].max end end # Cycle list if current_index.zero? @start_index = 0 elsif current_index == list.size - 1 @start_index = list.size - 1 - (@per_page - 1) end @end_index = @start_index + (@per_page - 1) @last_index = current_index sliced_list = list[@start_index..@end_index] page_range = (@start_index..@end_index) return sliced_list.zip(page_range).to_enum unless block_given? sliced_list.each_with_index do |item, index| block[item, @start_index + index] end end end # Paginator end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/question.rb000066400000000000000000000215051363125043200210360ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'converters' require_relative 'evaluator' require_relative 'question/modifier' require_relative 'question/validation' require_relative 'question/checks' require_relative 'utils' module TTY # A class responsible for shell prompt interactions. class Prompt # A class responsible for gathering user input # # @api public class Question include Checks UndefinedSetting = Class.new do def to_s "undefined" end alias_method :inspect, :to_s end # Store question message # @api public attr_reader :message attr_reader :modifier attr_reader :validation # Initialize a Question # # @api public def initialize(prompt, **options) @prompt = prompt @prefix = options.fetch(:prefix) { @prompt.prefix } @default = options.fetch(:default) { UndefinedSetting } @required = options.fetch(:required) { false } @echo = options.fetch(:echo) { true } @in = options.fetch(:in) { UndefinedSetting } @modifier = options.fetch(:modifier) { [] } @validation = options.fetch(:validation) { UndefinedSetting } @convert = options.fetch(:convert) { UndefinedSetting } @active_color = options.fetch(:active_color) { @prompt.active_color } @help_color = options.fetch(:help_color) { @prompt.help_color } @error_color = options.fetch(:error_color) { :red } @value = options.fetch(:value) { UndefinedSetting } @messages = Utils.deep_copy(options.fetch(:messages) { { } }) @done = false @first_render = true @input = nil @evaluator = Evaluator.new(self) @evaluator << CheckRequired @evaluator << CheckDefault @evaluator << CheckRange @evaluator << CheckValidation @evaluator << CheckModifier end # Stores all the error messages displayed to user # The currently supported messages are: # * :range? # * :required? # * :valid? attr_reader :messages # Retrieve message based on the key # # @param [Symbol] name # the name of message key # # @param [Hash] tokens # the tokens to evaluate # # @return [Array[String]] # # @api private def message_for(name, tokens = nil) template = @messages[name] if template && !template.match(/\%\{/).nil? [template % tokens] else [template || ''] end end # Call the question # # @param [String] message # # @return [self] # # @api public def call(message = '', &block) @message = message block.call(self) if block @prompt.subscribe(self) do render end end # Read answer and convert to type # # @api private def render @errors = [] until @done result = process_input(render_question) if result.failure? @errors = result.errors @prompt.print(render_error(result.errors)) else @done = true end question = render_question input_line = question + result.value.to_s total_lines = @prompt.count_screen_lines(input_line) @prompt.print(refresh(question.lines.count, total_lines)) end @prompt.print(render_question) convert_result(result.value) end # Render question # # @return [String] # # @api private def render_question header = [] if !Utils.blank?(@prefix) || !Utils.blank?(message) header << "#{@prefix}#{message} " end if !echo? header elsif @done header << @prompt.decorate(@input.to_s, @active_color) elsif default? && !Utils.blank?(@default) header << @prompt.decorate("(#{default})", @help_color) + ' ' end header << "\n" if @done header.join end # Decide how to handle input from user # # @api private def process_input(question) @input = read_input(question) if Utils.blank?(@input) @input = default? ? default : nil end @evaluator.(@input) end # Process input # # @api private def read_input(question) options = {echo: echo} if value? && @first_render options[:value] = @value @first_render = false end @prompt.read_line(question, **options).chomp end # Handle error condition # # @return [String] # # @api private def render_error(errors) errors.reduce([]) do |acc, err| acc << @prompt.decorate('>>', :red) + ' ' + err acc end.join("\n") end # Determine area of the screen to clear # # @param [Integer] lines # number of lines to clear # # @return [String] # # @api private def refresh(lines, lines_to_clear) output = [] if @done if @errors.count.zero? output << @prompt.cursor.up(lines) else lines += @errors.count lines_to_clear += @errors.count end else output << @prompt.cursor.up(lines) end output.join + @prompt.clear_lines(lines_to_clear) end # Convert value to expected type # # @param [Object] value # # @api private def convert_result(value) if convert? & !Utils.blank?(value) Converters.convert(@convert, value) else value end end # Specify answer conversion # # @api public def convert(value) @convert = value end # Check if conversion is set # # @return [Boolean] # # @api public def convert? @convert != UndefinedSetting end # Set default value. # # @api public def default(value = (not_set = true)) return @default if not_set @default = value end # Check if default value is set # # @return [Boolean] # # @api public def default? @default != UndefinedSetting end # Ensure that passed argument is present or not # # @return [Boolean] # # @api public def required(value = (not_set = true), message = nil) messages[:required?] = message if message return @required if not_set @required = value end alias_method :required?, :required # Set validation rule for an argument # # @param [Object] value # # @return [Question] # # @api public def validate(value = nil, message = nil, &block) messages[:valid?] = message if message @validation = (value || block) end # Prepopulate input with custom content # # @api public def value(val) return @value if val.nil? @value = val end # Check if custom value is present # # @api private def value? @value != UndefinedSetting end def validation? @validation != UndefinedSetting end # Modify string according to the rule given. # # @param [Symbol] rule # # @api public def modify(*rules) @modifier = rules end # Turn terminal echo on or off. This is used to secure the display so # that the entered characters are not echoed back to the screen. # # @api public def echo(value = nil) return @echo if value.nil? @echo = value end alias_method :echo?, :echo # Turn raw mode on or off. This enables character-based input. # # @api public def raw(value = nil) return @raw if value.nil? @raw = value end alias_method :raw?, :raw # Set expected range of values # # @param [String] value # # @api public def in(value = (not_set = true), message = nil) messages[:range?] = message if message if in? && !@in.is_a?(Range) @in = Converters.convert(:range, @in) end return @in if not_set @in = Converters.convert(:range, value) end # Check if range is set # # @return [Boolean] # # @api public def in? @in != UndefinedSetting end # @api public def to_s message.to_s end # String representation of this question # @api public def inspect "#<#{self.class.name} @message=#{message}, @input=#{@input}>" end end # Question end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/question/000077500000000000000000000000001363125043200205065ustar00rootroot00000000000000tty-prompt-0.21.0/lib/tty/prompt/question/checks.rb000066400000000000000000000045121363125043200222750ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt class Question module Checks # Check if modifications are applicable class CheckModifier def self.call(question, value) if !question.modifier.nil? || question.modifier [Modifier.new(question.modifier).apply_to(value)] else [value] end end end # Check if value is within range class CheckRange def self.float?(value) !/[-+]?(\d*[.])?\d+/.match(value.to_s).nil? end def self.int?(value) !/^[-+]?\d+$/.match(value.to_s).nil? end def self.cast(value) if float?(value) value.to_f elsif int?(value) value.to_i else value end end def self.call(question, value) if !question.in? || (question.in? && question.in.include?(cast(value))) [value] else tokens = {value: value, in: question.in} [value, question.message_for(:range?, tokens)] end end end # Check if input requires validation class CheckValidation def self.call(question, value) if !question.validation? || (question.required? && value.nil?) || (question.validation? && Validation.new(question.validation).call(value)) [value] else tokens = {valid: question.validation.inspect} [value, question.message_for(:valid?, tokens)] end end end # Check if default value provided class CheckDefault def self.call(question, value) if value.nil? && question.default? [question.default] else [value] end end end # Check if input is required class CheckRequired def self.call(question, value) if question.required? && !question.default? && value.nil? [value, question.message_for(:required?)] else [value] end end end end # Checks end # Question end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/question/modifier.rb000066400000000000000000000055071363125043200226400ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt class Question # A class representing String modifications. class Modifier attr_reader :modifiers # Initialize a Modifier # # @api public def initialize(modifiers) @modifiers = modifiers end # Change supplied value according to the given string transformation. # Valid settings are: # # @param [String] value # the string to be modified # # @return [String] # # @api private def apply_to(value) modifiers.reduce(value) do |result, mod| result = Modifier.letter_case(mod, result) Modifier.whitespace(mod, result) end end # Changes letter casing in a string according to valid modifications. # For invalid modification option the string is preserved. # # @param [Symbol] mod # the modification to change the string # # @option mod [Symbol] :up change to upper case # @option mod [Symbol] :upcase change to upper case # @option mod [Symbol] :uppercase change to upper case # @option mod [Symbol] :down change to lower case # @option mod [Symbol] :downcase change to lower case # @option mod [Symbol] :capitalize change all words to start # with uppercase case letter # # @return [String] # # @api public def self.letter_case(mod, value) return value unless value.is_a?(String) case mod when :up, :upcase, :uppercase value.upcase when :down, :downcase, :lowercase value.downcase when :capitalize value.capitalize else value end end # Changes whitespace in a string according to valid modifications. # # @param [Symbol] mod # the modification to change the string # # @option mod [String] :trim, :strip # remove whitespace for the start and end # @option mod [String] :chomp remove record separator from the end # @option mod [String] :collapse remove any duplicate whitespace # @option mod [String] :remove remove all whitespace # # @api public def self.whitespace(mod, value) return value unless value.is_a?(String) case mod when :trim, :strip value.strip when :chomp value.chomp when :collapse value.gsub(/\s+/, ' ') when :remove value.gsub(/\s+/, '') else value end end end # Modifier end # Question end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/question/validation.rb000066400000000000000000000035261363125043200231730ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt class Question # A class representing question validation. class Validation # Available validator names VALIDATORS = { email: /^[a-z0-9._%+-]+@([a-z0-9-]+\.)+[a-z]{2,6}$/i }.freeze attr_reader :pattern # Initialize a Validation # # @param [Object] pattern # # @return [undefined] # # @api private def initialize(pattern) @pattern = coerce(pattern) end # Convert validation into known type. # # @param [Object] pattern # # @raise [TTY::ValidationCoercion] # raised when failed to convert validation # # @api private def coerce(pattern) case pattern when String, Symbol, Proc pattern when Regexp Regexp.new(pattern.to_s) else raise ValidationCoercion, "Wrong type, got #{pattern.class}" end end # Test if the input passes the validation # # @example # Validation.new(/pattern/) # validation.call(input) # => true # # @param [Object] input # the input to validate # # @return [Boolean] # # @api public def call(input) if pattern.is_a?(String) || pattern.is_a?(Symbol) VALIDATORS.key?(pattern.to_sym) !VALIDATORS[pattern.to_sym].match(input.to_s).nil? elsif pattern.is_a?(Regexp) !pattern.match(input.to_s).nil? elsif pattern.is_a?(Proc) result = pattern.call(input.to_s) result.nil? ? false : result else false end end end # Validation end # Question end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/result.rb000066400000000000000000000016021363125043200205010ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt # Accumulates errors class Result attr_reader :question, :value, :errors def initialize(question, value, errors = []) @question = question @value = value @errors = errors end def with(condition = nil, &block) validator = (condition || block) (new_value, validation_error) = validator.call(question, value) accumulated_errors = errors + Array(validation_error) if accumulated_errors.empty? Success.new(question, new_value) else Failure.new(question, new_value, accumulated_errors) end end def success? is_a?(Success) end def failure? is_a?(Failure) end class Success < Result end class Failure < Result end end end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/slider.rb000066400000000000000000000113261363125043200204510ustar00rootroot00000000000000# frozen_string_literal: true module TTY # A class responsible for shell prompt interactions. class Prompt # A class responsible for gathering numeric input from range # # @api public class Slider HELP = '(Use arrow keys, press Enter to select)'.freeze FORMAT = ':slider %d'.freeze # Initailize a Slider # # @param [Prompt] prompt # the prompt # @param [Hash] options # the options to configure this slider # @option options [Integer] :min The minimum value # @option options [Integer] :max The maximum value # @option options [Integer] :step The step value # @option options [String] :format The display format # # @api public def initialize(prompt, **options) @prompt = prompt @prefix = options.fetch(:prefix) { @prompt.prefix } @min = options.fetch(:min) { 0 } @max = options.fetch(:max) { 10 } @step = options.fetch(:step) { 1 } @default = options[:default] @active_color = options.fetch(:active_color) { @prompt.active_color } @help_color = options.fetch(:help_color) { @prompt.help_color } @format = options.fetch(:format) { FORMAT } @symbols = @prompt.symbols.merge(options.fetch(:symbols, {})) @first_render = true @done = false end # Change symbols used by this prompt # # @param [Hash] new_symbols # the new symbols to use # # @api public def symbols(new_symbols = (not_set = true)) return @symbols if not_set @symbols.merge!(new_symbols) end # Setup initial active position # # @return [Integer] # # @api private def initial if @default.nil? range.size / 2 else range.index(@default) end end # Range of numbers to render # # @return [Array[Integer]] # # @apip private def range (@min..@max).step(@step).to_a end # @api public def default(value) @default = value end # @api public def min(value) @min = value end # @api public def max(value) @max = value end # @api public def step(value) @step = value end def format(value) @format = value end # Call the slider by passing question # # @param [String] question # the question to ask # # @apu public def call(question, &block) @question = question block.call(self) if block @active = initial @prompt.subscribe(self) do render end end def keyleft(*) @active -= 1 if @active > 0 end alias keydown keyleft def keyright(*) @active += 1 if (@active + 1) < range.size end alias keyup keyright def keyreturn(*) @done = true end alias keyspace keyreturn alias keyenter keyreturn private # Render an interactive range slider. # # @api private def render @prompt.print(@prompt.hide) until @done question = render_question @prompt.print(question) @prompt.read_keypress refresh(question.lines.count) end @prompt.print(render_question) answer ensure @prompt.print(@prompt.show) end # Clear screen # # @param [Integer] lines # the lines to clear # # @api private def refresh(lines) @prompt.print(@prompt.clear_lines(lines)) end # @return [Integer] # # @api private def answer range[@active] end # Render question with the slider # # @return [String] # # @api private def render_question header = ["#{@prefix}#{@question} "] if @done header << @prompt.decorate(answer.to_s, @active_color) header << "\n" else header << render_slider end if @first_render header << "\n" + @prompt.decorate(HELP, @help_color) @first_render = false end header.join end # Render slider representation # # @return [String] # # @api private def render_slider slider = (@symbols[:line] * @active) + @prompt.decorate(@symbols[:bullet], @active_color) + (@symbols[:line] * (range.size - @active - 1)) value = " #{range[@active]}" @format.gsub(':slider', slider) % [value] end end # Slider end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/statement.rb000066400000000000000000000025171363125043200211750ustar00rootroot00000000000000# frozen_string_literal: true module TTY # A class responsible for shell prompt interactions. class Prompt # A class representing a statement output to prompt. class Statement # Flag to display newline # # @api public attr_reader :newline # Color used to display statement # # @api public attr_reader :color # Initialize a Statement # # @param [TTY::Prompt] prompt # # @param [Hash] options # # @option options [Symbol] :newline # force a newline break after the message # # @option options [Symbol] :color # change the message display to color # # @api public def initialize(prompt, options = {}) @prompt = prompt @newline = options.fetch(:newline) { true } @color = options.fetch(:color) { false } end # Output the message to the prompt # # @param [String] message # the message to be printed to stdout # # @api public def call(message) message = @prompt.decorate(message, *color) if color if newline && /( |\t)(\e\[\d+(;\d+)*m)?\Z/ !~ message @prompt.puts message else @prompt.print message @prompt.flush end end end # Statement end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/suggestion.rb000066400000000000000000000053051363125043200213560ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'distance' module TTY # A class responsible for terminal prompt interactions. class Prompt # A class representing a suggestion out of possible choices # # @api public class Suggestion DEFAULT_INDENT = 8 SINGLE_TEXT = 'Did you mean this?' PLURAL_TEXT = 'Did you mean one of these?' # Number of spaces # # @api public attr_reader :indent # Text for a single suggestion # # @api public attr_reader :single_text # Text for multiple suggestions # # @api public attr_reader :plural_text # Initialize a Suggestion # # @api public def initialize(**options) @indent = options.fetch(:indent) { DEFAULT_INDENT } @single_text = options.fetch(:single_text) { SINGLE_TEXT } @plural_text = options.fetch(:plural_text) { PLURAL_TEXT } @suggestions = [] @comparator = Distance.new end # Suggest matches out of possibile strings # # @param [String] message # # @param [Array[String]] possibilities # # @api public def suggest(message, possibilities) distances = measure_distances(message, possibilities) minimum_distance = distances.keys.min max_distance = distances.keys.max if minimum_distance < max_distance @suggestions = distances[minimum_distance].sort end evaluate end private # Measure distances between messag and possibilities # # @param [String] message # # @param [Array[String]] possibilities # # @return [Hash] # # @api private def measure_distances(message, possibilities) distances = Hash.new { |hash, key| hash[key] = [] } possibilities.each do |possibility| distances[@comparator.distance(message, possibility)] << possibility end distances end # Build up a suggestion string # # @param [Array[String]] suggestions # # @return [String] # # @api private def evaluate return @suggestions if @suggestions.empty? if @suggestions.one? build_single_suggestion else build_multiple_suggestions end end # @api private def build_single_suggestion single_text + "\n" + (' ' * indent) + @suggestions.first end # @api private def build_multiple_suggestions plural_text + "\n" + @suggestions.map do |sugest| ' ' * indent + sugest end.join("\n") end end # Suggestion end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/symbols.rb000066400000000000000000000036671363125043200206700ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt # Cross platform common Unicode symbols. # # @api public module Symbols KEYS = { tick: '✓', cross: '✘', star: '★', square: '◼', square_empty: '◻', dot: '•', bullet: '●', bullet_empty: '○', marker: '‣', line: '─', pipe: '|', ellipsis: '…', radio_on: '⬢', radio_off: '⬡', checkbox_on: '☒', checkbox_off: '☐', circle: '◯', circle_on: 'ⓧ', circle_off: 'Ⓘ', arrow_up: '↑', arrow_down: '↓', arrow_up_down: '↕', arrow_left: '←', arrow_right: '→', arrow_left_right: '↔', heart: '♥', diamond: '♦', club: '♣', spade: '♠' }.freeze WIN_KEYS = { tick: '√', cross: 'x', star: '*', square: '[█]', square_empty: '[ ]', dot: '.', bullet: 'O', bullet_empty: '○', marker: '>', line: '-', pipe: '|', ellipsis: '...', radio_on: '(*)', radio_off: '( )', checkbox_on: '[×]', checkbox_off: '[ ]', circle: '( )', circle_on: '(x)', circle_off: '( )', arrow_up: '↑', arrow_down: '↓', arrow_up_down: '↕', arrow_left: '←', arrow_right: '→', arrow_left_right: '↔', heart: '♥', diamond: '♦', club: '♣', spade: '♠' }.freeze def symbols @symbols ||= windows? ? WIN_KEYS : KEYS end module_function :symbols # Check if Windowz # # @return [Boolean] # # @api public def windows? ::File::ALT_SEPARATOR == "\\" end module_function :windows? end # Symbols end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/timer.rb000066400000000000000000000026201363125043200203040ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt class Timer attr_reader :duration attr_reader :total attr_reader :interval def initialize(duration, interval) @duration = duration @interval = interval @total = 0.0 @current = nil @events = [] end def start return if @current @current = time_now end def stop return unless @current @current = nil end def runtime time_now - @current end def on_tick(&block) @events << block end def while_remaining start remaining = duration if @duration while remaining >= 0.0 if runtime >= total tick = duration - @total @events.each { |block| block.(tick) } @total += @interval end yield(remaining) remaining = duration - runtime end else loop { yield } end ensure stop end if defined?(Process::CLOCK_MONOTONIC) # Object representing current time def time_now ::Process.clock_gettime(Process::CLOCK_MONOTONIC) end else # Object represeting current time def time_now ::Time.now end end end # Timer end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/prompt/utils.rb000066400000000000000000000016001363125043200203210ustar00rootroot00000000000000# frozen_string_literal: true module TTY module Utils module_function BLANK_REGEX = /\A[[:space:]]*\z/o.freeze # Extract options hash from array argument # # @param [Array[Object]] args # # @api public def extract_options(args) options = args.last options.respond_to?(:to_hash) ? options.to_hash.dup : {} end def extract_options!(args) args.last.respond_to?(:to_hash) ? args.pop : {} end # Check if value is nil or an empty string # # @param [Object] value # the value to check # # @return [Boolean] # # @api public def blank?(value) value.nil? || value.respond_to?(:empty?) && value.empty? || BLANK_REGEX === value end # Deep copy object # # @api public def deep_copy(object) Marshal.load(Marshal.dump(object)) end end # Utils end # TTY tty-prompt-0.21.0/lib/tty/prompt/version.rb000066400000000000000000000001511363125043200206460ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt VERSION = "0.21.0" end # Prompt end # TTY tty-prompt-0.21.0/lib/tty/test_prompt.rb000066400000000000000000000007141363125043200202250ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'prompt' module TTY # Used for initializing test cases class TestPrompt < Prompt def initialize(options = {}) @input = StringIO.new @output = StringIO.new options.merge!({ input: @input, output: @output, env: { "TTY_TEST" => true }, enable_color: options.fetch(:enable_color) { true } }) super(options) end end # TestPrompt end # TTY tty-prompt-0.21.0/spec/000077500000000000000000000000001363125043200146625ustar00rootroot00000000000000tty-prompt-0.21.0/spec/spec_helper.rb000066400000000000000000000025101363125043200174760ustar00rootroot00000000000000# frozen_string_literal: true if ENV['COVERAGE'] || ENV['TRAVIS'] require 'simplecov' require 'coveralls' SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter ]) SimpleCov.start do command_name 'spec' add_filter 'spec' end end require 'tty-prompt' require 'stringio' class StringIO def wait_readable(*) true end end module Helpers def diff_output(actual_output, expected_output) puts "ACTUAL: #{actual_output.inspect}" puts "--------------------------------\n" puts "EXPECT: #{expected_output.inspect}" end end RSpec.configure do |config| config.include(Helpers) config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true end config.mock_with :rspec do |mocks| mocks.verify_partial_doubles = true end # Limits the available syntax to the non-monkey patched syntax that is recommended. config.disable_monkey_patching! # This setting enables warnings. It's recommended, but in some cases may # be too noisy due to issues in dependencies. config.warnings = true if config.files_to_run.one? config.default_formatter = 'doc' end config.profile_examples = 2 config.order = :random Kernel.srand config.seed end tty-prompt-0.21.0/spec/unit/000077500000000000000000000000001363125043200156415ustar00rootroot00000000000000tty-prompt-0.21.0/spec/unit/ask_spec.rb000066400000000000000000000113341363125043200177600ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, '#ask' do subject(:prompt) { TTY::TestPrompt.new } it 'asks question' do prompt.ask('What is your name?') expect(prompt.output.string).to eq([ "What is your name? ", "\e[1A\e[2K\e[1G", "What is your name? \n" ].join) end it 'asks an empty question ' do prompt = TTY::TestPrompt.new prompt.input << "\r" prompt.input.rewind answer = prompt.ask expect(answer).to eq(nil) expect(prompt.output.string).to eql("\e[2K\e[1G\n\e[1A\e[2K\e[1G\n") end it "asks an empty question and returns nil if EOF is sent to stdin" do prompt = TTY::TestPrompt.new prompt.input << nil prompt.input.rewind answer = prompt.ask('') expect(answer).to eql(nil) expect(prompt.output.string).to eq("\e[1A\e[2K\e[1G\n") end it "asks an empty question with prepopulated value" do prompt = TTY::TestPrompt.new prompt.input << "\n" prompt.input.rewind answer = prompt.ask value: "yes" expect(answer).to eq("yes") expect(prompt.output.string).to eq([ "yes\e[2K\e[1G", "yes\n\e[1A\e[2K\e[1G", "\e[32myes\e[0m\n" ].join) end it "asks question with prepopulated value" do prompt = TTY::TestPrompt.new prefix: "> " prompt.input << "\n" prompt.input.rewind answer = prompt.ask("Say?") do |q| q.value "yes" end expect(answer).to eq("yes") expect(prompt.output.string).to eq([ "> Say? yes\e[2K\e[1G", "> Say? yes\n\e[1A\e[2K\e[1G", "> Say? \e[32myes\e[0m\n" ].join) end it "asks a question with a prefix [?]" do prompt = TTY::TestPrompt.new(prefix: "[?] ") prompt.input << "\r" prompt.input.rewind answer = prompt.ask 'Are you Polish?' expect(answer).to eq(nil) expect(prompt.output.string).to eq([ "[?] Are you Polish? ", "\e[2K\e[1G[?] Are you Polish? \n", "\e[1A\e[2K\e[1G", "[?] Are you Polish? \n" ].join) end it 'asks a question with block' do prompt.input << '' prompt.input.rewind answer = prompt.ask "What is your name?" do |q| q.default 'Piotr' end expect(answer).to eq('Piotr') expect(prompt.output.string).to eq([ "What is your name? \e[90m(Piotr)\e[0m ", "\e[1A\e[2K\e[1G", "What is your name? \e[32mPiotr\e[0m\n" ].join) end it "changes question color" do prompt.input << '' prompt.input.rewind options = {default: 'Piotr', help_color: :red, active_color: :cyan} answer = prompt.ask("What is your name?", **options) expect(answer).to eq('Piotr') expect(prompt.output.string).to eq([ "What is your name? \e[31m(Piotr)\e[0m ", "\e[1A\e[2K\e[1G", "What is your name? \e[36mPiotr\e[0m\n" ].join) end it "permits empty default parameter" do prompt.input << "\r" prompt.input.rewind answer = prompt.ask("What is your name?", default: '') expect(answer).to eq('') expect(prompt.output.string).to eq([ "What is your name? ", "\e[2K\e[1GWhat is your name? \n", "\e[1A\e[2K\e[1G", "What is your name? \n" ].join) end it "permits nil default parameter" do prompt.input << "\r" prompt.input.rewind answer = prompt.ask("What is your name?", default: nil) expect(answer).to eq(nil) expect(prompt.output.string).to eq([ "What is your name? ", "\e[2K\e[1GWhat is your name? \n", "\e[1A\e[2K\e[1G", "What is your name? \n" ].join) end it "overwrites global settings" do global_settings = {prefix: "[?] ", active_color: :cyan, help_color: :red} prompt = TTY::TestPrompt.new(global_settings) prompt.input << "Piotr\r" prompt.input.rewind prompt.ask('What is your name?') prompt.input << "Piotr\r" prompt.input.rewind local_settings = {prefix: ':-) ', active_color: :blue, help_color: :magenta} prompt.ask('What is your name?', **local_settings) expect(prompt.output.string).to eq([ "[?] What is your name? ", "\e[2K\e[1G[?] What is your name? P", "\e[2K\e[1G[?] What is your name? Pi", "\e[2K\e[1G[?] What is your name? Pio", "\e[2K\e[1G[?] What is your name? Piot", "\e[2K\e[1G[?] What is your name? Piotr", "\e[2K\e[1G[?] What is your name? Piotr\n", "\e[1A\e[2K\e[1G", "[?] What is your name? \e[36mPiotr\e[0m\n", ":-) What is your name? ", "\e[2K\e[1G:-) What is your name? P", "\e[2K\e[1G:-) What is your name? Pi", "\e[2K\e[1G:-) What is your name? Pio", "\e[2K\e[1G:-) What is your name? Piot", "\e[2K\e[1G:-) What is your name? Piotr", "\e[2K\e[1G:-) What is your name? Piotr\n", "\e[1A\e[2K\e[1G", ":-) What is your name? \e[34mPiotr\e[0m\n" ].join) end end tty-prompt-0.21.0/spec/unit/block_paginator_spec.rb000066400000000000000000000057711363125043200223500ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::BlockPaginator, '#paginate' do it "ignores per_page when equal items " do list = %w(a b c d) paginator = described_class.new(per_page: 4) expect(paginator.paginate(list, 1).to_a).to eq([ ['a',0],['b',1],['c',2],['d',3]]) end it "ignores per_page when less items " do list = %w(a b c d) paginator = described_class.new(per_page: 5) expect(paginator.paginate(list, 1).to_a).to eq([ ['a',0],['b',1],['c',2],['d',3]]) end it "paginates items matching per_page count" do list = %w(a b c d e f) paginator = described_class.new(per_page: 3) expect(paginator.paginate(list, 1).to_a).to eq([['a',0], ['b',1], ['c',2]]) expect(paginator.paginate(list, 2).to_a).to eq([['a',0], ['b',1], ['c',2]]) expect(paginator.paginate(list, 3).to_a).to eq([['a',0], ['b',1], ['c',2]]) expect(paginator.paginate(list, 4).to_a).to eq([['d',3], ['e',4], ['f',5]]) expect(paginator.paginate(list, 5).to_a).to eq([['d',3], ['e',4], ['f',5]]) expect(paginator.paginate(list, 6).to_a).to eq([['d',3], ['e',4], ['f',5]]) expect(paginator.paginate(list, 7).to_a).to eq([['d',3], ['e',4], ['f',5]]) end it "paginates items not matching per_page count" do list = %w(a b c d e f g) paginator = described_class.new(per_page: 3) expect(paginator.paginate(list, 1).to_a).to eq([['a',0], ['b',1], ['c',2]]) expect(paginator.paginate(list, 2).to_a).to eq([['a',0], ['b',1], ['c',2]]) expect(paginator.paginate(list, 3).to_a).to eq([['a',0], ['b',1], ['c',2]]) expect(paginator.paginate(list, 4).to_a).to eq([['d',3], ['e',4], ['f',5]]) expect(paginator.paginate(list, 5).to_a).to eq([['d',3], ['e',4], ['f',5]]) expect(paginator.paginate(list, 6).to_a).to eq([['d',3], ['e',4], ['f',5]]) expect(paginator.paginate(list, 7).to_a).to eq([['g',6]]) expect(paginator.paginate(list, 8).to_a).to eq([['g',6]]) end it "finds both start and end index for current selection" do list = %w(a b c d e f g) paginator = described_class.new(per_page: 3, default: 0) paginator.paginate(list, 3) expect(paginator.start_index).to eq(0) expect(paginator.end_index).to eq(2) paginator.paginate(list, 4) expect(paginator.start_index).to eq(3) expect(paginator.end_index).to eq(5) paginator.paginate(list, 5) expect(paginator.start_index).to eq(3) expect(paginator.end_index).to eq(5) paginator.paginate(list, 7) expect(paginator.start_index).to eq(6) expect(paginator.end_index).to eq(6) end it "starts with default selection" do list = %w(a b c d e f g) paginator = described_class.new(per_page: 3, default: 3) expect(paginator.paginate(list, 4).to_a).to eq([['d',3], ['e',4], ['f',5]]) end it "doesn't accept invalid pagination" do list = %w(a b c d e f g) paginator = described_class.new(per_page: 0) expect { paginator.paginate(list, 4) }.to raise_error(TTY::Prompt::InvalidArgument, /per_page must be > 0/) end end tty-prompt-0.21.0/spec/unit/choice/000077500000000000000000000000001363125043200170735ustar00rootroot00000000000000tty-prompt-0.21.0/spec/unit/choice/eql_spec.rb000066400000000000000000000012001363125043200212040ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Choice, '#==' do it "is true with the same name and value attributes" do expect(described_class.new(:large, 1)). to eq(described_class.new(:large, 1)) end it "is false with different name attribute" do expect(described_class.new(:large, 1)). not_to eq(described_class.new(:medium, 1)) end it "is false with different value attribute" do expect(described_class.new(:large, 1)). not_to eq(described_class.new(:large, 2)) end it "is false with non-choice object" do expect(described_class.new(:large, 1)).not_to eq(:other) end end tty-prompt-0.21.0/spec/unit/choice/from_spec.rb000066400000000000000000000076451363125043200214110ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Choice, '#from' do it "skips Choice instance" do choice = described_class.new(:large, 1) expect(described_class.from(choice)).to eq(choice) end it "creates choice from string" do expected_choice = described_class.new('large', 'large') choice = described_class.from('large') expect(choice).to eq(expected_choice) expect(choice.name).to eq('large') expect(choice.value).to eq('large') end it "creates choice from array with one element" do expected_choice = described_class.new('large', 'large') choice = described_class.from([:large]) expect(choice).to eq(expected_choice) expect(choice.name).to eq('large') expect(choice.value).to eq('large') end it "creates choice from array with more than one element" do expected_choice = described_class.new('large', 1) choice = described_class.from([:large, 1]) expect(choice).to eq(expected_choice) expect(choice.name).to eq('large') expect(choice.value).to eq(1) end it "creates choice from array with false" do expected_choice = described_class.new('large', false) choice = described_class.from([:large, false]) expect(choice).to eq(expected_choice) expect(choice.name).to eq('large') expect(choice.value).to eq(false) end it "defaults value to name if value is nil" do expected_choice = described_class.new('large', 'large') choice = described_class.from([:large, nil]) expect(choice).to eq(expected_choice) expect(choice.name).to eq('large') expect(choice.value).to eq('large') end it "creates choice from hash value" do expected_choice = described_class.new('large', 1) choice = described_class.from({large: 1}) expect(choice).to eq(expected_choice) expect(choice.name).to eq('large') expect(choice.value).to eq(1) end it "creats choice from array with key value pair" do expected_choice = described_class.new('large', 1) choice = described_class.from([{'large' => 1}]) expect(choice).to eq(expected_choice) expect(choice.name).to eq('large') expect(choice.value).to eq(1) end it "creats choice from array with hash elements" do expected_choice = described_class.new('large', 1) choice = described_class.from([{name: 'large', value: 1}]) expect(choice).to eq(expected_choice) expect(choice.name).to eq('large') expect(choice.value).to eq(1) end it "creats choice from array with hash elements without value" do expected_choice = described_class.new('large', 'large') choice = described_class.from([{name: 'large'}]) expect(choice).to eq(expected_choice) expect(choice.name).to eq('large') expect(choice.value).to eq('large') end it "creates choice from hash with key property" do default = {key: 'h', name: 'Help', value: :help} expected_choice = described_class.new('Help', :help, key: 'h') choice = described_class.from(default) expect(choice).to eq(expected_choice) expect(choice.name).to eq('Help') expect(choice.value).to eq(:help) expect(choice.disabled?).to eq(false) end it "creates disabled choice" do expected_choice = described_class.new('Disabled', :none, disabled: true) choice = described_class.from({ name: 'Disabled', value: :none, disabled: 'unavailable'}) expect(choice).to eq(expected_choice) expect(choice.name).to eq('Disabled') expect(choice.value).to eq(:none) expect(choice.disabled?).to eq(true) end it "creates choice from an arbitrary object that responds to to_s call" do stub_const("Size", Class.new do def to_s 'large' end end) size = Size.new expected_choice = described_class.new(size, size) choice = described_class.from(size) expect(choice).to eq(expected_choice) expect(choice.name).to eq(size) expect(choice.value).to eq(size) expect(choice.disabled?).to eq(false) end end tty-prompt-0.21.0/spec/unit/choices/000077500000000000000000000000001363125043200172565ustar00rootroot00000000000000tty-prompt-0.21.0/spec/unit/choices/add_spec.rb000066400000000000000000000005301363125043200213430ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Choices, '#<<' do it "adds choice to collection" do choices = described_class.new expect(choices).to be_empty choice = TTY::Prompt::Choice.from([:label, 1]) choices << [:label, 1] expect(choices.size).to eq(1) expect(choices.to_ary).to eq([choice]) end end tty-prompt-0.21.0/spec/unit/choices/each_spec.rb000066400000000000000000000005531363125043200215200ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Choices, '.each' do it "iterates over collection" do choices = described_class[:large, :medium, :small] actual = [] choices.each do |choice| actual << choice.name end expect(actual).to eq([:large, :medium, :small]) expect(choices.each).to be_kind_of(Enumerator) end end tty-prompt-0.21.0/spec/unit/choices/find_by_spec.rb000066400000000000000000000005501363125043200222270ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Choices, '#find_by' do it "finds a matching choice by key name" do collection = [{name: 'large'},{name: 'medium'},{name: 'small'}] choice = TTY::Prompt::Choice.from(name: 'small') choices = described_class[*collection] expect(choices.find_by(:name, 'small')).to eq(choice) end end tty-prompt-0.21.0/spec/unit/choices/new_spec.rb000066400000000000000000000005151363125043200214070ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Choices, '.new' do it "creates choices collection" do choice_1 = TTY::Prompt::Choice.from(:label1) choice_2 = TTY::Prompt::Choice.from(:label2) collection = described_class[:label1, :label2] expect(collection.choices).to eq([choice_1, choice_2]) end end tty-prompt-0.21.0/spec/unit/choices/pluck_spec.rb000066400000000000000000000004621363125043200217350ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Choices, '#pluck' do it "plucks choice by key name" do collection = [{name: 'large'},{name: 'medium'},{name: 'small'}] choices = described_class[*collection] expect(choices.pluck(:name)).to eq(['large', 'medium', 'small']) end end tty-prompt-0.21.0/spec/unit/collect_spec.rb000066400000000000000000000044741363125043200206360ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, '#collect' do subject(:prompt) { TTY::TestPrompt.new } def collect(&block) prompt = subject count = 0 result = prompt.collect do while prompt.yes?("continue?") instance_eval(&block) count += 1 end end result[:count] = count result end context "when receiving multiple answers" do let(:colors) { %w(red blue yellow) } before do subject.input << "y\r" + colors.join("\ry\r") + "\rn\r" subject.input.rewind end it "collects as a list if values method used in chain" do result = collect { key(:colors).values.ask("color:") } expect(result[:count]).to eq(3) expect(result[:colors]).to eq(colors) end it "collects as a list if values method used in chain with block" do result = collect do key(:colors).values { key(:name).ask("color:") } end expect(result[:count]).to eq(3) expect(result[:colors]).to eq(colors.map { |c| { name: c } }) end context "with multiple keys" do let(:colors) { ["red\rblue", "yellow\rgreen"] } let(:expected_pairs) do colors.map { |s| Hash[%i(hot cold).zip(s.split("\r"))] } end it "collects into the appropriate keys" do result = collect do key(:pairs).values do key(:hot).ask("color:") key(:cold).ask("color:") end end expect(result[:count]).to eq(2) expect(result[:pairs]).to eq(expected_pairs) end end it "overrides a non-array key on multiple answers" do result = collect { key(:colors).ask("color:") } expect(result[:colors]).to eq(colors.last) expect(result[:count]).to eq(3) end end it "collects more than one answer" do prompt.input << "Piotr\r30\rStreet\rCity\r123\r" prompt.input.rewind result = prompt.collect do key(:name).ask('Name?') key(:age).ask('Age?', convert: :int) key(:address) do key(:street).ask('Street?', required: true) key(:city).ask('City?') key(:zip).ask('Zip?', validate: /\A\d{3}\Z/) end end expect(result).to include({ name: 'Piotr', age: 30, address: { street: 'Street', city: 'City', zip: '123' } }) end end tty-prompt-0.21.0/spec/unit/converters/000077500000000000000000000000001363125043200200335ustar00rootroot00000000000000tty-prompt-0.21.0/spec/unit/converters/convert_bool_spec.rb000066400000000000000000000030541363125043200240670ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, 'convert bool' do subject(:prompt) { TTY::TestPrompt.new} it 'fails to convert boolean' do prompt.input << 'invalid' prompt.input.rewind expect { prompt.ask("Do you read books?", convert: :bool) }.to raise_error(TTY::Prompt::ConversionError) end it "handles default values" do prompt.input << "\n" prompt.input.rewind response = prompt.ask('Do you read books?', convert: :bool, default: true) expect(response).to eql(true) expect(prompt.output.string).to eq([ "Do you read books? \e[90m(true)\e[0m ", "\e[2K\e[1GDo you read books? \e[90m(true)\e[0m \n", "\e[1A\e[2K\e[1G", "Do you read books? \e[32mtrue\e[0m\n" ].join) end it "handles default values" do prompt.input << "\n" prompt.input.rewind response = prompt.ask("Do you read books?") { |q| q.default true q.convert :bool } expect(response).to eq(true) end it 'converts negative boolean' do prompt.input << 'No' prompt.input.rewind response = prompt.ask('Do you read books?', convert: :bool) expect(response).to eq(false) end it 'converts positive boolean' do prompt.input << 'Yes' prompt.input.rewind response = prompt.ask("Do you read books?", convert: :bool) expect(response).to eq(true) end it 'converts single positive boolean' do prompt.input << 'y' prompt.input.rewind response = prompt.ask('Do you read books?', convert: :bool) expect(response).to eq(true) end end tty-prompt-0.21.0/spec/unit/converters/convert_char_spec.rb000066400000000000000000000005031363125043200240450ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, 'convert char' do it 'reads single character' do prompt = TTY::TestPrompt.new prompt.input << "abcde" prompt.input.rewind response = prompt.ask("What is your favourite letter?", convert: :char) expect(response).to eq('a') end end tty-prompt-0.21.0/spec/unit/converters/convert_custom_spec.rb000066400000000000000000000006651363125043200244530ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, 'convert custom' do subject(:prompt) { TTY::TestPrompt.new } it 'converts response with custom conversion' do prompt.input << "one,two,three\n" prompt.input.rewind conversion = proc { |input| input.split(/,\s*/) } answer = prompt.ask('Ingredients? (comma sep list)', convert: conversion) expect(answer).to eq(['one','two','three']) end end tty-prompt-0.21.0/spec/unit/converters/convert_date_spec.rb000066400000000000000000000017211363125043200240500ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, 'convert date' do subject(:prompt) { TTY::TestPrompt.new} it 'fails to convert date' do prompt.input << 'invalid' prompt.input.rewind expect { prompt.ask("When were you born?", convert: :date) }.to raise_error(TTY::Prompt::ConversionError) end it 'converts date' do prompt.input << "20th April 1887" prompt.input.rewind response = prompt.ask("When were your born?", convert: :date) expect(response).to be_kind_of(Date) expect(response.day).to eq(20) expect(response.month).to eq(4) expect(response.year).to eq(1887) end it "converts datetime" do prompt.input << "20th April 1887" prompt.input.rewind response = prompt.ask("When were your born?", convert: :datetime) expect(response).to be_kind_of(DateTime) expect(response.day).to eq(20) expect(response.month).to eq(4) expect(response.year).to eq(1887) end end tty-prompt-0.21.0/spec/unit/converters/convert_file_spec.rb000066400000000000000000000007331363125043200240540ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, 'convert file' do it "converts to file" do ::File.write('test.txt', 'foobar') prompt = TTY::TestPrompt.new prompt.input << "test.txt" prompt.input.rewind answer = prompt.ask("Which file to open?", convert: :file) expect(::File.basename(answer)).to eq('test.txt') expect(::File.read(answer)).to eq('foobar') ::File.unlink('test.txt') unless Gem.win_platform? end end tty-prompt-0.21.0/spec/unit/converters/convert_number_spec.rb000066400000000000000000000017501363125043200244250ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, 'convert numbers' do subject(:prompt) { TTY::TestPrompt.new } it 'fails to convert integer' do prompt.input << 'invalid' prompt.input.rewind expect { prompt.ask("What temparture?", convert: :int) }.to raise_error(TTY::Prompt::ConversionError) end it 'converts integer' do prompt.input << 35 prompt.input.rewind answer = prompt.ask("What temperature?", convert: :int) expect(answer).to be_a(Integer) expect(answer).to eq(35) end it 'fails to convert float' do prompt.input << 'invalid' prompt.input.rewind expect { prompt.ask("How tall are you?", convert: :float) }.to raise_error(TTY::Prompt::ConversionError) end it 'converts float' do number = 6.666 prompt.input << number prompt.input.rewind answer = prompt.ask('How tall are you?', convert: :float) expect(answer).to be_a(Float) expect(answer).to eq(number) end end tty-prompt-0.21.0/spec/unit/converters/convert_path_spec.rb000066400000000000000000000005701363125043200240700ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, 'convert path' do subject(:prompt) { TTY::TestPrompt.new } it "converts pathname" do path = Pathname.new(::File.join(Dir.pwd, 'spec/unit')) prompt.input << "spec/unit" prompt.input.rewind answer = prompt.ask('File location?', convert: :path) expect(answer).to eql(path) end end tty-prompt-0.21.0/spec/unit/converters/convert_range_spec.rb000066400000000000000000000010721363125043200242260ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, 'convert range' do subject(:prompt) { TTY::TestPrompt.new} it 'converts with valid range' do prompt.input << "20-30" prompt.input.rewind answer = prompt.ask("Which age group?", convert: :range) expect(answer).to be_a(Range) expect(answer).to eq(20..30) end it "fails to convert to range" do prompt.input << "abcd" prompt.input.rewind expect { prompt.ask('Which age group?', convert: :range) }.to raise_error(TTY::Prompt::ConversionError) end end tty-prompt-0.21.0/spec/unit/converters/convert_regex_spec.rb000066400000000000000000000005141363125043200242440ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, 'convert regexp' do it "converts regex" do prompt = TTY::TestPrompt.new prompt.input << "[a-z]*" prompt.input.rewind answer = prompt.ask("Regex?", convert: :regexp) expect(answer).to be_a(Regexp) expect(answer).to eq(/[a-z]*/) end end tty-prompt-0.21.0/spec/unit/converters/convert_string_spec.rb000066400000000000000000000011211363125043200244330ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, 'convert string' do it 'converts string' do prompt = TTY::TestPrompt.new prompt.input << 'Piotr' prompt.input.rewind answer = prompt.ask("What is your name?", convert: :string) expect(answer).to be_a(String) expect(answer).to eq('Piotr') end it "converts symbol" do prompt = TTY::TestPrompt.new prompt.input << 'Piotr' prompt.input.rewind answer = prompt.ask("What is your name?", convert: :symbol) expect(answer).to be_a(Symbol) expect(answer).to eq(:Piotr) end end tty-prompt-0.21.0/spec/unit/converters/on_error_spec.rb000066400000000000000000000003661363125043200232240ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Converters do it "enforces block argument" do expect { TTY::Prompt::Converters.on_error }.to raise_error(ArgumentError, 'You need to provide a block argument.') end end tty-prompt-0.21.0/spec/unit/distance/000077500000000000000000000000001363125043200174335ustar00rootroot00000000000000tty-prompt-0.21.0/spec/unit/distance/distance_spec.rb000066400000000000000000000026061363125043200225700ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Distance, '.distance' do let(:object) { described_class.new } subject(:distance) { object.distance(*strings) } context 'when nil' do let(:strings) { [nil, nil] } it { is_expected.to eql(0) } end context 'when empty' do let(:strings) { ['', ''] } it { is_expected.to eql(0) } end context 'with one non empty' do let(:strings) { ['abc', ''] } it { is_expected.to eql(3) } end context 'when single char' do let(:strings) { ['a', 'abc'] } it { is_expected.to eql(2) } end context 'when similar' do let(:strings) { ['abc', 'abc'] } it { is_expected.to eql(0) } end context 'when similar' do let(:strings) { ['abc', 'acb'] } it { is_expected.to eql(1) } end context 'when end similar' do let(:strings) { ['saturday', 'sunday'] } it { is_expected.to eql(3) } end context 'when contain similar' do let(:strings) { ['which', 'witch'] } it { is_expected.to eql(2) } end context 'when prefix' do let(:strings) { ['sta', 'status'] } it { is_expected.to eql(3) } end context 'when similar' do let(:strings) { ['smellyfish','jellyfish'] } it { is_expected.to eql(2) } end context 'when unicode' do let(:strings) { ['マラソン五輪代表', 'ララソン五輪代表'] } it { is_expected.to eql(1) } end end tty-prompt-0.21.0/spec/unit/enum_select_spec.rb000066400000000000000000000374061363125043200215150ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt do let(:symbols) { TTY::Prompt::Symbols.symbols } def output_helper(prompt, choices, active, options = {}) enum = options.fetch(:enum, ')') input = options[:input] error = options[:error] default = options.fetch(:default, 1) out = [] out << prompt << " \n" out << choices.map.with_index do |c, i| name = c.is_a?(Hash) ? c[:name] : c disabled = c.is_a?(Hash) ? c[:disabled] : false num = (i + 1).to_s + enum if disabled "\e[31m#{symbols[:cross]}\e[0m #{num} #{name} #{disabled}" elsif name == active " \e[32m#{num} #{name}\e[0m" else " #{num} #{name}" end end.join("\n") out << "\n" choice = " Choose 1-#{choices.count} [#{default}]: " choice = choice + input.to_s if input out << choice if error out << "\n" out << "\e[31m>>\e[0m #{error}" out << "\e[A\e[1G\e[#{choice.size}C" end out << "\e[2K\e[1G\e[1A" * (choices.count + 1) out << "\e[2K\e[1G\e[J" out.join end def exit_message(prompt, choice) "#{prompt} \e[32m#{choice}\e[0m\n" end it "raises configuration error when wrong default" do prompt = TTY::TestPrompt.new choices = %w(/bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny) expect { prompt.enum_select("Select an editor?", choices, default: 100) }.to raise_error(TTY::Prompt::ConfigurationError, /default index 100 out of range \(1 - 3\)/) end it "selects default option when return pressed immediately" do choices = %w(/bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny) prompt = TTY::TestPrompt.new prompt.input << "\n" prompt.input.rewind answer = prompt.enum_select("Select an editor?", choices) expect(answer).to eq('/bin/nano') expected_output = [ output_helper("Select an editor?", choices, "/bin/nano"), exit_message("Select an editor?", "/bin/nano") ].join expect(prompt.output.string).to eq(expected_output) end it "selects option by index from the list" do choices = %w(/bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny) prompt = TTY::TestPrompt.new prompt.input << "3\n" prompt.input.rewind answer = prompt.enum_select("Select an editor?", choices, default: 2) expect(answer).to eq('/usr/bin/vim.tiny') expected_output = [ output_helper("Select an editor?", choices, "/usr/bin/vim.basic", default: 2), output_helper("Select an editor?", choices, "/usr/bin/vim.tiny", default: 2, input: '3'), exit_message("Select an editor?", "/usr/bin/vim.tiny") ].join expect(prompt.output.string).to eq(expected_output) end it "selects option through DSL" do choices = %w(/bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny) prompt = TTY::TestPrompt.new prompt.input << "1\n" prompt.input.rewind answer = prompt.enum_select("Select an editor?") do |menu| menu.default 2 menu.enum '.' menu.choice "/bin/nano" menu.choice "/usr/bin/vim.basic" menu.choice "/usr/bin/vim.tiny" end expect(answer).to eq('/bin/nano') expected_output = [ output_helper("Select an editor?", choices, "/usr/bin/vim.basic", default: 2, enum: '.'), output_helper("Select an editor?", choices, "/bin/nano", default: 2, enum: '.', input: 1), exit_message("Select an editor?", "/bin/nano") ].join expect(prompt.output.string).to eq(expected_output) end it "selects option through DSL with key and value" do choices = %w(nano vim emacs) prompt = TTY::TestPrompt.new prompt.input << "\n" prompt.input.rewind answer = prompt.enum_select("Select an editor?") do |menu| menu.default 2 menu.choice :nano, '/bin/nano' menu.choice :vim, '/usr/bin/vim' menu.choice :emacs, '/usr/bin/emacs' end expect(answer).to eq('/usr/bin/vim') expected_output = [ output_helper("Select an editor?", choices, "vim", default: 2), exit_message("Select an editor?", "vim") ].join expect(prompt.output.string).to eq(expected_output) end it "changes colors for selection, hint and error" do prompt = TTY::TestPrompt.new choices = %w(/bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny) prompt.input << "\n" prompt.input.rewind options = {active_color: :red, help_color: :blue, error_color: :green} answer = prompt.enum_select("Select an editor?", choices, options) expect(answer).to eq('/bin/nano') expected_output = [ "Select an editor? \n", " \e[31m1) /bin/nano\e[0m\n", " 2) /usr/bin/vim.basic\n", " 3) /usr/bin/vim.tiny\n", " Choose 1-3 [1]: ", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "Select an editor? \e[31m/bin/nano\e[0m\n" ].join expect(prompt.output.string).to eq(expected_output) end it "changes global symbols" do prompt = TTY::TestPrompt.new(symbols: {cross: 'x'}) choices = ['A', {name: 'B', disabled: '(out)'}, 'C'] prompt.input << "\n" prompt.input.rewind answer = prompt.enum_select("What letter?", choices) expect(answer).to eq("A") expected_output = [ "What letter? \n", " \e[32m1) A\e[0m\n", "\e[31mx\e[0m 2) B (out)\n", " 3) C\n", " Choose 1-3 [1]: ", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \e[32mA\e[0m\n", ].join expect(prompt.output.string).to eq(expected_output) end it "changes global symbols through DSL" do prompt = TTY::TestPrompt.new choices = ['A', {name: 'B', disabled: '(out)'}, 'C'] prompt.input << "\n" prompt.input.rewind answer = prompt.enum_select("What letter?", choices) do |menu| menu.symbols cross: 'x' menu.choices choices end expect(answer).to eq("A") expected_output = [ "What letter? \n", " \e[32m1) A\e[0m\n", "\e[31mx\e[0m 2) B (out)\n", " 3) C\n", " Choose 1-3 [1]: ", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \e[32mA\e[0m\n", ].join expect(prompt.output.string).to eq(expected_output) end it "displays error with unrecognized input" do choices = %w(/bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny) prompt = TTY::TestPrompt.new prompt.input << "11\n2\n" prompt.input.rewind answer = prompt.enum_select("Select an editor?", choices) expect(answer).to eq('/usr/bin/vim.basic') expected_output = [ output_helper("Select an editor?", choices, "/bin/nano"), output_helper("Select an editor?", choices, "/bin/nano", input: '1'), output_helper("Select an editor?", choices, "/bin/nano", input: '11'), output_helper("Select an editor?", choices, "/bin/nano", error: 'Please enter a valid number', input: ''), output_helper("Select an editor?", choices, "/usr/bin/vim.basic", error: 'Please enter a valid number', input: '2'), exit_message("Select an editor?", "/usr/bin/vim.basic") ].join expect(prompt.output.string).to eq(expected_output) end it "paginates long selections" do choices = %w(A B C D E F G H) prompt = TTY::TestPrompt.new prompt.input << "\n" prompt.input.rewind answer = prompt.enum_select("What letter?", choices, per_page: 3, default: 4) expect(answer).to eq('D') expect(prompt.output.string).to eq([ "What letter? \n", " \e[32m4) D\e[0m\n", " 5) E\n", " 6) F\n", " Choose 1-8 [4]: ", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \e[32mD\e[0m\n" ].join) end it "doesn't paginate short selections" do choices = %i(A B C D) prompt = TTY::TestPrompt.new prompt.input << "\r" prompt.input.rewind answer = prompt.enum_select("What letter?", choices, per_page: 4, default: 1) expect(answer).to eq(:A) expected_output = output_helper("What letter?", choices, :A) + exit_message("What letter?", :A) expect(prompt.output.string).to eq(expected_output) end it "shows pages matching input" do prompt = TTY::TestPrompt.new choices = %w(A B C D E F G H) prompt.input << "11\n\b\n" prompt.input.rewind value = prompt.enum_select("What letter?", choices, per_page: 3) expect(value).to eq('A') expect(prompt.output.string).to eq([ "What letter? \n", " \e[32m1) A\e[0m\n", " 2) B\n", " 3) C\n", " Choose 1-8 [1]: ", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \n", " \e[32m1) A\e[0m\n", " 2) B\n", " 3) C\n", " Choose 1-8 [1]: 1", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[19C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \n", " \e[32m1) A\e[0m\n", " 2) B\n", " 3) C\n", " Choose 1-8 [1]: 11", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[20C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \n", " \e[32m1) A\e[0m\n", " 2) B\n", " 3) C\n", " Choose 1-8 [1]: \n", "\e[31m>>\e[0m Please enter a valid number", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \n", " \e[32m1) A\e[0m\n", " 2) B\n", " 3) C\n", " Choose 1-8 [1]: \n", "\e[31m>>\e[0m Please enter a valid number", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \e[32mA\e[0m\n" ].join) end it "switches through pages with tab key" do prompt = TTY::TestPrompt.new choices = %w(A B C D E F G H) prompt.input << "\t\n" prompt.input.rewind value = prompt.enum_select("What letter?") do |menu| menu.default 4 menu.per_page 3 menu.choices choices end expect(value).to eq('D') expect(prompt.output.string).to eq([ "What letter? \n", " \e[32m4) D\e[0m\n", " 5) E\n", " 6) F\n", " Choose 1-8 [4]: ", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \n", " 7) G\n", " 8) H\n", " Choose 1-8 [4]: ", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G\e[J", "What letter? \e[32mD\e[0m\n" ].join) end it "doesn't cycle around by default" do prompt = TTY::TestPrompt.new choices = %w(A B C D E F) prompt.input << "\t" << "\t" << "\n" prompt.input.rewind value = prompt.enum_select("What letter?") do |menu| menu.default 1 menu.per_page 3 menu.choices choices end expect(value).to eq("A") expect(prompt.output.string).to eq([ "What letter? \n", " \e[32m1) A\e[0m\n", " 2) B\n", " 3) C\n", " Choose 1-6 [1]: ", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \n", " 4) D\n", " 5) E\n", " 6) F\n", " Choose 1-6 [1]: ", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \n", " 4) D\n", " 5) E\n", " 6) F\n", " Choose 1-6 [1]: ", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \e[32mA\e[0m\n" ].join) end it "cycles around when configured to do so" do prompt = TTY::TestPrompt.new choices = %w(A B C D E F) prompt.input << "\t" << "\t" << "\n" prompt.input.rewind value = prompt.enum_select("What letter?", cycle: true) do |menu| menu.default 1 menu.per_page 3 menu.choices choices end expect(value).to eq("A") expect(prompt.output.string).to eq([ "What letter? \n", " \e[32m1) A\e[0m\n", " 2) B\n", " 3) C\n", " Choose 1-6 [1]: ", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \n", " 4) D\n", " 5) E\n", " 6) F\n", " Choose 1-6 [1]: ", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \n", " \e[32m1) A\e[0m\n", " 2) B\n", " 3) C\n", " Choose 1-6 [1]: ", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \e[32mA\e[0m\n" ].join) end context "with :disabled choice" do it "fails when active item is also disabled" do prompt = TTY::TestPrompt.new choices = [{name: 'A', disabled: true}, 'B', 'C', 'D', 'E'] expect { prompt.enum_select("What letter?", choices, default: 1) }.to raise_error(TTY::Prompt::ConfigurationError, /default index 1 matches disabled choice item/) end it "finds first non-disabled index" do prompt = TTY::TestPrompt.new choices = [{name: 'A', disabled: true}, {name:'B', disabled: true}, 'C', 'D'] prompt = TTY::TestPrompt.new prompt.input << "\n" prompt.input.rewind answer = prompt.enum_select("What letter?", choices) expect(answer).to eq('C') end it "doesn't allow to choose disabled choice and defaults" do choices = ['A', {name: 'B', disabled: '(out)'}, 'C', 'D', 'E', 'F'] prompt = TTY::TestPrompt.new prompt.input << "2" << "\n" << "3" << "\n" prompt.input.rewind answer = prompt.enum_select("What letter?", choices) expect(answer).to eq("C") expected_output = [ output_helper("What letter?", choices, 'A'), output_helper("What letter?", choices, 'A', input: '2'), output_helper("What letter?", choices, 'A', input: '', error: 'Please enter a valid number'), output_helper("What letter?", choices, 'C', input: '3', error: 'Please enter a valid number'), exit_message("What letter?", "C") ].join expect(prompt.output.string).to eq(expected_output) end it "omits disabled choice when navigating with numbers" do choices = [ {name: 'A'}, {name: 'B', disabled: '(out)'}, {name: 'C', disabled: '(out)'}, {name: 'D'}, {name: 'E'} ] prompt = TTY::TestPrompt.new prompt.on(:keypress) { |e| prompt.trigger(:keydelete) if e.value == "B"} prompt.input << "2" << "\u007F" << "3" << "\u007F" << '4' << "\n" prompt.input.rewind answer = prompt.enum_select("What letter?", choices) expect(answer).to eq("D") expected_output = [ output_helper("What letter?", choices, 'A'), output_helper("What letter?", choices, 'A', input: '2'), output_helper("What letter?", choices, 'A', input: ''), output_helper("What letter?", choices, 'A', input: '3'), output_helper("What letter?", choices, 'A', input: ''), output_helper("What letter?", choices, 'D', input: '4'), exit_message("What letter?", "D") ].join expect(prompt.output.string).to eq(expected_output) end end end tty-prompt-0.21.0/spec/unit/error_spec.rb000066400000000000000000000011611363125043200203300ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, '.error' do subject(:prompt) { TTY::TestPrompt.new } it 'displays one message' do prompt.error "Nothing is fine!" expect(prompt.output.string).to eql "\e[31mNothing is fine!\e[0m\n" end it 'displays many messages' do prompt.error "Nothing is fine!", "All is broken!" expect(prompt.output.string).to eql "\e[31mNothing is fine!\e[0m\n\e[31mAll is broken!\e[0m\n" end it 'displays message with option' do prompt.error "Nothing is fine!", newline: false expect(prompt.output.string).to eql "\e[31mNothing is fine!\e[0m" end end tty-prompt-0.21.0/spec/unit/evaluator_spec.rb000066400000000000000000000030341363125043200212020ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Evaluator do it "checks chained validation procs" do question = double(:question) evaluator = TTY::Prompt::Evaluator.new(question) evaluator.check { |quest, value| if value < 21 [value, ["#{value} is not bigger than 21"]] else value end } evaluator.check { |quest, value| if value < 42 [value, ["#{value} is not bigger than 42"]] else value end } answer = evaluator.call(2) expect(answer.errors.count).to eq(2) expect(answer.value).to eq(2) expect(answer.success?).to eq(false) expect(answer.failure?).to eq(true) end it "checks chained validation objects" do question = double(:question) evaluator = TTY::Prompt::Evaluator.new(question) LessThan21 = Class.new do def self.call(quest, value) if value < 21 [value, ["#{value} is not bigger than 21"]] else value end end end LessThan42 = Class.new do def self.call(quest, value) if value < 42 [value, ["#{value} is not bigger than 42"]] else value end end end evaluator.check(LessThan21) evaluator.check(LessThan42) answer = evaluator.call(2) expect(answer.errors).to match_array([ "2 is not bigger than 21", "2 is not bigger than 42" ]) expect(answer.value).to eq(2) expect(answer.success?).to eq(false) expect(answer.failure?).to eq(true) end end tty-prompt-0.21.0/spec/unit/expand_spec.rb000066400000000000000000000177571363125043200205000ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, '#expand' do subject(:prompt) { TTY::TestPrompt.new } let(:choices) { [{ key: 'y', name: 'Overwrite', value: :yes }, { key: 'n', name: 'Skip', value: :no }, { key: 'a', name: 'Overwrite all', value: :all }, { key: 'd', name: 'Show diff', value: :diff }, { key: 'q', name: 'Quit', value: :quit }] } it "expands default option" do prompt.input << "\n" prompt.input.rewind result = prompt.expand('Overwrite Gemfile?', choices) expect(result).to eq(:yes) expected_output = [ "Overwrite Gemfile? (enter \"h\" for help) [\e[32my\e[0m,n,a,d,q,h] ", "\e[2K\e[1G", "Overwrite Gemfile? \e[32mOverwrite\e[0m\n" ].join expect(prompt.output.string).to eq(expected_output) end it "changes default option" do prompt.input << "\n" prompt.input.rewind result = prompt.expand('Overwrite Gemfile?', choices, default: 3) expect(result).to eq(:all) expected_output = [ "Overwrite Gemfile? (enter \"h\" for help) [y,n,\e[32ma\e[0m,d,q,h] ", "\e[2K\e[1G", "Overwrite Gemfile? \e[32mOverwrite all\e[0m\n" ].join expect(prompt.output.string).to eq(expected_output) end it "expands chosen option with extra information" do prompt.input << "a\n" prompt.input.rewind result = prompt.expand('Overwrite Gemfile?', choices) expect(result).to eq(:all) expected_output = [ "Overwrite Gemfile? (enter \"h\" for help) [\e[32my\e[0m,n,a,d,q,h] ", "\e[2K\e[1G", "Overwrite Gemfile? (enter \"h\" for help) [y,n,\e[32ma\e[0m,d,q,h] ", "a\n", "\e[32m>> \e[0mOverwrite all", "\e[A\e[1G\e[55C", "\e[2K\e[1G", "\e[1B", "\e[2K\e[1G", "\e[A\e[1G", "Overwrite Gemfile? \e[32mOverwrite all\e[0m\n" ].join expect(prompt.output.string).to eq(expected_output) end it "expands help option and then defaults" do prompt.input << "h\nd\n" prompt.input.rewind result = prompt.expand('Overwrite Gemfile?', choices) expect(result).to eq(:diff) expected_output = [ "Overwrite Gemfile? (enter \"h\" for help) [\e[32my\e[0m,n,a,d,q,h] ", "\e[2K\e[1G", "Overwrite Gemfile? (enter \"h\" for help) [y,n,a,d,q,\e[32mh\e[0m] h\n", "\e[32m>> \e[0mprint help", "\e[A\e[1G\e[55C", "\e[2K\e[1G", "\e[1B", "\e[2K\e[1G", "\e[A\e[1G", "Overwrite Gemfile? \n", " y - Overwrite\n", " n - Skip\n", " a - Overwrite all\n", " d - Show diff\n", " q - Quit\n", " h - print help\n", " Choice [y]: ", "\e[2K\e[1G\e[1A" * 7, "\e[2K\e[1G", "Overwrite Gemfile? \n", " y - Overwrite\n", " n - Skip\n", " a - Overwrite all\n", " \e[32md - Show diff\e[0m\n", " q - Quit\n", " h - print help\n", " Choice [y]: d", "\e[2K\e[1G\e[1A" * 7, "\e[2K\e[1G", "Overwrite Gemfile? \e[32mShow diff\e[0m\n", ].join expect(prompt.output.string).to eq(expected_output) end it "automatically expands hint" do prompt.input << "d\n" prompt.input.rewind result = prompt.expand('Overwrite Gemfile?', choices, auto_hint: true) expect(result).to eq(:diff) expected_output = [ "Overwrite Gemfile? (enter \"h\" for help) [\e[32my\e[0m,n,a,d,q,h] ", "\n\e[32m>> \e[0mOverwrite", "\e[A\e[1G\e[54C", "\e[2K\e[1G", "\e[1B", "\e[2K\e[1G", "\e[A\e[1G", "Overwrite Gemfile? (enter \"h\" for help) [y,n,a,\e[32md\e[0m,q,h] ", "d\n", "\e[32m>> \e[0mShow diff", "\e[A\e[1G\e[55C", "\e[2K\e[1G", "\e[1B", "\e[2K\e[1G", "\e[A\e[1G", "Overwrite Gemfile? \e[32mShow diff\e[0m\n", "\e[32m>> \e[0mShow diff", "\e[A\e[1G\e[28C\n" ].join expect(prompt.output.string).to eq(expected_output) end it "informs about invalid input when automatically expanding hint" do prompt = TTY::TestPrompt.new prompt.on(:keypress) { |e| prompt.trigger(:keybackspace) if e.value == "w" } prompt.input << "y" << "y" << "\u007F" << "\r" prompt.input.rewind result = prompt.expand('Overwrite Gemfile?', choices, defualt: 1, auto_hint: true) expect(result).to eq(:yes) expected_output = [ "Overwrite Gemfile? (enter \"h\" for help) [\e[32my\e[0m,n,a,d,q,h] ", "\n\e[32m>> \e[0mOverwrite", "\e[A\e[1G\e[54C", "\e[2K\e[1G", "\e[1B", "\e[2K\e[1G", "\e[A\e[1G", "Overwrite Gemfile? (enter \"h\" for help) [\e[32my\e[0m,n,a,d,q,h] ", "y\n", "\e[32m>> \e[0mOverwrite", "\e[A\e[1G\e[55C", "\e[2K\e[1G", "\e[1B", "\e[2K\e[1G", "\e[A\e[1G", "Overwrite Gemfile? (enter \"h\" for help) [y,n,a,d,q,h] ", "yy\n", "\e[32m>> \e[0minvalid option", "\e[A\e[1G\e[56C", "\e[2K\e[1G", "\e[1B", "\e[2K\e[1G", "\e[A\e[1G", "Overwrite Gemfile? (enter \"h\" for help) [\e[32my\e[0m,n,a,d,q,h] ", "y\n", "\e[32m>> \e[0mOverwrite", "\e[A\e[1G\e[55C", "\e[2K\e[1G", "\e[1B", "\e[2K\e[1G", "\e[A\e[1G", "Overwrite Gemfile? \e[32mOverwrite\e[0m\n", "\e[32m>> \e[0mOverwrite", "\e[A\e[1G\e[28C\n" ].join expect(prompt.output.string).to eq(expected_output) end it "specifies options through DSL" do prompt.input << "\n" prompt.input.rewind result = prompt.expand('Overwrite Gemfile?') do |q| q.default 4 q.choice key: 'y', name: 'Overwrite', value: :yes q.choice key: 'n', name: 'Skip', value: :no q.choice key: 'a', name: 'Overwrite all', value: :all q.choice key: 'd', name: 'Show diff', value: :diff q.choice key: 'q', name: 'Quit', value: :quit end expect(result).to eq(:diff) expected_output = [ "Overwrite Gemfile? (enter \"h\" for help) [y,n,a,\e[32md\e[0m,q,h] ", "\e[2K\e[1G", "Overwrite Gemfile? \e[32mShow diff\e[0m\n" ].join expect(prompt.output.string).to eq(expected_output) end it "specifies options through DSL and executes value" do prompt.input << "\n" prompt.input.rewind result = prompt.expand('Overwrite Gemfile?') do |q| q.choice key: 'y', name: 'Overwrite' do :ok end q.choice key: 'n', name: 'Skip', value: :no q.choice key: 'a', name: 'Overwrite all', value: :all q.choice key: 'd', name: 'Show diff', value: :diff q.choice key: 'q', name: 'Quit', value: :quit end expect(result).to eq(:ok) expected_output = [ "Overwrite Gemfile? (enter \"h\" for help) [\e[32my\e[0m,n,a,d,q,h] ", "\e[2K\e[1G", "Overwrite Gemfile? \e[32mOverwrite\e[0m\n" ].join expect(prompt.output.string).to eq(expected_output) end it "fails to expand due to lack of key attribute" do choices = [{ name: 'Overwrite', value: :yes }] expect { prompt.expand('Overwrite Gemfile?', choices) }.to raise_error(TTY::Prompt::ConfigurationError, /Choice Overwrite is missing a :key attribute/) end it "fails to expand due to wrong key length" do choices = [{ key: 'long', name: 'Overwrite', value: :yes }] expect { prompt.expand('Overwrite Gemfile?', choices) }.to raise_error(TTY::Prompt::ConfigurationError, /Choice key `long` is more than one character long/) end it "fails to expand due to reserve key" do choices = [{ key: 'h', name: 'Overwrite', value: :yes }] expect { prompt.expand('Overwrite Gemfile?', choices) }.to raise_error(TTY::Prompt::ConfigurationError, /Choice key `h` is reserved for help menu/) end it "fails to expand due to duplicate key" do choices = [{ key: 'y', name: 'Overwrite', value: :yes }, { key: 'y', name: 'Change', value: :yes }] expect { prompt.expand('Overwrite Gemfile?', choices) }.to raise_error(TTY::Prompt::ConfigurationError, /Choice key `y` is a duplicate/) end end tty-prompt-0.21.0/spec/unit/keypress_spec.rb000066400000000000000000000031061363125043200210450ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, '#keypress' do it 'receives line feed with echo on' do prompt = TTY::TestPrompt.new prompt.input << "\n" prompt.input.rewind answer = prompt.keypress("Press key:", echo: true) expect(answer).to eq("\n") expect(prompt.output.string).to eq([ "Press key: ", "\e[2K\e[1G\e[1A\e[2K\e[1G", "Press key: \e[32m\n\e[0m\n", ].join) end it 'asks for a keypress with echo on' do prompt = TTY::TestPrompt.new prompt.input << "abcd" prompt.input.rewind answer = prompt.keypress("Press key:", echo: true) expect(answer).to eq("a") expect(prompt.output.string).to eq([ "Press key: ", "\e[2K\e[1G", "Press key: \e[32ma\e[0m\n", ].join) end it 'asks for a keypress with echo off' do prompt = TTY::TestPrompt.new prompt.input << "abcd" prompt.input.rewind answer = prompt.keypress("Press key:") expect(answer).to eq("a") expect(prompt.output.string).to eq([ "Press key: ", "\e[2K\e[1G", "Press key: \n", ].join) end it "interrupts input" do prompt = TTY::TestPrompt.new(interrupt: :exit) prompt.input << "\x03" prompt.input.rewind expect { prompt.keypress("Press key:") }.to raise_error(SystemExit) end it "timeouts when no key provided" do prompt = TTY::TestPrompt.new(interrupt: :exit) prompt.keypress("Press any key or continue in :countdown", timeout: 0.01) expect(prompt.output.string).to include("Press any key or continue in 0.00") end end tty-prompt-0.21.0/spec/unit/mask_spec.rb000066400000000000000000000074171363125043200201440ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, '#mask' do subject(:prompt) { TTY::TestPrompt.new } let(:symbols) { TTY::Prompt::Symbols.symbols } it "masks output by default" do prompt.input << "pass\r" prompt.input.rewind answer = prompt.mask("What is your password?") expect(answer).to eql("pass") expect(prompt.output.string).to eq([ "What is your password? ", "\e[2K\e[1G", "What is your password? #{symbols[:dot]}", "\e[2K\e[1G", "What is your password? #{symbols[:dot] * 2}", "\e[2K\e[1G", "What is your password? #{symbols[:dot] * 3}", "\e[2K\e[1G", "What is your password? #{symbols[:dot] * 4}", "\e[2K\e[1G", "What is your password? \e[32m#{symbols[:dot] * 4}\e[0m\n", "\e[1A\e[2K\e[1G", "What is your password? \e[32m#{symbols[:dot] * 4}\e[0m\n" ].join) end it 'masks output with custom character' do prompt.input << "pass\r" prompt.input.rewind answer = prompt.mask("What is your password?") { |q| q.mask('*') } expect(answer).to eql("pass") expect(prompt.output.string).to eq([ "What is your password? ", "\e[2K\e[1G", "What is your password? *", "\e[2K\e[1G", "What is your password? **", "\e[2K\e[1G", "What is your password? ***", "\e[2K\e[1G", "What is your password? ****", "\e[2K\e[1G", "What is your password? \e[32m****\e[0m\n", "\e[1A\e[2K\e[1G", "What is your password? \e[32m****\e[0m\n", ].join) end it "masks with unicode character" do prompt.input << "lov\n" prompt.input.rewind answer = prompt.mask("What is your password?", mask: "\u2665") expect(answer).to eql("lov") expect(prompt.output.string).to eq([ "What is your password? ", "\e[2K\e[1G", "What is your password? ♥", "\e[2K\e[1G", "What is your password? ♥♥", "\e[2K\e[1G", "What is your password? ♥♥♥", "\e[2K\e[1G", "What is your password? \e[32m♥♥♥\e[0m\n", "\e[1A\e[2K\e[1G", "What is your password? \e[32m♥♥♥\e[0m\n", ].join) end it 'ignores mask if echo is off' do prompt.input << "pass\n" prompt.input.rewind answer = prompt.mask('What is your password?') do |q| q.echo false q.mask '*' end expect(answer).to eql("pass") expect(prompt.output.string).to eq([ "What is your password? ", "\e[2K\e[1G", "What is your password? ", "\e[2K\e[1G", "What is your password? ", "\e[2K\e[1G", "What is your password? ", "\e[2K\e[1G", "What is your password? ", "\e[2K\e[1G", "What is your password? \n", "\e[1A\e[2K\e[1G", "What is your password? \n", ].join) end it "validates input" do prompt = TTY::TestPrompt.new(symbols: {dot: '*'}) prompt.input << "no\nyes\n" prompt.input.rewind answer = prompt.mask('What is your password?') do |q| q.echo true q.validate(/[a-z]{3,4}/) q.messages[:valid?] = 'Not valid' end expect(answer).to eq('yes') expect(prompt.output.string).to eq([ "What is your password? ", "\e[2K\e[1G", "What is your password? *", "\e[2K\e[1G", "What is your password? **", "\e[2K\e[1G", "What is your password? \e[32m**\e[0m\n", "\e[31m>>\e[0m Not valid", "\e[1A\e[2K\e[1G", "What is your password? \e[31m**\e[0m", "\e[2K\e[1G", "What is your password? *", "\e[2K\e[1G", "What is your password? **", "\e[2K\e[1G", "What is your password? ***", "\e[2K\e[1G", "What is your password? \e[32m***\e[0m\n", "\e[2K\e[1G", "\e[1A\e[2K\e[1G", "What is your password? \e[32m***\e[0m\n" ].join) end end tty-prompt-0.21.0/spec/unit/multi_select_spec.rb000066400000000000000000000724071363125043200217030ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt do let(:symbols) { TTY::Prompt::Symbols.symbols } let(:up_down) { "#{symbols[:arrow_up]}/#{symbols[:arrow_down]}" } let(:left_right) { "#{symbols[:arrow_left]}/#{symbols[:arrow_right]}"} def output_helper(prompt, choices, active, selected, options = {}) raise ":init requires :hint" if options[:init] && options[:hint].nil? hint = options[:hint] init = options.fetch(:init, false) enum = options[:enum] out = [] out << "\e[?25l" if init out << prompt << " " out << "(min. #{options[:min]}) " if options[:min] out << "(max. #{options[:max]}) " if options[:max] out << selected.join(', ') out << " " if init && !selected.empty? out << "\e[90m" if init out << (init ? "(#{hint})\e[0m" : " (#{hint})") if hint out << "\n" out << choices.map.with_index do |choice, i| name = choice.is_a?(Hash) ? choice[:name] : choice disabled = choice.is_a?(Hash) ? choice[:disabled] : false num = (i + 1).to_s + enum if enum prefix = name == active ? "#{symbols[:marker]} " : " " prefix += if disabled "\e[31m#{symbols[:cross]}\e[0m #{num}#{name} #{disabled}" elsif selected.include?(name) "\e[32m#{symbols[:radio_on]}\e[0m #{num}#{name}" else "#{symbols[:radio_off]} #{num}#{name}" end prefix end.join("\n") out << "\e[2K\e[1G\e[1A" * choices.count out << "\e[2K\e[1G" out.join end def exit_message(prompt, choices) "#{prompt} \e[32m#{choices.join(', ')}\e[0m\n\e[?25h" end # Ensure a wide prompt on CI before { allow(TTY::Screen).to receive(:width).and_return(200) } it "selects nothing when return pressed immediately" do prompt = TTY::TestPrompt.new choices = %i(vodka beer wine whisky bourbon) prompt.input << "\r" prompt.input.rewind expect(prompt.multi_select("Select drinks?", choices)). to eq([]) expected_output = [ "\e[?25lSelect drinks? \e[90m(Use #{up_down} arrow keys, press Space to select and Enter to finish)\e[0m\n", "#{symbols[:marker]} #{symbols[:radio_off]} vodka\n", " #{symbols[:radio_off]} beer\n", " #{symbols[:radio_off]} wine\n", " #{symbols[:radio_off]} whisky\n", " #{symbols[:radio_off]} bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "Select drinks? \n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end it "selects item when space pressed" do prompt = TTY::TestPrompt.new choices = %w(vodka beer wine whisky bourbon) prompt.input << " \r" prompt.input.rewind expect(prompt.multi_select("Select drinks?", choices)). to eq(['vodka']) expected_output = [ "\e[?25lSelect drinks? \e[90m(Use #{up_down} arrow keys, press Space to select and Enter to finish)\e[0m\n", "#{symbols[:marker]} #{symbols[:radio_off]} vodka\n", " #{symbols[:radio_off]} beer\n", " #{symbols[:radio_off]} wine\n", " #{symbols[:radio_off]} whisky\n", " #{symbols[:radio_off]} bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "Select drinks? vodka\n", "#{symbols[:marker]} \e[32m#{symbols[:radio_on]}\e[0m vodka\n", " #{symbols[:radio_off]} beer\n", " #{symbols[:radio_off]} wine\n", " #{symbols[:radio_off]} whisky\n", " #{symbols[:radio_off]} bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "Select drinks? \e[32mvodka\e[0m\n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end it "selects item when space pressed but doesn't echo item if echo: false" do prompt = TTY::TestPrompt.new choices = %w(vodka beer wine whisky bourbon) prompt.input << " \r" prompt.input.rewind expect(prompt.multi_select("Select drinks?", choices, echo: false)). to eq(['vodka']) expected_output = [ "\e[?25lSelect drinks? \e[90m(Use #{up_down} arrow keys, press Space to select and Enter to finish)\e[0m\n", "#{symbols[:marker]} #{symbols[:radio_off]} vodka\n", " #{symbols[:radio_off]} beer\n", " #{symbols[:radio_off]} wine\n", " #{symbols[:radio_off]} whisky\n", " #{symbols[:radio_off]} bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "Select drinks? \n", "#{symbols[:marker]} \e[32m#{symbols[:radio_on]}\e[0m vodka\n", " #{symbols[:radio_off]} beer\n", " #{symbols[:radio_off]} wine\n", " #{symbols[:radio_off]} whisky\n", " #{symbols[:radio_off]} bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "Select drinks? \n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end it "sets choice custom values" do prompt = TTY::TestPrompt.new choices = {vodka: 1, beer: 2, wine: 3, whisky: 4, bourbon: 5} prompt.input << " \r" prompt.input.rewind expect(prompt.multi_select("Select drinks?", choices)).to eq([1]) expected_output = [ "\e[?25lSelect drinks? \e[90m(Use #{up_down} arrow keys, press Space to select and Enter to finish)\e[0m\n", "#{symbols[:marker]} #{symbols[:radio_off]} vodka\n", " #{symbols[:radio_off]} beer\n", " #{symbols[:radio_off]} wine\n", " #{symbols[:radio_off]} whisky\n", " #{symbols[:radio_off]} bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "Select drinks? vodka\n", "#{symbols[:marker]} \e[32m#{symbols[:radio_on]}\e[0m vodka\n", " #{symbols[:radio_off]} beer\n", " #{symbols[:radio_off]} wine\n", " #{symbols[:radio_off]} whisky\n", " #{symbols[:radio_off]} bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "Select drinks? \e[32mvodka\e[0m\n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end it "sets choice name and value through DSL" do prompt = TTY::TestPrompt.new prompt.input << " \r" prompt.input.rewind value = prompt.multi_select("Select drinks?") do |menu| menu.symbols marker: '>', radio_off: '-', radio_on: '=' menu.enum ')' menu.choice :vodka, {score: 1} menu.choice :beer, 2 menu.choice :wine, 3 menu.choices whisky: 4, bourbon: 5 end expect(value).to eq([{score: 1}]) expect(prompt.output.string).to eq([ "\e[?25lSelect drinks? \e[90m(Use #{up_down} arrow or number (1-5) keys, press Space to select and Enter to finish)\e[0m\n", "> - 1) vodka\n", " - 2) beer\n", " - 3) wine\n", " - 4) whisky\n", " - 5) bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "Select drinks? vodka\n", "> \e[32m=\e[0m 1) vodka\n", " - 2) beer\n", " - 3) wine\n", " - 4) whisky\n", " - 5) bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "Select drinks? \e[32mvodka\e[0m\n\e[?25h" ].join) end it "sets default options through DSL syntax" do prompt = TTY::TestPrompt.new prompt.input << "\r" prompt.input.rewind value = prompt.multi_select("Select drinks?") do |menu| menu.default 2, 5 menu.choice :vodka, {score: 10} menu.choice :beer, {score: 20} menu.choice :wine, {score: 30} menu.choice :whisky, {score: 40} menu.choice :bourbon, {score: 50} end expect(value).to match_array([{score: 20}, {score: 50}]) expect(prompt.output.string).to eq([ "\e[?25lSelect drinks? beer, bourbon \e[90m(Use #{up_down} arrow keys, press Space to select and Enter to finish)\e[0m\n", " #{symbols[:radio_off]} vodka\n", " \e[32m#{symbols[:radio_on]}\e[0m beer\n", " #{symbols[:radio_off]} wine\n", " #{symbols[:radio_off]} whisky\n", "#{symbols[:marker]} \e[32m#{symbols[:radio_on]}\e[0m bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "Select drinks? \e[32mbeer, bourbon\e[0m\n\e[?25h", ].join) end it "sets default options through hash syntax" do prompt = TTY::TestPrompt.new prompt.input << "\r" prompt.input.rewind value = prompt.multi_select("Select drinks?", default: [2, 5]) do |menu| menu.choice :vodka, {score: 10} menu.choice :beer, {score: 20} menu.choice :wine, {score: 30} menu.choice :whisky, {score: 40} menu.choice :bourbon, {score: 50} end expect(value).to match_array([{score: 20}, {score: 50}]) end it "raises error for defaults out of range" do prompt = TTY::TestPrompt.new prompt.input << "\r" prompt.input.rewind expect { prompt.multi_select("Select drinks?", default: [2, 6]) do |menu| menu.choice :vodka, {score: 10} menu.choice :beer, {score: 20} menu.choice :wine, {score: 30} menu.choice :whisky, {score: 40} menu.choice :bourbon, {score: 50} end }.to raise_error(TTY::Prompt::ConfigurationError, /default index `6` out of range \(1 - 5\)/) end it "sets prompt prefix" do prompt = TTY::TestPrompt.new(prefix: '[?] ') choices = %w(vodka beer wine whisky bourbon) prompt.input << "\r" prompt.input.rewind expect(prompt.multi_select("Select drinks?", choices)). to eq([]) expect(prompt.output.string).to eq([ "\e[?25l[?] Select drinks? \e[90m(Use #{up_down} arrow keys, press Space to select and Enter to finish)\e[0m\n", "#{symbols[:marker]} #{symbols[:radio_off]} vodka\n", " #{symbols[:radio_off]} beer\n", " #{symbols[:radio_off]} wine\n", " #{symbols[:radio_off]} whisky\n", " #{symbols[:radio_off]} bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "[?] Select drinks? \n\e[?25h" ].join) end it "changes selected item color & marker" do prompt = TTY::TestPrompt.new choices = %w(vodka beer wine whisky bourbon) prompt.input << "\r" prompt.input.rewind options = {default: [1], active_color: :blue, symbols: {marker: '>'}} expect(prompt.multi_select("Select drinks?", choices, options)). to eq(['vodka']) expect(prompt.output.string).to eq([ "\e[?25lSelect drinks? vodka \e[90m(Use #{up_down} arrow keys, press Space to select and Enter to finish)\e[0m\n", "> \e[34m#{symbols[:radio_on]}\e[0m vodka\n", " #{symbols[:radio_off]} beer\n", " #{symbols[:radio_off]} wine\n", " #{symbols[:radio_off]} whisky\n", " #{symbols[:radio_off]} bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "Select drinks? \e[34mvodka\e[0m\n\e[?25h" ].join) end it "changes help text and color" do prompt = TTY::TestPrompt.new choices = %w(vodka beer wine whisky bourbon) prompt.input << "\r" prompt.input.rewind options = { help: "(Bash keyboard)", help_color: :cyan } answer = prompt.multi_select("Select drinks?", choices, options) expect(answer).to eq([]) expected_output = [ "\e[?25lSelect drinks? \e[36m(Bash keyboard)\e[0m\n", "#{symbols[:marker]} #{symbols[:radio_off]} vodka\n", " #{symbols[:radio_off]} beer\n", " #{symbols[:radio_off]} wine\n", " #{symbols[:radio_off]} whisky\n", " #{symbols[:radio_off]} bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "Select drinks? \n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end context "when paginated" do it "paginates long selections" do prompt = TTY::TestPrompt.new choices = %w(A B C D E F G H) prompt.input << "\r" prompt.input.rewind answer = prompt.multi_select("What letter?", choices, per_page: 3, default: 4) expect(answer).to eq(['D']) expected_output = [ "\e[?25lWhat letter? D \e[90m(Use #{up_down} and #{left_right} arrow keys, press Space to select and Enter to finish)\e[0m\n", "#{symbols[:marker]} \e[32m#{symbols[:radio_on]}\e[0m D\n", " #{symbols[:radio_off]} E\n", " #{symbols[:radio_off]} F", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What letter? \e[32mD\e[0m\n\e[?25h", ].join expect(prompt.output.string).to eq(expected_output) end it "paginates choices as hash object" do prompt = TTY::TestPrompt.new choices = {A: 1, B: 2, C: 3, D: 4, E: 5, F: 6, G: 7, H: 8} prompt.input << "\r" prompt.input.rewind answer = prompt.multi_select("What letter?", choices, default: 4, per_page: 3) expect(answer).to eq([4]) expected_output = [ "\e[?25lWhat letter? D ", "\e[90m(Use #{up_down} and #{left_right} arrow keys, press Space to select and Enter to finish)\e[0m\n", "#{symbols[:marker]} \e[32m#{symbols[:radio_on]}\e[0m D\n", " #{symbols[:radio_off]} E\n", " #{symbols[:radio_off]} F", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What letter? \e[32mD\e[0m\n\e[?25h", ].join expect(prompt.output.string).to eq(expected_output) end it "paginates long selections through DSL" do prompt = TTY::TestPrompt.new choices = %w(A B C D E F G H) prompt.input << "\r" prompt.input.rewind answer = prompt.multi_select("What letter?") do |menu| menu.per_page 3 menu.default 4 menu.choices choices end expect(answer).to eq(['D']) expected_output = [ "\e[?25lWhat letter? D ", "\e[90m(Use #{up_down} and #{left_right} arrow keys, press Space to select and Enter to finish)\e[0m\n", "#{symbols[:marker]} \e[32m#{symbols[:radio_on]}\e[0m D\n", " #{symbols[:radio_off]} E\n", " #{symbols[:radio_off]} F", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What letter? \e[32mD\e[0m\n\e[?25h", ].join expect(prompt.output.string).to eq(expected_output) end it "doesn't paginate short selections" do prompt = TTY::TestPrompt.new choices = %w(A B C D) prompt.input << "\r" prompt.input.rewind value = prompt.multi_select("What letter?", choices, per_page: 4, default: 1) expect(value).to eq(['A']) expect(prompt.output.string).to eq([ "\e[?25lWhat letter? A ", "\e[90m(Use #{up_down} arrow keys, press Space to select and Enter to finish)\e[0m\n", "#{symbols[:marker]} \e[32m#{symbols[:radio_on]}\e[0m A\n", " #{symbols[:radio_off]} B\n", " #{symbols[:radio_off]} C\n", " #{symbols[:radio_off]} D", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G", "What letter? \e[32mA\e[0m\n\e[?25h", ].join) end it "navigates evenly paged output with right arrow until end of selection" do prompt = TTY::TestPrompt.new choices = ('1'..'12').to_a prompt.on(:keypress) { |e| prompt.trigger(:keyright) if e.value == "l" } prompt.input << "l" << "l" << "l" << " " << "\r" prompt.input.rewind answer = prompt.multi_select("What number?", choices, per_page: 4) expect(answer).to eq(["9"]) expected_output = [ output_helper("What number?", choices[0..3], "1", [], init: true, hint: "Use #{up_down} and #{left_right} arrow keys, press Space to select and Enter to finish"), output_helper("What number?", choices[4..7], "5", []), output_helper("What number?", choices[8..11], "9", []), output_helper("What number?", choices[8..11], "9", []), output_helper("What number?", choices[8..11], "9", ["9"]), "What number? \e[32m9\e[0m\n\e[?25h", ].join expect(prompt.output.string).to eq(expected_output) end it "navigates unevenly paged output with right arrow until the end of selection" do prompt = TTY::TestPrompt.new choices = ('1'..'10').to_a prompt.on(:keypress) { |e| prompt.trigger(:keyright) if e.value == "l" } prompt.input << "l" << "l" << "l" << " " << "\r" prompt.input.rewind answer = prompt.multi_select("What number?", choices, default: 4, per_page: 4) expect(answer).to eq(['4', '10']) expected_output = [ output_helper("What number?", choices[3..6], "4", ["4"], init: true, hint: "Use #{up_down} and #{left_right} arrow keys, press Space to select and Enter to finish"), output_helper("What number?", choices[4..7], "8", ["4"]), output_helper("What number?", choices[8..9], "10", ["4"]), output_helper("What number?", choices[8..9], "10", ["4"]), output_helper("What number?", choices[8..9], "10", ["4", "10"]), "What number? \e[32m4, 10\e[0m\n\e[?25h", ].join expect(prompt.output.string).to eq(expected_output) end it "navigates left and right" do prompt = TTY::TestPrompt.new choices = ('1'..'10').to_a prompt.on(:keypress) { |e| prompt.trigger(:keyright) if e.value == "l" prompt.trigger(:keyleft) if e.value == "h" } prompt.input << "l" << "l" << "h" << " " << "\r" prompt.input.rewind answer = prompt.multi_select("What number?", choices, default: 2, per_page: 4) expect(answer).to eq(['2', '6']) expected_output = [ output_helper("What number?", choices[0..3], "2", ["2"], init: true, hint: "Use #{up_down} and #{left_right} arrow keys, press Space to select and Enter to finish"), output_helper("What number?", choices[4..7], "6", ["2"]), output_helper("What number?", choices[8..9], "10", ["2"]), output_helper("What number?", choices[4..7], "6", ["2"]), output_helper("What number?", choices[4..7], "6", ["2", "6"]), "What number? \e[32m2, 6\e[0m\n\e[?25h", ].join expect(prompt.output.string).to eq(expected_output) end it "combines up/down navigation with left/right" do prompt = TTY::TestPrompt.new choices = ('1'..'11').to_a prompt.on(:keypress) { |e| prompt.trigger(:keyup) if e.value == "k" prompt.trigger(:keydown) if e.value == "j" prompt.trigger(:keyright) if e.value == "l" prompt.trigger(:keyleft) if e.value == "h" } prompt.input << "j" << "l" << "k" << "k" << "h" << " " << "\r" prompt.input.rewind answer = prompt.multi_select("What number?", choices, default: 2, per_page: 4) expect(answer).to eq(['2', '1']) expected_output = [ output_helper("What number?", choices[0..3], "2", ["2"], init: true, hint: "Use #{up_down} and #{left_right} arrow keys, press Space to select and Enter to finish"), output_helper("What number?", choices[0..3], "3", ["2"]), output_helper("What number?", choices[4..7], "7", ["2"]), output_helper("What number?", choices[4..7], "6", ["2"]), output_helper("What number?", choices[3..6], "5", ["2"]), output_helper("What number?", choices[0..3], "1", ["2"]), output_helper("What number?", choices[0..3], "1", ["2", "1"]), "What number? \e[32m2, 1\e[0m\n\e[?25h", ].join expect(prompt.output.string).to eq(expected_output) end end context "with :cycle" do it "doesn't cycle by default" do prompt = TTY::TestPrompt.new choices = %w(A B C) prompt.on(:keypress) { |e| prompt.trigger(:keydown) if e.value == "j" } prompt.input << "j" << "j" << "j" << " " << "\r" prompt.input.rewind value = prompt.multi_select("What letter?", choices) expect(value).to eq(["C"]) expect(prompt.output.string).to eq( output_helper("What letter?", choices, "A", [], init: true, hint: "Use #{up_down} arrow keys, press Space to select and Enter to finish") + output_helper("What letter?", choices, "B", []) + output_helper("What letter?", choices, "C", []) + output_helper("What letter?", choices, "C", []) + output_helper("What letter?", choices, "C", ["C"]) + "What letter? \e[32mC\e[0m\n\e[?25h" ) end it "cycles when configured to do so" do prompt = TTY::TestPrompt.new choices = %w(A B C) prompt.on(:keypress) { |e| prompt.trigger(:keydown) if e.value == "j" } prompt.input << "j" << "j" << "j" << " " << "\r" prompt.input.rewind value = prompt.multi_select("What letter?", choices, cycle: true) expect(value).to eq(["A"]) expect(prompt.output.string).to eq( output_helper("What letter?", choices, "A", [], init: true, hint: "Use #{up_down} arrow keys, press Space to select and Enter to finish") + output_helper("What letter?", choices, "B", []) + output_helper("What letter?", choices, "C", []) + output_helper("What letter?", choices, "A", []) + output_helper("What letter?", choices, "A", ["A"]) + "What letter? \e[32mA\e[0m\n\e[?25h" ) end it "cycles choices using left/right arrows" do prompt = TTY::TestPrompt.new choices = ('1'..'10').to_a prompt.on(:keypress) { |e| prompt.trigger(:keyright) if e.value == "l" prompt.trigger(:keyleft) if e.value == "h" } prompt.input << "l" << "l" << "l" << "h" << " " << "\r" prompt.input.rewind answer = prompt.multi_select("What number?", choices, default: 2, per_page: 4, cycle: true) expect(answer).to eq(['2', '10']) expected_output = [ "\e[?25lWhat number? 2 ", "\e[90m(Use #{up_down} and #{left_right} arrow keys, press Space to select and Enter to finish)\e[0m\n", " #{symbols[:radio_off]} 1\n", "#{symbols[:marker]} \e[32m#{symbols[:radio_on]}\e[0m 2\n", " #{symbols[:radio_off]} 3\n", " #{symbols[:radio_off]} 4", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G", "What number? 2\n", " #{symbols[:radio_off]} 5\n", "#{symbols[:marker]} #{symbols[:radio_off]} 6\n", " #{symbols[:radio_off]} 7\n", " #{symbols[:radio_off]} 8", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G", "What number? 2\n", " #{symbols[:radio_off]} 9\n", "#{symbols[:marker]} #{symbols[:radio_off]} 10", "\e[2K\e[1G\e[1A" * 2, "\e[2K\e[1G", "What number? 2\n", " #{symbols[:radio_off]} 1\n", "#{symbols[:marker]} \e[32m#{symbols[:radio_on]}\e[0m 2\n", " #{symbols[:radio_off]} 3\n", " #{symbols[:radio_off]} 4", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G", "What number? 2\n", " #{symbols[:radio_off]} 9\n", "#{symbols[:marker]} #{symbols[:radio_off]} 10", "\e[2K\e[1G\e[1A" * 2, "\e[2K\e[1G", "What number? 2, 10\n", " #{symbols[:radio_off]} 9\n", "#{symbols[:marker]} \e[32m#{symbols[:radio_on]}\e[0m 10", "\e[2K\e[1G\e[1A" * 2, "\e[2K\e[1G", "What number? \e[32m2, 10\e[0m\n\e[?25h", ].join expect(prompt.output.string).to eq(expected_output) end end context "with filter" do it "doesn't lose the selection when switching between filters" do prompt = TTY::TestPrompt.new prompt.on(:keypress) { |e| prompt.trigger(:keydelete) if e.value == "\r" } prompt.input << " " # select `Tiny` prompt.input << "a" << " " # match and select `Large` prompt.input << "\u007F" # backspace (shows all) prompt.input << "\r" prompt.input.rewind answer = prompt.multi_select("What size?", %w(Tiny Medium Large Huge), filter: true) expect(answer).to eql(%w(Tiny Large)) expected_prompt_output = output_helper("What size?", %w(Tiny Medium Large Huge), "Tiny", %w(), init: true, hint: "Use #{up_down} arrow keys, press Space to select and Enter to finish, and letter keys to filter") + output_helper("What size?", %w(Tiny Medium Large Huge), "Tiny", %w(Tiny)) + output_helper("What size?", %w(Large), "Large", %w(Tiny), hint: 'Filter: "a"') + output_helper("What size?", %w(Large), "Large", %w(Tiny Large), hint: 'Filter: "a"') + output_helper("What size?", %w(Tiny Medium Large Huge), "Tiny", %w(Tiny Large)) + exit_message("What size?", %w(Tiny Large)) expect(prompt.output.string).to eql(expected_prompt_output) end end context "with :disabled" do it "fails when default item is also disabled" do prompt = TTY::TestPrompt.new choices = [ {name: 'vodka', disabled: true}, 'beer', 'wine', 'whisky', 'bourbon' ] expect { prompt.multi_select("Select drinks?", choices, default: 1) }.to raise_error(TTY::Prompt::ConfigurationError, /default index `1` matches disabled choice item/) end it "adjusts active index to match first non-disabled choice" do choices = [ {name: 'vodka', disabled: true}, 'beer', 'wine', 'whisky', 'bourbon' ] prompt = TTY::TestPrompt.new prompt.input << " " << "\r" prompt.input.rewind answer = prompt.multi_select("Select drinks?", choices) expect(answer).to eq(['beer']) end it "omits disabled choice when nagivating menu" do choices = [ {name: 'A'}, {name: 'B', disabled: '(out)'}, {name: 'C', disabled: '(out)'}, {name: 'D'}, {name: 'E'} ] prompt = TTY::TestPrompt.new prompt.on(:keypress) { |e| prompt.trigger(:keydown) if e.value == "j" } prompt.input << "j" << " " << "j" << " " << "\r" prompt.input.rewind answer = prompt.multi_select("What letter?", choices) expect(answer).to eq(%w[D E]) expected_output = output_helper("What letter?", choices, "A", [], init: true, hint: "Use #{up_down} arrow keys, press Space to select and Enter to finish") + output_helper("What letter?", choices, "D", []) + output_helper("What letter?", choices, "D", %w[D]) + output_helper("What letter?", choices, "E", %w[D]) + output_helper("What letter?", choices, "E", %w[D E]) + exit_message("What letter?", %w[D E]) expect(prompt.output.string).to eq(expected_output) end it "omits disabled choice when number key is pressed" do choices = [ {name: 'vodka', value: 1}, {name: 'beer', value: 1, disabled: true}, {name: 'wine', value: 1}, {name: 'whisky', value: 1, disabled: true}, {name: 'bourbon', value: 1} ] prompt = TTY::TestPrompt.new prompt.input << "2" << " \r" prompt.input.rewind answer = prompt.multi_select("Select drinks?") do |menu| menu.enum ')' menu.choice :vodka, 1 menu.choice :beer, 2, disabled: true menu.choice :wine, 3 menu.choice :whisky, 4, disabled: true menu.choice :bourbon, 5 end expect(answer).to eq([1]) expected_output = output_helper("Select drinks?", choices, "vodka", [], init: true, enum: ') ', hint: "Use #{up_down} arrow or number (1-5) keys, press Space to select and Enter to finish") + output_helper("Select drinks?", choices, "vodka", [], enum: ') ') + output_helper("Select drinks?", choices, "vodka", %w[vodka], enum: ') ') + exit_message("Select drinks?", %w[vodka]) expect(prompt.output.string).to eq(expected_output) end end context "with :min" do it "requires number of choices" do prompt = TTY::TestPrompt.new choices = %w(A B C) prompt.on(:keypress) { |e| prompt.trigger(:keydown) if e.value == "j" } prompt.input << " " << "\r" << "j" << " " << "\r" prompt.input.rewind value = prompt.multi_select("What letter?", choices, min: 2, per_page: 100) expect(value).to eq(["A", "B"]) expected_output = output_helper("What letter?", choices, "A", [], init: true, min: 2, hint: "Use #{up_down} arrow keys, press Space to select and Enter to finish") + output_helper("What letter?", choices, "A", %w[A], min: 2) + output_helper("What letter?", choices, "A", %w[A], min: 2) + output_helper("What letter?", choices, "B", %w[A], min: 2) + output_helper("What letter?", choices, "B", %w[A B], min: 2) + exit_message("What letter?", %w[A B]) expect(prompt.output.string).to eq(expected_output) end end context "with :max" do it "limits number of choices" do prompt = TTY::TestPrompt.new choices = %w(A B C) prompt.on(:keypress) { |e| prompt.trigger(:keyup) if e.value == "k" prompt.trigger(:keydown) if e.value == "j" } prompt.input << " " << "j" << " " << "j" << " " << "k" << " " << "j" << " " << "\r" prompt.input.rewind value = prompt.multi_select("What letter?", choices, max: 2, per_page: 100) expect(value).to eq(["A", "C"]) expected_output = output_helper("What letter?", choices, "A", [], init: true, max: 2, hint: "Use #{up_down} arrow keys, press Space to select and Enter to finish") + output_helper("What letter?", choices, "A", %w[A], max: 2) + output_helper("What letter?", choices, "B", %w[A], max: 2) + output_helper("What letter?", choices, "B", %w[A B], max: 2) + output_helper("What letter?", choices, "C", %w[A B], max: 2) + output_helper("What letter?", choices, "C", %w[A B], max: 2) + output_helper("What letter?", choices, "B", %w[A B], max: 2) + output_helper("What letter?", choices, "B", %w[A], max: 2) + output_helper("What letter?", choices, "C", %w[A], max: 2) + output_helper("What letter?", choices, "C", %w[A C], max: 2) + exit_message("What letter?", %w[A C]) expect(prompt.output.string).to eq(expected_output) end end end tty-prompt-0.21.0/spec/unit/multiline_spec.rb000066400000000000000000000041041363125043200212010ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, '#multiline' do it 'reads no lines' do prompt = TTY::TestPrompt.new prompt.input << "\C-d" prompt.input.rewind answer = prompt.multiline("Description?") expect(answer).to eq([]) expect(prompt.output.string).to eq([ "Description? \e[90m(Press CTRL-D or CTRL-Z to finish)\e[0m\n", "\e[2K\e[1G\e[1A\e[2K\e[1G", "Description? \n" ].join) end it "uses defualt when no input" do prompt = TTY::TestPrompt.new prompt.input << "\C-d" prompt.input.rewind answer = prompt.multiline("Description?", default: 'A super sweet prompt') expect(answer).to eq([]) expect(prompt.output.string).to eq([ "Description? \e[90m(Press CTRL-D or CTRL-Z to finish)\e[0m\n", "\e[2K\e[1G\e[1A\e[2K\e[1G", "Description? \e[32mA super sweet prompt\e[0m\n" ].join) end it "changes help text" do prompt = TTY::TestPrompt.new prompt.input << "\C-d" prompt.input.rewind answer = prompt.multiline("Description?") do |q| q.default 'A super sweet prompt' q.help '(Press thy ctrl-d to end)' end expect(answer).to eq([]) expect(prompt.output.string).to eq([ "Description? \e[90m(Press thy ctrl-d to end)\e[0m\n", "\e[2K\e[1G\e[1A\e[2K\e[1G", "Description? \e[32mA super sweet prompt\e[0m\n" ].join) end it 'reads multiple lines with empty lines' do prompt = TTY::TestPrompt.new prompt.input << "aa\n\nbb\n\n\ncc\C-d" prompt.input.rewind answer = prompt.multiline("Description?") expect(answer).to eq(["aa\n", "bb\n", "cc"]) expect(prompt.output.string).to eq([ "Description? \e[90m(Press CTRL-D or CTRL-Z to finish)\e[0m\n", "\e[2K\e[1Ga", "\e[2K\e[1Gaa", "\e[2K\e[1Gaa\n", "\e[2K\e[1G\n", "\e[2K\e[1Gb", "\e[2K\e[1Gbb", "\e[2K\e[1Gbb\n", "\e[2K\e[1G\n", "\e[2K\e[1G\n", "\e[2K\e[1Gc", "\e[2K\e[1Gcc", "\e[2K\e[1G\e[1A" * 6, "\e[2K\e[1G", "Description? \e[32maa ...\e[0m\n" ].join) end end tty-prompt-0.21.0/spec/unit/new_spec.rb000066400000000000000000000010041363125043200177640ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, '#new' do let(:env) { { "TTY_TEST" => true } } it "sets prefix" do prompt = described_class.new(prefix: "[?]", env: env) expect(prompt.prefix).to eq("[?]") end it "sets input stream" do prompt = described_class.new(input: :stream1, env: env) expect(prompt.input).to eq(:stream1) end it "sets output stream" do prompt = described_class.new(output: :stream2, env: env) expect(prompt.output).to eq(:stream2) end end tty-prompt-0.21.0/spec/unit/ok_spec.rb000066400000000000000000000003701363125043200176110ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, 'ok' do subject(:prompt) { TTY::TestPrompt.new } it 'prints text in green' do prompt.ok("All is fine") expect(prompt.output.string).to eq("\e[32mAll is fine\e[0m\n") end end tty-prompt-0.21.0/spec/unit/paginator_spec.rb000066400000000000000000000064021363125043200211660ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Paginator, '#paginate' do it "ignores per_page when equal items " do list = %w(a b c d) paginator = described_class.new(per_page: 4) expect(paginator.paginate(list, 1).to_a).to eq([ ['a',0],['b',1],['c',2],['d',3]]) end it "ignores per_page when less items " do list = %w(a b c d) paginator = described_class.new(per_page: 5) expect(paginator.paginate(list, 1).to_a).to eq([ ['a',0],['b',1],['c',2],['d',3]]) end it "paginates items matching per_page count" do list = %w(a b c d e f) paginator = described_class.new(per_page: 3) expect(paginator.paginate(list, 1).to_a).to eq([['a',0], ['b',1], ['c',2]]) expect(paginator.paginate(list, 2).to_a).to eq([['a',0], ['b',1], ['c',2]]) expect(paginator.paginate(list, 3).to_a).to eq([['b',1], ['c',2], ['d',3]]) expect(paginator.paginate(list, 4).to_a).to eq([['c',2], ['d',3], ['e',4]]) expect(paginator.paginate(list, 5).to_a).to eq([['d',3], ['e',4], ['f',5]]) expect(paginator.paginate(list, 6).to_a).to eq([['d',3], ['e',4], ['f',5]]) expect(paginator.paginate(list, 7).to_a).to eq([['d',3], ['e',4], ['f',5]]) end it "paginates items not matching per_page count" do list = %w(a b c d e f g) paginator = described_class.new(per_page: 3) expect(paginator.paginate(list, 1).to_a).to eq([['a',0], ['b',1], ['c',2]]) expect(paginator.paginate(list, 2).to_a).to eq([['a',0], ['b',1], ['c',2]]) expect(paginator.paginate(list, 3).to_a).to eq([['b',1], ['c',2], ['d',3]]) expect(paginator.paginate(list, 4).to_a).to eq([['c',2], ['d',3], ['e',4]]) expect(paginator.paginate(list, 5).to_a).to eq([['d',3], ['e',4], ['f',5]]) expect(paginator.paginate(list, 6).to_a).to eq([['e',4], ['f',5], ['g',6]]) expect(paginator.paginate(list, 7).to_a).to eq([['e',4], ['f',5], ['g',6]]) expect(paginator.paginate(list, 8).to_a).to eq([['e',4], ['f',5], ['g',6]]) end it "finds both start and end index for current selection" do list = %w(a b c d e f g) paginator = described_class.new(per_page: 3, default: 0) paginator.paginate(list, 2) expect(paginator.start_index).to eq(0) expect(paginator.end_index).to eq(2) paginator.paginate(list, 3) expect(paginator.start_index).to eq(1) expect(paginator.end_index).to eq(3) paginator.paginate(list, 4) expect(paginator.start_index).to eq(2) expect(paginator.end_index).to eq(4) paginator.paginate(list, 5) expect(paginator.start_index).to eq(3) expect(paginator.end_index).to eq(5) paginator.paginate(list, 7) expect(paginator.start_index).to eq(4) expect(paginator.end_index).to eq(6) paginator.paginate(list, 8) expect(paginator.start_index).to eq(4) expect(paginator.end_index).to eq(6) end it "starts with default selection" do list = %w(a b c d e f g) paginator = described_class.new(per_page: 3, default: 3) expect(paginator.paginate(list, 4).to_a).to eq([['d',3], ['e',4], ['f',5]]) end it "doesn't accept invalid pagination" do list = %w(a b c d e f g) paginator = described_class.new(per_page: 0) expect { paginator.paginate(list, 4) }.to raise_error(TTY::Prompt::InvalidArgument, /per_page must be > 0/) end end tty-prompt-0.21.0/spec/unit/question/000077500000000000000000000000001363125043200175105ustar00rootroot00000000000000tty-prompt-0.21.0/spec/unit/question/checks_spec.rb000066400000000000000000000055321363125043200223140ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question do subject(:prompt) { TTY::TestPrompt.new } it "passes range check" do question = described_class.new(prompt) question.in 1..10 result = TTY::Prompt::Question::Checks::CheckRange.call(question, 2) expect(result).to eq([2]) end it "fails range check" do question = described_class.new(prompt, messages: TTY::Prompt.messages) question.in 1..10 result = TTY::Prompt::Question::Checks::CheckRange.call(question, 11) expect(result).to eq([11, ["Value 11 must be within the range 1..10"]]) end it "fails range check" do question = described_class.new(prompt) question.in 1..10, 'Outside of range!' result = TTY::Prompt::Question::Checks::CheckRange.call(question, 11) expect(result).to eq([11, ['Outside of range!']]) end it "passes validation check" do question = described_class.new(prompt) question.validate(/\A\d{5}\Z/) result = TTY::Prompt::Question::Checks::CheckValidation.call(question, '12345') expect(result).to eq(['12345']) end it "fails validation check" do question = described_class.new(prompt, messages: TTY::Prompt.messages) question.validate(/\A\d{5}\Z/) result = TTY::Prompt::Question::Checks::CheckValidation.call(question, '123') expect(result).to eq(['123', ['Your answer is invalid (must match /\\A\\d{5}\\Z/)']]) end it "fails validation check with inlined custom message" do question = described_class.new(prompt) question.validate(/\A\w+@\w+\.\w+\Z/, 'Invalid email address') result = TTY::Prompt::Question::Checks::CheckValidation.call(question, 'piotr@com') expect(result).to eq(['piotr@com', ['Invalid email address']]) end it "fails validation check with custom message" do question = described_class.new(prompt) question.validate(/\A\w+@\w+\.\w+\Z/) question.messages[:valid?] = 'Invalid email address' result = TTY::Prompt::Question::Checks::CheckValidation.call(question, 'piotr@com') expect(result).to eq(['piotr@com', ['Invalid email address']]) end it "passes required check" do question = described_class.new(prompt) question.required true result = TTY::Prompt::Question::Checks::CheckRequired.call(question, 'Piotr') expect(result).to eq(['Piotr']) end it "fails required check" do question = described_class.new(prompt, messages: TTY::Prompt.messages) question.required true result = TTY::Prompt::Question::Checks::CheckRequired.call(question, nil) expect(result).to eq([nil, ['Value must be provided']]) end it "fails required check with custom message" do question = described_class.new(prompt) question.required true, 'Required input' result = TTY::Prompt::Question::Checks::CheckRequired.call(question, nil) expect(result).to eq([nil, ['Required input']]) end end tty-prompt-0.21.0/spec/unit/question/default_spec.rb000066400000000000000000000016341363125043200224770ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, '#default' do subject(:prompt) { TTY::TestPrompt.new } it 'uses default value' do name = 'Anonymous' prompt.input << "\n" prompt.input.rewind answer = prompt.ask('What is your name?', default: name) expect(answer).to eq(name) expect(prompt.output.string).to eq([ "What is your name? \e[90m(Anonymous)\e[0m ", "\e[2K\e[1GWhat is your name? \e[90m(Anonymous)\e[0m \n", "\e[1A\e[2K\e[1G", "What is your name? \e[32mAnonymous\e[0m\n" ].join) end it 'uses default value in block' do name = 'Anonymous' answer = prompt.ask('What is your name?') { |q| q.default(name) } expect(answer).to eq(name) expect(prompt.output.string).to eq([ "What is your name? \e[90m(Anonymous)\e[0m ", "\e[1A\e[2K\e[1G", "What is your name? \e[32mAnonymous\e[0m\n" ].join) end end tty-prompt-0.21.0/spec/unit/question/echo_spec.rb000066400000000000000000000022701363125043200217660ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, '#echo' do subject(:prompt) { TTY::TestPrompt.new } it 'asks with echo on' do prompt.input << "password" prompt.input.rewind answer = prompt.ask("What is your password?") { |q| q.echo(true) } expect(answer).to eql("password") expect(prompt.output.string).to eq([ "What is your password? ", "\e[2K\e[1GWhat is your password? p", "\e[2K\e[1GWhat is your password? pa", "\e[2K\e[1GWhat is your password? pas", "\e[2K\e[1GWhat is your password? pass", "\e[2K\e[1GWhat is your password? passw", "\e[2K\e[1GWhat is your password? passwo", "\e[2K\e[1GWhat is your password? passwor", "\e[2K\e[1GWhat is your password? password", "\e[1A\e[2K\e[1G", "What is your password? \e[32mpassword\e[0m\n" ].join) end it 'asks with echo off' do prompt.input << "password" prompt.input.rewind answer = prompt.ask("What is your password?", echo: false) expect(answer).to eql("password") expect(prompt.output.string).to eq([ "What is your password? ", "\e[1A\e[2K\e[1G", "What is your password? \n" ].join) end end tty-prompt-0.21.0/spec/unit/question/in_spec.rb000066400000000000000000000063701363125043200214630ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, '#in' do subject(:prompt) { TTY::TestPrompt.new } it "reads range from option" do prompt.input << '8' prompt.input.rewind answer = prompt.ask("How do you like it on scale 1-10?", in: '1-10') expect(answer).to eq('8') end it 'reads number within string range' do prompt.input << '8' prompt.input.rewind answer = prompt.ask("How do you like it on scale 1-10?") do |q| q.in('1-10') end expect(answer).to eq('8') expect(prompt.output.string).to eq([ "How do you like it on scale 1-10? ", "\e[2K\e[1GHow do you like it on scale 1-10? 8", "\e[1A\e[2K\e[1G", "How do you like it on scale 1-10? \e[32m8\e[0m\n", ].join) end it 'reads number within digit range' do prompt.input << '8.1' prompt.input.rewind answer = prompt.ask("How do you like it on scale 1-10?") do |q| q.in(1.0..11.5) end expect(answer).to eq('8.1') expect(prompt.output.string).to eq([ "How do you like it on scale 1-10? ", "\e[2K\e[1GHow do you like it on scale 1-10? 8", "\e[2K\e[1GHow do you like it on scale 1-10? 8.", "\e[2K\e[1GHow do you like it on scale 1-10? 8.1", "\e[1A\e[2K\e[1G", "How do you like it on scale 1-10? \e[32m8.1\e[0m\n", ].join) end it 'reads letters within range' do prompt.input << 'E' prompt.input.rewind answer = prompt.ask("Your favourite vitamin? (A-K)") do |q| q.in('A-K') end expect(answer).to eq('E') expect(prompt.output.string).to eq([ "Your favourite vitamin? (A-K) ", "\e[2K\e[1GYour favourite vitamin? (A-K) E", "\e[1A\e[2K\e[1G", "Your favourite vitamin? (A-K) \e[32mE\e[0m\n" ].join) end it "provides default error message when wrong input" do prompt.input << "A\n2\n" prompt.input.rewind answer = prompt.ask("How spicy on scale? (1-5)", in: '1-5') expect(answer).to eq('2') expect(prompt.output.string).to eq([ "How spicy on scale? (1-5) ", "\e[2K\e[1GHow spicy on scale? (1-5) A", "\e[2K\e[1GHow spicy on scale? (1-5) A\n", "\e[31m>>\e[0m Value A must be within the range 1..5\e[1A", "\e[2K\e[1G", "How spicy on scale? (1-5) ", "\e[2K\e[1GHow spicy on scale? (1-5) 2", "\e[2K\e[1GHow spicy on scale? (1-5) 2\n", "\e[2K\e[1G", "\e[1A\e[2K\e[1G", "How spicy on scale? (1-5) \e[32m2\e[0m\n" ].join) end it "overwrites default error message when wrong input" do prompt.input << "A\n2\n" prompt.input.rewind answer = prompt.ask("How spicy on scale? (1-5)") do |q| q.in '1-5' q.messages[:range?] = 'Ohh dear what is this %{value} doing in %{in}?' end expect(answer).to eq('2') expect(prompt.output.string).to eq([ "How spicy on scale? (1-5) ", "\e[2K\e[1GHow spicy on scale? (1-5) A", "\e[2K\e[1GHow spicy on scale? (1-5) A\n", "\e[31m>>\e[0m Ohh dear what is this A doing in 1..5?\e[1A", "\e[2K\e[1G", "How spicy on scale? (1-5) ", "\e[2K\e[1GHow spicy on scale? (1-5) 2", "\e[2K\e[1GHow spicy on scale? (1-5) 2\n", "\e[2K\e[1G", "\e[1A\e[2K\e[1G", "How spicy on scale? (1-5) \e[32m2\e[0m\n" ].join) end end tty-prompt-0.21.0/spec/unit/question/initialize_spec.rb000066400000000000000000000005131363125043200232070ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, '#initialize' do subject(:question) { described_class.new(TTY::TestPrompt.new)} it { expect(question.echo).to eq(true) } it { expect(question.modifier).to eq([]) } it { expect(question.validation).to eq(TTY::Prompt::Question::UndefinedSetting) } end tty-prompt-0.21.0/spec/unit/question/modifier/000077500000000000000000000000001363125043200213065ustar00rootroot00000000000000tty-prompt-0.21.0/spec/unit/question/modifier/apply_to_spec.rb000066400000000000000000000013651363125043200245010ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question::Modifier, '#apply_to' do let(:string) { "text to be modified"} it "doesn't apply modifiers" do modifier = described_class.new([]) expect(modifier.apply_to(string)).to eq(string) end it 'combines whitespace & letter case modifications' do modifiers = [:collapse, :capitalize] modifier = described_class.new(modifiers) modified = modifier.apply_to(string) expect(modified).to eq('Text to be modified') end it 'combines letter case & whitespace modifications' do modifiers = [:up, :collapse] modifier = described_class.new(modifiers) modified = modifier.apply_to(string) expect(modified).to eq('TEXT TO BE MODIFIED') end end tty-prompt-0.21.0/spec/unit/question/modifier/letter_case_spec.rb000066400000000000000000000021151363125043200251360ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question::Modifier, '#letter_case' do context "string" do let(:string) { 'text to modify' } it "changes to uppercase" do modified = described_class.letter_case(:up, string) expect(modified).to eq('TEXT TO MODIFY') end it "changes to lower case" do modified = described_class.letter_case(:down, string) expect(modified).to eq('text to modify') end it "capitalizes text" do modified = described_class.letter_case(:capitalize, string) expect(modified).to eq('Text to modify') end end context "nil (empty user input)" do let(:string) { nil } example "up returns nil" do modified = described_class.letter_case(:up, string) expect(modified).to be_nil end example "down returns nil" do modified = described_class.letter_case(:down, string) expect(modified).to be_nil end example "capitalize returns nil" do modified = described_class.letter_case(:capitalize, string) expect(modified).to be_nil end end end tty-prompt-0.21.0/spec/unit/question/modifier/whitespace_spec.rb000066400000000000000000000026251363125043200250060ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question::Modifier, '#whitespace' do context "string with whitespaces" do let(:string) { " text\t \n to\t modify\r\n" } it "trims whitespace" do modified = described_class.whitespace(:trim, string) expect(modified).to eq("text\t \n to\t modify") end it "chomps whitespace" do modified = described_class.whitespace(:chomp, string) expect(modified).to eq(" text\t \n to\t modify") end it "collapses text" do modified = described_class.whitespace(:collapse, string) expect(modified).to eq(" text to modify ") end it "removes whitespace" do modified = described_class.whitespace(:remove, string) expect(modified).to eq("texttomodify") end end context "nil (empty user input)" do let(:string) { nil } example "trim returns nil" do modified = described_class.whitespace(:trim, string) expect(modified).to be_nil end example "chomp returns nil" do modified = described_class.whitespace(:chomp, string) expect(modified).to be_nil end example "collapse returns nil" do modified = described_class.whitespace(:collapse, string) expect(modified).to be_nil end example "remove returns nil" do modified = described_class.whitespace(:remove, string) expect(modified).to be_nil end end end tty-prompt-0.21.0/spec/unit/question/modify_spec.rb000066400000000000000000000024321363125043200223370ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, '#modify' do subject(:prompt) { TTY::TestPrompt.new } it 'preserves answer for unkown modification' do prompt.input << 'piotr' prompt.input.rewind answer = prompt.ask("What is your name?") { |q| q.modify(:none) } expect(answer).to eq('piotr') end it 'converts to upper case' do prompt.input << 'piotr' prompt.input.rewind answer = prompt.ask("What is your name?") { |q| q.modify(:upcase) } expect(answer).to eq('PIOTR') end it 'trims whitespace' do prompt.input << " Some white\t space\t \there! \n" prompt.input.rewind answer = prompt.ask('Enter some text: ') { |q| q.modify(:trim) } expect(answer).to eq("Some white\t space\t \there!") end it 'collapses whitespace' do prompt.input << " Some white\t space\t \there! \n" prompt.input.rewind answer = prompt.ask('Enter some text: ') { |q| q.modify(:collapse) } expect(answer).to eq(' Some white space here! ') end it 'strips and collapses whitespace' do prompt.input << " Some white\t space\t \there! \n" prompt.input.rewind answer = prompt.ask('Enter some text: ') { |q| q.modify(:strip, :collapse) } expect(answer).to eq('Some white space here!') end end tty-prompt-0.21.0/spec/unit/question/required_spec.rb000066400000000000000000000055331363125043200226750ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, '#required' do subject(:prompt) { TTY::TestPrompt.new } it 'requires value to be present' do prompt.input << "Piotr" prompt.input.rewind prompt.ask('What is your name?') { |q| q.required(true) } expect(prompt.output.string).to eq([ "What is your name? ", "\e[2K\e[1GWhat is your name? P", "\e[2K\e[1GWhat is your name? Pi", "\e[2K\e[1GWhat is your name? Pio", "\e[2K\e[1GWhat is your name? Piot", "\e[2K\e[1GWhat is your name? Piotr", "\e[1A\e[2K\e[1G", "What is your name? \e[32mPiotr\e[0m\n" ].join) end it 'requires value to be present with option' do prompt.input << " \nPiotr" prompt.input.rewind prompt.ask('What is your name?', required: true) expect(prompt.output.string).to eq([ "What is your name? ", "\e[2K\e[1GWhat is your name? ", "\e[2K\e[1GWhat is your name? ", "\e[2K\e[1GWhat is your name? \n", "\e[31m>>\e[0m Value must be provided\e[1A", "\e[2K\e[1G", "What is your name? ", "\e[2K\e[1GWhat is your name? P", "\e[2K\e[1GWhat is your name? Pi", "\e[2K\e[1GWhat is your name? Pio", "\e[2K\e[1GWhat is your name? Piot", "\e[2K\e[1GWhat is your name? Piotr", "\e[2K\e[1G", "\e[1A\e[2K\e[1G", "What is your name? \e[32mPiotr\e[0m\n" ].join) end it "doesn't require value to be present" do prompt.input << '' prompt.input.rewind answer = prompt.ask('What is your name?') { |q| q.required(false) } expect(answer).to be_nil end it "uses required in validation check" do prompt.input << " \nexists\ntest\n" prompt.input.rewind answer = prompt.ask('File name?') do |q| q.required(true) q.validate { |v| !(v =~ /exists/) } q.messages[:required?] = 'File name must not be empty!' q.messages[:valid?] = 'File already exists!' end expect(answer).to eq('test') expect(prompt.output.string).to eq([ "File name? ", "\e[2K\e[1GFile name? ", "\e[2K\e[1GFile name? ", "\e[2K\e[1GFile name? \n", "\e[31m>>\e[0m File name must not be empty!", "\e[1A\e[2K\e[1G", "File name? ", "\e[2K\e[1GFile name? e", "\e[2K\e[1GFile name? ex", "\e[2K\e[1GFile name? exi", "\e[2K\e[1GFile name? exis", "\e[2K\e[1GFile name? exist", "\e[2K\e[1GFile name? exists", "\e[2K\e[1GFile name? exists\n", "\e[31m>>\e[0m File already exists!", "\e[1A\e[2K\e[1G", "File name? ", "\e[2K\e[1GFile name? t", "\e[2K\e[1GFile name? te", "\e[2K\e[1GFile name? tes", "\e[2K\e[1GFile name? test", "\e[2K\e[1GFile name? test\n", "\e[2K\e[1G", "\e[1A\e[2K\e[1G", "File name? \e[32mtest\e[0m\n", ].join) expect(answer).to eq('test') end end tty-prompt-0.21.0/spec/unit/question/validate_spec.rb000066400000000000000000000066251363125043200226510ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, '#validate' do subject(:prompt) { TTY::TestPrompt.new } it 'validates input with regex' do prompt.input << 'p.m' prompt.input.rewind answer = prompt.ask('What is your username?') do |q| q.validate(/^[^\.]+\.[^\.]+/) end expect(answer).to eq('p.m') expect(prompt.output.string).to eq([ "What is your username? ", "\e[2K\e[1GWhat is your username? p", "\e[2K\e[1GWhat is your username? p.", "\e[2K\e[1GWhat is your username? p.m", "\e[1A\e[2K\e[1G", "What is your username? \e[32mp.m\e[0m\n" ].join) end it 'validates input with proc' do prompt.input << 'piotr.murach' prompt.input.rewind answer = prompt.ask('What is your username?') do |q| q.validate { |input| input =~ /^[^\.]+\.[^\.]+/ } end expect(answer).to eq('piotr.murach') end it 'understands custom validation like :email' do prompt.input << 'piotr@example.com' prompt.input.rewind answer = prompt.ask('What is your email?') do |q| q.validate :email end expect(answer).to eq('piotr@example.com') end it "provides default error message for wrong input" do prompt.input << "wrong\np@m.com\n" prompt.input.rewind answer = prompt.ask('What is your email?') do |q| q.validate :email end expect(answer).to eq('p@m.com') expect(prompt.output.string).to eq([ "What is your email? ", "\e[2K\e[1GWhat is your email? w", "\e[2K\e[1GWhat is your email? wr", "\e[2K\e[1GWhat is your email? wro", "\e[2K\e[1GWhat is your email? wron", "\e[2K\e[1GWhat is your email? wrong", "\e[2K\e[1GWhat is your email? wrong\n", "\e[31m>>\e[0m Your answer is invalid (must match :email)\e[1A", "\e[2K\e[1G", "What is your email? ", "\e[2K\e[1GWhat is your email? p", "\e[2K\e[1GWhat is your email? p@", "\e[2K\e[1GWhat is your email? p@m", "\e[2K\e[1GWhat is your email? p@m.", "\e[2K\e[1GWhat is your email? p@m.c", "\e[2K\e[1GWhat is your email? p@m.co", "\e[2K\e[1GWhat is your email? p@m.com", "\e[2K\e[1GWhat is your email? p@m.com\n", "\e[2K\e[1G", "\e[1A\e[2K\e[1G", "What is your email? \e[32mp@m.com\e[0m\n" ].join) end it "provides custom error message for wrong input" do prompt.input << "wrong\np@m.com" prompt.input.rewind answer = prompt.ask('What is your email?') do |q| q.validate :email q.messages[:valid?] = 'Not an email!' end expect(answer).to eq('p@m.com') expect(prompt.output.string).to eq([ "What is your email? ", "\e[2K\e[1GWhat is your email? w", "\e[2K\e[1GWhat is your email? wr", "\e[2K\e[1GWhat is your email? wro", "\e[2K\e[1GWhat is your email? wron", "\e[2K\e[1GWhat is your email? wrong", "\e[2K\e[1GWhat is your email? wrong\n", "\e[31m>>\e[0m Not an email!\e[1A", "\e[2K\e[1G", "What is your email? ", "\e[2K\e[1GWhat is your email? p", "\e[2K\e[1GWhat is your email? p@", "\e[2K\e[1GWhat is your email? p@m", "\e[2K\e[1GWhat is your email? p@m.", "\e[2K\e[1GWhat is your email? p@m.c", "\e[2K\e[1GWhat is your email? p@m.co", "\e[2K\e[1GWhat is your email? p@m.com", "\e[2K\e[1G", "\e[1A\e[2K\e[1G", "What is your email? \e[32mp@m.com\e[0m\n" ].join) end end tty-prompt-0.21.0/spec/unit/question/validation/000077500000000000000000000000001363125043200216425ustar00rootroot00000000000000tty-prompt-0.21.0/spec/unit/question/validation/call_spec.rb000066400000000000000000000016361363125043200241220ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question::Validation, '#call' do let(:pattern) { /^[^\.]+\.[^\.]+/ } it "validates nil input" do validation = described_class.new(pattern) expect(validation.(nil)).to eq(false) end it "validates successfully when the value matches pattern" do validation = described_class.new(pattern) expect(validation.('piotr.murach')).to eq(true) end it "validates with a proc" do pat = proc { |input| !pattern.match(input).nil? } validation = described_class.new(pat) expect(validation.call('piotr.murach')).to eq(true) end it "validates with custom name" do validation = described_class.new(:email) expect(validation.call('piotr@example.com')).to eq(true) end it "fails validation when not maching pattern" do validation = described_class.new(pattern) expect(validation.('piotrmurach')).to eq(false) end end tty-prompt-0.21.0/spec/unit/question/validation/coerce_spec.rb000066400000000000000000000014531363125043200244440ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question::Validation, '#coerce' do let(:instance) { described_class.new } it "coerces lambda into proc" do pattern = lambda { "^[^\.]+\.[^\.]+" } validation = described_class.new(pattern) expect(validation.pattern).to be_a(Proc) end it "doesn't coerce symbols" do pattern = :email validation =described_class.new(pattern) expect(validation.pattern).to eq(:email) end it "coerces into regex" do pattern = /^[^\.]+\.[^\.]+/ validation = described_class.new(pattern) expect(validation.pattern).to be_a(Regexp) end it "fails to coerce pattern into validation" do pattern = Object.new expect { described_class.new(pattern) }.to raise_error(TTY::Prompt::ValidationCoercion) end end tty-prompt-0.21.0/spec/unit/result_spec.rb000066400000000000000000000020661363125043200205220ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Result do it "checks value to be invalid" do question = double(:question) result = TTY::Prompt::Result.new(question, nil) answer = result.with { |quest, value| if value.nil? [value, ["`#{value}` provided cannot be empty"]] else value end } expect(answer).to be_a(TTY::Prompt::Result::Failure) expect(answer.success?).to eq(false) expect(answer.errors).to eq(["`` provided cannot be empty"]) end it "checks value to be valid" do question = double(:question) result = TTY::Prompt::Result.new(question, 'Piotr') CheckRequired = Class.new do def self.call(quest, value) if value.nil? [value, ["`#{value}` provided cannot be empty"]] else value end end end answer = result.with(CheckRequired) expect(answer).to be_a(TTY::Prompt::Result::Success) expect(answer.success?).to eq(true) expect(answer.value).to eq('Piotr') expect(answer.errors).to eq([]) end end tty-prompt-0.21.0/spec/unit/say_spec.rb000066400000000000000000000034231363125043200177760ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, '#say' do subject(:prompt) { TTY::TestPrompt.new } it 'prints an empty message' do prompt.say('') expect(prompt.output.string).to eq('') end context 'with new line' do it 'prints a message with newline' do prompt.say("Hell yeah!\n") expect(prompt.output.string).to eq("Hell yeah!\n") end it 'prints a message with implicit newline' do prompt.say("Hell yeah!\n") expect(prompt.output.string).to eq("Hell yeah!\n") end it 'prints a message with newline within text' do prompt.say("Hell\n yeah!") expect(prompt.output.string).to eq("Hell\n yeah!\n") end it 'prints a message with newline within text and blank space' do prompt.say("Hell\n yeah! ") expect(prompt.output.string).to eq("Hell\n yeah! ") end it 'prints a message without newline' do prompt.say("Hell yeah!", newline: false) expect(prompt.output.string).to eq("Hell yeah!") end end context 'with tab or space' do it 'prints ' do prompt.say("Hell yeah!\t") expect(prompt.output.string).to eq("Hell yeah!\t") end end context 'with color' do it 'prints message with ansi color' do prompt.say('Hell yeah!', color: :green) expect(prompt.output.string).to eq("\e[32mHell yeah!\e[0m\n") end it 'prints message with ansi color without newline' do prompt.say('Hell yeah! ', color: :green) expect(prompt.output.string).to eq("\e[32mHell yeah! \e[0m") end end context 'without color' do it 'prints message without ansi' do prompt = TTY::TestPrompt.new(enable_color: false) prompt.say('Hell yeah!', color: :green) expect(prompt.output.string).to eq("Hell yeah!\n") end end end tty-prompt-0.21.0/spec/unit/select_spec.rb000066400000000000000000001005771363125043200204710ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, '#select' do subject(:prompt) { TTY::TestPrompt.new } let(:symbols) { TTY::Prompt::Symbols.symbols } let(:up_down) { "#{symbols[:arrow_up]}/#{symbols[:arrow_down]}" } let(:left_right) { "#{symbols[:arrow_left]}/#{symbols[:arrow_right]}"} def output_helper(prompt, choices, active, options = {}) raise ":init requires :hint" if options[:init] && options[:hint].nil? hint = options[:hint] init = options.fetch(:init, false) enum = options[:enum] out = [] out << "\e[?25l" if init out << prompt << " " out << "\e[90m(#{hint})\e[0m" if hint out << "\n" out << choices.map.with_index do |c, i| name = c.is_a?(Hash) ? c[:name] : c disabled = c.is_a?(Hash) ? c[:disabled] : false num = (i + 1).to_s + enum if enum if disabled "\e[31m#{symbols[:cross]}\e[0m #{num}#{name} #{disabled}" elsif name == active "\e[32m#{symbols[:marker]} #{num}#{name}\e[0m" else " #{num}#{name}" end end.join("\n") out << "\e[2K\e[1G\e[1A" * choices.count out << "\e[2K\e[1G" out << "\e[1A\e[2K\e[1G" if choices.empty? out.join end def exit_message(prompt, choice) "#{prompt} \e[32m#{choice}\e[0m\n\e[?25h" end # Ensure a wide prompt on CI before { allow(TTY::Screen).to receive(:width).and_return(200) } it "selects by default first option" do choices = %i(Large Medium Small) prompt.input << "\r" prompt.input.rewind expect(prompt.select('What size?', choices)).to eq(:Large) expected_output = [ "\e[?25lWhat size? \e[90m(Use #{up_down} arrow keys, press Enter to select)\e[0m\n", "\e[32m#{symbols[:marker]} Large\e[0m\n", " Medium\n", " Small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mLarge\e[0m\n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end it "allows navigation using events without errors" do choices = %w(Large Medium Small) prompt.input << "j" << "\r" prompt.input.rewind prompt.on(:keypress) do |event| prompt.trigger(:keydown) if event.value == "j" end expect { prompt.select('What size?', choices) }.not_to output.to_stderr expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[90m(Use #{up_down} arrow keys, press Enter to select)\e[0m\n", "\e[32m#{symbols[:marker]} Large\e[0m\n", " Medium\n", " Small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \n", " Large\n", "\e[32m#{symbols[:marker]} Medium\e[0m\n", " Small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mMedium\e[0m\n\e[?25h" ].join) end it "sets choice name and value" do choices = {large: 1, medium: 2, small: 3} prompt.input << " " prompt.input.rewind expect(prompt.select('What size?', choices, default: 1)).to eq(1) expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[90m(Use #{up_down} arrow keys, press Enter to select)\e[0m\n", "\e[32m#{symbols[:marker]} large\e[0m\n", " medium\n", " small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mlarge\e[0m\n\e[?25h" ].join) end it "sets choice name through DSL" do prompt.input << " " prompt.input.rewind value = prompt.select('What size?') do |menu| menu.symbols marker: '>' menu.choice "Large" menu.choice "Medium" menu.choice "Small" end expect(value).to eq('Large') expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[90m(Use #{up_down} arrow keys, press Enter to select)\e[0m\n", "\e[32m> Large\e[0m\n", " Medium\n", " Small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mLarge\e[0m\n\e[?25h" ].join) end it "sets choice name & value through DSL" do prompt = TTY::TestPrompt.new(symbols: {marker: '>'}) prompt.input << " " prompt.input.rewind value = prompt.select('What size?') do |menu| menu.choice :large, 1 menu.choice :medium, 2 menu.choice :small, 3 end expect(value).to eq(1) expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[90m(Use #{up_down} arrow keys, press Enter to select)\e[0m\n", "\e[32m> large\e[0m\n", " medium\n", " small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mlarge\e[0m\n\e[?25h" ].join) end it "sets choices and single choice through DSL" do prompt.input << " " prompt.input.rewind value = prompt.select('What size?') do |menu| menu.choice 'Large' menu.choices %w(Medium Small) end expect(value).to eq('Large') expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[90m(Use #{up_down} arrow keys, press Enter to select)\e[0m\n", "\e[32m#{symbols[:marker]} Large\e[0m\n", " Medium\n", " Small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mLarge\e[0m\n\e[?25h" ].join) end it "sets choice name & value through DSL" do prompt.input << " " prompt.input.rewind value = prompt.select('What size?') do |menu| menu.default 2 menu.enum '.' menu.choice :large, 1 menu.choice :medium, 2 menu.choice :small, 3 end expect(value).to eq(2) expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[90m(Use #{up_down} arrow or number (1-3) keys, press Enter to select)\e[0m\n", " 1. large\n", "\e[32m#{symbols[:marker]} 2. medium\e[0m\n", " 3. small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mmedium\e[0m\n\e[?25h" ].join) end it "sets choice value to proc and executes it" do prompt.input << " " prompt.input.rewind value = prompt.select('What size?', default: 2, enum: ')') do |menu| menu.choice :large, 1 menu.choice :medium do 'Good choice!' end menu.choice :small, 3 end expect(value).to eq('Good choice!') expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[90m(Use #{up_down} arrow or number (1-3) keys, press Enter to select)\e[0m\n", " 1) large\n", "\e[32m#{symbols[:marker]} 2) medium\e[0m\n", " 3) small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mmedium\e[0m\n\e[?25h" ].join) end it "sets default option through hash syntax" do choices = %w(Large Medium Small) prompt.input << " " prompt.input.rewind expect(prompt.select('What size?', choices, default: 2, enum: '.')).to eq('Medium') expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[90m(Use #{up_down} arrow or number (1-3) keys, press Enter to select)\e[0m\n", " 1. Large\n", "\e[32m#{symbols[:marker]} 2. Medium\e[0m\n", " 3. Small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mMedium\e[0m\n\e[?25h" ].join) end it "changes selected item color & marker" do choices = %w(Large Medium Small) prompt = TTY::TestPrompt.new(symbols: {marker: '>'}) prompt.input << " " prompt.input.rewind options = {active_color: :blue, help_color: :red, symbols: {marker: '>' }} value = prompt.select('What size?', choices, **options) expect(value).to eq('Large') expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[31m(Use #{up_down} arrow keys, press Enter to select)\e[0m\n", "\e[34m> Large\e[0m\n", " Medium\n", " Small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[34mLarge\e[0m\n\e[?25h" ].join) end it "changes help text" do choices = %w(Large Medium Small) prompt.input << " " prompt.input.rewind value = prompt.select('What size?', choices, help: "(Bash keyboard)") expect(value).to eq('Large') expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[90m(Bash keyboard)\e[0m\n", "\e[32m#{symbols[:marker]} Large\e[0m\n", " Medium\n", " Small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mLarge\e[0m\n\e[?25h" ].join) end it "changes help text through DSL" do choices = %w(Large Medium Small) prompt.input << " " prompt.input.rewind value = prompt.select('What size?') do |menu| menu.help "(Bash keyboard)" menu.choices choices end expect(value).to eq('Large') expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[90m(Bash keyboard)\e[0m\n", "\e[32m#{symbols[:marker]} Large\e[0m\n", " Medium\n", " Small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mLarge\e[0m\n\e[?25h" ].join) end it "sets prompt prefix" do prompt = TTY::TestPrompt.new(prefix: '[?] ') choices = %w(Large Medium Small) prompt.input << "\r" prompt.input.rewind expect(prompt.select('What size?', choices)).to eq('Large') expect(prompt.output.string).to eq([ "\e[?25l[?] What size? \e[90m(Use #{up_down} arrow keys, press Enter to select)\e[0m\n", "\e[32m#{symbols[:marker]} Large\e[0m\n", " Medium\n", " Small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "[?] What size? \e[32mLarge\e[0m\n\e[?25h" ].join) end context "when paginated" do it "paginates long selections" do choices = %w(A B C D E F G H) prompt.input << "\r" prompt.input.rewind answer = prompt.select("What letter?", choices, per_page: 3, default: 4) expect(answer).to eq('D') expected_output = [ "\e[?25lWhat letter? \e[90m(Use #{up_down} and #{left_right} arrow keys, press Enter to select)\e[0m\n", "\e[32m#{symbols[:marker]} D\e[0m\n", " E\n", " F", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What letter? \e[32mD\e[0m\n\e[?25h", ].join expect(prompt.output.string).to eq(expected_output) end it "paginates choices as hash object" do prompt = TTY::TestPrompt.new choices = {A: 1, B: 2, C: 3, D: 4, E: 5, F: 6, G: 7, H: 8} prompt.input << "\r" prompt.input.rewind answer = prompt.select("What letter?", choices, per_page: 3, default: 4) expect(answer).to eq(4) expected_output = [ "\e[?25lWhat letter? \e[90m(Use #{up_down} and #{left_right} arrow keys, press Enter to select)\e[0m\n", "\e[32m#{symbols[:marker]} D\e[0m\n", " E\n", " F", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What letter? \e[32mD\e[0m\n\e[?25h", ].join expect(prompt.output.string).to eq(expected_output) end it "paginates long selections through DSL" do prompt = TTY::TestPrompt.new choices = %w(A B C D E F G H) prompt.input << "\r" prompt.input.rewind answer = prompt.select("What letter?") do |menu| menu.per_page 3 menu.default 4 menu.choices choices end expect(answer).to eq('D') expected_output = [ "\e[?25lWhat letter? \e[90m(Use #{up_down} and #{left_right} arrow keys, press Enter to select)\e[0m\n", "\e[32m#{symbols[:marker]} D\e[0m\n", " E\n", " F", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What letter? \e[32mD\e[0m\n\e[?25h", ].join expect(prompt.output.string).to eq(expected_output) end it "navigates evenly paged output with right arrow until end of selection" do prompt = TTY::TestPrompt.new choices = ('1'..'12').to_a prompt.on(:keypress) { |e| prompt.trigger(:keyright) if e.value == "l" } prompt.input << "l" << "l" << "l" << "\r" prompt.input.rewind answer = prompt.select("What number?", choices, per_page: 4) expect(answer).to eq('9') expected_output = [ output_helper('What number?', choices[0..3], "1", init: true, hint: "Use #{up_down} and #{left_right} arrow keys, press Enter to select"), output_helper('What number?', choices[4..7], "5"), output_helper('What number?', choices[8..11], "9"), output_helper('What number?', choices[8..11], "9"), "What number? \e[32m9\e[0m\n\e[?25h", ].join expect(prompt.output.string).to eq(expected_output) end it "navigates unevenly paged output with right arrow until the end of selection" do prompt = TTY::TestPrompt.new choices = ('1'..'10').to_a prompt.on(:keypress) { |e| prompt.trigger(:keyright) if e.value == "l" } prompt.input << "l" << "l" << "l" << "\r" prompt.input.rewind answer = prompt.select("What number?", choices, default: 4, per_page: 4) expect(answer).to eq('10') expected_output = [ output_helper('What number?', choices[3..6], "4", init: true, hint: "Use #{up_down} and #{left_right} arrow keys, press Enter to select"), output_helper('What number?', choices[4..7], "8"), output_helper('What number?', choices[8..9], "10"), output_helper('What number?', choices[8..9], "10"), "What number? \e[32m10\e[0m\n\e[?25h", ].join expect(prompt.output.string).to eq(expected_output) end it "navigates left and right" do prompt = TTY::TestPrompt.new choices = ('1'..'10').to_a prompt.on(:keypress) { |e| prompt.trigger(:keyright) if e.value == "l" prompt.trigger(:keyleft) if e.value == "h" } prompt.input << "l" << "l" << "h" << "\r" prompt.input.rewind answer = prompt.select("What number?", choices, default: 2, per_page: 4) expect(answer).to eq('6') expected_output = [ output_helper('What number?', choices[0..3], "2", init: true, hint: "Use #{up_down} and #{left_right} arrow keys, press Enter to select"), output_helper('What number?', choices[4..7], "6"), output_helper('What number?', choices[8..9], "10"), output_helper('What number?', choices[4..7], "6"), "What number? \e[32m6\e[0m\n\e[?25h", ].join expect(prompt.output.string).to eq(expected_output) end it "combines up/down navigation with left/right" do prompt = TTY::TestPrompt.new choices = ('1'..'11').to_a prompt.on(:keypress) { |e| prompt.trigger(:keyup) if e.value == "k" prompt.trigger(:keydown) if e.value == "j" prompt.trigger(:keyright) if e.value == "l" prompt.trigger(:keyleft) if e.value == "h" } prompt.input << "j" << "l" << "k" << "k" << "h" << "\r" prompt.input.rewind answer = prompt.select("What number?", choices, default: 2, per_page: 4) expect(answer).to eq('1') expected_output = [ output_helper('What number?', choices[0..3], "2", init: true, hint: "Use #{up_down} and #{left_right} arrow keys, press Enter to select"), output_helper('What number?', choices[0..3], "3"), output_helper('What number?', choices[4..7], "7"), output_helper('What number?', choices[4..7], "6"), output_helper('What number?', choices[3..6], "5"), output_helper('What number?', choices[0..3], "1"), "What number? \e[32m1\e[0m\n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end it "navigates pages up/down with disabled items" do prompt = TTY::TestPrompt.new prompt.on(:keypress) { |e| prompt.trigger(:keyup) if e.value == "k" prompt.trigger(:keydown) if e.value == "j" } choices = [ '1', {name: '2', disabled: 'out'}, '3', {name: '4', disabled: 'out'}, '5', {name: '6', disabled: 'out'}, {name: '7', disabled: 'out'}, '8', '9', {name: '10', disabled: 'out'}, ] prompt.input << "j" << "j" << "j" << "j" << "\r" prompt.input.rewind answer = prompt.select("What number?", choices, per_page: 4) expect(answer).to eq('9') expected_output = [ output_helper('What number?', choices[0..3], "1", init: true, hint: "Use #{up_down} and #{left_right} arrow keys, press Enter to select"), output_helper('What number?', choices[0..3], "3"), output_helper('What number?', choices[2..5], "5"), output_helper('What number?', choices[5..8], "8"), output_helper('What number?', choices[6..9], "9"), "What number? \e[32m9\e[0m\n\e[?25h" ].join('') expect(prompt.output.string).to eq(expected_output) end it "navigates pages left/right with disabled items" do prompt = TTY::TestPrompt.new prompt.on(:keypress) { |e| prompt.trigger(:keyright) if e.value == "l" prompt.trigger(:keyleft) if e.value == "h" } choices = [ {name: '1', disabled: 'out'}, '2', {name: '3', disabled: 'out'}, '4', '5', {name: '6', disabled: 'out'}, '7', '8', '9', {name: '10', disabled: 'out'} ] prompt.input << "l" << "l" << "l" << "h" << "h" << "h" << "\r" prompt.input.rewind answer = prompt.select("What number?", choices, per_page: 4) expect(answer).to eq('2') expected_output = [ output_helper('What number?', choices[0..3], "2", init: true, hint: "Use #{up_down} and #{left_right} arrow keys, press Enter to select"), output_helper('What number?', choices[4..7], "7"), output_helper('What number?', choices[8..9], "9"), output_helper('What number?', choices[8..9], "9"), output_helper('What number?', choices[4..7], "5"), output_helper('What number?', choices[0..3], "2"), output_helper('What number?', choices[0..3], "2"), "What number? \e[32m2\e[0m\n\e[?25h" ].join('') expect(prompt.output.string).to eq(expected_output) end end context 'with :cycle option' do it "doesn't cycle by default" do prompt = TTY::TestPrompt.new choices = %w(A B C) prompt.on(:keypress) { |e| prompt.trigger(:keydown) if e.value == "j" } prompt.input << "j" << "j" << "j" << "\r" prompt.input.rewind value = prompt.select("What letter?", choices) expect(value).to eq("C") expected_output = [ output_helper("What letter?", choices, "A", init: true, hint: "Use #{up_down} arrow keys, press Enter to select"), output_helper("What letter?", choices, "B"), output_helper("What letter?", choices, "C"), output_helper("What letter?", choices, "C"), "What letter? \e[32mC\e[0m\n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end it "cycles around when configured to do so" do prompt = TTY::TestPrompt.new choices = %w(A B C) prompt.on(:keypress) { |e| prompt.trigger(:keydown) if e.value == "j" } prompt.input << "j" << "j" << "j" << "\r" prompt.input.rewind answer = prompt.select("What letter?", choices, cycle: true) expect(answer).to eq("A") expected_output = [ output_helper("What letter?", choices, "A", init: true, hint: "Use #{up_down} arrow keys, press Enter to select"), output_helper("What letter?", choices, "B"), output_helper("What letter?", choices, "C"), output_helper("What letter?", choices, "A"), "What letter? \e[32mA\e[0m\n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end it "cycles around disabled items" do prompt = TTY::TestPrompt.new choices = [ {name: 'A', disabled: '(out)'}, {name: 'B'}, {name: 'C', disabled: '(out)'}, {name: 'D'}, {name: 'E', disabled: '(out)'}, ] prompt.on(:keypress) { |e| prompt.trigger(:keydown) if e.value == "j" } prompt.input << "j" << "j" << "j" << "\r" prompt.input.rewind value = prompt.select("What letter?", choices, cycle: true, default: 2) expect(value).to eq("D") expected_output = output_helper("What letter?", choices, "B", init: true, hint: "Use #{up_down} arrow keys, press Enter to select") + output_helper("What letter?", choices, "D") + output_helper("What letter?", choices, "B") + output_helper("What letter?", choices, "D") + "What letter? \e[32mD\e[0m\n\e[?25h" expect(prompt.output.string).to eq(expected_output) end it "cycles choices using left/right arrows" do prompt = TTY::TestPrompt.new choices = ('1'..'10').to_a prompt.on(:keypress) { |e| prompt.trigger(:keyright) if e.value == "l" prompt.trigger(:keyleft) if e.value == "h" } prompt.input << "l" << "l" << "l" << "h" << "\r" prompt.input.rewind answer = prompt.select("What number?", choices, default: 2, per_page: 4, cycle: true) expect(answer).to eq('10') expected_output = [ output_helper('What number?', choices[0..3], "2", init: true, hint: "Use #{up_down} and #{left_right} arrow keys, press Enter to select"), output_helper('What number?', choices[4..7], "6"), output_helper('What number?', choices[8..9], "10"), output_helper('What number?', choices[0..3], "2"), output_helper('What number?', choices[8..9], "10"), "What number? \e[32m10\e[0m\n\e[?25h", ].join expect(prompt.output.string).to eq(expected_output) end it "cycles pages left/right with disabled items" do prompt = TTY::TestPrompt.new prompt.on(:keypress) { |e| prompt.trigger(:keyright) if e.value == "l" prompt.trigger(:keyleft) if e.value == "h" } choices = [ {name: '1', disabled: 'out'}, '2', {name: '3', disabled: 'out'}, '4', '5', {name: '6', disabled: 'out'}, '7', '8', '9', {name: '10', disabled: 'out'} ] prompt.input << "l" << "l" << "l" << "h" << "h" << "h" << "\r" prompt.input.rewind answer = prompt.select("What number?", choices, per_page: 4, cycle: true) expect(answer).to eq('2') expected_output = [ output_helper('What number?', choices[0..3], "2", init: true, hint: "Use #{up_down} and #{left_right} arrow keys, press Enter to select"), output_helper('What number?', choices[4..7], "7"), output_helper('What number?', choices[8..9], "9"), output_helper('What number?', choices[0..3], "2"), output_helper('What number?', choices[8..9], "9"), output_helper('What number?', choices[4..7], "5"), output_helper('What number?', choices[0..3], "2"), "What number? \e[32m2\e[0m\n\e[?25h" ].join('') expect(prompt.output.string).to eq(expected_output) end end it "verifies default index format" do prompt = TTY::TestPrompt.new choices = %w(Large Medium Small) prompt.input << "\r" prompt.input.rewind expect { prompt.select('What size?', choices, default: '') }.to raise_error(TTY::Prompt::ConfigurationError, /in range \(1 - 3\)/) end it "doesn't paginate short selections" do prompt = TTY::TestPrompt.new choices = %w(A B C D) prompt.input << "\r" prompt.input.rewind value = prompt.select("What letter?", choices, per_page: 4, default: 1) expect(value).to eq('A') expect(prompt.output.string).to eq([ "\e[?25lWhat letter? \e[90m(Use #{up_down} arrow keys, press Enter to select)\e[0m\n", "\e[32m#{symbols[:marker]} A\e[0m\n", " B\n", " C\n", " D", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G", "What letter? \e[32mA\e[0m\n\e[?25h", ].join) end it "verifies default index range" do prompt = TTY::TestPrompt.new choices = %w(Large Medium Small) prompt.input << "\r" prompt.input.rewind expect { prompt.select("What size?", choices, default: 10) }.to raise_error(TTY::Prompt::ConfigurationError, /`10` out of range \(1 - 3\)/) end context "with filter" do it "doesn't allow mixing enumeration and filter" do prompt = TTY::TestPrompt.new expect { prompt.select("What size?", [], enum: '.', filter: true) }.to raise_error(TTY::Prompt::ConfigurationError, "Enumeration can't be used with filter") end it "filters and chooses a uniquely matching entry, ignoring case" do prompt = TTY::TestPrompt.new prompt.input << "U" << "g" << "\r" prompt.input.rewind answer = prompt.select("What size?", %w(Small Medium Large Huge), filter: true) expect(answer).to eql("Huge") actual_prompt_output = prompt.output.string expected_prompt_output = output_helper("What size?", %w(Small Medium Large Huge), "Small", init: true, hint: "Use #{up_down} arrow keys, press Enter to select, and letter keys to filter") + output_helper("What size?", %w(Medium Huge), "Medium", hint: 'Filter: "U"') + output_helper("What size?", %w(Huge), "Huge", hint: 'Filter: "Ug"') + exit_message("What size?", "Huge") expect(actual_prompt_output).to eql(expected_prompt_output) end it "filters and chooses the first of multiple matching entries" do prompt = TTY::TestPrompt.new prompt.input << "g" << "\r" prompt.input.rewind answer = prompt.select("What size?", %w(Small Medium Large Huge), filter: true) expect(answer).to eql("Large") actual_prompt_output = prompt.output.string expected_prompt_output = output_helper("What size?", %w(Small Medium Large Huge), "Small", init: true, hint: "Use #{up_down} arrow keys, press Enter to select, and letter keys to filter") + output_helper("What size?", %w(Large Huge), "Large", hint: 'Filter: "g"') + exit_message("What size?", "Large") expect(actual_prompt_output).to eql(expected_prompt_output) end it "filters based on alphanumeric and punctuation characters" do prompt = TTY::TestPrompt.new prompt.input << "p" << "*" << "2" << "\r" prompt.input.rewind answer = prompt.select("What email?", %w(p*1@mail.com p*2@mail.com p*3@mail.com), filter: true) expect(answer).to eql("p*2@mail.com") actual_prompt_output = prompt.output.string expected_prompt_output = output_helper("What email?", %w(p*1@mail.com p*2@mail.com p*3@mail.com), "p*1@mail.com", init: true, hint: "Use #{up_down} arrow keys, press Enter to select, and letter keys to filter") + output_helper("What email?", %w(p*1@mail.com p*2@mail.com p*3@mail.com), "p*1@mail.com", hint: 'Filter: "p"') + output_helper("What email?", %w(p*1@mail.com p*2@mail.com p*3@mail.com), "p*1@mail.com", hint: 'Filter: "p*"') + output_helper("What email?", %w(p*2@mail.com), "p*2@mail.com", hint: 'Filter: "p*2"') + exit_message("What email?", "p*2@mail.com") expect(actual_prompt_output).to eql(expected_prompt_output) end # This test can't be done in an exact way, at least, with the current framework it "doesn't exit when there are no matching entries" do prompt = TTY::TestPrompt.new prompt.on(:keypress) { |e| prompt.trigger(:keybackspace) if e.value == "a" } prompt.input << "z" << "\r" # shows no entry, blocking exit prompt.input << "a" << "\r" # triggers Backspace before `a` (see above) prompt.input.rewind answer = prompt.select("What size?", %w(Tiny Medium Large Huge), filter: true) expect(answer).to eql("Large") actual_prompt_output = prompt.output.string expected_prompt_output = output_helper("What size?", %w(Tiny Medium Large Huge), "Tiny", init: true, hint: "Use #{up_down} arrow keys, press Enter to select, and letter keys to filter") + output_helper("What size?", %w(), "", hint: 'Filter: "z"') + output_helper("What size?", %w(), "", hint: 'Filter: "z"') + output_helper("What size?", %w(Large), "Large", hint: 'Filter: "a"') + exit_message("What size?", "Large") expect(actual_prompt_output).to eql(expected_prompt_output) end it "cancels a selection" do prompt = TTY::TestPrompt.new prompt.on(:keypress) { |e| prompt.trigger(:keydelete) if e.value == "S" } prompt.input << "Hu" prompt.input << "S" # triggers Canc before `S` (see above) prompt.input << "\r" prompt.input.rewind answer = prompt.select("What size?", %w(Small Medium Large Huge), filter: true) expect(answer).to eql("Small") expected_prompt_output = output_helper("What size?", %w(Small Medium Large Huge), "Small", init: true, hint: "Use #{up_down} arrow keys, press Enter to select, and letter keys to filter") + output_helper("What size?", %w(Huge), "Huge", hint: 'Filter: "H"') + output_helper("What size?", %w(Huge), "Huge", hint: 'Filter: "Hu"') + output_helper("What size?", %w(Small), "Small", hint: 'Filter: "S"') + exit_message("What size?", "Small") expect(prompt.output.string).to eql(expected_prompt_output) end end context 'with :disabled choice' do it "omits disabled choice when navigating menu" do choices = [ 'Small', 'Medium', {name: 'Large', disabled: '(out of stock)'}, 'Huge' ] prompt = TTY::TestPrompt.new prompt.input << "j" << "j" << "\r" prompt.input.rewind prompt.on(:keypress) { |e| prompt.trigger(:keydown) if e.value == "j" } answer = prompt.select("What size?", choices) expect(answer).to eq('Huge') expected_output = output_helper("What size?", choices, "Small", init: true, hint: "Use #{up_down} arrow keys, press Enter to select") + output_helper("What size?", choices, "Medium") + output_helper("What size?", choices, "Huge") + "What size? \e[32mHuge\e[0m\n\e[?25h" expect(prompt.output.string).to eq(expected_output) end it "doesn't show disabled choice when filtering choices" do choices = [ 'A', 'B', {name: 'C', disabled: '(unavailable)'}, 'D' ] prompt = TTY::TestPrompt.new prompt.on(:keypress) { |e| prompt.trigger(:keybackspace) if e.value == "a" } prompt.input << "c" << "\r" # nothing matches prompt.input << "a" << "\r" # backtracks & chooses default option prompt.input.rewind answer = prompt.select("What letter?", choices, filter: true) expect(answer).to eq('A') expected_output = output_helper("What letter?", choices, "A", init: true, hint: "Use #{up_down} arrow keys, press Enter to select, and letter keys to filter") + output_helper("What letter?", [], "", hint: 'Filter: "c"') + output_helper("What letter?", [], "", hint: 'Filter: "c"') + output_helper("What letter?", ['A'], "A", hint: 'Filter: "a"') + exit_message("What letter?", "A") expect(prompt.output.string).to eq(expected_output) end it "omits disabled choice when number key is pressed" do choices = [ 'Small', {name: 'Medium', disabled: '(out of stock)'}, 'Large' ] prompt = TTY::TestPrompt.new prompt.input << "2" << "\r" << "\r" prompt.input.rewind answer = prompt.select('What size?') do |menu| menu.enum ')' menu.choice 'Small', 1 menu.choice 'Medium', 2, disabled: '(out of stock)' menu.choice 'Large', 3 end expect(answer).to eq(1) expected_output = output_helper("What size?", choices, "Small", init: true, enum: ') ', hint: "Use #{up_down} arrow or number (1-3) keys, press Enter to select") + output_helper("What size?", choices, "Small", enum: ') ') + "What size? \e[32mSmall\e[0m\n\e[?25h" expect(prompt.output.string).to eq(expected_output) end it "sets active to be first non-disabled choice" do choices = [ {name: 'Small', disabled: '(out of stock)'}, 'Medium', 'Large', 'Huge' ] prompt = TTY::TestPrompt.new prompt.input << "\r" prompt.input.rewind answer = prompt.select("What size?", choices) expect(answer).to eq('Medium') end it "prevents setting default to disabled choice" do choices = [ {name: 'Small', disabled: '(out of stock)'}, 'Medium', 'Large', 'Huge' ] prompt = TTY::TestPrompt.new prompt.input << "\r" prompt.input.rewind expect { prompt.select("What size?", choices, default: 1) }.to raise_error(TTY::Prompt::ConfigurationError, /default index `1` matches disabled choice item/) end end end tty-prompt-0.21.0/spec/unit/slider_spec.rb000066400000000000000000000101671363125043200204670ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, '#slider' do subject(:prompt) { TTY::TestPrompt.new } let(:symbols) { TTY::Prompt::Symbols.symbols } it "specifies ranges & step" do prompt.input << "\r" prompt.input.rewind expect(prompt.slider('What size?', min: 32, max: 54, step: 2)).to eq(44) expect(prompt.output.string).to eq([ "\e[?25lWhat size? ", symbols[:line] * 6, "\e[32m#{symbols[:bullet]}\e[0m", "#{symbols[:line] * 5} 44", "\n\e[90m(Use arrow keys, press Enter to select)\e[0m", "\e[2K\e[1G\e[1A\e[2K\e[1G", "What size? \e[32m44\e[0m\n\e[?25h" ].join) end it "specifies default value" do prompt.input << "\r" prompt.input.rewind expect(prompt.slider('What size?', min: 32, max: 54, step: 2, default: 38)).to eq(38) expect(prompt.output.string).to eq([ "\e[?25lWhat size? ", symbols[:line] * 3, "\e[32m#{symbols[:bullet]}\e[0m", "#{symbols[:line] * 8} 38", "\n\e[90m(Use arrow keys, press Enter to select)\e[0m", "\e[2K\e[1G\e[1A\e[2K\e[1G", "What size? \e[32m38\e[0m\n\e[?25h" ].join) end it "specifies range through DSL" do prompt.input << "\r" prompt.input.rewind value = prompt.slider('What size?') do |range| range.default 6 range.min 0 range.max 20 range.step 2 range.format "|:slider| %d%%" end expect(value).to eq(6) expect(prompt.output.string).to eq([ "\e[?25lWhat size? ", symbols[:pipe] + symbols[:line] * 3, "\e[32m#{symbols[:bullet]}\e[0m", "#{symbols[:line] * 7 + symbols[:pipe]} 6%", "\n\e[90m(Use arrow keys, press Enter to select)\e[0m", "\e[2K\e[1G\e[1A\e[2K\e[1G", "What size? \e[32m6\e[0m\n\e[?25h" ].join) end it "changes display colors" do prompt.input << "\r" prompt.input.rewind options = {active_color: :red, help_color: :cyan} expect(prompt.slider('What size?', options)).to eq(5) expect(prompt.output.string).to eq([ "\e[?25lWhat size? ", symbols[:line] * 5, "\e[31m#{symbols[:bullet]}\e[0m", "#{symbols[:line] * 5} 5", "\n\e[36m(Use arrow keys, press Enter to select)\e[0m", "\e[2K\e[1G\e[1A\e[2K\e[1G", "What size? \e[31m5\e[0m\n\e[?25h" ].join) end it "doesn't allow values outside of range" do prompt.input << "l\r" prompt.input.rewind prompt.on(:keypress) do |event| if event.value = 'l' prompt.trigger(:keyright) end end res = prompt.slider('What size?', min: 0, max: 10, step: 1, default: 10) expect(res).to eq(10) expect(prompt.output.string).to eq([ "\e[?25lWhat size? ", symbols[:line] * 10, "\e[32m#{symbols[:bullet]}\e[0m 10", "\n\e[90m(Use arrow keys, press Enter to select)\e[0m", "\e[2K\e[1G\e[1A\e[2K\e[1G", "What size? ", symbols[:line] * 10, "\e[32m#{symbols[:bullet]}\e[0m 10", "\e[2K\e[1G", "What size? \e[32m10\e[0m\n\e[?25h" ].join) end it "changes all display symbols" do prompt = TTY::TestPrompt.new(symbols: { bullet: 'x', line: '_' }) prompt.input << "\r" prompt.input.rewind expect(prompt.slider('What size?', min: 32, max: 54, step: 2)).to eq(44) expect(prompt.output.string).to eq([ "\e[?25lWhat size? ", '_' * 6, "\e[32mx\e[0m", "#{'_' * 5} 44", "\n\e[90m(Use arrow keys, press Enter to select)\e[0m", "\e[2K\e[1G\e[1A\e[2K\e[1G", "What size? \e[32m44\e[0m\n\e[?25h" ].join) end it "changes all display symbols per instance" do prompt = TTY::TestPrompt.new prompt.input << "\r" prompt.input.rewind answer = prompt.slider('What size?', min: 32, max: 54, step: 2) do |range| range.symbols bullet: 'x', line: '_' end expect(answer).to eq(44) expect(prompt.output.string).to eq([ "\e[?25lWhat size? ", '_' * 6, "\e[32mx\e[0m", "#{'_' * 5} 44", "\n\e[90m(Use arrow keys, press Enter to select)\e[0m", "\e[2K\e[1G\e[1A\e[2K\e[1G", "What size? \e[32m44\e[0m\n\e[?25h" ].join) end end tty-prompt-0.21.0/spec/unit/statement/000077500000000000000000000000001363125043200176455ustar00rootroot00000000000000tty-prompt-0.21.0/spec/unit/statement/initialize_spec.rb000066400000000000000000000006531363125043200233510ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Statement, '#new' do it "forces newline after the prompt message" do prompt = TTY::TestPrompt.new statement = described_class.new(prompt) expect(statement.newline).to eq(true) end it "displays prompt message in color" do prompt = TTY::TestPrompt.new statement = described_class.new(prompt) expect(statement.color).to eq(false) end end tty-prompt-0.21.0/spec/unit/subscribe_spec.rb000066400000000000000000000011321363125043200211560ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, '#subscribe' do it "subscribes to key events only for the current prompt" do prompt = TTY::TestPrompt.new uuid = '14c3b412-e0c5-4ff5-9cd8-25ec3f18c702' prompt.input << "3\n#{uuid}\n" prompt.input.rewind keys = [] prompt.on(:keypress) do |event| keys << :enter if event.key.name == :enter end letter = prompt.enum_select('Select something', ('A'..'Z').to_a) id = prompt.ask('Request ID?') expect(letter).to eq('C') expect(id).to eq(uuid) expect(keys).to eq([:enter, :enter]) end end tty-prompt-0.21.0/spec/unit/suggest_spec.rb000066400000000000000000000016441363125043200206660ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, '#suggest' do let(:possible) { %w(status stage stash commit branch blame) } subject(:prompt) { TTY::TestPrompt.new } it 'suggests few matches' do prompt.suggest('sta', possible) expect(prompt.output.string). to eql("Did you mean one of these?\n stage\n stash\n") end it 'suggests a single match for one character' do prompt.suggest('b', possible) expect(prompt.output.string).to eql("Did you mean this?\n blame\n") end it 'suggests a single match for two characters' do prompt.suggest('co', possible) expect(prompt.output.string).to eql("Did you mean this?\n commit\n") end it 'suggests with different text and indentation' do prompt.suggest('b', possible, indent: 4, single_text: 'Perhaps you meant?') expect(prompt.output.string).to eql("Perhaps you meant?\n blame\n") end end tty-prompt-0.21.0/spec/unit/timer_spec.rb000066400000000000000000000011431363125043200203170ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Timer do it "times out loop execution" do timer = TTY::Prompt::Timer.new(0.03, 0.01) yielded = [] timer.while_remaining do |remaining| expect(remaining).to be_within(0.1).of(timer.duration - yielded.size * 0.01) yielded << remaining sleep(0.01) end end it "registers a tick event" do timer = TTY::Prompt::Timer.new(0.03, 0.01) yielded = [] timer.on_tick do |time| yielded << time end timer.while_remaining do # busy work end expect(yielded.size).to be >= 2 end end tty-prompt-0.21.0/spec/unit/warn_spec.rb000066400000000000000000000012201363125043200201420ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, '#warn' do subject(:prompt) { TTY::TestPrompt.new } it 'displays one message' do prompt.warn "Careful young apprentice!" expect(prompt.output.string).to eql "\e[33mCareful young apprentice!\e[0m\n" end it 'displays many messages' do prompt.warn "Careful there!", "It's dangerous!" expect(prompt.output.string).to eql "\e[33mCareful there!\e[0m\n\e[33mIt's dangerous!\e[0m\n" end it 'displays message with option' do prompt.warn "Careful young apprentice!", newline: false expect(prompt.output.string).to eql "\e[33mCareful young apprentice!\e[0m" end end tty-prompt-0.21.0/spec/unit/yes_no_spec.rb000066400000000000000000000246661363125043200205120ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, 'confirmation' do subject(:prompt) { TTY::TestPrompt.new } context '#yes?' do it 'agrees with question' do prompt.input << 'yes' prompt.input.rewind expect(prompt.yes?("Are you a human?")).to eq(true) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Y/n)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m y", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m ye", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m yes", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mYes\e[0m\n" ].join) end it 'disagrees with question' do prompt.input << 'no' prompt.input.rewind expect(prompt.yes?("Are you a human?")).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Y/n)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m n", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m no", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mno\e[0m\n" ].join) end it 'warns about invalid entry when using defaults' do prompt.input << "test" prompt.input.rewind prompt.yes?("Are you a human?") expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Y/n)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m t", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m te", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m tes", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m test", "\e[31m>>\e[0m Invalid input.\e[1A", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m ", "\e[2K\e[1G\e[1A\e[2K\e[1G", "Are you a human? \e[32mYes\e[0m\n" ].join) end it 'assumes default true' do prompt.input << "\r" prompt.input.rewind expect(prompt.yes?("Are you a human?")).to eq(true) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Y/n)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m \n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mYes\e[0m\n" ].join) end it 'changes default' do prompt.input << "\n" prompt.input.rewind expect(prompt.yes?("Are you a human?", default: false)).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(y/N)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m \n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mNo\e[0m\n" ].join) end it "defaults suffix and converter" do prompt.input << "Nope\n" prompt.input.rewind result = prompt.yes?("Are you a human?") do |q| q.positive 'Yup' q.negative 'nope' end expect(result).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Yup/nope)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Yup/nope)\e[0m N", "\e[2K\e[1GAre you a human? \e[90m(Yup/nope)\e[0m No", "\e[2K\e[1GAre you a human? \e[90m(Yup/nope)\e[0m Nop", "\e[2K\e[1GAre you a human? \e[90m(Yup/nope)\e[0m Nope", "\e[2K\e[1GAre you a human? \e[90m(Yup/nope)\e[0m Nope\n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mnope\e[0m\n" ].join) end it "defaults positive and negative" do prompt.input << "Nope\n" prompt.input.rewind result = prompt.yes?("Are you a human?") do |q| q.suffix 'Yup/nope' end expect(result).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Yup/nope)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Yup/nope)\e[0m N", "\e[2K\e[1GAre you a human? \e[90m(Yup/nope)\e[0m No", "\e[2K\e[1GAre you a human? \e[90m(Yup/nope)\e[0m Nop", "\e[2K\e[1GAre you a human? \e[90m(Yup/nope)\e[0m Nope", "\e[2K\e[1GAre you a human? \e[90m(Yup/nope)\e[0m Nope\n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mnope\e[0m\n" ].join) end it "accepts regex conflicting characters as suffix" do prompt.input << "]\n" prompt.input.rewind result = prompt.yes?("Are you a human? [ as yes and ] as no") do |q| q.suffix "[/]" end expect(result).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? [ as yes and ] as no \e[90m([/])\e[0m ", "\e[2K\e[1GAre you a human? [ as yes and ] as no \e[90m([/])\e[0m ]", "\e[2K\e[1GAre you a human? [ as yes and ] as no \e[90m([/])\e[0m ]\n", "\e[1A\e[2K\e[1G", "Are you a human? [ as yes and ] as no \e[32m]\e[0m\n" ].join) end it "customizes question through options" do prompt.input << "\r" prompt.input.rewind result = prompt.yes?("Are you a human?", suffix: 'Agree/Disagree', positive: 'Agree', negative: 'Disagree') expect(result).to eq(true) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Agree/Disagree)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m \n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mAgree\e[0m\n" ].join) end it "customizes question through DSL" do prompt.input << "disagree\r" prompt.input.rewind conversion = proc { |input| !input.match(/^agree$/i).nil? } result = prompt.yes?("Are you a human?") do |q| q.suffix 'Agree/Disagree' q.positive 'Agree' q.negative 'Disagree' q.convert conversion end expect(result).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Agree/Disagree)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m d", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m di", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m dis", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m disa", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m disag", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m disagr", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m disagre", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m disagree", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m disagree\n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mDisagree\e[0m\n" ].join) end end context '#no?' do it 'agrees with question' do prompt.input << 'no' prompt.input.rewind expect(prompt.no?("Are you a human?")).to eq(true) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(y/N)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m n", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m no", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mNo\e[0m\n" ].join) end it 'disagrees with question' do prompt.input << 'yes' prompt.input.rewind expect(prompt.no?("Are you a human?")).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(y/N)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m y", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m ye", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m yes", "\e[1A\e[2K\e[1G", "Are you a human? \e[32myes\e[0m\n" ].join) end it 'warns about invalid entry when using defaults' do prompt.input << "test" prompt.input.rewind prompt.no?("Are you a human?") expect(prompt.output.string).to eq([ "Are you a human? \e[90m(y/N)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m t", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m te", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m tes", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m test", "\e[31m>>\e[0m Invalid input.\e[1A", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m ", "\e[2K\e[1G\e[1A\e[2K\e[1G", "Are you a human? \e[32mNo\e[0m\n" ].join) end it 'assumes default false' do prompt.input << "\r" prompt.input.rewind expect(prompt.no?("Are you a human?")).to eq(true) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(y/N)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m \n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mNo\e[0m\n" ].join) end it 'changes default' do prompt.input << "\r" prompt.input.rewind expect(prompt.no?("Are you a human?", default: true)).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Y/n)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m \n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mYes\e[0m\n" ].join) end it "defaults suffix and converter" do prompt.input << "Yup\n" prompt.input.rewind result = prompt.no?("Are you a human?") do |q| q.positive 'yup' q.negative 'Nope' end expect(result).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(yup/Nope)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(yup/Nope)\e[0m Y", "\e[2K\e[1GAre you a human? \e[90m(yup/Nope)\e[0m Yu", "\e[2K\e[1GAre you a human? \e[90m(yup/Nope)\e[0m Yup", "\e[2K\e[1GAre you a human? \e[90m(yup/Nope)\e[0m Yup\n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32myup\e[0m\n" ].join) end it "customizes question through DSL" do prompt.input << "agree\r" prompt.input.rewind conversion = proc { |input| !input.match(/^agree$/i).nil? } result = prompt.no?("Are you a human?") do |q| q.suffix 'Agree/Disagree' q.positive 'Agree' q.negative 'Disagree' q.convert conversion end expect(result).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Agree/Disagree)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m a", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m ag", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m agr", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m agre", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m agree", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m agree\n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mAgree\e[0m\n" ].join) end end end tty-prompt-0.21.0/tasks/000077500000000000000000000000001363125043200150555ustar00rootroot00000000000000tty-prompt-0.21.0/tasks/console.rake000066400000000000000000000003331363125043200173620ustar00rootroot00000000000000# encoding: utf-8 desc 'Load gem inside irb console' task :console do require 'irb' require 'irb/completion' require File.join(__FILE__, '../../lib/tty-prompt') ARGV.clear IRB.start end task c: %w[ console ] tty-prompt-0.21.0/tasks/coverage.rake000066400000000000000000000003221363125043200175110ustar00rootroot00000000000000# encoding: utf-8 desc 'Measure code coverage' task :coverage do begin original, ENV['COVERAGE'] = ENV['COVERAGE'], 'true' Rake::Task['spec'].invoke ensure ENV['COVERAGE'] = original end end tty-prompt-0.21.0/tasks/spec.rake000066400000000000000000000012551363125043200166560ustar00rootroot00000000000000# encoding: utf-8 begin require 'rspec/core/rake_task' desc 'Run all specs' RSpec::Core::RakeTask.new(:spec) do |task| task.pattern = 'spec/{unit,integration}{,/*/**}/*_spec.rb' end namespace :spec do desc 'Run unit specs' RSpec::Core::RakeTask.new(:unit) do |task| task.pattern = 'spec/unit{,/*/**}/*_spec.rb' end desc 'Run integration specs' RSpec::Core::RakeTask.new(:integration) do |task| task.pattern = 'spec/integration{,/*/**}/*_spec.rb' end end rescue LoadError %w[spec spec:unit spec:integration].each do |name| task name do $stderr.puts "In order to run #{name}, do `gem install rspec`" end end end tty-prompt-0.21.0/tty-prompt.gemspec000066400000000000000000000027041363125043200174370ustar00rootroot00000000000000require_relative "lib/tty/prompt/version" Gem::Specification.new do |spec| spec.name = "tty-prompt" spec.version = TTY::Prompt::VERSION spec.authors = ["Piotr Murach"] spec.email = ["piotr@piotrmurach.com"] spec.summary = %q{A beautiful and powerful interactive command line prompt.} spec.description = %q{A beautiful and powerful interactive command line prompt with a robust API for getting and validating complex inputs.} spec.homepage = "https://ttytoolkit.org" spec.license = "MIT" if spec.respond_to?(:metadata=) spec.metadata = { "allowed_push_host" => "https://rubygems.org", "bug_tracker_uri" => "https://github.com/piotrmurach/tty-prompt/issues", "changelog_uri" => "https://github.com/piotrmurach/tty-prompt/blob/master/CHANGELOG.md", "documentation_uri" => "https://www.rubydoc.info/gems/tty-prompt", "homepage_uri" => spec.homepage, "source_code_uri" => "https://github.com/piotrmurach/tty-prompt" } end spec.files = Dir["lib/**/*"] spec.extra_rdoc_files = Dir["README.md", "CHANGELOG.md", "LICENSE.txt"] spec.require_paths = ["lib"] spec.required_ruby_version = ">= 2.0.0" spec.add_dependency "necromancer", "~> 0.5.0" spec.add_dependency "pastel", "~> 0.7.0" spec.add_dependency "tty-reader", "~> 0.7.0" spec.add_development_dependency "rake" spec.add_development_dependency "rspec", "~> 3.0" end